精通以太坊9:智能合约和solidity(3)

精通以太坊9:智能合约和solidity(3)

9.1错误处理(assert、require和revert)

合约的执行过程可能会中断,也可能会返回错误。Solidity中的错误处理由以下这几个关键字负责:assert、require、revert和throw(目前已经废弃)

合约遇到错误而中断执行时,所有的状态更改(变量、余额等)都会回滚,如果是一串合约的连续调用,则会一直恢复到最初触发这一串调用的那个合约时的状态。这确保了交易的原子化,意味着一个交易需要作为整体而成功,如果失败,不会发生任何状态改变,整体撤销。

assert和require这两个函数起到同样的作用,它们判断条件是否满足,如果不满足,就触发错误并且中止合约的执行。根据约定,assert用于判断输出条件为真的情况,也就是说,我们使用assert来判断内部条件。与之对应,require用来测试输入(例如函数的参数,或者交易对象的字段等),验证我们对这些外部变量的期望。

我们在函数修饰符onlyOwner中使用过require函数,用于验证交易消息的发送方的地址是否等于合约创建者的地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FP1H3VTi-1585930917371)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585926077786.png)]

require函数扮演了条件门的角色,如果条件不满足,它会阻止后续代码的执行,并且产生一个错误。

在Solidity v.0.4.22中,require也可以包含一个提示信息,用于指明发生错误的原因。错误信息会被记录在交易的日志中。因此我们可以更进一步优化代码,在其中加入这个报错信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uzAeXQSn-1585930917373)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585926135550.png)]

revert和throw函数用于立即中止合约的执行,并且把状态回滚。throw函数已经淘汰,并将在未来版本的Solidity中被移除,我们应该使用revert函数。revert函数也可以接收一个作为出错信息的参数,这个信息会被记录在交易的日志中。

无论我们是否进行边界条件的检查,有些合约在执行的过程中总是会产生错误。例如Faucet合约,我们并没有检查合约是否拥有足够满足提币申请的以太币。但是当合约中的余额不足以满足提币请求时,transfer函数会失败,发出错误信息并回滚当前交易的状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZkTn9sT-1585930917373)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585926334280.png)]

似这样的额外错误检查代码会少量地增加gas的开销,但是在出问题时可以提供更友好的报错信息。程序员需要根据程序本身的执行情况,在gas消耗和清晰报错信息之间寻求平衡。类似目前这个运行在测试网环境下的Faucet,我们肯定会倾向于获得更丰富的报错信息,因为这并不会产生真实的gas开销。也许在以太坊主网运行时,我们才会在gas的开销上更加精打细算。

9.2事件

当交易完成后(不论成功与否),它都会生成一个交易收据(详见第13章

易收据包含完整的日志条目,记录了交易执行过程中发生的动作。事件是Solidity的高级对象,用于生成这些日志。

事件对于轻量级客户端和DApp服务非常有用,这些客户端或者服务可以监控特定事件的发生,并在它们的用户界面中有所反应,或者修改自身程序中的某些状态,来体现出底层合约所发生的变化。

事件对象可以把参数序列化并且记录在以太坊区块链的事件日志中。你可以在参数之前添加indexed关键字,这样可以把索引过后的值作为可以搜索或者过滤的哈希表。

目前为止我们还没有在Faucet例子中添加任何事件,那么现在开始尝试吧。我们将添加两个事件,一个用于记录所有的提币操作,另外一个用于记录所有的存入操作。我们把这两个事件分别称为Withdrawal和Deposit。首先,我们在Faucet合约中定义这些事件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHLX1vaM-1585930917375)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585927213072.png)]

我们选择给地址变量添加indexed关键字,这样将允许任何访问Faucet的用户界面对这个变量进行搜索或者过滤。

接着,我们使用emit关键字把事件相关的数据写入交易日志:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFedReMe-1585930917377)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585927390185.png)]

