From 94d0e41c29cedca597a1b3bca5a2db45804eeb8b Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 12 Mar 2026 17:37:21 +0530 Subject: [PATCH 1/7] refactor to prepare for the debug trace transaction --- src/rpc/methods/eth.rs | 174 +++++++------- src/rpc/methods/eth/trace.rs | 140 ++++++++---- src/rpc/methods/eth/types.rs | 68 +++++- .../forest__rpc__tests__rpc__v0.snap | 214 +++++++++++++++++- .../forest__rpc__tests__rpc__v1.snap | 17 +- .../forest__rpc__tests__rpc__v2.snap | 17 +- src/state_manager/mod.rs | 182 ++++++++++----- 7 files changed, 597 insertions(+), 215 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 75514171ef38..5bfe7eb07b31 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -33,7 +33,7 @@ use crate::rpc::{ eth::{ errors::EthErrors, filter::{SkipEvent, event::EventFilter, mempool::MempoolFilter, tipset::TipSetFilter}, - types::{EthBlockTrace, EthTrace}, + types::EthBlockTrace, utils::decode_revert_reason, }, methods::chain::ChainGetTipSetV2, @@ -382,37 +382,6 @@ impl BlockNumberOrHash { } } -/// Selects which trace outputs to include in the `trace_call` response. -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum EthTraceType { - /// Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`) - /// with details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`. - Trace, - /// Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`) - /// caused by the simulated transaction. - /// - /// It shows `"from"` and `"to"` values for modified fields, using `"+"`, `"-"`, or `"="` for code changes. - StateDiff, -} - -lotus_json_with_self!(EthTraceType); - -/// Result payload returned by `trace_call`. -#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthTraceResults { - /// Output bytes from the transaction execution. - pub output: EthBytes, - /// State diff showing all account changes. - pub state_diff: Option, - /// Call trace hierarchy (empty when not requested). - #[serde(default)] - pub trace: Vec, -} - -lotus_json_with_self!(EthTraceResults); - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, GetSize)] #[serde(untagged)] // try a Vec, then a Vec pub enum Transactions { @@ -3396,22 +3365,53 @@ impl RpcMethod<1> for EthTraceBlock { } } -async fn eth_trace_block( +struct TipsetTraceEntry { + tx_hash: EthHash, + msg_position: i64, + invoc_result: ApiInvocResult, +} + +impl TipsetTraceEntry { + /// Builds Parity-style traces for this entry using the given state tree. + fn build_parity_traces( + &self, + state: &StateTree, + ) -> Result, ServerError> { + let mut env = trace::base_environment(state, &self.invoc_result.msg.from).map_err(|e| { + format!( + "when processing message {}: {}", + self.invoc_result.msg_cid, e + ) + })?; + if let Some(ref execution_trace) = self.invoc_result.execution_trace { + trace::build_traces(&mut env, &[], execution_trace.clone())?; + } + Ok(env.traces) + } +} + +/// Replays a tipset and resolves every non-system transaction into a [`TipsetTraceEntry`]. +async fn execute_tipset_traces( ctx: &Ctx, ts: &Tipset, ext: &http::Extensions, -) -> Result, ServerError> +) -> Result<(StateTree, Vec), ServerError> where DB: Blockstore + Send + Sync + 'static, { - let (state_root, trace) = ctx.state_manager.execution_trace(ts)?; + let (state_root, raw_traces) = { + let sm = ctx.state_manager.clone(); + let ts = ts.clone(); + tokio::task::spawn_blocking(move || sm.execution_trace(&ts)) + .await + .context("execution_trace task panicked")?? + }; + let state = ctx.state_manager.get_state_tree(&state_root)?; - let cid = ts.key().cid()?; - let block_hash: EthHash = cid.into(); - let mut all_traces = vec![]; + + let mut entries = Vec::new(); let mut msg_idx = 0; - for ir in trace.into_iter() { - // ignore messages from system actor + for ir in raw_traces { if ir.msg.from == system::ADDRESS.into() { continue; } @@ -3419,26 +3419,37 @@ where let tx_hash = EthGetTransactionHashByCid::handle(ctx.clone(), (ir.msg_cid,), ext).await?; let tx_hash = tx_hash .with_context(|| format!("cannot find transaction hash for cid {}", ir.msg_cid))?; - let mut env = trace::base_environment(&state, &ir.msg.from) - .map_err(|e| format!("when processing message {}: {}", ir.msg_cid, e))?; - if let Some(execution_trace) = ir.execution_trace { - trace::build_traces(&mut env, &[], execution_trace)?; - for trace in env.traces { - all_traces.push(EthBlockTrace { - trace: EthTrace { - r#type: trace.r#type, - subtraces: trace.subtraces, - trace_address: trace.trace_address, - action: trace.action, - result: trace.result, - error: trace.error, - }, - block_hash, - block_number: ts.epoch(), - transaction_hash: tx_hash, - transaction_position: msg_idx as i64, - }); - } + entries.push(TipsetTraceEntry { + tx_hash, + msg_position: msg_idx, + invoc_result: ir, + }); + } + + Ok((state, entries)) +} + +async fn eth_trace_block( + ctx: &Ctx, + ts: &Tipset, + ext: &http::Extensions, +) -> Result, ServerError> +where + DB: Blockstore + Send + Sync + 'static, +{ + let (state, entries) = execute_tipset_traces(ctx, ts, ext).await?; + let block_hash: EthHash = ts.key().cid()?.into(); + let mut all_traces = vec![]; + + for entry in entries { + for trace in entry.build_parity_traces(&state)? { + all_traces.push(EthBlockTrace { + trace, + block_hash, + block_number: ts.epoch(), + transaction_hash: entry.tx_hash, + transaction_position: entry.msg_position, + }); } } Ok(all_traces) @@ -3657,43 +3668,16 @@ async fn eth_trace_replay_block_transactions( where DB: Blockstore + Send + Sync + 'static, { - let (state_root, trace) = ctx.state_manager.execution_trace(ts)?; - - let state = ctx.state_manager.get_state_tree(&state_root)?; + let (state, entries) = execute_tipset_traces(ctx, ts, ext).await?; let mut all_traces = vec![]; - for ir in trace.into_iter() { - if ir.msg.from == system::ADDRESS.into() { - continue; - } - - let tx_hash = EthGetTransactionHashByCid::handle(ctx.clone(), (ir.msg_cid,), ext).await?; - let tx_hash = tx_hash - .with_context(|| format!("cannot find transaction hash for cid {}", ir.msg_cid))?; - - let mut env = trace::base_environment(&state, &ir.msg.from) - .map_err(|e| format!("when processing message {}: {}", ir.msg_cid, e))?; - - if let Some(execution_trace) = ir.execution_trace { - trace::build_traces(&mut env, &[], execution_trace)?; - - let get_output = || -> EthBytes { - env.traces - .first() - .map_or_else(EthBytes::default, |trace| match &trace.result { - TraceResult::Call(r) => r.output.clone(), - TraceResult::Create(r) => r.code.clone(), - }) - }; - - all_traces.push(EthReplayBlockTransactionTrace { - output: get_output(), - state_diff: None, - trace: env.traces.clone(), - transaction_hash: tx_hash, - vm_trace: None, - }); - }; + for entry in entries { + let traces = entry.build_parity_traces(&state)?; + all_traces.push(EthReplayBlockTransactionTrace { + full_trace: EthTraceResults::from_parity_traces(traces), + transaction_hash: entry.tx_hash, + vm_trace: None, + }); } Ok(all_traces) diff --git a/src/rpc/methods/eth/trace.rs b/src/rpc/methods/eth/trace.rs index 936f4fe3ee70..4080d36c317c 100644 --- a/src/rpc/methods/eth/trace.rs +++ b/src/rpc/methods/eth/trace.rs @@ -33,6 +33,18 @@ use std::borrow::Cow; use std::collections::BTreeMap; use tracing::debug; +/// Error string used in Parity-format traces. +pub(crate) const PARITY_TRACE_REVERT_ERROR: &str = "Reverted"; + +const PARITY_EVM_INVALID_INSTRUCTION: &str = "invalid instruction"; +const PARITY_EVM_UNDEFINED_INSTRUCTION: &str = "undefined instruction"; +const PARITY_EVM_STACK_UNDERFLOW: &str = "stack underflow"; +const PARITY_EVM_STACK_OVERFLOW: &str = "stack overflow"; +const PARITY_EVM_ILLEGAL_MEMORY_ACCESS: &str = "illegal memory access"; +const PARITY_EVM_BAD_JUMPDEST: &str = "invalid jump destination"; +const PARITY_EVM_SELFDESTRUCT_FAILED: &str = "self destruct failed"; +const PARITY_EVM_OUT_OF_GAS: &str = "out of gas"; + /// KAMT configuration matching the EVM actor in builtin-actors. // Code is taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L47 fn evm_kamt_config() -> KamtConfig { @@ -112,7 +124,7 @@ fn trace_err_msg(trace: &ExecutionTrace) -> Option { // EVM tools often expect this literal string. if code == ExitCode::SYS_OUT_OF_GAS { - return Some("out of gas".into()); + return Some(PARITY_EVM_OUT_OF_GAS.into()); } // indicate when we have a "system" error. @@ -123,18 +135,22 @@ fn trace_err_msg(trace: &ExecutionTrace) -> Option { // handle special exit codes from the EVM/EAM. if trace_is_evm_or_eam(trace) { match code.into() { - evm12::EVM_CONTRACT_REVERTED => return Some("Reverted".into()), // capitalized for compatibility - evm12::EVM_CONTRACT_INVALID_INSTRUCTION => return Some("invalid instruction".into()), + evm12::EVM_CONTRACT_REVERTED => return Some(PARITY_TRACE_REVERT_ERROR.into()), // capitalized for compatibility + evm12::EVM_CONTRACT_INVALID_INSTRUCTION => { + return Some(PARITY_EVM_INVALID_INSTRUCTION.into()); + } evm12::EVM_CONTRACT_UNDEFINED_INSTRUCTION => { - return Some("undefined instruction".into()); + return Some(PARITY_EVM_UNDEFINED_INSTRUCTION.into()); } - evm12::EVM_CONTRACT_STACK_UNDERFLOW => return Some("stack underflow".into()), - evm12::EVM_CONTRACT_STACK_OVERFLOW => return Some("stack overflow".into()), + evm12::EVM_CONTRACT_STACK_UNDERFLOW => return Some(PARITY_EVM_STACK_UNDERFLOW.into()), + evm12::EVM_CONTRACT_STACK_OVERFLOW => return Some(PARITY_EVM_STACK_OVERFLOW.into()), evm12::EVM_CONTRACT_ILLEGAL_MEMORY_ACCESS => { - return Some("illegal memory access".into()); + return Some(PARITY_EVM_ILLEGAL_MEMORY_ACCESS.into()); + } + evm12::EVM_CONTRACT_BAD_JUMPDEST => return Some(PARITY_EVM_BAD_JUMPDEST.into()), + evm12::EVM_CONTRACT_SELFDESTRUCT_FAILED => { + return Some(PARITY_EVM_SELFDESTRUCT_FAILED.into()); } - evm12::EVM_CONTRACT_BAD_JUMPDEST => return Some("invalid jump destination".into()), - evm12::EVM_CONTRACT_SELFDESTRUCT_FAILED => return Some("self destruct failed".into()), _ => (), } } @@ -659,6 +675,32 @@ fn trace_evm_private( } } +/// Returns the effective nonce for an actor: EVM nonce for EVM actors, sequence otherwise. +fn actor_nonce(store: &DB, actor: &ActorState) -> EthUint64 { + if is_evm_actor(&actor.code) { + EthUint64::from( + evm::State::load(store, actor.code, actor.state) + .map(|s| s.nonce()) + .unwrap_or(actor.sequence), + ) + } else { + EthUint64::from(actor.sequence) + } +} + +/// Returns the deployed bytecode of an EVM actor, or `None` for non-EVM actors. +fn actor_bytecode(store: &DB, actor: &ActorState) -> Option { + if !is_evm_actor(&actor.code) { + return None; + } + let evm_state = evm::State::load(store, actor.code, actor.state).ok()?; + store + .get(&evm_state.bytecode()) + .ok() + .flatten() + .map(EthBytes) +} + /// Build state diff by comparing pre and post-execution states for touched addresses. pub(crate) fn build_state_diff( store: &S, @@ -702,41 +744,14 @@ fn build_account_diff( let post_balance = post_actor.map(|a| EthBigInt(a.balance.atto().clone())); diff.balance = Delta::from_comparison(pre_balance, post_balance); - // Helper to get nonce from actor (uses EVM nonce for EVM actors) - let get_nonce = |actor: &ActorState| -> EthUint64 { - if is_evm_actor(&actor.code) { - EthUint64::from( - evm::State::load(store, actor.code, actor.state) - .map(|s| s.nonce()) - .unwrap_or(actor.sequence), - ) - } else { - EthUint64::from(actor.sequence) - } - }; - - // Helper to get bytecode from an EVM actor - let get_bytecode = |actor: &ActorState| -> Option { - if !is_evm_actor(&actor.code) { - return None; - } - - let evm_state = evm::State::load(store, actor.code, actor.state).ok()?; - store - .get(&evm_state.bytecode()) - .ok() - .flatten() - .map(EthBytes) - }; - // Compare nonce - let pre_nonce = pre_actor.map(get_nonce); - let post_nonce = post_actor.map(get_nonce); + let pre_nonce = pre_actor.map(|a| actor_nonce(store, a)); + let post_nonce = post_actor.map(|a| actor_nonce(store, a)); diff.nonce = Delta::from_comparison(pre_nonce, post_nonce); // Compare code (bytecode for EVM actors) - let pre_code = pre_actor.and_then(get_bytecode); - let post_code = post_actor.and_then(get_bytecode); + let pre_code = pre_actor.and_then(|a| actor_bytecode(store, a)); + let post_code = post_actor.and_then(|a| actor_bytecode(store, a)); diff.code = Delta::from_comparison(pre_code, post_code); // Compare storage slots for EVM actors @@ -1460,4 +1475,49 @@ mod tests { // Code should be unchanged (None -> None for non-EVM actors) assert!(diff.code.is_unchanged()); } + + #[test] + fn test_actor_nonce_non_evm() { + let store = MemoryDB::default(); + let actor = create_test_actor(1000, 42); + let nonce = actor_nonce(&store, &actor); + assert_eq!(nonce.0, 42); + } + + #[test] + fn test_actor_nonce_evm() { + let store = Arc::new(MemoryDB::default()); + if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 7, Some(&[0x60])) { + let nonce = actor_nonce(store.as_ref(), &actor); + // EVM actors use the EVM nonce field, not the actor sequence + assert_eq!(nonce.0, 7); + } + } + + #[test] + fn test_actor_bytecode_non_evm() { + let store = MemoryDB::default(); + let actor = create_test_actor(1000, 0); + assert!(actor_bytecode(&store, &actor).is_none()); + } + + #[test] + fn test_actor_bytecode_evm() { + let store = Arc::new(MemoryDB::default()); + let bytecode = &[0x60, 0x80, 0x60, 0x40, 0x52]; + if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 1, Some(bytecode)) { + let result = actor_bytecode(store.as_ref(), &actor); + assert_eq!(result, Some(EthBytes(bytecode.to_vec()))); + } + } + + #[test] + fn test_actor_bytecode_evm_no_bytecode() { + let store = Arc::new(MemoryDB::default()); + if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 1, None) { + // No bytecode stored => None (Cid::default() won't resolve to raw data) + let result = actor_bytecode(store.as_ref(), &actor); + assert!(result.is_none()); + } + } } diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index ef5759995d9a..391b7a415d16 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -317,13 +317,20 @@ pub struct EthCallMessage { pub gas_price: Option, #[serde(skip_serializing_if = "Option::is_none", default)] pub value: Option, - // Some clients use `input`, others use `data`. We have to support both. - #[serde(alias = "input", skip_serializing_if = "Option::is_none", default)] + // Some clients use `input`, others use `data`; both accepted, `input` takes precedence. + #[serde(skip_serializing_if = "Option::is_none", default)] pub data: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub input: Option, } lotus_json_with_self!(EthCallMessage); impl EthCallMessage { + /// Returns the effective calldata, preferring `input` over `data` when both are set. + pub fn effective_input(&self) -> Option<&EthBytes> { + self.input.as_ref().or(self.data.as_ref()) + } + pub fn convert_data_to_message_params(data: EthBytes) -> anyhow::Result { if data.0.is_empty() { Ok(RawBytes::new(data.0)) @@ -353,7 +360,8 @@ impl TryFrom for Message { } }; let params = tx - .data + .effective_input() + .cloned() .map(EthCallMessage::convert_data_to_message_params) .transpose()? .unwrap_or_default(); @@ -638,6 +646,54 @@ impl Default for TraceResult { } } +/// Selects which trace outputs to include in the `trace_call` response. +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum EthTraceType { + /// Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`) + /// with details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`. + Trace, + /// Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`) + /// caused by the simulated transaction. + /// + /// It shows `"from"` and `"to"` values for modified fields, using `"+"`, `"-"`, or `"="` for code changes. + StateDiff, +} + +lotus_json_with_self!(EthTraceType); + +/// Result payload returned by `trace_call`. +#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthTraceResults { + /// Output bytes from the transaction execution. + pub output: EthBytes, + /// State diff showing all account changes. + pub state_diff: Option, + /// Call trace hierarchy (empty when not requested). + #[serde(default)] + pub trace: Vec, +} + +lotus_json_with_self!(EthTraceResults); + +impl EthTraceResults { + /// Constructs from Parity traces, extracting output from the root trace. + pub fn from_parity_traces(traces: Vec) -> Self { + let output = traces + .first() + .map_or_else(EthBytes::default, |trace| match &trace.result { + TraceResult::Call(r) => r.output.clone(), + TraceResult::Create(r) => r.code.clone(), + }); + Self { + output, + state_diff: None, + trace: traces, + } + } +} + #[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct EthTrace { @@ -675,10 +731,10 @@ impl EthBlockTrace { #[derive(PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct EthReplayBlockTransactionTrace { - pub output: EthBytes, - pub state_diff: Option, - pub trace: Vec, + #[serde(flatten)] + pub full_trace: EthTraceResults, pub transaction_hash: EthHash, + /// `None` because FVM does not support opcode-level VM traces. pub vm_trace: Option, } lotus_json_with_self!(EthReplayBlockTransactionTrace); diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap index f38d34079dcb..256d9bff739a 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v0.snap @@ -4337,6 +4337,26 @@ methods: paramStructure: by-position components: schemas: + AccountDiff: + description: "Account state diff after transaction execution.\nTracks changes to balance, nonce, code, and storage." + type: object + properties: + balance: + $ref: "#/components/schemas/Delta" + code: + $ref: "#/components/schemas/Delta2" + nonce: + $ref: "#/components/schemas/Delta3" + storage: + description: All touched/changed storage values (key -> delta) + type: object + additionalProperties: + $ref: "#/components/schemas/Delta4" + required: + - balance + - code + - nonce + - storage ActorEvent: type: object properties: @@ -5113,6 +5133,58 @@ components: - tipset_keys - skip_checksum - dry_run + ChangedType: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthBigInt" + to: + description: Value after the change + $ref: "#/components/schemas/EthBigInt" + required: + - from + - to + ChangedType2: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthBytes" + to: + description: Value after the change + $ref: "#/components/schemas/EthBytes" + required: + - from + - to + ChangedType3: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthUint64" + to: + description: Value after the change + $ref: "#/components/schemas/EthUint64" + required: + - from + - to + ChangedType4: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthHash" + to: + description: Value after the change + $ref: "#/components/schemas/EthHash" + required: + - from + - to Cid: type: object properties: @@ -5213,6 +5285,126 @@ components: required: - Min - Max + Delta: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthBigInt" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthBigInt" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType" + additionalProperties: false + required: + - "*" + Delta2: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthBytes" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthBytes" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType2" + additionalProperties: false + required: + - "*" + Delta3: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthUint64" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthUint64" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType3" + additionalProperties: false + required: + - "*" + Delta4: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthHash" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthHash" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType4" + additionalProperties: false + required: + - "*" ECTipSet: type: object properties: @@ -5344,6 +5536,10 @@ components: anyOf: - $ref: "#/components/schemas/EthBigInt" - type: "null" + input: + anyOf: + - $ref: "#/components/schemas/EthBytes" + - type: "null" to: anyOf: - $ref: "#/components/schemas/EthAddress" @@ -5531,27 +5727,32 @@ components: - blockHash - blockNumber EthReplayBlockTransactionTrace: + description: "Result payload returned by `trace_call`." type: object properties: output: + description: Output bytes from the transaction execution. $ref: "#/components/schemas/EthBytes" stateDiff: - type: - - string - - "null" + description: State diff showing all account changes. + anyOf: + - $ref: "#/components/schemas/StateDiff" + - type: "null" trace: + description: Call trace hierarchy (empty when not requested). type: array + default: [] items: $ref: "#/components/schemas/EthTrace" transactionHash: $ref: "#/components/schemas/EthHash" vmTrace: + description: "`None` because FVM does not support opcode-level VM traces." type: - string - "null" required: - output - - trace - transactionHash EthSyncingResultLotusJson: anyOf: @@ -7333,6 +7534,11 @@ components: - Manifest - Bundle - ActorCids + StateDiff: + description: State diff containing all account changes from a transaction. + type: object + additionalProperties: + $ref: "#/components/schemas/AccountDiff" SupplementalData: type: object properties: diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index bd521775b553..36040a8d3123 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -5553,6 +5553,10 @@ components: anyOf: - $ref: "#/components/schemas/EthBigInt" - type: "null" + input: + anyOf: + - $ref: "#/components/schemas/EthBytes" + - type: "null" to: anyOf: - $ref: "#/components/schemas/EthAddress" @@ -5740,27 +5744,32 @@ components: - blockHash - blockNumber EthReplayBlockTransactionTrace: + description: "Result payload returned by `trace_call`." type: object properties: output: + description: Output bytes from the transaction execution. $ref: "#/components/schemas/EthBytes" stateDiff: - type: - - string - - "null" + description: State diff showing all account changes. + anyOf: + - $ref: "#/components/schemas/StateDiff" + - type: "null" trace: + description: Call trace hierarchy (empty when not requested). type: array + default: [] items: $ref: "#/components/schemas/EthTrace" transactionHash: $ref: "#/components/schemas/EthHash" vmTrace: + description: "`None` because FVM does not support opcode-level VM traces." type: - string - "null" required: - output - - trace - transactionHash EthSyncingResultLotusJson: anyOf: diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index 616856ec9bdf..20ab1274023f 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -2024,6 +2024,10 @@ components: anyOf: - $ref: "#/components/schemas/EthBigInt" - type: "null" + input: + anyOf: + - $ref: "#/components/schemas/EthBytes" + - type: "null" to: anyOf: - $ref: "#/components/schemas/EthAddress" @@ -2211,27 +2215,32 @@ components: - blockHash - blockNumber EthReplayBlockTransactionTrace: + description: "Result payload returned by `trace_call`." type: object properties: output: + description: Output bytes from the transaction execution. $ref: "#/components/schemas/EthBytes" stateDiff: - type: - - string - - "null" + description: State diff showing all account changes. + anyOf: + - $ref: "#/components/schemas/StateDiff" + - type: "null" trace: + description: Call trace hierarchy (empty when not requested). type: array + default: [] items: $ref: "#/components/schemas/EthTrace" transactionHash: $ref: "#/components/schemas/EthHash" vmTrace: + description: "`None` because FVM does not support opcode-level VM traces." type: - string - "null" required: - output - - trace - transactionHash EthSyncingResultLotusJson: anyOf: diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index c042351222c5..ae5d77b77609 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -1856,6 +1856,118 @@ where }) } +/// Shared context for creating VMs and preparing tipset state. +/// +/// Encapsulates randomness source, genesis info, VM construction, +/// null-epoch cron handling, and state migrations. +struct TipsetExecutor<'a, DB: Blockstore + Send + Sync + 'static> { + tipset: Tipset, + rand: ChainRand, + chain_config: Arc, + chain_index: Arc>>, + genesis_info: GenesisInfo, + engine: &'a MultiEngine, +} + +impl<'a, DB: Blockstore + Send + Sync + 'static> TipsetExecutor<'a, DB> { + fn new( + chain_index: Arc>>, + chain_config: Arc, + beacon: Arc, + engine: &'a MultiEngine, + tipset: Tipset, + ) -> Self { + let rand = ChainRand::new( + chain_config.clone(), + tipset.clone(), + chain_index.clone(), + beacon, + ); + let genesis_info = GenesisInfo::from_chain_config(chain_config.clone()); + Self { + tipset, + rand, + chain_config, + chain_index, + genesis_info, + engine, + } + } + + fn create_vm( + &self, + state_root: Cid, + epoch: ChainEpoch, + timestamp: u64, + trace: VMTrace, + ) -> anyhow::Result> { + let circ_supply = self.genesis_info.get_vm_circulating_supply( + epoch, + self.chain_index.db(), + &state_root, + )?; + VM::new( + ExecutionContext { + heaviest_tipset: self.tipset.clone(), + state_tree_root: state_root, + epoch, + rand: Box::new(self.rand.clone()), + base_fee: self.tipset.min_ticket_block().parent_base_fee.clone(), + circ_supply, + chain_config: self.chain_config.clone(), + chain_index: self.chain_index.clone(), + timestamp, + }, + self.engine, + trace, + ) + } + + /// Produces the state root ready for message execution by running + /// null-epoch `crons` and any pending state migrations. + fn prepare_parent_state( + &self, + genesis_timestamp: u64, + null_epoch_trace: VMTrace, + cron_callback: &mut Option, + ) -> anyhow::Result<(Cid, ChainEpoch, Vec)> + where + F: FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>, + { + use crate::shim::clock::EPOCH_DURATION_SECONDS; + + let mut parent_state = *self.tipset.parent_state(); + let parent_epoch = + Tipset::load_required(self.chain_index.db(), self.tipset.parents())?.epoch(); + let epoch = self.tipset.epoch(); + + for epoch_i in parent_epoch..epoch { + if epoch_i > parent_epoch { + let timestamp = genesis_timestamp + ((EPOCH_DURATION_SECONDS * epoch_i) as u64); + parent_state = stacker::grow(64 << 20, || -> anyhow::Result { + let mut vm = + self.create_vm(parent_state, epoch_i, timestamp, null_epoch_trace)?; + if let Err(e) = vm.run_cron(epoch_i, cron_callback.as_mut()) { + error!("Beginning of epoch cron failed to run: {e}"); + } + vm.flush() + })?; + } + if let Some(new_state) = run_state_migrations( + epoch_i, + &self.chain_config, + self.chain_index.db(), + &parent_state, + )? { + parent_state = new_state; + } + } + + let block_messages = BlockMessages::for_tipset(self.chain_index.db(), &self.tipset)?; + Ok((parent_state, epoch, block_messages)) + } +} + /// Messages are transactions that produce new states. The state (usually /// referred to as the 'state-tree') is a mapping from actor addresses to actor /// states. Each block contains the hash of the state-tree that should be used @@ -1953,7 +2065,6 @@ where // 4. execute block messages // 5. write the state-tree to the DB and return the CID - // step 1: special case for genesis block if tipset.epoch() == 0 { // NB: This is here because the process that executes blocks requires that the // block miner reference a valid miner in the state tree. Unless we create some @@ -1968,80 +2079,27 @@ where }); } - let rand = ChainRand::new( - Arc::clone(&chain_config), - tipset.clone(), - Arc::clone(&chain_index), + let exec = TipsetExecutor::new( + chain_index.clone(), + chain_config, beacon, + engine, + tipset.clone(), ); - let genesis_info = GenesisInfo::from_chain_config(chain_config.clone()); - let create_vm = |state_root: Cid, epoch, timestamp| { - let circulating_supply = - genesis_info.get_vm_circulating_supply(epoch, chain_index.db(), &state_root)?; - VM::new( - ExecutionContext { - heaviest_tipset: tipset.clone(), - state_tree_root: state_root, - epoch, - rand: Box::new(rand.clone()), - base_fee: tipset.min_ticket_block().parent_base_fee.clone(), - circ_supply: circulating_supply, - chain_config: Arc::clone(&chain_config), - chain_index: Arc::clone(&chain_index), - timestamp, - }, - engine, - enable_tracing, - ) - }; - - let mut parent_state = *tipset.parent_state(); - - let parent_epoch = Tipset::load_required(chain_index.db(), tipset.parents())?.epoch(); - let epoch = tipset.epoch(); - - for epoch_i in parent_epoch..epoch { - if epoch_i > parent_epoch { - // step 2: running cron for any null-tipsets - let timestamp = genesis_timestamp + ((EPOCH_DURATION_SECONDS * epoch_i) as u64); - - // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from - // FVM, but that introduces some constraints, and possible deadlocks. - parent_state = stacker::grow(64 << 20, || -> anyhow::Result { - let mut vm = create_vm(parent_state, epoch_i, timestamp)?; - // run cron for null rounds if any - if let Err(e) = vm.run_cron(epoch_i, callback.as_mut()) { - error!("Beginning of epoch cron failed to run: {}", e); - } - vm.flush() - })?; - } - - // step 3: run migrations - if let Some(new_state) = - run_state_migrations(epoch_i, &chain_config, chain_index.db(), &parent_state)? - { - parent_state = new_state; - } - } - - let block_messages = BlockMessages::for_tipset(chain_index.db(), &tipset) - .map_err(|e| Error::Other(e.to_string()))?; + let (parent_state, epoch, block_messages) = + exec.prepare_parent_state(genesis_timestamp, enable_tracing, &mut callback)?; // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from // FVM, but that introduces some constraints, and possible deadlocks. stacker::grow(64 << 20, || -> anyhow::Result { - let mut vm = create_vm(parent_state, epoch, tipset.min_timestamp())?; + let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?; - // step 4: apply tipset messages let (receipts, events, events_roots) = vm.apply_block_messages(&block_messages, epoch, callback)?; - // step 5: construct receipt root from receipts let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts)?; - // step 6: store events AMTs in the blockstore for (msg_events, events_root) in events.iter().zip(events_roots.iter()) { if let Some(event_root) = events_root { // Store the events AMT - the root CID should match the one computed by FVM From 9ac05d3c53f9406ec027d180a08e3aef93c1c066 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 12 Mar 2026 18:26:26 +0530 Subject: [PATCH 2/7] address comments --- src/rpc/methods/eth/trace.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/rpc/methods/eth/trace.rs b/src/rpc/methods/eth/trace.rs index 4080d36c317c..afd7855ab619 100644 --- a/src/rpc/methods/eth/trace.rs +++ b/src/rpc/methods/eth/trace.rs @@ -676,15 +676,13 @@ fn trace_evm_private( } /// Returns the effective nonce for an actor: EVM nonce for EVM actors, sequence otherwise. -fn actor_nonce(store: &DB, actor: &ActorState) -> EthUint64 { +fn actor_nonce(store: &DB, actor: &ActorState) -> anyhow::Result { if is_evm_actor(&actor.code) { - EthUint64::from( - evm::State::load(store, actor.code, actor.state) - .map(|s| s.nonce()) - .unwrap_or(actor.sequence), - ) + let evm_state = evm::State::load(store, actor.code, actor.state) + .context("failed to load EVM state for nonce")?; + Ok(EthUint64::from(evm_state.nonce())) } else { - EthUint64::from(actor.sequence) + Ok(EthUint64::from(actor.sequence)) } } @@ -745,8 +743,8 @@ fn build_account_diff( diff.balance = Delta::from_comparison(pre_balance, post_balance); // Compare nonce - let pre_nonce = pre_actor.map(|a| actor_nonce(store, a)); - let post_nonce = post_actor.map(|a| actor_nonce(store, a)); + let pre_nonce = pre_actor.map(|a| actor_nonce(store, a)).transpose()?; + let post_nonce = post_actor.map(|a| actor_nonce(store, a)).transpose()?; diff.nonce = Delta::from_comparison(pre_nonce, post_nonce); // Compare code (bytecode for EVM actors) @@ -1480,7 +1478,7 @@ mod tests { fn test_actor_nonce_non_evm() { let store = MemoryDB::default(); let actor = create_test_actor(1000, 42); - let nonce = actor_nonce(&store, &actor); + let nonce = actor_nonce(&store, &actor).unwrap(); assert_eq!(nonce.0, 42); } @@ -1488,7 +1486,7 @@ mod tests { fn test_actor_nonce_evm() { let store = Arc::new(MemoryDB::default()); if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 7, Some(&[0x60])) { - let nonce = actor_nonce(store.as_ref(), &actor); + let nonce = actor_nonce(store.as_ref(), &actor).unwrap(); // EVM actors use the EVM nonce field, not the actor sequence assert_eq!(nonce.0, 7); } From f5811d238690faab536230c791ff57db29924ab3 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 13 Mar 2026 18:20:38 +0530 Subject: [PATCH 3/7] refactor and address commentsclear --- src/rpc/methods/eth.rs | 35 +- src/rpc/methods/eth/trace/mod.rs | 9 + src/rpc/methods/eth/trace/parity.rs | 663 ++++++++++++++++ .../eth/{trace.rs => trace/state_diff.rs} | 726 +----------------- src/rpc/methods/eth/trace/types.rs | 424 ++++++++++ src/rpc/methods/eth/types.rs | 411 ---------- src/rpc/methods/eth/utils.rs | 37 +- src/state_manager/mod.rs | 6 + .../subcommands/api_cmd/api_compare_tests.rs | 3 +- 9 files changed, 1183 insertions(+), 1131 deletions(-) create mode 100644 src/rpc/methods/eth/trace/mod.rs create mode 100644 src/rpc/methods/eth/trace/parity.rs rename src/rpc/methods/eth/{trace.rs => trace/state_diff.rs} (52%) create mode 100644 src/rpc/methods/eth/trace/types.rs diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 5bfe7eb07b31..50769164474c 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -7,13 +7,14 @@ pub mod filter; pub mod pubsub; pub(crate) mod pubsub_trait; mod tipset_resolver; -mod trace; +pub(crate) mod trace; pub mod types; mod utils; pub use tipset_resolver::TipsetResolver; use self::eth_tx::*; use self::filter::hex_str_to_epoch; +use self::trace::types::*; use self::types::*; use super::gas; use crate::blocks::{Tipset, TipsetKey}; @@ -33,7 +34,6 @@ use crate::rpc::{ eth::{ errors::EthErrors, filter::{SkipEvent, event::EventFilter, mempool::MempoolFilter, tipset::TipSetFilter}, - types::EthBlockTrace, utils::decode_revert_reason, }, methods::chain::ChainGetTipSetV2, @@ -3365,37 +3365,12 @@ impl RpcMethod<1> for EthTraceBlock { } } -struct TipsetTraceEntry { - tx_hash: EthHash, - msg_position: i64, - invoc_result: ApiInvocResult, -} - -impl TipsetTraceEntry { - /// Builds Parity-style traces for this entry using the given state tree. - fn build_parity_traces( - &self, - state: &StateTree, - ) -> Result, ServerError> { - let mut env = trace::base_environment(state, &self.invoc_result.msg.from).map_err(|e| { - format!( - "when processing message {}: {}", - self.invoc_result.msg_cid, e - ) - })?; - if let Some(ref execution_trace) = self.invoc_result.execution_trace { - trace::build_traces(&mut env, &[], execution_trace.clone())?; - } - Ok(env.traces) - } -} - -/// Replays a tipset and resolves every non-system transaction into a [`TipsetTraceEntry`]. +/// Replays a tipset and resolves every non-system transaction into a [`trace::TipsetTraceEntry`]. async fn execute_tipset_traces( ctx: &Ctx, ts: &Tipset, ext: &http::Extensions, -) -> Result<(StateTree, Vec), ServerError> +) -> Result<(StateTree, Vec), ServerError> where DB: Blockstore + Send + Sync + 'static, { @@ -3419,7 +3394,7 @@ where let tx_hash = EthGetTransactionHashByCid::handle(ctx.clone(), (ir.msg_cid,), ext).await?; let tx_hash = tx_hash .with_context(|| format!("cannot find transaction hash for cid {}", ir.msg_cid))?; - entries.push(TipsetTraceEntry { + entries.push(trace::TipsetTraceEntry { tx_hash, msg_position: msg_idx, invoc_result: ir, diff --git a/src/rpc/methods/eth/trace/mod.rs b/src/rpc/methods/eth/trace/mod.rs new file mode 100644 index 000000000000..501d18fb758d --- /dev/null +++ b/src/rpc/methods/eth/trace/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +mod parity; +mod state_diff; +pub(crate) mod types; + +pub(super) use parity::*; +pub(crate) use state_diff::build_state_diff; diff --git a/src/rpc/methods/eth/trace/parity.rs b/src/rpc/methods/eth/trace/parity.rs new file mode 100644 index 000000000000..cd8f5c8a7873 --- /dev/null +++ b/src/rpc/methods/eth/trace/parity.rs @@ -0,0 +1,663 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::super::types::{EthAddress, EthBytes, EthHash}; +use super::super::utils::{decode_params, decode_return}; +use super::super::{ + decode_payload, encode_filecoin_params_as_abi, encode_filecoin_returns_as_abi, + lookup_eth_address, +}; +use super::types::{ + EthCallTraceAction, EthCallTraceResult, EthCreateTraceAction, EthCreateTraceResult, EthTrace, + TraceAction, TraceResult, +}; +use crate::eth::{EAMMethod, EVMMethod}; +use crate::rpc::methods::state::ExecutionTrace; +use crate::rpc::state::ActorTrace; +use crate::shim::fvm_shared_latest::METHOD_CONSTRUCTOR; +use crate::shim::{actors::is_evm_actor, address::Address, error::ExitCode, state_tree::StateTree}; +use anyhow::{Context, bail}; +use fil_actor_eam_state::v12 as eam12; +use fil_actor_evm_state::v15 as evm12; +use fil_actor_init_state::v12::ExecReturn; +use fil_actor_init_state::v15::Method as InitMethod; +use fvm_ipld_blockstore::Blockstore; +use num::FromPrimitive; +use tracing::debug; + +/// Error string used in Parity-format traces. +const PARITY_TRACE_REVERT_ERROR: &str = "Reverted"; +const PARITY_EVM_INVALID_INSTRUCTION: &str = "invalid instruction"; +const PARITY_EVM_UNDEFINED_INSTRUCTION: &str = "undefined instruction"; +const PARITY_EVM_STACK_UNDERFLOW: &str = "stack underflow"; +const PARITY_EVM_STACK_OVERFLOW: &str = "stack overflow"; +const PARITY_EVM_ILLEGAL_MEMORY_ACCESS: &str = "illegal memory access"; +const PARITY_EVM_BAD_JUMPDEST: &str = "invalid jump destination"; +const PARITY_EVM_SELFDESTRUCT_FAILED: &str = "self destruct failed"; +const PARITY_EVM_OUT_OF_GAS: &str = "out of gas"; + +#[derive(Default)] +pub struct Environment { + caller: EthAddress, + is_evm: bool, + subtrace_count: i64, + pub traces: Vec, + last_byte_code: Option, +} + +pub fn base_environment( + state: &StateTree, + from: &Address, +) -> anyhow::Result { + let sender = lookup_eth_address(from, state)? + .with_context(|| format!("top-level message sender {from} s could not be found"))?; + Ok(Environment { + caller: sender, + ..Environment::default() + }) +} + +fn trace_to_address(trace: &ActorTrace) -> EthAddress { + if let Some(addr) = trace.state.delegated_address + && let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr.into()) + { + return eth_addr; + } + EthAddress::from_actor_id(trace.id) +} + +/// Returns true if the trace is a call to an EVM or EAM actor. +fn trace_is_evm_or_eam(trace: &ExecutionTrace) -> bool { + if let Some(invoked_actor) = &trace.invoked_actor { + is_evm_actor(&invoked_actor.state.code) + || invoked_actor.id != Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap() + } else { + false + } +} + +/// Returns true if the trace is a call to an EVM or EAM actor. +fn trace_err_msg(trace: &ExecutionTrace) -> Option { + let code = trace.msg_rct.exit_code; + + if code.is_success() { + return None; + } + + // EVM tools often expect this literal string. + if code == ExitCode::SYS_OUT_OF_GAS { + return Some(PARITY_EVM_OUT_OF_GAS.into()); + } + + // indicate when we have a "system" error. + if code < ExitCode::FIRST_ACTOR_ERROR_CODE.into() { + return Some(format!("vm error: {code}")); + } + + // handle special exit codes from the EVM/EAM. + if trace_is_evm_or_eam(trace) { + match code.into() { + evm12::EVM_CONTRACT_REVERTED => return Some(PARITY_TRACE_REVERT_ERROR.into()), // capitalized for compatibility + evm12::EVM_CONTRACT_INVALID_INSTRUCTION => { + return Some(PARITY_EVM_INVALID_INSTRUCTION.into()); + } + evm12::EVM_CONTRACT_UNDEFINED_INSTRUCTION => { + return Some(PARITY_EVM_UNDEFINED_INSTRUCTION.into()); + } + evm12::EVM_CONTRACT_STACK_UNDERFLOW => return Some(PARITY_EVM_STACK_UNDERFLOW.into()), + evm12::EVM_CONTRACT_STACK_OVERFLOW => return Some(PARITY_EVM_STACK_OVERFLOW.into()), + evm12::EVM_CONTRACT_ILLEGAL_MEMORY_ACCESS => { + return Some(PARITY_EVM_ILLEGAL_MEMORY_ACCESS.into()); + } + evm12::EVM_CONTRACT_BAD_JUMPDEST => return Some(PARITY_EVM_BAD_JUMPDEST.into()), + evm12::EVM_CONTRACT_SELFDESTRUCT_FAILED => { + return Some(PARITY_EVM_SELFDESTRUCT_FAILED.into()); + } + _ => (), + } + } + // everything else... + Some(format!("actor error: {code}")) +} + +/// Recursively builds the traces for a given ExecutionTrace by walking the subcalls +pub fn build_traces( + env: &mut Environment, + address: &[i64], + trace: ExecutionTrace, +) -> anyhow::Result<()> { + let (trace, recurse_into) = build_trace(env, address, trace)?; + + let last_trace_idx = if let Some(trace) = trace { + let len = env.traces.len(); + env.traces.push(trace); + env.subtrace_count += 1; + Some(len) + } else { + None + }; + + // Skip if there's nothing more to do and/or `build_trace` told us to skip this one. + let (recurse_into, invoked_actor) = if let Some(trace) = recurse_into { + if let Some(invoked_actor) = &trace.invoked_actor { + let invoked_actor = invoked_actor.clone(); + (trace, invoked_actor) + } else { + return Ok(()); + } + } else { + return Ok(()); + }; + + let mut sub_env = Environment { + caller: trace_to_address(&invoked_actor), + is_evm: is_evm_actor(&invoked_actor.state.code), + traces: env.traces.clone(), + ..Environment::default() + }; + for subcall in recurse_into.subcalls.into_iter() { + let mut new_address = address.to_vec(); + new_address.push(sub_env.subtrace_count); + build_traces(&mut sub_env, &new_address, subcall)?; + } + env.traces = sub_env.traces; + if let Some(idx) = last_trace_idx { + env.traces.get_mut(idx).expect("Infallible").subtraces = sub_env.subtrace_count; + } + + Ok(()) +} + +// `build_trace` processes the passed execution trace and updates the environment, if necessary. +// +// On success, it returns a trace to add (or `None` to skip) and the trace to recurse into (or `None` to skip). +fn build_trace( + env: &mut Environment, + address: &[i64], + trace: ExecutionTrace, +) -> anyhow::Result<(Option, Option)> { + // This function first assumes that the call is a "native" call, then handles all the "not + // native" cases. If we get any unexpected results in any of these special cases, we just + // keep the "native" interpretation and move on. + // + // 1. If we're invoking a contract (even if the caller is a native account/actor), we + // attempt to decode the params/return value as a contract invocation. + // 2. If we're calling the EAM and/or init actor, we try to treat the call as a CREATE. + // 3. Finally, if the caller is an EVM smart contract and it's calling a "private" (1-1023) + // method, we know something special is going on. We look for calls related to + // DELEGATECALL and drop everything else (everything else includes calls triggered by, + // e.g., EXTCODEHASH). + + // If we don't have sufficient funds, or we have a fatal error, or we have some + // other syscall error: skip the entire trace to mimic Ethereum (Ethereum records + // traces _after_ checking things like this). + // + // NOTE: The FFI currently folds all unknown syscall errors into "sys assertion + // failed" which is turned into SysErrFatal. + if !address.is_empty() + && Into::::into(trace.msg_rct.exit_code) == ExitCode::SYS_INSUFFICIENT_FUNDS + { + return Ok((None, None)); + } + + // We may fail before we can even invoke the actor. In that case, we have no 100% reliable + // way of getting its address (e.g., due to reverts) so we're just going to drop the entire + // trace. This is OK (ish) because the call never really "happened". + if trace.invoked_actor.is_none() { + return Ok((None, None)); + } + + // Step 2: Decode as a contract invocation + // + // Normal EVM calls. We don't care if the caller/receiver are actually EVM actors, we only + // care if the call _looks_ like an EVM call. If we fail to decode it as an EVM call, we + // fallback on interpreting it as a native call. + let method = EVMMethod::from_u64(trace.msg.method); + if let Some(EVMMethod::InvokeContract) = method { + let (trace, exec_trace) = trace_evm_call(env, address, trace)?; + return Ok((Some(trace), Some(exec_trace))); + } + + // Step 3: Decode as a contract deployment + match trace.msg.to { + Address::INIT_ACTOR => { + let method = InitMethod::from_u64(trace.msg.method); + match method { + Some(InitMethod::Exec) | Some(InitMethod::Exec4) => { + return trace_native_create(env, address, &trace); + } + _ => (), + } + } + Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR => { + let method = EAMMethod::from_u64(trace.msg.method); + match method { + Some(EAMMethod::Create) + | Some(EAMMethod::Create2) + | Some(EAMMethod::CreateExternal) => { + return trace_eth_create(env, address, &trace); + } + _ => (), + } + } + _ => (), + } + + // Step 4: Handle DELEGATECALL + // + // EVM contracts cannot call methods in the range 1-1023, only the EVM itself can. So, if we + // see a call in this range, we know it's an implementation detail of the EVM and not an + // explicit call by the user. + // + // While the EVM calls several methods in this range (some we've already handled above with + // respect to the EAM), we only care about the ones relevant DELEGATECALL and can _ignore_ + // all the others. + if env.is_evm && trace.msg.method > 0 && trace.msg.method < 1024 { + return trace_evm_private(env, address, &trace); + } + + Ok((Some(trace_native_call(env, address, &trace)?), Some(trace))) +} + +// Build an EthTrace for a "call" with the given input & output. +fn trace_call( + env: &mut Environment, + address: &[i64], + trace: &ExecutionTrace, + input: EthBytes, + output: EthBytes, +) -> anyhow::Result { + if let Some(invoked_actor) = &trace.invoked_actor { + let to = trace_to_address(invoked_actor); + let call_type: String = if trace.msg.read_only.unwrap_or_default() { + "staticcall" + } else { + "call" + } + .into(); + + Ok(EthTrace { + r#type: "call".into(), + action: TraceAction::Call(EthCallTraceAction { + call_type, + from: env.caller, + to: Some(to), + gas: trace.msg.gas_limit.unwrap_or_default().into(), + value: trace.msg.value.clone().into(), + input, + }), + result: TraceResult::Call(EthCallTraceResult { + gas_used: trace.sum_gas().total_gas.into(), + output, + }), + trace_address: Vec::from(address), + error: trace_err_msg(trace), + ..EthTrace::default() + }) + } else { + bail!("no invoked actor") + } +} + +// Build an EthTrace for a "call", parsing the inputs & outputs as a "native" FVM call. +fn trace_native_call( + env: &mut Environment, + address: &[i64], + trace: &ExecutionTrace, +) -> anyhow::Result { + trace_call( + env, + address, + trace, + encode_filecoin_params_as_abi(trace.msg.method, trace.msg.params_codec, &trace.msg.params)?, + EthBytes(encode_filecoin_returns_as_abi( + trace.msg_rct.exit_code.value().into(), + trace.msg_rct.return_codec, + &trace.msg_rct.r#return, + )), + ) +} + +// Build an EthTrace for a "call", parsing the inputs & outputs as an EVM call (falling back on +// treating it as a native call). +fn trace_evm_call( + env: &mut Environment, + address: &[i64], + trace: ExecutionTrace, +) -> anyhow::Result<(EthTrace, ExecutionTrace)> { + let input = match decode_payload(&trace.msg.params, trace.msg.params_codec) { + Ok(value) => value, + Err(err) => { + debug!("failed to decode contract invocation payload: {err}"); + return Ok((trace_native_call(env, address, &trace)?, trace)); + } + }; + let output = match decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec) { + Ok(value) => value, + Err(err) => { + debug!("failed to decode contract invocation return: {err}"); + return Ok((trace_native_call(env, address, &trace)?, trace)); + } + }; + Ok((trace_call(env, address, &trace, input, output)?, trace)) +} + +// Build an EthTrace for a native "create" operation. This should only be called with an +// ExecutionTrace is an Exec or Exec4 method invocation on the Init actor. + +fn trace_native_create( + env: &mut Environment, + address: &[i64], + trace: &ExecutionTrace, +) -> anyhow::Result<(Option, Option)> { + if trace.msg.read_only.unwrap_or_default() { + // "create" isn't valid in a staticcall, so we just skip this trace + // (couldn't have created an actor anyways). + // This mimic's the EVM: it doesn't trace CREATE calls when in + // read-only mode. + return Ok((None, None)); + } + + let sub_trace = trace + .subcalls + .iter() + .find(|c| c.msg.method == METHOD_CONSTRUCTOR); + + let sub_trace = if let Some(sub_trace) = sub_trace { + sub_trace + } else { + // If we succeed in calling Exec/Exec4 but don't even try to construct + // something, we have a bug in our tracing logic or a mismatch between our + // tracing logic and the actors. + if trace.msg_rct.exit_code.is_success() { + bail!("successful Exec/Exec4 call failed to call a constructor"); + } + // Otherwise, this can happen if creation fails early (bad params, + // out of gas, contract already exists, etc.). The EVM wouldn't + // trace such cases, so we don't either. + // + // NOTE: It's actually impossible to run out of gas before calling + // initcode in the EVM (without running out of gas in the calling + // contract), but this is an equivalent edge-case to InvokedActor + // being nil, so we treat it the same way and skip the entire + // operation. + return Ok((None, None)); + }; + + // Native actors that aren't the EAM can attempt to call Exec4, but such + // call should fail immediately without ever attempting to construct an + // actor. I'm catching this here because it likely means that there's a bug + // in our trace-conversion logic. + if trace.msg.method == (InitMethod::Exec4 as u64) { + bail!("direct call to Exec4 successfully called a constructor!"); + } + + let mut output = EthBytes::default(); + let mut create_addr = EthAddress::default(); + if trace.msg_rct.exit_code.is_success() { + // We're supposed to put the "installed bytecode" here. But this + // isn't an EVM actor, so we just put some invalid bytecode (this is + // the answer you'd get if you called EXTCODECOPY on a native + // non-account actor, anyways). + output = EthBytes(vec![0xFE]); + + // Extract the address of the created actor from the return value. + let init_return: ExecReturn = decode_return(&trace.msg_rct)?; + let actor_id = init_return.id_address.id()?; + let eth_addr = EthAddress::from_actor_id(actor_id); + create_addr = eth_addr; + } + + Ok(( + Some(EthTrace { + r#type: "create".into(), + action: TraceAction::Create(EthCreateTraceAction { + from: env.caller, + gas: trace.msg.gas_limit.unwrap_or_default().into(), + value: trace.msg.value.clone().into(), + // If we get here, this isn't a native EVM create. Those always go through + // the EAM. So we have no "real" initcode and must use the sentinel value + // for "invalid" initcode. + init: EthBytes(vec![0xFE]), + }), + result: TraceResult::Create(EthCreateTraceResult { + gas_used: trace.sum_gas().total_gas.into(), + address: Some(create_addr), + code: output, + }), + trace_address: Vec::from(address), + error: trace_err_msg(trace), + ..EthTrace::default() + }), + Some(sub_trace.clone()), + )) +} + +// Decode the parameters and return value of an EVM smart contract creation through the EAM. This +// should only be called with an ExecutionTrace for a Create, Create2, or CreateExternal method +// invocation on the EAM. +fn decode_create_via_eam(trace: &ExecutionTrace) -> anyhow::Result<(Vec, EthAddress)> { + let init_code = match EAMMethod::from_u64(trace.msg.method) { + Some(EAMMethod::Create) => { + let params = decode_params::(&trace.msg)?; + params.initcode + } + Some(EAMMethod::Create2) => { + let params = decode_params::(&trace.msg)?; + params.initcode + } + Some(EAMMethod::CreateExternal) => { + decode_payload(&trace.msg.params, trace.msg.params_codec)?.into() + } + _ => bail!("unexpected CREATE method {}", trace.msg.method), + }; + let ret = decode_return::(&trace.msg_rct)?; + + Ok((init_code, ret.eth_address.0.into())) +} + +// Build an EthTrace for an EVM "create" operation. This should only be called with an +// ExecutionTrace for a Create, Create2, or CreateExternal method invocation on the EAM. +fn trace_eth_create( + env: &mut Environment, + address: &[i64], + trace: &ExecutionTrace, +) -> anyhow::Result<(Option, Option)> { + // Same as the Init actor case above, see the comment there. + if trace.msg.read_only.unwrap_or_default() { + return Ok((None, None)); + } + + // Look for a call to either a constructor or the EVM's resurrect method. + let sub_trace = trace + .subcalls + .iter() + .filter_map(|et| { + if et.msg.to == Address::INIT_ACTOR { + et.subcalls + .iter() + .find(|et| et.msg.method == METHOD_CONSTRUCTOR) + } else { + match EVMMethod::from_u64(et.msg.method) { + Some(EVMMethod::Resurrect) => Some(et), + _ => None, + } + } + }) + .next(); + + // Same as the Init actor case above, see the comment there. + let sub_trace = if let Some(sub_trace) = sub_trace { + sub_trace + } else { + if trace.msg_rct.exit_code.is_success() { + bail!("successful Create/Create2 call failed to call a constructor"); + } + return Ok((None, None)); + }; + + // Decode inputs & determine create type. + let (init_code, create_addr) = decode_create_via_eam(trace)?; + + // Handle the output. + let output = match trace.msg_rct.exit_code.value() { + 0 => { + // success + // We're _supposed_ to include the contracts bytecode here, but we + // can't do that reliably (e.g., if some part of the trace reverts). + // So we don't try and include a sentinel "impossible bytecode" + // value (the value specified by EIP-3541). + EthBytes(vec![0xFE]) + } + 33 => { + // Reverted, parse the revert message. + // If we managed to call the constructor, parse/return its revert message. If we + // fail, we just return no output. + decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec)? + } + _ => EthBytes::default(), + }; + + Ok(( + Some(EthTrace { + r#type: "create".into(), + action: TraceAction::Create(EthCreateTraceAction { + from: env.caller, + gas: trace.msg.gas_limit.unwrap_or_default().into(), + value: trace.msg.value.clone().into(), + init: init_code.into(), + }), + result: TraceResult::Create(EthCreateTraceResult { + gas_used: trace.sum_gas().total_gas.into(), + address: Some(create_addr), + code: output, + }), + trace_address: Vec::from(address), + error: trace_err_msg(trace), + ..EthTrace::default() + }), + Some(sub_trace.clone()), + )) +} + +// Build an EthTrace for a "private" method invocation from the EVM. This should only be called with +// an ExecutionTrace from an EVM instance and on a method between 1 and 1023 inclusive. +fn trace_evm_private( + env: &mut Environment, + address: &[i64], + trace: &ExecutionTrace, +) -> anyhow::Result<(Option, Option)> { + // The EVM actor implements DELEGATECALL by: + // + // 1. Asking the callee for its bytecode by calling it on the GetBytecode method. + // 2. Recursively invoking the currently executing contract on the + // InvokeContractDelegate method. + // + // The code below "reconstructs" that delegate call by: + // + // 1. Remembering the last contract on which we called GetBytecode. + // 2. Treating the contract invoked in step 1 as the DELEGATECALL receiver. + // + // Note, however: GetBytecode will be called, e.g., if the user invokes the + // EXTCODECOPY instruction. It's not an error to see multiple GetBytecode calls + // before we see an InvokeContractDelegate. + match EVMMethod::from_u64(trace.msg.method) { + Some(EVMMethod::GetBytecode) => { + // NOTE: I'm not checking anything about the receiver here. The EVM won't + // DELEGATECALL any non-EVM actor, but there's no need to encode that fact + // here in case we decide to loosen this up in the future. + env.last_byte_code = None; + if trace.msg_rct.exit_code.is_success() + && let Option::Some(actor_trace) = &trace.invoked_actor + { + let to = trace_to_address(actor_trace); + env.last_byte_code = Some(to); + } + Ok((None, None)) + } + Some(EVMMethod::InvokeContractDelegate) => { + // NOTE: We return errors in all the failure cases below instead of trying + // to continue because the caller is an EVM actor. If something goes wrong + // here, there's a bug in our EVM implementation. + + // Handle delegate calls + // + // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, + // method 6. + // 2) Check that the previous trace calls another actor on method 3 + // (GetByteCode) and they are at the same level (same parent) + // 3) Treat this as a delegate call to actor A. + if env.last_byte_code.is_none() { + bail!("unknown bytecode for delegate call"); + } + + if let Option::Some(actor_trace) = &trace.invoked_actor { + let to = trace_to_address(actor_trace); + if env.caller != to { + bail!( + "delegate-call not from address to self: {:?} != {:?}", + env.caller, + to + ); + } + } + + let dp = decode_params::(&trace.msg)?; + + let output = decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec) + .map_err(|e| anyhow::anyhow!("failed to decode delegate-call return: {}", e))?; + + Ok(( + Some(EthTrace { + r#type: "call".into(), + action: TraceAction::Call(EthCallTraceAction { + call_type: "delegatecall".into(), + from: env.caller, + to: env.last_byte_code, + gas: trace.msg.gas_limit.unwrap_or_default().into(), + value: trace.msg.value.clone().into(), + input: dp.input.into(), + }), + result: TraceResult::Call(EthCallTraceResult { + gas_used: trace.sum_gas().total_gas.into(), + output, + }), + trace_address: Vec::from(address), + error: trace_err_msg(trace), + ..EthTrace::default() + }), + Some(trace.clone()), + )) + } + _ => { + // We drop all other "private" calls from FEVM. We _forbid_ explicit calls between 0 and + // 1024 (exclusive), so any calls in this range must be implementation details. + Ok((None, None)) + } + } +} + +pub(in crate::rpc::methods::eth) struct TipsetTraceEntry { + pub tx_hash: EthHash, + pub msg_position: i64, + pub invoc_result: crate::rpc::state::ApiInvocResult, +} + +impl TipsetTraceEntry { + /// Builds Parity-style traces for this entry using the given state tree. + pub fn build_parity_traces( + &self, + state: &StateTree, + ) -> Result, crate::rpc::error::ServerError> { + let mut env = base_environment(state, &self.invoc_result.msg.from).map_err(|e| { + format!( + "when processing message {}: {}", + self.invoc_result.msg_cid, e + ) + })?; + if let Some(ref execution_trace) = self.invoc_result.execution_trace { + build_traces(&mut env, &[], execution_trace.clone())?; + } + Ok(env.traces) + } +} diff --git a/src/rpc/methods/eth/trace.rs b/src/rpc/methods/eth/trace/state_diff.rs similarity index 52% rename from src/rpc/methods/eth/trace.rs rename to src/rpc/methods/eth/trace/state_diff.rs index afd7855ab619..704ca0431e73 100644 --- a/src/rpc/methods/eth/trace.rs +++ b/src/rpc/methods/eth/trace/state_diff.rs @@ -1,50 +1,20 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use super::types::{ - EthAddress, EthBytes, EthCallTraceAction, EthHash, EthTrace, TraceAction, TraceResult, -}; -use super::utils::{decode_params, decode_return}; -use super::{ - EthCallTraceResult, EthCreateTraceAction, EthCreateTraceResult, decode_payload, - encode_filecoin_params_as_abi, encode_filecoin_returns_as_abi, -}; -use crate::eth::{EAMMethod, EVMMethod}; -use crate::rpc::eth::types::{AccountDiff, Delta, StateDiff}; -use crate::rpc::eth::{EthBigInt, EthUint64}; -use crate::rpc::methods::eth::lookup_eth_address; -use crate::rpc::methods::state::ExecutionTrace; -use crate::rpc::state::ActorTrace; -use crate::shim::actors::{EVMActorStateLoad, evm}; -use crate::shim::fvm_shared_latest::METHOD_CONSTRUCTOR; -use crate::shim::state_tree::ActorState; -use crate::shim::{actors::is_evm_actor, address::Address, error::ExitCode, state_tree::StateTree}; +use super::super::types::{EthAddress, EthHash}; +use super::super::utils::ActorStateEthExt as _; +use super::types::{AccountDiff, ChangedType, Delta, StateDiff}; +use crate::rpc::eth::EthBigInt; +use crate::shim::actors::{EVMActorStateLoad as _, evm, is_evm_actor}; +use crate::shim::state_tree::{ActorState, StateTree}; use ahash::{HashMap, HashSet}; -use anyhow::{Context, bail}; -use fil_actor_eam_state::v12 as eam12; use fil_actor_evm_state::evm_shared::v17::uints::U256; -use fil_actor_evm_state::v15 as evm12; -use fil_actor_init_state::v12::ExecReturn; -use fil_actor_init_state::v15::Method as InitMethod; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_kamt::{AsHashedKey, Config as KamtConfig, HashedKey, Kamt}; -use num::FromPrimitive; use std::borrow::Cow; use std::collections::BTreeMap; use tracing::debug; -/// Error string used in Parity-format traces. -pub(crate) const PARITY_TRACE_REVERT_ERROR: &str = "Reverted"; - -const PARITY_EVM_INVALID_INSTRUCTION: &str = "invalid instruction"; -const PARITY_EVM_UNDEFINED_INSTRUCTION: &str = "undefined instruction"; -const PARITY_EVM_STACK_UNDERFLOW: &str = "stack underflow"; -const PARITY_EVM_STACK_OVERFLOW: &str = "stack overflow"; -const PARITY_EVM_ILLEGAL_MEMORY_ACCESS: &str = "illegal memory access"; -const PARITY_EVM_BAD_JUMPDEST: &str = "invalid jump destination"; -const PARITY_EVM_SELFDESTRUCT_FAILED: &str = "self destruct failed"; -const PARITY_EVM_OUT_OF_GAS: &str = "out of gas"; - /// KAMT configuration matching the EVM actor in builtin-actors. // Code is taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L47 fn evm_kamt_config() -> KamtConfig { @@ -57,7 +27,7 @@ fn evm_kamt_config() -> KamtConfig { /// Hash algorithm for EVM storage KAMT. // Code taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L49. -pub struct EvmStateHashAlgorithm; +struct EvmStateHashAlgorithm; impl AsHashedKey for EvmStateHashAlgorithm { fn as_hashed_key(key: &U256) -> Cow<'_, HashedKey<32>> { @@ -74,631 +44,6 @@ fn u256_to_eth_hash(value: &U256) -> EthHash { const ZERO_HASH: EthHash = EthHash(ethereum_types::H256([0u8; 32])); -#[derive(Default)] -pub struct Environment { - caller: EthAddress, - is_evm: bool, - subtrace_count: i64, - pub traces: Vec, - last_byte_code: Option, -} - -pub fn base_environment( - state: &StateTree, - from: &Address, -) -> anyhow::Result { - let sender = lookup_eth_address(from, state)? - .with_context(|| format!("top-level message sender {from} s could not be found"))?; - Ok(Environment { - caller: sender, - ..Environment::default() - }) -} - -fn trace_to_address(trace: &ActorTrace) -> EthAddress { - if let Some(addr) = trace.state.delegated_address - && let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr.into()) - { - return eth_addr; - } - EthAddress::from_actor_id(trace.id) -} - -/// Returns true if the trace is a call to an EVM or EAM actor. -fn trace_is_evm_or_eam(trace: &ExecutionTrace) -> bool { - if let Some(invoked_actor) = &trace.invoked_actor { - is_evm_actor(&invoked_actor.state.code) - || invoked_actor.id != Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap() - } else { - false - } -} - -/// Returns true if the trace is a call to an EVM or EAM actor. -fn trace_err_msg(trace: &ExecutionTrace) -> Option { - let code = trace.msg_rct.exit_code; - - if code.is_success() { - return None; - } - - // EVM tools often expect this literal string. - if code == ExitCode::SYS_OUT_OF_GAS { - return Some(PARITY_EVM_OUT_OF_GAS.into()); - } - - // indicate when we have a "system" error. - if code < ExitCode::FIRST_ACTOR_ERROR_CODE.into() { - return Some(format!("vm error: {code}")); - } - - // handle special exit codes from the EVM/EAM. - if trace_is_evm_or_eam(trace) { - match code.into() { - evm12::EVM_CONTRACT_REVERTED => return Some(PARITY_TRACE_REVERT_ERROR.into()), // capitalized for compatibility - evm12::EVM_CONTRACT_INVALID_INSTRUCTION => { - return Some(PARITY_EVM_INVALID_INSTRUCTION.into()); - } - evm12::EVM_CONTRACT_UNDEFINED_INSTRUCTION => { - return Some(PARITY_EVM_UNDEFINED_INSTRUCTION.into()); - } - evm12::EVM_CONTRACT_STACK_UNDERFLOW => return Some(PARITY_EVM_STACK_UNDERFLOW.into()), - evm12::EVM_CONTRACT_STACK_OVERFLOW => return Some(PARITY_EVM_STACK_OVERFLOW.into()), - evm12::EVM_CONTRACT_ILLEGAL_MEMORY_ACCESS => { - return Some(PARITY_EVM_ILLEGAL_MEMORY_ACCESS.into()); - } - evm12::EVM_CONTRACT_BAD_JUMPDEST => return Some(PARITY_EVM_BAD_JUMPDEST.into()), - evm12::EVM_CONTRACT_SELFDESTRUCT_FAILED => { - return Some(PARITY_EVM_SELFDESTRUCT_FAILED.into()); - } - _ => (), - } - } - // everything else... - Some(format!("actor error: {code}")) -} - -/// Recursively builds the traces for a given ExecutionTrace by walking the subcalls -pub fn build_traces( - env: &mut Environment, - address: &[i64], - trace: ExecutionTrace, -) -> anyhow::Result<()> { - let (trace, recurse_into) = build_trace(env, address, trace)?; - - let last_trace_idx = if let Some(trace) = trace { - let len = env.traces.len(); - env.traces.push(trace); - env.subtrace_count += 1; - Some(len) - } else { - None - }; - - // Skip if there's nothing more to do and/or `build_trace` told us to skip this one. - let (recurse_into, invoked_actor) = if let Some(trace) = recurse_into { - if let Some(invoked_actor) = &trace.invoked_actor { - let invoked_actor = invoked_actor.clone(); - (trace, invoked_actor) - } else { - return Ok(()); - } - } else { - return Ok(()); - }; - - let mut sub_env = Environment { - caller: trace_to_address(&invoked_actor), - is_evm: is_evm_actor(&invoked_actor.state.code), - traces: env.traces.clone(), - ..Environment::default() - }; - for subcall in recurse_into.subcalls.into_iter() { - let mut new_address = address.to_vec(); - new_address.push(sub_env.subtrace_count); - build_traces(&mut sub_env, &new_address, subcall)?; - } - env.traces = sub_env.traces; - if let Some(idx) = last_trace_idx { - env.traces.get_mut(idx).expect("Infallible").subtraces = sub_env.subtrace_count; - } - - Ok(()) -} - -// `build_trace` processes the passed execution trace and updates the environment, if necessary. -// -// On success, it returns a trace to add (or `None` to skip) and the trace to recurse into (or `None` to skip). -fn build_trace( - env: &mut Environment, - address: &[i64], - trace: ExecutionTrace, -) -> anyhow::Result<(Option, Option)> { - // This function first assumes that the call is a "native" call, then handles all the "not - // native" cases. If we get any unexpected results in any of these special cases, we just - // keep the "native" interpretation and move on. - // - // 1. If we're invoking a contract (even if the caller is a native account/actor), we - // attempt to decode the params/return value as a contract invocation. - // 2. If we're calling the EAM and/or init actor, we try to treat the call as a CREATE. - // 3. Finally, if the caller is an EVM smart contract and it's calling a "private" (1-1023) - // method, we know something special is going on. We look for calls related to - // DELEGATECALL and drop everything else (everything else includes calls triggered by, - // e.g., EXTCODEHASH). - - // If we don't have sufficient funds, or we have a fatal error, or we have some - // other syscall error: skip the entire trace to mimic Ethereum (Ethereum records - // traces _after_ checking things like this). - // - // NOTE: The FFI currently folds all unknown syscall errors into "sys assertion - // failed" which is turned into SysErrFatal. - if !address.is_empty() - && Into::::into(trace.msg_rct.exit_code) == ExitCode::SYS_INSUFFICIENT_FUNDS - { - return Ok((None, None)); - } - - // We may fail before we can even invoke the actor. In that case, we have no 100% reliable - // way of getting its address (e.g., due to reverts) so we're just going to drop the entire - // trace. This is OK (ish) because the call never really "happened". - if trace.invoked_actor.is_none() { - return Ok((None, None)); - } - - // Step 2: Decode as a contract invocation - // - // Normal EVM calls. We don't care if the caller/receiver are actually EVM actors, we only - // care if the call _looks_ like an EVM call. If we fail to decode it as an EVM call, we - // fallback on interpreting it as a native call. - let method = EVMMethod::from_u64(trace.msg.method); - if let Some(EVMMethod::InvokeContract) = method { - let (trace, exec_trace) = trace_evm_call(env, address, trace)?; - return Ok((Some(trace), Some(exec_trace))); - } - - // Step 3: Decode as a contract deployment - match trace.msg.to { - Address::INIT_ACTOR => { - let method = InitMethod::from_u64(trace.msg.method); - match method { - Some(InitMethod::Exec) | Some(InitMethod::Exec4) => { - return trace_native_create(env, address, &trace); - } - _ => (), - } - } - Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR => { - let method = EAMMethod::from_u64(trace.msg.method); - match method { - Some(EAMMethod::Create) - | Some(EAMMethod::Create2) - | Some(EAMMethod::CreateExternal) => { - return trace_eth_create(env, address, &trace); - } - _ => (), - } - } - _ => (), - } - - // Step 4: Handle DELEGATECALL - // - // EVM contracts cannot call methods in the range 1-1023, only the EVM itself can. So, if we - // see a call in this range, we know it's an implementation detail of the EVM and not an - // explicit call by the user. - // - // While the EVM calls several methods in this range (some we've already handled above with - // respect to the EAM), we only care about the ones relevant DELEGATECALL and can _ignore_ - // all the others. - if env.is_evm && trace.msg.method > 0 && trace.msg.method < 1024 { - return trace_evm_private(env, address, &trace); - } - - Ok((Some(trace_native_call(env, address, &trace)?), Some(trace))) -} - -// Build an EthTrace for a "call" with the given input & output. -fn trace_call( - env: &mut Environment, - address: &[i64], - trace: &ExecutionTrace, - input: EthBytes, - output: EthBytes, -) -> anyhow::Result { - if let Some(invoked_actor) = &trace.invoked_actor { - let to = trace_to_address(invoked_actor); - let call_type: String = if trace.msg.read_only.unwrap_or_default() { - "staticcall" - } else { - "call" - } - .into(); - - Ok(EthTrace { - r#type: "call".into(), - action: TraceAction::Call(EthCallTraceAction { - call_type, - from: env.caller, - to: Some(to), - gas: trace.msg.gas_limit.unwrap_or_default().into(), - value: trace.msg.value.clone().into(), - input, - }), - result: TraceResult::Call(EthCallTraceResult { - gas_used: trace.sum_gas().total_gas.into(), - output, - }), - trace_address: Vec::from(address), - error: trace_err_msg(trace), - ..EthTrace::default() - }) - } else { - bail!("no invoked actor") - } -} - -// Build an EthTrace for a "call", parsing the inputs & outputs as a "native" FVM call. -fn trace_native_call( - env: &mut Environment, - address: &[i64], - trace: &ExecutionTrace, -) -> anyhow::Result { - trace_call( - env, - address, - trace, - encode_filecoin_params_as_abi(trace.msg.method, trace.msg.params_codec, &trace.msg.params)?, - EthBytes(encode_filecoin_returns_as_abi( - trace.msg_rct.exit_code.value().into(), - trace.msg_rct.return_codec, - &trace.msg_rct.r#return, - )), - ) -} - -// Build an EthTrace for a "call", parsing the inputs & outputs as an EVM call (falling back on -// treating it as a native call). -fn trace_evm_call( - env: &mut Environment, - address: &[i64], - trace: ExecutionTrace, -) -> anyhow::Result<(EthTrace, ExecutionTrace)> { - let input = match decode_payload(&trace.msg.params, trace.msg.params_codec) { - Ok(value) => value, - Err(err) => { - debug!("failed to decode contract invocation payload: {err}"); - return Ok((trace_native_call(env, address, &trace)?, trace)); - } - }; - let output = match decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec) { - Ok(value) => value, - Err(err) => { - debug!("failed to decode contract invocation return: {err}"); - return Ok((trace_native_call(env, address, &trace)?, trace)); - } - }; - Ok((trace_call(env, address, &trace, input, output)?, trace)) -} - -// Build an EthTrace for a native "create" operation. This should only be called with an -// ExecutionTrace is an Exec or Exec4 method invocation on the Init actor. - -fn trace_native_create( - env: &mut Environment, - address: &[i64], - trace: &ExecutionTrace, -) -> anyhow::Result<(Option, Option)> { - if trace.msg.read_only.unwrap_or_default() { - // "create" isn't valid in a staticcall, so we just skip this trace - // (couldn't have created an actor anyways). - // This mimic's the EVM: it doesn't trace CREATE calls when in - // read-only mode. - return Ok((None, None)); - } - - let sub_trace = trace - .subcalls - .iter() - .find(|c| c.msg.method == METHOD_CONSTRUCTOR); - - let sub_trace = if let Some(sub_trace) = sub_trace { - sub_trace - } else { - // If we succeed in calling Exec/Exec4 but don't even try to construct - // something, we have a bug in our tracing logic or a mismatch between our - // tracing logic and the actors. - if trace.msg_rct.exit_code.is_success() { - bail!("successful Exec/Exec4 call failed to call a constructor"); - } - // Otherwise, this can happen if creation fails early (bad params, - // out of gas, contract already exists, etc.). The EVM wouldn't - // trace such cases, so we don't either. - // - // NOTE: It's actually impossible to run out of gas before calling - // initcode in the EVM (without running out of gas in the calling - // contract), but this is an equivalent edge-case to InvokedActor - // being nil, so we treat it the same way and skip the entire - // operation. - return Ok((None, None)); - }; - - // Native actors that aren't the EAM can attempt to call Exec4, but such - // call should fail immediately without ever attempting to construct an - // actor. I'm catching this here because it likely means that there's a bug - // in our trace-conversion logic. - if trace.msg.method == (InitMethod::Exec4 as u64) { - bail!("direct call to Exec4 successfully called a constructor!"); - } - - let mut output = EthBytes::default(); - let mut create_addr = EthAddress::default(); - if trace.msg_rct.exit_code.is_success() { - // We're supposed to put the "installed bytecode" here. But this - // isn't an EVM actor, so we just put some invalid bytecode (this is - // the answer you'd get if you called EXTCODECOPY on a native - // non-account actor, anyways). - output = EthBytes(vec![0xFE]); - - // Extract the address of the created actor from the return value. - let init_return: ExecReturn = decode_return(&trace.msg_rct)?; - let actor_id = init_return.id_address.id()?; - let eth_addr = EthAddress::from_actor_id(actor_id); - create_addr = eth_addr; - } - - Ok(( - Some(EthTrace { - r#type: "create".into(), - action: TraceAction::Create(EthCreateTraceAction { - from: env.caller, - gas: trace.msg.gas_limit.unwrap_or_default().into(), - value: trace.msg.value.clone().into(), - // If we get here, this isn't a native EVM create. Those always go through - // the EAM. So we have no "real" initcode and must use the sentinel value - // for "invalid" initcode. - init: EthBytes(vec![0xFE]), - }), - result: TraceResult::Create(EthCreateTraceResult { - gas_used: trace.sum_gas().total_gas.into(), - address: Some(create_addr), - code: output, - }), - trace_address: Vec::from(address), - error: trace_err_msg(trace), - ..EthTrace::default() - }), - Some(sub_trace.clone()), - )) -} - -// Decode the parameters and return value of an EVM smart contract creation through the EAM. This -// should only be called with an ExecutionTrace for a Create, Create2, or CreateExternal method -// invocation on the EAM. -fn decode_create_via_eam(trace: &ExecutionTrace) -> anyhow::Result<(Vec, EthAddress)> { - let init_code = match EAMMethod::from_u64(trace.msg.method) { - Some(EAMMethod::Create) => { - let params = decode_params::(&trace.msg)?; - params.initcode - } - Some(EAMMethod::Create2) => { - let params = decode_params::(&trace.msg)?; - params.initcode - } - Some(EAMMethod::CreateExternal) => { - decode_payload(&trace.msg.params, trace.msg.params_codec)?.into() - } - _ => bail!("unexpected CREATE method {}", trace.msg.method), - }; - let ret = decode_return::(&trace.msg_rct)?; - - Ok((init_code, ret.eth_address.0.into())) -} - -// Build an EthTrace for an EVM "create" operation. This should only be called with an -// ExecutionTrace for a Create, Create2, or CreateExternal method invocation on the EAM. -fn trace_eth_create( - env: &mut Environment, - address: &[i64], - trace: &ExecutionTrace, -) -> anyhow::Result<(Option, Option)> { - // Same as the Init actor case above, see the comment there. - if trace.msg.read_only.unwrap_or_default() { - return Ok((None, None)); - } - - // Look for a call to either a constructor or the EVM's resurrect method. - let sub_trace = trace - .subcalls - .iter() - .filter_map(|et| { - if et.msg.to == Address::INIT_ACTOR { - et.subcalls - .iter() - .find(|et| et.msg.method == METHOD_CONSTRUCTOR) - } else { - match EVMMethod::from_u64(et.msg.method) { - Some(EVMMethod::Resurrect) => Some(et), - _ => None, - } - } - }) - .next(); - - // Same as the Init actor case above, see the comment there. - let sub_trace = if let Some(sub_trace) = sub_trace { - sub_trace - } else { - if trace.msg_rct.exit_code.is_success() { - bail!("successful Create/Create2 call failed to call a constructor"); - } - return Ok((None, None)); - }; - - // Decode inputs & determine create type. - let (init_code, create_addr) = decode_create_via_eam(trace)?; - - // Handle the output. - let output = match trace.msg_rct.exit_code.value() { - 0 => { - // success - // We're _supposed_ to include the contracts bytecode here, but we - // can't do that reliably (e.g., if some part of the trace reverts). - // So we don't try and include a sentinel "impossible bytecode" - // value (the value specified by EIP-3541). - EthBytes(vec![0xFE]) - } - 33 => { - // Reverted, parse the revert message. - // If we managed to call the constructor, parse/return its revert message. If we - // fail, we just return no output. - decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec)? - } - _ => EthBytes::default(), - }; - - Ok(( - Some(EthTrace { - r#type: "create".into(), - action: TraceAction::Create(EthCreateTraceAction { - from: env.caller, - gas: trace.msg.gas_limit.unwrap_or_default().into(), - value: trace.msg.value.clone().into(), - init: init_code.into(), - }), - result: TraceResult::Create(EthCreateTraceResult { - gas_used: trace.sum_gas().total_gas.into(), - address: Some(create_addr), - code: output, - }), - trace_address: Vec::from(address), - error: trace_err_msg(trace), - ..EthTrace::default() - }), - Some(sub_trace.clone()), - )) -} - -// Build an EthTrace for a "private" method invocation from the EVM. This should only be called with -// an ExecutionTrace from an EVM instance and on a method between 1 and 1023 inclusive. -fn trace_evm_private( - env: &mut Environment, - address: &[i64], - trace: &ExecutionTrace, -) -> anyhow::Result<(Option, Option)> { - // The EVM actor implements DELEGATECALL by: - // - // 1. Asking the callee for its bytecode by calling it on the GetBytecode method. - // 2. Recursively invoking the currently executing contract on the - // InvokeContractDelegate method. - // - // The code below "reconstructs" that delegate call by: - // - // 1. Remembering the last contract on which we called GetBytecode. - // 2. Treating the contract invoked in step 1 as the DELEGATECALL receiver. - // - // Note, however: GetBytecode will be called, e.g., if the user invokes the - // EXTCODECOPY instruction. It's not an error to see multiple GetBytecode calls - // before we see an InvokeContractDelegate. - match EVMMethod::from_u64(trace.msg.method) { - Some(EVMMethod::GetBytecode) => { - // NOTE: I'm not checking anything about the receiver here. The EVM won't - // DELEGATECALL any non-EVM actor, but there's no need to encode that fact - // here in case we decide to loosen this up in the future. - env.last_byte_code = None; - if trace.msg_rct.exit_code.is_success() - && let Option::Some(actor_trace) = &trace.invoked_actor - { - let to = trace_to_address(actor_trace); - env.last_byte_code = Some(to); - } - Ok((None, None)) - } - Some(EVMMethod::InvokeContractDelegate) => { - // NOTE: We return errors in all the failure cases below instead of trying - // to continue because the caller is an EVM actor. If something goes wrong - // here, there's a bug in our EVM implementation. - - // Handle delegate calls - // - // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, - // method 6. - // 2) Check that the previous trace calls another actor on method 3 - // (GetByteCode) and they are at the same level (same parent) - // 3) Treat this as a delegate call to actor A. - if env.last_byte_code.is_none() { - bail!("unknown bytecode for delegate call"); - } - - if let Option::Some(actor_trace) = &trace.invoked_actor { - let to = trace_to_address(actor_trace); - if env.caller != to { - bail!( - "delegate-call not from address to self: {:?} != {:?}", - env.caller, - to - ); - } - } - - let dp = decode_params::(&trace.msg)?; - - let output = decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec) - .map_err(|e| anyhow::anyhow!("failed to decode delegate-call return: {}", e))?; - - Ok(( - Some(EthTrace { - r#type: "call".into(), - action: TraceAction::Call(EthCallTraceAction { - call_type: "delegatecall".into(), - from: env.caller, - to: env.last_byte_code, - gas: trace.msg.gas_limit.unwrap_or_default().into(), - value: trace.msg.value.clone().into(), - input: dp.input.into(), - }), - result: TraceResult::Call(EthCallTraceResult { - gas_used: trace.sum_gas().total_gas.into(), - output, - }), - trace_address: Vec::from(address), - error: trace_err_msg(trace), - ..EthTrace::default() - }), - Some(trace.clone()), - )) - } - _ => { - // We drop all other "private" calls from FEVM. We _forbid_ explicit calls between 0 and - // 1024 (exclusive), so any calls in this range must be implementation details. - Ok((None, None)) - } - } -} - -/// Returns the effective nonce for an actor: EVM nonce for EVM actors, sequence otherwise. -fn actor_nonce(store: &DB, actor: &ActorState) -> anyhow::Result { - if is_evm_actor(&actor.code) { - let evm_state = evm::State::load(store, actor.code, actor.state) - .context("failed to load EVM state for nonce")?; - Ok(EthUint64::from(evm_state.nonce())) - } else { - Ok(EthUint64::from(actor.sequence)) - } -} - -/// Returns the deployed bytecode of an EVM actor, or `None` for non-EVM actors. -fn actor_bytecode(store: &DB, actor: &ActorState) -> Option { - if !is_evm_actor(&actor.code) { - return None; - } - let evm_state = evm::State::load(store, actor.code, actor.state).ok()?; - store - .get(&evm_state.bytecode()) - .ok() - .flatten() - .map(EthBytes) -} - /// Build state diff by comparing pre and post-execution states for touched addresses. pub(crate) fn build_state_diff( store: &S, @@ -743,13 +88,19 @@ fn build_account_diff( diff.balance = Delta::from_comparison(pre_balance, post_balance); // Compare nonce - let pre_nonce = pre_actor.map(|a| actor_nonce(store, a)).transpose()?; - let post_nonce = post_actor.map(|a| actor_nonce(store, a)).transpose()?; + let pre_nonce = pre_actor.map(|a| a.eth_nonce(store)).transpose()?; + let post_nonce = post_actor.map(|a| a.eth_nonce(store)).transpose()?; diff.nonce = Delta::from_comparison(pre_nonce, post_nonce); // Compare code (bytecode for EVM actors) - let pre_code = pre_actor.and_then(|a| actor_bytecode(store, a)); - let post_code = post_actor.and_then(|a| actor_bytecode(store, a)); + let pre_code = pre_actor + .map(|a| a.eth_bytecode(store)) + .transpose()? + .flatten(); + let post_code = post_actor + .map(|a| a.eth_bytecode(store)) + .transpose()? + .flatten(); diff.code = Delta::from_comparison(pre_code, post_code); // Compare storage slots for EVM actors @@ -807,7 +158,7 @@ fn diff_evm_storage_for_actors( // Value changed diff.insert( key_hash, - Delta::Changed(super::types::ChangedType { + Delta::Changed(ChangedType { from: pre_hash, to: u256_to_eth_hash(post_value), }), @@ -820,7 +171,7 @@ fn diff_evm_storage_for_actors( // Slot cleared (value → zero) diff.insert( key_hash, - Delta::Changed(super::types::ChangedType { + Delta::Changed(ChangedType { from: pre_hash, to: ZERO_HASH, }), @@ -835,7 +186,7 @@ fn diff_evm_storage_for_actors( let key_hash = EthHash(ethereum_types::H256(*key_bytes)); diff.insert( key_hash, - Delta::Changed(super::types::ChangedType { + Delta::Changed(ChangedType { from: ZERO_HASH, to: u256_to_eth_hash(post_value), }), @@ -897,11 +248,12 @@ mod tests { use super::*; use crate::db::MemoryDB; use crate::networks::ACTOR_BUNDLES_METADATA; - use crate::rpc::eth::types::ChangedType; + use crate::rpc::eth::EthUint64; + use crate::rpc::eth::types::EthBytes; use crate::shim::address::Address as FilecoinAddress; use crate::shim::econ::TokenAmount; use crate::shim::machine::BuiltinActor; - use crate::shim::state_tree::{StateTree, StateTreeVersion}; + use crate::shim::state_tree::StateTreeVersion; use crate::utils::db::CborStoreExt as _; use ahash::HashSetExt as _; use cid::Cid; @@ -1478,44 +830,44 @@ mod tests { fn test_actor_nonce_non_evm() { let store = MemoryDB::default(); let actor = create_test_actor(1000, 42); - let nonce = actor_nonce(&store, &actor).unwrap(); + let nonce = actor.eth_nonce(&store).unwrap(); assert_eq!(nonce.0, 42); } #[test] fn test_actor_nonce_evm() { let store = Arc::new(MemoryDB::default()); - if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 7, Some(&[0x60])) { - let nonce = actor_nonce(store.as_ref(), &actor).unwrap(); - // EVM actors use the EVM nonce field, not the actor sequence - assert_eq!(nonce.0, 7); - } + let actor = create_evm_actor_with_bytecode(&store, 1000, 0, 7, Some(&[0x60])) + .expect("failed to create EVM actor fixture"); + let nonce = actor.eth_nonce(store.as_ref()).unwrap(); + // EVM actors use the EVM nonce field, not the actor sequence + assert_eq!(nonce.0, 7); } #[test] fn test_actor_bytecode_non_evm() { let store = MemoryDB::default(); let actor = create_test_actor(1000, 0); - assert!(actor_bytecode(&store, &actor).is_none()); + assert!(actor.eth_bytecode(&store).unwrap().is_none()); } #[test] fn test_actor_bytecode_evm() { let store = Arc::new(MemoryDB::default()); let bytecode = &[0x60, 0x80, 0x60, 0x40, 0x52]; - if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 1, Some(bytecode)) { - let result = actor_bytecode(store.as_ref(), &actor); - assert_eq!(result, Some(EthBytes(bytecode.to_vec()))); - } + let actor = create_evm_actor_with_bytecode(&store, 1000, 0, 1, Some(bytecode)) + .expect("failed to create EVM actor fixture"); + let result = actor.eth_bytecode(store.as_ref()).unwrap(); + assert_eq!(result, Some(EthBytes(bytecode.to_vec()))); } #[test] fn test_actor_bytecode_evm_no_bytecode() { let store = Arc::new(MemoryDB::default()); - if let Some(actor) = create_evm_actor_with_bytecode(&store, 1000, 0, 1, None) { - // No bytecode stored => None (Cid::default() won't resolve to raw data) - let result = actor_bytecode(store.as_ref(), &actor); - assert!(result.is_none()); - } + let actor = create_evm_actor_with_bytecode(&store, 1000, 0, 1, None) + .expect("failed to create EVM actor fixture"); + // No bytecode stored => None (Cid::default() won't resolve to raw data) + let result = actor.eth_bytecode(store.as_ref()).unwrap(); + assert!(result.is_none()); } } diff --git a/src/rpc/methods/eth/trace/types.rs b/src/rpc/methods/eth/trace/types.rs new file mode 100644 index 000000000000..1b4c42de5205 --- /dev/null +++ b/src/rpc/methods/eth/trace/types.rs @@ -0,0 +1,424 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::super::types::{EthAddress, EthAddressList, EthBytes, EthHash}; +use super::super::{EthBigInt, EthUint64}; +use crate::lotus_json::lotus_json_with_self; +use anyhow::{Result, bail}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthCallTraceAction { + pub call_type: String, + pub from: EthAddress, + pub to: Option, + pub gas: EthUint64, + pub value: EthBigInt, + pub input: EthBytes, +} + +#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthCreateTraceAction { + pub from: EthAddress, + pub gas: EthUint64, + pub value: EthBigInt, + pub init: EthBytes, +} + +#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum TraceAction { + Call(EthCallTraceAction), + Create(EthCreateTraceAction), +} + +impl Default for TraceAction { + fn default() -> Self { + TraceAction::Call(EthCallTraceAction::default()) + } +} + +#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthCallTraceResult { + pub gas_used: EthUint64, + pub output: EthBytes, +} + +#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthCreateTraceResult { + pub address: Option, + pub gas_used: EthUint64, + pub code: EthBytes, +} + +#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum TraceResult { + Call(EthCallTraceResult), + Create(EthCreateTraceResult), +} + +impl Default for TraceResult { + fn default() -> Self { + TraceResult::Call(EthCallTraceResult::default()) + } +} + +/// Selects which trace outputs to include in the `trace_call` response. +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum EthTraceType { + /// Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`) + /// with details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`. + Trace, + /// Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`) + /// caused by the simulated transaction. + /// + /// It shows `"from"` and `"to"` values for modified fields, using `"+"`, `"-"`, or `"="` for code changes. + StateDiff, +} + +lotus_json_with_self!(EthTraceType); + +/// Result payload returned by `trace_call`. +#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthTraceResults { + /// Output bytes from the transaction execution. + pub output: EthBytes, + /// State diff showing all account changes. + pub state_diff: Option, + /// Call trace hierarchy (empty when not requested). + #[serde(default)] + pub trace: Vec, +} + +lotus_json_with_self!(EthTraceResults); + +impl EthTraceResults { + /// Constructs from Parity traces, extracting output from the root trace. + pub fn from_parity_traces(traces: Vec) -> Self { + let output = traces + .first() + .map_or_else(EthBytes::default, |trace| match &trace.result { + TraceResult::Call(r) => r.output.clone(), + TraceResult::Create(r) => r.code.clone(), + }); + Self { + output, + state_diff: None, + trace: traces, + } + } +} + +#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthTrace { + pub r#type: String, + pub subtraces: i64, + pub trace_address: Vec, + pub action: TraceAction, + pub result: TraceResult, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthBlockTrace { + #[serde(flatten)] + pub trace: EthTrace, + pub block_hash: EthHash, + pub block_number: i64, + pub transaction_hash: EthHash, + pub transaction_position: i64, +} +lotus_json_with_self!(EthBlockTrace); + +impl EthBlockTrace { + pub fn sort_key(&self) -> (i64, i64, &[i64]) { + ( + self.block_number, + self.transaction_position, + self.trace.trace_address.as_slice(), + ) + } +} + +#[derive(PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthReplayBlockTransactionTrace { + #[serde(flatten)] + pub full_trace: EthTraceResults, + pub transaction_hash: EthHash, + /// `None` because FVM does not support opcode-level VM traces. + pub vm_trace: Option, +} +lotus_json_with_self!(EthReplayBlockTransactionTrace); + +// EthTraceFilterCriteria defines the criteria for filtering traces. +#[derive(Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthTraceFilterCriteria { + /// Interpreted as an epoch (in hex) or one of "latest" for last mined block, "pending" for not yet committed messages. + /// Optional, default: "latest". + /// Note: "earliest" is not a permitted value. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub from_block: Option, + + /// Interpreted as an epoch (in hex) or one of "latest" for last mined block, "pending" for not yet committed messages. + /// Optional, default: "latest". + /// Note: "earliest" is not a permitted value. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub to_block: Option, + + /// Actor address or a list of addresses from which transactions that generate traces should originate. + /// Optional, default: None. + /// The JSON decoding must treat a string as equivalent to an array with one value, for example + /// "0x8888f1f195afa192cfee86069858" must be decoded as [ "0x8888f1f195afa192cfee86069858" ] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub from_address: Option, + + /// Actor address or a list of addresses to which transactions that generate traces are sent. + /// Optional, default: None. + /// The JSON decoding must treat a string as equivalent to an array with one value, for example + /// "0x8888f1f195afa192cfee86069858" must be decoded as [ "0x8888f1f195afa192cfee86069858" ] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub to_address: Option, + + /// After specifies the offset for pagination of trace results. The number of traces to skip before returning results. + /// Optional, default: None. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub after: Option, + + /// Limits the number of traces returned. + /// Optional, default: all traces. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub count: Option, +} +lotus_json_with_self!(EthTraceFilterCriteria); + +impl EthTrace { + pub fn match_filter_criteria( + &self, + from_decoded_addresses: &Option, + to_decoded_addresses: &Option, + ) -> Result { + let (trace_to, trace_from) = match &self.action { + TraceAction::Call(action) => (action.to, action.from), + TraceAction::Create(action) => { + let address = match &self.result { + TraceResult::Create(result) => result + .address + .ok_or_else(|| anyhow::anyhow!("address is nil in create trace result"))?, + _ => bail!("invalid create trace result"), + }; + (Some(address), action.from) + } + }; + + // Match FromAddress + if let Some(from_addresses) = from_decoded_addresses + && !from_addresses.is_empty() + && !from_addresses.contains(&trace_from) + { + return Ok(false); + } + + // Match ToAddress + if let Some(to_addresses) = to_decoded_addresses + && !to_addresses.is_empty() + && !trace_to.is_some_and(|to| to_addresses.contains(&to)) + { + return Ok(false); + } + + Ok(true) + } +} + +/// Represents a changed value with before and after states. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ChangedType { + /// Value before the change + pub from: T, + /// Value after the change + pub to: T, +} + +/// Represents how a value changed during transaction execution. +// Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L84 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum Delta { + /// Existing value didn't change. + #[serde(rename = "=")] + #[default] + Unchanged, + /// A new value was added (account/storage created). + #[serde(rename = "+")] + Added(T), + /// The existing value was removed (account/storage deleted). + #[serde(rename = "-")] + Removed(T), + /// The existing value changed from one value to another. + #[serde(rename = "*")] + Changed(ChangedType), +} + +impl Delta { + /// Compares optional old/new values and returns the appropriate delta variant: + /// `Unchanged` if both are equal or absent, + /// `Added` if only new exists, + /// `Removed` if only old exists, + /// `Changed` if both exist but differ. + pub fn from_comparison(old: Option, new: Option) -> Self { + match (old, new) { + (None, None) => Delta::Unchanged, + (None, Some(new_val)) => Delta::Added(new_val), + (Some(old_val), None) => Delta::Removed(old_val), + (Some(old_val), Some(new_val)) => { + if old_val == new_val { + Delta::Unchanged + } else { + Delta::Changed(ChangedType { + from: old_val, + to: new_val, + }) + } + } + } + } + + pub fn is_unchanged(&self) -> bool { + matches!(self, Delta::Unchanged) + } +} + +/// Account state diff after transaction execution. +/// Tracks changes to balance, nonce, code, and storage. +// Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L156 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct AccountDiff { + pub balance: Delta, + pub code: Delta, + pub nonce: Delta, + /// All touched/changed storage values (key -> delta) + pub storage: BTreeMap>, +} + +impl AccountDiff { + /// Returns true if the account diff contains no changes. + pub fn is_unchanged(&self) -> bool { + self.balance.is_unchanged() + && self.code.is_unchanged() + && self.nonce.is_unchanged() + && self.storage.is_empty() + } +} + +/// State diff containing all account changes from a transaction. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(transparent)] +pub struct StateDiff(pub BTreeMap); + +impl StateDiff { + pub fn new() -> Self { + Self(BTreeMap::new()) + } + + /// Inserts the account diff only if it contains at least one change. + pub fn insert_if_changed(&mut self, addr: EthAddress, diff: AccountDiff) { + if !diff.is_unchanged() { + self.0.insert(addr, diff); + } + } +} + +lotus_json_with_self!(StateDiff); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_changed_type_serialization() { + let changed = ChangedType { + from: 10u64, + to: 20u64, + }; + let json = serde_json::to_string(&changed).unwrap(); + assert_eq!(json, r#"{"from":10,"to":20}"#); + + let deserialized: ChangedType = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, changed); + } + + #[test] + fn test_delta_unchanged() { + let delta: Delta = Delta::from_comparison(Some(42), Some(42)); + assert!(delta.is_unchanged()); + assert_eq!(delta, Delta::Unchanged); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#""=""#); + } + + #[test] + fn test_delta_added() { + let delta: Delta = Delta::from_comparison(None, Some(100)); + assert!(!delta.is_unchanged()); + assert_eq!(delta, Delta::Added(100)); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#"{"+":100}"#); + } + + #[test] + fn test_delta_removed() { + let delta: Delta = Delta::from_comparison(Some(50), None); + assert!(!delta.is_unchanged()); + assert_eq!(delta, Delta::Removed(50)); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#"{"-":50}"#); + } + + #[test] + fn test_delta_changed() { + let delta: Delta = Delta::from_comparison(Some(10), Some(20)); + assert!(!delta.is_unchanged()); + assert_eq!(delta, Delta::Changed(ChangedType { from: 10, to: 20 })); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#"{"*":{"from":10,"to":20}}"#); + } + + #[test] + fn test_delta_none_none() { + let delta: Delta = Delta::from_comparison(None, None); + assert!(delta.is_unchanged()); + assert_eq!(delta, Delta::Unchanged); + } + + #[test] + fn test_delta_deserialization() { + let unchanged: Delta = serde_json::from_str(r#""=""#).unwrap(); + assert_eq!(unchanged, Delta::Unchanged); + + let added: Delta = serde_json::from_str(r#"{"+":42}"#).unwrap(); + assert_eq!(added, Delta::Added(42)); + + let removed: Delta = serde_json::from_str(r#"{"-":42}"#).unwrap(); + assert_eq!(removed, Delta::Removed(42)); + + let changed: Delta = serde_json::from_str(r#"{"*":{"from":10,"to":20}}"#).unwrap(); + assert_eq!(changed, Delta::Changed(ChangedType { from: 10, to: 20 })); + } +} diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index 391b7a415d16..84a3f608bf79 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -11,7 +11,6 @@ use jsonrpsee::types::SubscriptionId; use libsecp256k1::util::FULL_PUBLIC_KEY_SIZE; use rand::Rng; use serde::de::{IntoDeserializer, value::StringDeserializer}; -use std::collections::BTreeMap; use std::{hash::Hash, ops::Deref}; pub const METHOD_GET_BYTE_CODE: u64 = 3; @@ -317,7 +316,6 @@ pub struct EthCallMessage { pub gas_price: Option, #[serde(skip_serializing_if = "Option::is_none", default)] pub value: Option, - // Some clients use `input`, others use `data`; both accepted, `input` takes precedence. #[serde(skip_serializing_if = "Option::is_none", default)] pub data: Option, #[serde(skip_serializing_if = "Option::is_none", default)] @@ -585,340 +583,6 @@ impl PartialEq for EthFilterResult { } } -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthCallTraceAction { - pub call_type: String, - pub from: EthAddress, - pub to: Option, - pub gas: EthUint64, - pub value: EthBigInt, - pub input: EthBytes, -} - -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthCreateTraceAction { - pub from: EthAddress, - pub gas: EthUint64, - pub value: EthBigInt, - pub init: EthBytes, -} - -#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum TraceAction { - Call(EthCallTraceAction), - Create(EthCreateTraceAction), -} - -impl Default for TraceAction { - fn default() -> Self { - TraceAction::Call(EthCallTraceAction::default()) - } -} - -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthCallTraceResult { - pub gas_used: EthUint64, - pub output: EthBytes, -} - -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthCreateTraceResult { - pub address: Option, - pub gas_used: EthUint64, - pub code: EthBytes, -} - -#[derive(Eq, Hash, PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum TraceResult { - Call(EthCallTraceResult), - Create(EthCreateTraceResult), -} - -impl Default for TraceResult { - fn default() -> Self { - TraceResult::Call(EthCallTraceResult::default()) - } -} - -/// Selects which trace outputs to include in the `trace_call` response. -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum EthTraceType { - /// Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`) - /// with details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`. - Trace, - /// Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`) - /// caused by the simulated transaction. - /// - /// It shows `"from"` and `"to"` values for modified fields, using `"+"`, `"-"`, or `"="` for code changes. - StateDiff, -} - -lotus_json_with_self!(EthTraceType); - -/// Result payload returned by `trace_call`. -#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthTraceResults { - /// Output bytes from the transaction execution. - pub output: EthBytes, - /// State diff showing all account changes. - pub state_diff: Option, - /// Call trace hierarchy (empty when not requested). - #[serde(default)] - pub trace: Vec, -} - -lotus_json_with_self!(EthTraceResults); - -impl EthTraceResults { - /// Constructs from Parity traces, extracting output from the root trace. - pub fn from_parity_traces(traces: Vec) -> Self { - let output = traces - .first() - .map_or_else(EthBytes::default, |trace| match &trace.result { - TraceResult::Call(r) => r.output.clone(), - TraceResult::Create(r) => r.code.clone(), - }); - Self { - output, - state_diff: None, - trace: traces, - } - } -} - -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthTrace { - pub r#type: String, - pub subtraces: i64, - pub trace_address: Vec, - pub action: TraceAction, - pub result: TraceResult, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthBlockTrace { - #[serde(flatten)] - pub trace: EthTrace, - pub block_hash: EthHash, - pub block_number: i64, - pub transaction_hash: EthHash, - pub transaction_position: i64, -} -lotus_json_with_self!(EthBlockTrace); - -impl EthBlockTrace { - pub fn sort_key(&self) -> (i64, i64, &[i64]) { - ( - self.block_number, - self.transaction_position, - self.trace.trace_address.as_slice(), - ) - } -} - -#[derive(PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthReplayBlockTransactionTrace { - #[serde(flatten)] - pub full_trace: EthTraceResults, - pub transaction_hash: EthHash, - /// `None` because FVM does not support opcode-level VM traces. - pub vm_trace: Option, -} -lotus_json_with_self!(EthReplayBlockTransactionTrace); - -// EthTraceFilterCriteria defines the criteria for filtering traces. -#[derive(Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EthTraceFilterCriteria { - /// Interpreted as an epoch (in hex) or one of "latest" for last mined block, "pending" for not yet committed messages. - /// Optional, default: "latest". - /// Note: "earliest" is not a permitted value. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub from_block: Option, - - /// Interpreted as an epoch (in hex) or one of "latest" for last mined block, "pending" for not yet committed messages. - /// Optional, default: "latest". - /// Note: "earliest" is not a permitted value. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub to_block: Option, - - /// Actor address or a list of addresses from which transactions that generate traces should originate. - /// Optional, default: None. - /// The JSON decoding must treat a string as equivalent to an array with one value, for example - /// "0x8888f1f195afa192cfee86069858" must be decoded as [ "0x8888f1f195afa192cfee86069858" ] - #[serde(skip_serializing_if = "Option::is_none", default)] - pub from_address: Option, - - /// Actor address or a list of addresses to which transactions that generate traces are sent. - /// Optional, default: None. - /// The JSON decoding must treat a string as equivalent to an array with one value, for example - /// "0x8888f1f195afa192cfee86069858" must be decoded as [ "0x8888f1f195afa192cfee86069858" ] - #[serde(skip_serializing_if = "Option::is_none", default)] - pub to_address: Option, - - /// After specifies the offset for pagination of trace results. The number of traces to skip before returning results. - /// Optional, default: None. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub after: Option, - - /// Limits the number of traces returned. - /// Optional, default: all traces. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub count: Option, -} -lotus_json_with_self!(EthTraceFilterCriteria); - -impl EthTrace { - pub fn match_filter_criteria( - &self, - from_decoded_addresses: &Option, - to_decoded_addresses: &Option, - ) -> Result { - let (trace_to, trace_from) = match &self.action { - TraceAction::Call(action) => (action.to, action.from), - TraceAction::Create(action) => { - let address = match &self.result { - TraceResult::Create(result) => result - .address - .ok_or_else(|| anyhow::anyhow!("address is nil in create trace result"))?, - _ => bail!("invalid create trace result"), - }; - (Some(address), action.from) - } - }; - - // Match FromAddress - if let Some(from_addresses) = from_decoded_addresses - && !from_addresses.is_empty() - && !from_addresses.contains(&trace_from) - { - return Ok(false); - } - - // Match ToAddress - if let Some(to_addresses) = to_decoded_addresses - && !to_addresses.is_empty() - && !trace_to.is_some_and(|to| to_addresses.contains(&to)) - { - return Ok(false); - } - - Ok(true) - } -} - -/// Represents a changed value with before and after states. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct ChangedType { - /// Value before the change - pub from: T, - /// Value after the change - pub to: T, -} - -/// Represents how a value changed during transaction execution. -// Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L84 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum Delta { - /// Existing value didn't change. - #[serde(rename = "=")] - #[default] - Unchanged, - /// A new value was added (account/storage created). - #[serde(rename = "+")] - Added(T), - /// The existing value was removed (account/storage deleted). - #[serde(rename = "-")] - Removed(T), - /// The existing value changed from one value to another. - #[serde(rename = "*")] - Changed(ChangedType), -} - -impl Delta { - /// Compares optional old/new values and returns the appropriate delta variant: - /// `Unchanged` if both are equal or absent, - /// `Added` if only new exists, - /// `Removed` if only old exists, - /// `Changed` if both exist but differ. - pub fn from_comparison(old: Option, new: Option) -> Self { - match (old, new) { - (None, None) => Delta::Unchanged, - (None, Some(new_val)) => Delta::Added(new_val), - (Some(old_val), None) => Delta::Removed(old_val), - (Some(old_val), Some(new_val)) => { - if old_val == new_val { - Delta::Unchanged - } else { - Delta::Changed(ChangedType { - from: old_val, - to: new_val, - }) - } - } - } - } - - pub fn is_unchanged(&self) -> bool { - matches!(self, Delta::Unchanged) - } -} - -/// Account state diff after transaction execution. -/// Tracks changes to balance, nonce, code, and storage. -// Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L156 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct AccountDiff { - pub balance: Delta, - pub code: Delta, - pub nonce: Delta, - /// All touched/changed storage values (key -> delta) - pub storage: BTreeMap>, -} - -impl AccountDiff { - /// Returns true if the account diff contains no changes. - pub fn is_unchanged(&self) -> bool { - self.balance.is_unchanged() - && self.code.is_unchanged() - && self.nonce.is_unchanged() - && self.storage.is_empty() - } -} - -/// State diff containing all account changes from a transaction. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(transparent)] -pub struct StateDiff(pub BTreeMap); - -impl StateDiff { - pub fn new() -> Self { - Self(BTreeMap::new()) - } - - /// Inserts the account diff only if it contains at least one change. - pub fn insert_if_changed(&mut self, addr: EthAddress, diff: AccountDiff) { - if !diff.is_unchanged() { - self.0.insert(addr, diff); - } - } -} - -lotus_json_with_self!(StateDiff); - #[cfg(test)] mod tests { use super::*; @@ -975,79 +639,4 @@ mod tests { let result = EthAddress::eth_address_from_pub_key(&pubkey).unwrap(); assert_eq!(result, expected_eth_address); } - - #[test] - fn test_changed_type_serialization() { - let changed = ChangedType { - from: 10u64, - to: 20u64, - }; - let json = serde_json::to_string(&changed).unwrap(); - assert_eq!(json, r#"{"from":10,"to":20}"#); - - let deserialized: ChangedType = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized, changed); - } - - #[test] - fn test_delta_unchanged() { - let delta: Delta = Delta::from_comparison(Some(42), Some(42)); - assert!(delta.is_unchanged()); - assert_eq!(delta, Delta::Unchanged); - - let json = serde_json::to_string(&delta).unwrap(); - assert_eq!(json, r#""=""#); - } - - #[test] - fn test_delta_added() { - let delta: Delta = Delta::from_comparison(None, Some(100)); - assert!(!delta.is_unchanged()); - assert_eq!(delta, Delta::Added(100)); - - let json = serde_json::to_string(&delta).unwrap(); - assert_eq!(json, r#"{"+":100}"#); - } - - #[test] - fn test_delta_removed() { - let delta: Delta = Delta::from_comparison(Some(50), None); - assert!(!delta.is_unchanged()); - assert_eq!(delta, Delta::Removed(50)); - - let json = serde_json::to_string(&delta).unwrap(); - assert_eq!(json, r#"{"-":50}"#); - } - - #[test] - fn test_delta_changed() { - let delta: Delta = Delta::from_comparison(Some(10), Some(20)); - assert!(!delta.is_unchanged()); - assert_eq!(delta, Delta::Changed(ChangedType { from: 10, to: 20 })); - - let json = serde_json::to_string(&delta).unwrap(); - assert_eq!(json, r#"{"*":{"from":10,"to":20}}"#); - } - - #[test] - fn test_delta_none_none() { - let delta: Delta = Delta::from_comparison(None, None); - assert!(delta.is_unchanged()); - assert_eq!(delta, Delta::Unchanged); - } - - #[test] - fn test_delta_deserialization() { - let unchanged: Delta = serde_json::from_str(r#""=""#).unwrap(); - assert_eq!(unchanged, Delta::Unchanged); - - let added: Delta = serde_json::from_str(r#"{"+":42}"#).unwrap(); - assert_eq!(added, Delta::Added(42)); - - let removed: Delta = serde_json::from_str(r#"{"-":42}"#).unwrap(); - assert_eq!(removed, Delta::Removed(42)); - - let changed: Delta = serde_json::from_str(r#"{"*":{"from":10,"to":20}}"#).unwrap(); - assert_eq!(changed, Delta::Changed(ChangedType { from: 10, to: 20 })); - } } diff --git a/src/rpc/methods/eth/utils.rs b/src/rpc/methods/eth/utils.rs index 9152ca8955fe..35d2c9cc2d89 100644 --- a/src/rpc/methods/eth/utils.rs +++ b/src/rpc/methods/eth/utils.rs @@ -3,13 +3,14 @@ use super::types::{EthAddress, EthBytes}; use crate::rpc::state::{MessageTrace, ReturnTrace}; +use crate::shim::actors::{EVMActorStateLoad as _, evm, is_evm_actor}; use crate::shim::address::Address as FilecoinAddress; use crate::shim::fvm_shared_latest::IDENTITY_HASH; -use crate::shim::state_tree::StateTree; +use crate::shim::state_tree::{ActorState, StateTree}; use ahash::{HashMap, HashMapExt}; use crate::rpc::eth::{EVM_WORD_LENGTH, EthUint64}; -use anyhow::{Result, bail}; +use anyhow::{Context as _, Result, bail}; use cbor4ii::core::Value; use cbor4ii::core::dec::Decode as _; use fvm_ipld_blockstore::Blockstore; @@ -59,6 +60,38 @@ pub fn lookup_eth_address( Ok(Some(EthAddress::from_actor_id(id_addr))) } +/// Extension trait for querying Ethereum-relevant state from a Filecoin actor. +pub(crate) trait ActorStateEthExt { + /// Returns the effective nonce: EVM nonce for EVM actors, sequence otherwise. + fn eth_nonce(&self, store: &DB) -> anyhow::Result; + /// Returns the deployed bytecode of an EVM actor, or `None` for non-EVM actors. + fn eth_bytecode(&self, store: &DB) -> anyhow::Result>; +} + +impl ActorStateEthExt for ActorState { + fn eth_nonce(&self, store: &DB) -> anyhow::Result { + if is_evm_actor(&self.code) { + let evm_state = evm::State::load(store, self.code, self.state) + .context("failed to load EVM state for nonce")?; + Ok(EthUint64::from(evm_state.nonce())) + } else { + Ok(EthUint64::from(self.sequence)) + } + } + + fn eth_bytecode(&self, store: &DB) -> anyhow::Result> { + if !is_evm_actor(&self.code) { + return Ok(None); + } + let evm_state = evm::State::load(store, self.code, self.state) + .context("failed to load EVM state for bytecode")?; + let bytecode = store + .get(&evm_state.bytecode()) + .context("failed to read EVM bytecode")?; + Ok(bytecode.map(EthBytes)) + } +} + /// Decodes the payload using the given codec. pub fn decode_payload(payload: &RawBytes, codec: u64) -> Result { match codec { diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index ae5d77b77609..8ff2be33ed8e 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -2065,6 +2065,7 @@ where // 4. execute block messages // 5. write the state-tree to the DB and return the CID + // step 1: special case for genesis block if tipset.epoch() == 0 { // NB: This is here because the process that executes blocks requires that the // block miner reference a valid miner in the state tree. Unless we create some @@ -2087,6 +2088,8 @@ where tipset.clone(), ); + // step 2: running cron for any null-tipsets + // step 3: run migrations let (parent_state, epoch, block_messages) = exec.prepare_parent_state(genesis_timestamp, enable_tracing, &mut callback)?; @@ -2095,11 +2098,14 @@ where stacker::grow(64 << 20, || -> anyhow::Result { let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?; + // step 4: apply tipset messages let (receipts, events, events_roots) = vm.apply_block_messages(&block_messages, epoch, callback)?; + // step 5: construct receipt root from receipts let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts)?; + // step 6: store events AMTs in the blockstore for (msg_events, events_root) in events.iter().zip(events_roots.iter()) { if let Some(event_root) = events_root { // Store the events AMT - the root CID should match the one computed by FVM diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index b78f54d8cb64..3648e6ef7c98 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -11,7 +11,8 @@ use crate::message::{Message as _, SignedMessage}; use crate::rpc::auth::AuthNewParams; use crate::rpc::beacon::BeaconGetEntry; use crate::rpc::eth::{ - BlockNumberOrHash, EthInt64, Predefined, new_eth_tx_from_signed_message, types::*, + BlockNumberOrHash, EthInt64, Predefined, new_eth_tx_from_signed_message, trace::types::*, + types::*, }; use crate::rpc::gas::{GasEstimateGasLimit, GasEstimateMessageGas}; use crate::rpc::miner::BlockTemplate; From ee8e7476798e295cb9658879addb495036cb9c62 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 13 Mar 2026 19:51:16 +0530 Subject: [PATCH 4/7] address coderabbit comment --- src/rpc/methods/eth/trace/parity.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/eth/trace/parity.rs b/src/rpc/methods/eth/trace/parity.rs index cd8f5c8a7873..575caadfba7f 100644 --- a/src/rpc/methods/eth/trace/parity.rs +++ b/src/rpc/methods/eth/trace/parity.rs @@ -513,7 +513,10 @@ fn trace_eth_create( // Reverted, parse the revert message. // If we managed to call the constructor, parse/return its revert message. If we // fail, we just return no output. - decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec)? + decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec).unwrap_or_else(|err| { + debug!("failed to decode create revert payload: {err}"); + EthBytes::default() + }) } _ => EthBytes::default(), }; From 2870060999b67f4b58c561768fc08d1ea58c5d0a Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 13 Mar 2026 20:03:37 +0530 Subject: [PATCH 5/7] address more coderabbit comment --- src/rpc/methods/eth/trace/parity.rs | 11 ++++++----- src/state_manager/mod.rs | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/rpc/methods/eth/trace/parity.rs b/src/rpc/methods/eth/trace/parity.rs index 575caadfba7f..87f73c44b87d 100644 --- a/src/rpc/methods/eth/trace/parity.rs +++ b/src/rpc/methods/eth/trace/parity.rs @@ -50,7 +50,7 @@ pub fn base_environment( from: &Address, ) -> anyhow::Result { let sender = lookup_eth_address(from, state)? - .with_context(|| format!("top-level message sender {from} s could not be found"))?; + .with_context(|| format!("top-level message sender {from} could not be found"))?; Ok(Environment { caller: sender, ..Environment::default() @@ -513,10 +513,11 @@ fn trace_eth_create( // Reverted, parse the revert message. // If we managed to call the constructor, parse/return its revert message. If we // fail, we just return no output. - decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec).unwrap_or_else(|err| { - debug!("failed to decode create revert payload: {err}"); - EthBytes::default() - }) + decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec) + .unwrap_or_else(|err| { + debug!("failed to decode create revert payload: {err}"); + EthBytes::default() + }) } _ => EthBytes::default(), }; diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 8ff2be33ed8e..a6272a33d3f0 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -1949,6 +1949,7 @@ impl<'a, DB: Blockstore + Send + Sync + 'static> TipsetExecutor<'a, DB> { self.create_vm(parent_state, epoch_i, timestamp, null_epoch_trace)?; if let Err(e) = vm.run_cron(epoch_i, cron_callback.as_mut()) { error!("Beginning of epoch cron failed to run: {e}"); + return Err(e); } vm.flush() })?; From 2881724e4a4371abecb136ab049ad51c78aaef6b Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Mon, 16 Mar 2026 14:10:30 +0530 Subject: [PATCH 6/7] fix merge issue and address comment --- src/rpc/methods/eth/trace/parity.rs | 7 +++---- src/rpc/methods/eth/trace/types.rs | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rpc/methods/eth/trace/parity.rs b/src/rpc/methods/eth/trace/parity.rs index 87f73c44b87d..f5b066e91116 100644 --- a/src/rpc/methods/eth/trace/parity.rs +++ b/src/rpc/methods/eth/trace/parity.rs @@ -393,7 +393,7 @@ fn trace_native_create( } let mut output = EthBytes::default(); - let mut create_addr = EthAddress::default(); + let mut create_addr = None; if trace.msg_rct.exit_code.is_success() { // We're supposed to put the "installed bytecode" here. But this // isn't an EVM actor, so we just put some invalid bytecode (this is @@ -404,8 +404,7 @@ fn trace_native_create( // Extract the address of the created actor from the return value. let init_return: ExecReturn = decode_return(&trace.msg_rct)?; let actor_id = init_return.id_address.id()?; - let eth_addr = EthAddress::from_actor_id(actor_id); - create_addr = eth_addr; + create_addr = Some(EthAddress::from_actor_id(actor_id)); } Ok(( @@ -422,7 +421,7 @@ fn trace_native_create( }), result: TraceResult::Create(EthCreateTraceResult { gas_used: trace.sum_gas().total_gas.into(), - address: Some(create_addr), + address: create_addr, code: output, }), trace_address: Vec::from(address), diff --git a/src/rpc/methods/eth/trace/types.rs b/src/rpc/methods/eth/trace/types.rs index 1b4c42de5205..0c41858916a1 100644 --- a/src/rpc/methods/eth/trace/types.rs +++ b/src/rpc/methods/eth/trace/types.rs @@ -52,6 +52,7 @@ pub struct EthCallTraceResult { #[derive(Eq, Hash, PartialEq, Default, Serialize, Deserialize, Debug, Clone, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct EthCreateTraceResult { + #[serde(skip_serializing_if = "Option::is_none")] pub address: Option, pub gas_used: EthUint64, pub code: EthBytes, @@ -208,8 +209,8 @@ lotus_json_with_self!(EthTraceFilterCriteria); impl EthTrace { pub fn match_filter_criteria( &self, - from_decoded_addresses: &Option, - to_decoded_addresses: &Option, + from_decoded_addresses: Option<&EthAddressList>, + to_decoded_addresses: Option<&EthAddressList>, ) -> Result { let (trace_to, trace_from) = match &self.action { TraceAction::Call(action) => (action.to, action.from), From ad3c2582d22b9a98f4e5a8e0dd47aa50903a50c7 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Mon, 16 Mar 2026 15:48:03 +0530 Subject: [PATCH 7/7] add docs in trace related files --- src/rpc/methods/eth/trace/mod.rs | 9 +++++++++ src/rpc/methods/eth/trace/parity.rs | 6 ++++++ src/rpc/methods/eth/trace/state_diff.rs | 5 +++++ src/rpc/methods/eth/trace/types.rs | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/src/rpc/methods/eth/trace/mod.rs b/src/rpc/methods/eth/trace/mod.rs index 501d18fb758d..1f8e8c6e8dd1 100644 --- a/src/rpc/methods/eth/trace/mod.rs +++ b/src/rpc/methods/eth/trace/mod.rs @@ -1,6 +1,15 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +//! Ethereum trace construction and state diff logic. +//! +//! Submodules: +//! - [`parity`] — builds Parity-compatible [`types::EthTrace`] entries from +//! FVM execution traces. +//! - [`state_diff`] — computes account-level state diffs between pre/post +//! execution. +//! - [`types`] — shared type definitions for all `trace_*` RPC responses. + mod parity; mod state_diff; pub(crate) mod types; diff --git a/src/rpc/methods/eth/trace/parity.rs b/src/rpc/methods/eth/trace/parity.rs index f5b066e91116..7057ba6bafc5 100644 --- a/src/rpc/methods/eth/trace/parity.rs +++ b/src/rpc/methods/eth/trace/parity.rs @@ -1,6 +1,12 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +//! Parity-style trace construction from Filecoin execution traces. +//! +//! Converts FVM [`ExecutionTrace`] trees into Parity-compatible [`EthTrace`] +//! entries. Handles EVM calls, delegate calls, and contract creation through +//! both native (`Init`) and EVM (`EAM`) paths. + use super::super::types::{EthAddress, EthBytes, EthHash}; use super::super::utils::{decode_params, decode_return}; use super::super::{ diff --git a/src/rpc/methods/eth/trace/state_diff.rs b/src/rpc/methods/eth/trace/state_diff.rs index 704ca0431e73..9eec5949ca1d 100644 --- a/src/rpc/methods/eth/trace/state_diff.rs +++ b/src/rpc/methods/eth/trace/state_diff.rs @@ -1,6 +1,11 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +//! State diff computation for `trace_call` and related RPC methods. +//! +//! Compares pre- and post-execution actor states to produce per-account diffs +//! covering balance, nonce, code, and storage. + use super::super::types::{EthAddress, EthHash}; use super::super::utils::ActorStateEthExt as _; use super::types::{AccountDiff, ChangedType, Delta, StateDiff}; diff --git a/src/rpc/methods/eth/trace/types.rs b/src/rpc/methods/eth/trace/types.rs index 0c41858916a1..d23ceed23821 100644 --- a/src/rpc/methods/eth/trace/types.rs +++ b/src/rpc/methods/eth/trace/types.rs @@ -1,6 +1,11 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +//! Shared type definitions for trace-related RPC responses. +//! +//! Trace actions, results, filter criteria, and state-diff primitives used +//! across the `trace_*` RPC methods. + use super::super::types::{EthAddress, EthAddressList, EthBytes, EthHash}; use super::super::{EthBigInt, EthUint64}; use crate::lotus_json::lotus_json_with_self;