From 08ba48f803e12e7ba4575f4cbec9f3cbf56637c4 Mon Sep 17 00:00:00 2001 From: tachyontec Date: Mon, 27 Jul 2026 19:52:37 +0300 Subject: [PATCH 1/2] chain/ethereum: Log reproducible call details on call failures Call handler decoding errors and eth_call reverts named the function but nothing that identifies the call itself, so there was no way to replay a failure. The address is already at hand in both places; data_source.rs even builds a logging_extras block containing it, just after the decode errors have returned. Add the contract address, block, transaction hash, calldata and raw output to these messages, 0x-prefixed so they can be pasted into an RPC request. Empty return data now prints as 0x instead of nothing, which used to look like a formatting bug. Closes #3404 --- chain/ethereum/src/data_source.rs | 25 +++++++++-- chain/ethereum/src/ethereum_adapter.rs | 62 ++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 709884496e8..28f517a45c4 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -854,13 +854,28 @@ impl DataSource { ) })?; + // Details that let a subgraph developer identify and reproduce the + // exact on-chain call that failed to decode. Shared by both the input + // and the output decoding errors below. + let call_context = format!( + "contract: {:#x}, from: {:#x}, block: #{} ({:#x}), transaction: {}, \ + raw input: 0x{}", + call.to, + call.from, + call.block_number, + call.block_hash, + call.transaction_hash + .map(|hash| format!("{:#x}", hash)) + .unwrap_or_else(|| "none".to_string()), + hex::encode(&call.input.0), + ); + let values = match function_abi .abi_decode_input(&call.input.0[4..]) .with_context(|| { format!( - "Generating function inputs for the call {:?} failed, raw input: {}", - &function_abi, - hex::encode(&call.input.0) + "Generating function inputs for the call {:?} failed, {}", + &function_abi, call_context ) }) { Ok(val) => val, @@ -890,8 +905,10 @@ impl DataSource { .abi_decode_output(&call.output.0) .with_context(|| { format!( - "Decoding function outputs for the call {:?} failed, raw output: {}", + "Decoding function outputs for the call {:?} failed, {}, \ + raw output: 0x{}", &function_abi, + call_context, hex::encode(&call.output.0) ) })?; diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 9c85f0ac551..5f304a031f8 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1528,9 +1528,9 @@ impl EthereumAdapterTrait for EthereumAdapter { trace!(logger, "eth_call"; "fn" => &call.function.name, - "address" => hex::encode(call.address), - "data" => hex::encode(req.encoded_call.as_ref()), - "block_hash" => call.block_ptr.hash_hex(), + "address" => format!("{:#x}", call.address), + "data" => format!("0x{}", hex::encode(req.encoded_call.as_ref())), + "block_hash" => format!("0x{}", call.block_ptr.hash_hex()), "block_number" => call.block_ptr.block_number() ); Ok(req) @@ -1544,8 +1544,13 @@ impl EthereumAdapterTrait for EthereumAdapter { let call::Response { retval, source, - req: _, + req, } = resp; + // Details of the `eth_call` that failed, so that it can be reproduced by + // hand. Mirrors the field names used by the `eth_call` trace above. + let address = format!("{:#x}", req.address); + let data = format!("0x{}", hex::encode(req.encoded_call.as_ref())); + let block_hash = format!("0x{}", call.block_ptr.hash_hex()); match retval { call::Retval::Value(output) => match call.function.abi_decode_output(&output) { Ok(tokens) => (Some(tokens), source), @@ -1553,7 +1558,15 @@ impl EthereumAdapterTrait for EthereumAdapter { // Decode failures are reverts. The reasoning is that if Solidity fails to // decode an argument, that's a revert, so the same goes for the output. let reason = format!("failed to decode output: {}", e); - info!(logger, "Contract call reverted"; "reason" => reason); + info!(logger, "Contract call reverted"; + "reason" => reason, + "fn" => &call.function.name, + "address" => address, + "data" => data, + "output" => format!("0x{}", hex::encode(&output)), + "block_hash" => block_hash, + "block_number" => call.block_ptr.block_number() + ); (None, call::Source::Rpc) } }, @@ -1561,33 +1574,64 @@ impl EthereumAdapterTrait for EthereumAdapter { // We got a `0x` response. For old Geth, this can mean a revert. It can also be // that the contract actually returned an empty response. A view call is meant // to return something, so we treat empty responses the same as reverts. - info!(logger, "Contract call reverted"; "reason" => "empty response"); + info!(logger, "Contract call reverted"; + "reason" => "empty response", + "fn" => &call.function.name, + "address" => address, + "data" => data, + "output" => "0x", + "block_hash" => block_hash, + "block_number" => call.block_ptr.block_number() + ); (None, call::Source::Rpc) } } } fn log_call_error(logger: &ProviderLogger, e: &ContractCallError, call: &ContractCall) { + // Identify the call that failed so it can be reproduced by hand. The + // calldata is re-encoded here because the request may have failed before + // one was built; encoding errors are reported rather than propagated + // since this is only ever run on an error path. + let address = format!("{:#x}", call.address); + let data = match call.function.abi_encode_input(&call.args) { + Ok(encoded) => format!("0x{}", hex::encode(encoded)), + Err(e) => format!("", e), + }; + let block_hash = format!("0x{}", call.block_ptr.hash_hex()); + let block_number = call.block_ptr.block_number(); match e { ContractCallError::AlloyError(e) => error!( logger, "Ethereum node returned an error when calling function \"{}\" of contract \"{}\": {}", call.function.name, call.contract_name, - e + e; + "address" => address, + "data" => data, + "block_hash" => block_hash, + "block_number" => block_number ), ContractCallError::Timeout => error!( logger, "Ethereum node did not respond when calling function \"{}\" of contract \"{}\"", call.function.name, - call.contract_name + call.contract_name; + "address" => address, + "data" => data, + "block_hash" => block_hash, + "block_number" => block_number ), _ => error!( logger, "Failed to call function \"{}\" of contract \"{}\": {}", call.function.name, call.contract_name, - e + e; + "address" => address, + "data" => data, + "block_hash" => block_hash, + "block_number" => block_number ), } } From 3c7be5fb31e21f41ec6d75d20e56cd8df7c5a670 Mon Sep 17 00:00:00 2001 From: tachyontec Date: Mon, 27 Jul 2026 19:58:21 +0300 Subject: [PATCH 2/2] chain/ethereum: Use function signature in call decoding errors The call decoding errors printed the resolved function with {:?}, which expands to the full alloy Function struct and dominates the message. Use Function::signature_with_outputs() instead, rendering the same information as `withdraw()(uint256)`. This changes the text of a deterministic subgraph error. That text does not participate in Proof of Indexing: the PoI event for a deterministic error carries only a redacted-event count and has no message field, so this is not consensus-breaking. It does change the derived id of rows in the local subgraph_error table, which is a per-node dedup key only. --- chain/ethereum/src/data_source.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 28f517a45c4..fdfdd833045 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -874,8 +874,9 @@ impl DataSource { .abi_decode_input(&call.input.0[4..]) .with_context(|| { format!( - "Generating function inputs for the call {:?} failed, {}", - &function_abi, call_context + "Generating function inputs for the call {} failed, {}", + function_abi.signature_with_outputs(), + call_context ) }) { Ok(val) => val, @@ -905,9 +906,9 @@ impl DataSource { .abi_decode_output(&call.output.0) .with_context(|| { format!( - "Decoding function outputs for the call {:?} failed, {}, \ + "Decoding function outputs for the call {} failed, {}, \ raw output: 0x{}", - &function_abi, + function_abi.signature_with_outputs(), call_context, hex::encode(&call.output.0) )