diff --git a/crates/networking/rpc/tracing.rs b/crates/networking/rpc/tracing.rs index b6f005f64d..6f638d281c 100644 --- a/crates/networking/rpc/tracing.rs +++ b/crates/networking/rpc/tracing.rs @@ -1,15 +1,18 @@ +use std::collections::HashMap; use std::time::Duration; -use ethrex_common::H256; +use ethrex_common::{Address, H256}; use ethrex_common::{ serde_utils, - tracing::{CallTraceFrame, PrestateResult, StructLoggerEmit, StructLoggerResult}, + tracing::{CallTraceFrame, CallType, PrestateResult, StructLoggerEmit, StructLoggerResult}, }; use ethrex_vm::tracing::OpcodeTracerConfig; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::{rpc::RpcHandler, types::block_identifier::BlockIdentifier, utils::RpcErr}; +use crate::{ + rpc::RpcApiContext, rpc::RpcHandler, types::block_identifier::BlockIdentifier, utils::RpcErr, +}; /// Default max amount of blocks to re-excute if it is not given const DEFAULT_REEXEC: u32 = 128; @@ -59,6 +62,19 @@ enum TracerType { /// `structLogger` wrapper shape (`{failed, gas, returnValue, structLogs}`). /// Selected via `"tracer": "opcodeTracer"`. OpcodeTracer, + /// Multiplexer tracer that runs multiple sub-tracers on the same + /// transaction and returns `{tracerName: result}` per sub-tracer. + /// Selected via `"tracer": "muxTracer"`. + /// + /// **Divergences from geth**: + /// - geth attaches all sub-tracers to a single execution; ethrex re-runs + /// the transaction once per sub-tracer (correct output, N× the cost). + /// - `debug_traceBlockByNumber` with muxTracer is not yet supported and + /// returns a `BadParams` error. + /// - Supported sub-tracers: `callTracer`, `prestateTracer`, `opcodeTracer`, + /// `4byteTracer`, `noopTracer`. Other registered tracers + /// (e.g. `flatCallTracer`) are not yet routable from inside muxTracer. + MuxTracer, } #[derive(Deserialize, Default)] @@ -209,6 +225,34 @@ impl RpcHandler for TraceTransactionRequest { emit, })?) } + TracerType::MuxTracer => { + let mux_config: HashMap = + if let Some(value) = &self.trace_config.tracer_config { + serde_json::from_value(value.clone())? + } else { + return Err(RpcErr::BadParams( + "muxTracer requires tracerConfig".to_owned(), + )); + }; + let mut results = serde_json::Map::new(); + // NOTE: each sub-tracer receives the full `timeout`, not a + // proportional share — with N sub-tracers the wall-clock budget + // is up to N × timeout. This matches geth (which also re-executes + // per tracer in its JS tracer path) and is documented on MuxTracer. + for (tracer_name, sub_config) in &mux_config { + let result = run_tx_sub_tracer( + tracer_name, + sub_config, + self.tx_hash, + reexec, + timeout, + &context, + ) + .await?; + results.insert(tracer_name.clone(), result); + } + Ok(Value::Object(results)) + } } } } @@ -355,6 +399,463 @@ impl RpcHandler for TraceBlockByNumberRequest { .collect::>()?; Ok(serde_json::to_value(block_trace)?) } + TracerType::MuxTracer => Err(RpcErr::BadParams( + "muxTracer is not supported for debug_traceBlockByNumber yet \ + — use debug_traceTransaction with muxTracer per transaction" + .to_owned(), + )), } } } + +/// Runs a single sub-tracer for a transaction as part of a muxTracer request. +async fn run_tx_sub_tracer( + tracer_name: &str, + sub_config: &Value, + tx_hash: H256, + reexec: u32, + timeout: Duration, + context: &RpcApiContext, +) -> Result { + match tracer_name { + "callTracer" => { + let config: CallTracerConfig = serde_json::from_value(sub_config.clone())?; + let call_trace = context + .blockchain + .trace_transaction_calls( + tx_hash, + reexec, + timeout, + config.only_top_call, + config.with_log, + ) + .await + .map_err(|err| RpcErr::Internal(err.to_string()))?; + let top_frame = call_trace + .into_iter() + .next() + .ok_or(RpcErr::Internal("Empty call trace".to_string()))?; + Ok(serde_json::to_value(top_frame)?) + } + "prestateTracer" => { + let config: PrestateTracerConfig = serde_json::from_value(sub_config.clone())?; + config.validate()?; + let result = context + .blockchain + .trace_transaction_prestate( + tx_hash, + reexec, + timeout, + config.diff_mode, + config.include_empty, + ) + .await + .map_err(|err| RpcErr::Internal(err.to_string()))?; + match result { + PrestateResult::Prestate(trace) => Ok(serde_json::to_value(trace)?), + PrestateResult::Diff(diff) => Ok(serde_json::to_value(diff)?), + } + } + "opcodeTracer" => { + let cfg: OpcodeTracerConfig = serde_json::from_value(sub_config.clone())?; + let emit = StructLoggerEmit { + mem_size: cfg.enable_memory, + return_data: cfg.enable_return_data, + refund: false, + }; + let result = context + .blockchain + .trace_transaction_opcodes(tx_hash, reexec, timeout, cfg) + .await + .map_err(|err| RpcErr::Internal(err.to_string()))?; + Ok(serde_json::to_value(StructLoggerResult { + result: &result, + emit, + })?) + } + "4byteTracer" => { + // 4byteTracer accepts no configuration; any supplied sub_config is ignored. + let call_trace = context + .blockchain + .trace_transaction_calls(tx_hash, reexec, timeout, false, false) + .await + .map_err(|err| RpcErr::Internal(err.to_string()))?; + let top_frame = call_trace + .into_iter() + .next() + .ok_or_else(|| RpcErr::Internal("Empty call trace".to_string()))?; + let mut selectors = HashMap::new(); + collect_four_byte_selectors(&top_frame, &mut selectors); + Ok(serde_json::to_value(selectors)?) + } + "noopTracer" => Ok(serde_json::to_value(serde_json::Map::new())?), + unknown => Err(RpcErr::BadParams(format!("unknown sub-tracer: {unknown}"))), + } +} + +/// Collects 4-byte function selectors and calldata sizes from a call trace +/// tree, matching geth's built-in `4byteTracer` +/// (https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/native/4byte.go): +/// +/// - The top-level transaction call is **not** counted; only nested calls are. +/// - `CALL`, `DELEGATECALL`, `STATICCALL`, and `CALLCODE` are counted +/// (matching geth's `CaptureEnter`, which fires for all call types). +/// `CREATE`, `CREATE2`, and `SELFDESTRUCT` are skipped because their +/// input is init-code, not an ABI-encoded call. +/// - Invocations targeting precompile addresses are skipped. +/// - The reported size is `len(calldata) - 4` (the argument-bytes length). +fn collect_four_byte_selectors(top_frame: &CallTraceFrame, selectors: &mut HashMap) { + for sub_call in &top_frame.calls { + collect_four_byte_recursive(sub_call, selectors); + } +} + +fn collect_four_byte_recursive(frame: &CallTraceFrame, selectors: &mut HashMap) { + if matches!(frame.call_type, CallType::CALL | CallType::DELEGATECALL | CallType::STATICCALL | CallType::CALLCODE) + && frame.input.len() >= 4 + && !is_precompile_address(&frame.to) + { + let selector = hex::encode(&frame.input[..4]); + let arg_size = frame.input.len() - 4; + let key = format!("0x{selector}-{arg_size}"); + *selectors.entry(key).or_insert(0) += 1; + } + for sub_call in &frame.calls { + collect_four_byte_recursive(sub_call, selectors); + } +} + +/// Fork-agnostic precompile address check used by `4byteTracer`. Treats any +/// address that maps to a precompile in some fork ethrex supports as a +/// precompile — slightly more aggressive than geth's per-fork check but +/// defensible: calldata to those addresses is never a function selector. +fn is_precompile_address(addr: &Address) -> bool { + let bytes = addr.as_bytes(); + // L1 precompiles occupy 0x...01 through 0x...11 (BLAKE2F at 0x09, point + // evaluation at 0x0a, BLS12 family up to 0x11). + if bytes[..19].iter().all(|&b| b == 0) && (1..=0x11).contains(&bytes[19]) { + return true; + } + // L2 P256VERIFY sits at 0x...0100. + if bytes[..18].iter().all(|&b| b == 0) && bytes[18] == 0x01 && bytes[19] == 0x00 { + return true; + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rpc::RpcHandler; + use bytes::Bytes; + use serde_json::json; + + // --- TraceTransactionRequest parse tests --- + + #[test] + fn parse_trace_tx_with_hash_only() { + let params = Some(vec![json!( + "0x0000000000000000000000000000000000000000000000000000000000000001" + )]); + let req = TraceTransactionRequest::parse(¶ms).unwrap(); + assert_eq!(req.tx_hash, H256::from_low_u64_be(1)); + } + + #[test] + fn parse_trace_tx_with_config() { + let params = Some(vec![ + json!("0x0000000000000000000000000000000000000000000000000000000000000001"), + json!({"tracer": "callTracer", "tracerConfig": {"onlyTopCall": true}}), + ]); + let req = TraceTransactionRequest::parse(¶ms).unwrap(); + assert_eq!(req.tx_hash, H256::from_low_u64_be(1)); + assert!(matches!(req.trace_config.tracer, TracerType::CallTracer)); + } + + #[test] + fn parse_trace_tx_no_params() { + let result = TraceTransactionRequest::parse(&None); + assert!(result.is_err()); + } + + #[test] + fn parse_trace_tx_too_many_params() { + let params = Some(vec![json!("0x01"), json!({}), json!("extra")]); + let result = TraceTransactionRequest::parse(¶ms); + assert!(result.is_err()); + } + + #[test] + fn parse_trace_tx_default_tracer_is_call_tracer() { + let params = Some(vec![json!( + "0x0000000000000000000000000000000000000000000000000000000000000001" + )]); + let req = TraceTransactionRequest::parse(¶ms).unwrap(); + assert!(matches!(req.trace_config.tracer, TracerType::CallTracer)); + } + + // --- TraceBlockByNumberRequest parse tests --- + + #[test] + fn parse_trace_block_by_number_latest() { + let params = Some(vec![json!("latest")]); + let req = TraceBlockByNumberRequest::parse(¶ms).unwrap(); + assert!(matches!( + req.block, + BlockIdentifier::Tag(crate::types::block_identifier::BlockTag::Latest) + )); + } + + #[test] + fn parse_trace_block_by_number_hex() { + let params = Some(vec![json!("0xa")]); + let req = TraceBlockByNumberRequest::parse(¶ms).unwrap(); + assert!(matches!(req.block, BlockIdentifier::Number(10))); + } + + #[test] + fn parse_trace_block_by_number_with_config() { + let params = Some(vec![json!("0x1"), json!({"tracer": "prestateTracer"})]); + let req = TraceBlockByNumberRequest::parse(¶ms).unwrap(); + assert!(matches!( + req.trace_config.tracer, + TracerType::PrestateTracer + )); + } + + #[test] + fn parse_trace_block_by_number_no_params() { + let result = TraceBlockByNumberRequest::parse(&None); + assert!(result.is_err()); + } + + // --- TracerType deserialization tests --- + + #[test] + fn deserialize_tracer_type_call_tracer() { + let t: TracerType = serde_json::from_value(json!("callTracer")).unwrap(); + assert!(matches!(t, TracerType::CallTracer)); + } + + #[test] + fn deserialize_tracer_type_prestate_tracer() { + let t: TracerType = serde_json::from_value(json!("prestateTracer")).unwrap(); + assert!(matches!(t, TracerType::PrestateTracer)); + } + + #[test] + fn deserialize_tracer_type_opcode_tracer() { + let t: TracerType = serde_json::from_value(json!("opcodeTracer")).unwrap(); + assert!(matches!(t, TracerType::OpcodeTracer)); + } + + #[test] + fn deserialize_tracer_type_mux_tracer() { + let t: TracerType = serde_json::from_value(json!("muxTracer")).unwrap(); + assert!(matches!(t, TracerType::MuxTracer)); + } + + #[test] + fn deserialize_tracer_type_unknown_fails() { + let result = serde_json::from_value::(json!("unknownTracer")); + assert!(result.is_err()); + } + + // --- TraceConfig deserialization tests --- + + #[test] + fn deserialize_trace_config_defaults() { + let cfg: TraceConfig = serde_json::from_value(json!({})).unwrap(); + assert!(matches!(cfg.tracer, TracerType::CallTracer)); + assert!(cfg.tracer_config.is_none()); + assert!(cfg.timeout.is_none()); + assert!(cfg.reexec.is_none()); + } + + #[test] + fn deserialize_trace_config_with_timeout() { + let cfg: TraceConfig = serde_json::from_value(json!({"timeout": "10s"})).unwrap(); + assert_eq!(cfg.timeout, Some(Duration::from_secs(10))); + } + + #[test] + fn deserialize_trace_config_with_reexec() { + let cfg: TraceConfig = serde_json::from_value(json!({"reexec": 256})).unwrap(); + assert_eq!(cfg.reexec, Some(256)); + } + + // --- PrestateTracerConfig validation tests --- + + #[test] + fn prestate_config_default_is_valid() { + let cfg = PrestateTracerConfig::default(); + assert!(cfg.validate().is_ok()); + } + + #[test] + fn prestate_config_diff_mode_only_is_valid() { + let cfg = PrestateTracerConfig { + diff_mode: true, + include_empty: false, + }; + assert!(cfg.validate().is_ok()); + } + + #[test] + fn prestate_config_include_empty_only_is_valid() { + let cfg = PrestateTracerConfig { + diff_mode: false, + include_empty: true, + }; + assert!(cfg.validate().is_ok()); + } + + #[test] + fn prestate_config_diff_mode_and_include_empty_is_invalid() { + let cfg = PrestateTracerConfig { + diff_mode: true, + include_empty: true, + }; + assert!(cfg.validate().is_err()); + } + + // --- collect_four_byte_selectors tests --- + // + // Helpers that build a top-level frame with `calls` children. The tracer + // skips the top frame itself (matches geth's depth-0 skip), so the helper + // makes that intent explicit in the test bodies. + + fn top_frame_with_calls(calls: Vec) -> CallTraceFrame { + CallTraceFrame { + call_type: CallType::CALL, + input: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef, 0xff]), + calls, + ..Default::default() + } + } + + fn collect(top: &CallTraceFrame) -> HashMap { + let mut selectors = HashMap::new(); + collect_four_byte_selectors(top, &mut selectors); + selectors + } + + #[test] + fn four_byte_skips_top_level_call() { + let top = top_frame_with_calls(vec![]); + assert!(collect(&top).is_empty()); + } + + #[test] + fn four_byte_short_input_subcall_ignored() { + let short = CallTraceFrame { + call_type: CallType::CALL, + input: Bytes::from_static(&[0xa9, 0x05, 0x9c]), + ..Default::default() + }; + assert!(collect(&top_frame_with_calls(vec![short])).is_empty()); + } + + #[test] + fn four_byte_single_subcall_uses_arg_size() { + let child = CallTraceFrame { + call_type: CallType::CALL, + input: Bytes::from_static(&[0xa9, 0x05, 0x9c, 0xbb, 0x00, 0x01]), + ..Default::default() + }; + let s = collect(&top_frame_with_calls(vec![child])); + // 6 bytes total → selector + 2 arg bytes → key suffix "-2", not "-6". + assert_eq!(s.len(), 1); + assert_eq!(s["0xa9059cbb-2"], 1); + } + + #[test] + fn four_byte_nested_subcalls() { + let grandchild = CallTraceFrame { + call_type: CallType::CALL, + input: Bytes::from_static(&[0x23, 0xb8, 0x72, 0xdd, 0x01, 0x02, 0x03]), + ..Default::default() + }; + let child = CallTraceFrame { + call_type: CallType::CALL, + input: Bytes::from_static(&[0xa9, 0x05, 0x9c, 0xbb, 0xaa]), + calls: vec![grandchild], + ..Default::default() + }; + let s = collect(&top_frame_with_calls(vec![child])); + assert_eq!(s.len(), 2); + assert_eq!(s["0xa9059cbb-1"], 1); + assert_eq!(s["0x23b872dd-3"], 1); + } + + #[test] + fn four_byte_duplicate_subcalls_counted() { + let mk = || CallTraceFrame { + call_type: CallType::CALL, + input: Bytes::from_static(&[0xa9, 0x05, 0x9c, 0xbb, 0xaa]), + ..Default::default() + }; + let s = collect(&top_frame_with_calls(vec![mk(), mk()])); + assert_eq!(s.len(), 1); + assert_eq!(s["0xa9059cbb-1"], 2); + } + + #[test] + fn four_byte_counts_all_call_types_except_create_and_selfdestruct() { + let mk_with = |call_type: CallType| CallTraceFrame { + call_type, + input: Bytes::from_static(&[0xa9, 0x05, 0x9c, 0xbb, 0x01]), + ..Default::default() + }; + let top = top_frame_with_calls(vec![ + mk_with(CallType::CALL), + mk_with(CallType::DELEGATECALL), + mk_with(CallType::STATICCALL), + mk_with(CallType::CALLCODE), + mk_with(CallType::CREATE), + mk_with(CallType::CREATE2), + mk_with(CallType::SELFDESTRUCT), + ]); + let s = collect(&top); + // CALL + DELEGATECALL + STATICCALL + CALLCODE = 4 hits. + assert_eq!(s.len(), 1); + assert_eq!(s["0xa9059cbb-1"], 4); + } + + #[test] + fn four_byte_skips_precompile_targets() { + let mk = |addr: Address| CallTraceFrame { + call_type: CallType::CALL, + to: addr, + input: Bytes::from_static(&[0xa9, 0x05, 0x9c, 0xbb, 0x01]), + ..Default::default() + }; + let top = top_frame_with_calls(vec![ + mk(Address::from_low_u64_be(0x01)), // ECRECOVER + mk(Address::from_low_u64_be(0x09)), // BLAKE2F + mk(Address::from_low_u64_be(0x0a)), // POINT_EVALUATION + mk(Address::from_low_u64_be(0x11)), // BLS12_MAP_FP2_TO_G2 + mk(Address::from_low_u64_be(0x100)), // P256VERIFY (L2) + ]); + assert!(collect(&top).is_empty()); + } + + // --- MuxTracer parse tests --- + + #[test] + fn parse_trace_tx_mux_tracer() { + let params = Some(vec![ + json!("0x0000000000000000000000000000000000000000000000000000000000000001"), + json!({ + "tracer": "muxTracer", + "tracerConfig": { + "callTracer": {}, + "noopTracer": {} + } + }), + ]); + let req = TraceTransactionRequest::parse(¶ms).unwrap(); + assert!(matches!(req.trace_config.tracer, TracerType::MuxTracer)); + assert!(req.trace_config.tracer_config.is_some()); + } +} diff --git a/test/tests/rpc/debug_mux_tracer_tests.rs b/test/tests/rpc/debug_mux_tracer_tests.rs new file mode 100644 index 0000000000..7307ae7337 --- /dev/null +++ b/test/tests/rpc/debug_mux_tracer_tests.rs @@ -0,0 +1,187 @@ +use serde_json::{Value, json}; + +use super::helpers::{rpc_call, rpc_call_expect_err, setup_single_transfer_block}; + +async fn trace_with( + store: ðrex_storage::Store, + tx: ethrex_common::H256, + config: Value, +) -> Value { + rpc_call( + store, + "debug_traceTransaction", + vec![json!(format!("{tx:#x}")), config], + ) + .await +} + +#[tokio::test] +async fn mux_tracer_call_subtracer_matches_standalone() { + // The mux output's `callTracer` field must match what running + // `callTracer` alone produces. This is the load-bearing correctness + // invariant — anything subtler than equality means the multiplexer is + // mutating sub-tracer behaviour. + let env = setup_single_transfer_block().await; + + let mux = trace_with( + &env.store, + env.tx_hash, + json!({ + "tracer": "muxTracer", + "tracerConfig": { "callTracer": {"onlyTopCall": true} }, + }), + ) + .await; + let standalone = trace_with( + &env.store, + env.tx_hash, + json!({ + "tracer": "callTracer", + "tracerConfig": {"onlyTopCall": true}, + }), + ) + .await; + assert_eq!( + mux["callTracer"], standalone, + "muxTracer's callTracer slot must equal the standalone callTracer output" + ); +} + +#[tokio::test] +async fn mux_tracer_prestate_subtracer_matches_standalone() { + let env = setup_single_transfer_block().await; + let mux = trace_with( + &env.store, + env.tx_hash, + json!({ + "tracer": "muxTracer", + "tracerConfig": { "prestateTracer": {} }, + }), + ) + .await; + let standalone = trace_with(&env.store, env.tx_hash, json!({"tracer": "prestateTracer"})).await; + assert_eq!(mux["prestateTracer"], standalone); +} + +#[tokio::test] +async fn mux_tracer_multiple_subtracers_each_match_standalone() { + let env = setup_single_transfer_block().await; + let mux = trace_with( + &env.store, + env.tx_hash, + json!({ + "tracer": "muxTracer", + "tracerConfig": { + "callTracer": {}, + "prestateTracer": {"diffMode": true}, + }, + }), + ) + .await; + let call_standalone = + trace_with(&env.store, env.tx_hash, json!({"tracer": "callTracer"})).await; + let prestate_standalone = trace_with( + &env.store, + env.tx_hash, + json!({ + "tracer": "prestateTracer", + "tracerConfig": {"diffMode": true}, + }), + ) + .await; + assert_eq!(mux["callTracer"], call_standalone); + assert_eq!(mux["prestateTracer"], prestate_standalone); + assert_eq!( + mux.as_object().unwrap().len(), + 2, + "no spurious sub-tracer slots" + ); +} + +#[tokio::test] +async fn mux_tracer_noop_returns_empty_object() { + let env = setup_single_transfer_block().await; + let mux = trace_with( + &env.store, + env.tx_hash, + json!({ + "tracer": "muxTracer", + "tracerConfig": { "noopTracer": {} }, + }), + ) + .await; + assert_eq!( + mux["noopTracer"], + json!({}), + "noopTracer slot must be an empty object" + ); +} + +#[tokio::test] +async fn mux_tracer_unknown_subtracer_errors() { + let env = setup_single_transfer_block().await; + let err = rpc_call_expect_err( + &env.store, + "debug_traceTransaction", + vec![ + json!(format!("{:#x}", env.tx_hash)), + json!({ + "tracer": "muxTracer", + "tracerConfig": { "bogusTracer": {} }, + }), + ], + ) + .await; + let msg = format!("{err:?}"); + assert!( + msg.contains("unknown sub-tracer"), + "expected unknown-sub-tracer error, got: {msg}" + ); +} + +#[tokio::test] +async fn mux_tracer_missing_tracer_config_errors() { + let env = setup_single_transfer_block().await; + let err = rpc_call_expect_err( + &env.store, + "debug_traceTransaction", + vec![ + json!(format!("{:#x}", env.tx_hash)), + json!({"tracer": "muxTracer"}), + ], + ) + .await; + let msg = format!("{err:?}"); + assert!( + msg.contains("tracerConfig"), + "expected missing-config error, got: {msg}" + ); +} + +#[tokio::test] +async fn mux_tracer_block_level_returns_bad_params() { + // debug_traceBlockByNumber with muxTracer is not supported. It must + // surface as `BadParams` (user input) not `Internal` (server fault). + let env = setup_single_transfer_block().await; + let err = rpc_call_expect_err( + &env.store, + "debug_traceBlockByNumber", + vec![ + json!(format!("{:#x}", env.block.header.number)), + json!({ + "tracer": "muxTracer", + "tracerConfig": { "callTracer": {} }, + }), + ], + ) + .await; + let msg = format!("{err:?}"); + assert!( + msg.contains("BadParams"), + "block-level muxTracer must surface as BadParams, got: {msg}" + ); + assert!( + msg.contains("muxTracer"), + "error message should mention muxTracer, got: {msg}" + ); +} diff --git a/test/tests/rpc/helpers.rs b/test/tests/rpc/helpers.rs new file mode 100644 index 0000000000..ac9ca90d7c --- /dev/null +++ b/test/tests/rpc/helpers.rs @@ -0,0 +1,191 @@ +//! Shared setup helpers for the rpc integration tests. Each test file builds +//! its own in-memory `Store`, funds a known sender, and uses [`rpc_call`] to +//! drive the dispatcher just like a live request would. Individual test files +//! only need a subset of the helpers, so the module is permissive about dead +//! code rather than gating each item separately. +#![allow(dead_code)] + +use std::{fs::File, io::BufReader, path::PathBuf}; + +use bytes::Bytes; +use ethrex_blockchain::{ + Blockchain, + payload::{BuildPayloadArgs, create_payload}, +}; +use ethrex_common::{ + Address, H160, H256, U256, + types::{ + Block, BlockHeader, DEFAULT_BUILDER_GAS_CEIL, EIP1559Transaction, ELASTICITY_MULTIPLIER, + GenesisAccount, Transaction, TxKind, + }, +}; +use ethrex_l2_rpc::signer::{LocalSigner, Signable, Signer}; +use ethrex_rpc::rpc::map_http_requests; +use ethrex_rpc::test_utils::default_context_with_storage; +use ethrex_rpc::utils::{RpcErr, RpcRequest}; +use ethrex_storage::{EngineType, Store}; +use secp256k1::SecretKey; +use serde_json::{Value, json}; + +pub const TEST_PRIVATE_KEY: &str = + "850643a0224065ecce3882673c21f56bcf6eef86274cc21cadff15930b59fc8c"; +pub const TEST_MAX_FEE_PER_GAS: u64 = 10_000_000_000; +pub const TEST_GAS_LIMIT: u64 = 100_000; + +pub fn test_secret_key() -> SecretKey { + SecretKey::from_slice(&hex::decode(TEST_PRIVATE_KEY).unwrap()).unwrap() +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") +} + +pub fn sender_from_key(sk: &SecretKey) -> Address { + LocalSigner::new(*sk).address +} + +pub async fn setup_store(sender: Address) -> (Store, u64) { + let file = File::open(workspace_root().join("fixtures/genesis/execution-api.json")) + .expect("Failed to open genesis file"); + let reader = BufReader::new(file); + let mut genesis: ethrex_common::types::Genesis = + serde_json::from_reader(reader).expect("Failed to deserialize genesis file"); + let chain_id = genesis.config.chain_id; + genesis.alloc.insert( + sender, + GenesisAccount { + balance: U256::from(10).pow(U256::from(20)), + code: Bytes::new(), + storage: Default::default(), + nonce: 0, + }, + ); + let mut store = + Store::new("store.db", EngineType::InMemory).expect("Failed to build DB for testing"); + store + .add_initial_state(genesis) + .await + .expect("Failed to add genesis state"); + (store, chain_id) +} + +pub async fn build_block( + store: &Store, + blockchain: &Blockchain, + parent_header: &BlockHeader, +) -> Block { + let args = BuildPayloadArgs { + parent: parent_header.hash(), + timestamp: parent_header.timestamp + 12, + fee_recipient: H160::zero(), + random: H256::zero(), + withdrawals: Some(Vec::new()), + beacon_root: Some(H256::zero()), + slot_number: None, + version: 1, + elasticity_multiplier: ELASTICITY_MULTIPLIER, + gas_ceil: DEFAULT_BUILDER_GAS_CEIL, + }; + let block = create_payload(&args, store, Bytes::new()).unwrap(); + let result = blockchain.build_payload(block).unwrap(); + result.payload +} + +pub async fn create_transfer_tx( + chain_id: u64, + nonce: u64, + to: Address, + value: U256, + signer: &Signer, +) -> Transaction { + let mut tx = Transaction::EIP1559Transaction(EIP1559Transaction { + chain_id, + nonce, + max_priority_fee_per_gas: 0, + max_fee_per_gas: TEST_MAX_FEE_PER_GAS, + gas_limit: TEST_GAS_LIMIT, + to: TxKind::Call(to), + value, + data: Bytes::new(), + ..Default::default() + }); + tx.sign_inplace(signer).await.unwrap(); + tx +} + +pub async fn build_and_execute_block( + store: &Store, + blockchain: &Blockchain, + parent_header: &BlockHeader, + transactions: Vec, +) -> Block { + for tx in &transactions { + blockchain + .add_transaction_to_pool(tx.clone()) + .await + .expect("tx should enter pool"); + } + let block = build_block(store, blockchain, parent_header).await; + assert_eq!(block.body.transactions.len(), transactions.len()); + blockchain + .add_block(block.clone()) + .expect("block should be valid"); + store + .forkchoice_update(vec![], block.header.number, block.hash(), None, None) + .await + .unwrap(); + block +} + +pub async fn rpc_call(store: &Store, method: &str, params: Vec) -> Value { + let request = build_rpc_request(method, params); + let context = default_context_with_storage(store.clone()).await; + map_http_requests(&request, context) + .await + .expect("RPC call should succeed") +} + +pub async fn rpc_call_expect_err(store: &Store, method: &str, params: Vec) -> RpcErr { + let request = build_rpc_request(method, params); + let context = default_context_with_storage(store.clone()).await; + map_http_requests(&request, context) + .await + .expect_err("RPC call should fail") +} + +fn build_rpc_request(method: &str, params: Vec) -> RpcRequest { + let body = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": 1, + }); + serde_json::from_value(body).expect("valid RPC request") +} + +pub struct TestEnv { + pub store: Store, + pub block: Block, + pub tx_hash: H256, + pub sender: Address, +} + +pub async fn setup_single_transfer_block() -> TestEnv { + let sk = test_secret_key(); + let sender = sender_from_key(&sk); + let signer: Signer = LocalSigner::new(sk).into(); + let (store, chain_id) = setup_store(sender).await; + let blockchain = Blockchain::default_with_store(store.clone()); + let genesis_header = store.get_block_header(0).unwrap().unwrap(); + let recipient = Address::from_low_u64_be(0xAA); + let value = U256::from(1_000_000_000_000_000_000u64); + let tx = create_transfer_tx(chain_id, 0, recipient, value, &signer).await; + let tx_hash = tx.hash(); + let block = build_and_execute_block(&store, &blockchain, &genesis_header, vec![tx]).await; + TestEnv { + store, + block, + tx_hash, + sender, + } +} diff --git a/test/tests/rpc/mod.rs b/test/tests/rpc/mod.rs index f61eb7f11a..764923a1d3 100644 --- a/test/tests/rpc/mod.rs +++ b/test/tests/rpc/mod.rs @@ -1,4 +1,6 @@ mod authrpc_batch_tests; mod client_version_tests; +mod debug_mux_tracer_tests; +mod helpers; mod http_batch_tests; mod subscription_manager_tests;