Skip to content

Commit a83d025

Browse files
committed
fix(ethereum): handle trace_filter traces missing result.output via compat fallback
1 parent 545cd70 commit a83d025

1 file changed

Lines changed: 112 additions & 4 deletions

File tree

chain/ethereum/src/ethereum_adapter.rs

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@ use graph::{
4747
blockchain::{BlockPtr, IngestorError, block_stream::BlockWithTriggers},
4848
prelude::{
4949
BlockNumber, ChainStore, CheapClone, DynTryFuture, Error, EthereumCallCache, Logger,
50-
TimeoutError,
50+
TimeoutError, serde_json,
5151
anyhow::{self, Context, anyhow, bail, ensure},
5252
debug, error, hex, info, retry, trace, warn,
5353
},
5454
};
5555
use itertools::Itertools;
56+
use serde_json::Value;
5657
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
5758
use std::convert::TryFrom;
5859
use std::iter::FromIterator;
@@ -91,6 +92,9 @@ type AlloyProvider = FillProvider<
9192
AnyNetworkBare,
9293
>;
9394

95+
const MISSING_TRACE_OUTPUT_ERROR: &str =
96+
"data did not match any variant of untagged enum TraceOutput";
97+
9498
#[derive(Clone)]
9599
pub struct EthereumAdapter {
96100
logger: Logger,
@@ -256,7 +260,20 @@ impl EthereumAdapter {
256260
let alloy_trace_filter = Self::build_trace_filter(from, to, &addresses);
257261
let start = Instant::now();
258262

259-
let result = self.alloy.trace_filter(&alloy_trace_filter).await;
263+
let result = match self.alloy.trace_filter(&alloy_trace_filter).await {
264+
Ok(traces) => Ok(traces),
265+
Err(error) if Self::is_missing_trace_output_error(&error) => {
266+
warn!(
267+
&logger,
268+
"trace_filter returned traces with missing result.output; retrying with compatibility parser";
269+
"from" => from,
270+
"to" => to,
271+
);
272+
self.trace_filter_with_compat_patch(&alloy_trace_filter)
273+
.await
274+
}
275+
Err(error) => Err(Error::from(error)),
276+
};
260277

261278
if let Ok(traces) = &result {
262279
self.log_trace_results(&logger, from, to, traces.len());
@@ -271,7 +288,7 @@ impl EthereumAdapter {
271288
&logger,
272289
);
273290

274-
result.map_err(Error::from)
291+
result
275292
}
276293

277294
fn build_trace_filter(
@@ -313,7 +330,7 @@ impl EthereumAdapter {
313330
&self,
314331
subgraph_metrics: &Arc<SubgraphEthRpcMetrics>,
315332
elapsed: f64,
316-
result: &Result<Vec<LocalizedTransactionTrace>, RpcError<TransportErrorKind>>,
333+
result: &Result<Vec<LocalizedTransactionTrace>, Error>,
317334
from: BlockNumber,
318335
to: BlockNumber,
319336
logger: &ProviderLogger,
@@ -332,6 +349,46 @@ impl EthereumAdapter {
332349
}
333350
}
334351

352+
fn is_missing_trace_output_error(error: &RpcError<TransportErrorKind>) -> bool {
353+
error.to_string().contains(MISSING_TRACE_OUTPUT_ERROR)
354+
}
355+
356+
async fn trace_filter_with_compat_patch(
357+
&self,
358+
alloy_trace_filter: &AlloyTraceFilter,
359+
) -> Result<Vec<LocalizedTransactionTrace>, Error> {
360+
let mut batch = alloy::rpc::client::BatchRequest::new(self.alloy.client());
361+
let trace_future = batch
362+
.add_call::<(AlloyTraceFilter,), Value>("trace_filter", &(alloy_trace_filter.clone(),))
363+
.map_err(Error::from)?;
364+
365+
batch.send().await.map_err(Error::from)?;
366+
367+
let mut raw_traces = trace_future.await.map_err(Error::from)?;
368+
Self::patch_missing_trace_output(&mut raw_traces);
369+
370+
serde_json::from_value(raw_traces).map_err(Error::from)
371+
}
372+
373+
fn patch_missing_trace_output(raw_traces: &mut Value) {
374+
let Some(traces) = raw_traces.as_array_mut() else {
375+
return;
376+
};
377+
378+
for trace in traces {
379+
let Some(result) = trace.get_mut("result") else {
380+
continue;
381+
};
382+
let Some(result_obj) = result.as_object_mut() else {
383+
continue;
384+
};
385+
386+
if result_obj.contains_key("gasUsed") && !result_obj.contains_key("output") {
387+
result_obj.insert("output".to_owned(), Value::String("0x".to_owned()));
388+
}
389+
}
390+
}
391+
335392
// This is a lazy check for block receipt support. It is only called once and then the result is
336393
// cached. The result is not used for anything critical, so it is fine to be lazy.
337394
async fn check_block_receipt_support_and_update_cache(
@@ -2710,6 +2767,7 @@ mod tests {
27102767
use graph::blockchain::BlockPtr;
27112768
use graph::components::ethereum::AnyNetworkBare;
27122769
use graph::prelude::alloy::primitives::{Address, B256, Bytes};
2770+
use graph::prelude::alloy::providers::ext::TraceApi;
27132771
use graph::prelude::alloy::providers::ProviderBuilder;
27142772
use graph::prelude::alloy::providers::mock::Asserter;
27152773
use graph::prelude::{EthereumCall, LightEthereumBlock, create_minimal_block_for_test};
@@ -2861,6 +2919,56 @@ mod tests {
28612919
.unwrap();
28622920
}
28632921

2922+
#[graph::test]
2923+
async fn missing_output_trace_repro() {
2924+
let trace_filter_response = r#"[{
2925+
"action": {
2926+
"from": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
2927+
"value": "0x0",
2928+
"gas": "0x0",
2929+
"init": "0x",
2930+
"address": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
2931+
"refund_address": "0x4c3ccc98c01103be72bcfd29e1d2454c98d1a6e3",
2932+
"balance": "0x0"
2933+
},
2934+
"blockHash": "0x6b747793a61c3ce4e5f3355cf80edcb6aa465913ed43f4b0136d93803cf330f3",
2935+
"blockNumber": 66762070,
2936+
"result": {
2937+
"gasUsed": "0x0"
2938+
},
2939+
"subtraces": 0,
2940+
"traceAddress": [1, 1],
2941+
"transactionHash": "0x5b3dc50c4c7bd9b0e80469b21febbc5d1b54b364a01b22b1e9c426e4632e0b8f",
2942+
"transactionPosition": 0,
2943+
"type": "suicide"
2944+
}]"#;
2945+
2946+
let json_value: Value = serde_json::from_str(trace_filter_response).unwrap();
2947+
let asserter = Asserter::new();
2948+
let provider = ProviderBuilder::<_, _, AnyNetworkBare>::default()
2949+
.network::<AnyNetworkBare>()
2950+
.connect_mocked_client(asserter.clone());
2951+
2952+
asserter.push_success(&json_value);
2953+
2954+
let err = provider
2955+
.trace_filter(
2956+
&graph::prelude::alloy::rpc::types::trace::filter::TraceFilter::default(),
2957+
)
2958+
.await
2959+
.expect_err("trace_filter should fail to deserialize when result.output is missing");
2960+
2961+
assert!(
2962+
err.to_string()
2963+
.contains(super::MISSING_TRACE_OUTPUT_ERROR),
2964+
"unexpected error: {err:#}"
2965+
);
2966+
2967+
let mut patched: Value = serde_json::from_str(trace_filter_response).unwrap();
2968+
super::EthereumAdapter::patch_missing_trace_output(&mut patched);
2969+
assert_eq!(patched[0]["result"]["output"], Value::String("0x".to_string()));
2970+
}
2971+
28642972
#[test]
28652973
fn parse_block_triggers_specific_call_not_found() {
28662974
let block = create_minimal_block_for_test(2, hash(2));

0 commit comments

Comments
 (0)