diff --git a/crates/networking/rpc/eth/block.rs b/crates/networking/rpc/eth/block.rs index 68a1d5538fc..7b4dc0edba4 100644 --- a/crates/networking/rpc/eth/block.rs +++ b/crates/networking/rpc/eth/block.rs @@ -453,5 +453,27 @@ pub async fn get_all_block_receipts( return Ok(Vec::new()); } let block_hash = header.hash(); - Ok(storage.get_receipts_for_block(&block_hash).await?) + let receipts = storage.get_receipts_for_block(&block_hash).await?; + // `get_receipts_for_block` returns a bare Vec, so a block whose receipts are + // absent is indistinguishable from a block that genuinely has none. Returning + // the empty list would be a wrong answer rather than a reported failure, so + // check it against the block's own transaction count. This mirrors the + // mismatch check the by-index receipt path already performs. + let expected = match storage.get_block_body_by_hash(block_hash).await? { + Some(body) => body.transactions.len(), + // No body means the block's history is not retained; without it there is + // nothing to validate the receipt count against. + None => { + return Err(RpcErr::Internal(format!( + "Body unavailable for block {block_hash:#x}, cannot serve its receipts" + ))); + } + }; + if receipts.len() != expected { + return Err(RpcErr::Internal(format!( + "Expected {expected} receipts for block {block_hash:#x}, got {}", + receipts.len() + ))); + } + Ok(receipts) } diff --git a/test/tests/rpc/mod.rs b/test/tests/rpc/mod.rs index 6bd383bfc01..98d6e6b6cc6 100644 --- a/test/tests/rpc/mod.rs +++ b/test/tests/rpc/mod.rs @@ -6,6 +6,7 @@ mod estimate_gas_tests; mod fork_choice_tests; mod http_batch_tests; mod missing_rpc_methods_tests; +mod raw_receipts_completeness_tests; mod send_raw_transaction_tests; mod subscription_manager_tests; mod trace_call_tests; diff --git a/test/tests/rpc/raw_receipts_completeness_tests.rs b/test/tests/rpc/raw_receipts_completeness_tests.rs new file mode 100644 index 00000000000..1b58f1d0f4d --- /dev/null +++ b/test/tests/rpc/raw_receipts_completeness_tests.rs @@ -0,0 +1,63 @@ +//! `debug_getRawReceipts` must not present a block's receipts as empty when they +//! are merely absent. +//! +//! `get_receipts_for_block` returns a bare `Vec`, so "this block has no receipts +//! stored" and "this block had no transactions" are the same value. Handing back +//! the empty list is a wrong answer rather than a reported failure, and once +//! history pruning lands (#6673) an absent receipt set becomes a normal +//! steady-state outcome rather than a corruption signal. + +use ethrex_rpc::test_utils::{ + add_legacy_tx_blocks, call_http, default_context_with_storage, setup_store, +}; + +#[tokio::test] +async fn raw_receipts_reports_a_block_whose_receipts_are_absent() { + let store = setup_store().await; + // The harness stores blocks and their transactions but no receipts, which is + // exactly the shape a pruned block presents: body present, receipts gone. + add_legacy_tx_blocks(&store, 1, 1).await; + let context = default_context_with_storage(store).await; + + let response = call_http( + context, + r#"{"jsonrpc":"2.0","method":"debug_getRawReceipts","params":["0x1"],"id":1}"#.to_string(), + ) + .await; + + assert!( + response.get("error").is_some(), + "a block with 1 transaction and no stored receipts must report a failure \ + rather than returning an empty list; got {response}" + ); + assert_ne!( + response["result"].as_array().map(|a| a.len()), + Some(0), + "must not answer with an empty receipt list: {response}" + ); +} + +#[tokio::test] +async fn raw_receipts_still_serves_genesis_as_empty() { + let store = setup_store().await; + add_legacy_tx_blocks(&store, 1, 1).await; + let context = default_context_with_storage(store).await; + + // Genesis legitimately has no receipts and is short-circuited before the + // completeness check, so it must keep answering with an empty list. + let response = call_http( + context, + r#"{"jsonrpc":"2.0","method":"debug_getRawReceipts","params":["0x0"],"id":1}"#.to_string(), + ) + .await; + + assert!( + response.get("error").is_none(), + "genesis must not be reported as a failure: {response}" + ); + assert_eq!( + response["result"].as_array().map(|a| a.len()), + Some(0), + "genesis has no receipts: {response}" + ); +}