智能合约本质上是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。
智能合约账户保存了合约当前的运行状态,包括当前余额(balance)、交易次数(nonce)、合约代码(code)、存储(storage 数据结构是MPT,合约的执行数据保存在这里)。
智能合约的最常用的语言是Solidity,语法上与JavaScript很接近。如下图所示:
“pragma solidity ^0.4.21”声明solidity版本号,不同版本在语法上有一些差别;
contract相当于“class(类)”,里面定义了一些状态变量。solidity是强类型编程语言,大部分类型与常用编程语言类似,address类型是solidity特有的类型;
mapping哈希表不支持遍历,所以需要单独创建一个数组,用于遍历key值,例如上图的哈希表bids和数组bidders;
event(事件)用于记录日志,使用emit调用该日志函数;
constructor是构造函数,仅在合约创建时调用一次;
接下来3个成员函数都是public,可以被外部账户或合约账户调用。
如果外部账户是转账给另一个外部账户,那么与比特币的转账几乎相同;如果转账给合约账户,那么就是发起这个账户的合约调用,调用的函数及参数在“TX DATA”域中说明,如下图所示:
其它域的说明如下:
“SENDER ADDRESS”为发起转账的地址;
“TO CONTRACT ADDRESS”为接收的合约账户;
“VALUE”为转账金额(金额为0说明仅调用函数,没有转账);
“GAS USED”是该交易花费的汽油量;
“GAS PRICE”是单位汽油的价格;
“GAS LIMIT”为发起人最多愿意花费的汽油量。
一个合约可以调用另一个合约,但是合约账户不能主动调用另一个合约,必须由外部账户发起。
给出另一个合约的地址,直接调用。如下图所示,callAFooDirectly的参数是一个合约地址,将地址转换为合约实例,然后就可以调用该合约的foo函数:
如果在执行a.foo()过程中抛出错误,则callAFooDirectly也抛出错误,本次调用全部回滚。ua为执行a.foo(“call foo directly”)的返回值。
另外可以通过.gas() 和 .value() 调整提供的gas数量或提供一些ETH。
通过address类型call函数调用智能合约。下面这个例子相当于A(addr).foo(“call foo by func call”),如下图所示:
call函数的第一个参数被编码成4个字节,表示要调用的函数的签名。其它参数会被扩展到32字节,表示要调用函数的参数。
返回一个布尔值表明了被调用的函数已经执行完毕(true)或者引发 了一个EVM异常(false),无法获取函数返回值。而发起调用的函数并不会产生异常,继续执行。
与直接调用类似,也可以通过.gas() 和 .value() 调整提供的gas数量或提供一些ETH。
代理调用delegatecall()函数使用方法与call()相同,只是不能使用.value()。
另外call()会切换到被调用的智能合约上下文中,delegatecall()只使用给定地址的代码,其它属性(存储,余额等)都取自当前合约。delegatecall 的目的是使用存储在另外一个合约中的库代码。
如果合约账户接收外部转账(“VALUE”域不为0),被调用的函数必须标注payable。例如下图所示,bid函数接收拍卖出价,以太币存储在合约内,可以防止恶意出价:
如果转账交易的“TX DATA”域中没有说明函数名,或域里的函数名不存在,则默认调用fallback函数,如果合约中没有定义fallback函数(合约中不一定存在fallback函数),那么将调用失败。函数定义如下所示:
function() public [payable]{
……
}
智能合约的代码写完后,要编译成bytecode。创建合约时,外部帐户发起一个转账交易到0x0的地址,转账金额(“VALUE”域)是0,但是要支付汽油费,合约的代码放在data域里。
智能合约运行在EVM(Ethereum Virtual Machine)上,EVM的寻找空间为256位。以太坊是一个交易驱动的状态机,调用智能合约的交易发布到区块链上后,每个矿工都会执行这个交易,从当前状态确定性地转移到下一个状态。
智能合约是个Turing-complete Programming Model。理论上可证明,不存在任何算法可判断出任一程序是否会停机。
以太坊中使用汽油费机制,防止出现死循环。合约中的指令在执行时要收取汽油费,由发起交易的人来支付。EVM中不同指令消耗的汽油费是不一样的,简单的指令很便宜,复杂的或者需要存储状态的指令就很贵读取。读取公共数据时,则不需要汽油费。汽油费在txdata结构体中,代码如下所示:
AccountNonce是交易序号,用于防止重放攻击,Price是单位汽油价格,GasLimit是愿意支付的最大汽油量,Recipient是收款人地址,Amount是转账金额,Payload是合约函数和参数,即前面章节所述的txDATA域。
实际系统中,节点收到一个智能合约调用时,先按照GasLimit从发起的账户里扣掉Gas fee,增加自己账户余额,然后根据实际执行结果算出花费的汽油费,多出的汽油费会退还。如果执行中发现汽油费不足,会引起回滚,但是执行中扣掉的汽油费不会退还;
GasLimit:
区块头结构体中的GasUsed指的所有交易用的汽油总和;GasLimit指的是区块内所有交易能够消耗的汽油上限,与txData结构体中的GasLimit没有关系,用于限制区块内消耗的资源(比特币中使用区块小于1M来限制)。每个矿工在打包区块时,都可以在父区块的GasLimit基础上调整±1/1024。
合约执行异常的汽油费
合约执行过程中出现任何异常,都会回滚,同时扣掉对应汽油费,所以出错的交易也会包含在区块中。合约执行结果体现在交易收据中,即Receipt结构体中的Status域。
其他节点收到区块后会扔掉自己的执行结果,同时验证区块内的交易,包括验证汽油费的扣除是否合法。然后更新本地数据结构,继续挖矿。
智能合约的执行具有原子性,一旦遇到异常(比如汽油费不够),除特殊情况外,执行操作全部回滚,不会只执行一部分。智能合约中不存在自定义的try-catch结构,不可以捕获异常。
可以抛出错误的语句如下:
嵌套调用是指一个合约调用另一个合约中的函数。如果被调用的合约执行过程中发生异常,有些调用方法会导致发起调用的这个合约也跟着一起回滚,而call()函数调用则不会连环回滚,只会使得当前调用失败,得到false返回值。
一个合约直接向一个合约帐户里转账,没有指明调用哪个函数,仍然会引起嵌套调用,这时调用的是fallback函数。
智能合约不支持任何造成执行结果不确定的操作,比如多线程下对内存的访问、真随机数产生。
另外智能合约无法获取与系统环境相关的信息,因为每个系统的环境都是不一样的,所以只能获得一些固定信息。
智能合约可以获得的区块信息如下:
智能合约可以获得的合约调用信息如下:
所有智能合约均可显式地转换成地址类型,当前智能合约得到另一个合约的address,便可通过address调用对应合约的成员变量和函数,如下所示:
转账给合约地址方式如下所示: