diff --git a/Cargo.lock b/Cargo.lock index 5a683ac3..c9b1c87a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f021a55afd68ff2364ccfddaa364fc9a38a72200cdc74fcfb8dc3231d38f2c" +checksum = "d213580c17d239ae83c0d897ac3315db7cda83d2d4936a9823cc3517552f2e24" dependencies = [ "alloy-eips", "alloy-primitives", @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7473a19f02b25f8e1e8c69d35f02c07245694d11bd91bfe00e9190ac106b3838" +checksum = "2a15b4b0f6bab47aae017d52bb5a739bda381553c09fb9918b7172721ef5f5de" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -118,7 +118,9 @@ dependencies = [ "derive_more 2.0.1", "either", "serde", + "serde_with", "sha2", + "thiserror 2.0.16", ] [[package]] @@ -249,9 +251,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30be84f45d4f687b00efaba1e6290cbf53ccc8f6b8fbb54e4c2f9d2a0474ce95" +checksum = "f1b3b1078b8775077525bc9fe9f6577e815ceaecd6c412a4f3b4d8aa2836e8f6" dependencies = [ "alloy-primitives", "serde", @@ -346,12 +348,12 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e29436068f836727d4e7c819ae6bf6f9c9e19a32e96fc23e814709a277f23a" +checksum = "3b5becb9c269a7d05a2f28d549f86df5a5dbc923e2667eff84fdecac8cda534c" dependencies = [ "alloy-primitives", - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.106", @@ -885,7 +887,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6620814f83b784938b29ca6ca8867967a93a4f7f08c1b7ec6256d12c1b53abe8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.106", @@ -1130,8 +1132,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1148,13 +1160,39 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.106", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.106", ] @@ -1493,6 +1531,7 @@ dependencies = [ name = "evm_rpc" version = "2.4.0" dependencies = [ + "alloy-consensus", "alloy-primitives", "alloy-rpc-types", "assert_matches", @@ -1559,6 +1598,7 @@ dependencies = [ name = "evm_rpc_types" version = "2.0.0" dependencies = [ + "alloy-consensus", "alloy-primitives", "alloy-rpc-types", "candid", @@ -3999,7 +4039,7 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.106", diff --git a/Cargo.toml b/Cargo.toml index bf6eb012..aa62a892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } regex = "1.11" [dev-dependencies] +alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } alloy-rpc-types = { workspace = true } assert_matches = { workspace = true } @@ -67,6 +68,7 @@ rand = "0.8" tokio = "1.44.1" [workspace.dependencies] +alloy-consensus = "1.0.26" alloy-primitives = "1.3.0" alloy-rpc-types = "1.0.23" assert_matches = "1.5.0" diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 2fc713e3..cf13b9d4 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -22,4 +22,4 @@ serde = { workspace = true } strum = { workspace = true } [dev-dependencies] -tokio = { workspace = true, features = ["full"] } \ No newline at end of file +tokio = { workspace = true, features = ["full"] } diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index da9136c1..5afeefdc 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -183,9 +183,11 @@ pub mod fixtures; mod request; mod runtime; -use crate::request::{Request, RequestBuilder}; +use crate::request::{ + GetBlockByNumberRequest, GetBlockByNumberRequestBuilder, Request, RequestBuilder, +}; use candid::{CandidType, Principal}; -use evm_rpc_types::{ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; +use evm_rpc_types::{BlockTag, ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; use ic_error_types::RejectCode; use request::{GetLogsRequest, GetLogsRequestBuilder}; pub use runtime::{IcRuntime, Runtime}; @@ -319,6 +321,67 @@ impl ClientBuilder { } impl EvmRpcClient { + /// Call `eth_getBlockByNumber` on the EVM RPC canister. + /// + /// # Examples + /// + /// ```rust + /// use alloy_primitives::{address, b256, bytes}; + /// use alloy_rpc_types::BlockNumberOrTag; + /// use evm_rpc_client::EvmRpcClient; + /// + /// # use evm_rpc_types::{Block, Hex, Hex20, Hex32, Hex256, MultiRpcResult, Nat256}; + /// # use std::str::FromStr; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let client = EvmRpcClient::builder_for_ic() + /// # .with_default_stub_response(MultiRpcResult::Consistent(Ok(Block { + /// # base_fee_per_gas: None, + /// # number: Nat256::ZERO, + /// # difficulty: Some(Nat256::ZERO), + /// # extra_data: Hex::from(vec![]), + /// # gas_limit: Nat256::ZERO, + /// # gas_used: Nat256::ZERO, + /// # hash: Hex32::from(b256!("0x47302c2ebfb29611c74f917a380f3cf45c9dfe9de3554e18bff9a9ca7c8454e2")), + /// # logs_bloom: Hex256::from([0; 256]), + /// # miner: Hex20::from([0; 20]), + /// # mix_hash: Hex32::from([0; 32]), + /// # nonce: Nat256::ZERO, + /// # parent_hash: Hex32::from([0; 32]), + /// # receipts_root: Hex32::from([0; 32]), + /// # sha3_uncles: Hex32::from([0; 32]), + /// # size: Nat256::ZERO, + /// # state_root: Hex32::from([0; 32]), + /// # timestamp: Nat256::ZERO, + /// # total_difficulty: Some(Nat256::ZERO), + /// # transactions: vec![], + /// # transactions_root: Some(Hex32::from([0; 32])), + /// # uncles: vec![], + /// # }))) + /// .build(); + /// + /// let result = client + /// .get_block_by_number(BlockNumberOrTag::Number(23225439)) + /// .send() + /// .await + /// .expect_consistent() + /// .unwrap(); + /// + /// assert_eq!(result.hash(), b256!("0x47302c2ebfb29611c74f917a380f3cf45c9dfe9de3554e18bff9a9ca7c8454e2")); + /// # Ok(()) + /// # } + /// ``` + pub fn get_block_by_number( + &self, + params: impl Into, + ) -> GetBlockByNumberRequestBuilder { + RequestBuilder::new( + self.clone(), + GetBlockByNumberRequest::new(params.into()), + 10_000_000_000, + ) + } + /// Call `eth_getLogs` on the EVM RPC canister. /// /// # Examples @@ -353,7 +416,6 @@ impl EvmRpcClient { /// /// let result = client /// .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) - /// .with_cycles(10_000_000_000) /// .send() /// .await /// .expect_consistent(); diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index fc828953..47f23edf 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -8,6 +8,38 @@ use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; use strum::EnumIter; +#[derive(Debug, Clone)] +pub struct GetBlockByNumberRequest(BlockTag); + +impl GetBlockByNumberRequest { + pub fn new(params: BlockTag) -> Self { + Self(params) + } +} + +impl EvmRpcRequest for GetBlockByNumberRequest { + type Config = RpcConfig; + type Params = BlockTag; + type CandidOutput = MultiRpcResult; + type Output = MultiRpcResult; + + fn endpoint(&self) -> EvmRpcEndpoint { + EvmRpcEndpoint::GetBlockByNumber + } + + fn params(self) -> Self::Params { + self.0 + } +} + +pub type GetBlockByNumberRequestBuilder = RequestBuilder< + R, + RpcConfig, + BlockTag, + MultiRpcResult, + MultiRpcResult, +>; + #[derive(Debug, Clone)] pub struct GetLogsRequest(GetLogsArgs); @@ -92,6 +124,8 @@ pub trait EvmRpcRequest { /// Endpoint on the EVM RPC canister triggering a call to EVM providers. #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, EnumIter)] pub enum EvmRpcEndpoint { + /// `eth_getBlockByNumber` endpoint. + GetBlockByNumber, /// `eth_getLogs` endpoint. GetLogs, } @@ -100,6 +134,7 @@ impl EvmRpcEndpoint { /// Method name on the EVM RPC canister pub fn rpc_method(&self) -> &'static str { match &self { + Self::GetBlockByNumber => "eth_getBlockByNumber", Self::GetLogs => "eth_getLogs", } } diff --git a/evm_rpc_types/Cargo.toml b/evm_rpc_types/Cargo.toml index 879e96b9..df825e8e 100644 --- a/evm_rpc_types/Cargo.toml +++ b/evm_rpc_types/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_types" [dependencies] +alloy-consensus = { workspace = true, optional = true } alloy-primitives = { workspace = true, optional = true } alloy-rpc-types = { workspace = true, optional = true } candid = { workspace = true } @@ -29,5 +30,8 @@ proptest = { workspace = true } serde_json = { workspace = true } [features] -default = ["alloy"] -alloy = ["dep:alloy-primitives", "dep:alloy-rpc-types"] \ No newline at end of file +alloy = [ + "dep:alloy-consensus", + "dep:alloy-primitives", + "dep:alloy-rpc-types" +] \ No newline at end of file diff --git a/evm_rpc_types/src/alloy.rs b/evm_rpc_types/src/alloy.rs index f6e09a1f..4ecc823d 100644 --- a/evm_rpc_types/src/alloy.rs +++ b/evm_rpc_types/src/alloy.rs @@ -1,4 +1,5 @@ -use crate::{Hex, Hex20, Hex32}; +use crate::{Hex, Hex20, Hex256, Hex32, Nat256, RpcError}; +use alloy_primitives::ruint::{ToUintError, UintTryFrom}; impl From for alloy_primitives::Address { fn from(value: Hex20) -> Self { @@ -35,3 +36,41 @@ impl From for Hex { Hex(value.to_vec()) } } + +impl From for Nat256 { + fn from(value: alloy_primitives::U256) -> Self { + Nat256::from_be_bytes(value.to_be_bytes()) + } +} + +impl UintTryFrom for alloy_primitives::U256 { + fn uint_try_from(value: Nat256) -> Result> { + Ok(alloy_primitives::U256::from_be_bytes(value.into_be_bytes())) + } +} + +impl From for alloy_primitives::Bloom { + fn from(value: Hex256) -> Self { + alloy_primitives::Bloom::from(value.0) + } +} + +impl From for Hex256 { + fn from(value: alloy_primitives::Bloom) -> Self { + Hex256::from(value.into_array()) + } +} + +impl TryFrom for alloy_primitives::B64 { + type Error = RpcError; + + fn try_from(value: Nat256) -> Result { + Ok(alloy_primitives::B64::from(u64::try_from(value)?)) + } +} + +impl From for Nat256 { + fn from(value: alloy_primitives::B64) -> Self { + Nat256::from(u64::from(value)) + } +} diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index 7f59008b..ea483c83 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -1,4 +1,7 @@ -use crate::{LogEntry, RpcError, ValidationError}; +use crate::{Block, Hex32, LogEntry, Nat256, RpcError, ValidationError}; +use alloy_primitives::{B256, U256}; +use alloy_rpc_types::BlockTransactions; +use candid::Nat; impl TryFrom for alloy_rpc_types::Log { type Error = RpcError; @@ -20,12 +23,109 @@ impl TryFrom for alloy_rpc_types::Log { )))?, }, block_hash: entry.block_hash.map(alloy_primitives::BlockHash::from), - block_number: entry.block_number.map(u64::try_from).transpose()?, + block_number: entry + .block_number + .map(|value| u64_try_from_nat256(value, "block_number")) + .transpose()?, block_timestamp: None, transaction_hash: entry.transaction_hash.map(alloy_primitives::TxHash::from), - transaction_index: entry.transaction_index.map(u64::try_from).transpose()?, - log_index: entry.log_index.map(u64::try_from).transpose()?, + transaction_index: entry + .transaction_index + .map(|value| u64_try_from_nat256(value, "transaction_index")) + .transpose()?, + log_index: entry + .log_index + .map(|value| u64_try_from_nat256(value, "log_index")) + .transpose()?, removed: entry.removed, }) } } + +impl TryFrom for alloy_rpc_types::Block { + type Error = RpcError; + + fn try_from(value: Block) -> Result { + Ok(Self { + header: alloy_rpc_types::Header { + hash: alloy_primitives::BlockHash::from(value.hash), + inner: alloy_consensus::Header { + parent_hash: alloy_primitives::BlockHash::from(value.parent_hash), + ommers_hash: alloy_primitives::BlockHash::from(value.sha3_uncles), + beneficiary: alloy_primitives::Address::from(value.miner), + state_root: alloy_primitives::B256::from(value.state_root), + transactions_root: validate_transactions_root(value.transactions_root)?, + receipts_root: alloy_primitives::B256::from(value.receipts_root), + logs_bloom: alloy_primitives::Bloom::from(value.logs_bloom), + difficulty: validate_difficulty(&value.number, value.difficulty)?, + number: u64_try_from_nat256(value.number, "number")?, + gas_limit: u64_try_from_nat256(value.gas_limit, "gas_limit")?, + gas_used: u64_try_from_nat256(value.gas_used, "gas_used")?, + timestamp: u64_try_from_nat256(value.timestamp, "timestamp")?, + extra_data: alloy_primitives::Bytes::from(value.extra_data), + mix_hash: alloy_primitives::B256::from(value.mix_hash), + nonce: alloy_primitives::B64::try_from(value.nonce)?, + base_fee_per_gas: value + .base_fee_per_gas + .map(|value| u64_try_from_nat256(value, "base_fee_per_gas")) + .transpose()?, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + }, + total_difficulty: value.total_difficulty.map(U256::from), + size: Some(U256::from(value.size)), + }, + uncles: value + .uncles + .into_iter() + .map(alloy_primitives::B256::from) + .collect(), + transactions: BlockTransactions::Hashes( + value + .transactions + .into_iter() + .map(alloy_primitives::B256::from) + .collect(), + ), + withdrawals: None, + }) + } +} + +fn validate_difficulty(number: &Nat256, difficulty: Option) -> Result { + const PARIS_BLOCK: u64 = 15_537_394; + if number.as_ref() < &Nat::from(PARIS_BLOCK) { + difficulty + .map(U256::from) + .ok_or(RpcError::ValidationError(ValidationError::Custom( + "Block before Paris upgrade but missing difficulty".into(), + ))) + } else { + match difficulty.map(U256::from) { + None | Some(U256::ZERO) => Ok(U256::ZERO), + _ => Err(RpcError::ValidationError(ValidationError::Custom( + "Block after Paris upgrade with non-zero difficulty".into(), + ))), + } + } +} + +fn validate_transactions_root(transactions_root: Option) -> Result { + transactions_root + .map(alloy_primitives::B256::from) + .ok_or(RpcError::ValidationError(ValidationError::Custom( + "Block does not have a transactions root field".to_string(), + ))) +} + +fn u64_try_from_nat256(value: Nat256, field_name: &str) -> Result { + u64::try_from(Nat::from(value).0).map_err(|err| { + RpcError::ValidationError(ValidationError::Custom(format!( + "Failed to convert field `{}` to u64: {:?}", + field_name, err + ))) + }) +} diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs index d76ffe0d..93027c58 100644 --- a/evm_rpc_types/src/response/test.rs +++ b/evm_rpc_types/src/response/test.rs @@ -1,77 +1,196 @@ -use crate::{Hex, Hex20, Hex32}; -use proptest::prelude::Strategy; -use proptest::proptest; -use std::ops::RangeInclusive; +use crate::{Block, Hex, Hex20, Hex256, Hex32, LogEntry, Nat256}; +use num_bigint::BigUint; +use proptest::{ + arbitrary::any, + collection::vec, + option, + prelude::{Just, Strategy}, + prop_assert_eq, prop_compose, proptest, +}; +use serde_json::Value; +use std::{ops::RangeInclusive, str::FromStr}; +// To check conversion from `evm_rpc_types` to `alloy_rpc_types`, these tests generate an arbitrary +// (valid) type from the `evm_rpc_types` crate, convert it to the corresponding `alloy_rpc_types` +// type, and compare both serialized values. +// This is done so that we can check conversion for randomly generated values and not just a few +// hardcoded instances. #[cfg(feature = "alloy")] mod alloy_conversion_tests { use super::*; - use crate::{LogEntry, Nat256}; - use num_bigint::BigUint; - use proptest::arbitrary::any; - use proptest::option; - use serde_json::Value; - use std::str::FromStr; + + const PARIS_BLOCK: u64 = 15_537_394; proptest! { #[test] - fn should_convert_from_alloy(entry in arb_log_entry()) { - // Convert a number serialized as a hexadecimal string into an array of u32 digits. - // This is needed to compare a serialized `alloy_rpc_types::Log` with an - // `evm_rpc_types::LogEntry` since `transactionIndex`, `logIndex` and `blockNumber` get - // serialized as hex strings by alloy but as integers in `evm_rpc_types`. - fn hex_to_u32_digits(serialized: &mut Value, field: &str) { - if let Some(Value::String(hex)) = serialized.get(field) { - let hex = hex.strip_prefix("0x").unwrap_or(hex); - let digits = BigUint::parse_bytes(hex.as_bytes(), 16).unwrap().to_u32_digits(); - serialized[field] = digits.into(); + fn should_convert_log_to_alloy(entry in arb_log_entry()) { + let serialized = serde_json::to_value(&entry).unwrap(); + + let alloy_log = alloy_rpc_types::Log::try_from(entry.clone()).unwrap(); + let alloy_serialized = serde_json::to_value(&alloy_log).unwrap(); + + prop_assert_eq!(serialized, canonicalize_log(alloy_serialized)); + } + + #[test] + fn should_convert_pre_paris_block_to_alloy(block in arb_pre_paris_block()) { + let serialized = serde_json::to_value(&block).unwrap(); + + let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); + let alloy_serialized = serde_json::to_value(&alloy_block).unwrap(); + + prop_assert_eq!(serialized, canonicalize_block(alloy_serialized)); + } + + #[test] + fn should_convert_post_paris_block_to_alloy(block in arb_post_paris_block()) { + // For post-Paris blocks, the difficulty field is optional. However, the `difficulty` field + // is mandatory in the `alloy_rpc_types::Block` type. Therefore, convert `null` values to 0. + fn canonicalize_difficulty (mut serialized_block: Value) -> Value { + if let Some(Value::Null) = serialized_block.get("difficulty") { + serialized_block["difficulty"] = Value::from(Vec::::new()); } + serialized_block } - let serialized = serde_json::to_value(&entry).unwrap(); + let serialized = serde_json::to_value(&block).unwrap(); - let mut alloy_serialized = serde_json::to_value(&alloy_rpc_types::Log::try_from(entry.clone()).unwrap()).unwrap(); - hex_to_u32_digits(&mut alloy_serialized, "transactionIndex"); - hex_to_u32_digits(&mut alloy_serialized, "logIndex"); - hex_to_u32_digits(&mut alloy_serialized, "blockNumber"); + let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); + let alloy_serialized = serde_json::to_value(&alloy_block).unwrap(); - assert_eq!(serialized, alloy_serialized); + prop_assert_eq!(canonicalize_difficulty(serialized), canonicalize_block(alloy_serialized)); } } - fn arb_log_entry() -> impl Strategy { - ( - arb_hex20(), - arb_hex(), - option::of(any::().prop_map(Nat256::from)), - option::of(arb_hex32()), - option::of(any::().prop_map(Nat256::from)), - option::of(arb_hex32()), - option::of(any::().prop_map(Nat256::from)), - any::(), + fn canonicalize_log(mut serialized_log: Value) -> Value { + // Convert hex-encoded numerical values to arrays of `u32` digits. + hex_to_u32_digits(&mut serialized_log, "transactionIndex"); + hex_to_u32_digits(&mut serialized_log, "logIndex"); + hex_to_u32_digits(&mut serialized_log, "blockNumber"); + serialized_log + } + + fn canonicalize_block(mut serialized_block: Value) -> Value { + // Convert hex-encoded numerical values to arrays of `u32` digits. + hex_to_u32_digits(&mut serialized_block, "baseFeePerGas"); + hex_to_u32_digits(&mut serialized_block, "number"); + hex_to_u32_digits(&mut serialized_block, "difficulty"); + hex_to_u32_digits(&mut serialized_block, "gasLimit"); + hex_to_u32_digits(&mut serialized_block, "gasUsed"); + hex_to_u32_digits(&mut serialized_block, "nonce"); + hex_to_u32_digits(&mut serialized_block, "size"); + hex_to_u32_digits(&mut serialized_block, "timestamp"); + hex_to_u32_digits(&mut serialized_block, "totalDifficulty"); + // Add `null` for values that alloy skips during serialization when they are absent. + add_null_if_absent(&mut serialized_block, "baseFeePerGas"); + add_null_if_absent(&mut serialized_block, "totalDifficulty"); + serialized_block + } + + fn arb_pre_paris_block() -> impl Strategy { + arb_block( + (0..PARIS_BLOCK).prop_map(Nat256::from), + arb_nat256().prop_map(Some), + ) + } + + fn arb_post_paris_block() -> impl Strategy { + arb_block( + (PARIS_BLOCK..).prop_map(Nat256::from), + option::of(Just(Nat256::ZERO)), + ) + } + + prop_compose! { + fn arb_block( + number_strategy: impl Strategy, + difficulty_strategy: impl Strategy> ) - .prop_map( - |( - address, - data, - block_number, - transaction_hash, - transaction_index, - block_hash, - log_index, - removed, - )| LogEntry { - address, - topics: vec![], - data, - block_number, - transaction_hash, - transaction_index, - block_hash, - log_index, - removed, - }, - ) + ( + base_fee_per_gas in option::of(arb_u64()), + number in number_strategy, + difficulty in difficulty_strategy, + extra_data in arb_hex(), + gas_limit in arb_u64(), + gas_used in arb_u64(), + hash in arb_hex32(), + logs_bloom in arb_hex256(), + miner in arb_hex20(), + mix_hash in arb_hex32(), + nonce in arb_u64(), + parent_hash in arb_hex32(), + receipts_root in arb_hex32(), + sha3_uncles in arb_hex32(), + size in arb_u64(), + state_root in arb_hex32(), + timestamp in arb_u64(), + total_difficulty in option::of(arb_nat256()), + transactions in vec(arb_hex32(), 0..100), + transactions_root in arb_hex32(), + uncles in vec(arb_hex32(), 0..100), + ) -> Block { + Block { + base_fee_per_gas, + number, + difficulty, + extra_data, + gas_limit, + gas_used, + hash, + logs_bloom, + miner, + mix_hash, + nonce, + parent_hash, + receipts_root, + sha3_uncles, + size, + state_root, + timestamp, + total_difficulty, + transactions, + // The `transactionsRoot` field is mandatory as per the Ethereum JSON-RPC API. + // See: https://ethereum.github.io/execution-apis/api-documentation/ + transactions_root: Some(transactions_root), + uncles, + } + } + } + + prop_compose! { + fn arb_log_entry() + ( + address in arb_hex20(), + topics in vec(arb_hex32(), 0..=4), + data in arb_hex(), + block_number in option::of(arb_u64()), + transaction_hash in option::of(arb_hex32()), + transaction_index in option::of(arb_u64()), + block_hash in option::of(arb_hex32()), + log_index in option::of(arb_u64()), + removed in any::(), + ) -> LogEntry { + LogEntry { + address, + topics, + data, + block_number, + transaction_hash, + transaction_index, + block_hash, + log_index, + removed, + } + } + } + + // `u64` wrapped in a `Nat256` + fn arb_u64() -> impl Strategy { + any::().prop_map(Nat256::from) + } + + fn arb_nat256() -> impl Strategy { + any::<[u8; 32]>().prop_map(Nat256::from_be_bytes) } fn arb_hex20() -> impl Strategy { @@ -82,9 +201,37 @@ mod alloy_conversion_tests { arb_var_len_hex_string(32..=32_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) } + fn arb_hex256() -> impl Strategy { + arb_var_len_hex_string(256..=256_usize).prop_map(|s| Hex256::from_str(s.as_str()).unwrap()) + } + fn arb_hex() -> impl Strategy { arb_var_len_hex_string(0..=100_usize).prop_map(|s| Hex::from_str(s.as_str()).unwrap()) } + + // This method checks if the given `serde_json::Value` contains the given field, and if so, + // it parses its value as a hexadecimal string and converts it to an array of u32 digits. + // This is needed to compare serialized values between `alloy_rpc_types` and `evm_rpc_types` + // since the former serialized integers as hex strings, but the latter as arrays of u32 digits. + fn hex_to_u32_digits(serialized: &mut Value, field: &str) { + if let Some(Value::String(hex)) = serialized.get(field) { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + let digits = BigUint::parse_bytes(hex.as_bytes(), 16) + .unwrap() + .to_u32_digits(); + serialized[field] = digits.into(); + } + } + + // This method checks if the given `serde_json` contains the given field, and if not, sets its + // value to `serde_json::Value::Null`. + // This is needed to compare serialized values because some fields are skipped during + // serialization in `alloy_rpc_types` but not `evm_rpc_types` + fn add_null_if_absent(serialized: &mut Value, field: &str) { + if serialized.get(field).is_none() { + serialized[field] = Value::Null; + } + } } fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { diff --git a/evm_rpc_types/src/result/alloy.rs b/evm_rpc_types/src/result/alloy.rs index 33655c29..115781db 100644 --- a/evm_rpc_types/src/result/alloy.rs +++ b/evm_rpc_types/src/result/alloy.rs @@ -1,4 +1,4 @@ -use crate::{LogEntry, MultiRpcResult}; +use crate::{Block, LogEntry, MultiRpcResult}; impl From>> for MultiRpcResult> { fn from(result: MultiRpcResult>) -> Self { @@ -9,3 +9,9 @@ impl From>> for MultiRpcResult> for MultiRpcResult { + fn from(result: MultiRpcResult) -> Self { + result.and_then(alloy_rpc_types::Block::try_from) + } +} diff --git a/evm_rpc_types/src/tests.rs b/evm_rpc_types/src/tests.rs index 1bccb8d8..a5bbfb30 100644 --- a/evm_rpc_types/src/tests.rs +++ b/evm_rpc_types/src/tests.rs @@ -185,14 +185,24 @@ mod hex_string { #[cfg(feature = "alloy")] mod alloy_conversion_tests { use super::*; - use alloy_primitives::{Address, Bytes, B256}; + use alloy_primitives::{Address, Bloom, Bytes, B256, B64, U256}; proptest! { #[test] - fn should_convert_to_and_from_alloy(hex20 in arb_hex20(), hex32 in arb_hex32(), hex in arb_hex()) { + fn should_convert_to_and_from_alloy( + hex20 in arb_hex20(), + hex32 in arb_hex32(), + hex256 in arb_hex256(), + hex in arb_hex(), + wrapped_u64 in arb_u64(), + nat256 in arb_nat256(), + ) { prop_assert_eq!(hex20.clone(), Hex20::from(Address::from(hex20))); prop_assert_eq!(hex32.clone(), Hex32::from(B256::from(hex32))); + prop_assert_eq!(hex256.clone(), Hex256::from(Bloom::from(hex256))); prop_assert_eq!(hex.clone(), Hex::from(Bytes::from(hex))); + prop_assert_eq!(wrapped_u64.clone(), Nat256::from(B64::try_from(wrapped_u64).unwrap())); + prop_assert_eq!(nat256.clone(), Nat256::from(U256::from(nat256))); } } @@ -204,9 +214,21 @@ mod alloy_conversion_tests { arb_var_len_hex_string(32..=32_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) } + fn arb_hex256() -> impl Strategy { + arb_var_len_hex_string(256..=256_usize).prop_map(|s| Hex256::from_str(s.as_str()).unwrap()) + } + fn arb_hex() -> impl Strategy { arb_var_len_hex_string(0..=100_usize).prop_map(|s| Hex::from_str(s.as_str()).unwrap()) } + + fn arb_u64() -> impl Strategy { + any::().prop_map(Nat256::from) + } + + fn arb_nat256() -> impl Strategy { + any::<[u8; 32]>().prop_map(Nat256::from_be_bytes) + } } fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { diff --git a/tests/tests.rs b/tests/tests.rs index d78d6b4a..6d20776d 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -10,8 +10,8 @@ use crate::{ }, setup::EvmRpcNonblockingSetup, }; -use alloy_primitives::{address, b256, bytes}; -use alloy_rpc_types::BlockNumberOrTag; +use alloy_primitives::{address, b256, bloom, bytes}; +use alloy_rpc_types::{BlockNumberOrTag, BlockTransactions}; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; use canlog::{Log, LogEntry}; @@ -40,7 +40,7 @@ use pocket_ic::common::rest::{ }; use pocket_ic::{ErrorCode, PocketIc, PocketIcBuilder, RejectResponse}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::json; +use serde_json::{json, Value}; use std::{iter, marker::PhantomData, str::FromStr, sync::Arc, time::Duration}; const DEFAULT_CALLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]); @@ -242,18 +242,6 @@ impl EvmRpcSetup { ) } - pub fn eth_get_block_by_number( - &self, - source: RpcServices, - config: Option, - block: evm_rpc_types::BlockTag, - ) -> CallFlow> { - self.call_update( - "eth_getBlockByNumber", - Encode!(&source, &config, &block).unwrap(), - ) - } - pub fn eth_get_transaction_receipt( &self, source: RpcServices, @@ -821,124 +809,258 @@ async fn eth_get_logs_should_fail_when_block_range_too_large() { } } -#[test] -fn eth_get_block_by_number_should_succeed() { - let [response_0, response_1, response_2] = json_rpc_sequential_id( - json!({"jsonrpc":"2.0","result":{"baseFeePerGas":"0xd7232aa34","difficulty":"0x0","extraData":"0x546974616e2028746974616e6275696c6465722e78797a29","gasLimit":"0x1c9c380","gasUsed":"0xa768c4","hash":"0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae","logsBloom":"0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b","miner":"0x388c818ca8b9251b393131c08a736a67ccb19297","mixHash":"0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f","nonce":"0x0000000000000000","number":"0x11db01d","parentHash":"0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae","receiptsRoot":"0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xcd35","stateRoot":"0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6","timestamp":"0x656f96f3","totalDifficulty":"0xc70d815d562d3cfa955","withdrawalsRoot":"0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48"},"id":0}), - ); +#[tokio::test] +async fn eth_get_block_by_number_should_succeed() { + fn mock_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") + .with_params(json!(["latest", false])) + } + + fn mock_response() -> JsonRpcResponse { + JsonRpcResponse::from(json!({ + "jsonrpc": "2.0", + "result": { + "baseFeePerGas": "0xd7232aa34", + "difficulty": "0x0", + "extraData": "0x546974616e2028746974616e6275696c6465722e78797a29", + "gasLimit": "0x1c9c380", + "gasUsed": "0xa768c4", + "hash": "0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae", + "logsBloom": "0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b", + "miner": "0x388c818ca8b9251b393131c08a736a67ccb19297", + "mixHash": "0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f", + "nonce": "0x0000000000000000", + "number": "0x11db01d", + "parentHash": "0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae", + "receiptsRoot": "0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0xcd35", + "stateRoot": "0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6", + "timestamp": "0x656f96f3", + "withdrawalsRoot": "0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48", + "transactionsRoot": "0x93a1ad3d067009259b508cc95fde63b5efd7e9d8b55754314c173fdde8c0826a", + }, + "id": 0 + })) + } + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + let mut offset = 0_u64; + for source in RPC_SERVICES { - let setup = EvmRpcSetup::new().mock_api_keys(); + let mocks = MockHttpOutcallsBuilder::new() + .given(mock_request().with_id(offset)) + .respond_with(mock_response().with_id(offset)) + .given(mock_request().with_id(1 + offset)) + .respond_with(mock_response().with_id(1 + offset)) + .given(mock_request().with_id(2 + offset)) + .respond_with(mock_response().with_id(2 + offset)); + let response = setup - .eth_get_block_by_number(source.clone(), None, evm_rpc_types::BlockTag::Latest) - .mock_http_once(MockOutcallBuilder::new(200, response_0.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_1.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_2.clone())) - .wait() + .client(mocks) + .with_rpc_sources(source.clone()) + .build() + .get_block_by_number(BlockNumberOrTag::Latest) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!( - response, - evm_rpc_types::Block { - base_fee_per_gas: Some(57_750_497_844_u64.into()), - difficulty: Some(Nat256::ZERO), - extra_data: "0x546974616e2028746974616e6275696c6465722e78797a29".parse().unwrap(), - gas_limit: 0x1c9c380_u32.into(), - gas_used: 0xa768c4_u32.into(), - hash: "0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae".parse().unwrap(), - logs_bloom: "0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b".parse().unwrap(), - miner: "0x388c818ca8b9251b393131c08a736a67ccb19297".parse().unwrap(), - mix_hash: "0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f".parse().unwrap(), - nonce: Nat256::ZERO, - number: 18_722_845_u32.into(), - parent_hash: "0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae".parse().unwrap(), - receipts_root: "0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929".parse().unwrap(), - sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347".parse().unwrap(), - size: 0xcd35_u32.into(), - state_root: "0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6".parse().unwrap(), - timestamp: 0x656f96f3_u32.into(), + offset += 3; + + assert_eq!(response, alloy_rpc_types::Block { + header: alloy_rpc_types::Header { + hash: b256!("0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae"), + inner: alloy_consensus::Header { + parent_hash: b256!("0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae"), + ommers_hash: b256!("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"), + beneficiary: address!("0x388c818ca8b9251b393131c08a736a67ccb19297"), + state_root: b256!("0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6"), + transactions_root: b256!("0x93a1ad3d067009259b508cc95fde63b5efd7e9d8b55754314c173fdde8c0826a"), + receipts_root: b256!("0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929"), + logs_bloom: bloom!("0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b"), + difficulty: alloy_primitives::U256::ZERO, + number: 18_722_845_u64, + gas_limit: 0x1c9c380_u64, + gas_used: 0xa768c4_u64, + timestamp: 0x656f96f3_u64, + extra_data: bytes!("0x546974616e2028746974616e6275696c6465722e78797a29"), + mix_hash: b256!("0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f"), + nonce: alloy_primitives::B64::ZERO, + base_fee_per_gas: Some(57_750_497_844_u64), + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + }, total_difficulty: None, - transactions: vec![], - transactions_root: None, - uncles: vec![], - } - ); + size: Some(alloy_primitives::U256::from(0xcd35_u64)), + }, + uncles: vec![], + transactions: BlockTransactions::Hashes(vec![]), + withdrawals: None, + }); } } -#[test] -fn eth_get_block_by_number_pre_london_fork_should_succeed() { - let [response_0, response_1, response_2] = json_rpc_sequential_id( - json!({"jsonrpc":"2.0","id":0,"result":{"number":"0x0","hash":"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3","transactions":[],"totalDifficulty":"0x400000000","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","extraData":"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa","nonce":"0x0000000000000042","miner":"0x0000000000000000000000000000000000000000","difficulty":"0x400000000","gasLimit":"0x1388","gasUsed":"0x0","uncles":[],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x21c","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","timestamp":"0x0"}}), - ); +#[tokio::test] +async fn eth_get_block_by_number_pre_london_fork_should_succeed() { + fn mock_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") + .with_params(json!(["latest", false])) + } + + fn mock_response() -> JsonRpcResponse { + JsonRpcResponse::from(json!({ + "jsonrpc":"2.0", + "id":0, + "result":{ + "number":"0x0", + "hash":"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "transactions":[], + "totalDifficulty":"0x400000000", + "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "extraData":"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "nonce":"0x0000000000000042", + "miner":"0x0000000000000000000000000000000000000000", + "difficulty":"0x400000000", + "gasLimit":"0x1388", + "gasUsed":"0x0", + "uncles":[], + "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size":"0x21c", + "transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "stateRoot":"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp":"0x0" + } + })) + } + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + let mut offset = 0_u64; + for source in RPC_SERVICES { - let setup = EvmRpcSetup::new().mock_api_keys(); + let mocks = MockHttpOutcallsBuilder::new() + .given(mock_request().with_id(offset)) + .respond_with(mock_response().with_id(offset)) + .given(mock_request().with_id(1 + offset)) + .respond_with(mock_response().with_id(1 + offset)) + .given(mock_request().with_id(2 + offset)) + .respond_with(mock_response().with_id(2 + offset)); + let response = setup - .eth_get_block_by_number(source.clone(), None, evm_rpc_types::BlockTag::Latest) - .mock_http_once(MockOutcallBuilder::new(200, response_0.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_1.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_2.clone())) - .wait() + .client(mocks) + .with_rpc_sources(source.clone()) + .build() + .get_block_by_number(BlockNumberOrTag::Latest) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!( - response, - evm_rpc_types::Block { - base_fee_per_gas: None, - difficulty: Some(0x400000000_u64.into()), - extra_data: "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa".parse().unwrap(), - gas_limit: 0x1388_u32.into(), - gas_used: Nat256::ZERO, - hash: "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3".parse().unwrap(), - logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(), - miner: "0x0000000000000000000000000000000000000000".parse().unwrap(), - mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(), - nonce: 0x0000000000000042_u32.into(), - number: Nat256::ZERO, - parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(), - receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421".parse().unwrap(), - sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347".parse().unwrap(), - size: 0x21c_u32.into(), - state_root: "0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544".parse().unwrap(), - timestamp: Nat256::ZERO, + offset += 3; + + assert_eq!(response, alloy_rpc_types::Block { + header: alloy_rpc_types::Header { + hash: b256!("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"), + inner: alloy_consensus::Header { + parent_hash: b256!("0x0000000000000000000000000000000000000000000000000000000000000000"), + ommers_hash: b256!("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"), + beneficiary: address!("0x0000000000000000000000000000000000000000"), + state_root: b256!("0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544"), + transactions_root: b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + receipts_root: b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + logs_bloom: bloom!("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + difficulty: alloy_primitives::U256::from(0x400000000_u64), + number: 0_u64, + gas_limit: 0x1388_u64, + gas_used: 0_u64, + timestamp: 0_u64, + extra_data: bytes!("0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"), + mix_hash: b256!("0x0000000000000000000000000000000000000000000000000000000000000000"), + nonce: alloy_primitives::B64::from(0x0000000000000042_u64), + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + }, total_difficulty: None, - transactions: vec![], - transactions_root: Some("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421".parse().unwrap()), - uncles: vec![], - } - ); + size: Some(alloy_primitives::U256::from(0x21c_u64)), + }, + uncles: vec![], + transactions: BlockTransactions::Hashes(vec![]), + withdrawals: None, + }); } } -#[test] -fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inconsistent() { - let setup = EvmRpcSetup::new().mock_api_keys(); - let [response_0, mut response_1] = json_rpc_sequential_id( - json!({"jsonrpc":"2.0","result":{"baseFeePerGas":"0xd7232aa34","difficulty":"0x0","extraData":"0x546974616e2028746974616e6275696c6465722e78797a29","gasLimit":"0x1c9c380","gasUsed":"0xa768c4","hash":"0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae","logsBloom":"0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b","miner":"0x388c818ca8b9251b393131c08a736a67ccb19297","mixHash":"0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f","nonce":"0x0000000000000000","number":"0x11db01d","parentHash":"0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae","receiptsRoot":"0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xcd35","stateRoot":"0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6","timestamp":"0x656f96f3","totalDifficulty":"0xc70d815d562d3cfa955","withdrawalsRoot":"0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48"},"id":0}), - ); - assert_eq!( - Some(json!("0xc70d815d562d3cfa955")), - response_1["result"] - .as_object_mut() - .unwrap() - .remove("totalDifficulty") - ); +#[tokio::test] +async fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inconsistent() { + fn mock_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") + .with_params(json!(["latest", false])) + } + + fn mock_response(total_difficulty: Option<&str>) -> JsonRpcResponse { + let mut body = json!({ + "jsonrpc":"2.0", + "result":{ + "baseFeePerGas":"0xd7232aa34", + "difficulty":"0x0", + "extraData":"0x546974616e2028746974616e6275696c6465722e78797a29", + "gasLimit":"0x1c9c380", + "gasUsed":"0xa768c4", + "hash":"0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae", + "logsBloom":"0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b", + "miner":"0x388c818ca8b9251b393131c08a736a67ccb19297", + "mixHash":"0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f", + "nonce":"0x0000000000000000", + "number":"0x11db01d", + "parentHash":"0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae", + "receiptsRoot":"0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929", + "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size":"0xcd35", + "stateRoot":"0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6", + "timestamp":"0x656f96f3", + "withdrawalsRoot":"0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48", + "transactionsRoot":"0x93a1ad3d067009259b508cc95fde63b5efd7e9d8b55754314c173fdde8c0826a", + }, + "id":0 + }); + if let Some(total_difficulty) = total_difficulty { + body.get_mut("result").unwrap()["totalDifficulty"] = + Value::String(total_difficulty.to_string()); + } + JsonRpcResponse::from(body) + } + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + + let mocks = MockHttpOutcallsBuilder::new() + .given(mock_request().with_id(0_u64)) + .respond_with(mock_response(Some("0xc70d815d562d3cfa955")).with_id(0_u64)) + .given(mock_request().with_id(1_u64)) + .respond_with(mock_response(None).with_id(1_u64)); + let response = setup - .eth_get_block_by_number( - RpcServices::EthMainnet(Some(vec![ - EthMainnetService::Ankr, - EthMainnetService::PublicNode, - ])), - None, - evm_rpc_types::BlockTag::Latest, - ) - .mock_http_once(MockOutcallBuilder::new(200, response_0)) - .mock_http_once(MockOutcallBuilder::new(200, response_1)) - .wait() + .client(mocks) + .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ + EthMainnetService::Ankr, + EthMainnetService::PublicNode, + ]))) + .build() + .get_block_by_number(BlockNumberOrTag::Latest) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!(response.number, 18_722_845_u32.into()); - assert_eq!(response.total_difficulty, None); + assert_eq!(response.number(), 18_722_845_u64); + assert_eq!(response.header.total_difficulty, None); } #[test]