学习目标:
- 了解智能合约调试
- 掌握智能合约调试的方法
Truffle >= 4.0
以太坊的智能合约就是代码。与你在其他地方找到的“纸质”合同不同,此合约需要以非常精确的
方式说明问题。
这是一件好事,想象一下如果我们的合约编写不正确,我们的交易会失败,进而导致ether的丢失,更别提浪费的时间和精力了。
幸运的是,Truffle(v4.0)有一个内置的调试器来调试你的代码。所以当发生错误时,你能准确的找出并解决它。
在本教程中,我们将基础合约迁移到测试区块链,向其中引入一些错误,并通过使用内置的Truffle调试器来解决每个问题。
一个基础的智能合约
最基础的智能合约是一个简单的存储合约。
pragma solidity ^0.4.17;
contract SimpleStorage {
uint myVariable;
function set(uint x) public {
myVariable = x;
}
function get() constant public returns (uint) {
return myVariable;
}
}
这个合约做了两件事:
- 允许你给myVariable设置一个整形变量
- 允许你查询变量的值
这是一个十分有趣的合约,但这不是我们的侧重点。我们关心的是的是当出错时会发生什么。
首先,让我们构建我们的环境
部署最基础的智能合约
- 创建一个新目录,我们将在本地安装我们的合约
mkdir simple-storage
cd simple-storage
- 创建一个Truffle项目
truffle init
这将创建诸如contracts /和migrations /的目录,并将它们填充到我们将合同部署到区块链时将使用的文件中。
- 在contracts/目录下使用如下内容创建一个叫做Store.sol的文件
pragma solidity ^0.4.17;
contract SimpleStorage {
uint myVariable;
function set(uint x) public {
myVariable = x;
}
function get() constant public returns (uint) {
return myVariable;
}
}
这是我们将要调试的合同。尽管此文件的全部细节超出了本教程的范围,但请注意,有一个名为SimpleStorage的合同,其中包含数值变量myVariable和两个函数:set()和get():第一个函数将值存储在该变量中,而第二个函数查询存储的值。
- 在migrations/目录,使用下面的内容创建一个名为2_deployed_contract.js的文件
var SimpleStorage = artifacts.require("SimpleStorage");
module.exports = function(deployer) {
deployer.deploy(SimpleStorage);
};
这个文件是允许我们将SimpleStorage合约部署到区块链上的指令。
- 在终端上,编译智能合约
truffle compile
- 打开第二个终端并运行truffle develop命令来直接构建一个开发区块链到Truffle,以便我们用来测试我们的合约:
truffle develop
控制台将显示truffle(develop)>的提示.从这开始,除非特别指定,所有的命令将在此提示符下键入
- 随着控制台的启动和运行,我们现在能通过运行我们的migrations来将合约部署到区块链上:
migrate
响应应该如下所示,但是具体ID会有所不同:
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x16b77c30bc039f85add166ddb7d4bf8dbdd8a4871ff9982bffa46ac149d0660a
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying ConvertLib...
... 0x55d9fb704c8b7cd327b363e98cda6236b421812ea46dbc13de97bd50d7870054
ConvertLib: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Linking ConvertLib to MetaCoin
Deploying MetaCoin...
... 0xc39eee15828b0e1c1c6a6528d01a5a1b727eed7f0cd5af50970bafc737227a71
MetaCoin: 0xf25186b5081ff5ce73482ad761db0eb0d25abfbf
Saving successful migration to network...
... 0x059cf1bbc372b9348ce487de910358801bbbd1c89182853439bec0afaee6c7db
Saving artifacts...
与基础的智能合约进行交互
现在,智能合约已经通过truffle develop 部署到测试网络了,truffle develop启动了一个基于Ganache的控制台,这是一种内置于Truffle中的本地开发区块链。
我们接下来想通过和智能合约交互来看当正确运行时它是怎么工作的.我们将使用truffle develop控制台.
注意:如果您想知道为什么我们不需要挖矿来获取交易以确保交易安全,Truffle开发控制台已经为我们处理了这个问题。如果使用不同的网络,则需要确保您通过区块链获取交易。
- 在truffle develop运行的终端,执行如下命令:
SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});
该命令使用SimpleStorage合同,然后调用其中定义的get()函数。然后将它返回的字符串输出转换为数字:
0
这表明我们的变量myVariable被设置为0,即使我们尚未将此变量设置为任何值。这是因为具有整数类型的变量会自动使用Solidity中的零值填充.
- 现在,让我们在我们的合约上执行一笔交易.我们将通过set()函数完成这步操作,通过set()函数我们能将变量设置成一些整数.运行如下命令:
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
这将变量设置为4.输出显示交易相关的一些信息,包括交易ID(hash),交易接收以及交易过程中触发的所有事件日志:
{ tx: '0x8a7d3343dd2aaa0438157faae678ca57cc6485825bb4ed2ebefe90609dd268ce',
receipt:
{ transactionHash: '0x8a7d3343dd2aaa0438157faae678ca57cc6485825bb4ed2ebefe90609dd268ce',
transactionIndex: 0,
blockHash: '0x2be1f2abd7c30f7b06dc6b9f3293d02a443ec37c136cdf25a7b50b81062249c0',
blockNumber: 5,
gasUsed: 42487,
cumulativeGasUsed: 42487,
contractAddress: null,
logs: [ [Object] ],
status: '0x01',
logsBloom: '0x00000000000000000000000000000000000000000000000002000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000010000000000000' },
logs:
[ { logIndex: 0,
transactionIndex: 0,
transactionHash: '0x8a7d3343dd2aaa0438157faae678ca57cc6485825bb4ed2ebefe90609dd268ce',
blockHash: '0x2be1f2abd7c30f7b06dc6b9f3293d02a443ec37c136cdf25a7b50b81062249c0',
blockNumber: 5,
address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
type: 'mined',
event: 'Odd',
args: {} } ] }
对我们来说最重要的是交易ID(这里列出的tr和transactionHash).在我们调试的时候我们需要复制改值。
- 为了验证这个变量的值已经改变,我们再次执行get()函数:
SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});
输出如下:
4
调试错误
上面显示了合约应该如何工作。现在,我们将向合约中引入一些小错误并重新部署。我们将看到问题本身如何呈现,并使用Truffle的内置调试功能来解决问题。
我们将看看以下问题:
无限循环
无效的错误检查
没有错误,但功能没有按照需要运行
问题(一):无限循环
在以太坊区块链中,不能将交易设置为永久运行。交易可以运行直到达到gas限制。一旦发生这种情况,交易将出错,并且将返回“out of gas”错误。由于gas的价格在以太网中,这可能会对现实世界产生影响。所以修复一个“out of gas”的错误至关重要.
错误介绍
- 用编辑器打开contracts/目录下的Store.sol文件.
- 用下面内容替代set()函数:
function set(uint x) public {
while(true) {
myVariable = x;
}
}
由于while(true)条件,这个语句永远不会终止
测试合约
Truffle开发控制台能够迁移更新的合同,而无需退出并重新启动控制台。由于迁移命令可以一步完成编译和迁移,因此我们可以一步重置我们的区块链合约。
- 在Truffle开发控制台,更新合约:
migrate --reset
你将看到编译和迁移输出
- 为了便于查找错误,我们将打开另一个日志记录控制台。例如,这可以让我们在交易失败时查看交易ID。在另一个终端窗口中,运行以下命令:
truffle develop --log
现在离开当前窗口,回到第一个控制台
- 现在,我们准备执行交易.运行set()函数:
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
一个错误将显示出来:
Error: VM Exception while processing transaction: out of gas
而且,在有日志的控制台中,你将看到更多信息:
develop:testrpc eth_sendTransaction +0ms
develop:testrpc +1s
develop:testrpc Transaction: 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f +2ms
develop:testrpc Gas usage: 4712388 +11ms
develop:testrpc Block Number: 6 +15ms
develop:testrpc Runtime Error: out of gas +0ms
develop:testrpc +16ms
由于我们的失败和交易ID,现在我们可以调试交易了.
调试问题
Truffle包含一个内置的调试器。启动此命令的命令是从Truffle Develop控制台debug <交易ID>,或者从终端truffle debug <交易ID>.现在开始吧.
- 在Truffle Develop控制台中,从日志控制台复制交易ID并将其作为参数粘贴到debug命令中:
debug 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f
你将看到如下输出:
Gathering transaction data...
Addresses affected:
0x377bbcae5327695b32a1784e0e13bedc8e078c9c - SimpleStorage
Commands:
(enter) last command entered (step next)
(o) step over, (i) step into, (u) step out, (n) step next
(;) step instruction, (p) print instruction, (h) print this help, (q) quit
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
1: pragma solidity ^0.4.17;
2:
3: contract SimpleStorage {
^^^^^^^^^^^^^^^^^^^^^^^
debug(develop:0xe4933407...)>
这是一个交互式控制台。您可以使用列出的命令以不同的方式与代码交互。
- 与代码交互的最常见方式是“下一步”,该步骤一次执行代码一条指令。按Enter或n来执行此操作:
输出如下:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
4: uint myVariable;
5:
6: function set(uint x) public {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
请注意,该程序已转到位于第6行的下一条指令。(该指示符指向发生指令的确切部分。)
- 再次按Enter键跳到下一条指令:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
5:
6: function set(uint x) public {
7: while(true) {
^^^^^^^^^^^^
- 一直按Enter
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
5:
6: function set(uint x) public {
7: while(true) {
^^^^
debug(develop:0xe4933407...)>
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
5:
6: function set(uint x) public {
7: while(true) {
^^^^^^^^^^^^
debug(develop:0xe4933407...)>
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
6: function set(uint x) public {
7: while(true) {
8: myVariable = x;
^
debug(develop:0xe4933407...)>
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
6: function set(uint x) public {
7: while(true) {
8: myVariable = x;
^^^^^^^^^^
debug(develop:0xe4933407...)>
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
6: function set(uint x) public {
7: while(true) {
8: myVariable = x;
^^^^^^^^^^^^^^
debug(develop:0xe4933407...)>
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
5:
6: function set(uint x) public {
7: while(true) {
^^^^^^^^^^^^
请注意,这些步骤最终会重复。事实上,反复按下回车键将永远重复这些交易(或者至少在交易用完之前)。这会告诉你问题在哪里。
- 按q退出调试器
案例(二):一个无效的错误检查
智能合约可以使用像assert()这样的语句来确保满足某些条件。但这也可能会与合约状态以不可调和的方式发生冲突。
这里我们将介绍这样一个条件,然后看看调试器如何找到它。
错误介绍
- 打开Store.sol
- 将set()函数替换为以下内容:
function set(uint x) public {
assert(x == 0);
myVariable = x;
}
这与原始版本相同,但添加了assert()函数,进行测试以确保x == 0.当我们将该变量设置为其他值,我们就会遇到问题。
测试合约
和之前一样,我们将重置合约.
- 在Truffle开发控制台,将合约重置为初始部署状态:
migrate --reset
- 现在我们准备测试新的交易了.运行和上面相同的命令:
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
你将看到一个错误:
Error: VM Exception while processing transaction: invalid opcode
这意味着我们的操作有问题。
调试错误
- 复制交易ID并将其用作debug命令的参数
debug 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f
现在,我们回到调试器
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
1: pragma solidity ^0.4.17;
2:
3: contract SimpleStorage {
^^^^^^^^^^^^^^^^^^^^^^^
debug(develop:0xe4933407...)>
- 按Enter键几次以逐句通过代码。最终,调试器将停止并显示错误消息:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
5:
6: function set(uint x) public {
7: assert(x == 0);
^^^^^^^^^^^^^^
debug(develop:0x7e060037...)>
Transaction halted with a RUNTIME ERROR.
This is likely due to an intentional halting expression, like
assert(), require() or revert(). It can also be due to out-of-gas
exceptions. Please inspect your transaction parameters and
contract code to determine the meaning of this error.
这是触发错误的最后一个事件。你可以看到它是assert()引发的错误。
案例(三):功能没有按需运行
有时候,错误并不是真正的错误,因为它不会在运行时造成问题,而只是做一些你不打算做的事情。
以一个事件为例,如果我们的变量很奇怪,另一个事件会在我们的变量是偶数时运行。如果我们意外地交换了这个条件以便相反的函数能够运行,那么它不会导致错误;不过,合同会出乎意料。
再次,我们可以使用调试器来查看出错的地方。
错误介绍
- 再次打开Store.sol文件
- 将set()函数替换为以下内容:
event Odd();
event Even();
function set(uint x) public {
myVariable = x;
if (x % 2 == 0) {
Odd();
} else {
Even();
}
}
该代码引入了两个虚拟事件,Odd()和Even(),它们是基于一个条件触发的,该条件检查x是否可以被2整除。
但请注意,我们已将结果反转过来。如果x可以被2整除,Odd()事件将会运行。
测试合约
开始之前,我们同样重置区块链上的合约
- 在Truffle开发控制台,重置合约
migrate --reset
你将看到编译和迁移的输出
- 现在我们准备测试新的交易。运行与上面相同的命令:
SimpleStorage.deployed().then(function(instance){return instance.set(4);});
注意:这里并没有错误,如期返回交易ID及其详情:
{ tx: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42',
receipt:
{ transactionHash: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42',
transactionIndex: 0,
blockHash: '0x08d7c35904e4a93298ed5be862227fcf18383fec374759202cf9e513b390956f',
blockNumber: 5,
gasUsed: 42404,
cumulativeGasUsed: 42404,
contractAddress: null,
logs: [ [Object] ] },
logs:
[ { logIndex: 0,
transactionIndex: 0,
transactionHash: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42',
blockHash: '0x08d7c35904e4a93298ed5be862227fcf18383fec374759202cf9e513b390956f',
blockNumber: 5,
address: '0x377bbcae5327695b32a1784e0e13bedc8e078c9c',
type: 'mined',
event: 'Odd',
args: {} } ] }
但请注意交易记录显示事件Odd。这是错误的,所以我们的工作是找出为什么被调用。
测试合约
- 复制该事务ID并将其用作debug命令的参数:
debug 0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42
- 按Enter键多次循环执行步骤。最终你会看到是什么原因导致了Odd()事件:
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
10: function set(uint x) public {
11: myVariable = x;
12: if (x % 2 == 0) {
^^^^^^^^^^^^^^^^
debug(develop:0x7f799ad5...)>
Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
11: myVariable = x;
12: if (x % 2 == 0) {
13: Odd();
^^^^^
debug(develop:0x7f799ad5...)>
问题很明显。是条件语句出了问题。
总结
凭借在Truffle中直接调试合约的能力,你可以使你的智能合约变得坚如磐石,随时可以部署。