From 5b7e76264dc947c82cd484aee0044997be7c8232 Mon Sep 17 00:00:00 2001 From: Dustin Brickwood Date: Wed, 27 Sep 2023 09:19:53 -0500 Subject: [PATCH 1/7] chore: quick fix for accounts endpoint --- src/node.rs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/node.rs b/src/node.rs index e7476236..4896ef46 100644 --- a/src/node.rs +++ b/src/node.rs @@ -270,6 +270,7 @@ pub struct InMemoryNodeInner { pub console_log_handler: ConsoleLogHandler, pub system_contracts: SystemContracts, pub impersonated_accounts: HashSet
, + pub rich_accounts: HashSet } type L2TxResult = ( @@ -712,6 +713,7 @@ impl InMemoryNode { console_log_handler: ConsoleLogHandler::default(), system_contracts: SystemContracts::from_options(system_contracts_options), impersonated_accounts: Default::default(), + rich_accounts: HashSet::new(), } } else { let mut block_hashes = HashMap::::new(); @@ -737,6 +739,7 @@ impl InMemoryNode { console_log_handler: ConsoleLogHandler::default(), system_contracts: SystemContracts::from_options(system_contracts_options), impersonated_accounts: Default::default(), + rich_accounts: HashSet::new(), } }; @@ -781,6 +784,8 @@ impl InMemoryNode { for (key, value) in keys.iter() { inner.fork_storage.set_value(*key, *value); } + inner.rich_accounts.insert(address); + } /// Runs L2 'eth call' method - that doesn't commit to a block. @@ -2289,11 +2294,29 @@ impl EthNamespaceT for { Ok(zksync_basic_types::web3::types::SyncState::NotSyncing).into_boxed_future() } - - fn accounts( - &self, - ) -> jsonrpc_core::BoxFuture>> { - not_implemented("accounts") + /// Returns a list of available accounts. + /// + /// This function fetches the rich accounts from the inner state, clones them, + /// and returns them as a list of addresses (`H160`). + /// + /// # Errors + /// + /// Returns a `jsonrpc_core::Result` error if acquiring a write lock on the inner state fails. + /// + /// # Returns + /// + /// A `BoxFuture` containing a `jsonrpc_core::Result` that resolves to a `Vec` of addresses. + fn accounts(&self) -> jsonrpc_core::BoxFuture>> { + let inner = Arc::clone(&self.inner); + let writer = match inner.write() { + Ok(r) => r, + Err(_) => { + return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() + } + }; + + let accounts: Vec = writer.rich_accounts.clone().into_iter().collect(); + futures::future::ok(accounts).boxed() } fn coinbase( From 1d4db346392209f2a4bbbcdaceae07b4428a9a83 Mon Sep 17 00:00:00 2001 From: MexicanAce Date: Wed, 27 Sep 2023 16:41:17 +0100 Subject: [PATCH 2/7] chore: Add fork source check for eth_getTransactionByHash. Add block_number and block_hash to transactions within run_l2_tx_inner. --- src/node.rs | 112 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/src/node.rs b/src/node.rs index 4896ef46..805b4ad5 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1176,13 +1176,18 @@ impl InMemoryNode { // The computed block hash here will be different than that in production. let hash = compute_hash(batch_env.number.0, l2_tx.hash()); + + let mut transaction = zksync_types::api::Transaction::from(l2_tx); + transaction.block_hash = Some(*inner.block_hashes.get(&inner.current_miniblock).unwrap()); + transaction.block_number = Some(U64::from(inner.current_miniblock)); + let block = Block { hash, number: U64::from(inner.current_miniblock.saturating_add(1)), timestamp: U256::from(batch_env.timestamp), l1_batch_number: Some(U64::from(batch_env.number.0)), transactions: vec![TransactionVariant::Full( - zksync_types::api::Transaction::from(l2_tx), + transaction, )], gas_used: U256::from(tx_result.statistics.gas_used), gas_limit: U256::from(BLOCK_GAS_LIMIT), @@ -1814,50 +1819,69 @@ impl EthNamespaceT for let reader = inner .read() .map_err(|_| into_jsrpc_error(Web3Error::InternalError))?; - let tx_result = reader.tx_results.get(&hash); - - Ok(tx_result.and_then(|TransactionResult { info, .. }| { - let input_data = info.tx.common_data.input.clone().or(None)?; - - let chain_id = info.tx.common_data.extract_chain_id().or(None)?; - - Some(zksync_types::api::Transaction { - hash, - nonce: U256::from(info.tx.common_data.nonce.0), - block_hash: Some(hash), - block_number: Some(U64::from(info.miniblock_number)), - transaction_index: Some(U64::from(1)), - from: Some(info.tx.initiator_account()), - to: Some(info.tx.recipient_account()), - value: info.tx.execute.value, - gas_price: Default::default(), - gas: Default::default(), - input: input_data.data.into(), - v: Some(chain_id.into()), - r: Some(U256::zero()), - s: Some(U256::zero()), - raw: None, - transaction_type: { - let tx_type = match info.tx.common_data.transaction_type { - zksync_types::l2::TransactionType::LegacyTransaction => 0, - zksync_types::l2::TransactionType::EIP2930Transaction => 1, - zksync_types::l2::TransactionType::EIP1559Transaction => 2, - zksync_types::l2::TransactionType::EIP712Transaction => 113, - zksync_types::l2::TransactionType::PriorityOpTransaction => 255, - zksync_types::l2::TransactionType::ProtocolUpgradeTransaction => 254, - }; - Some(tx_type.into()) - }, - access_list: None, - max_fee_per_gas: Some(info.tx.common_data.fee.max_fee_per_gas), - max_priority_fee_per_gas: Some( - info.tx.common_data.fee.max_priority_fee_per_gas, - ), - chain_id: chain_id.into(), - l1_batch_number: Some(U64::from(info.batch_number as u64)), - l1_batch_tx_index: None, + + + let maybe_result = { + // try retrieving transaction from memory, and if unavailable subsequently from the fork + reader.tx_results.get(&hash).and_then(|TransactionResult { info, .. }| { + let input_data = info.tx.common_data.input.clone().or(None)?; + + let chain_id = info.tx.common_data.extract_chain_id().or(None)?; + + Some(zksync_types::api::Transaction { + hash, + nonce: U256::from(info.tx.common_data.nonce.0), + block_hash: Some(hash), + block_number: Some(U64::from(info.miniblock_number)), + transaction_index: Some(U64::from(1)), + from: Some(info.tx.initiator_account()), + to: Some(info.tx.recipient_account()), + value: info.tx.execute.value, + gas_price: Default::default(), + gas: Default::default(), + input: input_data.data.into(), + v: Some(chain_id.into()), + r: Some(U256::zero()), + s: Some(U256::zero()), + raw: None, + transaction_type: { + let tx_type = match info.tx.common_data.transaction_type { + zksync_types::l2::TransactionType::LegacyTransaction => 0, + zksync_types::l2::TransactionType::EIP2930Transaction => 1, + zksync_types::l2::TransactionType::EIP1559Transaction => 2, + zksync_types::l2::TransactionType::EIP712Transaction => 113, + zksync_types::l2::TransactionType::PriorityOpTransaction => 255, + zksync_types::l2::TransactionType::ProtocolUpgradeTransaction => 254, + }; + Some(tx_type.into()) + }, + access_list: None, + max_fee_per_gas: Some(info.tx.common_data.fee.max_fee_per_gas), + max_priority_fee_per_gas: Some( + info.tx.common_data.fee.max_priority_fee_per_gas, + ), + chain_id: chain_id.into(), + l1_batch_number: Some(U64::from(info.batch_number as u64)), + l1_batch_tx_index: None, + }) + }).or_else(|| { + reader + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_transaction_by_hash(hash) + .ok() + .flatten() + }) }) - })) + }; + + Ok(maybe_result) }) } From 202ffe5fb9afd31583e77db74fe1a49f9bdef746 Mon Sep 17 00:00:00 2001 From: Dustin Brickwood Date: Wed, 27 Sep 2023 12:01:16 -0500 Subject: [PATCH 3/7] chore: finishing basic rivet support: * handles unwrap * adds unit test for eth_accounts * updates docs * updates cargo.toml version to latest --- Cargo.lock | 2 +- Cargo.toml | 2 +- SUPPORTED_APIS.md | 25 ++++++++++++++++++++++- src/node.rs | 48 +++++++++++++++++++++++++++++++++------------ test_endpoints.http | 11 +++++++++++ 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0550ca0c..e1d6b3e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1776,7 +1776,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "era_test_node" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.5" dependencies = [ "anyhow", "bigdecimal", diff --git a/Cargo.toml b/Cargo.toml index b8986232..86081cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "era_test_node" -version = "0.1.0-alpha.3" +version = "0.1.0-alpha.5" edition = "2018" authors = ["The Matter Labs Team "] homepage = "https://zksync.io/" diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index f829cf16..3ee413fe 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -25,7 +25,7 @@ The `status` options are: | `DEBUG` | `debug_traceBlockByHash` | `NOT IMPLEMENTED`
[GitHub Issue #63](https://github.com/matter-labs/era-test-node/issues/63) | Returns structured traces for operations within the block of the specified block hash | | `DEBUG` | `debug_traceBlockByNumber` | `NOT IMPLEMENTED`
[GitHub Issue #64](https://github.com/matter-labs/era-test-node/issues/64) | Returns structured traces for operations within the block of the specified block number | | `DEBUG` | `debug_traceTransaction` | `NOT IMPLEMENTED`
[GitHub Issue #65](https://github.com/matter-labs/era-test-node/issues/65) | Returns a structured trace of the execution of the specified transaction | -| `ETH` | `eth_accounts` | `NOT IMPLEMENTED`
[GitHub Issue #50](https://github.com/matter-labs/era-test-node/issues/50) | Returns a list of addresses owned by client | +| `ETH` | `eth_accounts` | `SUPPORTED` | Returns a list of addresses owned by client | | [`ETH`](#eth-namespace) | [`eth_chainId`](#eth_chainid) | `SUPPORTED` | Returns the currently configured chain id
_(default is `260`)_ | | `ETH` | `eth_coinbase` | `NOT IMPLEMENTED` | Returns the client coinbase address | | [`ETH`](#eth-namespace) | [`eth_estimateGas`](#eth_estimategas) | `SUPPORTED` | Generates and returns an estimate of how much gas is necessary for the transaction to complete | @@ -364,6 +364,29 @@ curl --request POST \ ## `ETH NAMESPACE` +### `eth_accounts` + +[source](src/node.rs) + +Returns the current chain id + +#### Arguments + ++ _NONE_ + +#### Status + +`SUPPORTED` + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{"jsonrpc": "2.0","id": "1","method": "eth_accounts","params": []}' +``` + ### `eth_chainId` [source](src/node.rs) diff --git a/src/node.rs b/src/node.rs index 805b4ad5..7f0ff7c4 100644 --- a/src/node.rs +++ b/src/node.rs @@ -270,7 +270,7 @@ pub struct InMemoryNodeInner { pub console_log_handler: ConsoleLogHandler, pub system_contracts: SystemContracts, pub impersonated_accounts: HashSet
, - pub rich_accounts: HashSet + pub rich_accounts: HashSet, } type L2TxResult = ( @@ -785,7 +785,6 @@ impl InMemoryNode { inner.fork_storage.set_value(*key, *value); } inner.rich_accounts.insert(address); - } /// Runs L2 'eth call' method - that doesn't commit to a block. @@ -1176,9 +1175,16 @@ impl InMemoryNode { // The computed block hash here will be different than that in production. let hash = compute_hash(batch_env.number.0, l2_tx.hash()); - + let mut transaction = zksync_types::api::Transaction::from(l2_tx); - transaction.block_hash = Some(*inner.block_hashes.get(&inner.current_miniblock).unwrap()); + let block_hash = inner + .block_hashes + .get(&inner.current_miniblock) + .ok_or(format!( + "Block hash not found for block: {}", + inner.current_miniblock + ))?; + transaction.block_hash = Some(*block_hash); transaction.block_number = Some(U64::from(inner.current_miniblock)); let block = Block { @@ -1186,9 +1192,7 @@ impl InMemoryNode { number: U64::from(inner.current_miniblock.saturating_add(1)), timestamp: U256::from(batch_env.timestamp), l1_batch_number: Some(U64::from(batch_env.number.0)), - transactions: vec![TransactionVariant::Full( - transaction, - )], + transactions: vec![TransactionVariant::Full(transaction)], gas_used: U256::from(tx_result.statistics.gas_used), gas_limit: U256::from(BLOCK_GAS_LIMIT), ..Default::default() @@ -1820,14 +1824,11 @@ impl EthNamespaceT for .read() .map_err(|_| into_jsrpc_error(Web3Error::InternalError))?; - let maybe_result = { // try retrieving transaction from memory, and if unavailable subsequently from the fork reader.tx_results.get(&hash).and_then(|TransactionResult { info, .. }| { let input_data = info.tx.common_data.input.clone().or(None)?; - let chain_id = info.tx.common_data.extract_chain_id().or(None)?; - Some(zksync_types::api::Transaction { hash, nonce: U256::from(info.tx.common_data.nonce.0), @@ -2320,7 +2321,7 @@ impl EthNamespaceT for } /// Returns a list of available accounts. /// - /// This function fetches the rich accounts from the inner state, clones them, + /// This function fetches the rich accounts from the inner state, clones them, /// and returns them as a list of addresses (`H160`). /// /// # Errors @@ -2338,7 +2339,7 @@ impl EthNamespaceT for return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() } }; - + let accounts: Vec = writer.rich_accounts.clone().into_iter().collect(); futures::future::ok(accounts).boxed() } @@ -2438,7 +2439,7 @@ mod tests { cache::CacheConfig, http_fork_source::HttpForkSource, node::InMemoryNode, - testing::{self, ForkBlockConfig, LogBuilder, MockServer}, + testing::{self, ForkBlockConfig, LogBuilder, MockServer, RICH_WALLETS}, }; use zksync_types::api::BlockNumber; use zksync_web3_decl::types::{SyncState, ValueOrArray}; @@ -3369,4 +3370,25 @@ mod tests { .expect("failed getting filter changes"); assert_eq!(0, result.len()); } + + #[tokio::test] + async fn test_accounts() { + let node = InMemoryNode::::default(); + + let private_key = H256::repeat_byte(0x01); + let from_account = PackedEthSignature::address_from_private_key(&private_key).unwrap(); + node.set_rich_account(from_account); + + let account_result = node.accounts().await; + let expected_accounts: Vec = vec![from_account]; + + match account_result { + Ok(accounts) => { + assert_eq!(expected_accounts, accounts); + } + Err(e) => { + panic!("Failed to fetch accounts: {:?}", e); + } + } + } } diff --git a/test_endpoints.http b/test_endpoints.http index 13613b4c..30bd0f20 100644 --- a/test_endpoints.http +++ b/test_endpoints.http @@ -560,4 +560,15 @@ content-type: application/json "id": "1", "method": "eth_feeHistory", "params": ["0x1", "latest", [25, 50 , 75]] +} + +### +POST http://localhost:8011 +content-type: application/json + +{ + "jsonrpc": "2.0", + "id": "1", + "method": "eth_accounts", + "params": [] } \ No newline at end of file From aafbaa715d80ddb08f5eec4fddf4773bbe21139d Mon Sep 17 00:00:00 2001 From: Dustin Brickwood Date: Wed, 27 Sep 2023 12:02:36 -0500 Subject: [PATCH 4/7] chore: fix docs error --- SUPPORTED_APIS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 3ee413fe..df2c441d 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -368,7 +368,7 @@ curl --request POST \ [source](src/node.rs) -Returns the current chain id +Returns a list of addresses owned by client #### Arguments From ee433c094a5630ad11b59f197260941af0e7f736 Mon Sep 17 00:00:00 2001 From: Dustin Brickwood Date: Wed, 27 Sep 2023 12:05:22 -0500 Subject: [PATCH 5/7] chore: remove uneeded import --- src/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node.rs b/src/node.rs index 7f0ff7c4..b81f19b1 100644 --- a/src/node.rs +++ b/src/node.rs @@ -2439,7 +2439,7 @@ mod tests { cache::CacheConfig, http_fork_source::HttpForkSource, node::InMemoryNode, - testing::{self, ForkBlockConfig, LogBuilder, MockServer, RICH_WALLETS}, + testing::{self, ForkBlockConfig, LogBuilder, MockServer}, }; use zksync_types::api::BlockNumber; use zksync_web3_decl::types::{SyncState, ValueOrArray}; From 7af4a588125fc7ad2544390cf35502ca96d56806 Mon Sep 17 00:00:00 2001 From: Dustin Brickwood Date: Wed, 27 Sep 2023 12:12:28 -0500 Subject: [PATCH 6/7] fix: documentation --- src/node.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node.rs b/src/node.rs index b81f19b1..9ace8599 100644 --- a/src/node.rs +++ b/src/node.rs @@ -2321,8 +2321,7 @@ impl EthNamespaceT for } /// Returns a list of available accounts. /// - /// This function fetches the rich accounts from the inner state, clones them, - /// and returns them as a list of addresses (`H160`). + /// This function fetches the accounts from the inner state, and returns them as a list of addresses (`H160`). /// /// # Errors /// From 32c3ed7b91405763ec6853b69d67f14ae9c5a607 Mon Sep 17 00:00:00 2001 From: Dustin Brickwood Date: Wed, 27 Sep 2023 15:55:23 -0500 Subject: [PATCH 7/7] chore: add e2e test --- e2e-tests/helpers/constants.ts | 24 ++++++++++++------------ e2e-tests/test/eth-apis.test.ts | 20 ++++++++++++++++++++ src/node.rs | 4 ++-- 3 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 e2e-tests/test/eth-apis.test.ts diff --git a/e2e-tests/helpers/constants.ts b/e2e-tests/helpers/constants.ts index 0a1ec928..8adca28d 100644 --- a/e2e-tests/helpers/constants.ts +++ b/e2e-tests/helpers/constants.ts @@ -1,42 +1,42 @@ -export const RichAccounts = { - 0: { +export const RichAccounts = [ + { Account: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", PrivateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", }, - 1: { + { Account: "0xa61464658AfeAf65CccaaFD3a512b69A83B77618", PrivateKey: "0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3", }, - 2: { + { Account: "0x0D43eB5B8a47bA8900d84AA36656c92024e9772e", PrivateKey: "0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e", }, - 3: { + { Account: "0xA13c10C0D5bd6f79041B9835c63f91de35A15883", PrivateKey: "0x850683b40d4a740aa6e745f889a6fdc8327be76e122f5aba645a5b02d0248db8", }, - 4: { + { Account: "0x8002cD98Cfb563492A6fB3E7C8243b7B9Ad4cc92", PrivateKey: "0xf12e28c0eb1ef4ff90478f6805b68d63737b7f33abfa091601140805da450d93", }, - 5: { + { Account: "0x4F9133D1d3F50011A6859807C837bdCB31Aaab13", PrivateKey: "0xe667e57a9b8aaa6709e51ff7d093f1c5b73b63f9987e4ab4aa9a5c699e024ee8", }, - 6: { + { Account: "0xbd29A1B981925B94eEc5c4F1125AF02a2Ec4d1cA", PrivateKey: "0x28a574ab2de8a00364d5dd4b07c4f2f574ef7fcc2a86a197f65abaec836d1959", }, - 7: { + { Account: "0xedB6F5B4aab3dD95C7806Af42881FF12BE7e9daa", PrivateKey: "0x74d8b3a188f7260f67698eb44da07397a298df5427df681ef68c45b34b61f998", }, - 8: { + { Account: "0xe706e60ab5Dc512C36A4646D719b889F398cbBcB", PrivateKey: "0xbe79721778b48bcc679b78edac0ce48306a8578186ffcb9f2ee455ae6efeace1", }, - 9: { + { Account: "0xE90E12261CCb0F3F7976Ae611A29e84a6A85f424", PrivateKey: "0x3eb15da85647edd9a1159a4a13b9e7c56877c4eb33f614546d4db06a51868b1c", }, -} as const; +] as const; diff --git a/e2e-tests/test/eth-apis.test.ts b/e2e-tests/test/eth-apis.test.ts new file mode 100644 index 00000000..6c8d8e11 --- /dev/null +++ b/e2e-tests/test/eth-apis.test.ts @@ -0,0 +1,20 @@ +import { expect } from "chai"; +import { getTestProvider } from "../helpers/utils"; +import { RichAccounts } from "../helpers/constants"; +import { ethers } from "ethers"; + +const provider = getTestProvider(); + +describe("eth_accounts", function () { + it("Should return rich accounts", async function () { + // Arrange + const richAccounts = RichAccounts.map((ra) => ethers.utils.getAddress(ra.Account)).sort(); + + // Act + const response: string[] = await provider.send("eth_accounts", []); + const accounts = response.map((addr) => ethers.utils.getAddress(addr)).sort(); + + // Assert + expect(accounts).to.deep.equal(richAccounts); + }); +}); diff --git a/src/node.rs b/src/node.rs index 9ace8599..2503cad8 100644 --- a/src/node.rs +++ b/src/node.rs @@ -2332,14 +2332,14 @@ impl EthNamespaceT for /// A `BoxFuture` containing a `jsonrpc_core::Result` that resolves to a `Vec` of addresses. fn accounts(&self) -> jsonrpc_core::BoxFuture>> { let inner = Arc::clone(&self.inner); - let writer = match inner.write() { + let reader = match inner.read() { Ok(r) => r, Err(_) => { return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() } }; - let accounts: Vec = writer.rich_accounts.clone().into_iter().collect(); + let accounts: Vec = reader.rich_accounts.clone().into_iter().collect(); futures::future::ok(accounts).boxed() }