以太坊中的智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。合约的账户保存了合约当前的运行状态,主要包含了4部分内容。
智能合约一般使用Solidity语言进行编写,语法上与JavaScript相似。如下是一段Solidity编写的智能合约的代码,这段代码是一个商品拍卖的智能合约。所有参与拍卖的人员对商品进行竞价,每次竞价时都会将相应的价格发送到智能合约中,合约会自动记录竞价人的报价,拍卖结束时,出价最高者获得拍卖品,同时出价最高者的钱会发送给受益人。其他人可以使用withDraw函数拿回自己的钱。代码详细内容见注释。
pragma solididity ^0.4.21 // 声明使用的solidity版本
contract SimpleAuction{ // 声明一个SimplaAuction的合约类
address public beneficiary; // 拍卖受益人
uint public auctionEnd; // 拍卖截止日期
address public highestBidder; // 当前的最高出价人
mapping(address => uint) bids; // 所有竞拍者的出价,map结构
address[] bidders; // 所有竞拍者数组
// 需要记录的事件,event主要用来记录日志
event HighestBidIncreased(address bidder, uint amount); // 出价最高的人发生变动
event Pay2Beneficiary(address winner, uint amount); // 竞拍成功者的钱发送给受益人
/// constructor是构造函数
/// _biddingTime 表示拍卖时长
/// _beneficiary 表示拍卖受益人
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关键字,不推荐使用类名命名构造函数。构造函数只能有1个。构造函数仅仅在合约创建的时候调用一次。
bid()函数中,可以看到有一个 payable 关键字。如果一个函数添加了关键字payable,表明该函数接受转账,如果一个函数不写payable关键字,表明该函数不接受转账。
bid()函数, withdraw()函数,pay2Beneficiary()函数是成员函数,他们有public修饰,表示可供外部调用。
solidity中的map,其结构不支持遍历,这就意味着需要手动记录map中的元素。一般使用数组进行记录。上述代码中使用bidders记录参与竞拍的人。solidity中的数组元素既可以是定长数组,也可以是可变数组。
编写好智能合约之后,如何将该智能合约发布到区块链上呢?在以太坊中,发布一个智能合约,只需要将该合约的内容写入到一笔交易即可。具体过程如下:
通过上述步骤就可以创建一个智能合约,以后调用智能合约时就将交易的收款地址写为智能合约的地址即可。
智能合约无法主动执行,因此智能合约要么是被外部帐户调用,要么被其他智能合约调用,外部账户调用智能合约和内部账户调用智能合约的方法有所不同,下文将分别予以说明。
外部账户调用智能合约时,具体步骤如下:
下图中的接收地址中填入了调用的智能合约地址,data域中填入了要调用的函数和参数的编码值,value为0。
智能合约之间的调用则不需要通过发布交易进行,而是直接使用代码进行交互,调用的方法一般分为2种:
1. 创建被调用合约对象后直接调用相关成员函数。
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;
// 在合约B中创建合约A的对象,然后调用A中的foo()函数,返回结果存在ua中
function callAFooDirectly(address addr) public {
A a = A(addr);
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;
}
}
实际上,还有另外一种智能合约调用方式,即使用delegatecall方法,而delegatecall则类似于我们的函数调用,delegatecall函数中使用的所有上下文参数,均来自于调用发起合约,而不是被调用的合约。
调用智能合约更多详细信息,参考solidity中文文档。
至此,调用智能合约的方法基本叙述完毕,而伴随着智能合约另外一些特征,本文也会予以介绍。
fallback()是一个很特殊的函数。它是智能合约中的一个匿名函数,这个函数没有名称、没有参数,也没有返回值,只有访问类型和函数体。其形式如下:
funcion() public [payable]{...}
匿名函数只有如下两种情况下才会被调用:
1. 向某个合约地址转账,data域为空时。
2. 向某个合约地址转账,data域中填写函数在智能合约中不存在时
用一句话总结,就是data域中的数据被解析后找不到一个可以匹配的函数,就会调用fallback()函数。
fallback()函数仍然可以用payable修饰,添加了payable函数之后表明匿名函数接收转账,如果没有payable,表明该函数不接收转账。如果匿名函数没有payable的情况下转账金额不是0,此时执行fallback()函数就会抛出异常。
智能合约的设计语言solidity是图灵完备语言,这就意味着智能合约中可以包括循环。随之而来的问题是,如果智能合约中出现死循环怎么办?而程序在执行之前无法判断是否会出现死循环。因此,智能合约中引入了汽油费。智能合约执行在EVM中,EVM对执行指令进行了标价,每执行一条指令,就需要消耗相应的汽油,不同的指令因为复杂程度不同,消耗的汽油量会有所不同。
回想一下以太坊中一笔交易的结构:
type txdata struct{
AccountNonce uint; // 交易次数
GasPrice *bit.Int; // 单位汽油价格
GasLimit uint64; // 本交易愿意支付的最大汽油量
Recipient *common.Address // 接收账户地址
Amount *big.Int // 转账金额
Payload []byte // data域
}
以太坊中的交易进行执行,可以看作是一个原子操作,要么全部执行完毕,完成转账;如果执行抛出异常,则执行中的操作全部回滚。所以智能合约在执行时有如下条件判断的语句,在执行前会判断条件,说明如下:
- 智能合约中不存在自定义的try-catch的结构。
- 智能合约执行过程中遇到异常,除非特殊情况,否则本次的执行操作会全部回滚。
- solidity中可以抛出错误的语句有:
- assert(bool condition):如果条件不满足就会抛出错误,用于抛出内部错误,和c++中的assert相同,可以用于Debug。
- require(bool condition):如果条件不满足,也抛出错误,用于检测外部输入条件是否合法。
- revert():无条件抛出异常,终止运行并且回滚状态变动。
// 获取给定区块的哈希值,只能获取最近的256个区块,不包括当前区块。
block.blockhash(uint blockNumber) returns (bytes32)
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) // 交易发起者
需要说明的有如下两点:
变量 | 类型 | 说明 |
---|---|---|
address.balance | 成员变量,uint256类型 | 返回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都可以用来进行转账,区别在于发送的汽油费不同。
矿工执行某个调用智能合约的交易,执行过程中出错,是否需要发布到区块链上?
先执行智能合约再发布区块,还是先发布区块再执行智能合约?
智能合约支持多线程吗?