区块链研究实验室|truffle-编写精准时间依赖测试

IP归属:

当你在以太坊上搭建一个工资类型的Dapp时,编写精确的时间依赖性测试是必要的。

在开始之前,你需要搭建的环境如下:

 

  • 您的测试框架是Truffle、Ganache+Mocha。

  • 测试尽可能精确(<5秒误差范围)。

在为以太坊智能合约编写的测试中操纵时间会带来一些问题。

 

陷阱#1:RPC

虽然在Ganache中完全可以把时间向前和向后跳跃,但有多种方法可以实现这一目标。要获得准确的结果,必须按以下方式使用evm_mine RPC方法:

 

{
  "jsonrpc""2.0",
  "method""evm_mine",
  "params": ["NUMBER_OF_SECONDS"],
  "id"1
}

advanceBlockAtTime.json

也作为javascript的许可:

const advanceBlockAtTime = (time) => {
  return new Promise((resolve, reject) => {
    web3.currentProvider.send(
      {
        jsonrpc: "2.0",
        method: "evm_mine",
        params: [time],
        id: new Date().getTime(),
      },
      (err, _) => {
        if (err) {
          return reject(err);
        }
        const newBlockHash = web3.eth.getBlock("latest").hash;

        return resolve(newBlockHash);
      },
    );
  });
};

advanceBlockAtTime.js 

笔记:

  • 如果没有NUMBER_OF_SECONDS参数,则RPC调用仅增加块高度,但不会及时跳转。

  • “id”参数是可选的,但很好。你在测试时把什么值放进去并不重要。还有evm_increaseTime,它增加了Ganache的“内部时钟”,这样无论何时挖掘下一个区块,它都有一个时间戳偏移量。这增加了开销:

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

advanceTimeAndBlock.js 

 

没错,你必须进行两次RPC调用,而第一种方法只需要一次。


Jakub Wojciechowski提出道具,为ganache-core提供PR#13。如果没有确定性和原子性的方法在Ganache中及时跳跃,那么编写准确的测试将会很困难。

陷阱#2:运行时间(run time)

代码本身需要时间来执行。具体来说,Javascript许可需要花费不可忽略的时间来解决问题。

这很明显,对吧?当为以太坊智能合约编写依赖时间的测试时,事情变得很微妙。

考虑以下内容:

  1. 测试用例运行所需的时间

  2. 您希望将来跳转的秒数

  3. 在控制台中提交yarn run test时的unix时间戳

取决于这些变量,您的测试块可能会在一秒或多秒的过程中被捕获,因此您的断言可能会中断。

例如:

 

describe("when the stream did start but not end"function() {
  beforeEach(async function() {
    await advanceBlockAtTime(
      now
        .plus(STANDARD_TIME_OFFSET)
        .plus(5)
        .toNumber(),
    );
  });

  describe("when the withdrawal amount is within the available balance"function() {
    const amount = new BigNumber(5).multipliedBy(1e18).toString(10);

    it("makes the withdrawal"async function() {
      const balance = await this.token.balanceOf(recipient);
      await this.sablier.withdraw(streamId, amount, opts);
      const newBalance = await this.token.balanceOf(recipient);
      balance.should.be.bignumber.equal(newBalance.minus(amount));
    });
  });
});

makeWithdrawal.js 

 

这样做是因为它要求Sablier合同撤回以前存入的余额。ERC-1620中规定调用者可以退出多少的规则。


我在区块之前记录了mocha的unix时间戳,我测量了使用节点的性能时序api运行测试所需的时间:

t0 1565455128964
Call to sablier.withdraw took 115.92673601210117 milliseconds.
            1) makes the withdrawal

sablierWithdrawTest.txt 

如果您将115加上156545128964,则最终得到一个以9079结尾的数字,因此秒数从8增加到9。这就是打破这个断言的原因,因为我期望X的余额,当我实际得到X+1时。(超过秒数=更多钱)

 

