第一章:智能合约简介
单击此处查看原文
一个简单的例子
我们从一个最基础的例子开始,即使你是零基础也无所谓,你将会阅读到详细的文档。
Storage
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public constant returns (uint) {
return storedData;
}
}
首行只是简单的告诉编译器使用 0.4.0 版本或者更新的版本(不包括 0.5.0 ),不影响功能。这么做可以保证合约代码在新的编译环境中不会突然出现不同的行为而导致 bug。通常关键词 pragma
在这里的作用就是告诉编译器要如何处理源代码。
从某种意义上说,用 Solidity 写的合约就是一个 functions 和 state 的集合,归属于以太坊区块链中的一个指定地址。
第三行uint storedData;
声明了一个 state variable :storedData
,类型是 uint
(unsigned integer of 256 bits)。你可以认为它是数据库中的单个插槽,并可以通过调用代码中的一些方法来查询和修改,以此管理数据库。
在以太坊中,合约被永久持有。在此例中,set
和 get
方法可以用来修改和查询变量的值。
若要访问一个 state variable ,你不需要加前缀 this
,它不同于其他语言的通常做法。
这个智能合约并没有做很多事情(因为基础建设以太坊团队都帮你做好了),它除了允许世界上的任何用户存储单个数字之外,也没有一个可行的方法来阻止你 publishing 这个数字。当然,任何人都可以调用 set
方法,用不同的值来覆盖你的数字,而原来的数字仍然被保存在区块链上。接下来,我们将会看到如何启用访问限制来使得只有你自己可以修改自己的数字。
注意:
所有的标识符(合约名、方法名、变量名)都被限定为只可以使用 ascii 字符来定义。只有 string 类型的变量你才可以给它赋一个 utf-8 的值。
警告:
使用 Unicode 文本要格外小心,因为类似的(甚至是相同的)字符可以有不同的代码点,这意味着它们可以被编码成不同的 byte array
Subcurrency 示例
译者注: currency 是货币的意思,之后出现的 xxxcurrency 你可以把他们拆成 xxx currency 来理解
下面的合约将会实现一个最简单的 cryptocurrency 。它可以凭空产生货币,不过只有创造者才可以做到(it is trivial to implement a different issuance scheme. 这里是一句括号内的说明性文字,翻译出来有点奇怪就不翻译了哈哈)。 此外人们不需要提供用户名、密码来注册账号就可以互相发送货币,只是需要借助一对 Ethereum keypair 。
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() public {
minter = msg.sender;
}
function mint(address receiver, uint amount) public {
if (msg.sender != minter) return;
balances[receiver] += amount;
}
function send(address receiver, uint amount) public {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Sent(msg.sender, receiver, amount);
}
}
这个合约介绍了一些新概念,一行行来看。
address public minter;
这一行声明了一个公有 state variable ,类型为 address
。
address
类型是一个不允许进行算数运算的 160 位的值。这非常适合用于存储合约地址或者外部人员的 keypairs 。
public
关键字自动生成了一个方法,用来允许你访问 state variable 的当前值。如果没有这个关键字,其他的合约无法访问该变量。生成的方法如下:
function minter() returns (address) {
return minter;
}
然而,你去手动添加一个完全一样的方法是没用的,因为我们需要的是相同名称的一个 function 和一个 state variable。
而现在为了产生这两个东西,只需要使用一个 public
关键字,剩下的编译器已经帮你做好了。
下一行,mapping (address => uint) public balances;
, 同样也创建了一个公共 state variable ,不过这是个更为复杂的数据类型。这个类型将地址映射到无符号整型。
映射可以看作是做实际初始化的 hash tables,这样每个可能的 key 都会存在并且都可以映射到一个字节表示都为 0 的值。
这个比喻不会太过分,话虽如此,但你既不能获取映射的所有键,也不能获取所有值。所以要么你就记住(或者更好的,保留一个列表或者更高级的数据类型)添加了什么东西到映射中,要么你就在一个不需要使用到的上下文中使用它,就像这个。在此例中 public
关键字生成的 getter function
稍微有点复杂,它看起来就像下面的代码:
function balances(address _account) public view returns (uint) {
return balances[_account];
}
如你所见,你可以使用这个方法轻松地查询单个账户的余额。
event Sent(address from, address to, uint amount);
这一行声明了一个所谓的 event
,在方法的最后一行被触发。
用户接口(当然还有服务端接口)可以低成本的监听在区块链上被触发的事件。只要事件被触发,监听者也会收到参数 from
、to
、 amount
,这可以轻松地追踪 transactions 。为了监听这个事件,你需要这样使用:
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
,是一个构造器,只在合约被创建的时候运行,之后都不能再调用。它永久保存了用户创建的合约地址:msg
(还有 tx
和 block
) 是一个 magic 全局变量,它包含了一些可以允许访问区块链的属性。msg.sender
是当期方法调用的地址。
最后,方法实际上都会以合约结尾,可以被用户调用,合约都是 mint
和 send
。如果 mint
被非本账户调用去创建合约,什么都不会发生。另一方面, send
可以被任何人(但必须也是持币人)使用来向别的用户发送货币。
注意如果你使用了合约来发送货币到一个地址,在你使用区块链浏览器查找那个地址的时候,你看不到任何相关信息。因为事实上你发送的货币、改变的余额仅仅是被存储在这个独有的货币合约的寄存器中。
通过使用事件,很轻松就可以创建一个“区块链浏览器”,用来追踪你新货币的 transactions 和余额。
区块链基础
对于程序员来说区块链概念并不会难以理解,原因是大部分的难点(mining,hashing,elliptic-curve cryptography,peer-to-peer networks,etc)都已经有了一套特定的 features 和 promises 。
一旦接受这些特性,你就无需再为底层技术而发愁 —— 就像是难道你必须知道亚马逊云的内部实现才能使用它的功能?
Transactions
一条区块链就是一个全球共享的 transactional database 。这意味着每个参与进网络的人都可以查询数据库中的记录。如果你想更改数据库中的记录,
你必须创建一个所谓的 transaction 来让其他所有人都接受。 transaction 这个词意味着你希望做出的改动(假设你希望同一时间更改两个值)要么全部都成功,要么全部都失败。另外,当你的 transaction 保存到了数据库,没有别的 transaction 可以再修改它。
比如说,假设一个表记录了一个电子货币账户的所有余额,如果发起了一个转账请求,数据库的 transactional nature 会保证一个账户减去资金,另一个账户增加相应的资金。如果由于某些原因,目标账户增加余额失败了,那么转账的源账户资金也不会有任何改动。
另外,一笔 transaction 总是会被发送者(创建者)签名加密,直接保护了数据库即将做出的修改的访问权限。
在上述电子账户的例子中,用了一个简单的验证来保证必须持有账户相应的 key 才可以进行转账。
区块
一个需要克服的重要难点,在比特币中被称为“双重支付攻击” (double-spend attach,也叫双花攻击):
如果网络中两笔 transactions 都想清空一个账户会发生什么事情?这也就是所谓的冲突。
一个抽象的回答是:你不必在意。transaction 的顺序是由你来选择的,transaction 被绑定在区块中,之后会被执行,并且分布在所有参与的节点中。如果有两笔 transactions 互斥,后来的一笔 transaction 总是会被拒绝成为区块的一部分。
这些区块在时间上形成一个线性序列,由此也形成了“区块链”这个词。区块以一定的间隔被追加到链条中 —— 以太坊中的间隔大约是17秒。
作为 order selection mechanism(硬翻过来就是:订单选择机制,也称为“采矿”)的一部分,区块可能偶尔会被回滚,不过也只是在链条的最顶端才会出现,越多的区块被追加到顶部,回滚发生的概率也就越小。所以你的 transaction 有可能被回滚甚至从区块链中删除,不过等待时间越久,发生的可能性越小。
The Ethereum Virtual Machine
概览
EVM 是以太坊智能合约的运行时环境。它不仅是沙盒,事实上也是完全隔离的,意思就是 EVM 中的代码运行不会访问网络、文件系统或其他进程,甚至智能合约之间也被限制访问。
Accounts
以太坊有两种 account,共享同一个地址空间:
-
external accounts
,被公私密钥对控制 -
contract accounts
,被账户存放的代码控制
external accounts
的地址由公钥决定,而合约的地址是在合约被创建时生成的(来源于创建者的地址并且它会发送 transaction 的数量,也就是所谓的“nonce”)。
无论账户是否存储代码,两种类型都被 EVM 一视同仁。
每个账户都持久存储了一个键值对,把一个 256-bit 的 word 映射成另一个 256-bit 的 word,称之为 storage
。
另外,每个账户在 Ether(Wei 可能更准确)中都有一个 balance
,可以发送 transaction 来修改。
Transactions
一笔 transaction 是一条从 A 账户到 B 账户的 message
(两个账户可能是同一个,也有可能是特殊的 zero-account
,详情见下方),可以包含二进制数据(就是它的 payload)和 Ether 。
如果目标账户有代码,那么这段代码会被执行,传递过来的 payload 会作为实参输入。
如果目标账户是 zero-account
(也就是账户地址为 0),那么这笔 transaction 会创建一个新的合约。如前所述,合约地址不是 0 ,而是一个来自发送者的地址及其发送的 transaction 数量(nonce)。这个合约创建的 transaction 的 payload 会被转成 EVM bytecode 来执行。执行的结果会被作为代码永久保存在合约中。这也意味着为了创建一个合约,你不用发送合约的真实代码,but in fact code that returns that code 。
Gas
在创建 transaction 的时候,每次都需要支付一定数量的 gas ,目的是限制执行 transaction 所需的工作量,并为此执行付费。当 EVM 执行 transaction 的时候, gas 会以特定的规则逐渐耗尽。
gas price 是 transaction 的创建者设定好的,必须从发送方账户支付 gas_price * gas
的预付款。如果执行之后有 gas 遗留,将会原路退还。
如果 gas 在任意时刻用完了(也就是说变成了负数),就触发一个 out-of-gas
的异常,会回滚对当前调用帧的 state
做出的修改。
Storage, Memory and the Stack
每个账户都有一个持久内存区称为 storage
。storage
是一个 key-value store,将 256-bit 的 word 映射到另一 256-bit 的 word 。
从一个合约中列举 storage
是不可能的,因为读取的成本比较昂贵,修改 storage
更贵,一个合约除了它自己以外不能读取或修改 storage
。
第二个内存区域叫做 memory
,其中一个合约为每个 message
的调用获取一个新的实例(a freshly cleared instance)。memory
是线性的,可以在字节级别处理,但是读取的 width 被限制为 256 bits,而写入可以是 8 bits 或者 256 bits 。当访问(读或者写)一个之前从未接触的的 memory word
(比如一个 word 内的任意偏移量)时,memory
由一个 256-bit 的 word 展开。在展开的时候,必须支付 gas ,memory
越大越贵(尺寸的 2 次幂)。
EVM 并不是一个 register machine,而是一个 stack machine ,所有的计算都是在一个名为 stack
的区域上执行的。它的最大 size 是 1024 个元素。通过以下方式访问 stack
仅限于顶部:将最上面的 16 个元素之一复制到 stack
的顶部,或者用下面的 16 个元素之一来交换顶层元素。所有其他的操作都从 stack
的最上面取两个(或者一个或更多,取决于操作)元素,然后将结果 push 到 stack
上。当然,你可以把 stack
的元素移动到 storage
或者 memory
,但是如果不先把 stack
顶部删除就不能随意访问 stack
深处的元素。
Instruction Set
为了避免不正确的实现导致一致性问题,EVM instruction set 一直都保持最小化。所有的 instruction 操作都在基本数据类型上进行,也就是 256-bit word 。通常的算法,位、逻辑、比较都是有的,有条件和无条件的跳转也都有。另外,合约可以访问当前区块的相关属性,比如序号(number)和时间戳(timestamp)。
调用 Message
合约可以调用另外的合约或者调用 message 来发送 Ether 到 non-contact 账户。调用 message 跟交易类似,它们有 source、target、data payload、Ether、gas、return data 。事实上,每笔交易都由一个 top-level message 调用,转而又可以创建更多的 message 调用。
一个合约可以通过内部 message 调用来决定尚存的 gas 有多少需要被发送和有多少需要被留下。如果在内部调用的时候发生了 out-of-gas 异常(或其他的异常),就会通过推送到 stack
上的错误值来发送信号,此时只有跟调用一起发送的 gas 被用完。在 Solidity 中,这种情况下调用合约默认会触发一个 manual exception ,所以会导致各种异常冒泡般地涌出。
如上所述,被调用的合约(可以跟调用者相同)将会收到一个 freshly cleared instance of memory ,并且有权调用 payload ,payload 将被发送到一个隔离的区域叫做 calldata
,执行完毕时,它将会返回一个数据存储在调用者预分配的 memory
中。
调用的深度被限制在 1024, 意味着在更复杂的调用中,循环优先级大于递归。
Delegatecall / Callcode and Libraries
调用 message 的时候有一个特殊的变量叫做 delegatecall
,它跟调用 message 是一样的,除了目标地址中的代码是在被调用的合约中执行的,并且 msg.sender 和 msg.value 的值不改变。
意味着一个合约可以在运行时从不同的地址动态载入代码。storage
、当前地址和余额仍然指向被调用的合约,仅仅只有代码是来自被调用的地址。
这使得在 Solidity 实现 library 特性成为可能:可重用的 library 代码可以被应用到一个合约的 storage
,比如说:为了实现一个复杂的数据结构。
Logs
可以在一种特殊的索引数据结构中存储数据,它可以映射到区块级别。这种特性称为 logs
,用来实现 events
。logs
创建之后合约无法访问其数据,不过它们可以有限的从外部访问区块链。自从 log data
的一部分被存储到 bloom filter之后,就可以高效并安全加密地搜索这些数据,网络节点(轻型客户端)也无需下载整个区块链就可以访问这些日志。
Create
合约可以用一种特殊的操作码来生成另外的合约(也就是说它们不是简单的调用 zero address
)。调用 message 和调用 create 的唯一区别在于 payload data
会被用于执行并且返回结果会被作为代码存储,调用者/创建者会收到 stack
上新合约的地址。
Self-destruct
唯一可能从区块链上移除代码的做法就是让在相应地址下的合约执行 selfdestruct
操作,该地址遗留的 Ether 会发送到指定的目标然后 storage
和 代码就会从 state
中移除。
警告:
即使一个合约的代码没有selfdestruct
,它也可以通过delegatecall
或者callcode
来执行该操作。
注意:
修改旧合约可能会也可能不会被以太坊客户端实现。另外,归档的节点可以选择保留合约的storage
和code indefinitely
。
注意:
目前external accounts
不能从state
中移除。