Repository navigation
fix(eth,core): bound the GetBlockHeaders hash walk #16946 #21482, refs #2858 - #2867
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
a67257b to
641794c
Compare
09d3852 to
4d35c5f
Compare
4d35c5f to
906fc74
Compare
906fc74 to
e3b614d
Compare
810c411 to
d49d4c5
Compare
14ec5f3 to
368daa3
Compare
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.
368daa3 to
f1bae88
Compare
There was a problem hiding this comment.
🟢 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
GetBlockHashesFromHashAPI 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
Refs #2856, #2858.
Stacked on #2866 (ticket #2857), which has since merged: this branch starts where that one ended, so
GetAncestoris available, and after the rebase onto the resultingdev-upgradethe diff shows only this PR's own two commits. The base staysdev-upgradebecause the forks' branches do not exist in this repository.Summary
A by-hash
GetBlockHeadersrequest carrying a non-zeroSkipused to walk the chain by materialising a slice ofSkip+1hashes per step.Skipcomes 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, thefirstflag,maxNonCanonical := uint64(100)and theGetAncestorwalk), the serve lookup budget comes from#21482(017831dd5b), and the deadGetBlockHashesFromHashhelper was removed by#23100(6289137827) — none of which this tree ever took.firstflag) and both hash-traversal directions go throughGetAncestor, so a canonical chain is answered from the canonical hash index without walking.GetBlockHashesFromHash(the hash-per-step walk itself) has no production caller left and is deleted fromBlockChainandHeaderChain; 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
+1overflows (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): thefirstflag,maxNonCanonical := uint64(100), theGetAncestorwalk and the non-canonical budget it spends.#21482—017831dd5b(core, eth: split eth package, implement snap protocol): the serve cap and thelookups < 2*maxHeadersServebudget.#23100—6289137827(consensus/clique, core: API cleanup): the removal ofGetBlockHashesFromHash, which upstream also found test-only.Evidence
TestGetBlockHeadersSkipAllocation— a forward hash query withSkipon the order of the chain height must not allocate in proportion to that skip (measured withruntime.MemStatsagainst a benign query of the same reply size). Fails on the pre-fix code.Skip;TestGetBlockHeadersNonCanonicalBudgetcovers a reverse walk from a side chain deeper than the non-canonical budget.gofmt -l/goimports -lclean,go build ./...,go vet ./core/ ./eth/,go test -count=1 ./core/ ./eth/all pass.grep -rn GetBlockHashesFromHash --include=*.go .is empty.