Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions chain/ethereum/src/ethereum_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,23 @@ use graph::prelude::{
TransactionInput, TransactionRequest,
trace::{filter::TraceFilter as AlloyTraceFilter, parity::LocalizedTransactionTrace},
},
transports::{RpcError, TransportErrorKind},
transports::RpcError,
},
tokio::try_join,
};
use graph::components::ethereum::json_patch;
use graph::slog::o;
use graph::{
blockchain::{BlockPtr, IngestorError, block_stream::BlockWithTriggers},
prelude::{
BlockNumber, ChainStore, CheapClone, DynTryFuture, Error, EthereumCallCache, Logger,
TimeoutError,
TimeoutError, serde_json,
anyhow::{self, Context, anyhow, bail, ensure},
debug, error, hex, info, retry, trace, warn,
},
};
use itertools::Itertools;
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::convert::TryFrom;
use std::iter::FromIterator;
Expand Down Expand Up @@ -256,7 +258,23 @@ impl EthereumAdapter {
let alloy_trace_filter = Self::build_trace_filter(from, to, &addresses);
let start = Instant::now();

let result = self.alloy.trace_filter(&alloy_trace_filter).await;
let result = match self.alloy.trace_filter(&alloy_trace_filter).await {
Ok(traces) => Ok(traces),
Err(RpcError::DeserError { text, err })
if err
.to_string()
.contains(json_patch::MISSING_TRACE_OUTPUT_ERROR) =>
{
warn!(
&logger,
"trace_filter returned traces with missing result.output; applying compatibility patch";
"from" => from,
"to" => to,
);
Self::deserialize_trace_filter_response_with_patch(text)
}
Err(error) => Err(Error::from(error)),
};

if let Ok(traces) = &result {
self.log_trace_results(&logger, from, to, traces.len());
Expand All @@ -271,7 +289,7 @@ impl EthereumAdapter {
&logger,
);

result.map_err(Error::from)
result
}

fn build_trace_filter(
Expand Down Expand Up @@ -313,7 +331,7 @@ impl EthereumAdapter {
&self,
subgraph_metrics: &Arc<SubgraphEthRpcMetrics>,
elapsed: f64,
result: &Result<Vec<LocalizedTransactionTrace>, RpcError<TransportErrorKind>>,
result: &Result<Vec<LocalizedTransactionTrace>, Error>,
from: BlockNumber,
to: BlockNumber,
logger: &ProviderLogger,
Expand All @@ -332,6 +350,15 @@ impl EthereumAdapter {
}
}

fn deserialize_trace_filter_response_with_patch(
response_text: String,
) -> Result<Vec<LocalizedTransactionTrace>, Error> {
// TODO: remove after alloy-rs/alloy#3931 is released and adopted.
let mut raw_traces: Value = serde_json::from_str(&response_text).map_err(Error::from)?;
json_patch::patch_missing_trace_output(&mut raw_traces);
serde_json::from_value(raw_traces).map_err(Error::from)
}

// This is a lazy check for block receipt support. It is only called once and then the result is
// cached. The result is not used for anything critical, so it is fine to be lazy.
async fn check_block_receipt_support_and_update_cache(
Expand Down Expand Up @@ -2697,8 +2724,9 @@ mod tests {
block_trigger_types_from_intervals, check_block_receipt_support, parse_block_triggers,
};
use graph::blockchain::BlockPtr;
use graph::components::ethereum::AnyNetworkBare;
use graph::components::ethereum::{AnyNetworkBare, json_patch};
use graph::prelude::alloy::primitives::{Address, B256, Bytes};
use graph::prelude::alloy::providers::ext::TraceApi;
use graph::prelude::alloy::providers::ProviderBuilder;
use graph::prelude::alloy::providers::mock::Asserter;
use graph::prelude::{EthereumCall, LightEthereumBlock, create_minimal_block_for_test};
Expand Down Expand Up @@ -2850,6 +2878,56 @@ mod tests {
.unwrap();
}

#[graph::test]
async fn missing_output_trace_repro() {
let trace_filter_response = r#"[{
"action": {
"from": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
"value": "0x0",
"gas": "0x0",
"init": "0x",
"address": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
"refundAddress": "0x4c3ccc98c01103be72bcfd29e1d2454c98d1a6e3",
"balance": "0x0"
},
"blockHash": "0x6b747793a61c3ce4e5f3355cf80edcb6aa465913ed43f4b0136d93803cf330f3",
"blockNumber": 66762070,
"result": {
"gasUsed": "0x0"
},
"subtraces": 0,
"traceAddress": [1, 1],
"transactionHash": "0x5b3dc50c4c7bd9b0e80469b21febbc5d1b54b364a01b22b1e9c426e4632e0b8f",
"transactionPosition": 0,
"type": "suicide"
}]"#;

let json_value: Value = serde_json::from_str(trace_filter_response).unwrap();
let asserter = Asserter::new();
let provider = ProviderBuilder::<_, _, AnyNetworkBare>::default()
.network::<AnyNetworkBare>()
.connect_mocked_client(asserter.clone());

asserter.push_success(&json_value);

let err = provider
.trace_filter(
&graph::prelude::alloy::rpc::types::trace::filter::TraceFilter::default(),
)
.await
.expect_err("trace_filter should fail to deserialize when result.output is missing");

assert!(
err.to_string()
.contains(json_patch::MISSING_TRACE_OUTPUT_ERROR),
"unexpected error: {err:#}"
);

let mut patched: Value = serde_json::from_str(trace_filter_response).unwrap();
json_patch::patch_missing_trace_output(&mut patched);
assert_eq!(patched[0]["result"]["output"], Value::String("0x".to_string()));
}

#[test]
fn parse_block_triggers_specific_call_not_found() {
let block = create_minimal_block_for_test(2, hash(2));
Expand Down
100 changes: 100 additions & 0 deletions graph/src/components/ethereum/json_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

use serde_json::Value;

pub const MISSING_TRACE_OUTPUT_ERROR: &str =
"data did not match any variant of untagged enum TraceOutput";

pub fn patch_type_field(obj: &mut Value) -> bool {
if let Value::Object(map) = obj
&& !map.contains_key("type")
Expand Down Expand Up @@ -44,9 +47,33 @@ pub fn patch_receipts(result: &mut Value) -> bool {
}
}

pub fn patch_missing_trace_output(raw_traces: &mut Value) -> bool {
let Some(traces) = raw_traces.as_array_mut() else {
return false;
};

let mut patched = false;
for trace in traces {
let Some(result) = trace.get_mut("result") else {
continue;
};
let Some(result_obj) = result.as_object_mut() else {
continue;
};

if result_obj.contains_key("gasUsed") && !result_obj.contains_key("output") {
result_obj.insert("output".to_owned(), Value::String("0x".to_owned()));
patched = true;
}
}

patched
}

#[cfg(test)]
mod tests {
use super::*;
use alloy::rpc::types::trace::parity::LocalizedTransactionTrace;
use serde_json::json;

#[test]
Expand Down Expand Up @@ -120,4 +147,77 @@ mod tests {
let mut val = Value::Null;
assert!(!patch_receipts(&mut val));
}

#[test]
fn patch_missing_trace_output_adds_missing_output() {
let mut traces = json!([{
"result": {
"gasUsed": "0x0"
}
}]);
assert!(patch_missing_trace_output(&mut traces));
assert_eq!(traces[0]["result"]["output"], "0x");
}

#[test]
fn patch_missing_trace_output_preserves_existing_output() {
let mut traces = json!([{
"result": {
"gasUsed": "0x1",
"output": "0xabc"
}
}]);
assert!(!patch_missing_trace_output(&mut traces));
assert_eq!(traces[0]["result"]["output"], "0xabc");
}

#[test]
fn patch_missing_trace_output_create_without_result_output() {
let mut traces = json!([{
"result": {
"code": "0x60016000"
}
}]);
assert!(!patch_missing_trace_output(&mut traces));
assert_eq!(traces[0]["result"]["code"], "0x60016000");
}

#[test]
fn patch_missing_trace_output_handles_missing_result() {
let mut traces = json!([{
"action": {
"to": "0x0000000000000000000000000000000000000000"
}
}]);
assert!(!patch_missing_trace_output(&mut traces));
}

#[test]
fn patch_missing_trace_output_deserializes_sonic_fixture() {
let mut traces = json!([{
"action": {
"from": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
"value": "0x0",
"gas": "0x0",
"init": "0x",
"address": "0xf7cf0d9398d06d5cb7e4d37dc1e18a829bfff934",
"refundAddress": "0x4c3ccc98c01103be72bcfd29e1d2454c98d1a6e3",
"balance": "0x0"
},
"blockHash": "0x6b747793a61c3ce4e5f3355cf80edcb6aa465913ed43f4b0136d93803cf330f3",
"blockNumber": 66762070,
"result": {
"gasUsed": "0x0"
},
"subtraces": 0,
"traceAddress": [1, 1],
"transactionHash": "0x5b3dc50c4c7bd9b0e80469b21febbc5d1b54b364a01b22b1e9c426e4632e0b8f",
"transactionPosition": 0,
"type": "suicide"
}]);

assert!(patch_missing_trace_output(&mut traces));
let decoded: Vec<LocalizedTransactionTrace> = serde_json::from_value(traces).unwrap();
assert_eq!(decoded.len(), 1);
}
}