• 智能合约调试指南


     

    不像你在其他地方看到的纸质合约,以太坊的智能合约是代码组成的,需要你以非常谨慎的态度去对待它。

    (这是一件好事,想象下如果现实世界的合同需要编译的话会更清晰么?)

    如果我们的合同没有被正确的编码出来, 我们的交易可能会失败,导致以太币的损失(以 gas 的形式),更不用说浪费时间和精力。

    幸运的是,Truffle (版本 4 以上) 内置了逐步调试的功能,所以一旦发生错误,你可以很快发现并修复它。

    在本教程中,我们将在测试的区块链环境中部署一个基础的合同,并引入一些错误,通过 Truffle 内置调试器修复它们。

    一个基础的智能合约

    一个最基础的合同是一个简单的存储类型的智能合约。(这个例子改编自 Solidity documentation)

    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)为特定整数值。
    • 允许你查询一个选定的值。

    这不是一个非常有趣的合约,但是这不是重点。我们想看看出错后会发生什么当事情。

    首先我们配置环境。

    部署智能合约

    1. 首先为我们的合约创建一个新的本地目录:
    mkdir simple-storage
    cd simple-storage

    2. 创建一个空的 Truffle 项目

    truffle init

    这个命令将创建目录,比如contracts/和migrations/,并生成一些文件用于帮助部署合约到区块链。

    3.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()。第一个函数设置变量内容,第二个获取变量。

    4. migrations/ 目录下,有个 2_deploy_contracts.js 文件。

    var SimpleStorage = artifacts.require("SimpleStorage");
    module.exports = function(deployer) {
      deployer.deploy(SimpleStorage);
    };

    这个文件是管理部署SimpleStorage合约的。

    5. 在终端中,编译此合约

    truffle compile

    6. 再开一个终端,运行 truffle develop ,开启 truffle 内置的测试区块链,这样我们可以使用它来测试合约。

    truffle develop

    这个命令将出现提示符 truffle(develop)>, 从现在开始,所有的命令都在此提示符下输入(特殊情况会说明)

    7. 当开发控制台运行起来后,我们可以部署我们的合约了。

    migrate

    下面的相应有些类似,除了 id 不同。

    Running migration: 1_initial_migration.js
       Replacing Migrations...
       ... 0xe4f911d95904c808a81f28de1e70a377968608348b627a66efa60077a900fb4c
       Migrations: 0x3ed10fd31b3fbb2c262e6ab074dd3c684b8aa06b
     Saving successful migration to network...
       ... 0x429a40ee574664a48753a33ea0c103fc78c5ca7750961d567d518ff7a31eefda
     Saving artifacts...
     Running migration: 2_deploy_contracts.js
       Replacing SimpleStorage...
       ... 0x6783341ba67d5c0415daa647513771f14cb8a3103cc5c15dab61e86a7ab0cfd2
       SimpleStorage: 0x377bbcae5327695b32a1784e0e13bedc8e078c9c
     Saving successful migration to network...
       ... 0x6e25158c01a403d33079db641cb4d46b6245fd2e9196093d9e5984e45d64a866
     Saving artifacts...

    和基础的智能合约交互

    智能合约通过truffle develop部署到了测试网络中,运行 console 而不是 Ganache,一个内置在 Truffle 的本地区块链。

    1. 在truffle develop运行的终端中,输入如下命令:
    SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});

    这个命令找到 SimpleStorage 合约,然后调用get()命令,通常返回一个字符串并转化为数字。

    0

    myVariable被设置为 0,尽管我们还没给它赋值。这是因为 Solidity 数值型变量会自动被赋值为 0,不像其他语言会是NULL和undefined。

    2. 现在我们运行一条交易命令,调用set()设置我们的变量为其他值。

    SimpleStorage.deployed().then(function(instance){return instance.set(4);});

    设置变量为 4,返回的信息包括交易(交易 id ,交易receipt 和一些交易的时间 log)

    { tx: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42',
       receipt:
    { transactionHash: '0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42',
      transactionIndex: 0,
      blockHash: '0x60adbf0523622dc1be52c627f37644ce0a343c8e7c8955b34c5a592da7d7c651',
      blockNumber: 5,
      gasUsed: 41577,
      cumulativeGasUsed: 41577,
      contractAddress: null,
      logs: [] },
       logs: [] }

    最重要的是交易的 id (在这里是 tx 和 transactionHash)。我们需要赋值这个值用来调试。

    3. 想验证值是否已经改变,运行get()

    SimpleStorage.deployed().then(function(instance){return instance.get.call();}).then(function(value){return value.toNumber()});

    输出

    4

    调试错误

    上文展示了智能合约如何工作,现在我们引入一些错误到合约中。

    针对如下几个错误

    • 无限循环
    • 无效错误检查
    • 无错误,但是函数没有按预期执行

    错误 #1: 无限循环

    在以太坊区块链中,交易不能永远执行下去。

    一个交易会一直执行到 gas 用尽。一旦发生这种情况,交易会返回out of gas错误。

    因为 gas 是以以太币计费的,所以会造成真实的资产损失,所以修复这种错误迫在眉睫。

    引入错误

    1. 在编辑器打开contracts/目录中的Store.sol
    2. 替换set()
    function set(uint x) public {
      while(true) {
        myVariable = x;
      }
    }

    因为while(true)所以函数永远不会退出。

    测试合约

    Truffle 的开发终端不重启就可以重新部署合约。我们可以通过migrate一步编译和部署合约

    1. Truffle 的开发终端中,更新合约
    migrate --reset

    2. 为了更好的捕获错误,我们打开第二个终端

    truffle develop --log

    然后回到之前的终端中

    3. 现在我们可以运行交易了,运行set()命令

    SimpleStorage.deployed().then(function(instance){return instance.set(4);});

    捕获到错误

    Error: VM Exception while processing transaction: out of gas

    在 log 的终端中,我们可以看到更多信息

    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

    通过这些信息,我们可以调试这个交易

    调试错误

    调出 debug 的命令是在 Truffle 开发终端输入debug <Transaction ID>或者直接在终端中输入truffle debug <Transaction ID>,现在让我们开始吧!

    1. 在 Truffle 开发终端中,复制粘贴交易 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...)>

    这是个交互式命令行,你可以用列出的命令和程序交互

    1. 最常用的命令是 `step next`,命令执行一次往下一行代码,快捷键是 `Enter` 或者 `n`
    输出

     Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
        4:   uint myVariable;
        5:
        6: function set(uint x) public {
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    注意程序已经移动到了下一个命令,第六行中。
    2. 键入回车

     Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
    
            5:
            6: function set(uint x) public {
            7:   while(true) {
            ^^^^^^^^^^^^

    3. 不断按回车

     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) {
             ^^^^^^^^^^^^

    请注意,最终步骤会一直重复。事实上,按回车会永远重复那些交易(直到用完 gas )。而这会告诉你问题在哪里。

    错误 #2: 无效错误检查

    智能合约可以用 assert() 来保证必要特定条件会出现。这种和合约状态的冲突是不可调和的。
    引入错误
    现在我们来引入这个错误,看看调试器如何发现它。
    1. 再次打开 `Store.sol`
    2. 替换 `set()` 函数

     function set(uint x) public {
          assert(x == 0);
          myVariable = x;
        }

    和前一个版本一样,只是多了 `assert()` 函数,保证 `x == 0`,如果我们设置 x 为其他值,我们就会发现错误。

    测试合约
    和之前一样,我们重置下合约
    1. migrate --reset
    2. SimpleStorage.deployed().then(function(instance){return instance.set(4);});
    我们会看到如下错误

     Error: VM Exception while processing transaction: invalid opcode

    调试错误
    1. 复制交易 id 到 debug 命令下

    debug 0xe493340792ab92b95ac40e43dca6bc88fba7fd67191989d59ca30f79320e883f

    回到调试器

    Store.sol | 0x377bbcae5327695b32a1784e0e13bedc8e078c9c:
    1: pragma solidity ^0.4.17;
       2:
       3: contract SimpleStorage {
          ^^^^^^^^^^^^^^^^^^^^^^^
    debug(develop:0xe4933407...)>

    1. 键入回车

    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.

    我们可以看到最后的事件中触发了错误.

    错误 #3: 无错误,但是函数没有按预期执行

    有时候,错误不一定是真正的错误,它在运行时间内不会引起问题,只是不会按预期执行。
    举个例子,一个事件将会在变量是奇数的时候执行,而另一个事件在偶数的时候执行。如果我们调换了这个条件,让相反的事件执行了。它斌不会触发错误,然而,合约会不按照我们的预期执行下去。
    我们再次用调试器来找出错误。
    引入错误
    1. 再次打开 `Store.sol`
    2. 替换 `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()` 事件触发了。
    测试合约
    1. migrate --reset
    2. SimpleStorage.deployed().then(function(instance){return instance.set(4);});
    没有错误产生,输出如下

    { 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: {} } ] }

    logs 里面显示调用了 Odd 事件,这是不对的,我们的任务是找到这个事件为什么会被触发。
    调试错误
    1. 复制交易 id 到 debug 命令下

    debug 0x7f799ad56584199db36bd617b77cc1d825ff18714e80da9d2d5a0a9fff5b4d42

    2. 键入回车几次,最后我们将看到调用 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 中的调试能力,你可以编写更健壮的智能合约。

  • 相关阅读:
    复数除法
    base operand of '->' has non-pointer type 'const Comple
    virtual关键字
    & 引用
    const用法
    Iptable与firewalld防火墙
    存储结构与磁盘划分
    Linux系统中用户身份与文件权限
    计时器小程序——由浅入深实例讲解
    ASP.NET编程十大技巧(他人总结)
  • 原文地址:https://www.cnblogs.com/lvdongjie/p/11200758.html
Copyright © 2020-2023  润新知