Skip to content

fix(eth,core): bound the GetBlockHeaders hash walk #16946 #21482, refs #2858 - #2867

Merged
gzliudan merged 2 commits into
XinFinOrg:dev-upgradefrom
gzliudan:fix-eth-header-skip
Oct 27, 2026
Merged

gzliudan merged 2 commits into
XinFinOrg:dev-upgradefrom
gzliudan:fix-eth-header-skip

Conversation

@gzliudan

@gzliudan gzliudan commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #2856, #2858.

Stacked on #2866 (ticket #2857), which has since merged: this branch starts where that one ended, so GetAncestor is available, and after the rebase onto the resulting dev-upgrade the diff shows only this PR's own two commits. The base stays dev-upgrade because the forks' branches do not exist in this repository.

Summary

A by-hash GetBlockHeaders request carrying a non-zero Skip used to walk the chain by materialising a slice of Skip+1 hashes per step. Skip comes straight off the wire and is bounded only by chain height, so one message from any peer could ask this node to allocate a chain-height-sized slice and perform tens of millions of header lookups. The fix is upstream's: the loop body is #16946's (049f5b3572, the first flag, maxNonCanonical := uint64(100) and the GetAncestor walk), the serve lookup budget comes from #21482 (017831dd5b), and the dead GetBlockHashesFromHash helper was removed by #23100 (6289137827) — none of which this tree ever took.

  • The hash origin is now resolved once (first flag) and both hash-traversal directions go through GetAncestor, so a canonical chain is answered from the canonical hash index without walking.
  • A walk that leaves the canonical chain is paid for out of one 100-header non-canonical budget per message; an exhausted budget ends the reply instead of walking on.
  • The loop carries over the lookup budget of twice the serve cap (2 × 192). With the serve cap applied first the budget cannot be reached here, so it is a defensive bound rather than the gate that ends a walk: the gate is the 100-header non-canonical budget.
  • GetBlockHashesFromHash (the hash-per-step walk itself) has no production caller left and is deleted from BlockChain and HeaderChain; the test that used it builds its expectation from the chain by number instead.

Canonical-origin replies are byte-identical, except for a reverse skip whose +1 overflows (Skip == MaxUint64): the old code repeated the origin, the new code stops after it, matching upstream. Otherwise the only wire-visible change is that a reply from a non-canonical origin stops after the budget rather than walking arbitrarily far.

Upstream reference

  • #16946 — 049f5b3572 (core, eth, les: more efficient hash-based header chain retrieval): the first flag, maxNonCanonical := uint64(100), the GetAncestor walk and the non-canonical budget it spends.
  • #21482 — 017831dd5b (core, eth: split eth package, implement snap protocol): the serve cap and the lookups < 2*maxHeadersServe budget.
  • #23100 — 6289137827 (consensus/clique, core: API cleanup): the removal of GetBlockHashesFromHash, which upstream also found test-only.

Evidence

  • TestGetBlockHeadersSkipAllocation — a forward hash query with Skip on the order of the chain height must not allocate in proportion to that skip (measured with runtime.MemStats against a benign query of the same reply size). Fails on the pre-fix code.
  • Table cases added for a request whose origin hash the node cannot resolve and for a chain-height Skip; TestGetBlockHeadersNonCanonicalBudget covers a reverse walk from a side chain deeper than the non-canonical budget.
  • gofmt -l / goimports -l clean, go build ./..., go vet ./core/ ./eth/, go test -count=1 ./core/ ./eth/ all pass.
  • grep -rn GetBlockHashesFromHash --include=*.go . is empty.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c40f4c3c-2574-442c-91e6-320ef0c482e3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gzliudan
