智能合约介绍

翻译原文
date:20170612

一个简单的智能合约

让我们中最简单的例子开始。现在对所有这一切不了解都没有关系。我们会在后续的文章中介绍他们的点点滴滴。

存储(storage)
pragma solidity ^0.4.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) {
        storedData = x;
    }

    function get() constant returns (uint) {
        return storedData;
    }
}

第一行说明了这份代码的运行环境是Solidity的0.4.0版本或者兼容这个版本的后续版本(可以到0.5.0,但是不包括0.5.0)。这保证了在不同版本的编译器里,代码的执行效果都是一样的。prama关键词用来指示编译器如何编译代码(例如c和c++里的#pragma once)。
用Solidity编写的合约是一个代码和数据的集合。合约保存在以太坊区块链中一个特定的地址中。uint storedData;这行代码声明了一个名为storedData的、类型为uint(256位的无符号整形数据)的变量。你可以想像成在数据库中的一个小小的数据片,我们可以通过调用函数来查询和改变这个值。在以太坊中,这些函数总是在各自合约中的函数(?In the case of Ethereum,this is always the owning contract.)。在这个例子中,setget函数可以用来改变和取出变量storedData的值。
使用变量,我们不需要加this.前缀,这一点和其他的语言相同。
这个合约并没有做很多事情(很多基础的事情,以太坊平台已经帮你完成了),除了实现这样的功能:允许任何人存储一个数据和获取数据。任何人都可以调用set函数来覆盖你写入的数据,但是你之前的数据已经写入到区块链中了。稍后,我们给合约将增加限制访问的功能。这样一来,那就只有你才能改变这个数字了。

子货币例子(subcurrency example)

以下的合约将实现一个简单的加密货币。凭空产生货币是可能的(?It is possible to generate coins out of thin air),但是只有生成合约的人才能这样做。这是一种狭隘的实现发行计划。(?it is trivial to implement a different issuance scheme)
而且,所有人都可以发送货币给任何人,而不需要用户名密码注册。你所需要的只是是以太坊的密钥对。

pragma solidity ^0.4.0;

contract Coin {
    // The keyword "public" makes those variables
    // readable from outside.
    address public minter;
    mapping (address => uint) public balances;

    // Events allow light clients to react on
    // changes efficiently.
    event Sent(address from, address to, uint amount);

    // This is the constructor whose code is
    // run only when the contract is created.
    function Coin() {
        minter = msg.sender;
    }

    function mint(address receiver, uint amount) {
        if (msg.sender != minter) return;
        balances[receiver] += amount;
    }

    function send(address receiver, uint amount) {
        if (balances[msg.sender] < amount) return;
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        Sent(msg.sender, receiver, amount);
    }
}

这个合约中将会引入新的概念。我们来一一介绍。
address public minter;这一行声明了一个公有的address类型的变量。address类型是一个160位的值,它不允许进行任何的算数运算。它合适用于存储合同的地址或者其他人的键值对。关键词public自动的生成一个函数用于获取这个变量的值。如果没有这个关键词,那其他合同将无法获取到这个变量。自动生成的函数如下所示:

function minter() returns (address) { return minter; }

如果直接添加这样的函数是行不通的,因为函数和变量的名称相同。但是别担心,编译器会自动完成这个操作的,把两者区分开来。
下一行,mapping (address => uint) public balances;同样是生成一个公有变量,但是有着更加复杂的数据类型。这个类型将地址映射成为一个无符号整形。该映射可以看作是一个哈希表,这个表里罗列了所有可能的键,并且对应于一个字节表示为全零的值。(?Mappings can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to a value whose byte-representation is all zeros.)但是这个逻辑是行不通的,因为不可能列举完所有的键,所有的值。所以要么留意下你映射的类型(最好使用更加高级的类型),要么在不需要的地方使用它,就像这个例子一样。(?So either keep in mind (or better, keep a list or use a more advanced data type) what you added to the mapping or use it in a context where this is not needed, like this one. )(ps:这段话有些难以理解。。。)这个例子由于public关键字而生成的getter函数会比较复杂些,如下所示:

function balances(address _account) returns (uint) {
    return balances[_account];
}

如你所见,你可以使用这个函数很方便的查询到余额。
下一行,event Sent(address from, address to, uint amount);声明了所谓的“事件”,这个事件通过最后一行的send函数触发。接口(服务器程序)能够很容易的监听到这些事件的触发。当事件触发的时候,监听函数将会接收到fromtoamount参数。为了能够监听这个事件,代码应该这么写:

Coin.Sent().watch({}, '', function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
})