虽然编写一个可以计算另一个程序P2完成所需时间的程序P1是不可能的(参见图灵的暂停问题),但我们可以安全地假设你的beforeEach和它之间的阻塞时间不应超过1秒。这假设您的节点实例和ganache之间的来回通信几乎是即时的,即使在运行覆盖时也是如此。

这是修复:

 

balance.should.bignumber.satisfy(function(num{
  return (
    num.isEqualTo(newBalance.minus(amount)) || num.isEqualTo(newBalance.minus(amount).plus(ONE_UNIT))
  );
});

makeWithdrawalFix.js 

 

其中ONE_UNIT是每秒分配的一个货币单位,根据Sablier模型。它不完美,但比使用“大于”或“小于”平等检查更好。

最后,正如OpenZeppelin团队在此论证的那样,您可能不需要这种精确度。如果你的dapp不直接涉及时间戳或区块数,那么容忍更大的时间偏移是完全正常的。

陷阱#3:BeforeEach和AfterEach

您的里程可能会有所不同,但您可能希望在“beforeEach”中向前跳跃并在“afterEach”中向后跳跃。这是因为您的合同可能在“describe”块的范围内定义了一些变量,并且您希望运行一系列“it”块,这些块都采用相同的状态。不回复“afterEach”只会永远增加时间戳。

例如:

 

describe("when the stream did start but not end"function() {
  const amount = new BigNumber(5).multipliedBy(1e18).toString(10);

  beforeEach(async function() {
    await web3.utils.advanceBlockAtTime(
      now
        .plus(STANDARD_TIME_OFFSET)
        .plus(5)
        .toNumber(),
    );
  });

  it("test1"function() {});

  it("test2"function() {});

  it("test3"function() {});

  afterEach(async function() {
    await web3.utils.advanceBlockAtTime(now.toNumber());
  });
});

beforeAfterEach.js 

 

正如您在上面的代码片段中看到的,我们有三个测试,其中我们假设提取的金额为5。在Sabilier的上下文中,在时间15秒内前进将产生15秒的可提取量,因此我们必须在“aftereach”块中返回到原始状态。

陷阱#4:快照

在完成所有测试后返回到原始状态。它可能对CI或其他外部环境有帮助。

takeSnapshot = async () => {
  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 = async (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);
      },
    );
  });
};

snapshotFunctions.js

 

在代码库中定义这些函数,然后将其插入到一个根测试文件中:

 

let snapshot;
let snapshotId;

before(async () => {
  snapshot = await takeSnapshot();
  snapshotId = snapshot.result;
});

after(async () => {
  await revertToSnapshot(snapshotId);
});

truffleSnapshots.js 

 

现在你的区块链将在所有神奇的时间跳跃后恢复到原来的时间戳。

本文中使用的一些零碎内容受到启发或从其他着作中获取,例如Ethan Wessel令人惊叹的“使用truffle测试时间”。 该文章唯一的警告是使用evm_mine和evm_increaseTime,我们在上面解释了为什么这不理想。

此外,这是一个非常好的StackExchange线程,它具有block.timestamp和一些GitHub线程的固有安全性,它们揭示了在Ganache(1和2)中确定性时间跳跃的历史。

 

本文来源:陀螺科技 文章作者:区块链研究实验室
收藏
举报
区块链研究实验室
累计发布内容13篇 累计总热度10万+

陀螺科技现已开放专栏入驻,详情请见入驻指南: https://www.tuoluo.cn/article/detail-27547.html

区块链研究实验室专栏: https://www.tuoluo.cn/columns/author1286336/

本文网址: https://www.tuoluo.cn/article/detail-57392.html

免责声明:
1、本文版权归原作者所有,仅代表作者本人观点,不代表陀螺科技观点或立场。
2、如发现文章、图片等侵权行为,侵权责任将由作者本人承担。

相关文章