Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

feat: add miner.timestampIncrement option #3131

Merged
merged 12 commits into from
Jun 7, 2022
2 changes: 1 addition & 1 deletion docs/assets/js/ganache/ganache.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/assets/js/ganache/ganache.min.js.map

Large diffs are not rendered by default.

203 changes: 96 additions & 107 deletions docs/index.html

Large diffs are not rendered by default.

1,220 changes: 617 additions & 603 deletions docs/typedoc/api.json

Large diffs are not rendered by default.

202 changes: 98 additions & 104 deletions docs/typedoc/classes/default.html

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export default class EthereumApi implements Api {
* console.log("end", await provider.send("eth_blockNumber"));
* ```
*/
async evm_mine(): Promise<"0x0">;
async evm_mine(timestamp: number): Promise<"0x0">;
async evm_mine(options: Ethereum.MineOptions): Promise<"0x0">;
@assertArgLength(0, 1)
Expand Down Expand Up @@ -550,19 +551,22 @@ export default class EthereumApi implements Api {
*/
@assertArgLength(0, 1)
async evm_setTime(time: number | QUANTITY | Date) {
let t: number;
let timestamp: number;
switch (typeof time) {
case "object":
t = time.getTime();
timestamp = time.getTime();
break;
case "number":
t = time;
timestamp = time;
break;
default:
t = Quantity.from(time).toNumber();
timestamp = Quantity.from(time).toNumber();
break;
}
return Math.floor(this.#blockchain.setTime(t) / 1000);
const blockchain = this.#blockchain;
const offsetMilliseconds = blockchain.setTimeDiff(timestamp);
// convert offsetMilliseconds to seconds:
return Math.floor(offsetMilliseconds / 1000);
}

/**
Expand Down
73 changes: 52 additions & 21 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,17 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {

{
// create first block
let firstBlockTime: number;
if (options.chain.time != null) {
// If we were given a timestamp, use it instead of the `_currentTime`
const t = options.chain.time.getTime();
firstBlockTime = Math.floor(t / 1000);
this.setTime(t);
} else {
firstBlockTime = this.#currentTime();

// if we don't have a time from the user get one now
if (options.chain.time == null) options.chain.time = new Date();

const timestamp = options.chain.time.getTime();
const firstBlockTime = Math.floor(timestamp / 1000);

// if we are using clock time we need to record the time offset so
// other blocks can have timestamps relative to our initial time.
if (options.miner.timestampIncrement === "clock") {
this.#timeAdjustment = timestamp - Date.now();
}

// if we don't already have a latest block, create a genesis block!
Expand Down Expand Up @@ -557,14 +560,19 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
#readyNextBlock = (previousBlock: Block, timestamp?: number) => {
const previousHeader = previousBlock.header;
const previousNumber = previousHeader.number.toBigInt() || 0n;
const minerOptions = this.#options.miner;
if (timestamp == null) {
timestamp = this.#adjustedTime(previousHeader.timestamp);
}

return new RuntimeBlock(
Quantity.from(previousNumber + 1n),
previousBlock.hash(),
this.coinbase,
this.#options.miner.blockGasLimit.toBuffer(),
minerOptions.blockGasLimit.toBuffer(),
BUFFER_ZERO,
Quantity.from(timestamp == null ? this.#currentTime() : timestamp),
this.#options.miner.difficulty,
Quantity.from(timestamp),
minerOptions.difficulty,
previousHeader.totalDifficulty,
Block.calcNextBaseFee(previousBlock)
);
Expand Down Expand Up @@ -777,32 +785,55 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
}));
};

/**
* The number of milliseconds time should be adjusted by when computing the
* "time" for a block.
*/
#timeAdjustment: number = 0;

/**
* Returns the timestamp, adjusted by the timeAdjustment offset, in seconds.
* @param precedingTimestamp - the timestamp of the block to be used as the
* time source if `timestampIncrement` is not "clock".
*/
#currentTime = () => {
return Math.floor((Date.now() + this.#timeAdjustment) / 1000);
#adjustedTime = (precedingTimestamp: Quantity) => {
const timeAdjustment = this.#timeAdjustment;
const timestampIncrement = this.#options.miner.timestampIncrement;
if (timestampIncrement === "clock") {
return Math.floor((Date.now() + timeAdjustment) / 1000);
} else {
return (
precedingTimestamp.toNumber() +
Math.floor(timeAdjustment / 1000) +
timestampIncrement.toNumber()
);
}
};

/**
* @param seconds -
* @param milliseconds - the number of milliseconds to adjust the time by.
* Negative numbers are treated as 0.
* @returns the total time offset *in milliseconds*
*/
public increaseTime(seconds: number) {
if (seconds < 0) {
seconds = 0;
public increaseTime(milliseconds: number) {
if (milliseconds < 0) {
milliseconds = 0;
}
return (this.#timeAdjustment += seconds);
return (this.#timeAdjustment += milliseconds);
}

/**
* @param seconds -
* @param newTime - the number of milliseconds to adjust the time by. Can be negative.
* @returns the total time offset *in milliseconds*
*/
public setTime(timestamp: number) {
return (this.#timeAdjustment = timestamp - Date.now());
public setTimeDiff(newTime: number) {
// when using clock time use Date.now(), otherwise use the timestamp of the
// current latest block
const currentTime =
this.#options.miner.timestampIncrement === "clock"
? Date.now()
: this.blocks.latest.header.timestamp.toNumber() * 1000;
return (this.#timeAdjustment = newTime - currentTime);
}

#deleteBlockData = async (
Expand Down
17 changes: 14 additions & 3 deletions src/chains/ethereum/ethereum/src/forking/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,13 @@ export class Fork {

public async initialize() {
let cacheProm: Promise<PersistentCache>;
const { fork: options } = this.#options;
if (options.deleteCache) await PersistentCache.deleteDb();
if (options.disableCache === false) {
const {
fork: forkOptions,
chain: chainOptions,
miner: minerOptions
} = this.#options;
if (forkOptions.deleteCache) await PersistentCache.deleteDb();
if (forkOptions.disableCache === false) {
// ignore cache start up errors as it is possible there is an `open`
// conflict if another ganache fork is running at the time this one is
// started. The cache isn't required (though performance will be
Expand All @@ -223,6 +227,13 @@ export class Fork {
this.blockNumber.toBigInt()
);
this.block = new Block(BlockManager.rawFromJSON(block, common), common);
if (!chainOptions.time && minerOptions.timestampIncrement !== "clock") {
chainOptions.time = new Date(
davidmurdoch marked this conversation as resolved.
Show resolved Hide resolved
(this.block.header.timestamp.toNumber() +
minerOptions.timestampIncrement.toNumber()) *
1000
);
}
if (cache) await this.initCache(cache);
}
private async initCache(cache: PersistentCache) {
Expand Down
179 changes: 179 additions & 0 deletions src/chains/ethereum/ethereum/tests/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,185 @@ describe("provider", () => {
});
});
});

it("uses the default 'clock' time for the `timestampIncrement` option", async () => {
const provider = await getProvider();
const options = provider.getOptions();
assert.strictEqual(options.miner.timestampIncrement, "clock");

const timeBeforeMiningBlock = Math.floor(Date.now() / 1000);
await provider.request({
method: "evm_mine",
params: []
});
const timeAfterMiningBlock = Math.floor(Date.now() / 1000);
const block = await provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false]
});
// the `block.timestamp` can be the same as `timeBeforeMiningBlock` and/or
// `timeAfterMiningBlock` because the precision of `block.timestamp` is 1
// second (floored), and mining happens much quicker than 1 second.
assert(
parseInt(block.timestamp) >= timeBeforeMiningBlock,
`block wasn't mined at the right time, should have been on or after ${timeBeforeMiningBlock}, was ${parseInt(
block.timestamp
)}`
);
assert(
parseInt(block.timestamp) <= timeAfterMiningBlock,
`block wasn't mined at the right time, should have been on or before ${timeAfterMiningBlock}, was ${parseInt(
block.timestamp
)}`
);
await provider.disconnect();
});

it("uses the timestampIncrement option", async () => {
const time = new Date("2019-01-01T00:00:00.000Z");
const timestampIncrement = 5;
const provider = await getProvider({
chain: { time },
miner: { timestampIncrement }
});
await provider.request({
method: "evm_mine",
params: []
});
const block = await provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false]
});
assert.strictEqual(
parseInt(block.timestamp),
Math.floor(+time / 1000) + timestampIncrement
);
await provider.disconnect();
});

it("uses time adjustment after `evm_setTime` when `timestampIncrement` is used", async () => {
const time = new Date("2019-01-01T00:00:00.000Z");
const timestampIncrement = 5;
const fastForward = 100 * 1000; // 100 seconds
const provider = await getProvider({
chain: { time },
miner: { timestampIncrement }
});
await provider.request({
method: "evm_setTime",
// fastForward into the future
params: [`0x${(fastForward + +time).toString(16)}`]
});
await provider.request({
method: "evm_mine",
params: []
});
const block = await provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false]
});
const expectedTime =
Math.floor((fastForward + +time) / 1000) + timestampIncrement;
assert.strictEqual(parseInt(block.timestamp), expectedTime);

await provider.disconnect();
});

it("uses time adjustment after `evm_increaseTime` when `timestampIncrement` is used", async () => {
const time = new Date("2019-01-01T00:00:00.000Z");
const timestampIncrement = 5; // seconds
const fastForward = 100; // seconds
const provider = await getProvider({
chain: { time },
miner: { timestampIncrement }
});
await provider.request({
method: "evm_increaseTime",
// fastForward into the future, evm_increaseTime param is in seconds
params: [`0x${fastForward.toString(16)}`]
});
await provider.request({
method: "evm_mine",
params: []
});
const block = await provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false]
});
const expectedTime =
Math.floor(+time / 1000) + fastForward + timestampIncrement;
assert.strictEqual(parseInt(block.timestamp), expectedTime);
await provider.disconnect();
});

it("uses the `timestampIncrement` for the first block when forking", async () => {
const time = new Date("2019-01-01T00:00:00.000Z");
const timestampIncrement = 5;
const fakeMainnet = await getProvider({
jeffsmale90 marked this conversation as resolved.
Show resolved Hide resolved
chain: { time }
});
const provider = await getProvider({
fork: { provider: fakeMainnet as any },
miner: { timestampIncrement }
});
const block = await provider.request({
jeffsmale90 marked this conversation as resolved.
Show resolved Hide resolved
method: "eth_getBlockByNumber",
params: ["latest", false]
});
assert.strictEqual(
parseInt(block.timestamp),
+time / 1000 + timestampIncrement
);
await provider.disconnect();
await fakeMainnet.disconnect();
});

it("uses the `time` option for the first block even when `timestampIncrement` is not 'clock' when forking", async () => {
const time = new Date("2019-01-01T00:00:00.000Z");
const timestampIncrement = 5;
const fakeMainnet = await getProvider({
chain: { time }
});
const time2 = new Date("2020-01-01T00:00:00.000Z");
const provider = await getProvider({
fork: { provider: fakeMainnet as any },
chain: { time: time2 },
miner: { timestampIncrement }
});
const block = await provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false]
});
assert.strictEqual(parseInt(block.timestamp), +time2 / 1000);
await provider.disconnect();
jeffsmale90 marked this conversation as resolved.
Show resolved Hide resolved
await fakeMainnet.disconnect();
});

it("uses the `timestampIncrement` option when interval mining", async () => {
const time = new Date("2019-01-01T00:00:00.000Z");
const blockTime = 2; // only mine once every 2 seconds
const timestampIncrement = 1; // only increment by 1 second per block
const provider = await getProvider({
chain: { time },
miner: { blockTime, timestampIncrement }
});
const subId = await provider.request({
method: "eth_subscribe",
params: ["newHeads"]
});
await provider.once("message");
await provider.request({ method: "eth_unsubscribe", params: [subId] });
const block = await provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false]
});
assert.strictEqual(
parseInt(block.timestamp),
+time / 1000 + timestampIncrement,
"block.timestamp is not the expected value"
);
await provider.disconnect();
}).timeout(10000);
});

describe("interface", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/chains/ethereum/options/src/chain-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export type ChainConfig = {
* The id of the network returned by the RPC method `net_version`.
*
* Defaults to the current timestamp, via JavaScript's `Date.now()` (the
* number of millisconds since the UNIX epoch).
* number of milliseconds since the UNIX epoch).
*
* @defaultValue Date.now()
*/
Expand All @@ -96,7 +96,7 @@ export type ChainConfig = {
* `evm_increaseTime` RPC, to test time-dependent code.
*/
readonly time: {
type: Date;
type: Date | null;
rawType: Date | string | number;
legacy: {
/**
Expand Down Expand Up @@ -176,7 +176,7 @@ export const ChainOptions: Definitions<ChainConfig> = {
cliType: "number"
},
time: {
normalize: rawInput => new Date(rawInput),
normalize: rawInput => (rawInput !== undefined ? new Date(rawInput) : null),
cliDescription: "Date that the first block should start.",
legacyName: "time",
cliAliases: ["t", "time"],
Expand Down