以太坊中的智能合约(Smart Coantract)
创建智能合约
以太坊中的智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。只能合约是以太坊和比特币系统最大的区别。在以太坊中,智能合约的账户保存了合约当前的运行状态,主要包含了4部分内容。
balance:当前余额
nonce: 交易次数
code: 合约代码
storge: 存储,是一棵MPT
智能合约一般使用Solidity语言进行编写,语法上与JavaScript相似。
如下是一段Solidity编写的智能合约的代码,这段代码是一个商品拍卖的智能合约,具体内容可以见注释。
pragma solididity ^0.4.21 // 声明使用的solidity版本
contract SimpleAuction{
address public beneficiary; // 拍卖受益人
uint public auctionEnd; // 拍卖截止日期
address public highestBidder; // 当前的最高出价人
mapping(address => uint) bids; // 所有竞拍者的出价
address[] bidders; // 所有竞拍者
// 需要记录的事件,event主要用来记录日志
event HighestBidIncreased(address bidder, uint amount);
event Pay2Beneficiary(address winner, uint amount);
/// constructor是构造函数
constructor(uint _biddingTime, address _beneficiary) public
{
beneficiary = _beneficiary;
auctionEnd = now + _biddingTime;
}
/// 对拍卖进行竞价,如果之前出过价,就会把之前的价格与当前价格求和作为竞价
function bid() public payable{...}
/// 参与投标的人在拍卖结束后取回自己的钱
function withdraw() public returns(bool){}
/// 结束拍卖,将最高出价的钱发送给受益人
function pay2Beneficiary() public returns(bools){}
}
智能合约的构造函数,叫做constructor,可以理解为c++中的类名,构造函数只能有1个。构造函数仅仅在合约创建的时候调用一次,在solidity语言创建之初使用类名来建立构造函数,但是现在solidity的版本中推荐为constructor作为构造函数名。
bid()函数中,可以看到有一个payable的标志。如果一个函数添加了关键字payable,表明该函数接受转账,如果一个函数不写payable关键字,表明该函数不接受转账。
bid()函数, withdraw()函数,pay2Beneficiary()函数是成员函数,他们有public修饰,表示可供外部调用。
上述代码中值得一提的是,solidity中的map,不支持遍历,这就意味着需要手动记录map中的元素。一般使用数组进行记录。solidity中的数组元素既可以是定长数组,也可以是可变数组。
编写好智能合约之后,如何将该智能合约发布到区块链上呢?这个需要通过在以太坊中构建一笔交易来实现。具体过程如下:
将智能合约代码编译为二进制字节码。
利用一个外部帐户发起一个转账交易,交易地址为0x0,转账金额设置为0,但是需要支付汽油费。
合约的代码放在data域。
填写交易其他部分内容,
发布交易,交易执行完毕后会返回给创建者智能合约的地址
通过上述步骤就可以创建一个智能合约,智能合约运行在EVM(Ethereum Viartual Machice)上。
调用智能合约
智能合约被设置为无法主动执行,这就意味着智能合约的执行,要么是被外部帐户调用,或者被其他智能合约调用。
外部账户调用智能合约
无论是外部帐户还是其他智能合约的调用,他们的方法是一样的。具体步骤如下:
创建一笔交易,交易的接收地址为要调用的智能合约的地址。
把要调用的函数名称和该函数需要的参数进行编码,随后填入data域中。
填写其他交易内容,发布交易。
下图中的接收地址中填入了调用的只能合约地址,data域中填入了要调用的函数和参数的编码值。
智能合约账户调用智能合约
智能合约也可以调用另外一个智能合约,调用的方法有2种:
创建被调用合约对象后直接调用。
使用address类型的call()函数。
创建对象后直接使用的示例代码如下。
contract A{
event LogCallFoo(string str); // 定义一个事件
function constructor(address addr) public{} // 构造函数
function foo(string str) return (uint){
emit LogCallFoo(str); // 写日志操作
return 123;
}
}
contract B{
uint ua;
function callAFooDirectly(address addr) public {
A a = A(addr); // 创建一个A的对象
ua = a.foo("call foo directly");
}
}
在合约B中,构建了智能合约A的对象,然后调用了A中的foo函数。需要指出的是,在执行a.foo()的过程中如果抛出了错误,那么callAFooDirectly()函数也会抛出错误,这一笔交易全部回滚到交易前的状态。
使用address类型的call()函数的示例代码如下。
contract C{
function callAFooByCall(address addr) public return (bool){
bytes4 funcsig = bytes4(keccak256("foo(string)")); // 将要调用的函数编码成为4字节
if(addr.call(funcsig, "call foo by func call")) // address.call
return true;
return false;
}
}
上述addr.call(funcsig,”call foo by func call”)中,funcsig表示被调用函数的签名,funcsig是一个4字节大小的参数。而”call foo by func call”则是被调用函数的参数,如果有多个参数,则应该跟在后面。被调用函数的参数会被扩展成为32字节。
如果函数执行成功,则会返回true,执行失败或者引发异常,则会返回false。
和第一种调用函数方法相比,使用address.call(),即使被调用函数失败,也不会引起回滚,这是他们的区别。
fallback()函数
为什么单独把fallback()函数拎出来介绍一下呢?因为fallback()是一个很特殊的函数。它是智能合约中的一个匿名函数,这个函数没有参数,也没有返回值,也没有名称,只有访问类型和函数体。形式如下:
funcion() public [payable]{...}
这个函数只有如下两种情况下才会被调用:
直接向一个合约地址转账但是data域为空。
被调用的函数不存在的时候。
用一句话总结,就是data域中的数据被解析后找不到一个可以匹配的函数,就会调用fallback()函数。
fallback()函数仍然可以用payable修饰,添加了payable函数之后表明匿名函数接收转账,如果没有payable,表明该函数不接收转账。如果匿名函数没有payable的情况下转账金额不为0,这时候执行fallback()函数就会抛出异常。
汽油费(gas fee)
智能合约的设计语言solidity是图灵完备语言,这就意味着智能合约中可以包括循环。随之而来的问题是,如果智能合约中出现死循环怎么办?
于是智能合约中引入了汽油费。EVM中对智能合约的执行指令进行了标价,每执行一次指令,就需要支付相应的汽油费。而每个交易中都有一个gas limit字段。如果执行中出现了死循环,执行所需要的gas fee就会超出gas limit,此时EVM就会强行停止智能合约的执行,这样就能有效的防止死循环。以太坊中一个交易的数据结构如下:
type txdata struct{
AccountNonce uint; // 交易次数
GasPrice *bit.Int; // 单位汽油价格
GasLimit uint64; // 本交易愿意支付的最大汽油量
Recipient *common.Address // 接收账户地址
Amount *big.Int // 转账金额
Payload []byte // data域
}
GasLimit * gasLimit 就是本次交易支付的最大gas fee,如果智能合约执行过程的消耗gas fee超过了他们的乘积,EVM就会停止执行,同时回滚至执行前的状态,但是之前扣掉的汽油费不会返还,这样是为了防止denil of service攻击。
以太坊中不同指令消耗的gas不同,复杂的指令消耗的gas 更多。
以太坊中的错误处理
以太坊中的交易进行执行,是一个原子操作,要么全部执行,要么不知行,但是执行过程中会出现错误,以太坊中对错误的执行需要如下几个概念。
智能合约中不存在自定义的try-catch的结构。
一旦遇到异常,除非特殊情况,否则本次的执行会全部回滚。
solidity中可以抛出错误的语句有:
assert(bool condition):如果条件不满足就会抛出错误,用于抛出内部错误,和c++中的assert相同,可以用于Debug。
require(bool condition):如果条件不满足,也抛出错误,用于检测外部输入条件是否合法。
revert():无条件抛出异常,终止运行并且回滚状态变动。
智能合约可以得到的区块信息
block.blockhash(uint blockNumber) returns (bytes32) // 获取给定区块的哈希值,只能获取最近的256个区
// 块,不包括当前区块。
block.coinbase(address) // 挖出当前区块的矿工地址
block.difficulty(uint) // 当前区块的难度
block.gaslimit(uint) // 当前区块的gas限额
block.number(uint) // 当前区块号
block.timestamp(uint) // 当前区块以秒计数的时间戳
智能合约可以获得的调用信息
msg.data (bytes): 完整的calldata
mas.gas ( uint): 剩余的gas
mas.sender (address): 消息发送者(当前调用)
msg.sig (bytes4): calldata的前4字节(即函数标识符)
msg.value (uint): 随消息发送的wei的数量
now (uint): 目前区块的时间戳(和前面的block.timestamp相同)
tx.gasprice (uint): 交易的gas价格
tx.origin (address): 交易发起者
需要说明的有如下两点:
智能合约调用的信息,全部是变量,而不是函数调用,括号中的类型,是这些变量的返回类型。
msg.sender和tx.origin是有区别的,msg.sender表示调用当前合约的地址,不一定是交易的发起者。因为一笔交易中发起的合约A可以调用合约B,此时对于B来说,msg.sender是A,tx.origin是交易发起者。
智能合约中的地址类型
address.balance // 返回uint256类型,返回address中以Wei计量的余额
address.transfer(uint256 amount) // 向address所在的地址发送amount数量的Wei,失败时抛出异
// 常,发送2300gas矿工费,该矿工费不可调节。
address.send(uint256 amount) return (bool): // 向address发送amount书来那个的Wei,失败时返回false,
// 发送2300的gas矿工费,该矿工费不可调节。
address.call(...) return (bool) // 发出底层CALL,失败返回false,发送所有可用的gas进行调用,
// 发送的gas不可调节。
address.callcode(...) return (bool) // 发出底层CallCODE,失败时返回false,发送所有可用的gas,
// 发送的gas不可调节。
address.delegatecall(...) return (bool) // 调用底层DELEGATECALL,失败返回false,发送所有可用gas
// 发送的gas不可调节。
注意:所有智能合约都可以显式的转换称地址类型。transfer和send以及call都可以用来进行转账,区别在于发送的汽油费不同。
有趣的问题
矿工执行某个调用智能合约的交易,执行过程中出错,是否需要发布到区块链上?
- 答:需要发布到区块链上,虽然执行失败,但是需要扣掉gas fee,发布到区块链上,其他矿工执行失败时也相应的扣掉汽油费,只不过此时扣掉的汽油费不是转给自己,而是转给发布区块的矿工账户。
先执行智能合约再发布区块,还是先发布区块再执行智能合约?
- 答:先执行智能合约,再发布到区块。每一个新发布区块中最新的三个状态树、交易树、收据树的哈希值,都是执行完智能合约之后才能得到。挖到区块的矿工发布区块之后,其他矿工随之执行新区块中的交易,同步更新本地存储的状态树、交易树和收据树,以此维持数据同步。
智能合约支持多线程吗?
- 智能合约的solidity不支持多线程。以太坊是一个交易驱动的状态机,因此面对同一种输入,必须到达一个确定的状态。但是多线程的问题在于多核对内存访问顺序不一样,就会引起状态变化,这不利于维护区块链中状态的一致性。同时,其他可能造成不一致的操作,智能合约也不支持。最明显的例子就是以太坊中的智能合约没办法产生真正意义下的随机数。