diff --git a/Cargo.lock b/Cargo.lock index 34de36ca..eb0f40d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -884,9 +884,20 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata 0.1.10", +] + +[[package]] +name = "bstr" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", "serde", @@ -1773,6 +1784,7 @@ dependencies = [ "eyre", "futures 0.3.28", "hex", + "httptest", "itertools", "jsonrpc-core 18.0.0 (git+https://github.com/matter-labs/jsonrpc.git?branch=master)", "jsonrpc-core-client", @@ -2312,7 +2324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ "aho-corasick", - "bstr", + "bstr 1.6.2", "fnv", "log", "regex", @@ -2692,6 +2704,28 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httptest" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f25cfb6def593d43fae1ead24861f217e93bc70768a45cc149a69b5f049df4" +dependencies = [ + "bstr 0.2.17", + "bytes 1.4.0", + "crossbeam-channel 0.5.8", + "form_urlencoded", + "futures 0.3.28", + "http", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + [[package]] name = "humantime" version = "2.1.0" @@ -3453,9 +3487,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" diff --git a/Cargo.toml b/Cargo.toml index 0817de94..c7dc772e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,4 +43,7 @@ serde_json = "1.0.67" bigdecimal = { version = "0.2.0" } hex = "0.4" ethabi = "16.0.0" -itertools = "0.10.5" \ No newline at end of file +itertools = "0.10.5" + +[dev-dependencies] +httptest = "0.15.4" \ No newline at end of file diff --git a/src/fork.rs b/src/fork.rs index 9e7d3ba3..6a224b01 100644 --- a/src/fork.rs +++ b/src/fork.rs @@ -14,7 +14,7 @@ use tokio::runtime::Builder; use zksync_basic_types::{Address, L1BatchNumber, L2ChainId, MiniblockNumber, H256, U256, U64}; use zksync_types::{ - api::{BlockIdVariant, BlockNumber, Transaction}, + api::{Block, BlockIdVariant, BlockNumber, Transaction, TransactionVariant}, l2::L2Tx, StorageKey, }; @@ -205,6 +205,13 @@ pub trait ForkSource { &self, block_number: MiniblockNumber, ) -> eyre::Result>; + + /// Returns the block for a given hash. + fn get_block_by_hash( + &self, + hash: H256, + full_transactions: bool, + ) -> eyre::Result>>; } /// Holds the information about the original chain. @@ -216,6 +223,7 @@ pub struct ForkDetails { // Block number at which we forked (the next block to create is l1_block + 1) pub l1_block: L1BatchNumber, pub l2_miniblock: u64, + pub l2_miniblock_hash: Option, pub block_timestamp: u64, pub overwrite_chain_id: Option, pub l1_gas_price: u64, @@ -248,6 +256,7 @@ impl ForkDetails { l1_block: l1_batch_number, block_timestamp: block_details.base.timestamp, l2_miniblock: miniblock, + l2_miniblock_hash: block_details.base.root_hash, overwrite_chain_id: chain_id, l1_gas_price: block_details.base.l1_gas_price, } diff --git a/src/http_fork_source.rs b/src/http_fork_source.rs index d71f0eb1..773f44a7 100644 --- a/src/http_fork_source.rs +++ b/src/http_fork_source.rs @@ -59,4 +59,14 @@ impl ForkSource for HttpForkSource { block_on(async move { client.get_raw_block_transactions(block_number).await }) .wrap_err("fork http client failed") } + + fn get_block_by_hash( + &self, + hash: zksync_basic_types::H256, + full_transactions: bool, + ) -> eyre::Result>> { + let client = self.create_client(); + block_on(async move { client.get_block_by_hash(hash, full_transactions).await }) + .wrap_err("fork http client failed") + } } diff --git a/src/lib.rs b/src/lib.rs index 1796fa15..ae14e6ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,5 @@ pub mod node; pub mod resolver; pub mod utils; pub mod zks; + +mod testing; diff --git a/src/main.rs b/src/main.rs index b860430f..7c87dd69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,7 @@ mod formatter; mod http_fork_source; mod node; mod resolver; +mod testing; mod utils; mod zks; diff --git a/src/node.rs b/src/node.rs index 6227ec4a..1b0f7ec4 100644 --- a/src/node.rs +++ b/src/node.rs @@ -27,7 +27,7 @@ use vm::{ }, HistoryDisabled, HistoryEnabled, OracleTools, TxRevertReason, VmBlockResult, }; -use zksync_basic_types::{AccountTreeId, Bytes, H160, H256, U256, U64}; +use zksync_basic_types::{web3::signing::keccak256, AccountTreeId, Bytes, H160, H256, U256, U64}; use zksync_contracts::{ read_playground_block_bootloader_bytecode, read_sys_contract_bytecode, read_zbin_bytecode, BaseSystemContracts, ContractLanguage, SystemContractCode, @@ -37,7 +37,7 @@ use zksync_core::api_server::web3::backend_jsonrpc::{ }; use zksync_state::{ReadStorage, StorageView, WriteStorage}; use zksync_types::{ - api::{Log, TransactionReceipt, TransactionVariant}, + api::{Log, TransactionReceipt}, fee::Fee, get_code_key, get_nonce_key, l2::L2Tx, @@ -84,13 +84,23 @@ pub const ESTIMATE_GAS_SCALE_FACTOR: f32 = 1.3; /// Basic information about the generated block (which is block l1 batch and miniblock). /// Currently, this test node supports exactly one transaction per block. +#[derive(Debug, Clone)] pub struct BlockInfo { pub batch_number: u32, pub block_timestamp: u64, - /// Transaction included in this block. + /// Hash associated with this block. + pub hash: H256, + /// Transaction included in this block pub tx_hash: H256, } +impl BlockInfo { + pub fn compute_hash(block_number: u32, tx_hash: H256) -> H256 { + let digest = [&block_number.to_be_bytes()[..], tx_hash.as_bytes()].concat(); + H256(keccak256(&digest)) + } +} + /// Information about the executed transaction. pub struct TxExecutionInfo { pub tx: L2Tx, @@ -200,7 +210,9 @@ pub struct InMemoryNodeInner { // Map from transaction to details about the exeuction pub tx_results: HashMap, // Map from batch number to information about the block. - pub blocks: HashMap, + pub blocks: HashMap, + // Map from batch number to information about the block. + pub block_hashes: HashMap, // Underlying storage pub fork_storage: ForkStorage, // Debug level information. @@ -664,6 +676,7 @@ impl InMemoryNode { .unwrap_or(L1_GAS_PRICE), tx_results: Default::default(), blocks: Default::default(), + block_hashes: Default::default(), fork_storage: ForkStorage::new(fork, dev_use_local_contracts), show_calls, show_storage_logs, @@ -799,6 +812,7 @@ impl InMemoryNode { let block = BlockInfo { batch_number: block_context.block_number, block_timestamp: block_context.block_timestamp, + hash: BlockInfo::compute_hash(block_context.block_number, l2_tx.hash()), tx_hash: l2_tx.hash(), }; @@ -948,7 +962,8 @@ impl InMemoryNode { result, }, ); - inner.blocks.insert(block.batch_number, block); + inner.block_hashes.insert(block.batch_number, block.hash); + inner.blocks.insert(block.hash, block); { inner.current_timestamp += 1; inner.current_batch += 1; @@ -1342,7 +1357,7 @@ impl EthNamespaceT for fn get_block_by_hash( &self, hash: zksync_basic_types::H256, - _full_transactions: bool, + full_transactions: bool, ) -> jsonrpc_core::BoxFuture< jsonrpc_core::Result< Option>, @@ -1356,25 +1371,58 @@ impl EthNamespaceT for .read() .map_err(|_| into_jsrpc_error(Web3Error::InternalError))?; - let matching_transaction = reader.tx_results.get(&hash); - if matching_transaction.is_none() { - return Err(into_jsrpc_error(Web3Error::InvalidTransactionData( - zksync_types::ethabi::Error::InvalidData, - ))); - } + // try retrieving block from memory and subsequently from fork, if unavailable + let mut fetched_block = false; + let matching_block = reader.blocks.get(&hash).cloned().or_else(|| { + reader + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_block_by_hash(hash, full_transactions) + .ok() + .flatten() + .map(|block| { + fetched_block = true; + BlockInfo { + batch_number: block + .l1_batch_number + .unwrap_or_default() + .as_u32(), + block_timestamp: block.timestamp.as_u64(), + hash: block.hash, + tx_hash: Default::default(), + } + }) + }) + }); - let matching_block = reader - .blocks - .get(&matching_transaction.unwrap().batch_number); - if matching_block.is_none() { + let Some(matching_block) = matching_block else { return Err(into_jsrpc_error(Web3Error::NoBlock)); + }; + + let current_batch = reader.current_batch; + drop(reader); + + if fetched_block { + let mut writer = inner + .write() + .map_err(|_| into_jsrpc_error(Web3Error::InternalError))?; + + writer + .block_hashes + .insert(matching_block.batch_number, hash); + writer.blocks.insert(hash, matching_block.clone()); } - let txn: Vec = vec![]; let block = zksync_types::api::Block { - transactions: txn, - number: U64::from(matching_block.unwrap().batch_number), - l1_batch_number: Some(U64::from(reader.current_batch)), + hash, + number: U64::from(matching_block.batch_number), + l1_batch_number: Some(U64::from(current_batch)), gas_limit: U256::from(ETH_CALL_GAS_LIMIT), ..Default::default() }; @@ -1637,3 +1685,147 @@ impl EthNamespaceT for not_implemented("fee history") } } + +#[cfg(test)] +mod tests { + use zksync_types::{Address, L2ChainId, Nonce, PackedEthSignature}; + + use crate::{http_fork_source::HttpForkSource, node::InMemoryNode}; + + use super::*; + + #[tokio::test] + async fn test_get_block_by_hash() { + let node = InMemoryNode::::default(); + + let private_key = H256::random(); + let from_account = PackedEthSignature::address_from_private_key(&private_key) + .expect("failed generating address"); + node.set_rich_account(from_account); + let mut tx = L2Tx::new_signed( + Address::random(), + vec![], + Nonce(0), + Fee { + gas_limit: U256::from(1_000_000), + max_fee_per_gas: U256::from(250_000_000), + max_priority_fee_per_gas: U256::from(250_000_000), + gas_per_pubdata_limit: U256::from(20000), + }, + U256::from(1), + L2ChainId(260), + &private_key, + None, + Default::default(), + ) + .unwrap(); + tx.set_input(vec![], H256::repeat_byte(0x01)); + + node.apply_txs(vec![tx.into()]).expect("failed applying tx"); + + let expected_block_hash = + H256::from_str("0x89c0aa770eba1f187235bdad80de9c01fe81bca415d442ca892f087da56fa109") + .unwrap(); + let actual_block = node + .get_block_by_hash(expected_block_hash, false) + .await + .expect("failed fetching block by hash") + .expect("no block"); + + assert_eq!(expected_block_hash, actual_block.hash); + assert_eq!(U64::from(1), actual_block.number); + assert_eq!(Some(U64::from(2)), actual_block.l1_batch_number); + } + + #[tokio::test] + async fn test_get_block_by_hash_uses_fork_source_and_caches_it() { + let input_block_hash_str = + "0x0101010101010101010101010101010101010101010101010101010101010101"; + let input_block_hash = H256::from_str(input_block_hash_str).expect("invalid hash"); + + let mock_server = crate::testing::MockServer::run(); + + // mock a single eth_getBlockByHash call + mock_server.expect(serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "eth_getBlockByHash", + "params": [ + input_block_hash_str, + false + ], + }), serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "result": { + "hash": input_block_hash_str, + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "miner": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "number": "0x1", + "l1BatchNumber": "0x2", + "gasUsed": "0x0", + "gasLimit": "0xffffffff", + "baseFeePerGas": "0x1dcd6500", + "extraData": "0x", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x63ecc41a", + "l1BatchTimestamp": "0x63ecbd12", + "difficulty": "0x0", + "totalDifficulty": "0x0", + "sealFields": [], + "uncles": [], + "transactions": [], + "size": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000" + }, + })); + let node = InMemoryNode::::new( + Some(ForkDetails::from_network(&mock_server.url(), None).await), + crate::node::ShowCalls::None, + ShowStorageLogs::None, + ShowVMDetails::None, + false, + false, + ); + + let actual_block = node + .get_block_by_hash(input_block_hash, false) + .await + .expect("failed fetching block by hash") + .expect("no block"); + + assert_eq!(input_block_hash, actual_block.hash); + assert_eq!(U64::from(2), actual_block.number); + assert_eq!( + Some(U64::from(node.inner.read().unwrap().current_batch)), + actual_block.l1_batch_number + ); + assert_eq!( + Some(input_block_hash), + node.inner + .read() + .unwrap() + .block_hashes + .get(&actual_block.number.as_u32()) + .copied() + ); + + let actual_cached_block = node + .get_block_by_hash(input_block_hash, false) + .await + .expect("failed fetching cached block by hash") + .expect("no block"); + + assert_eq!(input_block_hash, actual_cached_block.hash); + assert_eq!(U64::from(2), actual_cached_block.number); + assert_eq!( + Some(U64::from(node.inner.read().unwrap().current_batch)), + actual_cached_block.l1_batch_number + ); + } +} diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 00000000..55464114 --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,102 @@ +//! This file hold testing helpers for other unit tests. +//! +//! There is MockServer that can help simulate a forked network. +//! + +#![cfg(test)] + +use httptest::{ + matchers::{eq, json_decoded, request}, + responders::json_encoded, + Expectation, Server, +}; + +/// A HTTP server that can be used to mock a fork source. +pub struct MockServer { + /// The implementation for [httptest::Server]. + pub inner: Server, +} + +impl MockServer { + /// Start the mock server with pre-defined calls used to fetch the fork's state. + pub fn run() -> Self { + let server = Server::run(); + + // setup initial fork calls + server.expect( + Expectation::matching(request::body(json_decoded(eq(serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "eth_blockNumber", + }))))) + .respond_with(json_encoded(serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "result": "0xa", + }))), + ); + server.expect( + Expectation::matching(request::body(json_decoded(eq(serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "zks_getBlockDetails", + "params": vec![ 10 ], + }))))) + .respond_with(json_encoded(serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "number": 10, + "l1BatchNumber": 5, + "timestamp": 1676461082u64, + "l1TxCount": 0, + "l2TxCount": 0, + "rootHash": "0x086c9487350539c884510044efce5e3f2aaffca4215c12b9044506375097fecd", + "status": "verified", + "commitTxHash": "0x9f5b07e968787514667fae74e77ecab766be42acd602c85cfdbda1dc3dd9902f", + "committedAt": "2023-02-15T11:40:39.326104Z", + "proveTxHash": "0xac8fe9fdcbeb5f1e59c41e6bd33b75d405af84e4b968cd598c2d3f59c9c925c8", + "provenAt": "2023-02-15T12:42:40.073918Z", + "executeTxHash": "0x65d50174b214b05e82936c4064023cbea5f6f8135e30b4887986b316a2178a39", + "executedAt": "2023-02-15T12:43:20.330052Z", + "l1GasPrice": 29860969933u64, + "l2FairGasPrice": 500000000u64, + "baseSystemContractsHashes": { + "bootloader": "0x0100038581be3d0e201b3cc45d151ef5cc59eb3a0f146ad44f0f72abf00b594c", + "default_aa": "0x0100038dc66b69be75ec31653c64cb931678299b9b659472772b2550b703f41c" + }, + "operatorAddress": "0xfeee860e7aae671124e9a4e61139f3a5085dfeee", + "protocolVersion": null + }, + }))), + ); + server.expect( + Expectation::matching(request::body(json_decoded(eq(serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "eth_getStorageAt", + "params": vec!["0x000000000000000000000000000000000000800a","0xe9472b134a1b5f7b935d5debff2691f95801214eafffdeabbf0e366da383104e","0xa"], + }))))).times(0..) + .respond_with(json_encoded(serde_json::json!({ + "jsonrpc": "2.0", + "id": 0, + "result": "0x0000000000000000000000000000000000000000000000000000000000000000", + }))), + ); + + MockServer { inner: server } + } + + /// Retrieve the mock server's url. + pub fn url(&self) -> String { + self.inner.url("").to_string() + } + + /// Assert an exactly single call expectation with a given request and the provided response. + pub fn expect(&self, request: serde_json::Value, response: serde_json::Value) { + self.inner.expect( + Expectation::matching(request::body(json_decoded(eq(request)))) + .respond_with(json_encoded(response)), + ); + } +}