编写完成的Faucet.sol合约的完整代码如下:代码7-3:Faucet8.sol:

经过修改的合约代码,添加了事件

捕获事件

好了,我们已经设定了合约对外发出事件,那么如何看到交易日志的内容,或者如何捕获这些事件呢?web3.js库提供了一个代表交易执行结果的数据结构,其中包含交易的日志。从中我们可以看到由交易生成的事件。我们使用truffle来针对修改过之后的Faucet合约运行一个测试交易,根据附录中有关Truffle框架的内容中的步骤设定一个项目目录,编译Faucet的代码

GitHub代码库(https://github.com/ethereumbook/ethereumbook)中获得:

当合约部署完毕后,使用deployed函数,我们执行了两个交易。第一个交易是一笔存入交易(使用send),它会触发Deposit事件并在交易日志中记录如下内容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8dgUAVR-1585930917378)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585927747261.png)]

接着,我们使用withdraw函数进行一个提币的操作,这会触发Withdrawal事件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UuWKa6ad-1585930917380)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585927771430.png)]

为了获取这些事件,我们可以查看由交易返回的结果(res)中的logs数组。第一个日志项(logs[0])中包含事件的名称logs[0].event和事件的参数logs[0].args。通过在控制台显示这些内容,我们可以查阅对应事件的名称和事件参数。事件是一种非常有用的机制,不仅可以用于合约之间的通信,也可以用于开发环节中的代码调试。

9.3调用其它合约(send、call、callcode和delegatecall)

从合约中调用其他合约是非常有用但同时也有潜在危险的操作。我们将研究实现这一目标的各种方法,并评估每种方法的风险。简而言之,风险源于这样一个事实:你可能不知道自己正在调用的合约,或者不知道谁在调用你的合约。在编写智能合约时,必须记住,虽然大部分情况是处理外部账户发起的合约调用,但是没有什么可以阻止任意复杂的甚至可能是恶意的合约被你的代码调用,或者调用你的代码。

创建新的实例

调用自己编写的合约往往是最安全的做法,因为你非常清楚被调合约的接口和行为。你可以直接使用关键字new来实例化一个合约,如其他面向对象编程语言一样。在Solidity中,关键字new会在以太坊区块链上创建一个合约实例,并且返回一个可供引用的对象。例如,我们假设现在需要从名为Token的其他合约中创建一个Faucet实例,并且调用它:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7u3nORel-1585930917381)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585928082695.png)]

这样的合约构建机制可以保障调用方了解合约的具体类型和接口。Faucet合约的代码实现必须被定义在Token的范围内,如果合约的定义在其他文件中,那么就需要通过import语句来实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rr9Bfsxs-1585930917382)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585928135670.png)]

new关键字也接收可选的参数,这些参数用来指定合约创建时转入的以太币数量,以及可能需要传递给新合约构造函数的一些参数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhwAj5gb-1585930917383)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585928288658.png)]

需要注意,这时开发和部署Token合约的外部账号是Token合约的所有者,而Faucet合约是由Token合约创建和部署的,因此它的所有者是Token合约,也只有Token合约可以调用Faucet的destroy函数。

添加已存在的实例

调用合约的另一种方法是转换合约现有实例的地址。使用这种方法,可以将已知接口应用于已存在的实例。因此,非常重要的一点是,一定要明确知晓你正在处理的地址上的实例确实是自己所假设的类型。我们来看一个例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oVUeSU8t-1585930917384)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585928377116.png)]

在这段代码中,我们从构造函数的参数_f中获得一个地址,并把这个地址类型转换为Faucet对象。这比之前的机制要危险得多,因为我们无法确定这个地址是否指向了希望调用的Faucet实例。调用withdraw函数时,需要假设这个函数接收我们传入的参数,并且的确执行Faucet中所声明的那些代码。但是我们无法得到保证。也许,这个合约地址上的withdraw会执行我们完全无法预料的代码,而仅仅是函数的名字相同。因此,使用作为输入传递的地址并将它们转换为特定对象,比自己创建合约然后调用的做法更危险。