这里注意下,接口是如何调用自动生成的函数balance
下面说下比较特殊的函数,Coin函数是合约的构造函数,它在合约创建的时候执行,并且不会延迟执行。它会固定的存储创建人的addressmsg(还有txblock)是一个全局变量,包含了一些允许进入区块链的属性信息。msg.sender的值恒为函数调用者的address
最后,这些函数实现了合约,并且可以被用户使用来履行mintsend约定。如果mint被非创建者调用,那么不会进行任何操作。另一方面,send可以被其他任何有货币的用户调用,来进行转账操作。需要注意的是,如果你通过这个合约给一个address转账,在区块链浏览器中你在这个address中将看不到任何有关这个操作的信息。因为这些转账信息,余额信息都是存在这个特定的合约当中。通过使用事件,你可以轻易的实现该货币的区块链浏览器,来记录该货币的转账和余额信息。

区块链基础

区块链对于程序员来说并不是难于理解的一个概念。因为很多难题(挖矿,哈希,椭圆曲线加密,p2p网络等)都是为了提供一系列的功能和承诺。一旦你接受了这些功能,你就不会担心难于理解技术问题了。难道你必须理解阿里云怎么提供服务之后才能使用它吗?

交易(transactions)

一条区块链是一个共享的交易数据库。这意味着所有加入网络的人都可以访问数据。如果你想要对这个数据库做些改变,那么你必须要进行能够被别人认可的交易。交易这个词说明了要么你完成改变数据(假设你要同时修改两个值),要么什么也没有发生。另外,当你的交易被数据库接受之后,其他的交易不会改变它。
举个例子,想象一个表格,它列举了一个电子货币里所有账户的余额。如果发起一个转账操作,将一笔钱从一个账户打到另一个账户,这个交易性质的数据库保证了,一个账户余额减少,另一个账户一定多出相应的金额。如果一个账户无法收钱,那么原账户并不会少钱。
另外,一个交易总是被发起者(创建者)加密。这直观的保护了要修改的数据。在电子货币这个例子中,简单的验证保证了只有拥有key的人才能从中转账。

区块

在比特币项目中,需要克服的一个比较大的问题是所谓的“双花攻击”。双花攻击就是网络中有两笔交易都想清空一个账户,而引发冲突。
对这个问题比较抽象的解答就是,你无需关心。会自动为你确认交易顺序。交易写入区块,然后执行、分发给网络中所有参与的节点。如果两笔交易相互矛盾,那么后一笔交易会驳回,不会写入区块。
这些区块根据时间,串在一起,形成”区块链“。这就是区块链的由来。区块按照特定的时间加入到区块链中。在以太坊平台中,这个时间是17秒左右。
由于顺序选择机制(所谓的挖矿),顶端的区块可能经常的撤回。越多的区块写入区块链中,你的交易越不容易撤回。

以太坊虚拟机(EVM)

概览

以太坊虚拟机(EVM)是以太坊平台智能合约的运行环境。它并不是一个沙盒,但是是完全独立的,这意味着EVM不能访问网络,文件系统或者其他进程。有些智能合约也被约束为不能访问其他的智能合约。

账号

以太坊平台有两种账号,但是公用一个地址空间:对外账号——被键值对控制着——和合约账号——与账号一同保存的代码。
对外账号的address由公钥决定,而合约账号的地址由合约创建的时候确认(它根据创建者的地址、交易的数目,组成所谓的“nonce”——“Number used once“——临时数据)。
无论账号中是否有代码,EVM都是相同对待的。
每个账号都有一个固定的键值对,对应于256比特字到256比特字的存储。(?Every account has a persistent key-value store mapping 256-bit words to 256-bit words called storage.)
另外,每个账号都有以太币余额,它可以通过进行比特币交易来改变。

交易

交易是一种从一个账号发送到另一个账号(可能是同个账号,或者0账号)的消息。它包含二进制数据(负载)和以太币。
如果目标账号包含代码,那么执行代码,二进制负载数据作为代码的输入。
如果目标账号是0账号(address为0),交易会生成一个新的合约。之前提到过的,合约账号是由发送者的address和转账金额生成的,绝对是非0账号。这类创建合约交易的负载成为EVM的字节码并且执行。执行的输出被永久保存在合约代码中。这意味着,要创建一个合同,你并不发送合同的实际代码,而是返回合同代码的代码。(ps:有点绕,可以看原文)

燃料(gas)

