fix(compliance): return null instead of panicking for unavailable blocks - #144
Merged
Merged
Conversation
sprites0
force-pushed
the
fix/compliance-unknown-block-panic
branch
from
July 25, 2026 18:48
a0ea72b to
4899ea1
Compare
`system_tx_count_for_block` assumed the caller had validated `block_id`,
but it is called with raw user input before any block lookup, so
`header_by_id` returns `Ok(None)` for any block the node does not have
locally (unknown, or a locally built pending block numbered `latest + 1`),
and the `unwrap()` panicked a tokio worker.
It now returns `Result<Option<usize>>`: provider errors propagate as RPC
errors, and a missing header makes the caller report the block as
unknown. Returning `null` rather than unfiltered data matters here, since
the system tx count is what makes the response compliant.
Two neighbouring panics on the same paths are fixed as well:
- `adjust_transaction_receipt` underflowed `meta.index - system_tx_count`
and panicked on `.nth(..).unwrap()` for a receipt request on any system
tx hash, which is hidden in compliant mode. Those now return null, as
does the block-vanished race that was `unreachable!()`.
- `adjust_log` unwrapped the `receipts_by_block` provider result; the log
is now dropped instead of panicking the subscription task.
The index arithmetic is left unclamped. Saturating the subtractions would
serve a wrong transaction or log index instead of failing, which is worse
in a layer whose job is emitting correct indices. Instead the underflow is
made impossible: `adjust_block_receipts` decided what to drop with
`cumulative_gas_used == 0` but computed the adjusted index from
`header.extras.system_tx_count`, two different definitions of "system
transaction". A receipt reads zero only because
`HlBlockExecutor::execute_transaction` skips accumulating gas for system
txs, so the value stays zero for as long as the block's system tx prefix
lasts; a zero-gas-price tx landing after a gas-consuming one would leak a
system tx receipt to compliant clients and shift every following index by
one. It now partitions on `idx < system_tx_count`, which is what
`block_receipts_with_system_txs` already does, so the two methods can no
longer disagree about the same block.
`tests/run_tests.sh` gains the issue-143 checks: seven methods against
unknown-hash/future-number asserting `result: null`, a `pending` no-panic
check, and a system tx receipt check that discovers a system tx by walking
back from the head, skipping when the window has none.
Verified against a testnet snapshot at head 49675750, comparing release
builds of nb-20260613 and this branch: `eth_getBlockByNumber("pending")`
panicked at rpc.rs:671:59 and `eth_getTransactionReceipt` on a system tx
hash at rpc.rs:662:91, both now returning null with zero panics under
repeated and concurrent calls. Responses are byte-identical between the
two binaries over 28 blocks containing system transactions (171 responses)
and 16 blocks without them (109 responses).
Fixes #143
sprites0
force-pushed
the
fix/compliance-unknown-block-panic
branch
from
July 25, 2026 18:55
4899ea1 to
7bc699b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #143.
Root cause
system_tx_count_for_blockassumed the caller had validatedblock_id, but it runs before anyblock lookup on raw user input, so
header_by_idreturnsOk(None)and the secondunwrap()panics a tokio worker.
The reported trigger turned out not to be pruned/below-earliest blocks. Those return
nullvia the existing
block_by_idguard, and a testnet datadir carries real headers from block 0anyway (
init-state --without-evmwrites placeholder headers for every block below the startheight — verified against a public testnet snapshot:
static_file_headers_*are populated forsegments 0-99, while
transactions/receiptsare empty below34000000_34499999).The actual trigger is
eth_getBlockByNumber("pending"). reth's defaultPendingBlockKindisFull, so the EthApi builds a local pending block numberedlatest + 1, andadjust_blockthenasks the provider for a header that by definition is not in the database.
Fix
system_tx_count_for_blocknow returnsResult<Option<usize>, Eth::Error>: provider errorspropagate as RPC errors and a missing header makes the caller report the block as unknown.
Returning
nullrather than unfiltered data matters here, since the system tx count is what makesthe response compliant.
Two neighbouring panics on the same paths are fixed as well:
adjust_transaction_receiptunderflowedmeta.index - system_tx_countand panicked on.nth(..).unwrap()for a receipt request on any system tx hash, which is hidden in compliantmode. Those now return null, as does the block-vanished race that was
unreachable!().adjust_logunwrapped thereceipts_by_blockprovider result; the log is now dropped insteadof panicking the subscription task.
safe/finalizedbefore the CL sets them hitProviderError::FinalizedBlockNotFound, whichpanicked on the first unwrap; that now surfaces as an RPC error.
Why the index arithmetic is not clamped
An earlier revision of this branch wrapped every subtraction in
saturating_sub. That was wrong:clamping would emit
transactionIndex: 0for a transaction that is not index 0, in the one modulewhose job is emitting correct indices, and
adjust_transaction_receiptwould thennth()thewrong receipt entirely.
overflowing_subis no better unless the flag is acted on, at which pointit is a wordier
checked_sub. (count - U256::from(..)never panicked in the first place — ruintimplements
Subaswrapping_sub.)The second commit removes the need for clamping instead.
adjust_block_receiptsdecided what todrop with
cumulative_gas_used == 0but computed the adjusted index fromheader.extras.system_tx_count. Those are two different definitions of "system transaction",and there are three in the codebase:
header.extras.system_tx_count, a prefix lengthadjust_block,getBlockTransactionCount*,block_receipts_with_system_txs, the offset inadjust_transaction_receiptreceipt.cumulative_gas_used == 0adjust_block_receipts' filter,adjust_loggas_price == 0, anywhere in the blockevm/config.rswhen building the header,eth_getEvmSystemTxsByBlock*A receipt reads zero only because
HlBlockExecutor::execute_transactionskips accumulating gasfor system txs, so the value stays zero for as long as the block's system tx prefix lasts. If a
zero-gas-price tx ever landed after a gas-consuming one, B would not drop it while A counted it —
leaking a system tx receipt to compliant clients and shifting every following index by one.
Partitioning on
idx < system_tx_count(whatblock_receipts_with_system_txsalready does) makesthe arithmetic non-negative by construction and stops
eth_getBlockReceiptsandeth_getBlockReceiptsWithSystemTxfrom disagreeing about the same block.adjust_log(notion B) andeth_getEvmSystemTxsByBlock*(notion C) are left alone here; thoseinconsistencies predate this PR and deserve their own change.
Verification
Release builds of
nb-20260613and this branch, run against the same testnet snapshot datadirwith
--chain testnet --hl-node-compliant, head at block 49675750.Both panics reproduce on the unpatched binary and are gone on this branch:
eth_getBlockByNumber("pending")rpc.rs:671:59, client getsEmpty reply from servernull, 0 panics under 200 concurrent callseth_getTransactionReceipt(<system tx>)rpc.rs:662:91null, 0 panicsThe first is identical in line and column to the report in #143. Every other candidate — unknown
hash, future number, block
0x1,safe,finalized,eth_getBlockReceipts,eth_getTransactionReceipton an unknown hash — returnednullwith zero panics on bothbinaries.
No regression on the normal path, over two response sets captured from both binaries:
testnet), covering
eth_getBlockReceipts,eth_getBlockReceiptsWithSystemTx,eth_getBlockByNumber(full),getBlockTransactionCountByNumber,eth_getEvmSystemTxsByBlockNumber,eth_getTransactionReceiptfor every regular tx, and per-blocketh_getLogs— 171 responses,byte-identical (
d3c69d2062d5dc043cf9772614f43985). This is the set that exercises thepartitioning change.
(
b00c8311855560e8fe0fcefbcb398a92). Note this set says nothing about the partitioning change:with
system_tx_count == 0both the old and new filters are no-ops.tests/run_tests.shgains the issue-143 checks: seven methods against unknown-hash/future-numberasserting
result: null, apendingno-panic check, and a system tx receipt check that discoversa system tx by walking back from the head. The last one fails on
nb-20260613and passes here.