Skip to content
Closed
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
95 changes: 79 additions & 16 deletions chain/ethereum/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,22 +191,36 @@ impl PatchingHttp {
method == "eth_getTransactionReceipt" || method == "eth_getBlockReceipts"
}

fn patch_rpc_response(response: &mut Value) -> bool {
response
fn patch_rpc_response(method: Option<&str>, response: &mut Value) -> bool {
let mut patched = response
.get_mut("result")
.map(|result| {
// Celo pre-Gingerbread blocks omit `sha3Uncles`; alloy needs it.
if let Some(m) = method
&& json_patch::is_block_method(m)
&& let Some(result_obj) = result.as_object_mut()
{
return json_patch::patch_uncles_hash(result_obj);
}
false
})
.unwrap_or(false);
patched |= response
.get_mut("result")
.map(json_patch::patch_receipts)
.unwrap_or(false)
.unwrap_or(false);
patched
}

fn patch_response(body: &[u8]) -> Option<Vec<u8>> {
fn patch_response(method: Option<&str>, body: &[u8]) -> Option<Vec<u8>> {
let mut json: Value = serde_json::from_slice(body).ok()?;

let patched = match &mut json {
Value::Object(_) => Self::patch_rpc_response(&mut json),
Value::Object(_) => Self::patch_rpc_response(method, &mut json),
Value::Array(batch) => {
let mut patched = false;
for r in batch {
patched |= Self::patch_rpc_response(r);
patched |= Self::patch_rpc_response(method, r);
}
patched
}
Expand Down Expand Up @@ -235,15 +249,32 @@ impl Service<RequestPacket> for PatchingHttp {
let url = self.url.clone();
let no_eip2718 = self.no_eip2718;

let should_patch = if no_eip2718 {
// Receipt patching is gated on `no_eip2718` (only chains without typed
// transactions need it). Block patching (missing `sha3Uncles` on
// pre-Gingerbread Celo) runs unconditionally: it only fires when the
// field is absent, which standard chains never hit.
let (patch_receipts, is_block_call) = match &request {
RequestPacket::Single(req) => (
no_eip2718 && Self::is_receipt_method(req.method()),
json_patch::is_block_method(req.method()),
),
RequestPacket::Batch(reqs) => (
no_eip2718 && reqs.iter().any(|r| Self::is_receipt_method(r.method())),
reqs.iter().any(|r| json_patch::is_block_method(r.method())),
),
};
// `patch_method` borrows `request`; convert to owned so it can move into
// the async block.
let patch_method: Option<String> = if is_block_call {
match &request {
RequestPacket::Single(req) => Self::is_receipt_method(req.method()),
RequestPacket::Batch(reqs) => {
reqs.iter().any(|r| Self::is_receipt_method(r.method()))
}
RequestPacket::Single(req) => Some(req.method().to_string()),
RequestPacket::Batch(reqs) => reqs
.iter()
.find(|r| json_patch::is_block_method(r.method()))
.map(|r| r.method().to_string()),
}
} else {
false
None
};

Box::pin(async move {
Expand All @@ -265,7 +296,10 @@ impl Service<RequestPacket> for PatchingHttp {
));
}

if should_patch && let Some(patched) = Self::patch_response(&body) {
let should_patch = patch_receipts || patch_method.is_some();
if should_patch
&& let Some(patched) = Self::patch_response(patch_method.as_deref(), &body)
{
return serde_json::from_slice(&patched).map_err(|err| {
TransportError::deser_err(err, String::from_utf8_lossy(&patched))
});
Expand All @@ -283,23 +317,52 @@ mod tests {
#[test]
fn patch_response_single() {
let body = br#"{"jsonrpc":"2.0","id":1,"result":{"status":"0x1"}}"#;
let patched = PatchingHttp::patch_response(body).unwrap();
let patched = PatchingHttp::patch_response(None, body).unwrap();
let json: Value = serde_json::from_slice(&patched).unwrap();
assert_eq!(json["result"]["type"], "0x0");
}

#[test]
fn patch_response_returns_none_when_type_exists() {
let body = br#"{"jsonrpc":"2.0","id":1,"result":{"status":"0x1","type":"0x2"}}"#;
assert!(PatchingHttp::patch_response(body).is_none());
assert!(PatchingHttp::patch_response(None, body).is_none());
}

#[test]
fn patch_response_batch() {
let body = br#"[{"jsonrpc":"2.0","id":1,"result":{"status":"0x1"}},{"jsonrpc":"2.0","id":2,"result":{"status":"0x1"}}]"#;
let patched = PatchingHttp::patch_response(body).unwrap();
let patched = PatchingHttp::patch_response(None, body).unwrap();
let json: Value = serde_json::from_slice(&patched).unwrap();
assert_eq!(json[0]["result"]["type"], "0x0");
assert_eq!(json[1]["result"]["type"], "0x0");
}

#[test]
fn patch_response_block_adds_missing_sha3_uncles() {
// Pre-Gingerbread Celo block: no `sha3Uncles` in the header.
let body = br#"{"jsonrpc":"2.0","id":1,"result":{"number":"0x1","parentHash":"0xaa..bb","timestamp":"0x0"}}"#;
let patched = PatchingHttp::patch_response(Some("eth_getBlockByNumber"), body).unwrap();
let json: Value = serde_json::from_slice(&patched).unwrap();
assert_eq!(
json["result"]["sha3Uncles"],
"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
);
}

#[test]
fn patch_response_block_keeps_existing_sha3_uncles() {
// `type` present so the receipt patch is a no-op; only the uncles patch
// could fire, and the field already exists.
let body =
br#"{"jsonrpc":"2.0","id":1,"result":{"number":"0x1","type":"0x2","sha3Uncles":"0x1234"}}"#;
assert!(PatchingHttp::patch_response(Some("eth_getBlockByNumber"), body).is_none());
}

#[test]
fn patch_response_non_block_method_leaves_block_alone() {
// A result without sha3Uncles from a non-block method is not patched by
// the uncles rule (the receipt patch may still add `type`).
let body = br#"{"jsonrpc":"2.0","id":1,"result":{"number":"0x1","type":"0x2"}}"#;
assert!(PatchingHttp::patch_response(Some("eth_getLogs"), body).is_none());
}
}
28 changes: 28 additions & 0 deletions graph/src/components/ethereum/json_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,37 @@
//! transaction) where missing.
//!
//! Also used by `PatchingHttp` for chains that don't support EIP-2718 typed transactions.
//!
//! Celo blocks from before the Gingerbread hard fork have no uncles hash in
//! their header, so RPC nodes omit `sha3Uncles` entirely and alloy rejects the
//! block with a missing-field error. Where the field is absent, patch in the
//! canonical empty-uncles hash (keccak256 of the RLP of an empty list), which
//! is what those blocks' headers commit to.

use serde_json::Value;

/// keccak256(rlp([])) — the uncles hash of a block with no uncles.
const EMPTY_UNCLES_HASH: &str =
"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347";

/// Methods whose result is a block, or a map carrying block-like fields.
pub fn is_block_method(method: &str) -> bool {
method == "eth_getBlockByHash"
|| method == "eth_getBlockByNumber"
|| method.starts_with("eth_getBlockByNumberAndRange")
}

pub fn patch_uncles_hash(block: &mut serde_json::Map<String, Value>) -> bool {
if !block.contains_key("sha3Uncles") {
block.insert(
"sha3Uncles".to_string(),
Value::String(EMPTY_UNCLES_HASH.to_string()),
);
return true;
}
false
}

pub fn patch_type_field(obj: &mut Value) -> bool {
if let Value::Object(map) = obj
&& !map.contains_key("type")
Expand Down