Skip to content

fix(evm-rpc): fetch a block again when its hash does not verify - #579

Open
iankressin wants to merge 4 commits into
masterfrom
fix/evm-rpc-retry-block-hash-mismatch
Open

iankressin wants to merge 4 commits into
masterfrom
fix/evm-rpc-retry-block-hash-mismatch

Conversation

@iankressin

@iankressin iankressin commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Why

Rpc.mapBlock asserted the block hash, so one header that did not hash to its own hash crashed the process. Providers that spread calls over several backends can return such headers after a network upgrade. After Avalanche's Helicon upgrade, some C-Chain responses omit the six new header fields (targetExponent, minPriceExponent, settledHeight, settledGasUnix, settledGasNumerator, settledExcess) but keep the correct hash. Fetching the same block again returns the full header.

Change

  • mapBlock: on a hash mismatch, log a warning (block number, returned and calculated hash) and return the block flagged _isInvalid with _errorMessage: 'failed to verify block hash'. The other checks are skipped for that block.
  • getBlockBatch: logs, receipts and traces are fetched only for unflagged blocks. A flagged block's data would be thrown away, and the logs-bloom and receipts-root checks read the same bad header, so they could throw before the caller saw the flag.
  • The existing handling for flagged blocks then fetches it again. getBlocks retries up to 5 times, then throws failed to verify block hash with the block numbers. PollStream cuts the batch at the flagged block and asks for it on the next poll.
  • Finalizer: a flagged block no longer counts as proof of finality. It is probed again later, like a missing one.

A header that never verifies (for example, a hash formula that lacks a new fork's fields) still stops ingestion. getBlocks gives up after 5 attempts. At the chain head, PollStream asks for the block again after each headPollInterval with no limit, but once the finalized head is more than strideSize blocks past it, ingest moves to the getBlocks path, which gives up as above. Only a finalized head that stops moving keeps the head path waiting.

Tests

  • test/rpc.test.ts: the tampered-hash test now expects a flagged block instead of an exception. New Avalanche tests: the Helicon fixture verifies, and the same block without the six fields is flagged.
  • get-blocks.test.ts: a flagged block is fetched again and replaced; after 5 failures the error names the hash failure.
  • finalizer.test.ts (new): a flagged block does not set the finalized head.
  • ingest.test.ts: a head block whose hash never verifies ends ingestion with failed to verify block hash once the finalized head moves past it.
  • test/rpc.test.ts: a block whose header fails the hash check (a zeroed logsBloom) comes back flagged, with no receipts fetched, while verifyLogsBloom is on. Before this change the logs-bloom check threw.
  • vitest --run in evm/evm-rpc: 279 passed, 29 skipped. tsc clean.
  • Also ran getBlocks and Rpc over a fake client that served a real mainnet response without the Helicon fields, then the full one: one warning, then the verified block.

🤖 Generated with Claude Code

A block whose header does not hash to its `hash` crashed the process.
Some providers spread calls over several backends, and after a network
upgrade not every backend returns the new header fields: after
Avalanche's Helicon upgrade, some C-Chain responses omit the six new
fields while keeping the correct `hash`. One such response was enough
to stop ingestion.

mapBlock now logs the mismatch and flags the block `_isInvalid`, so the
existing paths fetch it again: getBlocks up to 5 times before failing
with 'failed to verify block hash', PollStream on its next poll. The
finalizer treats a flagged block as unknown and probes it again later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8ruhLh1ZL58YedAa4SpaB
Copilot AI lite review requested due to automatic review settings September 23, 2026 15:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate findings remain in the RPC implementation and tests.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Updates EVM RPC block handling to retry hash-mismatched headers and prevent invalid blocks from advancing finality.

Changes:

  • Flags and logs block hash mismatches.
  • Retries invalid blocks and reports persistent failures.
  • Adds Avalanche Helicon and finality regression coverage.
  • Records the patch release change.
File Description
evm/​evm-rpc/​test/​rpc.test.ts Tests hash mismatch and Helicon behavior.
evm/​evm-rpc/​src/​rpc.ts Flags and logs invalid block hashes.
evm/​evm-rpc/​src/​data-source/​get-blocks.test.ts Tests retry and failure behavior.
evm/​evm-rpc/​src/​data-source/​finalizer.ts Excludes invalid blocks from finality.
evm/​evm-rpc/​src/​data-source/​finalizer.test.ts Tests invalid finality handling.
common/​changes/​@subsquid/​evm-rpc/​fix-evm-rpc-retry-block-hash-mismatch_2026-09-23-18-00.json Records the patch release change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread evm/evm-rpc/test/rpc.test.ts Outdated
Co-authored-by: iankressin <29215044+iankressin@users.noreply.github.com>
…estion

The head path waits for a flagged block without limit. Once the
finalized head is more than `strideSize` blocks past it, ingest moves to
the catch-up path, which gives up after its retries, so such a block
still stops the process instead of stalling it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8ruhLh1ZL58YedAa4SpaB

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Flagged blocks can still undergo requested-data processing, preventing reliable retries.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Skip invalid blocks when adding requested data

evm/​evm-rpc/​src/​rpc.ts:261

This return does not short-circuit getBlockBatch: when the caller requests logs, receipts, or traces, the method still passes the flagged block to addRequestedData afterward. Those routines can validate or mutate the block again (for example, addLogs can throw during logs-bloom verification), so the hash-invalid response may never reach getBlocks as a retryable block. Skip _isInvalid blocks while adding requested data, while retaining them in the returned array for retry.

getBlockBatch passed a hash-flagged block on to addRequestedData. Its
data would be thrown away when the block is fetched again, and the logs
and receipts checks read the same bad header, so a failed logs-bloom or
receipts-root check could throw before the caller saw the flag. Only
unflagged blocks get logs, receipts and traces now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8ruhLh1ZL58YedAa4SpaB

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Log requests must exclude invalid blocks in the middle of a range.

Review effort: Lite
Findings: None

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