gzliudan force-pushed the fix-eth-header-skip branch from a67257b to 641794c Compare October 22, 2026 11:20
@gzliudan gzliudan changed the title fix(eth,core): bound the GetBlockHeaders hash walk, close #2858 fix(eth,core): bound the GetBlockHeaders hash walk #21482, close #2858 Oct 22, 2026
@gzliudan
gzliudan force-pushed the fix-eth-header-skip branch 2 times, most recently from 09d3852 to 4d35c5f Compare October 22, 2026 22:23
@gzliudan gzliudan changed the title fix(eth,core): bound the GetBlockHeaders hash walk #21482, close #2858 fix(eth,core): bound the GetBlockHeaders hash walk #16946 #21482, close #2858 Oct 22, 2026
@gzliudan
gzliudan force-pushed the fix-eth-header-skip branch from 4d35c5f to 906fc74 Compare October 22, 2026 22:28
@gzliudan gzliudan changed the title fix(eth,core): bound the GetBlockHeaders hash walk #16946 #21482, close #2858 fix(eth,core): bound the GetBlockHeaders hash walk #16946 #21482, refs #2858 Oct 22, 2026
@gzliudan
gzliudan force-pushed the fix-eth-header-skip branch from 906fc74 to e3b614d Compare October 24, 2026 02:54
@gzliudan
gzliudan force-pushed the fix-eth-header-skip branch 2 times, most recently from 810c411 to d49d4c5 Compare October 26, 2026 05:32
@gzliudan
gzliudan force-pushed the fix-eth-header-skip branch 7 times, most recently from 14ec5f3 to 368daa3 Compare October 27, 2026 16:54
XinFinOrg#2858

The by-hash branch of the GetBlockHeaders reply walked the chain by materialising a slice of Skip+1 hashes. Skip comes straight off the wire and is bounded only by chain height, so a single request could ask the node to allocate a chain-height-sized slice and perform as many header lookups as the skip was long.

Adopt the ancestor lookup added with XinFinOrg#2857 instead: the origin is resolved once, forward and backward hash traversal both go through GetAncestor, and a walk off the canonical chain is paid for out of one 100-header non-canonical budget per message.

Queries whose origin is on the canonical chain are answered through the canonical hash index, so their replies are unchanged, except for a reverse skip whose +1 overflows (Skip == MaxUint64), where the reply stops at the origin instead of repeating it.

GetBlockHashesFromHash has no production caller left, and the hash-per-step walk it implemented is exactly the defect, so it is deleted from both BlockChain and HeaderChain; the header test that used it to build its expectation walks the chain by number instead.

Ref: ethereum#23100 (6289137, consensus/clique, core: API cleanup) removed the same helper upstream, where it was only used by tests. ethereum#16946 (049f5b3, core, eth, les: more efficient hash-based header chain retrieval) added the loop body this commit follows.
…21482

The loop takes the upstream lookup budget of twice the serve cap (downloader.MaxHeaderFetch, 192 here): the condition stops the walk after 2 * 192 lookups. With the serve cap applied first the budget cannot be reached in this tree -- every non-breaking iteration appends one header, so the loop ends at 192 -- and it is carried over as a defensive bound, not as the gate that ends a walk. The gate is the 100-header non-canonical budget above.

Ref: ethereum#21482 (017831d, core, eth: split eth package, implement snap protocol) added the budget together with the serve cap.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused upstream-aligned change has targeted regression coverage and no unresolved review findings; tests were not independently executed.

No open issues 🎉

What changed in this PR

This PR addresses the peer-controlled header-walk allocation risk on dev-upgrade by using the existing bounded GetAncestor lookup, following upstream geth behavior.

Changes:

  • Replaces hash-by-hash traversal with ancestor lookups, a shared non-canonical traversal budget, and a defensive lookup limit.
  • Adds regression coverage for allocation growth, side-chain traversal, unknown origins, and skip boundaries.
  • Removes the obsolete GetBlockHashesFromHash API and updates test expectations.
File Description
eth/​handler.go Uses bounded ancestor lookups for hash-based header requests.
eth/​handler_test.go Adds allocation, traversal-budget, and boundary regression tests.
core/​headerchain.go Removes the obsolete hash-slice traversal helper.
core/​blockchain_reader.go Removes the unused wrapper for that helper.

Review effort: Balanced

@gzliudan
gzliudan merged commit 99bca80 into XinFinOrg:dev-upgrade Oct 27, 2026
15 checks passed
@gzliudan
gzliudan deleted the fix-eth-header-skip branch October 27, 2026 17:30
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.

5 participants