diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 0206b109..905d4f0e 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -14,6 +14,8 @@ The `status` options are: | Namespace | API |
Status
| Description | | --- | --- | --- | --- | +| `ANVIL` | `anvil_setBlockTimestampInterval` | `SUPPORTED` | Sets the block timestamp interval | +| `ANVIL` | `anvil_removeBlockTimestampInterval` | `SUPPORTED` | Removes the block timestamp interval | | `ANVIL` | `anvil_setMinGasPrice` | `NOT IMPLEMENTED` | Set the minimum gas price for the node. Unsupported for ZKsync as it is only relevant for pre-EIP1559 chains | | `ANVIL` | `anvil_setLoggingEnabled` | `SUPPORTED` | Enables or disables logging | | `ANVIL` | `anvil_snapshot` | `SUPPORTED` | Snapshot the state of the blockchain at the current block | diff --git a/e2e-tests/test/anvil-apis.test.ts b/e2e-tests/test/anvil-apis.test.ts index 82cc75b4..2afffdbd 100644 --- a/e2e-tests/test/anvil-apis.test.ts +++ b/e2e-tests/test/anvil-apis.test.ts @@ -11,6 +11,47 @@ import * as fs from "node:fs"; const provider = getTestProvider(); +describe("anvil_setBlockTimestampInterval & anvil_removeBlockTimestampInterval", function () { + it("Should control timestamp interval between blocks", async function () { + // Arrange + const interval = 42; + let expectedTimestamp: number = await provider.send("config_getCurrentTimestamp", []); + expectedTimestamp += interval; + const wallet = new Wallet(RichAccounts[0].PrivateKey, provider); + const userWallet = Wallet.createRandom().connect(provider); + + // Set interval + await provider.send("anvil_setBlockTimestampInterval", [interval]); + + const txResponse = await wallet.sendTransaction({ + to: userWallet.address, + value: ethers.utils.parseEther("0.1"), + }); + const txReceipt = await txResponse.wait(); + + // Assert new block is `interval` apart from start + const newBlockTimestamp = (await provider.getBlock(txReceipt.blockNumber)).timestamp; + expect(newBlockTimestamp).to.equal(expectedTimestamp); + + // Accomodate for virtual block + expectedTimestamp += interval; + + // Remove interval + const result: boolean = await provider.send("anvil_removeBlockTimestampInterval", []); + expect(result); + + const txResponse2 = await wallet.sendTransaction({ + to: userWallet.address, + value: ethers.utils.parseEther("0.1"), + }); + const txReceipt2 = await txResponse2.wait(); + + // Assert new block is `1` apart from previous block + const newBlockTimestamp2 = (await provider.getBlock(txReceipt2.blockNumber)).timestamp; + expect(newBlockTimestamp2).to.equal(expectedTimestamp + 1); + }); +}); + describe("anvil_setLoggingEnabled", function () { it("Should disable and enable logging", async function () { // Arrange diff --git a/src/namespaces/anvil.rs b/src/namespaces/anvil.rs index 61f13c00..b77e320a 100644 --- a/src/namespaces/anvil.rs +++ b/src/namespaces/anvil.rs @@ -6,6 +6,23 @@ use crate::utils::Numeric; #[rpc] pub trait AnvilNamespaceT { + /// Sets the block timestamp interval. All future blocks' timestamps will + /// have the provided amount of seconds in-between of them. Does not affect + /// the block production interval. + /// + /// # Arguments + /// + /// * `seconds` - The minimum gas price to be set + #[rpc(name = "anvil_setBlockTimestampInterval")] + fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()>; + + /// Removes the block timestamp interval if it exists. + /// + /// # Returns + /// `true` if an existing interval was removed, `false` otherwise + #[rpc(name = "anvil_removeBlockTimestampInterval")] + fn remove_block_timestamp_interval(&self) -> RpcResult; + /// Set the minimum gas price for the node. Unsupported for ZKsync as it is only relevant for /// pre-EIP1559 chains. /// diff --git a/src/node/anvil.rs b/src/node/anvil.rs index ff208bd0..96ef63a2 100644 --- a/src/node/anvil.rs +++ b/src/node/anvil.rs @@ -12,6 +12,15 @@ use crate::{ impl AnvilNamespaceT for InMemoryNode { + fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()> { + self.time.set_block_timestamp_interval(seconds); + Ok(()).into_boxed_future() + } + + fn remove_block_timestamp_interval(&self) -> RpcResult { + Ok(self.time.remove_block_timestamp_interval()).into_boxed_future() + } + fn set_min_gas_price(&self, _gas: U256) -> RpcResult<()> { tracing::info!("anvil_setMinGasPrice is unsupported as ZKsync is a post-EIP1559 chain"); Err(into_jsrpc_error(Web3Error::MethodNotImplemented)).into_boxed_future() diff --git a/src/node/block_producer.rs b/src/node/block_producer.rs index 8f81a15e..4302eb07 100644 --- a/src/node/block_producer.rs +++ b/src/node/block_producer.rs @@ -46,7 +46,7 @@ impl Future for BlockProducer { .contracts(TxExecutionMode::VerifyExecute, impersonating) .clone(); pin.node - .seal_block(txs, base_system_contracts) + .seal_block(&mut pin.node.time.lock(), txs, base_system_contracts) .expect("block sealing failed"); } } diff --git a/src/node/config_api.rs b/src/node/config_api.rs index cbc03ccc..4536db88 100644 --- a/src/node/config_api.rs +++ b/src/node/config_api.rs @@ -1,5 +1,6 @@ use zksync_web3_decl::error::Web3Error; +use crate::node::time::ReadTime; use crate::{ config::show_details::{ShowCalls, ShowGasDetails, ShowStorageLogs, ShowVMDetails}, fork::ForkSource, @@ -37,7 +38,7 @@ impl Configurat } fn config_get_current_timestamp(&self) -> Result { - Ok(self.time.last_timestamp()) + Ok(self.time.current_timestamp()) } fn config_set_show_calls(&self, value: String) -> Result { diff --git a/src/node/debug.rs b/src/node/debug.rs index 0e2ff817..9dbe6be5 100644 --- a/src/node/debug.rs +++ b/src/node/debug.rs @@ -144,6 +144,7 @@ impl DebugNames ) -> RpcResult { let only_top = options.is_some_and(|o| o.tracer_config.only_top_call); let inner = self.get_inner().clone(); + let time = self.time.clone(); Box::pin(async move { if block.is_some() && !matches!(block, Some(BlockId::Number(BlockNumber::Latest))) { return Err(jsonrpc_core::Error::invalid_params( @@ -165,7 +166,8 @@ impl DebugNames let storage = StorageView::new(&inner.fork_storage).into_rc_ptr(); // init vm - let (mut l1_batch_env, _block_context) = inner.create_l1_batch_env(storage.clone()); + let (mut l1_batch_env, _block_context) = + inner.create_l1_batch_env(&time, storage.clone()); // update the enforced_base_fee within l1_batch_env to match the logic in zksync_core l1_batch_env.enforced_base_fee = Some(l2_tx.common_data.fee.max_fee_per_gas.as_u64()); diff --git a/src/node/eth.rs b/src/node/eth.rs index 55ba5ed2..d284e152 100644 --- a/src/node/eth.rs +++ b/src/node/eth.rs @@ -662,7 +662,7 @@ impl EthNamespa } }; - let result: jsonrpc_core::Result = reader.estimate_gas_impl(req); + let result: jsonrpc_core::Result = reader.estimate_gas_impl(&self.time, req); match result { Ok(fee) => Ok(fee.gas_limit).into_boxed_future(), Err(err) => return futures::future::err(err).boxed(), @@ -2841,7 +2841,7 @@ mod tests { inner.current_batch = 1; inner.current_miniblock = 1; inner.current_miniblock_hash = H256::repeat_byte(0x1); - inner.time.set_last_timestamp_unchecked(1); + node.time.set_current_timestamp_unchecked(1); inner .filters .add_block_filter() @@ -2858,7 +2858,6 @@ mod tests { let storage = inner.fork_storage.inner.read().unwrap(); let expected_snapshot = Snapshot { - current_timestamp: inner.time.last_timestamp(), current_batch: inner.current_batch, current_miniblock: inner.current_miniblock, current_miniblock_hash: inner.current_miniblock_hash, @@ -2876,10 +2875,6 @@ mod tests { }; let actual_snapshot = inner.snapshot().expect("failed taking snapshot"); - assert_eq!( - expected_snapshot.current_timestamp, - actual_snapshot.current_timestamp - ); assert_eq!( expected_snapshot.current_batch, actual_snapshot.current_batch @@ -2947,7 +2942,7 @@ mod tests { inner.current_batch = 1; inner.current_miniblock = 1; inner.current_miniblock_hash = H256::repeat_byte(0x1); - inner.time.set_last_timestamp_unchecked(1); + node.time.set_current_timestamp_unchecked(1); inner .filters .add_block_filter() @@ -2965,7 +2960,6 @@ mod tests { let expected_snapshot = { let storage = inner.fork_storage.inner.read().unwrap(); Snapshot { - current_timestamp: inner.time.last_timestamp(), current_batch: inner.current_batch, current_miniblock: inner.current_miniblock, current_miniblock_hash: inner.current_miniblock_hash, @@ -3000,7 +2994,7 @@ mod tests { inner.current_batch = 2; inner.current_miniblock = 2; inner.current_miniblock_hash = H256::repeat_byte(0x2); - inner.time.set_last_timestamp_unchecked(2); + node.time.set_current_timestamp_unchecked(2); inner .filters .add_pending_transaction_filter() @@ -3021,10 +3015,6 @@ mod tests { .expect("failed restoring snapshot"); let storage = inner.fork_storage.inner.read().unwrap(); - assert_eq!( - expected_snapshot.current_timestamp, - inner.time.last_timestamp() - ); assert_eq!(expected_snapshot.current_batch, inner.current_batch); assert_eq!(expected_snapshot.current_miniblock, inner.current_miniblock); assert_eq!( diff --git a/src/node/in_memory.rs b/src/node/in_memory.rs index 7a723495..86abc513 100644 --- a/src/node/in_memory.rs +++ b/src/node/in_memory.rs @@ -47,7 +47,7 @@ use zksync_utils::{bytecode::hash_bytecode, h256_to_account_address, h256_to_u25 use zksync_web3_decl::error::Web3Error; use crate::node::impersonate::{ImpersonationManager, ImpersonationState}; -use crate::node::time::TimestampManager; +use crate::node::time::{AdvanceTime, ReadTime, TimestampManager}; use crate::node::TxPool; use crate::{ bootloader_debug::{BootloaderDebug, BootloaderDebugTracer}, @@ -196,8 +196,6 @@ impl TransactionResult { /// S - is the Source of the Fork. #[derive(Clone)] pub struct InMemoryNodeInner { - /// Supplies timestamps that are unique across the system. - pub time: TimestampManager, /// The latest batch number that was already generated. /// Next block will be current_batch + 1 pub current_batch: u32, @@ -240,7 +238,7 @@ impl InMemoryNodeInner { pub fn new( fork: Option, config: &TestNodeConfig, - time: TimestampManager, + time: &TimestampManager, impersonation: ImpersonationManager, ) -> Self { let updated_config = config.clone(); @@ -267,10 +265,9 @@ impl InMemoryNodeInner { f.estimate_gas_scale_factor, ) }; - time.set_last_timestamp_unchecked(f.block_timestamp); + time.set_current_timestamp_unchecked(f.block_timestamp); InMemoryNodeInner { - time, current_batch: f.l1_block.0, current_miniblock: f.l2_miniblock, current_miniblock_hash: f.l2_miniblock_hash, @@ -302,10 +299,9 @@ impl InMemoryNodeInner { let mut blocks = HashMap::>::new(); blocks.insert(block_hash, create_genesis(NON_FORK_FIRST_BLOCK_TIMESTAMP)); let fee_input_provider = TestNodeFeeInputProvider::default(); - time.set_last_timestamp_unchecked(NON_FORK_FIRST_BLOCK_TIMESTAMP); + time.set_current_timestamp_unchecked(NON_FORK_FIRST_BLOCK_TIMESTAMP); InMemoryNodeInner { - time, current_batch: 0, current_miniblock: 0, current_miniblock_hash: block_hash, @@ -338,31 +334,28 @@ impl InMemoryNodeInner { /// We compute l1/l2 block details from storage to support fork testing, where the storage /// can be updated mid execution and no longer matches with the initial node's state. /// The L1 & L2 timestamps are also compared with node's timestamp to ensure it always increases monotonically. - pub fn create_l1_batch_env( + pub fn create_l1_batch_env( &self, + time: &T, storage: StoragePtr, ) -> (L1BatchEnv, BlockContext) { tracing::debug!("Creating l1 batch env..."); - let (last_l1_block_num, last_l1_block_ts) = load_last_l1_batch(storage.clone()) - .map(|(num, ts)| (num as u32, ts)) - .unwrap_or_else(|| (self.current_batch, self.time.last_timestamp())); + let last_l1_block_num = load_last_l1_batch(storage.clone()) + .map(|(num, _)| num as u32) + .unwrap_or(self.current_batch); let last_l2_block = load_last_l2_block(&storage).unwrap_or_else(|| L2Block { number: self.current_miniblock as u32, hash: L2BlockHasher::legacy_hash(L2BlockNumber(self.current_miniblock as u32)), - timestamp: self.time.last_timestamp(), + timestamp: time.current_timestamp(), }); - let latest_timestamp = std::cmp::max( - std::cmp::max(last_l1_block_ts, last_l2_block.timestamp), - self.time.last_timestamp(), - ); - let block_ctx = BlockContext::from_current( - last_l1_block_num, - last_l2_block.number as u64, - latest_timestamp, - ) - .new_batch(); + let block_ctx = BlockContext { + hash: H256::zero(), + batch: last_l1_block_num.saturating_add(1), + miniblock: (last_l2_block.number as u64).saturating_add(1), + timestamp: time.peek_next_timestamp(), + }; let fee_input = if let Some(fork) = &self .fork_storage @@ -434,8 +427,9 @@ impl InMemoryNodeInner { /// # Returns /// /// A `Result` with a `Fee` representing the estimated gas related data. - pub fn estimate_gas_impl( + pub fn estimate_gas_impl( &self, + time: &T, req: zksync_types::transaction_request::CallRequest, ) -> jsonrpc_core::Result { let mut request_with_gas_per_pubdata_overridden = req; @@ -505,7 +499,7 @@ impl InMemoryNodeInner { let storage = storage_view.into_rc_ptr(); let execution_mode = TxExecutionMode::EstimateFee; - let (mut batch_env, _) = self.create_l1_batch_env(storage.clone()); + let (mut batch_env, _) = self.create_l1_batch_env(time, storage.clone()); batch_env.fee_input = fee_input; let system_env = self.create_system_env(system_contracts, execution_mode); @@ -800,7 +794,6 @@ impl InMemoryNodeInner { .map_err(|err| format!("failed acquiring read lock on storage: {:?}", err))?; Ok(Snapshot { - current_timestamp: self.time.last_timestamp(), current_batch: self.current_batch, current_miniblock: self.current_miniblock, current_miniblock_hash: self.current_miniblock_hash, @@ -826,8 +819,6 @@ impl InMemoryNodeInner { .write() .map_err(|err| format!("failed acquiring write lock on storage: {:?}", err))?; - self.time - .set_last_timestamp_unchecked(snapshot.current_timestamp); self.current_batch = snapshot.current_batch; self.current_miniblock = snapshot.current_miniblock; self.current_miniblock_hash = snapshot.current_miniblock_hash; @@ -845,13 +836,64 @@ impl InMemoryNodeInner { Ok(()) } + + fn apply_block( + &mut self, + time: &mut T, + block: Block, + index: u32, + ) { + // archive current state before we produce new batch/blocks + if let Err(err) = self.archive_state() { + tracing::error!( + "failed archiving state for block {}: {}", + self.current_miniblock, + err + ); + } + + self.current_miniblock = self.current_miniblock.saturating_add(1); + let expected_timestamp = time.advance_timestamp(); + + let actual_l1_batch_number = block + .l1_batch_number + .expect("block must have a l1_batch_number"); + if actual_l1_batch_number.as_u32() != self.current_batch { + panic!( + "expected next block to have batch_number {}, got {}", + self.current_batch, + actual_l1_batch_number.as_u32() + ); + } + + if block.number.as_u64() != self.current_miniblock { + panic!( + "expected next block to have miniblock {}, got {} | {index}", + self.current_miniblock, + block.number.as_u64() + ); + } + + if block.timestamp.as_u64() != expected_timestamp { + panic!( + "expected next block to have timestamp {}, got {} | {index}", + expected_timestamp, + block.timestamp.as_u64() + ); + } + + let block_hash = block.hash; + self.current_miniblock_hash = block_hash; + self.block_hashes.insert(block.number.as_u64(), block.hash); + self.blocks.insert(block.hash, block); + self.filters.notify_new_block(block_hash); + } } /// Creates a restorable snapshot for the [InMemoryNodeInner]. The snapshot contains all the necessary /// data required to restore the [InMemoryNodeInner] state to a previous point in time. #[derive(Debug, Clone, Default)] pub struct Snapshot { - pub(crate) current_timestamp: u64, pub(crate) current_batch: u32, pub(crate) current_miniblock: u64, pub(crate) current_miniblock_hash: H256, @@ -922,7 +964,7 @@ impl InMemoryNode { pool: TxPool, ) -> Self { let system_contracts_options = config.system_contracts_options; - let inner = InMemoryNodeInner::new(fork, config, time.clone(), impersonation.clone()); + let inner = InMemoryNodeInner::new(fork, config, &time, impersonation.clone()); InMemoryNode { inner: Arc::new(RwLock::new(inner)), snapshots: Default::default(), @@ -979,12 +1021,7 @@ impl InMemoryNode { pub fn reset(&self, fork: Option) -> Result<(), String> { let config = self.get_config()?; - let inner = InMemoryNodeInner::new( - fork, - &config, - TimestampManager::default(), - ImpersonationManager::default(), - ); + let inner = InMemoryNodeInner::new(fork, &config, &self.time, self.impersonation.clone()); let mut writer = self .snapshots @@ -1021,11 +1058,13 @@ impl InMemoryNode { pub fn apply_txs(&self, txs: Vec) -> anyhow::Result<()> { tracing::info!("Running {:?} transactions (one per batch)", txs.len()); + // Lock time so that the produced blocks are guaranteed to be sequential in time. + let mut time = self.time.lock(); for tx in txs { // Getting contracts is reasonably cheap, so we don't cache them. We may need differing contracts // depending on whether impersonation should be enabled for a transaction. let system_contracts = self.system_contracts_for_tx(tx.initiator_account())?; - self.seal_block(vec![tx], system_contracts)?; + self.seal_block(&mut time, vec![tx], system_contracts)?; } Ok(()) @@ -1103,7 +1142,7 @@ impl InMemoryNode { // init vm - let (batch_env, _) = inner.create_l1_batch_env(storage.clone()); + let (batch_env, _) = inner.create_l1_batch_env(&self.time, storage.clone()); let system_env = inner.create_system_env(base_contracts, execution_mode); let mut vm: Vm<_, HistoryDisabled> = Vm::new(batch_env, system_env, storage.clone()); @@ -1642,8 +1681,12 @@ impl InMemoryNode { Ok(()) } - pub fn seal_block( + // Requirement for `TimeExclusive` ensures that we have exclusive writeable access to time + // manager. Meaning we can construct blocks and apply them without worrying about TOCTOU with + // timestamps. + pub fn seal_block( &self, + time: &mut T, txs: Vec, system_contracts: BaseSystemContracts, ) -> anyhow::Result { @@ -1654,7 +1697,7 @@ impl InMemoryNode { .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))?; let storage = StorageView::new(inner.fork_storage.clone()).into_rc_ptr(); let system_env = inner.create_system_env(system_contracts, TxExecutionMode::VerifyExecute); - let (batch_env, mut block_ctx) = inner.create_l1_batch_env(storage.clone()); + let (batch_env, mut block_ctx) = inner.create_l1_batch_env(time, storage.clone()); drop(inner); let mut vm: Vm<_, HistoryDisabled> = @@ -1733,7 +1776,8 @@ impl InMemoryNode { gas_used, logs_bloom, ); - let mut blocks = vec![block]; + inner.current_batch = inner.current_batch.saturating_add(1); + inner.apply_block(time, block, 0); // Hack to ensure we don't mine twice the amount of requested empty blocks (i.e. one per // batch). @@ -1743,7 +1787,7 @@ impl InMemoryNode { // we are adding one l2 block at the end of each batch (to handle things like remaining events etc). // You can look at insert_fictive_l2_block function in VM to see how this fake block is inserted. let parent_block_hash = block_ctx.hash; - let block_ctx = block_ctx.new_block(); + let block_ctx = block_ctx.new_block(time); let hash = compute_hash(block_ctx.miniblock, []); let virtual_block = create_block( @@ -1756,56 +1800,7 @@ impl InMemoryNode { U256::zero(), Bloom::zero(), ); - blocks.push(virtual_block); - } - - inner.current_batch = inner.current_batch.saturating_add(1); - - for (i, block) in blocks.into_iter().enumerate() { - // archive current state before we produce new batch/blocks - if let Err(err) = inner.archive_state() { - tracing::error!( - "failed archiving state for block {}: {}", - inner.current_miniblock, - err - ); - } - - inner.current_miniblock = inner.current_miniblock.saturating_add(1); - let expected_timestamp = inner.time.next_timestamp(); - - let actual_l1_batch_number = block - .l1_batch_number - .expect("block must have a l1_batch_number"); - if actual_l1_batch_number.as_u32() != inner.current_batch { - panic!( - "expected next block to have batch_number {}, got {}", - inner.current_batch, - actual_l1_batch_number.as_u32() - ); - } - - if block.number.as_u64() != inner.current_miniblock { - panic!( - "expected next block to have miniblock {}, got {} | {i}", - inner.current_miniblock, - block.number.as_u64() - ); - } - - if block.timestamp.as_u64() != expected_timestamp { - panic!( - "expected next block to have timestamp {}, got {} | {i}", - expected_timestamp, - block.timestamp.as_u64() - ); - } - - let block_hash = block.hash; - inner.current_miniblock_hash = block_hash; - inner.block_hashes.insert(block.number.as_u64(), block.hash); - inner.blocks.insert(block.hash, block); - inner.filters.notify_new_block(block_hash); + inner.apply_block(time, virtual_block, 1); } Ok(L2BlockNumber(block_ctx.miniblock as u32)) @@ -1843,33 +1838,13 @@ pub struct BlockContext { } impl BlockContext { - /// Create the current instance that represents the latest block. - pub fn from_current(batch: u32, miniblock: u64, timestamp: u64) -> Self { - Self { - hash: H256::zero(), - batch, - miniblock, - timestamp, - } - } - - /// Create the next batch instance that has all parameters incremented by `1`. - pub fn new_batch(&self) -> Self { - Self { - hash: H256::zero(), - batch: self.batch.saturating_add(1), - miniblock: self.miniblock.saturating_add(1), - timestamp: self.timestamp.saturating_add(1), - } - } - /// Create the next batch instance that uses the same batch number, and has all other parameters incremented by `1`. - pub fn new_block(&self) -> BlockContext { + pub fn new_block(&self, time: &T) -> BlockContext { Self { hash: H256::zero(), batch: self.batch, miniblock: self.miniblock.saturating_add(1), - timestamp: self.timestamp.saturating_add(1), + timestamp: time.peek_next_timestamp(), } } } @@ -1924,7 +1899,7 @@ mod tests { let inner = node.inner.read().unwrap(); let storage = StorageView::new(inner.fork_storage.clone()).into_rc_ptr(); let system_env = inner.create_system_env(system_contracts, TxExecutionMode::VerifyExecute); - let (batch_env, block_ctx) = inner.create_l1_batch_env(storage.clone()); + let (batch_env, block_ctx) = inner.create_l1_batch_env(&node.time, storage.clone()); let vm: Vm<_, HistoryDisabled> = Vm::new(batch_env.clone(), system_env, storage); (block_ctx, batch_env, vm) @@ -2021,7 +1996,8 @@ mod tests { let system_contracts = node .system_contracts_for_tx(tx.initiator_account()) .unwrap(); - node.seal_block(vec![tx], system_contracts).unwrap(); + node.seal_block(&mut node.time.lock(), vec![tx], system_contracts) + .unwrap(); let external_storage = node.inner.read().unwrap().fork_storage.clone(); // Execute next transaction using a fresh in-memory node and the external fork storage diff --git a/src/node/in_memory_ext.rs b/src/node/in_memory_ext.rs index 9c7bcf6c..1fb476a1 100644 --- a/src/node/in_memory_ext.rs +++ b/src/node/in_memory_ext.rs @@ -46,7 +46,7 @@ impl InMemoryNo /// The new timestamp value for the InMemoryNodeInner. pub fn set_next_block_timestamp(&self, timestamp: Numeric) -> Result<()> { let timestamp: u64 = timestamp.try_into().context("The timestamp is too big")?; - self.time.advance_timestamp(timestamp - 1) + self.time.enforce_next_timestamp(timestamp) } /// Set the current timestamp for the node. @@ -59,7 +59,7 @@ impl InMemoryNo /// # Returns /// The difference between the `current_timestamp` and the new timestamp for the InMemoryNodeInner. pub fn set_time(&self, timestamp: Numeric) -> Result { - Ok(self.time.set_last_timestamp_unchecked( + Ok(self.time.set_current_timestamp_unchecked( timestamp.try_into().context("The timestamp is too big")?, )) } @@ -76,7 +76,8 @@ impl InMemoryNo .read() .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) .map(|inner| inner.system_contracts.contracts_for_l2_call().clone())?; - let block_number = self.seal_block(vec![], bootloader_code.clone())?; + let block_number = + self.seal_block(&mut self.time.lock(), vec![], bootloader_code.clone())?; tracing::info!("👷 Mined block #{}", block_number); Ok("0x0".to_string()) } @@ -207,21 +208,20 @@ impl InMemoryNo if num_blocks == 0 { return Ok(()); } + if num_blocks > 1 && interval_sec == 0 { + anyhow::bail!("Provided interval is `0`; unable to produce {num_blocks} blocks with the same timestamp"); + } let bootloader_code = self .get_inner() .read() .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) .map(|inner| inner.system_contracts.contracts_for_l2_call().clone())?; - for i in 0..num_blocks { - if i != 0 { - // Accounts for the default increment of 1 done in `seal_block`. Note that - // there is no guarantee that blocks produced by this method will have *exactly* - // `interval` seconds in-between of their respective timestamps. Instead, we treat - // it as the minimum amount of time that should have passed in-between of blocks. - self.time.increase_time(interval_sec.saturating_sub(1)); - } - self.seal_block(vec![], bootloader_code.clone())?; + let mut time = self + .time + .lock_with_offsets((0..num_blocks).map(|i| i * interval_sec)); + for _ in 0..num_blocks { + self.seal_block(&mut time, vec![], bootloader_code.clone())?; } tracing::info!("👷 Mined {} blocks", num_blocks); @@ -367,8 +367,8 @@ mod tests { use super::*; use crate::fork::ForkStorage; use crate::namespaces::EthNamespaceT; - use crate::node::time::TimestampManager; - use crate::node::{InMemoryNodeInner, Snapshot, TxPool}; + use crate::node::time::{ReadTime, TimestampManager}; + use crate::node::{ImpersonationManager, InMemoryNodeInner, Snapshot, TxPool}; use crate::{http_fork_source::HttpForkSource, node::InMemoryNode}; use std::str::FromStr; use std::sync::{Arc, RwLock}; @@ -478,8 +478,9 @@ mod tests { async fn test_reset() { let old_snapshots = Arc::new(RwLock::new(vec![Snapshot::default()])); let old_system_contracts_options = Default::default(); + let time = TimestampManager::new(123); + let impersonation = ImpersonationManager::default(); let old_inner = InMemoryNodeInner:: { - time: TimestampManager::new(123), current_batch: 100, current_miniblock: 300, current_miniblock_hash: H256::random(), @@ -492,12 +493,10 @@ mod tests { config: Default::default(), console_log_handler: Default::default(), system_contracts: Default::default(), - impersonation: Default::default(), + impersonation: impersonation.clone(), rich_accounts: Default::default(), previous_states: Default::default(), }; - let time = old_inner.time.clone(); - let impersonation = old_inner.impersonation.clone(); let pool = TxPool::new(impersonation.clone()); let node = InMemoryNode:: { @@ -525,7 +524,7 @@ mod tests { assert_eq!(node.snapshots.read().unwrap().len(), 0); let inner = node.inner.read().unwrap(); - assert_eq!(inner.time.last_timestamp(), 1000); + assert_eq!(node.time.current_timestamp(), 1000); assert_eq!(inner.current_batch, 0); assert_eq!(inner.current_miniblock, 0); assert_ne!(inner.current_miniblock_hash, H256::random()); @@ -659,21 +658,13 @@ mod tests { let node = InMemoryNode::::default(); let increase_value_seconds = 0u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); let expected_response = increase_value_seconds; let actual_response = node .increase_time(increase_value_seconds.into()) .expect("failed increasing timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); assert_eq!( @@ -688,22 +679,14 @@ mod tests { let node = InMemoryNode::::default(); let increase_value_seconds = u64::MAX; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); assert_ne!(0, timestamp_before, "initial timestamp must be non zero",); let expected_response = increase_value_seconds; let actual_response = node .increase_time(increase_value_seconds.into()) .expect("failed increasing timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); assert_eq!( @@ -718,21 +701,13 @@ mod tests { let node = InMemoryNode::::default(); let increase_value_seconds = 100u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); let expected_response = increase_value_seconds; let actual_response = node .increase_time(increase_value_seconds.into()) .expect("failed increasing timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); assert_eq!( @@ -747,11 +722,7 @@ mod tests { let node = InMemoryNode::::default(); let new_timestamp = 10_000u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); assert_ne!( timestamp_before, new_timestamp, "timestamps must be different" @@ -759,15 +730,11 @@ mod tests { node.set_next_block_timestamp(new_timestamp.into()) .expect("failed setting timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + node.mine_block().expect("failed to mine a block"); + let timestamp_after = node.time.current_timestamp(); assert_eq!( - new_timestamp, - timestamp_after + 1, + new_timestamp, timestamp_after, "timestamp was not set correctly", ); } @@ -776,16 +743,14 @@ mod tests { async fn test_set_next_block_timestamp_past_fails() { let node = InMemoryNode::::default(); - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); let new_timestamp = timestamp_before + 500; node.set_next_block_timestamp(new_timestamp.into()) .expect("failed setting timestamp"); + node.mine_block().expect("failed to mine a block"); + let result = node.set_next_block_timestamp(timestamp_before.into()); assert!(result.is_err(), "expected an error for timestamp in past"); @@ -796,21 +761,13 @@ mod tests { let node = InMemoryNode::::default(); let new_timestamp = 1000u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); assert_eq!(timestamp_before, new_timestamp, "timestamps must be same"); let response = node.set_next_block_timestamp(new_timestamp.into()); assert!(response.is_err()); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!( timestamp_before, timestamp_after, "timestamp must not change", @@ -822,22 +779,14 @@ mod tests { let node = InMemoryNode::::default(); let new_time = 10_000u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); assert_ne!(timestamp_before, new_time, "timestamps must be different"); let expected_response = 9000; let actual_response = node .set_time(new_time.into()) .expect("failed setting timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); assert_eq!(new_time, timestamp_after, "timestamp was not set correctly",); @@ -848,22 +797,14 @@ mod tests { let node = InMemoryNode::::default(); let new_time = 10u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); assert_ne!(timestamp_before, new_time, "timestamps must be different"); let expected_response = -990; let actual_response = node .set_time(new_time.into()) .expect("failed setting timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); assert_eq!(new_time, timestamp_after, "timestamp was not set correctly",); @@ -874,22 +815,14 @@ mod tests { let node = InMemoryNode::::default(); let new_time = 1000u64; - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_before = node.time.current_timestamp(); assert_eq!(timestamp_before, new_time, "timestamps must be same"); let expected_response = 0; let actual_response = node .set_time(new_time.into()) .expect("failed setting timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .expect("failed reading timestamp"); + let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); assert_eq!( @@ -903,11 +836,7 @@ mod tests { let node = InMemoryNode::::default(); for new_time in [0, u64::MAX] { - let timestamp_before = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .unwrap_or_else(|_| panic!("case {}: failed reading timestamp", new_time)); + let timestamp_before = node.time.current_timestamp(); assert_ne!( timestamp_before, new_time, "case {new_time}: timestamps must be different" @@ -917,11 +846,7 @@ mod tests { let actual_response = node .set_time(new_time.into()) .expect("failed setting timestamp"); - let timestamp_after = node - .get_inner() - .read() - .map(|inner| inner.time.last_timestamp()) - .unwrap_or_else(|_| panic!("case {}: failed reading timestamp", new_time)); + let timestamp_after = node.time.current_timestamp(); assert_eq!( expected_response, actual_response, diff --git a/src/node/time.rs b/src/node/time.rs index 068f34da..379a6e0b 100644 --- a/src/node/time.rs +++ b/src/node/time.rs @@ -1,82 +1,225 @@ use anyhow::anyhow; -use std::sync::{Arc, RwLock}; +use std::collections::VecDeque; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +/// Shared readable view on time. +pub trait ReadTime { + /// Returns timestamp (in seconds) that the clock is currently on. + fn current_timestamp(&self) -> u64; + + /// Peek at what the next call to `advance_timestamp` will return. + fn peek_next_timestamp(&self) -> u64; +} + +/// Writeable view on time management. The owner of this view should be able to treat it as +/// exclusive access to the underlying clock. +pub trait AdvanceTime: ReadTime { + /// Advances clock to the next timestamp and returns that timestamp in seconds. + /// + /// Subsequent calls to this method return monotonically increasing values. Time difference + /// between calls is implementation-specific. + fn advance_timestamp(&mut self) -> u64; +} /// Manages timestamps (in seconds) across the system. /// /// Clones always agree on the underlying timestamp and updating one affects all other instances. #[derive(Clone, Debug, Default)] pub struct TimestampManager { - /// The latest timestamp (in seconds) that has already been used. - last_timestamp: Arc>, + internal: Arc>, } impl TimestampManager { - pub fn new(last_timestamp: u64) -> TimestampManager { + pub fn new(current_timestamp: u64) -> TimestampManager { TimestampManager { - last_timestamp: Arc::new(RwLock::new(last_timestamp)), + internal: Arc::new(RwLock::new(TimestampManagerInternal { + current_timestamp, + next_timestamp: None, + interval: None, + })), } } - /// Returns the last timestamp (in seconds) that has already been used. - pub fn last_timestamp(&self) -> u64 { - *self - .last_timestamp + fn get(&self) -> RwLockReadGuard { + self.internal .read() .expect("TimestampManager lock is poisoned") } - /// Returns the next unique timestamp (in seconds) to be used. - pub fn next_timestamp(&self) -> u64 { - let mut guard = self - .last_timestamp + fn get_mut(&self) -> RwLockWriteGuard { + self.internal .write() - .expect("TimestampManager lock is poisoned"); - let next_timestamp = *guard + 1; - *guard = next_timestamp; - - next_timestamp + .expect("TimestampManager lock is poisoned") } /// Sets last used timestamp (in seconds) to the provided value and returns the difference /// between new value and old value (represented as a signed number of seconds). - pub fn set_last_timestamp_unchecked(&self, timestamp: u64) -> i128 { - let mut guard = self - .last_timestamp - .write() - .expect("TimestampManager lock is poisoned"); - let diff = (timestamp as i128).saturating_sub(*guard as i128); - *guard = timestamp; + pub fn set_current_timestamp_unchecked(&self, timestamp: u64) -> i128 { + let mut this = self.get_mut(); + let diff = (timestamp as i128).saturating_sub(this.current_timestamp as i128); + this.reset_to(timestamp); diff } - /// Advances internal timestamp (in seconds) to the provided value. + /// Forces clock to return provided value as the next timestamp. Time skip will not be performed + /// before the next invocation of `advance_timestamp`. /// /// Expects provided timestamp to be in the future, returns error otherwise. - pub fn advance_timestamp(&self, timestamp: u64) -> anyhow::Result<()> { - let mut guard = self - .last_timestamp - .write() - .expect("TimestampManager lock is poisoned"); - if timestamp < *guard { + pub fn enforce_next_timestamp(&self, timestamp: u64) -> anyhow::Result<()> { + let mut this = self.get_mut(); + if timestamp <= this.current_timestamp { Err(anyhow!( - "timestamp ({}) must be greater or equal than current timestamp ({})", + "timestamp ({}) must be greater than the last used timestamp ({})", timestamp, - *guard + this.current_timestamp )) } else { - *guard = timestamp; + this.next_timestamp.replace(timestamp); Ok(()) } } /// Fast-forwards time by the given amount of seconds. pub fn increase_time(&self, seconds: u64) -> u64 { - let mut guard = self - .last_timestamp - .write() - .expect("TimestampManager lock is poisoned"); - let next = guard.saturating_add(seconds); - *guard = next; + let mut this = self.get_mut(); + let next = this.current_timestamp.saturating_add(seconds); + this.reset_to(next); next } + + /// Sets an interval to use when computing the next timestamp + /// + /// If an interval already exists, this will update the interval, otherwise a new interval will + /// be set starting with the current timestamp. + pub fn set_block_timestamp_interval(&self, seconds: u64) { + self.get_mut().interval.replace(seconds); + } + + /// Removes the interval. Returns true if it existed before being removed, false otherwise. + pub fn remove_block_timestamp_interval(&self) -> bool { + self.get_mut().interval.take().is_some() + } + + /// Returns an exclusively owned writeable view on this [`TimeManager`] instance. + /// + /// Use this method when you need to ensure that no one else can access [`TimeManager`] during + /// this view's lifetime. + pub fn lock(&self) -> impl AdvanceTime + '_ { + self.lock_with_offsets([]) + } + + /// Returns an exclusively owned writeable view on this [`TimeManager`] instance where first N + /// timestamps will be offset by the provided amount of seconds (where `N` is the size of + /// iterator). + /// + /// Use this method when you need to ensure that no one else can access [`TimeManager`] during + /// this view's lifetime while also pre-setting first `N` returned timestamps. + pub fn lock_with_offsets<'a, I: IntoIterator>( + &'a self, + offsets: I, + ) -> impl AdvanceTime + 'a + where + ::IntoIter: 'a, + { + let guard = self.get_mut(); + TimeLockWithOffsets { + start_timestamp: guard.peek_next_timestamp(), + guard, + offsets: offsets.into_iter().collect::>(), + } + } +} + +impl ReadTime for TimestampManager { + fn current_timestamp(&self) -> u64 { + (*self.get()).current_timestamp() + } + + fn peek_next_timestamp(&self) -> u64 { + (*self.get()).peek_next_timestamp() + } +} + +#[derive(Debug, Default)] +struct TimestampManagerInternal { + /// The current timestamp (in seconds). This timestamp is considered to be used already: there + /// might be a logical event that already happened on that timestamp (e.g. a block was sealed + /// with this timestamp). + current_timestamp: u64, + /// The next timestamp (in seconds) that the clock will be forced to advance to. + next_timestamp: Option, + /// The interval to use when determining the next timestamp to advance to. + interval: Option, +} + +impl TimestampManagerInternal { + fn reset_to(&mut self, timestamp: u64) { + self.next_timestamp.take(); + self.current_timestamp = timestamp; + } + + fn interval(&self) -> u64 { + self.interval.unwrap_or(1) + } +} + +impl ReadTime for TimestampManagerInternal { + fn current_timestamp(&self) -> u64 { + self.current_timestamp + } + + fn peek_next_timestamp(&self) -> u64 { + self.next_timestamp + .unwrap_or_else(|| self.current_timestamp.saturating_add(self.interval())) + } +} + +impl AdvanceTime for TimestampManagerInternal { + fn advance_timestamp(&mut self) -> u64 { + let next_timestamp = match self.next_timestamp.take() { + Some(next_timestamp) => next_timestamp, + None => self.current_timestamp.saturating_add(self.interval()), + }; + + self.current_timestamp = next_timestamp; + next_timestamp + } +} + +struct TimeLockWithOffsets<'a> { + /// The first timestamp that would have been returned without accounting for offsets + start_timestamp: u64, + /// Exclusive writable ownership over the corresponding [`TimestampManager`] + guard: RwLockWriteGuard<'a, TimestampManagerInternal>, + /// A queue of offsets (relative to `start_timestamp`) to be used for next `N` timestamps + offsets: VecDeque, +} + +impl ReadTime for TimeLockWithOffsets<'_> { + fn current_timestamp(&self) -> u64 { + self.guard.current_timestamp() + } + + fn peek_next_timestamp(&self) -> u64 { + match self.offsets.front() { + Some(offset) => self.start_timestamp.saturating_add(*offset), + None => self.guard.peek_next_timestamp(), + } + } +} + +impl AdvanceTime for TimeLockWithOffsets<'_> { + fn advance_timestamp(&mut self) -> u64 { + match self.offsets.pop_front() { + Some(offset) => { + let timestamp = self.start_timestamp.saturating_add(offset); + // Persist last used timestamp in the underlying state as this instance can be + // dropped before we finish iterating all values. + self.guard.reset_to(timestamp); + + timestamp + } + None => self.guard.advance_timestamp(), + } + } } diff --git a/src/node/zks.rs b/src/node/zks.rs index fc383ee8..17049bda 100644 --- a/src/node/zks.rs +++ b/src/node/zks.rs @@ -47,7 +47,7 @@ impl ZksNamespa "Failed to acquire read lock for inner node state.", ))) }) - .and_then(|reader| reader.estimate_gas_impl(req)) + .and_then(|reader| reader.estimate_gas_impl(&self.time, req)) .into_boxed_future() }