原生call和delegatecall

Solidity还提供了一些更底层的函数,用于调用其他合约。这些直接对应着EVM的字节码,允许开发者手动构建合约对合约的调用。因此,这些都是最灵活但也是最危险的合约调用机制

以下是一个相同的例子,使用了call方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oxDlx846-1585930917385)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585929306723.png)]

如你所见,这样的调用无异于盲调用,基本上就是在合约的上下文中构建一个原生的交易。这会让合约暴露在一系列安全风险之下,其中最主要的就是可重入性攻击,后文会有专门一节讨论这个主题。如果遇到问题,call函数会返回false,这样我们就能评估返回值,并用于错误处理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JH1eGDZt-1585930917386)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585929359341.png)]

call的另外一种选择是使用delegatecall,这个方法替代了更危险的callcode。callcode这个方法即将被废弃,因此不应该再继续使用它。

正如我们在前文中提到的,delegatecall与call并不完全相同,其中的msg上下文并不会发生变化。例如,call会把msg.sender的值替换为发起调用的合约,但是delegatecall仍旧保持msg.sender不变。简而言之,delegatecall其实就是在当前合约的上下文中运行另外一个合约。这通常用于调用来自库中的代码。程序需要区分哪些代码以库合约的方式出现,哪些代码用于跟当前合约的数据打交道。

使用delegate调用需要非常谨慎。它可能会导致一些无法预料的副作用,特别是在调用的合约并不是库的情况下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nckKk3Ox-1585930917387)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585929650336.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4P8O6Qds-1585930917387)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585929674606.png)]

caller是主合约,它调用了一个库合约calledLibrary和另外一个合约calledContract。库合约和这个被调用的合约都有一个名为calledFunction的函数,调用这个函数会在交易日志中记录一个calledEvent事件。这个日志包含三部分数据:msg.sender、tx.origin和this。每次调用calledFunction时,由于调用方式的不同,它的执行上下文肯定也会不同。取决于是采用直接调用还是delegatecall的方式,这些执行上下文的变量可能完全不一样。

在caller中,我们首先通过直接调用的方式逐一调用库和另外一个合约中的calledFunction函数。然后,我们使用底层方法call和delegatecall来调用calledContract. calledFunction。通过这样的方式,我们可以观察不同调用机制的行为差异。

我们在Truffle开发环境中运行这些命令,然后查看生成的事件信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NTOtD7Q6-1585930917388)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930082545.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SmoVPF7f-1585930917389)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930100211.png)]

我们来分析这些调用背后到底发生了什么。我们调用make_calls函数,并把calledContract的地址传递给它。执行这个函数所对应的一系列调用产生了四个事件。我们来看看make_calls的代码,并逐一分析。

第一个调用是:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4mUpfrzu-1585930917390)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930136987.png)]

![

这里使用了calledFunction的高级ABI定义,直接调用了calledContract.calledFunction,产生的事件是:](C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930152266.png)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5AAyvWTK-1585930917391)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930160709.png)]

如你所见,msg.sender是caller合约的上下文。tx.origin的值对应的是我们在Truffle中的测试账户web3.eth.accounts[0],对caller的调用交易从这个账户发出。事件由calledContract这个合约产生,事件的最后一个参数可以证明这一点。

make_calls中的第二个调用是向库合约发起的调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fOe8UamQ-1585930917392)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930251002.png)]

这个调用看似跟调用普通的合约没有任何区别,但是它的行为却大相径庭。我们来看看这次调用产生的事件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgAJNU8J-1585930917393)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930371737.png)]

