一个简单的智能合约
让我们从一个最基础的例子入手。哪怕你现在对于智能合约还没有一点了解,那也没关系,我们将在后面介绍更多有关于它的知识。
存储
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public constant returns (uint) {
return storedData;
}
}
第一行简单地声明源码适用的Solidity版本,在版本号0.4.0及以上(上限是0.5.0,但不包含)时,可以保证代码功能不会出现不同的表现。关键字prama告诉编译器正确地使用对应的方式编译源码。
对于Solidity而言,一个合约是在以太坊区块链上对应地址上的代码和数据的集合,代码表示这个合约的功能,数据表示这个合约的状态。uint storedData;
这行代码,声明了一个叫storedData
的uint
状态变量,它是一个无符号整形变量,长度是256位。你可以把它看成是一个存在数据库中的记录,并可以通过数据库操作来读取和修改。在以太坊中,始终用合约中的函数来操作它的数据,所以,set
和get
可以被用来修改和获取storedData
这个变量的值。
要访问一个状态变量,你不需要使用在其他编程语言中使用的this.
前缀。
以上这个合约可以让你发布一个数字,但是并没有做一些(可行的)工作来限制世界上的其他人来修改它,因为所有人都可以访问这个数字。任何人都可以只通过再次调用set
方法来把一个不同的值覆盖掉你的数字,但是在区块链历史记录中依然保存了原来的数字值。稍后,我们将看到你如何可以加强访问限制,来实现只有你才能修改这个数字。
说明:所有的符号(合约的名字,函数名字和变量名字)是严格地只能使用ASCII字符,在字符串变量中,可以使用UTF-8编码的内容。
警告:小心使用Unicode文本,因为有些看上去很像(或者甚至相同)的符号在编译成字节码时会有不同的结果。
子币示例
下面的合约将会实现一个最简单的加密货币。凭空产生子币是有可能的,但是只有合约的创建者能够可以做到(很容易实现一套不同的发行机制)。再者,任何人可以不需要使用用户名和密码注册就可以相互间发送子币,而仅需以太坊的密钥对。
pragma solidity ^0.4.21;
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;
emit Sent(msg.sender, receiver, amount);
}
}
这个合约引入了一些新的概念,让我们一个一个来介绍。
address public minter;
这行声明了一个address类型的状态变量,它可以被公共访问。address
类型表示一个160位的值,不允许任何算术操作。它适合用来存储合约地址和外部用户的密钥对。public
关键字自动生成一个函数让你在合约外可以直接访问这个状态变量的值。如果没有这个关键字,其它的合约将无法访问到这个状态变量。编译器生成的这个函数和以下的代码几乎一致:
function minter() returns (address) { return minter; }
当然,增加一个像这个的函数将不会奏效,因为我们有了一个函数和一个状态变量拥有相同的名字,很幸运,编译器将会告诉这个问题。
下一行,
mapping (address => uint) public balances;
也创建了一个公开的状态变量,但是它有更复杂的类型,这个类型是地址数据和整形数据的映射。映射关系可以被看作是一种哈希表,其中的每一个存在的键都会映射到一个字节序列全是零的值。这种类比可能不是很恰当,因为既不可能获得映射关系的所有键的列表,也不可能获得所有值的列表。所以,要么记住你添加的映射关系(也许更好),或者在不需要映射关系的上下文中使用它。public
关键字会生成一个getter
函数,在这个示例中它可能有一点复杂:
function balances(address _account) public view 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));
}
})
请注意自动生成的balances
这个函数是如何在用户界面中调用的。
Coin
这个特殊的函数是合约的构造函数,它在合约被创建时调用,之后就不能再次被调用。它永久性地存储了合约创建者的地址:msg
(以及tx
和block
)是很神奇的全局变量,他包含一些属性可以访问到区块链。msg.sender
总是当前(外部)的调用者函数的地址。
最后,在合约部署后,最终能被用户和其他合约访问到的函数是mint
和send
。如果mint
被创建者以外的任何账户访问,什么事都不会发生。另一方面,send
函数可以被任何拥有币的账户来发送币给其他任何账户。请注意,如果你使用这个合约去发送币给一个地址,你不会在区块链浏览器中看到任何信息,因为实际上你发送币和余额变动只会存储在合约关联的数据存储器中。
区块链基础
区块链概念对于程序员来说并不难理解。因为大多难点(挖矿、哈希、团员曲线密码学、点对点网络等)都只是用于提供一些特定功能和保障。你只需先接受它们,对于底层技术并不必担心。难道你一定要先懂亚马逊的AWS的内部原理才去使用它吗?
事务
区块链是一个全球共享的事务性数据库。这意味着,每一个人都可以加入这个网络来读取数据库中的数据,如果你想改变其中的一些数据,你必须要创建一个被其他所有人接受的事务。事务这个词,意味着你要做的操作(假设你想要同时改变两个值)要么全完成,要么全不完成。另外,当你的事务完成后,没有其他事务可以改变它。
举个例子,假设有一张表记录了所有账户的电子货币余额,如果请求从一个账户转账到另一个账户,数据库事务会保证从一个账户中减少的数量,永远会让另一个账户中加上那个对应的数量。如果处于某些原因不能从目标账户中增加数量,那么源账户也不会被改变。
区块
在比特比中,一个主要的要解决的难题是“两次花费攻击”,它发生在网络中两个事务都想要花光同一个账户的余额,所谓的冲突交易?
这个问题的答案就是你不需要关心它。你会得到一个事务顺序,事务会被打包进所谓的“区块”中,然后它们将会被分发到所有参与的节点中执行。如果两个事务相互有矛盾,那么最后被确认的将不会成为有效的区块。
区块按照时间形成一个线性序列,这就是“区块链”这个词的来源。区块以一定的时间间隔被添加到链,以太坊区块链的平时间大约是17秒。
作为“顺序选择机制”(也就是所谓的“挖矿”)的一部分,偶尔可能会发生区块被回滚的情况,但只会发生在链的末尾。被添加的区块越多,被回滚的几率就越小。所以,你的事务被回滚甚至被从区块链中删除是有可能的,但是你等待的时间越长,这种情况发生的几率越小。
以太坊虚拟机
概述
以太坊虚拟机EVM是智能合约的运行环境,它是沙盒化的,而且是完全隔离的,这意味着在EVM中运行的代码不能访问网络,不能访问文件系统,以及其他的进程,智能合约甚至只能有限地访问其他智能合约。
账户
在以太坊平台中有两类账户,它们共享同一个地址空间:外部账户是被公私密钥对(例如人)控制的,合约账户受同时保存的代码控制。
外部账户的地址是由公钥决定的,而合约账户的地址是在合约被创建时确定(这个地址通过合约创建者的地址和从该地址发送的事务数量计算而得,即是所谓的“nonce”)。
无论账户是否存储代码,这两类账户在EVM中同等的。
每一个账户都用一个持久化的键值对来存储,键和值的长度都是256位,我们称之为空间。
另外,每一个账户都有一个以太币余额(单位叫Wei),它可以被包含以太币操作的事务中修改。
交易
一个交易是一个从一个账户转账到另一个账户(可能与前者账户相同,或者是一个特别的零账户)的消息。它可以包含二进制数据(它的负载)以太币。
如果目标账户包含代码,代码会被执行,它的负载江北作为输入数据。
如果目标账户是零账户(账户地址是0),那么交易会创建一个新的合约。如前所述,合约地址不会是零地址,而是根据创建者和它的交易数量计算而得(也就是“nonce”)。这个合约创建事务的负载会被EVM转化成字节码后执行。这个执行的输出被作为合约代码永久存储。这意味着,为了创建一个合约,你不是发送合约的实际代码,而是发送能够产生实际代码的代码。
Gas
一旦创建,每一个交易都会消费一定数量的gas,目的是限制执行工作量在一个必须的量来执行交易同时为这次交易付费。当EVM执行交易,gas根据特定规则逐渐耗尽。
gas price是一个由交易创建者设置的值,从创建者账户中支付gas_price * gas,如果交易执行后还有gas剩余,会按原路返还。
无论在什么情况下gas被用完,一个out-of-gas的异常会被触发,在当前调用帧中所有的状态修改都将被回滚。
存储、内存和栈
每个账户都有一个持久化的内存区域,叫做Storage。Storage是一个键值存储,它是一个256位到256位的映射关系。在合约中存储枚举是不可能的,而且读取storage的成本很高,更别说修改。一个合约可以读取也可以写自己的storage。
第二个内存区域叫memory,合约会为每一次消息调用获取一份新的并清空的内存实例,memory是线性的,而且可以按照字节位单位来寻址,但是读取的最大长度是256位,而写操作可以是8位也可以是256位。memory是按照一个字(256位)来扩展的,不论是读还是写之前从未访问过的memory(如在word内的任何偏移)。随着扩展,gas的消费必须被支付。memory费用的增长会比空间的增长更多,费用增长会以指数级增长。
EVM不是基于寄存器的,而是基于栈的,因此所有计算都是在栈区域执行。它有最大1024个单元,每一个单元包含一些长度为256位的字 。只能在栈的顶端进行访问:允许拷贝最顶端的16个单元中的一个到栈的顶端,或者交换最顶端的单元和随后的16个单元。所有其他的操作会把最顶端的两个(或者一个,或者更多,取决于操作)单元,把运算结果压入栈。当然把栈单元移入到memory或者storage是可以的,但是在不移除顶部单元而仅仅访问栈上制定深度的单元是不可能的。
指令集
EVM的指令集保持了最小化,目的是为了避免错误的实现导致共识问题的发生。所有质量操作都是针对长度为256位的数据类型。拥有常规算术运算、位、逻辑和比较运算,可以条件转移和非条件转移,而且合约可以访问当前区块的相关属性,比如合约的编号和时间戳。
消息调用
合约能够使用消息调用来调用其他合约或者发送以太币到一个非合约账户。消息调用和交易非常类似,它们都有源、目标、数据负载、以太币、gas和返回值。事实上,每一个交易包含一个顶层消息调用,在这个消息调用中可以创建更多的消息调用。
一个合约能够决定在内部消息调用中多少剩余gas可以被发送,也能够决定多少需要被保留。如果在内部调用发生了out-of-gas异常(或者其他异常),它会被以一个错误值而压入栈顶。在这种情况下,只有和调用其他发送的gas会被消耗,在Solidity中,发起调用的合约默认会触发一个手动异常,因此那些异常可以冒泡出栈。
如前所述,被调用的合约(可以和调用者相同)将会获得一份新的并且清除过的内存实例,拥有访问payload的权限,它在一个独立的区域中提供,叫做calldata。当合约结束执行后,它能够返回一份数据,而且江北存储在调用者预先分配好的内存中。
消息调用的深度被限定位1024,这意味着,那些复杂的操作,循环调用比递归调用应该更优先。
委托调用、调用代码和库
存在一些特殊的消息调用,它们被称为委托调用,它们和一般的消息的区别在于目标地址中的代码是在正在被调用的合约的上下文中调用,并且msg.sender
和msg.value
的值不会改变。
这意味着,一个合约能够动态地从运行环境中不同的地址加载代码。存储、当前地址和余额仍指向发起调用的合约,仅仅代码是从被调用地址获取的。
这使得实现solidity的库功能成为现实:重用库中的代码可以被应用到一个合约的存储中,例如用来实现复杂的数据结构。
日志
可以在特殊的可索引的数据结构中存储数据,它映射所有的路径直到区块的层次。这个特性叫做logs,它被Solidity用来实现事件。合约在创建后不能访问日志,但是日志可以在区块链外高效地被访问,因为部分日志数据是被保存在bloom filters中,可以高效并安全地访问和搜索这些数据,因此网络节点不需要下载整个区块链也能找到这些日志,成为轻客户端。
创建
合约甚至可以通过使用一个特殊的指令来创建其他合约(如它们不是简单地调用零地址)。创建调用与普通消息调用的唯一区别是:payload会被执行,执行结果作为代码存储,调用者/创建者在站上获得的新的合约的地址。
自毁
从区块链移除合约代码的唯一可能是当一个合约执行selfdestuct
操作。地址上还保留的以太币会被发送到指定的目标然后其存储的storage和code被从状态中移除。
警告:尽管一个合约的代码没有包含selfdestuct的调用,它依然会使用delegatecall或则会callcode来执行自毁操作。
说明:就和与的删减会也可能不会被以太坊客户端实现。另外,归档节点可以选择无限期保留合约的存储和代码。
说明:外部账户目前还不能被从状体中移除。
原文地址(https://solidity.readthedocs.io/en/v0.4.21/introduction-to-smart-contracts.html)
反馈邮箱(mailto:[email protected])
项目地址(https://github.com/langzhenjun/solidity.git)