区块链研究实验室|以太坊智能合约的时间敏感测试

  • 时间:
  • 浏览:65
  • 来源:区块链技术网

依赖于真实时间来测试以太坊智能合约是非常困难。在本教程中,我们将详细介绍构建测试基础架构,以使用Solidity,Ganache和Truffle支持智能合约的时间敏感测试。

我们将探讨与时间相关的代码测试以及可能不为人所知的方法和工具。这些方法可以帮助开发人员社区编写更强大的合同和更多的单元测试,希望我们能够共同避免对DAO和Parity Wallet Bug进行类似的攻击。

 

测试软件版本:

1、Truffle v5.0.5 (core: 5.0.5)
2、Solidity v0.5.0 (solc-js)
3、Node v11.10.0
4、Ganache CLI v6.3.0 (ganache-core: 2.4.0)

 

时间设置

 

要介绍的第一个功能是在Ganache CLI中设置启动时间,该功能于2018年10月添加,是6.2.0版本的一部分。

 

ganache-cli — help

 -t, — time Date (ISO 8601) that the first block should start. Use this feature, along with the
 evm_increaseTime method to test time-dependent code. [string]

示例(设置时间)

启动客户端,日期设置为2019年2月15日15:53:00 UTC。

$ ganache-cli --time ‘2019-02-15T15:53:00+00:00’

在另一个设备中启动控制台,它将自动与客户端连接。

$ truffle console
truffle(development)> blockNum = await web3.eth.getBlockNumber()
undefined
truffle(development)> blockNum
0
truffle(development)> block = await web3.eth.getBlock(blockNum)
undefined
truffle(development)> block[‘timestamp’]
1550245980

将unix时间戳转换为日期字符串。

$ date -u -r “1550245980
Fri Feb 15 15:53:00 UTC 2019

日期时间戳是2019年2月15日 - 使用--time标志传递到ganache-cli的日期。

示例(不要设置时间)

在终端中启动客户端,但这次不设置时间。

 

$ ganache-cli

在另一个终端中启动控制台,它将自动与客户端连接。

$ truffle console
truffle(development)> blockNum = await web3.eth.getBlockNumber()
undefined
truffle(development)> blockNum
0
truffle(development)> block = await web3.eth.getBlock(blockNum)
undefined
truffle(development)> block[‘timestamp’]
1552252405

将unix时间戳转换为日期字符串。

$ date -u -r “1552252405
Sun Mar 10 21:13:25 UTC 2019

日期时间戳是创建示例的日期:2019年3月10日。

示例合约和代码测试

通过固定起始块时间,可以编写在特定时间之前和之后运行功能的测试。下面是一个示例TimeContract以及用于演示此操作的测试。

pragma solidity ^0.5.0;

contract TimeContract {
  uint256 private startTime;

  constructor(uint256 newStartTime) public {
    startTime = newStartTime;
  }

  /**
  * isNowAfter will return true if now is after the given start time
  */

  function isNowAfter() external view returns (bool){
      return (now >= startTime);
  }
}

TimeContract.sol 

 

const TimeContract = artifacts.require('./TimeContract');

const Sun_Feb_10_00_00_00_UTC_2019 = 1549756800;
const Wed_Mar_20_00_00_00_UTC_2019 = 1553040000;

contract('TimeContract'async (accounts) =>  {
    before('deploy TimeContract'async() => {
        instance_1 = await TimeContract.new(Sun_Feb_10_00_00_00_UTC_2019);
        instance_2 = await TimeContract.new(Wed_Mar_20_00_00_00_UTC_2019);
    });

    it("Sun Feb 10 00:00:00 UTC 2019 (before current time)"async() => {
        var output = await instance_1.isNowAfter.call();
        assert.equal(output, true"output should have been true");
    });

    it("Wed Mar 20 00:00:00 UTC 2019 (after current time)"async() => {
        var output = await instance_2.isNowAfter.call();
        assert.equal(output, false"output should have been false");
    });
});

TestTimeContract.js

第一个测试断言当前时间是在2019年2月10日,即2019 00:00:00 UTC之后。

第二次测试断言当前时间是在2019年3月20日星期三00:00:00 UTC之后。

测试结果

通过将当前时间设置为2019年2月15日星期五15:53:00 UTC来启动ganache客户端。

 

$ ganache-cli --time 2019-02-15T15:53:00+00:00

以下是测试的输出:

$ truffle test
Compiling ./contracts/Migrations.sol…
Compiling ./contracts/TimeContract.sol…

Contract: TimeContract
  Sun Feb 10 00:00:00 UTC 2019 (before current time)
  Wed Mar 20 00:00:00 UTC 2019 (after current time)
2 passing (181ms)

在这里可以看出,上面的断言确实是正确的。

跳跃时间

 

讨论的第二个特征是跳跃时间。

ganache EVM中有JSON RPC API方法,可以模拟时间事件:

 

方法

 

+-----------------------------+-----------------------------+
|           Action            |      Ganache EVM Name       |
+-----------------------------+-----------------------------+
|
 Advance the time            | evm_increaseTime            |