次,msg.sender不再是call,而是被测试账户的地址取代,也就是发起调用方的地址。这是因为,在调用库合约时,调用总是采用delegatecall方式进行,库合约的代码始终运行在调用发起方的上下文中。因此,当calledLibrary的代码运行时,它继承了caller的执行上下文,就如同库合约的代码直接运行在caller合约内部一样。this变量(在事件中表示为from)是caller合约的地址,即使这时代码的执行和变量访问都是在calledLibrary中发生的。

接下来的两个调用使用了底层的call和delegatecall,这可以验证我们的期望:这两个调用所发出的事件跟之前两次调用完全一致。

9.4与gas有关的注意事项

在进行智能合约开发时必须认真对待gas相关问题。gas用于限定以太坊允许一个交易运行所耗费的最大计算量。如果在执行过程中gas耗尽,会触发如下一系列事

· 抛出“out of gas”异常。

· 状态被恢复到执行开始之前。

· 所有在这次执行过程中的gas开销都会被作为交易费用,以太坊不会因为交易中止而退回gas或以太币。

gas的费用由交易的发起方支付,因此我们需要避免调用那些可能引发高额gas的合约或者函数。程序员的最佳策略就是设法避免合约可能产生的gas消耗。关于这一点,有一些在构建智能合约时需要遵循的最佳实践,用于尽可能把一个智能合约函数调用所产生的gas消耗最小化。

9.5避免动态尺寸的数组

任何对动态数组所执行的循环操作,例如对数组的每一个元素进行操作,或者通过遍历的方式来查找特定的元素,都有触发高额gas消耗的风险。因此,合约可能会在完成这样的循环之前就耗尽gas额度,既浪费了时间,也产生了不必要的以太币消耗。

9.6避免调用其他合约

调用其他合约,特别是那些gas消耗未知的合约,可能会产生高额的gas开销。避免使用那些没有经过测试和广泛使用的库合约。如果库合约没有经过大量的实战考验,那么使用它的风险肯定会很高。

9.7估计gas开销

如果需要估算调用合约中的某个方法可能引发的gas开销,在考虑这个方法接收的参数的情况下,你可以采用如下方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SGSsfdsW-1585930917393)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930672995.png)]

gasEstimate会告诉我们运行这样一个调用需要消耗的具体gas数量。但这只是一个估计的结果,因为在图灵完备的EVM上,通常不会存在那种每次调用所产生的gas消耗都完全相同的合约和函数。即使是生产环境中代码,每一次调用过程中它的执行路径也会不同,这将导致完全不同的gas开销。然而,大多数函数都不会乱来,多数情况下,estimateGas的估算还是八九不离十的。

为了获取当前网络中的gas价格,可以执行如下代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irTw7d4W-1585930917394)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930744856.png)]

然后通过这些数据,可以估算一个调用的gas开销;

var gasCostInEther=web3.fromWei((gasEstimate * gasPrice), ‘ether’);我们尝试用这样的方法来计算Faucet这个例子的gas开销,使用本书代码库(http://bit.ly/2zf0SIO)中的代码。我们启动Truffle开发环境,执行gas_estimates.js这个JavaScript文件,它包括如下内容。

代码7-5:gas_estimates.js:使用estimateGas函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1wVxl83L-1585930917396)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930807456.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0D5Ha3i-1585930917397)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930822986.png)]

执行之后,Truffle的开发环境控制台会显示如下信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sSBe2Vd5-1585930917398)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585930843672.png)]

建议在编写智能合约的过程中,把gas的开销评估作为其中的一个环节,这样可以避免合约部署到主网后发生高额gas开销这些之前可能没有想到的问题

917396)]

[外链图片转存中…(img-x0D5Ha3i-1585930917397)]

执行之后,Truffle的开发环境控制台会显示如下信息:

[外链图片转存中…(img-sSBe2Vd5-1585930917398)]

建议在编写智能合约的过程中,把gas的开销评估作为其中的一个环节,这样可以避免合约部署到主网后发生高额gas开销这些之前可能没有想到的问题

你可能感兴趣的:(GO语言和区块链)