diff --git a/docs/buidler-evm/README.md b/docs/buidler-evm/README.md index 3d1a201589..aa34464a85 100644 --- a/docs/buidler-evm/README.md +++ b/docs/buidler-evm/README.md @@ -227,9 +227,10 @@ To customise it, take a look at [the configuration section](/config/#buidler-evm #### Special testing/debugging methods - `evm_increaseTime` – same as Ganache. -- `evm_mine` – same as Ganache, except it doesn’t accept a timestamp. +- `evm_mine` – same as Ganache - `evm_revert` – same as Ganache. - `evm_snapshot` – same as Ganache. +- `evm_setNextBlockTimestamp` - set the timestamp to be used for the next block, if next block is mined with a timestamp, this set will be resetted, on the other hand, it is only effective for only 1 next block. ### Unsupported methods diff --git a/packages/buidler-core/src/internal/buidler-evm/provider/input.ts b/packages/buidler-core/src/internal/buidler-evm/provider/input.ts index cb3dae02a4..6117e1486d 100644 --- a/packages/buidler-core/src/internal/buidler-evm/provider/input.ts +++ b/packages/buidler-core/src/internal/buidler-evm/provider/input.ts @@ -316,6 +316,5 @@ export function validateParams(params: any[], ...types: Array>) { decoded.push(result.value); } - return decoded; } diff --git a/packages/buidler-core/src/internal/buidler-evm/provider/modules/evm.ts b/packages/buidler-core/src/internal/buidler-evm/provider/modules/evm.ts index 90034d1aeb..217ae90d4d 100644 --- a/packages/buidler-core/src/internal/buidler-evm/provider/modules/evm.ts +++ b/packages/buidler-core/src/internal/buidler-evm/provider/modules/evm.ts @@ -1,7 +1,11 @@ import { BN } from "ethereumjs-util"; import * as t from "io-ts"; -import { MethodNotFoundError, MethodNotSupportedError } from "../errors"; +import { + InvalidInputError, + MethodNotFoundError, + MethodNotSupportedError +} from "../errors"; import { rpcQuantity, validateParams } from "../input"; import { BuidlerNode } from "../node"; import { numberToRpcQuantity } from "../output"; @@ -19,6 +23,11 @@ export class EvmModule { case "evm_increaseTime": return this._increaseTimeAction(...this._increaseTimeParams(params)); + case "evm_setNextBlockTimestamp": + return this._setNextBlockTimestampAction( + ...this._setNextBlockTimestampParams(params) + ); + case "evm_mine": return this._mineAction(...this._mineParams(params)); @@ -32,6 +41,29 @@ export class EvmModule { throw new MethodNotFoundError(`Method ${method} not found`); } + // evm_setNextBlockTimestamp + + private _setNextBlockTimestampParams(params: any[]): [number] { + return validateParams(params, t.number); + } + + private async _setNextBlockTimestampAction( + timestamp: number + ): Promise { + const latestBlock = await this._node.getLatestBlock(); + const increment = new BN(timestamp).sub( + new BN(latestBlock.header.timestamp) + ); + if (increment.lte(new BN(0))) { + throw new InvalidInputError( + `Timestamp ${timestamp} is lower than previous block's timestamp` + + `${new BN(latestBlock.header.timestamp).toNumber()}` + ); + } + await this._node.setNextBlockTimestamp(new BN(timestamp)); + return timestamp.toString(); + } + // evm_increaseTime private _increaseTimeParams(params: any[]): [number] { @@ -47,12 +79,37 @@ export class EvmModule { // evm_mine - private _mineParams(params: any[]): [] { - return validateParams(params); + private _mineParams(params: any[]): [number] { + if (params.length === 0) { + params.push(0); + } + return validateParams(params, t.number); } - private async _mineAction(): Promise { - await this._node.mineEmptyBlock(); + private async _advanceTimeOffsetAccordingToTimestamp(timestamp: BN) { + const latestBlock = await this._node.getLatestBlock(); + const increment = new BN(timestamp).sub( + new BN(latestBlock.header.timestamp) + ); + if (increment.lte(new BN(0))) { + throw new InvalidInputError( + `Timestamp ${timestamp} is lower than previous block's timestamp` + ); + } + await this._node.increaseTime(increment); + } + + private async _mineAction(timestamp: number): Promise { + if (timestamp !== 0) { + await this._advanceTimeOffsetAccordingToTimestamp(new BN(timestamp)); + } else { + const nextBlockTimestamp = await this._node.getNextBlockTimestamp(); + if (!nextBlockTimestamp.eq(new BN(0))) { + timestamp = nextBlockTimestamp.toNumber(); + await this._advanceTimeOffsetAccordingToTimestamp(new BN(timestamp)); + } + } + await this._node.mineEmptyBlock(new BN(timestamp)); return numberToRpcQuantity(0); } diff --git a/packages/buidler-core/src/internal/buidler-evm/provider/node.ts b/packages/buidler-core/src/internal/buidler-evm/provider/node.ts index d2bb43133a..db5eb81790 100644 --- a/packages/buidler-core/src/internal/buidler-evm/provider/node.ts +++ b/packages/buidler-core/src/internal/buidler-evm/provider/node.ts @@ -132,6 +132,7 @@ interface Snapshot { latestBlock: Block; stateRoot: Buffer; blockTimeOffsetSeconds: BN; + nextBlockTimestamp: BN; transactionByHash: Map; transactionHashToBlockHash: Map; blockHashToTxBlockResults: Map; @@ -239,6 +240,7 @@ export class BuidlerNode extends EventEmitter { private readonly _accountPrivateKeys: Map = new Map(); private _blockTimeOffsetSeconds: BN = new BN(0); + private _nextBlockTimestamp: BN = new BN(0); private _transactionByHash: Map = new Map(); private _transactionHashToBlockHash: Map = new Map(); private _blockHashToTxBlockResults: Map = new Map(); @@ -358,6 +360,7 @@ export class BuidlerNode extends EventEmitter { await this._saveTransactionAsReceived(tx); const block = await this._getNextBlockTemplate(); + const needsTimestampIncrease = await this._timestampClashesWithPreviousBlockOne( block ); @@ -400,6 +403,8 @@ export class BuidlerNode extends EventEmitter { vmTracerError ); + await this._resetNextBlockTimestamp(); + return { trace: vmTrace, block, @@ -409,8 +414,13 @@ export class BuidlerNode extends EventEmitter { }; } - public async mineEmptyBlock() { + public async mineEmptyBlock(timestamp: BN) { const block = await this._getNextBlockTemplate(); + + if (!timestamp.eq(new BN(0))) { + await this._setBlockTimestamp(block, timestamp); + } + const needsTimestampIncrease = await this._timestampClashesWithPreviousBlockOne( block ); @@ -438,6 +448,8 @@ export class BuidlerNode extends EventEmitter { await this._saveBlockAsSuccessfullyRun(block, result); + await this._resetNextBlockTimestamp(); + return result; } catch (error) { // We set the state root to the previous one. This is equivalent to a @@ -653,6 +665,10 @@ export class BuidlerNode extends EventEmitter { return this._stateManager.getContractCode(address); } + public async setNextBlockTimestamp(timestamp: BN) { + this._nextBlockTimestamp = new BN(timestamp); + } + public async increaseTime(increment: BN) { this._blockTimeOffsetSeconds = this._blockTimeOffsetSeconds.add(increment); } @@ -661,6 +677,10 @@ export class BuidlerNode extends EventEmitter { return this._blockTimeOffsetSeconds; } + public async getNextBlockTimestamp(): Promise { + return this._nextBlockTimestamp; + } + public async getSuccessfulTransactionByHash( hash: Buffer ): Promise { @@ -714,6 +734,7 @@ export class BuidlerNode extends EventEmitter { latestBlock: await this.getLatestBlock(), stateRoot: await this._stateManager.getStateRoot(), blockTimeOffsetSeconds: new BN(this._blockTimeOffsetSeconds), + nextBlockTimestamp: new BN(this._nextBlockTimestamp), transactionByHash: new Map(this._transactionByHash.entries()), transactionHashToBlockHash: new Map( this._transactionHashToBlockHash.entries() @@ -757,6 +778,7 @@ export class BuidlerNode extends EventEmitter { this._blockchain.deleteAllFollowingBlocks(snapshot.latestBlock); await this._stateManager.setStateRoot(snapshot.stateRoot); this._blockTimeOffsetSeconds = newOffset; + this._nextBlockTimestamp = snapshot.nextBlockTimestamp; this._transactionByHash = snapshot.transactionByHash; this._transactionHashToBlockHash = snapshot.transactionHashToBlockHash; this._blockHashToTxBlockResults = snapshot.blockHashToTxBlockResults; @@ -1044,7 +1066,7 @@ export class BuidlerNode extends EventEmitter { header: { gasLimit: this._blockGasLimit, nonce: "0x42", - timestamp: await this._getNextBlockTimestamp() + timestamp: await this._getNextUsableBlockTimestamp() } }, { common: this._common } @@ -1062,9 +1084,16 @@ export class BuidlerNode extends EventEmitter { return block; } - private async _getNextBlockTimestamp(): Promise { - const realTimestamp = new BN(getCurrentTimestamp()); - return realTimestamp.add(this._blockTimeOffsetSeconds); + private async _resetNextBlockTimestamp() { + this._nextBlockTimestamp = new BN(0); + } + + private async _getNextUsableBlockTimestamp(): Promise { + if (this._nextBlockTimestamp.eq(new BN(0))) { + const realTimestamp = new BN(getCurrentTimestamp()); + return realTimestamp.add(this._blockTimeOffsetSeconds); + } + return new BN(this._nextBlockTimestamp); } private async _saveTransactionAsReceived(tx: Transaction) { @@ -1238,6 +1267,10 @@ export class BuidlerNode extends EventEmitter { block.header.timestamp = new BN(block.header.timestamp).addn(1); } + private async _setBlockTimestamp(block: Block, timestamp: BN) { + block.header.timestamp = new BN(timestamp); + } + private async _validateTransaction(tx: Transaction) { // Geth throws this error if a tx is sent twice if (await this._transactionWasSuccessful(tx)) { diff --git a/packages/buidler-core/test/internal/buidler-evm/provider/modules/evm.ts b/packages/buidler-core/test/internal/buidler-evm/provider/modules/evm.ts index 5ccd494d49..28acf3d72b 100644 --- a/packages/buidler-core/test/internal/buidler-evm/provider/modules/evm.ts +++ b/packages/buidler-core/test/internal/buidler-evm/provider/modules/evm.ts @@ -1,19 +1,55 @@ import { assert } from "chai"; -import { zeroAddress } from "ethereumjs-util"; +import { + bufferToHex, + privateToAddress, + toBuffer, + zeroAddress +} from "ethereumjs-util"; import { bufferToRpcData, numberToRpcQuantity, RpcBlockOutput } from "../../../../../src/internal/buidler-evm/provider/output"; +import { getCurrentTimestamp } from "../../../../../src/internal/buidler-evm/provider/utils"; import { rpcQuantityToNumber } from "../../../../../src/internal/core/providers/provider-utils"; +import { EthereumProvider } from "../../../../../src/types"; import { assertInvalidArgumentsError, - assertLatestBlockNumber + assertLatestBlockNumber, + assertQuantity } from "../../helpers/assertions"; +import { EXAMPLE_CONTRACT } from "../../helpers/contracts"; import { quantityToNumber } from "../../helpers/conversions"; import { setCWD } from "../../helpers/cwd"; -import { PROVIDERS } from "../../helpers/useProvider"; +import { + DEFAULT_ACCOUNTS, + DEFAULT_BLOCK_GAS_LIMIT, + PROVIDERS +} from "../../helpers/useProvider"; + +const DEFAULT_ACCOUNTS_ADDRESSES = DEFAULT_ACCOUNTS.map(account => + bufferToHex(privateToAddress(toBuffer(account.privateKey))).toLowerCase() +); + +async function deployContract( + provider: EthereumProvider, + deploymentCode: string +) { + const hash = await provider.send("eth_sendTransaction", [ + { + from: DEFAULT_ACCOUNTS_ADDRESSES[0], + data: deploymentCode, + gas: numberToRpcQuantity(DEFAULT_BLOCK_GAS_LIMIT) + } + ]); + + const { contractAddress } = await provider.send("eth_getTransactionReceipt", [ + hash + ]); + + return contractAddress; +} describe("Evm module", function() { PROVIDERS.forEach(provider => { @@ -80,6 +116,81 @@ describe("Evm module", function() { }); }); + describe("evm_setNextBlockTimestamp", async function() { + it("should set next block timestamp and the next EMPTY block will be mined with that timestamp", async function() { + const timestamp = getCurrentTimestamp() + 60; + + await this.provider.send("evm_setNextBlockTimestamp", [timestamp]); + await this.provider.send("evm_mine", []); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + ["latest", false] + ); + + assertQuantity(block.timestamp, timestamp); + }); + it("should set next block timestamp and the next tx will be mined with that timestamp", async function() { + const timestamp = getCurrentTimestamp() + 70; + + await this.provider.send("evm_setNextBlockTimestamp", [timestamp]); + await deployContract( + this.provider, + `0x${EXAMPLE_CONTRACT.bytecode.object}` + ); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + ["latest", false] + ); + + assertQuantity(block.timestamp, timestamp); + }); + it("should be able to set and replace an existing 'next block timestamp'", async function() { + const timestamp = getCurrentTimestamp() + 60; + + await this.provider.send("evm_setNextBlockTimestamp", [timestamp]); + await this.provider.send("evm_setNextBlockTimestamp", [ + timestamp + 10 + ]); + await this.provider.send("evm_mine", []); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + ["latest", false] + ); + + assertQuantity(block.timestamp, timestamp + 10); + }); + it("should be reset after the next block is mined", async function() { + const timestamp = getCurrentTimestamp() + 60; + + await this.provider.send("evm_setNextBlockTimestamp", [timestamp]); + await this.provider.send("evm_mine", []); + await this.provider.send("evm_mine", []); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + ["latest", false] + ); + + assert.isTrue(quantityToNumber(block.timestamp) > timestamp); + }); + it("should be overriden if next EMPTY block is mined with timestamp", async function() { + const timestamp = getCurrentTimestamp() + 90; + + await this.provider.send("evm_setNextBlockTimestamp", [timestamp]); + await this.provider.send("evm_mine", [timestamp + 100]); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + ["latest", false] + ); + + assertQuantity(block.timestamp, timestamp + 100); + }); + }); + describe("evm_mine", async function() { it("should mine an empty block", async function() { await this.provider.send("evm_mine"); @@ -100,6 +211,30 @@ describe("Evm module", function() { assert.isEmpty(block2.transactions); }); + it("should mine an empty block with exact timestamp", async function() { + const timestamp = getCurrentTimestamp() + 60; + await this.provider.send("evm_mine", [timestamp]); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + [numberToRpcQuantity(1), false] + ); + + assertQuantity(block.timestamp, timestamp); + }); + it("should mine an empty block with the timestamp and other later blocks have higher timestamp", async function() { + const timestamp = getCurrentTimestamp() + 60; + await this.provider.send("evm_mine", [timestamp]); + await this.provider.send("evm_mine"); + await this.provider.send("evm_mine"); + + const block: RpcBlockOutput = await this.provider.send( + "eth_getBlockByNumber", + [numberToRpcQuantity(2), false] + ); + + assert.isTrue(quantityToNumber(block.timestamp) > timestamp); + }); }); describe("Snapshot functionality", function() {