| Advance the block(s)        | evm_mine                    |
|
 Advance both time and block | evm_increaseTime + evm_mine |
| Save time                   | evm_snapshot                |
|
 Revert time                 | evm_revert                  |
+-----------------------------+-----------------------------+

看一下下面的代码片段,它包含测试帮助器方法的上述方法:

 

advanceTime = (time) => {
  return new Promise((resolve, reject) => {
    web3.currentProvider.send({
      jsonrpc: '2.0',
      method: 'evm_increaseTime',
      params: [time],
      id: new Date().getTime()
    }, (err, result) => {
      if (err) { return reject(err) }
      return resolve(result)
    })
  })
}

advanceBlock = () =>
 {
  return new Promise((resolve, reject) => {
    web3.currentProvider.send({
      jsonrpc: '2.0',
      method: 'evm_mine',
      id: new Date().getTime()
    }, (err, result) => {
      if (err) { return reject(err) }
      const newBlockHash = web3.eth.getBlock('latest').hash

      return resolve(newBlockHash)
    })
  })
}

takeSnapshot = () =>
 {
  return new Promise((resolve, reject) => {
    web3.currentProvider.send({
      jsonrpc: '2.0',
      method: 'evm_snapshot',
      id: new Date().getTime()
    }, (err, snapshotId) => {
      if (err) { return reject(err) }
      return resolve(snapshotId)
    })
  })
}

revertToSnapShot = (id) =>
 {
  return new Promise((resolve, reject) => {
    web3.currentProvider.send({
      jsonrpc: '2.0',
      method: 'evm_revert',
      params: [id],
      id: new Date().getTime()
    }, (err, result) => {
      if (err) { return reject(err) }
      return resolve(result)
    })
  })
}

advanceTimeAndBlock = async (time) => {
  await advanceTime(time)
  await advanceBlock()
  return Promise.resolve(web3.eth.getBlock('latest'))
}

module.exports = {
  advanceTime,
  advanceBlock,
  advanceTimeAndBlock,
  takeSnapshot,
  revertToSnapShot
}

utils.js

该片段是Andy Watt的代码扩展,来自本文开头提到的,增加了两个新方法。

请调用advanceTimeAndBlock(<time>)或advanceTime(<time>),以及要前进的秒数(<time>)。这些方法使用JSON RPC evm_increaseTime和evm_mine方法。

例如,要向前移动10天,请按以下方式使用utils.js文件:

//86400 seconds in a day
advancement = 86400 * 10 // 10 Days
await helper.advanceTimeAndBlock(advancement)

以下是更新的测试代码:

const TimeContract = artifacts.require('./TimeContract');
const helper = require('./utils/utils.js');

const Sun_Feb_10_00_00_00_UTC_2019 = 1549756800;
const Wed_Mar_20_00_00_00_UTC_2019 = 1553040000;
const SECONDS_IN_DAY = 86400;

contract('TimeContract'async (accounts) =>  {
    before('deploy TimeContract'async() => {
        instance_1 = await TimeContract.new(Sun_Feb_10_00_00_00_UTC_2019);
        instance_2 = await TimeContract.new(Wed_Mar_20_00_00_00_UTC_2019);
    });

    it("Sun Feb 10 00:00:00 UTC 2019 (before current time)"async() => {
        var output = await instance_1.isNowAfter.call();
        assert.equal(output, true"output should have been true");
    });

    it("Wed Mar 20 00:00:00 UTC 2019 (after current time)"async() => {
        var output = await instance_2.isNowAfter.call();
        assert.equal(output, false"output should have been false");
    });

    it("Wed Mar 20 00:00:00 UTC 2019 (after current time)"async() => {
        await helper.advanceTimeAndBlock(SECONDS_IN_DAY * 100); //advance 100 days
        var output = await instance_2.isNowAfter.call();
        assert.equal(output, true"output should have been true");
    });
});

TestTimeContract.js

 

在这里,最后一次测试将时间向前移动并断言'now'是在instance_2的日期之后。

这是实际应用的测试:

 

$ truffle test
Compiling ./contracts/Migrations.sol…
Compiling ./contracts/TimeContract.sol…
Contract: TimeContract
  Sun Feb 10 00:00:00 UTC 2019 (before current time) (39ms)
  Wed Mar 20 00:00:00 UTC 2019 (after current time)
  Wed Mar 20 00:00:00 UTC 2019 (after current time) (48ms)
3 passing (249ms)

可以看出断言是正确的。

但是,如果再次运行测试,则其中一个测试失败:

 

$ truffle test
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/TimeContract.sol...


  Contract: TimeContract
     Sun Feb 10 00:00:00 UTC 2019 (before current time)
    1) Wed Mar 20 00:00:00 UTC 2019 (after current time)
    > No events were emitted
     Wed Mar 20 00:00:00 UTC 2019 (after current time) (40ms)


  2 passing (237ms)
  1 failing

  1) Contract: TimeContract
       Wed Mar 20 00:00:00 UTC 2019 (after current time):

      output should have been false
      + expected - actual

      -true
      +false

      at Context.it (TestTimeContract.js:22:16)
      at processTicksAndRejections (internal/process/next_tick.js:81:5)

 

