diff --git a/Cargo.lock b/Cargo.lock index 81689f780..279b0be0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,6 +1395,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint", + "num-traits", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.3.0" @@ -1425,6 +1442,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.114", +] + [[package]] name = "ark-ff-macros" version = "0.3.0" @@ -1463,6 +1490,19 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "ark-poly" version = "0.5.0" @@ -1534,13 +1574,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" dependencies = [ - "ark-serialize-derive", + "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec", "digest 0.10.7", "num-bigint", ] +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint", + "serde_with", +] + [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -1552,6 +1605,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -1582,6 +1646,16 @@ dependencies = [ "rand 0.8.6", ] +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -9464,15 +9538,16 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.2" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" dependencies = [ "alloy-rlp", "arbitrary", "ark-ff 0.3.0", "ark-ff 0.4.2", "ark-ff 0.5.0", + "ark-ff 0.6.0", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 67354bcf5..a128781b2 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -290,6 +290,18 @@ pub enum EthRequest { #[serde(rename = "debug_getRawTransaction", with = "sequence")] DebugGetRawTransaction(TxHash), + /// reth's `debug_getRawReceipts` endpoint. + #[serde(rename = "debug_getRawReceipts", with = "sequence")] + DebugGetRawReceipts(BlockId), + + /// reth's `debug_getRawTransactions` endpoint. + #[serde(rename = "debug_getRawTransactions", with = "sequence")] + DebugGetRawTransactions(BlockId), + + /// geth's `debug_getRawHeader` endpoint. + #[serde(rename = "debug_getRawHeader", with = "sequence")] + DebugGetRawHeader(BlockId), + /// geth's `debug_traceTransaction` endpoint #[serde(rename = "debug_traceTransaction")] DebugTraceTransaction(B256, #[serde(default)] GethDebugTracingOptions), @@ -1424,6 +1436,40 @@ mod tests { let _req = serde_json::from_value::(value).unwrap(); } + #[test] + fn test_serde_debug_raw_receipts() { + let s = r#"{"jsonrpc":"2.0","method":"debug_getRawReceipts","params":["latest"],"id":1}"#; + let value: serde_json::Value = serde_json::from_str(s).unwrap(); + let _req = serde_json::from_value::(value).unwrap(); + + let s = r#"{"jsonrpc":"2.0","method":"debug_getRawReceipts","params":["0x3ed3a89bc10115a321aee238c02de214009f8532a65368e5df5eaf732ee7167c"],"id":1}"#; + let value: serde_json::Value = serde_json::from_str(s).unwrap(); + let _req = serde_json::from_value::(value).unwrap(); + } + + #[test] + fn test_serde_debug_raw_transactions() { + let s = + r#"{"jsonrpc":"2.0","method":"debug_getRawTransactions","params":["latest"],"id":1}"#; + let value: serde_json::Value = serde_json::from_str(s).unwrap(); + let _req = serde_json::from_value::(value).unwrap(); + + let s = r#"{"jsonrpc":"2.0","method":"debug_getRawTransactions","params":["0x3ed3a89bc10115a321aee238c02de214009f8532a65368e5df5eaf732ee7167c"],"id":1}"#; + let value: serde_json::Value = serde_json::from_str(s).unwrap(); + let _req = serde_json::from_value::(value).unwrap(); + } + + #[test] + fn test_serde_debug_raw_header() { + let s = r#"{"jsonrpc":"2.0","method":"debug_getRawHeader","params":["latest"],"id":1}"#; + let value: serde_json::Value = serde_json::from_str(s).unwrap(); + let _req = serde_json::from_value::(value).unwrap(); + + let s = r#"{"jsonrpc":"2.0","method":"debug_getRawHeader","params":["0x3ed3a89bc10115a321aee238c02de214009f8532a65368e5df5eaf732ee7167c"],"id":1}"#; + let value: serde_json::Value = serde_json::from_str(s).unwrap(); + let _req = serde_json::from_value::(value).unwrap(); + } + #[test] fn test_serde_debug_trace_transaction() { let s = r#"{"method": "debug_traceTransaction", "params": diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 5ce3b5574..4d9fbcb2f 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -330,6 +330,13 @@ impl EthApi { EthRequest::DebugGetRawTransaction(hash) => { self.raw_transaction(hash).await.to_rpc_result() } + EthRequest::DebugGetRawReceipts(block) => { + self.raw_receipts(block).await.to_rpc_result() + } + EthRequest::DebugGetRawTransactions(block) => { + self.raw_transactions(block).await.to_rpc_result() + } + EthRequest::DebugGetRawHeader(block) => self.raw_header(block).await.to_rpc_result(), // non eth-standard rpc calls EthRequest::DebugTraceTransaction(tx, opts) => { self.debug_trace_transaction(tx, opts).await.to_rpc_result() @@ -1883,6 +1890,54 @@ impl EthApi { self.inner_raw_transaction(hash).await } + /// Returns EIP-2718 encoded raw receipts for the block. + /// + /// Handler for RPC call: `debug_getRawReceipts`. + pub async fn raw_receipts(&self, block: BlockId) -> Result> { + node_info!("debug_getRawReceipts"); + + // In fork mode, serve pre-fork blocks from the upstream provider. + if let BlockRequest::Number(number) = self.block_request(Some(block)).await? + && let Some(fork) = self.get_fork() + && fork.predates_fork_inclusive(number) + { + let receipts = fork.block_receipts(number).await?.unwrap_or_default(); + return Ok(receipts + .into_iter() + .map(|receipt| { + receipt.0.inner.inner.map_logs(|log| log.inner).encoded_2718().into() + }) + .collect()); + } + + let block = self.backend.get_block(block).ok_or(BlockchainError::BlockNotFound)?; + let receipts = self + .backend + .mined_receipts(block.header.hash_slow()) + .ok_or(BlockchainError::BlockNotFound)?; + Ok(receipts.into_iter().map(|receipt| receipt.encoded_2718().into()).collect()) + } + + /// Returns EIP-2718 encoded raw transactions for the block. + /// + /// Handler for RPC call: `debug_getRawTransactions`. + pub async fn raw_transactions(&self, block: BlockId) -> Result> { + node_info!("debug_getRawTransactions"); + let Some(block) = self.backend.get_block(block) else { + return Ok(Vec::new()); + }; + Ok(block.body.transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect()) + } + + /// Returns RLP encoded raw block header. + /// + /// Handler for RPC call: `debug_getRawHeader`. + pub async fn raw_header(&self, block: BlockId) -> Result { + node_info!("debug_getRawHeader"); + let block = self.backend.get_block(block).ok_or(BlockchainError::BlockNotFound)?; + Ok(alloy_rlp::encode(&block.header).into()) + } + /// Returns EIP-2718 encoded raw transaction by block hash and index /// /// Handler for RPC call: `eth_getRawTransactionByBlockHashAndIndex` diff --git a/crates/anvil/src/server/beacon/handlers.rs b/crates/anvil/src/server/beacon/handlers.rs index 9310a7960..15876a8bf 100644 --- a/crates/anvil/src/server/beacon/handlers.rs +++ b/crates/anvil/src/server/beacon/handlers.rs @@ -87,6 +87,21 @@ pub async fn handle_get_genesis(State(api): State) -> Response { Err(_) => BeaconError::internal_error().into_response(), } } + +/// Handles requests for the Beacon chain configuration used by Base clients. +/// +/// GET /eth/v1/config/spec +pub async fn handle_get_spec(State(api): State) -> Response { + match api.anvil_get_interval_mining().ok().flatten().filter(|interval| *interval > 0) { + Some(interval) => Json(serde_json::json!({ + "data": { + "SECONDS_PER_SLOT": interval.to_string() + } + })) + .into_response(), + None => BeaconError::internal_error().into_response(), + } +} #[cfg(test)] mod tests { use super::*; diff --git a/crates/anvil/src/server/beacon/mod.rs b/crates/anvil/src/server/beacon/mod.rs index 9100186d4..e4df27b25 100644 --- a/crates/anvil/src/server/beacon/mod.rs +++ b/crates/anvil/src/server/beacon/mod.rs @@ -14,5 +14,6 @@ pub fn router(api: EthApi) -> Router { .route("/eth/v1/beacon/blob_sidecars/{block_id}", get(handlers::handle_get_blob_sidecars)) .route("/eth/v1/beacon/blobs/{block_id}", get(handlers::handle_get_blobs)) .route("/eth/v1/beacon/genesis", get(handlers::handle_get_genesis)) + .route("/eth/v1/config/spec", get(handlers::handle_get_spec)) .with_state(api) } diff --git a/crates/anvil/tests/it/beacon_api.rs b/crates/anvil/tests/it/beacon_api.rs index e73ea3593..9039acdc2 100644 --- a/crates/anvil/tests/it/beacon_api.rs +++ b/crates/anvil/tests/it/beacon_api.rs @@ -9,6 +9,7 @@ use alloy_serde::WithOtherFields; use anvil::{NodeConfig, spawn}; use foundry_evm::hardfork::EthereumHardfork; use ssz::Decode; +use std::time::Duration; #[tokio::test(flavor = "multi_thread")] async fn test_beacon_api_get_blob_sidecars() { @@ -258,3 +259,20 @@ async fn test_beacon_api_get_genesis() { FixedBytes::from([0x00, 0x00, 0x00, 0x00]) ); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_beacon_api_get_spec() { + let (_api, handle) = + spawn(NodeConfig::test().with_blocktime(Some(Duration::from_secs(4)))).await; + + let response: serde_json::Value = reqwest::Client::new() + .get(format!("{}/eth/v1/config/spec", handle.http_endpoint())) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + + assert_eq!(response, serde_json::json!({ "data": { "SECONDS_PER_SLOT": "4" } })); +} diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index f074b1402..f84d1644b 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -6,6 +6,7 @@ use crate::{ }; use alloy_chains::NamedChain; use alloy_eips::{ + eip2718::Decodable2718, eip7840::BlobParams, eip7910::{EthConfig, SystemContract}, }; @@ -24,6 +25,7 @@ use anvil::{EthereumHardfork, NodeConfig, NodeHandle, PrecompileFactory, eth::Et use foundry_common::provider::get_http_provider; use foundry_config::Config; use foundry_evm_networks::NetworkConfigs; +use foundry_primitives::FoundryReceiptEnvelope; use foundry_test_utils::rpc::{self, next_http_rpc_endpoint, next_rpc_endpoint}; use futures::StreamExt; use std::{ @@ -101,6 +103,40 @@ async fn test_fork_gas_limit_disabled_from_config() { let _ = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); } +// `debug_getRawReceipts` must serve pre-fork blocks from the upstream provider. +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_debug_get_raw_receipts() { + let (_api, handle) = spawn(fork_config()).await; + let provider = handle.http_provider(); + + // A pre-fork block known to contain transactions. + let block_number = BLOCK_NUMBER - 1; + let rpc_receipts = + provider.get_block_receipts(BlockId::number(block_number)).await.unwrap().unwrap(); + assert!(!rpc_receipts.is_empty()); + + let block = provider.get_block(BlockId::number(block_number)).await.unwrap().unwrap(); + let raw_by_number: Vec = provider + .client() + .request("debug_getRawReceipts", (BlockId::number(block_number),)) + .await + .unwrap(); + let raw_by_hash: Vec = provider + .client() + .request("debug_getRawReceipts", (BlockId::hash(block.header.hash),)) + .await + .unwrap(); + + assert_eq!(raw_by_number, raw_by_hash); + assert_eq!(raw_by_number.len(), rpc_receipts.len()); + + // Each entry decodes back into a receipt envelope matching the RPC receipt. + for (raw, rpc) in raw_by_number.iter().zip(rpc_receipts.iter()) { + let decoded = FoundryReceiptEnvelope::decode_2718(&mut raw.as_ref()).unwrap(); + assert_eq!(decoded.status(), rpc.status()); + } +} + #[tokio::test(flavor = "multi_thread")] async fn test_spawn_fork() { let (api, _handle) = spawn(fork_config()).await; diff --git a/crates/anvil/tests/it/transaction.rs b/crates/anvil/tests/it/transaction.rs index fcbf2647e..cfb5b640d 100644 --- a/crates/anvil/tests/it/transaction.rs +++ b/crates/anvil/tests/it/transaction.rs @@ -2,10 +2,14 @@ use crate::{ abi::{Greeter, Multicall, SimpleStorage}, utils::{connect_pubsub, http_provider_with_signer}, }; -use alloy_consensus::Transaction; -use alloy_network::{EthereumWallet, TransactionBuilder, TransactionResponse}; +use alloy_consensus::{Header, Transaction}; +use alloy_network::{ + EthereumWallet, ReceiptResponse, TransactionBuilder, TransactionResponse, + eip2718::Decodable2718, +}; use alloy_primitives::{Address, Bytes, FixedBytes, U256, address, hex, map::B256HashSet}; use alloy_provider::{Provider, WsConnect}; +use alloy_rlp::Decodable; use alloy_rpc_types::{ AccessList, AccessListItem, BlockId, BlockNumberOrTag, BlockOverrides, BlockTransactions, TransactionRequest, @@ -16,6 +20,7 @@ use alloy_sol_types::SolValue; use anvil::{NodeConfig, spawn}; use eyre::Ok; use foundry_evm::hardfork::EthereumHardfork; +use foundry_primitives::FoundryReceiptEnvelope; use futures::{FutureExt, StreamExt, future::join_all}; use revm::primitives::eip7825::TX_GAS_LIMIT_CAP; use std::{str::FromStr, time::Duration}; @@ -760,6 +765,134 @@ async fn can_get_raw_transaction() { assert_eq!(res1.unwrap(), res2.unwrap()); } +#[tokio::test(flavor = "multi_thread")] +async fn can_get_raw_receipts() { + let (api, handle) = spawn(NodeConfig::test()).await; + api.anvil_set_auto_mine(false).await.unwrap(); + + let provider = handle.http_provider(); + let accounts = handle.dev_wallets().collect::>(); + let first = TransactionRequest::default() + .from(accounts[0].address()) + .value(U256::from(1)) + .to(Address::random()); + let second = TransactionRequest::default() + .from(accounts[1].address()) + .value(U256::from(2)) + .to(Address::random()); + + let first = provider.send_transaction(WithOtherFields::new(first)).await.unwrap(); + let second = provider.send_transaction(WithOtherFields::new(second)).await.unwrap(); + + api.mine_one().await; + let first_receipt = first.get_receipt().await.unwrap(); + let second_receipt = second.get_receipt().await.unwrap(); + assert_eq!(first_receipt.block_number, Some(1)); + assert_eq!(second_receipt.block_number, Some(1)); + + let block = provider.get_block(BlockId::number(1)).await.unwrap().unwrap(); + let raw_by_number: Vec = + provider.client().request("debug_getRawReceipts", (BlockId::number(1),)).await.unwrap(); + let raw_by_hash: Vec = provider + .client() + .request("debug_getRawReceipts", (BlockId::hash(block.header.hash),)) + .await + .unwrap(); + let missing: Result, _> = + provider.client().request("debug_getRawReceipts", (BlockId::number(999),)).await; + + assert_eq!(raw_by_number, raw_by_hash); + assert_eq!(raw_by_number.len(), 2); + assert!(missing.is_err()); + + let mut first_raw = raw_by_number[0].as_ref(); + let first_decoded = FoundryReceiptEnvelope::decode_2718(&mut first_raw).unwrap(); + let mut second_raw = raw_by_number[1].as_ref(); + let second_decoded = FoundryReceiptEnvelope::decode_2718(&mut second_raw).unwrap(); + + assert!(first_decoded.status()); + assert!(second_decoded.status()); + assert_eq!(first_decoded.cumulative_gas_used(), 21_000); + assert_eq!(second_decoded.cumulative_gas_used(), 42_000); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_get_raw_transactions() { + let (api, handle) = spawn(NodeConfig::test()).await; + api.anvil_set_auto_mine(false).await.unwrap(); + + let provider = handle.http_provider(); + let accounts = handle.dev_wallets().collect::>(); + let first = TransactionRequest::default() + .from(accounts[0].address()) + .value(U256::from(1)) + .to(Address::random()); + let second = TransactionRequest::default() + .from(accounts[1].address()) + .value(U256::from(2)) + .to(Address::random()); + + let first = provider.send_transaction(WithOtherFields::new(first)).await.unwrap(); + let second = provider.send_transaction(WithOtherFields::new(second)).await.unwrap(); + let first_hash = *first.tx_hash(); + let second_hash = *second.tx_hash(); + + api.mine_one().await; + let first_receipt = first.get_receipt().await.unwrap(); + let second_receipt = second.get_receipt().await.unwrap(); + assert_eq!(first_receipt.block_number, Some(1)); + assert_eq!(second_receipt.block_number, Some(1)); + + let block = provider.get_block(BlockId::number(1)).await.unwrap().unwrap(); + let raw_by_number: Vec = + provider.client().request("debug_getRawTransactions", (BlockId::number(1),)).await.unwrap(); + let raw_by_hash: Vec = provider + .client() + .request("debug_getRawTransactions", (BlockId::hash(block.header.hash),)) + .await + .unwrap(); + let missing: Vec = provider + .client() + .request("debug_getRawTransactions", (BlockId::number(999),)) + .await + .unwrap(); + + assert_eq!(raw_by_number, raw_by_hash); + assert_eq!(raw_by_number.len(), 2); + assert!(missing.is_empty()); + + let first_raw = api.raw_transaction(first_hash).await.unwrap().unwrap(); + let second_raw = api.raw_transaction(second_hash).await.unwrap().unwrap(); + assert!(raw_by_number.contains(&first_raw)); + assert!(raw_by_number.contains(&second_raw)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn can_get_raw_header() { + let (_api, handle) = spawn(NodeConfig::test()).await; + let provider = handle.http_provider(); + + let from = handle.dev_wallets().next().unwrap().address(); + let tx = TransactionRequest::default().from(from).value(U256::from(1)).to(Address::random()); + provider.send_transaction(WithOtherFields::new(tx)).await.unwrap().get_receipt().await.unwrap(); + + let block = provider.get_block(BlockId::number(1)).await.unwrap().unwrap(); + let raw_by_number: Bytes = + provider.client().request("debug_getRawHeader", (BlockId::number(1),)).await.unwrap(); + let raw_by_hash: Bytes = provider + .client() + .request("debug_getRawHeader", (BlockId::hash(block.header.hash),)) + .await + .unwrap(); + + assert_eq!(raw_by_number, raw_by_hash); + assert!(!raw_by_number.is_empty()); + + let decoded = Header::decode(&mut raw_by_number.as_ref()).unwrap(); + assert_eq!(decoded.number, 1); + assert_eq!(decoded.hash_slow(), block.header.hash); +} + #[tokio::test(flavor = "multi_thread")] async fn test_first_nonce_is_zero() { let (api, handle) = spawn(NodeConfig::test()).await; diff --git a/deny.toml b/deny.toml index 319afb063..c9442847d 100644 --- a/deny.toml +++ b/deny.toml @@ -7,8 +7,6 @@ yanked = "warn" ignore = [ # https://rustsec.org/advisories/RUSTSEC-2024-0436 paste! is unmaintained "RUSTSEC-2024-0436", - # https://rustsec.org/advisories/RUSTSEC-2025-0141 bincode is unmaintained - "RUSTSEC-2025-0141", # proc-macro-error2 is currently still pulled in through alloy's proc-macro stack. "RUSTSEC-2026-0173", # quick-xml <0.41 DoS advisories (quadratic dup-attr check / unbounded namespace