翻译原文
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.)。在这个例子中,set
和get
函数可以用来改变和取出变量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
函数触发。接口(服务器程序)能够很容易的监听到这些事件的触发。当事件触发的时候,监听函数将会接收到from
,to
,amount
参数。为了能够监听这个事件,代码应该这么写:
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
函数是合约的构造函数,它在合约创建的时候执行,并且不会延迟执行。它会固定的存储创建人的address
:msg
(还有tx
和block
)是一个全局变量,包含了一些允许进入区块链的属性信息。msg.sender
的值恒为函数调用者的address
。
最后,这些函数实现了合约,并且可以被用户使用来履行mint
和send
约定。如果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.sender
和msg.value
不会改变,其他方面跟一般的消息调用一致。
这就意味着一个合约可以在运行时动态的从不同地址中调用代码。storage,当前地址和余额都是引用当前的调用合约,仅仅只是代码从调用地址中获取的,其他的都是当前合约的。
这就可以实现类库的功能:可以复用的代码可以放在合约的storage中,来实现复杂的数据结构。
日志
可以将数据保存在特殊的索引数据结构中,该结构与区块一一对应。这个功能称为日志功能,不需要实现事件(event)。在日志创建之后,合约就不能访问这些数据了。但是这些数据可以在区块链之外的地方轻松访问到。由于一些日志数据用bloom filter算法保存,所以可以搞笑的安全的方式访问到数据,而且节点也不需要下载所有的区块链(轻客户端),也能找到这些日志。
创建
合约可以用特殊的代码(不是简单的调用0地址)创建其他的合约。创建调用和其他一般的消息调用不同的一点是,创建调用的负载是可以执行的,并且返回值是代码,调用者(创建者)可以获取到新合约的地址。
自我销毁
代码从区块链中移除的唯一可能就是合约调用了selfdestruct
操作。该地址剩余的以太币将会返回到设定的账户中。storage和代码都从状态中移除了。
警告:即便合同中不包含selfdistruct
代码,它依旧可以通过delegatecall
或者callcode
来完成自毁
注意:除去老旧合同的功能也许会,也许不会出现在以太坊客户端中。另外,档案节点可以选择是否永久保留代码和storage。
注意:目前外部账号不能移除