Skip to content

fix(compliance): skip system txs in eth_get(Raw)TransactionByBlock{Hash,Number}AndIndex - #162

Closed
icmono wants to merge 2 commits into
hl-archive-node:node-builderfrom
icmono:fix/compliance-tx-by-index
Closed

icmono wants to merge 2 commits into
hl-archive-node:node-builderfrom
icmono:fix/compliance-tx-by-index

Conversation

@icmono

@icmono icmono commented Oct 28, 2026

Copy link
Copy Markdown

Problem

With --hl-node-compliant (or ?hl=true), eth_getBlockBy*, eth_getBlockReceipts, eth_getTransactionReceipt, logs and the tx-count methods hide system txs. The by-index lookups were not covered by the compliance layer and still index the full block:

  • eth_getTransactionByBlockHashAndIndex
  • eth_getTransactionByBlockNumberAndIndex
  • eth_getRawTransactionByBlockHashAndIndex
  • eth_getRawTransactionByBlockNumberAndIndex

So index N returns a different tx than the one eth_getBlockBy* lists at position N (and than hl-node / rpc.hyperliquid.xyz return), often a system tx.

Example, mainnet block 49,493,297 (0x7543f6df…c2ee3), index 0x1:

eth_getTransactionByBlockHashAndIndex
hl-node, rpc.hyperliquid.xyz 0x2a350753…622f7e (matches transactions[1] of the block)
nanoreth nb-20261007 0xfa20609f…3d04bf (a system tx)

This breaks Arbitrum Nitro nodes of Orbit chains that settle on HyperEVM: Nitro looks up batch transactions with eth_getTransactionByBlockHashAndIndex and then stalls, retrying forever on

failed to fetch batch mentioned by batch posting report: L1 client returned unexpected transaction hash 0xfa20… when looking up block 0x7543… transaction 1 with expected hash 0x2a35…

Fix

Add the four methods to EthBlockApi, following the same pattern as the other compliance overrides:

  • shift the requested index by the block's system_tx_count before the lookup, and shift transactionIndex in the response back;
  • for the by-number variants, resolve the tag (latest, …) to a concrete block number first, so the system tx count and the lookup refer to the same block;
  • non-compliant mode is unchanged.

Testing

  • Unit tests for the index mapping.
  • On a mainnet node running this patch: 1,104 by-hash / by-number lookups over 150 random blocks from the last ~150k blocks compared against hl-node, 0 mismatches; out-of-range indexes return null like hl-node.
  • A Nitro (Orbit) node that was stuck on the error above synced past it after switching to the patched build.
  • cargo test passes. cargo clippy --workspace --tests --all-features with -D warnings on current stable (1.99) fails with clippy::double_must_use on every #[rpc] trait in the crate, including ones this PR doesn't touch (e.g. call_forwarder.rs, tx_forwarder.rs, sync_server.rs); no other warnings.

Related: #160 (another system-tx difference vs hl-node).

…sh,Number}AndIndex

In hl-node-compliant mode, eth_getBlockBy*, receipts, logs and tx counts
hide system txs, but the by-index transaction lookups indexed the full
block. Index N therefore returned a different tx than the block lists at
position N (and than hl-node returns), often a system tx.

Shift the requested index by the block's system tx count, and shift
transactionIndex in the response back. Block tags are resolved to a
number first so the count and the lookup refer to the same block.
Non-compliant mode is unchanged.
@icmono
icmono force-pushed the fix/compliance-tx-by-index branch from 75057da to 5de251b Compare November 1, 2026 19:33
@gjwang

gjwang commented Nov 2, 2026

Copy link
Copy Markdown

Resolving latest once handles head advancement. Could the two by-number handlers also pin the resolved block hash across the lookup? At 5de251b, the helper returns a height and system-tx count, then each handler performs a separate number-based lookup.

If the two lookups can observe different canonical snapshots during a reorg, consider this regression case: header A at height N has one system tx; N is replaced by B with two before the lookup. Compliant index 0 is mapped using A’s count to full index 1, which on B is still a system tx. Both the structured and raw number-based methods need to keep the count and transaction tied to the same block. Carrying the resolved hash into a by-hash lookup, or rejecting/retrying a changed identity, would cover that case.

This is a source-review scenario, not a reproduced node reorg or a claim about the live validation reported above.

The system tx count and the transaction now always come from the same block,
even if the block at that height changed between the two lookups.
@icmono

icmono commented Nov 2, 2026

Copy link
Copy Markdown
Author

Good point, thanks. HyperEVM blocks are final, so I don’t expect this to happen in practice, but it’s cheap to rule out. In d46f5cb the by-number handlers resolve the block once to its hash plus system tx count (sealed_header_by_id), then do the lookup by hash. The count and the transaction now always come from the same block, and the by-number methods go through the same path as the by-hash ones.

Re-checked on a mainnet node running this commit: 177 lookups on 59 random recent blocks. By-number, by-hash and rpc.hyperliquid.xyz agree on all of them, and the raw variants agree too. Out-of-range indexes and unknown blocks return null.

@gjwang

gjwang commented Nov 2, 2026

Copy link
Copy Markdown

Thanks, that pins both indexed lookup paths to the same sealed header as the system-tx count. Agreed on finality—the example was a defensive consistency case, not an observed HyperEVM reorg. Appreciate you checking the raw variants too.

@sprites0

Copy link
Copy Markdown
Collaborator

Thanks! Landed on node-builder as 6aa2085 and f0cad31 (authorship preserved) and released in nb-20261110. Closing since the branch predates the CI clippy fix (#165).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants