Skip to content

Commit

Permalink
fix: block timestamp (#1873)
Browse files Browse the repository at this point in the history
* Squashed commits from block-timestamp

* fix: use pending block timestamp as mined block timestamp

* chore: remove unused import

* enha: fix dev offsets and evm_setnextblocktimestamp

* chore: remove useless test

* fix: lint

* fix: lint

* fix: 1s granularity

* chore: lint

* fix: lint

* fix: lint

* enha: initial test block timestamp contract

* chore: add to automine as well

* enha: improve external test case

* chore: simplify contract

* chore: change test

* chore; remove unused

* chore: lint

* chore: add some methods to get records

* feat: add hardhat compatibility

* enha: fix test

* fix: lint

* chore: remove

* chore: lint

* chore: simplify contract

* chore: wait

* chore: doc

* enha: use more methods to validate

* chore: add final timestamp comparison

* chore: improve automine test
  • Loading branch information
gabriel-aranha-cw authored Nov 19, 2024
1 parent 146330d commit 5e14bea
Show file tree
Hide file tree
Showing 16 changed files with 401 additions and 113 deletions.
34 changes: 34 additions & 0 deletions e2e/contracts/TestContractBlockTimestamp.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestContractBlockTimestamp {
event TimestampRecorded(uint256 timestamp, uint256 blockNumber);

struct TimeRecord {
uint256 timestamp;
uint256 blockNumber;
}

TimeRecord[] public records;

/// @dev Records current block timestamp and number
/// @return The recorded timestamp
function recordTimestamp() public returns (uint256) {
uint256 timestamp = block.timestamp;
records.push(TimeRecord(timestamp, block.number));
emit TimestampRecorded(timestamp, block.number);
return timestamp;
}

/// @dev Gets the current block timestamp
/// @return The current block.timestamp
function getCurrentTimestamp() public view returns (uint256) {
return block.timestamp;
}

/// @dev Gets all recorded timestamps
/// @return Array of TimeRecord structs
function getRecords() public view returns (TimeRecord[] memory) {
return records;
}
}
91 changes: 71 additions & 20 deletions e2e/test/automine/e2e-json-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TEST_BALANCE,
ZERO,
deployTestContractBalances,
deployTestContractBlockTimestamp,
prepareSignedTx,
send,
sendAndGetError,
Expand All @@ -29,9 +30,12 @@ import {

describe("JSON-RPC", () => {
describe("State", () => {
it("stratus_reset / hardhat_reset", async () => {
(await sendExpect("stratus_reset")).eq(true);
(await sendExpect("hardhat_reset")).eq(true);
it("reset", async () => {
if (isStratus) {
(await sendExpect("stratus_reset")).eq(true);
} else {
(await sendExpect("hardhat_reset")).eq(true);
}
});
});

Expand Down Expand Up @@ -229,42 +233,89 @@ describe("JSON-RPC", () => {
});

describe("evm_setNextBlockTimestamp", () => {
let target = Math.floor(Date.now() / 1000) + 10;
let initialTarget: number;

beforeEach(async () => {
await send("evm_setNextBlockTimestamp", [0]);
});

it("sets the next block timestamp", async () => {
await send("evm_setNextBlockTimestamp", [target]);
initialTarget = Math.floor(Date.now() / 1000) + 100;
await send("evm_setNextBlockTimestamp", [initialTarget]);
await sendEvmMine();
expect((await latest()).timestamp).eq(target);
expect((await latest()).timestamp).eq(initialTarget);
});

it("offsets subsequent timestamps", async () => {
const target = Math.floor(Date.now() / 1000) + 100;
await send("evm_setNextBlockTimestamp", [target]);
await sendEvmMine();

await new Promise((resolve) => setTimeout(resolve, 1000));
await sendEvmMine();
expect((await latest()).timestamp).to.be.greaterThan(target);
});

it("resets the changes when sending 0", async () => {
const currentTimestamp = (await latest()).timestamp;
await send("evm_setNextBlockTimestamp", [0]);
let mined_timestamp = Math.floor(Date.now() / 1000);
await sendEvmMine();
let latest_timestamp = (await latest()).timestamp;
expect(latest_timestamp)
.gte(mined_timestamp)
.lte(Math.floor(Date.now() / 1000));
const newTimestamp = (await latest()).timestamp;
expect(newTimestamp).to.be.greaterThan(currentTimestamp);
});

it("handle negative offsets", async () => {
const past = Math.floor(Date.now() / 1000);
await new Promise((resolve) => setTimeout(resolve, 2000));
await send("evm_setNextBlockTimestamp", [past]);
const currentBlock = await latest();
const futureTimestamp = currentBlock.timestamp + 100;

await send("evm_setNextBlockTimestamp", [futureTimestamp]);
await sendEvmMine();
expect((await latest()).timestamp).eq(past);
await new Promise((resolve) => setTimeout(resolve, 1000));

const pastTimestamp = currentBlock.timestamp - 100;
await send("evm_setNextBlockTimestamp", [pastTimestamp]);
await sendEvmMine();
expect((await latest()).timestamp)
.to.be.greaterThan(past)
.lessThan(Math.floor(Date.now() / 1000));

await send("evm_setNextBlockTimestamp", [0]);
const newTimestamp = (await latest()).timestamp;
expect(newTimestamp).to.be.greaterThan(futureTimestamp);
});
});

describe("Block timestamp", () => {
it("transaction executes with pending block timestamp", async () => {
await sendReset();
const contract = await deployTestContractBlockTimestamp();

// Get initial timestamp
const initialTimestamp = await contract.getCurrentTimestamp();
expect(initialTimestamp).to.be.gt(0);

// Record timestamp in contract
const tx = await contract.recordTimestamp();
const receipt = await tx.wait();

// Get the timestamp from contract event
const event = receipt.logs[0];
const recordedTimestamp = contract.interface.parseLog({
topics: event.topics,
data: event.data,
})?.args.timestamp;

// Get the block timestamp
const block = await ETHERJS.getBlock(receipt.blockNumber);
const blockTimestamp = block!.timestamp;

// Get stored record from contract
const records = await contract.getRecords();
expect(records.length).to.equal(1);

// Validate timestamps match across all sources
expect(recordedTimestamp).to.equal(blockTimestamp);
expect(records[0].timestamp).to.equal(recordedTimestamp);
expect(records[0].blockNumber).to.equal(receipt.blockNumber);

// Verify that time is advancing
const finalTimestamp = await contract.getCurrentTimestamp();
expect(finalTimestamp).to.be.gt(initialTimestamp);
});
});
});
Expand Down
126 changes: 105 additions & 21 deletions e2e/test/external/e2e-json-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { expect } from "chai";
import { TransactionReceipt, TransactionResponse, keccak256 } from "ethers";
import { Block, Bytes } from "web3-types";

import { TestContractBalances } from "../../typechain-types";
import { ALICE, BOB } from "../helpers/account";
import { BlockMode, currentBlockMode, isStratus } from "../helpers/network";
import {
Expand All @@ -17,6 +16,7 @@ import {
TEST_BALANCE,
ZERO,
deployTestContractBalances,
deployTestContractBlockTimestamp,
prepareSignedTx,
send,
sendAndGetError,
Expand All @@ -38,9 +38,12 @@ describe("JSON-RPC", () => {
});

describe("State", () => {
it("stratus_reset", async () => {
(await sendExpect("stratus_reset")).eq(true);
(await sendExpect("hardhat_reset")).eq(true);
it("reset", async () => {
if (isStratus) {
(await sendExpect("stratus_reset")).eq(true);
} else {
(await sendExpect("hardhat_reset")).eq(true);
}
});
});

Expand Down Expand Up @@ -354,42 +357,123 @@ describe("JSON-RPC", () => {
});

describe("evm_setNextBlockTimestamp", () => {
let target = Math.floor(Date.now() / 1000) + 10;
let initialTarget: number;

beforeEach(async () => {
await send("evm_setNextBlockTimestamp", [0]);
});

it("sets the next block timestamp", async () => {
await send("evm_setNextBlockTimestamp", [target]);
initialTarget = Math.floor(Date.now() / 1000) + 100;
await send("evm_setNextBlockTimestamp", [initialTarget]);
await sendEvmMine();
expect((await latest()).timestamp).eq(target);
expect((await latest()).timestamp).eq(initialTarget);
});

it("offsets subsequent timestamps", async () => {
const target = Math.floor(Date.now() / 1000) + 100;
await send("evm_setNextBlockTimestamp", [target]);
await sendEvmMine();

await new Promise((resolve) => setTimeout(resolve, 1000));
await sendEvmMine();
expect((await latest()).timestamp).to.be.greaterThan(target);
});

it("resets the changes when sending 0", async () => {
const currentTimestamp = (await latest()).timestamp;
await send("evm_setNextBlockTimestamp", [0]);
let mined_timestamp = Math.floor(Date.now() / 1000);
await sendEvmMine();
let latest_timestamp = (await latest()).timestamp;
expect(latest_timestamp)
.gte(mined_timestamp)
.lte(Math.floor(Date.now() / 1000));
const newTimestamp = (await latest()).timestamp;
expect(newTimestamp).to.be.greaterThan(currentTimestamp);
});

it("handle negative offsets", async () => {
const past = Math.floor(Date.now() / 1000);
await new Promise((resolve) => setTimeout(resolve, 2000));
await send("evm_setNextBlockTimestamp", [past]);
const currentBlock = await latest();
const futureTimestamp = currentBlock.timestamp + 100;

await send("evm_setNextBlockTimestamp", [futureTimestamp]);
await sendEvmMine();
expect((await latest()).timestamp).eq(past);
await new Promise((resolve) => setTimeout(resolve, 1000));

const pastTimestamp = currentBlock.timestamp - 100;
await send("evm_setNextBlockTimestamp", [pastTimestamp]);
await sendEvmMine();
expect((await latest()).timestamp)
.to.be.greaterThan(past)
.lessThan(Math.floor(Date.now() / 1000));

await send("evm_setNextBlockTimestamp", [0]);
const newTimestamp = (await latest()).timestamp;
expect(newTimestamp).to.be.greaterThan(futureTimestamp);
});
});

describe("Block timestamp", () => {
it("transaction executes with pending block timestamp", async () => {
await sendReset();
const contract = await deployTestContractBlockTimestamp();
await sendEvmMine();

await new Promise((resolve) => setTimeout(resolve, 2000));

// Get pending block timestamp
const pendingTimestamp = await contract.getCurrentTimestamp();
expect(pendingTimestamp).to.be.gt(0);

// Wait 2 seconds
await new Promise((resolve) => setTimeout(resolve, 2000));

// Send first transaction
const tx1 = await contract.recordTimestamp();

// Wait 2 seconds
await new Promise((resolve) => setTimeout(resolve, 2000));

// Send second transaction
const tx2 = await contract.recordTimestamp();

// Wait 2 seconds
await new Promise((resolve) => setTimeout(resolve, 2000));

// Mine block to include both transactions
await sendEvmMine();

const [receipt1, receipt2] = await Promise.all([tx1.wait(), tx2.wait()]);

// Get timestamps from contract events
const event1 = receipt1.logs[0];
const event2 = receipt2.logs[0];
const recordedTimestamp1 = contract.interface.parseLog({
topics: event1.topics,
data: event1.data,
})?.args.timestamp;
const recordedTimestamp2 = contract.interface.parseLog({
topics: event2.topics,
data: event2.data,
})?.args.timestamp;

// Get the block timestamp (both transactions are in the same block)
const block = await ETHERJS.getBlock(receipt1.blockNumber);
const blockTimestamp = block!.timestamp;

// Get all records from contract
const records = await contract.getRecords();
expect(records.length).to.equal(2);

// Verify that the pending block timestamp matches the final block timestamp and transaction timestamps
expect(pendingTimestamp).to.equal(blockTimestamp);
expect(pendingTimestamp).to.equal(recordedTimestamp1);
expect(pendingTimestamp).to.equal(recordedTimestamp2);

// Verify that the stored records in the contract match the event data
expect(records[0].timestamp).to.equal(recordedTimestamp1);
expect(records[0].blockNumber).to.equal(receipt1.blockNumber);
expect(records[1].timestamp).to.equal(recordedTimestamp2);
expect(records[1].blockNumber).to.equal(receipt2.blockNumber);

// Verify that both transactions see the same block timestamp
expect(recordedTimestamp1).to.equal(blockTimestamp);
expect(recordedTimestamp2).to.equal(blockTimestamp);

// Verify that time is advancing
const finalTimestamp = await contract.getCurrentTimestamp();
expect(finalTimestamp).to.be.gt(pendingTimestamp);
});
});
});
Expand Down
13 changes: 12 additions & 1 deletion e2e/test/helpers/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import { HttpNetworkConfig } from "hardhat/types";
import { Numbers } from "web3-types";
import { WebSocket } from "ws";

import { TestContractBalances, TestContractCounter, TestContractDenseStorage } from "../../typechain-types";
import {
TestContractBalances,
TestContractBlockTimestamp,
TestContractCounter,
TestContractDenseStorage,
} from "../../typechain-types";
import { Account, CHARLIE } from "./account";
import { currentMiningIntervalInMs, currentNetwork, isStratus } from "./network";

Expand Down Expand Up @@ -157,6 +162,12 @@ export async function deployTestContractBalances(): Promise<TestContractBalances
return await testContractFactory.connect(CHARLIE.signer()).deploy();
}

// Deploys the "TestBlockTimestamp" contract.
export async function deployTestContractBlockTimestamp(): Promise<TestContractBlockTimestamp> {
const testContractFactory = await ethers.getContractFactory("TestContractBlockTimestamp");
return await testContractFactory.connect(CHARLIE.signer()).deploy();
}

// Deploys the "TestContractCounter" contract.
export async function deployTestContractCounter(): Promise<TestContractCounter> {
const testContractFactory = await ethers.getContractFactory("TestContractCounter");
Expand Down
Loading

0 comments on commit 5e14bea

Please sign in to comment.