以太坊智能合约的原理和使用方法

一、智能合约概述

1.1 智能合约是什么

智能合约本质上是运行在区块链上的一段代码,代码的逻辑定义了合约的内容。
智能合约账户保存了合约当前的运行状态,包括当前余额(balance)、交易次数(nonce)、合约代码(code)、存储(storage 数据结构是MPT,合约的执行数据保存在这里)。

1.2 solidity语言

智能合约的最常用的语言是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”为发起人最多愿意花费的汽油量。

三、一个合约调用另一个合约

一个合约可以调用另一个合约,但是合约账户不能主动调用另一个合约,必须由外部账户发起。

3.1 直接调用

给出另一个合约的地址,直接调用。如下图所示,callAFooDirectly的参数是一个合约地址,将地址转换为合约实例,然后就可以调用该合约的foo函数:
在这里插入图片描述
如果在执行a.foo()过程中抛出错误,则callAFooDirectly也抛出错误,本次调用全部回滚。ua为执行a.foo(“call foo directly”)的返回值。
另外可以通过.gas() 和 .value() 调整提供的gas数量或提供一些ETH。

3.2 使用address类型的call()函数

通过address类型call函数调用智能合约。下面这个例子相当于A(addr).foo(“call foo by func call”),如下图所示:
在这里插入图片描述
call函数的第一个参数被编码成4个字节,表示要调用的函数的签名。其它参数会被扩展到32字节,表示要调用函数的参数。
返回一个布尔值表明了被调用的函数已经执行完毕(true)或者引发 了一个EVM异常(false),无法获取函数返回值。而发起调用的函数并不会产生异常,继续执行。
与直接调用类似,也可以通过.gas() 和 .value() 调整提供的gas数量或提供一些ETH。

3.3 代理调用delegatecall()函数

代理调用delegatecall()函数使用方法与call()相同,只是不能使用.value()。
另外call()会切换到被调用的智能合约上下文中,delegatecall()只使用给定地址的代码,其它属性(存储,余额等)都取自当前合约。delegatecall 的目的是使用存储在另外一个合约中的库代码。

3.4 payable

如果合约账户接收外部转账(“VALUE”域不为0),被调用的函数必须标注payable。例如下图所示,bid函数接收拍卖出价,以太币存储在合约内,可以防止恶意出价:
在这里插入图片描述

3.5 fallback()函数

如果转账交易的“TX DATA”域中没有说明函数名,或域里的函数名不存在,则默认调用fallback函数,如果合约中没有定义fallback函数(合约中不一定存在fallback函数),那么将调用失败。函数定义如下所示:
function() public [payable]{
……
}

  • 匿名函数,没有参数也没有返回值。
  • 如果转账金额(“VALUE”域)不是0,同样需要声明payable,否则会抛出异常。

四、智能合约的创建和运行

4.1 创建与运行

智能合约的代码写完后,要编译成bytecode。创建合约时,外部帐户发起一个转账交易到0x0的地址,转账金额(“VALUE”域)是0,但是要支付汽油费,合约的代码放在data域里。

智能合约运行在EVM(Ethereum Virtual Machine)上,EVM的寻找空间为256位。以太坊是一个交易驱动的状态机,调用智能合约的交易发布到区块链上后,每个矿工都会执行这个交易,从当前状态确定性地转移到下一个状态。

4.2 汽油费(gas fee)

智能合约是个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域。
其他节点收到区块后会扔掉自己的执行结果,同时验证区块内的交易,包括验证汽油费的扣除是否合法。然后更新本地数据结构,继续挖矿。

4.3 错误处理

智能合约的执行具有原子性,一旦遇到异常(比如汽油费不够),除特殊情况外,执行操作全部回滚,不会只执行一部分。智能合约中不存在自定义的try-catch结构,不可以捕获异常。
可以抛出错误的语句如下:

  • assert(bool condition):如果条件不满足就抛出—用于内部错误。
  • require(bool condition):如果条件不满足就抛出—用于函数输入或者外部组件引起的错误。
  • revert():无条件地抛出异常,终止运行并回滚状态变动。

4.4 嵌套调用

嵌套调用是指一个合约调用另一个合约中的函数。如果被调用的合约执行过程中发生异常,有些调用方法会导致发起调用的这个合约也跟着一起回滚,而call()函数调用则不会连环回滚,只会使得当前调用失败,得到false返回值。

一个合约直接向一个合约帐户里转账,没有指明调用哪个函数,仍然会引起嵌套调用,这时调用的是fallback函数。

五、智能合约可以获取的信息

智能合约不支持任何造成执行结果不确定的操作,比如多线程下对内存的访问、真随机数产生。
另外智能合约无法获取与系统环境相关的信息,因为每个系统的环境都是不一样的,所以只能获得一些固定信息。

5.1 区块信息

智能合约可以获得的区块信息如下:

  • block.blockhash(uint blockNumber) returns (bytes32):获得指定区块的哈希,仅对最近256个区块有效而不包括当前区块
  • block.coinbase (address):挖出当前区块的矿工地址
  • block.difficulty (uint):当前区块难度
  • block.gaslimit (uint):当前区块gas限额
  • block.number (uint):当前区块号
  • block.timestamp (uint):自unix epoch起始当前区块以秒计的时间戳

5.2 合约调用信息

智能合约可以获得的合约调用信息如下:

  • msg.data (bytes):完整的calldata
  • msg.gas (uint):剩余gas
  • msg.sender (address):消息发送者(当前调用)
  • msg.sig (bytes4):calldata的前4字节(也就是函数标识符)
  • msg.value (uint):随消息发送的wei的数量
  • now (uint):目前区块时间戳(block.timestamp)
  • tx.gasprice (uint):交易的gas价格
  • tx.origin (address):交易发起者(完全的调用链)

六、合约地址类型

6.1 合约地址调用

所有智能合约均可显式地转换成地址类型,当前智能合约得到另一个合约的address,便可通过address调用对应合约的成员变量和函数,如下所示:

  • <address>.balance (uint256):
    balance为成员变量,类型是uint256,以Wei为单位的地址类型的余额;
  • <address>.call(…) returns (bool):
    发出底层call,失败时返回false,发送所有可用gas,不可调节;
  • <address>.callcode(…) returns (bool):
    发出底层callcode,失败时返回false,发送所有可用gas,不可调节;
  • <address>.delegatecall(…) returns (bool):
    发出底层delegatecall,失败时返回false,发送所有可用gas,不可调节。

6.2 转账给合约

转账给合约地址方式如下所示:

  • <address>.transfer(uint256 amount):
    会触发调用fallback函数,向地址类型发送数量为amount的Wei,失败时抛出异常,同时发送2300gas的矿工费,不可调节;
  • <address>.send(uint256 amount) returns (bool):
    会触发调用fallback函数,向地址类型发送数量为amount的Wei,失败时返回fasle,同时发送2300gas的矿工费用,不可调节;
  • <address>.call.value(uint256 amount)():
    会触发调用fallback函数,向地址类型发送数量为amount的Wei,失败时返回fasle,发送所有可用gas,多余的gas再返回。
发布了21 篇原创文章 · 获赞 2 · 访问量 4275

猜你喜欢

转载自blog.csdn.net/ice_fire_x/article/details/104236394