在创建的时候,每个交易都会收取特定的gas,这个目的在于,限制交易执行的工作量并且对这个执行交易收费。当EVM执行交易,gas按照特定的规则缓慢的消耗掉了。
gas的价格在创建交易的时候就设定了,必须在交易之前支付gas价格 * gas数目的费用。如果执行完毕之后还有gas剩余,会原路返回到账户中。
如果gas在任何时候消耗完毕(例如成了负数),就会触发gas不足的异常,将会撤销本次交易的所有修改。

storage,内存和堆栈

每个账号都有一个永久的存储区域,称为storage。storage有一个键值存储,对应于256比特字到256比特字的映射。例举一个合同里的storage是不可能的。读取或者其他修改storage的操作都是代价昂贵的。合约只能修改自己的storage,不能修改其他的。
内存区域被称为内存(memory),每次执行的时候会获取到一个清除干净的内存。内存是线性的,可以实现byte寻址。但是读取必须是256位的,写入可以是8位或者256位的。当接触到(读取或者写入)之前为触及的内存字的时候,内存以256位的字长扩展。当扩展的时候,必须支付gas。所以内存消耗越大,花费更大。
EVM不是一种存储器机,而是一种堆栈机。所以所有的计算都在堆栈上执行。最大的是1024个元素并包含256位的字。访问stack的深度有如下的限制规则:可以将顶端下的16个栈中拷贝出一个元素放置在顶端,或者可以将顶端的元素与下面的16个元素中的任意一个互换。所有其他的操作将距离顶端的两个(或者一个,或者多个,全看操作需要)取出计算,将结果放置在栈顶端。当然也可以将堆栈元素放置在storage或者内存里。但是不移除栈顶的时候是无法再继续深入访问其他元素的。

指令集

EVM的指令集一直保持最小化,来避免导致一致性问题的可能性。所有指令操作都基于基本的数据类型,256位的字长。允许算术操作,位操作,逻辑操作以及比较操作。可以条件跳转和非条件跳转。另外,合约可以访问当前区块的相关属性,例如序号和时间戳。

消息调用(message call)

合约可以调用其他合约或者可以通过消息调用发送以太币到非合约账号。消息调用和jiao交易非常相似,有原始地址,目的地址,数据负载,以太币,gas和返回值。实际上,每个交易都是由高级的消息调用组成。所谓高级,就是可以生成其他消息调用。
一个合约可以决定发送多少gas,保留多少。如果一个gas不足异常(或者其他异常)在内部的消息调用中生成的时候,将会在堆栈顶标记一个错误的值。在这个例子中,当且仅当与消息一起发送的gas使用完毕时。在solidity中,调用合约引起一个认为异常,导致调用栈上升。(?理解的稀里糊涂,推荐看原文)
就像之前所说,被调用的合约(可以是调用者自身)能够得到一块崭新的内存,并且可以访问到负载(在单独的区域中提供,称之为calldata区域)。在执行结束之后,它可以返回数据,并存储到调用者的内存中。
调用的最大深度是1024,这意味着更复杂的调用可以采用递归,而不是循环。

代理调用/调用代码 和库(Delegatcall/Callcode and Libraries)

存在一种消息调用的变种,称为代理调用。代理调用就是说目的地址中的代码在当前调用者的上下文中运行,msg.sendermsg.value不会改变,其他方面跟一般的消息调用一致。
这就意味着一个合约可以在运行时动态的从不同地址中调用代码。storage,当前地址和余额都是引用当前的调用合约,仅仅只是代码从调用地址中获取的,其他的都是当前合约的。
这就可以实现类库的功能:可以复用的代码可以放在合约的storage中,来实现复杂的数据结构。

日志

可以将数据保存在特殊的索引数据结构中,该结构与区块一一对应。这个功能称为日志功能,不需要实现事件(event)。在日志创建之后,合约就不能访问这些数据了。但是这些数据可以在区块链之外的地方轻松访问到。由于一些日志数据用bloom filter算法保存,所以可以搞笑的安全的方式访问到数据,而且节点也不需要下载所有的区块链(轻客户端),也能找到这些日志。

创建

合约可以用特殊的代码(不是简单的调用0地址)创建其他的合约。创建调用和其他一般的消息调用不同的一点是,创建调用的负载是可以执行的,并且返回值是代码,调用者(创建者)可以获取到新合约的地址。

自我销毁

代码从区块链中移除的唯一可能就是合约调用了selfdestruct操作。该地址剩余的以太币将会返回到设定的账户中。storage和代码都从状态中移除了。

警告:即便合同中不包含selfdistruct代码,它依旧可以通过delegatecall或者callcode来完成自毁

注意:除去老旧合同的功能也许会,也许不会出现在以太坊客户端中。另外,档案节点可以选择是否永久保留代码和storage。

注意:目前外部账号不能移除

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容