这是为什么?

在之前的测试运行中,时间向前移动了100天。本地区块链及时记录了进展情况,并从我们的开始日期起保持在+100天。因此,下次运行测试时,它们将失败,因为第二次测试运行的开始日期将从上一次测试运行的结束日期开始。为了使这些测试再次成功运行,需要使用--time标志重新启动Ganache CLI,其初始日期如上所述:2019-02-15t15:53:00+00:00。

 

向后跳

当向后跳时,首先要捕捉当前时间,以便知道向后跳到哪里。TakeSnapshot()和RevertToSnapshot(ID)方法就是这样做的。这些方法使用json rpc evm_snapshot和evm_revert方法。

例如,要获取快照并捕获ID,请使用utils.js文件,方法如下:

snapShot = await helper.takeSnapshot()
snapshotId = snapShot[‘result’]


要使用捕获的ID还原快照,请使用:

await helper.revertToSnapShot(snapshotId)

以下是更新的测试代码:

const TimeContract = artifacts.require('./TimeContract');
const helper = require('./utils/utils.js');

const Sun_Feb_10_00_00_00_UTC_2019 = 1549756800;
const Wed_Mar_20_00_00_00_UTC_2019 = 1553040000;
const SECONDS_IN_DAY = 86400;

contract('TimeContract'async (accounts) =>  {
    before('deploy TimeContract'async() => {
        instance_1 = await TimeContract.new(Sun_Feb_10_00_00_00_UTC_2019);
        instance_2 = await TimeContract.new(Wed_Mar_20_00_00_00_UTC_2019);
    });

    beforeEach(async() => {
        snapShot = await helper.takeSnapshot();
        snapshotId = snapShot['result'];
    });
    afterEach(async() => {
        await helper.revertToSnapShot(snapshotId);
    });

    it("Sun Feb 10 00:00:00 UTC 2019 (before current time)"async() => {
        var output = await instance_1.isNowAfter.call();
        assert.equal(output, true"output should have been true");
    });

    it("Wed Mar 20 00:00:00 UTC 2019 (after current time)"async() => {
        var output = await instance_2.isNowAfter.call();
        assert.equal(output, false"output should have been false");
    });

    it("Wed Mar 20 00:00:00 UTC 2019 (after current time)"async() => {
        await helper.advanceTimeAndBlock(SECONDS_IN_DAY * 100); //advance 100 days
        var output = await instance_2.isNowAfter.call();
        assert.equal(output, true"output should have been true");
    });
});

TestTimeContract.js

 

这可确保在运行任何测试之前保存ID。在每次测试之后,使用ID调用revert以返回到运行测试之前的状态。

 

输出与“Jumping Forward”中的输出没有区别,但测试可以反复运行而不会失败。

在查看ganache-cli的输出时,请注意EVM命令,因为它保存,向前移动,并在时间上向后移动。

 

evm_snapshot
Saved snapshot #2
evm_increaseTime
evm_mine
eth_getBlockByNumber
eth_getBlockByNumber
eth_getBlockByNumber
net_version
eth_call
evm_revert
Reverting to snapshot #2

结论

通过引入操作时间的功能,可以单独测试依赖于时间的代码。引入无状态性确保了测试运行之间的等幂性,从而使区块链的测试开发变得更加容易。希望这里的演示有用,并使社区能够编写更安全、更可靠的合同。如果您有任何问题或意见,请在下面分享;欢迎合作,并鼓励流动性。

猜你喜欢

如何在本地以太坊测试网络hardhat中使用pancakeswap?

笔者将pancake前端工程pancake-frontend[4]配置成本地的hardhat[5]测试网环境,方便大家一起学习

2022-01-11

Elemon--月亮骑士实验室孵化的第一款游戏

Elemon是由Investor Spotlight(月亮骑士实验室)孵化的第一款游戏。

2022-01-07

一文说透以太坊上TVL最大的二层网络:Arbitrum

截至今日最新数据统计,以太坊扩容网络Arbitrum作为以太坊上TVL最大的二层网络,锁仓价值达到2.74亿美元。且在去年九月,Arbitrum宣布已部署以太坊主网测试Arbitrum One,超过250个团队申请接入开发者测试网。

2022-01-06

区块链红利吃饱后,这个巨头又想"征服"元宇宙?

据12月26日消息,百度与英伟达(NVIDIA)已达成协议,双方合作共建AI元宇宙。另外,在今日举行的百度AI开发者大会上,英伟达全球副总裁暨亚太区总裁 Raymond Teh将受邀出席,并发表主题演讲。

2021-12-27

以太坊伦敦升级后随之生效的以太坊EIP-1559是什么?

如何降低 ETH 手续费成为亟待解决的问题,除了 Layer 2 等解决方案, EIP-1559也引起行业关注。那么EIP 1559 究竟是什么,以及有什么用?让我们一探究竟!

2021-12-24