diff --git a/e2e/contracts/TestContractBlockTimestamp.sol b/e2e/contracts/TestContractBlockTimestamp.sol new file mode 100644 index 000000000..a4687fee6 --- /dev/null +++ b/e2e/contracts/TestContractBlockTimestamp.sol @@ -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; + } +} \ No newline at end of file diff --git a/e2e/test/automine/e2e-json-rpc.test.ts b/e2e/test/automine/e2e-json-rpc.test.ts index c94e10f31..b6fda44c1 100644 --- a/e2e/test/automine/e2e-json-rpc.test.ts +++ b/e2e/test/automine/e2e-json-rpc.test.ts @@ -14,6 +14,7 @@ import { TEST_BALANCE, ZERO, deployTestContractBalances, + deployTestContractBlockTimestamp, prepareSignedTx, send, sendAndGetError, @@ -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); + } }); }); @@ -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); }); }); }); diff --git a/e2e/test/external/e2e-json-rpc.test.ts b/e2e/test/external/e2e-json-rpc.test.ts index 634f0d01f..9a981e80c 100644 --- a/e2e/test/external/e2e-json-rpc.test.ts +++ b/e2e/test/external/e2e-json-rpc.test.ts @@ -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 { @@ -17,6 +16,7 @@ import { TEST_BALANCE, ZERO, deployTestContractBalances, + deployTestContractBlockTimestamp, prepareSignedTx, send, sendAndGetError, @@ -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); + } }); }); @@ -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); }); }); }); diff --git a/e2e/test/helpers/rpc.ts b/e2e/test/helpers/rpc.ts index 0009dcf6e..d5ba747a2 100644 --- a/e2e/test/helpers/rpc.ts +++ b/e2e/test/helpers/rpc.ts @@ -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"; @@ -157,6 +162,12 @@ export async function deployTestContractBalances(): Promise { + const testContractFactory = await ethers.getContractFactory("TestContractBlockTimestamp"); + return await testContractFactory.connect(CHARLIE.signer()).deploy(); +} + // Deploys the "TestContractCounter" contract. export async function deployTestContractCounter(): Promise { const testContractFactory = await ethers.getContractFactory("TestContractCounter"); diff --git a/justfile b/justfile index cdccc65a4..05eea6c46 100644 --- a/justfile +++ b/justfile @@ -159,6 +159,25 @@ e2e network="stratus" block_modes="automine" test="": fi done +# E2E: Starts and execute Hardhat tests in Hardhat +e2e-hardhat block-mode="automine" test="": + #!/bin/bash + if [ -d e2e ]; then + cd e2e + fi + + echo "-> Starting Hardhat" + BLOCK_MODE={{block-mode}} npx hardhat node & + + echo "-> Waiting Hardhat to start" + wait-service --tcp localhost:8545 -- echo + + echo "-> Running E2E tests" + just e2e hardhat {{block-mode}} {{test}} + + echo "-> Killing Hardhat" + killport 8545 + # E2E: Starts and execute Hardhat tests in Stratus e2e-stratus block-mode="automine" test="": #!/bin/bash @@ -282,7 +301,7 @@ e2e-leader-follower-up test="brlc" release_flag="--release": mkdir e2e_logs # Start Stratus with leader flag - RUST_BACKTRACE=1 RUST_LOG=info cargo ${CARGO_COMMAND} run {{release_flag}} --bin stratus --features dev -- --leader --block-mode 1s --perm-storage=rocks --rocks-path-prefix=temp_3000 --tokio-console-address=0.0.0.0:6668 --metrics-exporter-address=0.0.0.0:9000 -a 0.0.0.0:3000 > e2e_logs/stratus.log & + RUST_BACKTRACE=1 RUST_LOG=info cargo ${CARGO_COMMAND} run {{release_flag}} --bin stratus --features dev -- --leader --block-mode 1s --perm-storage=rocks --rocks-path-prefix=temp_3000 -a 0.0.0.0:3000 > e2e_logs/stratus.log & # Wait for Stratus with leader flag to start just _wait_for_stratus 3000 @@ -295,9 +314,9 @@ e2e-leader-follower-up test="brlc" release_flag="--release": just _log "Waiting Kafka start" wait-service --tcp 0.0.0.0:29092 -- echo docker exec kafka kafka-topics --create --topic stratus-events --bootstrap-server localhost:29092 --partitions 1 --replication-factor 1 - RUST_BACKTRACE=1 RUST_LOG=info cargo ${CARGO_COMMAND} run {{release_flag}} --bin stratus --features dev -- --follower --perm-storage=rocks --rocks-path-prefix=temp_3001 --tokio-console-address=0.0.0.0:6669 --metrics-exporter-address=0.0.0.0:9001 -a 0.0.0.0:3001 -r http://0.0.0.0:3000/ -w ws://0.0.0.0:3000/ --kafka-bootstrap-servers localhost:29092 --kafka-topic stratus-events --kafka-client-id stratus-producer --kafka-security-protocol none > e2e_logs/importer.log & + RUST_BACKTRACE=1 RUST_LOG=info cargo ${CARGO_COMMAND} run {{release_flag}} --bin stratus --features dev -- --follower --perm-storage=rocks --rocks-path-prefix=temp_3001 -a 0.0.0.0:3001 -r http://0.0.0.0:3000/ -w ws://0.0.0.0:3000/ --kafka-bootstrap-servers localhost:29092 --kafka-topic stratus-events --kafka-client-id stratus-producer --kafka-security-protocol none > e2e_logs/importer.log & else - RUST_BACKTRACE=1 RUST_LOG=info cargo ${CARGO_COMMAND} run {{release_flag}} --bin stratus --features dev -- --follower --perm-storage=rocks --rocks-path-prefix=temp_3001 --tokio-console-address=0.0.0.0:6669 --metrics-exporter-address=0.0.0.0:9001 -a 0.0.0.0:3001 -r http://0.0.0.0:3000/ -w ws://0.0.0.0:3000/ > e2e_logs/importer.log & + RUST_BACKTRACE=1 RUST_LOG=info cargo ${CARGO_COMMAND} run {{release_flag}} --bin stratus --features dev -- --follower --perm-storage=rocks --rocks-path-prefix=temp_3001 -a 0.0.0.0:3001 -r http://0.0.0.0:3000/ -w ws://0.0.0.0:3000/ > e2e_logs/importer.log & fi # Wait for Stratus with follower flag to start just _wait_for_stratus 3001 diff --git a/src/eth/executor/evm_input.rs b/src/eth/executor/evm_input.rs index 450fa0a82..886eb4a5f 100644 --- a/src/eth/executor/evm_input.rs +++ b/src/eth/executor/evm_input.rs @@ -10,6 +10,7 @@ use crate::eth::primitives::ExternalReceipt; use crate::eth::primitives::ExternalTransaction; use crate::eth::primitives::Gas; use crate::eth::primitives::Nonce; +use crate::eth::primitives::PendingBlockHeader; use crate::eth::primitives::TransactionInput; use crate::eth::primitives::UnixTime; use crate::eth::primitives::Wei; @@ -81,7 +82,7 @@ pub struct EvmInput { impl EvmInput { /// Creates from a transaction that was sent directly to Stratus with `eth_sendRawTransaction`. - pub fn from_eth_transaction(input: TransactionInput, pending_block_number: BlockNumber) -> Self { + pub fn from_eth_transaction(input: TransactionInput, pending_header: PendingBlockHeader) -> Self { Self { from: input.signer, to: input.to, @@ -90,8 +91,8 @@ impl EvmInput { gas_limit: Gas::MAX, gas_price: Wei::ZERO, nonce: Some(input.nonce), - block_number: pending_block_number, - block_timestamp: UnixTime::now(), // TODO: this should come from the pending block + block_number: pending_header.number, + block_timestamp: *pending_header.timestamp, point_in_time: StoragePointInTime::Pending, chain_id: input.chain_id, } @@ -105,7 +106,7 @@ impl EvmInput { pub fn from_eth_call( input: CallInput, point_in_time: StoragePointInTime, - pending_block_number: BlockNumber, + pending_header: PendingBlockHeader, mined_block: Option, ) -> anyhow::Result { Ok(Self { @@ -117,11 +118,11 @@ impl EvmInput { gas_price: Wei::ZERO, nonce: None, block_number: match point_in_time { - StoragePointInTime::Mined | StoragePointInTime::Pending => pending_block_number, + StoragePointInTime::Mined | StoragePointInTime::Pending => pending_header.number, StoragePointInTime::MinedPast(number) => number, }, block_timestamp: match point_in_time { - StoragePointInTime::Mined | StoragePointInTime::Pending => UnixTime::now(), + StoragePointInTime::Mined | StoragePointInTime::Pending => *pending_header.timestamp, StoragePointInTime::MinedPast(_) => match mined_block { Some(block) => block.header.timestamp, None => return log_and_err!("failed to create EvmInput: couldn't determine mined block timestamp"), diff --git a/src/eth/executor/executor.rs b/src/eth/executor/executor.rs index e72038b0f..1432c39a7 100644 --- a/src/eth/executor/executor.rs +++ b/src/eth/executor/executor.rs @@ -483,7 +483,7 @@ impl Executor { // prepare evm input let pending_header = self.storage.read_pending_block_header()?.unwrap_or_default(); - let evm_input = EvmInput::from_eth_transaction(tx_input.clone(), pending_header.number); + let evm_input = EvmInput::from_eth_transaction(tx_input.clone(), pending_header); // execute transaction in evm (retry only in case of conflict, but do not retry on other failures) tracing::info!( @@ -558,7 +558,7 @@ impl Executor { }; // execute - let evm_input = EvmInput::from_eth_call(call_input.clone(), point_in_time, pending_header.number, mined_block)?; + let evm_input = EvmInput::from_eth_call(call_input.clone(), point_in_time, pending_header, mined_block)?; let evm_route = match point_in_time { StoragePointInTime::Mined | StoragePointInTime::Pending => EvmRoute::CallPresent, StoragePointInTime::MinedPast(_) => EvmRoute::CallPast, diff --git a/src/eth/miner/miner.rs b/src/eth/miner/miner.rs index e3a555891..3aa877487 100644 --- a/src/eth/miner/miner.rs +++ b/src/eth/miner/miner.rs @@ -25,11 +25,11 @@ use crate::eth::primitives::Hash; use crate::eth::primitives::Index; use crate::eth::primitives::LocalTransactionExecution; use crate::eth::primitives::LogMined; +use crate::eth::primitives::PendingBlockHeader; use crate::eth::primitives::Size; use crate::eth::primitives::StratusError; use crate::eth::primitives::TransactionExecution; use crate::eth::primitives::TransactionMined; -use crate::eth::primitives::UnixTime; use crate::eth::storage::StratusStorage; use crate::ext::not; use crate::ext::DisplayExt; @@ -306,7 +306,7 @@ impl Miner { } } - block_from_local(block.header.number, local_txs) + block_from_local(block.header, local_txs) } /// Persists a mined block to permanent storage and prepares new block. @@ -375,14 +375,8 @@ fn block_from_external(external_block: ExternalBlock, mined_txs: Vec) -> anyhow::Result { - // TODO: block timestamp should be set in the PendingBlock instead of being retrieved from the execution - let block_timestamp = match txs.first() { - Some(tx) => tx.result.execution.block_timestamp, - None => UnixTime::now(), - }; - - let mut block = Block::new(number, block_timestamp); +pub fn block_from_local(pending_header: PendingBlockHeader, txs: Vec) -> anyhow::Result { + let mut block = Block::new(pending_header.number, *pending_header.timestamp); block.transactions.reserve(txs.len()); block.header.size = Size::from(txs.len() as u64); diff --git a/src/eth/primitives/mod.rs b/src/eth/primitives/mod.rs index 6985996cd..bb32e37cb 100644 --- a/src/eth/primitives/mod.rs +++ b/src/eth/primitives/mod.rs @@ -32,7 +32,6 @@ mod log_topic; pub mod logs_bloom; mod miner_nonce; mod nonce; -mod now; mod pending_block; mod pending_block_header; mod size; @@ -45,6 +44,7 @@ mod transaction_input; mod transaction_mined; mod transaction_stage; mod unix_time; +mod unix_time_now; mod wei; pub use account::test_accounts; @@ -85,7 +85,6 @@ pub use log_mined::LogMined; pub use log_topic::LogTopic; pub use miner_nonce::MinerNonce; pub use nonce::Nonce; -pub use now::DateTimeNow; pub use pending_block::PendingBlock; pub use pending_block_header::PendingBlockHeader; pub use size::Size; @@ -100,6 +99,7 @@ pub use transaction_input::TransactionInput; pub use transaction_mined::TransactionMined; pub use transaction_stage::TransactionStage; pub use unix_time::UnixTime; +pub use unix_time_now::UnixTimeNow; pub use wei::Wei; // ----------------------------------------------------------------------------- @@ -138,7 +138,6 @@ mod tests { gen_test_serde!(CallInput); gen_test_serde!(ChainId); gen_test_serde!(CodeHash); - gen_test_serde!(DateTimeNow); gen_test_serde!(Difficulty); gen_test_serde!(EcdsaRs); gen_test_serde!(EcdsaV); @@ -171,5 +170,6 @@ mod tests { gen_test_serde!(TransactionInput); gen_test_serde!(TransactionMined); gen_test_serde!(UnixTime); + gen_test_serde!(UnixTimeNow); gen_test_serde!(Wei); } diff --git a/src/eth/primitives/now.rs b/src/eth/primitives/now.rs deleted file mode 100644 index d02763c65..000000000 --- a/src/eth/primitives/now.rs +++ /dev/null @@ -1,13 +0,0 @@ -use chrono::DateTime; -use chrono::Utc; -use display_json::DebugAsJson; - -/// DateTime that automatically sets the current time when created. -#[derive(DebugAsJson, Clone, PartialEq, Eq, derive_more::Deref, fake::Dummy, serde::Serialize, serde::Deserialize)] -pub struct DateTimeNow(#[deref] DateTime); - -impl Default for DateTimeNow { - fn default() -> Self { - Self(Utc::now()) - } -} diff --git a/src/eth/primitives/pending_block_header.rs b/src/eth/primitives/pending_block_header.rs index 59a82487b..47d48f023 100644 --- a/src/eth/primitives/pending_block_header.rs +++ b/src/eth/primitives/pending_block_header.rs @@ -1,21 +1,18 @@ use display_json::DebugAsJson; use crate::eth::primitives::BlockNumber; -use crate::eth::primitives::UnixTime; +use crate::eth::primitives::UnixTimeNow; /// Header of the pending block being mined. #[derive(DebugAsJson, Clone, Default, serde::Serialize)] pub struct PendingBlockHeader { pub number: BlockNumber, - pub timestamp: UnixTime, + pub timestamp: UnixTimeNow, } impl PendingBlockHeader { /// Creates a new [`PendingBlockHeader`] with the specified number and the current timestamp. pub fn new_at_now(number: BlockNumber) -> Self { - Self { - number, - timestamp: UnixTime::now(), - } + Self { number, ..Self::default() } } } diff --git a/src/eth/primitives/unix_time.rs b/src/eth/primitives/unix_time.rs index 59a3fe30b..e32835e90 100644 --- a/src/eth/primitives/unix_time.rs +++ b/src/eth/primitives/unix_time.rs @@ -29,16 +29,13 @@ impl UnixTime { } #[cfg(feature = "dev")] - pub fn set_offset(timestamp: UnixTime, latest_timestamp: UnixTime) -> anyhow::Result<()> { - offset::set(timestamp, latest_timestamp) + pub fn set_offset(current_block_timestamp: UnixTime, new_block_timestamp: UnixTime) -> anyhow::Result<()> { + offset::set(current_block_timestamp, new_block_timestamp) } - pub fn to_i64(&self) -> i64 { - self.0.try_into().expect("UNIX time is unrealistically high") - } - - pub fn as_u64(&self) -> u64 { - self.0 + #[cfg(feature = "dev")] + pub fn evm_set_next_block_timestamp_was_called() -> bool { + offset::evm_set_next_block_timestamp_was_called() } } @@ -115,6 +112,7 @@ impl TryFrom for i32 { #[cfg(feature = "dev")] mod offset { + use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicI64; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering::Acquire; @@ -123,32 +121,114 @@ mod offset { use super::UnixTime; use super::Utc; - pub static TIME_OFFSET: AtomicI64 = AtomicI64::new(0); - pub static NEXT_TIMESTAMP: AtomicU64 = AtomicU64::new(0); - - pub fn set(timestamp: UnixTime, latest_timestamp: UnixTime) -> anyhow::Result<()> { + /// Stores the time difference (in seconds) to apply to subsequent blocks + /// This maintains relative time offsets after an explicit timestamp is used + pub static NEW_TIMESTAMP_DIFF: AtomicI64 = AtomicI64::new(0); + + /// Stores the exact timestamp to use for the next block + /// Only used once, then reset to 0 + pub static NEW_TIMESTAMP: AtomicU64 = AtomicU64::new(0); + + /// Indicates whether evm_setNextBlockTimestamp was called and hasn't been consumed yet + pub static EVM_SET_NEXT_BLOCK_TIMESTAMP_WAS_CALLED: AtomicBool = AtomicBool::new(false); + + /// Stores the timestamp of the most recently processed block + /// Used to ensure that block timestamps always increase monotonically + pub static LAST_BLOCK_TIMESTAMP: AtomicI64 = AtomicI64::new(0); + + /// Sets the timestamp for the next block and calculates the offset for subsequent blocks + /// + /// # Scenarios: + /// 1. Setting a future timestamp: + /// - current_block = 100, new_timestamp = 110 + /// - diff = (110 - current_time) = +10 + /// - Next block will be exactly 110 + /// - Subsequent blocks will maintain the +10 offset from current time + /// + /// 2. Setting timestamp to 0 (reset): + /// - Stores the current block timestamp for reference + /// - Resets the time offset to 0 + /// - Next block will be current_block + 1 + /// - Subsequent blocks use current time + /// + /// 3. Setting a past timestamp (error): + /// - current_block = 100, new_timestamp = 90 + /// - Returns error: "timestamp can't be before the latest block" + pub fn set(current_block_timestamp: UnixTime, new_block_timestamp: UnixTime) -> anyhow::Result<()> { use crate::log_and_err; - let now = Utc::now().timestamp() as u64; - if *timestamp != 0 && *timestamp < *latest_timestamp { + if *new_block_timestamp == 0 { + LAST_BLOCK_TIMESTAMP.store(*current_block_timestamp as i64, SeqCst); + } + + if *new_block_timestamp != 0 && *new_block_timestamp < *current_block_timestamp { return log_and_err!("timestamp can't be before the latest block"); } - let diff: i64 = if *timestamp == 0 { 0 } else { (*timestamp as i128 - now as i128) as i64 }; - NEXT_TIMESTAMP.store(*timestamp, SeqCst); - TIME_OFFSET.store(diff, SeqCst); + let current_time = Utc::now().timestamp() as i64; + let diff: i64 = if *new_block_timestamp == 0 { + 0 + } else { + (*new_block_timestamp as i64).saturating_sub(current_time) + }; + + NEW_TIMESTAMP.store(*new_block_timestamp, SeqCst); + NEW_TIMESTAMP_DIFF.store(diff, SeqCst); + EVM_SET_NEXT_BLOCK_TIMESTAMP_WAS_CALLED.store(true, SeqCst); Ok(()) } + /// Returns the timestamp for the current block based on various conditions. + /// Ensures proper time progression by guaranteeing that each block's timestamp + /// is at least 1 second greater than the previous block. + /// + /// # Behavior + /// There are two main paths for timestamp generation: + /// + /// 1. When a specific timestamp was set (was_evm_timestamp_set = true): + /// - If new_timestamp is 0: Returns last_block_timestamp + 1 + /// - If new_timestamp > 0: Returns exactly new_timestamp + /// After this call, resets the timestamp flag and stored value + /// + /// 2. For subsequent blocks (was_evm_timestamp_set = false): + /// - If new_timestamp is set: Uses it as reference point + /// - Otherwise: Uses max(current_time + offset, last_block_timestamp) + /// In both cases, adds 1 second to ensure progression + /// + /// The function always updates LAST_BLOCK_TIMESTAMP with the returned value + /// to maintain the chain of increasing timestamps. pub fn now() -> UnixTime { - let offset_time = NEXT_TIMESTAMP.load(Acquire); - let time_offset = TIME_OFFSET.load(Acquire); - match offset_time { - 0 => UnixTime((Utc::now().timestamp() as i128 + time_offset as i128) as u64), - _ => { - let _ = NEXT_TIMESTAMP.fetch_update(SeqCst, SeqCst, |_| Some(0)); - UnixTime(offset_time) + let new_timestamp = NEW_TIMESTAMP.load(Acquire); + let new_timestamp_diff = NEW_TIMESTAMP_DIFF.load(Acquire); + let was_evm_timestamp_set = EVM_SET_NEXT_BLOCK_TIMESTAMP_WAS_CALLED.load(Acquire); + let last_block_timestamp = LAST_BLOCK_TIMESTAMP.load(Acquire); + let current_time = Utc::now().timestamp() as i64; + + let result = if !was_evm_timestamp_set { + let last_timestamp = if new_timestamp != 0 { + new_timestamp as i64 + } else { + std::cmp::max(current_time + new_timestamp_diff, last_block_timestamp) + }; + + UnixTime((last_timestamp + 1) as u64) + } else { + EVM_SET_NEXT_BLOCK_TIMESTAMP_WAS_CALLED.store(false, SeqCst); + NEW_TIMESTAMP.store(0, SeqCst); + + if new_timestamp == 0 { + UnixTime((last_block_timestamp + 1) as u64) + } else { + UnixTime(new_timestamp) } - } + }; + + LAST_BLOCK_TIMESTAMP.store(*result as i64, SeqCst); + result + } + + /// Returns whether evm_setNextBlockTimestamp was called and hasn't been consumed yet + pub fn evm_set_next_block_timestamp_was_called() -> bool { + EVM_SET_NEXT_BLOCK_TIMESTAMP_WAS_CALLED.load(Acquire) } } diff --git a/src/eth/primitives/unix_time_now.rs b/src/eth/primitives/unix_time_now.rs new file mode 100644 index 000000000..7b434ae76 --- /dev/null +++ b/src/eth/primitives/unix_time_now.rs @@ -0,0 +1,13 @@ +use display_json::DebugAsJson; + +use crate::eth::primitives::UnixTime; + +/// [`UnixTime`] that automatically sets the current time when created. +#[derive(DebugAsJson, Clone, PartialEq, Eq, derive_more::Deref, fake::Dummy, serde::Serialize, serde::Deserialize)] +pub struct UnixTimeNow(#[deref] UnixTime); + +impl Default for UnixTimeNow { + fn default() -> Self { + Self(UnixTime::now()) + } +} diff --git a/src/eth/rpc/rpc_server.rs b/src/eth/rpc/rpc_server.rs index 77740821e..7fd9d9604 100644 --- a/src/eth/rpc/rpc_server.rs +++ b/src/eth/rpc/rpc_server.rs @@ -269,7 +269,7 @@ fn evm_set_next_block_timestamp(params: Params<'_>, ctx: Arc, _: Ext let (_, timestamp) = next_rpc_param::(params.sequence())?; let latest = ctx.storage.read_block(&BlockFilter::Latest)?; match latest { - Some(block) => UnixTime::set_offset(timestamp, block.header.timestamp)?, + Some(block) => UnixTime::set_offset(block.header.timestamp, timestamp)?, None => return log_and_err!("reading latest block returned None")?, } Ok(to_json_value(timestamp)) diff --git a/src/eth/rpc/rpc_subscriptions.rs b/src/eth/rpc/rpc_subscriptions.rs index 02b622d22..f0f632dfd 100644 --- a/src/eth/rpc/rpc_subscriptions.rs +++ b/src/eth/rpc/rpc_subscriptions.rs @@ -17,12 +17,12 @@ use tokio::time::timeout; use tokio::time::Duration; use crate::eth::primitives::BlockHeader; -use crate::eth::primitives::DateTimeNow; use crate::eth::primitives::Hash; use crate::eth::primitives::LogFilter; use crate::eth::primitives::LogFilterInput; use crate::eth::primitives::LogMined; use crate::eth::primitives::StratusError; +use crate::eth::primitives::UnixTimeNow; use crate::eth::rpc::RpcClientApp; use crate::ext::not; use crate::ext::spawn_named; @@ -297,7 +297,7 @@ impl RpcSubscriptionsHandles { #[derive(Debug, derive_new::new)] pub struct Subscription { #[new(default)] - created_at: DateTimeNow, + created_at: UnixTimeNow, client: RpcClientApp, sink: Arc, diff --git a/src/eth/storage/inmemory/inmemory_temporary.rs b/src/eth/storage/inmemory/inmemory_temporary.rs index 437832a4a..7a7da2fb2 100644 --- a/src/eth/storage/inmemory/inmemory_temporary.rs +++ b/src/eth/storage/inmemory/inmemory_temporary.rs @@ -20,6 +20,10 @@ use crate::eth::primitives::Slot; use crate::eth::primitives::SlotIndex; use crate::eth::primitives::StratusError; use crate::eth::primitives::TransactionExecution; +#[cfg(feature = "dev")] +use crate::eth::primitives::UnixTime; +#[cfg(feature = "dev")] +use crate::eth::primitives::UnixTimeNow; use crate::eth::storage::TemporaryStorage; use crate::log_and_err; @@ -192,8 +196,21 @@ impl TemporaryStorage for InMemoryTemporaryStorage { /// TODO: we cannot allow more than one pending block. Where to put this check? fn finish_pending_block(&self) -> anyhow::Result { let mut states = self.lock_write(); + + #[cfg(feature = "dev")] + let mut finished_block = states.head.require_pending_block()?.clone(); + #[cfg(not(feature = "dev"))] let finished_block = states.head.require_pending_block()?.clone(); + #[cfg(feature = "dev")] + { + // Update block timestamp only if evm_setNextBlockTimestamp was called, + // otherwise keep the original timestamp from pending block creation + if UnixTime::evm_set_next_block_timestamp_was_called() { + finished_block.header.timestamp = UnixTimeNow::default(); + } + } + // remove last state if reached limit if states.len() + 1 >= MAX_BLOCKS { let _ = states.pop();