Skip to content

core, eth, trie: merge geth v1.17.0 (v1.17.4 sync, milestone 2/6) - #2319

Draft
pratikspatil024 wants to merge 245 commits into
ppatil-upstream-v1.16.9from
ppatil-upstream-v1.17.0
Draft

core, eth, trie: merge geth v1.17.0 (v1.17.4 sync, milestone 2/6)#2319
pratikspatil024 wants to merge 245 commits into
ppatil-upstream-v1.16.9from
ppatil-upstream-v1.17.0

Conversation

@pratikspatil024

@pratikspatil024 pratikspatil024 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Important

Reviewer guide — stacked PR 2 of 12. Part of the combined go-ethereum v1.17.4 + v1.17.5 upstream sync, which ships as one stable release. Every PR in the stack merges into the base branch upstream-merge-v1.17.4; that base merges into develop once, at the very end — not per-PR.

Merge-commit only — never squash. Squashing rewrites a branch's SHAs and breaks every PR stacked above it.

Review bottom-up: #2308#2319#2325#2328#2337#2340#2341#2342#2343#2345#2346#2354. Start at #2308 / #2319 — every PR above inherits them, so reviewing top-down means re-reviewing.

Expected-red / flaky checks (not code blockers): Quality metrics (diffguard — skipped by team decision; it also mis-scopes across a stacked diff, comparing against the bottom of the stack), and codecov/project (repo-wide coverage threshold; per-PR patch coverage is green). Kurtosis e2e occasionally flakes (~1-in-5, devtools-owned) and is re-run by hand. Full per-batch conflict-resolution reasoning is in docs/upstream-merges/.


Summary

Milestone 2/6 of the Bor ← go-ethereum v1.17.4 upstream sync: merges
go-ethereum v1.17.0 (upstream range v1.16.9-tip → v1.17.0 stable,
0cf3d3ba4) into Bor across 12 review-sized merge batches (~20 upstream
first-parent commits each) plus one standalone go-verkle-removal commit and a
milestone-chores commit.

Stacked on #2308 (v1.16.9, milestone 1/6). Base is ppatil-upstream-v1.16.9
for now; it retargets to upstream-merge-v1.17.4 when #2308 merges.

⚠️ Merge, never squash. Every batch is a real 2-parent merge commit so the
merge base advances and upstream history/authorship is preserved. Squash-merging
this PR (or any PR below it in the stack) rewrites SHAs and breaks every stacked
PR above it. Use a merge commit.

What landed

Most of v1.17.0 is bugfix/tooling/test sweeps that adopted cleanly. The
consensus-relevant decisions, per-batch, are in
docs/upstream-merges/v1.17.4/ledger.md; fork decisions in fork-register.md;
the deferred backlog (with adopt-steps) in needs-wiring.md.

  • No fork/EIP activated. Every v1.17.0 fork/EIP surface is merged dormant
    on Bor networks (EIP-8024 ExtraEips-only; Verkle gate nil). No precompile-set,
    gas-schedule, or activation-block change on any Bor preset. params.Rules.ChainID
    retained (declined upstream removal) to satisfy Bor's precompile-parity guard.
  • Adopted (consensus-relevant): crypto/keccak vendoring (#33323); eip4844
    latestBlobConfig(BlobConfig,error) keeping Bor's block-based gating + no-config
    return 0; opKeccak256 Keccak256Cache adapted to upstream's hasher; trienode-history
    (#32621/#33551/#33584) + NodeFullValueCheckpoint (#32727), both dormant by default;
    eth_getProofs-for-history; callTracer log index (goldens regenerated preserving Bor's
    0x1010 fee logs); pathdb history-index mutex combined with upstream's iterator
    refactor; trie decode CountValues error check; freezer fd-leak fix.
  • go-verkle scaffolding removed (mirrors upstream #33461; kept the dormant Verkle
    gate + binary-tree + stateless.Witness).
  • Deferred / declined (reverted to Bor HEAD, tracked in needs-wiring.md with
    adopt-steps — nothing dropped silently): the state code-read metrics line, core/vm
    write-protection + selfdestruct cluster, OpenTelemetry line (#33452/#33484/#33521/#33780;
    Bor has its own server-side OTel), delayed-p2p-decoding (#33835), EraE format (#32157),
    syncModer, slow-block stats, tx-announcement validation, snap-sync locking, legacypool
    alloc, --miner.maxblobs. These clash with Bor divergences (BlockSTM state, SafePool,
    ExcludeStateSyncReceipt, TD-in-DB, forked downloader, per-fork signer chain).
  • Milestone chores: regenerated eth/ethconfig/gen_config.go (TrienodeHistory +
    NodeFullValueCheckpoint + NoSnapServing TOML); exempted DumpBinTrieLeaves in the
    StateDB↔ParallelStateDB parity guard.

Executed tests

Beyond the standard CI gates:

  • go build ./... — clean. go vet ./... — clean except two pre-existing
    //nolint:govet copylocks (identical to develop). go mod tidy — no drift
    (this milestone adds no dependencies).
  • Full go test ./... — green after fixing the only two merge artifacts it
    surfaced (ethclient rpc.NewServer signature; internal/ethapi blockTimestamp
    goldens). Remaining failures are pre-existing and byte-identical to develop
    (VEBLOP miner nil-deref on non-Bor-network test harnesses in cmd/geth/cmd/devp2p/ethtest;
    t8n golden drift; a core/vm interrupt-race flake).
  • make test-integration (-tags integration ./tests/...) — pass, zero failures,
    including the tests/bor consensus e2e suite (621s).
  • govulncheck ./... — no merge-introduced vulnerabilities (3 called vulns are all
    pre-existing/develop-inherent dependency/toolchain versions).

Not run in-session (operator/CI release gates): diffguard mutation, kurtosis devnet,
and the devnet→testnet soak ladder. Kurtosis marginal value is low here — tests/bor
(consensus e2e) is green and the only observed panics are non-Bor-network paths.

Rollout notes

  • Not consensus-affecting on Bor networks. No fork activated, no precompile/gas/
    activation-height change; all adopted features either dormant-by-default or
    non-consensus (tooling/RPC/metrics/tests). Deferred upstream features were reverted
    to Bor HEAD.
  • Backward-compatible; no coordinated upgrade required for this milestone.
  • Operator-visible additions: blockTimestamp field on eth_getTransactionBy* RPC
    responses (additive); download progress-bar behavior; HTTP/2 JSON-RPC.
  • Follow-ups for the team (pre-existing, outside this sync): nil-guard the VEBLOP miner
    timeout (miner/worker.go:774) so non-Bor-network tests stop panicking; dependency
    bumps (x/text→v0.39.0, cascade develop's x/crypto v0.52.0, Go→1.26.5).

🤖 Generated with Claude Code

fjl and others added 30 commits November 4, 2025 13:28
Because the map iteration is unstable, we need to order logs by tx index
and keep the same order with receipts and their logs, so we can still
get the same `LogsHash` across runs.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
The version check incorrectly used `&&` instead of `||`, causing
versions like v1.0.x through v1.4.x to be allowed when they should be
rejected. These versions don't support EIP-712 signing which was
introduced in firmware v1.5.0.
Removes an unnecessary todo. This case is handled, the comment was an
artifact from Kev's refactor
…r (#33098)

failed in 32bit:

```
--- FAIL: TestDecodeSingleCorruptedData (0.00s)
panic: runtime error: slice bounds out of range [:-1501805520] [recovered, repanicked]

goroutine 38872 [running]:
testing.tRunner.func1.2({0x838db20, 0xa355620})
	/opt/actions-runner/_work/_tool/go/1.25.3/x64/src/testing/testing.go:1872 +0x29b
testing.tRunner.func1()
	/opt/actions-runner/_work/_tool/go/1.25.3/x64/src/testing/testing.go:1875 +0x414
panic({0x838db20, 0xa355620})
	/opt/actions-runner/_work/_tool/go/1.25.3/x64/src/runtime/panic.go:783 +0x103
github.com/ethereum/go-ethereum/triedb/pathdb.decodeSingle({0x9e57500, 0x1432, 0x1432}, 0x0)
	/opt/actions-runner/_work/go-ethereum/go-ethereum/triedb/pathdb/history_trienode.go:399 +0x18d6
github.com/ethereum/go-ethereum/triedb/pathdb.TestDecodeSingleCorruptedData(0xa2db9e8)
	/opt/actions-runner/_work/go-ethereum/go-ethereum/triedb/pathdb/history_trienode_test.go:698 +0x180
testing.tRunner(0xa2db9e8, 0x83c86e8)
	/opt/actions-runner/_work/_tool/go/1.25.3/x64/src/testing/testing.go:1934 +0x114
created by testing.(*T).Run in goroutine 1
	/opt/actions-runner/_work/_tool/go/1.25.3/x64/src/testing/testing.go:1997 +0x4b4
FAIL	github.com/ethereum/go-ethereum/triedb/pathdb	41.453s
?   	github.com/ethereum/go-ethereum/version	[no test files]
FAIL
```

Found in
https://github.com/ethereum/go-ethereum/actions/runs/18912701345/job/53990136071?pr=33052
- Replace hardcoded DigestLength 
- Add hash length validation
Rollups may want to use these to dynamically adjust blobs posted after
BPO forks.
The periodic sealing loop failed to reset its timer when sealBlock
returned an error, causing the timer to never fire again and effectively
halting block production in developer periodic mode after the first
failure. This is a bug because the loop relies on the timer to trigger
subsequent sealing attempts, and transient errors (e.g., pool races or
chain rewinds) should not permanently stop the loop. The change moves
timer.Reset after the sealing attempt unconditionally, ensuring the loop
continues ticking and retrying even when sealing fails, which matches
how other periodic timers in the codebase behave and preserves forward
progress.
[powdr](github.com/powdr-labs/powdr) has tested keeper in their womir
system and managed to get it to work. This PR adds wasm as a keeper
target. There's another plan by the zkevm team to support wasm with wasi
as well, so these PR adds both targets.

These currently uses the `example` tag, as there is no precompile
intefrace defined for either target yet. Nonetheless, this is useful for
testing these zkvms so it makes sense to support these experimental
targets already.
While updating to latest Geth, I noticed `OnCodeChangeV2` was not
properly handled in `SelfDestruct/6780`, this PR fixes this and bring a
unit test. Let me know if it's deemed more approriate to merge the tests
with the other one.
Context from Cloudflare blog:
https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive

We were able to reproduce the same issue discussed by Cloudflare in
their recent blog post above using the `ethclient`.
Add BlobTxs flag to filter.

Signed-off-by: Csaba Kiraly <csaba.kiraly@gmail.com>
Add nil check before calling tx.Type() to prevent panic when transaction
is not found.
The iterator loop in findTxInBlockBody returned the outer-scoped err
when iter.Err() was non-nil, which could incorrectly propagate a nil or
stale error and hide actual RLP decoding issues. This patch returns
iter.Err() as intended by the rlp list iterator API, matching
established patterns elsewhere in the codebase and improving diagnostics
when encountering malformed transaction entries.
…3186)

Co-authored-by: Gary Rong <garyrong0905@gmail.com>
…T, and move the transition tree to its own package (#32445)

This is broken off of #31730 to only focus on testing networks that
start with verkle at genesis.

The PR has seen a lot of work since its creation, and it now targets
creating and re-executing tests for a binary tree testnet without the
transition (so it starts at genesis). The transition tree has been moved
to its own package. It also replaces verkle with the binary tree for
this specific application.

---------

Co-authored-by: Gary Rong <garyrong0905@gmail.com>
Show the actual gas used in the block limit error so RPC clients see
useful numbers.
Fixes a potential panic in `randomDuration` when `min == max` by
handling the edge case explicitly.
`StateDB` lacks recording functionality, so it has been replaced with
`tractStateDB` and advanced
Regenerate gen_config.go for TrienodeHistory + NodeFullValueCheckpoint
TOML marshaling (+ sync NoSnapServing); exempt DumpBinTrieLeaves in the
StateDB/ParallelStateDB parity check; commit the v1.17.4-sync tracking docs.
Adapt TestBlockReceiptsPreservesCanonicalFlag to Bor's rpc.NewServer
signature; regenerate eth_getTransactionBy* goldens for the adopted
blockTimestamp field (state-sync-tx fixtures preserved).
@socket-security

socket-security Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgolang/​github.com/​grafana/​pyroscope-go@​v1.4.199100100100100

View full report

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.75668% with 1261 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.24%. Comparing base (5f1acde) to head (b598953).

Files with missing lines Patch % Lines
core/state/database_history.go 0.00% 114 Missing ⚠️
ethclient/gethclient/gen_callframe_json.go 0.00% 85 Missing ⚠️
eth/catalyst/api.go 0.00% 75 Missing ⚠️
core/txpool/blobpool/blobpool.go 45.60% 66 Missing and 2 partials ⚠️
internal/debug/pyroscope.go 0.00% 53 Missing ⚠️
ethclient/gethclient/gethclient.go 0.00% 48 Missing ⚠️
trie/bintrie/trie.go 17.24% 47 Missing and 1 partial ⚠️
trie/bintrie/internal_node.go 18.96% 38 Missing and 9 partials ⚠️
crypto/keccak/sha3.go 67.17% 34 Missing and 9 partials ⚠️
ethclient/gethclient/gen_calllog_json.go 0.00% 43 Missing ⚠️
... and 103 more

❌ Your patch check has failed because the patch coverage (38.75%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##           ppatil-upstream-v1.16.9    #2319      +/-   ##
===========================================================
+ Coverage                    54.21%   54.24%   +0.02%     
===========================================================
  Files                          908      912       +4     
  Lines                       162245   164111    +1866     
===========================================================
+ Hits                         87966    89015    +1049     
- Misses                       68842    69540     +698     
- Partials                      5437     5556     +119     
Files with missing lines Coverage Δ
accounts/accounts.go 100.00% <100.00%> (ø)
accounts/keystore/keystore.go 77.01% <100.00%> (+0.14%) ⬆️
beacon/light/canonical.go 67.60% <100.00%> (+1.42%) ⬆️
beacon/light/sync/head_sync.go 75.89% <100.00%> (+0.21%) ⬆️
beacon/params/config.go 42.46% <100.00%> (-0.59%) ⬇️
beacon/types/beacon_block.go 55.95% <100.00%> (ø)
common/bitutil/bitutil.go 94.38% <100.00%> (-0.70%) ⬇️
common/size.go 100.00% <100.00%> (ø)
core/blockchain.go 63.10% <100.00%> (+0.02%) ⬆️
core/blockchain_insert.go 87.64% <ø> (+10.41%) ⬆️
... and 154 more

... and 32 files with indirect coverage changes

Files with missing lines Coverage Δ
accounts/accounts.go 100.00% <100.00%> (ø)
accounts/keystore/keystore.go 77.01% <100.00%> (+0.14%) ⬆️
beacon/light/canonical.go 67.60% <100.00%> (+1.42%) ⬆️
beacon/light/sync/head_sync.go 75.89% <100.00%> (+0.21%) ⬆️
beacon/params/config.go 42.46% <100.00%> (-0.59%) ⬇️
beacon/types/beacon_block.go 55.95% <100.00%> (ø)
common/bitutil/bitutil.go 94.38% <100.00%> (-0.70%) ⬇️
common/size.go 100.00% <100.00%> (ø)
core/blockchain.go 63.10% <100.00%> (+0.02%) ⬆️
core/blockchain_insert.go 87.64% <ø> (+10.41%) ⬆️
... and 154 more

... and 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Develop-drift cascade: carries develop's #2347 (Kurtosis e2e and
stateless-e2e on every PR base) and #2333 (complete witnesses under
BlockSTM v2) one hop further up the stack.

Clean at this hop; this branch still predates upstream #34106.
The develop-drift cascade brought in #2333's prewalk and read-set tests,
which call newTrieReader with the verkle point-cache argument develop still
has. Upstream dropped that parameter within the v1.17.0 range, so the calls
carry one argument too many from this branch upward and the core/state test
package no longer typechecks. Same two-line adaptation the merge into
ppatil-upstream-v1.17.2 already carried; this branch and the one above it
were missed.

The reason they were missed is worth recording, because the failure mode is
easy to repeat: the cascade's lower hops were verified with go build, and
go build does not compile test files. Nothing was wrong with the merge — it
was clean and the production tree built — but a test package that cannot
typecheck fails both the lint and unit-tests jobs, which is exactly what CI
reported. go vet ./... catches this and go build ./... never will.

Verified: build clean; full-tree go vet clean apart from the two
pre-existing lock-copy findings in core/parallel_state_processor.go and
trie/secure_trie.go; make lint reports 0 issues; all seven of #2333's
prewalk and read-set tests pass.
@pratikspatil024
pratikspatil024 requested a lite review from Copilot August 12, 2026 09:20
@pratikspatil024

Copy link
Copy Markdown
Member Author

codegenie review

@pratikspatil024

Copy link
Copy Markdown
Member Author

@claude review

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

🧞 Codegenie Review

Warning

Review incomplete. Some review or verification work did not complete. Planning also fell back to deterministic default coverage. Treat the results below as partial and consider rerunning.

Sorry, this review is incomplete. The allotted max time of 30 minutes was reached and the review has been degraded. Re-run with --max-time <minutes> (config review.maxTime, or review.maxTimeMinutes in eval cases) for a higher time allotment.

Review incomplete: completed work produced no credible verified findings, but incomplete coverage or verification prevents a clean conclusion.

Coverage

Partial review: 973 hunks were not reviewed because budget was exhausted before dispatch.
Reviewed 0/1023 hunks before stopping.
Incomplete work: skipped 50, failed 973.
Coverage levels: deep 176, normal 745, light 52, skip 50.
Planning was degraded and deterministic fallbacks were used.
Unreviewed hunks by file:

  • accounts/abi/bind/v2/dep_tree_test.go: 1 hunk (budget stopped before dispatch)
  • accounts/accounts.go: 2 hunks (budget stopped before dispatch)
  • accounts/keystore/keystore.go: 2 hunks (budget stopped before dispatch)
  • accounts/keystore/presale.go: 1 hunk (budget stopped before dispatch)
  • accounts/scwallet/securechannel.go: 1 hunk (budget stopped before dispatch)
  • accounts/usbwallet/hub.go: 3 hunks (budget stopped before dispatch)
  • accounts/usbwallet/ledger.go: 1 hunk (budget stopped before dispatch)
  • accounts/usbwallet/wallet.go: 1 hunk (budget stopped before dispatch)
  • beacon/blsync/engineclient.go: 3 hunks (budget stopped before dispatch)
  • beacon/engine/types.go: 4 hunks (budget stopped before dispatch)
  • beacon/light/canonical.go: 1 hunk (budget stopped before dispatch)
  • beacon/light/sync/head_sync.go: 1 hunk (budget stopped before dispatch)
  • 291 additional files omitted.
  • Budget stopped review work (runtime reserve reached).
  • planner degraded; deterministic default plan used
  • This dossier is chunk 3 of 92 (chunkRoot beacon/params + beacon/types) from a 326-file, 997-hunk upstream merge PR. Only the 10 hunks across 8 beacon/* files are in scope here; the remaining v1.17.0 merge surface is covered by other chunks.; Dossier is chunk 9 of 92 for a 326-file, 997-hunk upstream merge; only the 7 cmd/geth deletion hunks in this chunk are planned here.; Dossier is chunk 12 of 92; only common/bitutil/bitutil_test.go is routed here out of 326 changed files in the overall merge PR.; The planner dossier is chunk 14 of 92 for a very large upstream merge. This plan schedules only the 12 hunks across the four files in this chunk (common/types.go, consensus/{beacon,clique,ethash}).; Dossier is chunk 16 of 92 (core/blockchain.go..core/filtermaps) from a 326-file, 997-hunk upstream merge; only the 9 hunks in this chunk are schedulable here.; Planner dossier is chunk 19 of 92 for a 326-file, 997-hunk upstream merge; this plan only schedules the two core/rawdb tablewriter files present in this chunk.; Dossier is chunk 22 of 92 for a 326-file upstream merge; only core/state/access_events.go (12 hunks) is routable in this planning pass.; Planner dossier is a chunked projection (chunk 33 of 92) scoped to core/txpool/blobpool/blobpool.go only; the full PR spans 326 files and 997 hunks. Hunk excerpts and per-hunk detail were omitted for all 21 hunks due to prompt budget, so coverage is routed from file-level priority and upstream commit metadata rather than diff text.
  • beacon/engine/gen_ed.go: generated file
  • cmd/keeper/go.sum: lockfile
  • core/tracing/gen_balance_change_reason_stringer.go: generated file
  • core/tracing/gen_code_change_reason_stringer.go: generated file
  • core/tracing/gen_nonce_change_reason_stringer.go: generated file
  • crypto/keccak/keccakf_amd64.s: generated file
  • crypto/keccak/testdata/keccakKats.json.deflate: binary file
  • eth/ethconfig/gen_config.go: generated file
  • ethclient/gethclient/gen_callframe_json.go: generated file
  • ethclient/gethclient/gen_calllog_json.go: generated file

Stats

  • 🤖 Model: anthropic claude-opus-5 high
  • 🧞 Codegenie: v0.5.5 (58f82a9b2c)
  • Elapsed time: 27m 31s
  • Git: 0xPolygon/bor from ppatil-upstream-v1.16.9 to ppatil-upstream-v1.17.0 (b598953730)
  • Review completeness: partial.
  • Usage: model calls 46, tokens 2402059, cost $16.8263.
  • Effective caps: tokens 8000000.
  • Budget dispatch blocks: stage 5 runtime 1627315/1800000; stage 5 runtime 1627322/1800000; stage 5 runtime 1627327/1800000; stage 5 runtime 1627332/1800000; stage 5 runtime 1627337/1800000; stage 5 runtime 1627341/1800000; stage 5 runtime 1627346/1800000; stage 5 runtime 1627350/1800000; stage 5 runtime 1627354/1800000; stage 5 runtime 1627358/1800000; stage 5 runtime 1627363/1800000; stage 5 runtime 1627367/1800000; stage 5 runtime 1627372/1800000; stage 5 runtime 1627376/1800000; stage 5 runtime 1627379/1800000; stage 5 runtime 1627384/1800000; stage 5 runtime 1627388/1800000; stage 5 runtime 1627392/1800000; stage 5 runtime 1627397/1800000; stage 5 runtime 1627401/1800000; stage 5 runtime 1627406/1800000; stage 5 runtime 1627409/1800000; stage 5 runtime 1627414/1800000; stage 5 runtime 1627418/1800000; stage 5 runtime 1627422/1800000; stage 5 runtime 1627426/1800000; stage 5 runtime 1627431/1800000; stage 5 runtime 1627434/1800000; stage 5 runtime 1627439/1800000; stage 5 runtime 1627443/1800000; stage 5 runtime 1627447/1800000; stage 5 runtime 1627451/1800000; stage 5 runtime 1627455/1800000; stage 5 runtime 1627459/1800000; stage 5 runtime 1627463/1800000; stage 5 runtime 1627468/1800000; stage 5 runtime 1627472/1800000; stage 5 runtime 1627476/1800000; stage 5 runtime 1627480/1800000; stage 5 runtime 1627484/1800000; stage 5 runtime 1627488/1800000; stage 5 runtime 1627492/1800000; stage 5 runtime 1627497/1800000; stage 5 runtime 1627500/1800000; stage 5 runtime 1627504/1800000; stage 5 runtime 1627509/1800000; stage 5 runtime 1627513/1800000; stage 5 runtime 1627517/1800000; stage 5 runtime 1627521/1800000; stage 5 runtime 1627526/1800000; stage 5 runtime 1627530/1800000; stage 5 runtime 1627534/1800000; stage 5 runtime 1627537/1800000; stage 5 runtime 1627541/1800000; stage 5 runtime 1627545/1800000; stage 5 runtime 1627550/1800000; stage 7 runtime 1640429/1800000.
  • Local context pressure: 75 degraded tool results, 242 degraded hunks.

⚠️ Review Incomplete

Completed review work produced no credible verified findings, but incomplete coverage or verification prevents a clean conclusion.

View Workflow Job

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 wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Comment on lines 72 to 83
// TODO: consider if parallelizing this is worth it, probably depending on len(ae.chunks).
keys := make([][]byte, 0, len(ae.chunks))
for chunk := range ae.chunks {
basePoint := ae.pointCache.Get(chunk.addr[:])
key := utils.GetTreeKeyWithEvaluatedAddress(basePoint, &chunk.treeIndex, chunk.leafKey)
var offset [32]byte
treeIndexBytes := chunk.treeIndex.Bytes32()
copy(offset[:31], treeIndexBytes[1:])
offset[31] = chunk.leafKey
key := bintrie.GetBinaryTreeKey(chunk.addr, offset[:])
keys = append(keys, key)
}
return keys
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AccessEvents.Keys() (core/state/access_events.go:71-83, new in this PR) reconstructs the binary-trie key for a storage slot from bintrie.StorageIndex()'s (treeIndex, leafKey) pair by taking treeIndex.Bytes32()[1:] as the 31-byte tree-index. This does not match trie/bintrie.GetBinaryTreeKeyStorageSlot() (used by BinaryTrie.GetStorage/UpdateStorage/DeleteStorage to actually read/write the slot), which builds its tree-index via a naive byte-copy that drops slot byte 30 instead of shifting right by 8 bits. For essentially any real (keccak-derived) storage slot the two derivations diverge, so the witness/access-key set built by Keys() points at the wrong trie location. Fix by making GetBinaryTreeKeyStorageSlot use the same arithmetic right-shift-by-8 as StorageIndex (or vice versa, whichever matches the EIP-6800 spec) so both paths agree. Currently unreachable since the binary-trie/EIP-4762 fork is dormant on all Bor networks, so this does not block merge, but must be fixed before that fork could ever activate.

Extended reasoning...

What the bug is: AccessEvents.Keys() (core/state/access_events.go:71-83) is new code in this PR that reconstructs a binary-trie key from a chunkAccessKey (an (addr, treeIndex, leafKey) triple recorded by SlotGas/bintrie.StorageIndex()). It does this by taking chunk.treeIndex.Bytes32()[1:] as the 31-byte tree-index portion of the key offset and chunk.leafKey as the final suffix byte, then calling bintrie.GetBinaryTreeKey(addr, offset).

The problem is that this reconstruction does not match the key actually used to store and retrieve the same storage slot in the trie. trie/bintrie.GetBinaryTreeKeyStorageSlot() — the function BinaryTrie.GetStorage, UpdateStorage, and DeleteStorage (trie/bintrie/trie.go:239, 282, 305) actually call to touch storage — builds its 31-byte tree-index stem via a naive byte-copy: k[0]=1; copy(k[1:31], slot[0:30]); k[31]=slot[31]. This silently drops slot[30] and effectively computes tree_index = 2^240 + (slot >> 16).

bintrie.StorageIndex() (the function that feeds Keys() via SlotGas), on the other hand, does the spec-correct EIP-6800 arithmetic: tree_index = 2^240 + (slot >> 8), extracting the suffix as slot[31].

Why they diverge: the two derivations only agree when slot < 256 (i.e., when byte 30 and everything above it in slot >> 8 is zero and shifting by 8 vs 16 makes no observable difference). For any storage slot at or above 256 — which covers virtually every real Solidity mapping/array slot, since those are keccak256 outputs — the two functions produce completely different 32-byte trie keys. This was verified concretely by multiple independent checks: e.g. for slot 0x10000, the direct GetBinaryTreeKeyStorageSlot key and the Keys()-style reconstruction via StorageIndex differ entirely, and one verifier found the exact off-by-one-byte signature (the storage key for slot 0x10000 equals the witness key for slot 0x100).

Why nothing currently catches this: AccessEvents was switched in this PR from the old verkle-tree utils.StorageIndex/PointCache derivation (which was presumably consistent with the verkle trie's own key derivation) to the new bintrie package. The pre-existing GetBinaryTreeKeyStorageSlot function was not updated to match, and no test in this PR exercises Keys() against the trie's actual storage locations for a realistic (large) slot value — the only test that exists is a narrowly-scoped unit test, and slots below 256 happen to accidentally agree.

Impact: Under EIP-4762/binary-trie execution, AccessEvents.Keys() is used to build the state-access witness key set (used for gas metering and stateless-witness verification). With this bug, it would report a bogus key for essentially every non-trivial storage slot access instead of the slot's real trie location, corrupting witness generation for any block that touches ordinary contract storage.

Concrete proof (byte-level walkthrough) for slot = 0x10000:

  1. StorageIndex(0x10000) computes treeIndex = 2^240 + (0x10000 >> 8) = 2^240 + 0x100. As a 32-byte big-endian value, this has 0x01 at byte index 29 (counting from 0), matching 0x100 sitting two bytes from the end.
  2. Keys() takes treeIndex.Bytes32()[1:] (bytes 1..31, i.e. effectively re-indexing bytes 1..30 of the original 32-byte value onto offset[0..29]) — the 0x01 lands at offset index 29, hash input becomes [..., 0x01, 0x00] with leafKey (slot[31]=0x00) appended as offset[31].
  3. GetBinaryTreeKeyStorageSlot(addr, 0x10000) instead copies slot[0:30] directly into k[1:31] and overwrites k[31]=slot[31]. For slot 0x10000 (32-byte big-endian: zeros except byte 29 = 0x01), this puts 0x01 at byte index 30 of the key offset (not 29), because it never shifts — it's a literal byte copy of the original slot bytes.
  4. The two 32-byte offset values passed into GetBinaryTreeKey(addr, offset) therefore differ (0x01 at position 29 vs. position 30), producing two entirely different sha256-derived 32-byte trie keys.

Fix: Align the two derivations — either make GetBinaryTreeKeyStorageSlot perform the same slot >> 8 arithmetic shift that StorageIndex uses (the EIP-6800-correct approach), or make Keys()/StorageIndex mirror whatever GetBinaryTreeKeyStorageSlot actually does. Either way, the witness-key derivation path and the actual trie storage/read path must agree bit-for-bit.

Comment thread core/vm/instructions.go
Comment on lines +1025 to +1050
func opDupN(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
code := scope.Contract.Code
i := *pc + 1

// If the immediate byte is missing, treat as 0x00 (same convention as PUSHn).
var x byte
if i < uint64(len(code)) {
x = code[i]
}

// This range is excluded to preserve compatibility with existing opcodes.
if x > 90 && x < 128 {
return nil, &ErrInvalidOpCode{opcode: OpCode(x)}
}
n := decodeSingle(x)

// DUPN duplicates the n'th stack item, so the stack must contain at least n elements.
if scope.Stack.len() < n {
return nil, &ErrStackUnderflow{stackLen: scope.Stack.len(), required: n}
}

//The n‘th stack item is duplicated at the top of the stack.
scope.Stack.push(scope.Stack.Back(n - 1))
*pc += 1
return nil, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 opDupN, opSwapN, and opExchange (core/vm/instructions.go, added for EIP-8024) each re-implement the identical snippet for reading the opcode's immediate byte with an implicit-zero default (code := scope.Contract.Code; i := *pc + 1; var x byte; if i < uint64(len(code)) { x = code[i] }), including the same explanatory comment. Extracting this into a small helper (e.g. immediateByte(pc *uint64, scope *ScopeContext) byte) would remove the triplication and keep the PUSHn-style boundary convention in one place for future fixes.

Extended reasoning...

What the duplication looks like

core/vm/instructions.go adds three new opcode handlers for EIP-8024: opDupN (~lines 1025-1050), opSwapN (~lines 1052-1079), and opExchange (~lines 1081-1111). Each begins with the byte-for-byte identical block:

code := scope.Contract.Code
i := *pc + 1

// If the immediate byte is missing, treat as 0x00 (same convention as PUSHn).
var x byte
if i < uint64(len(code)) {
    x = code[i]
}

This is the "read the immediate operand byte following the opcode, defaulting to 0x00 if the code buffer runs out" convention borrowed from the existing PUSHn opcodes, but here it's freshly copy-pasted three times in the same PR rather than factored into a shared helper.

Why this is worth flagging

Each of the three handlers also carries its own opcode-specific range check (x > 90 && x < 128 for DUPN/SWAPN vs x > 79 && x < 128 for EXCHANGE) and its own *pc += 1 increment, which legitimately differ per-opcode and are not part of this finding. Only the immediate-byte-read snippet itself — five identical lines plus an identical comment — is duplicated three times. Because the read logic and its boundary convention now exist in three copies, any future correction to that convention (e.g. a boundary/off-by-one fix, or a change in how out-of-bounds immediates are handled) has to be applied identically in three places, and it's easy to update one copy and miss the others.

Suggested fix

Extract the read into a small helper, e.g.:

// immediateByte returns the opcode's immediate byte at pc+1, or 0x00 if the
// code buffer doesn't extend that far (same convention as PUSHn).
func immediateByte(pc *uint64, scope *ScopeContext) byte {
    code := scope.Contract.Code
    i := *pc + 1
    if i < uint64(len(code)) {
        return code[i]
    }
    return 0
}

and call x := immediateByte(pc, scope) at the top of each of opDupN, opSwapN, and opExchange, leaving each opcode's own range check and *pc += 1 untouched. This is a pure refactor — the read logic and its behavior are unchanged, only its location — so there's no risk of altering opcode semantics. Since this is a tiny, inlinable helper on a hot execution path (the EVM interpreter loop), there's no meaningful performance concern either; the Go compiler will happily inline it.

Step-by-step verification

  1. Open core/vm/instructions.go and locate opDupN: it contains code := scope.Contract.Code; i := *pc + 1; var x byte; if i < uint64(len(code)) { x = code[i] } with the comment "If the immediate byte is missing, treat as 0x00 (same convention as PUSHn)."
  2. Locate opSwapN immediately below it: the identical five-line block and identical comment appear again, verbatim.
  3. Locate opExchange immediately below that: the same five-line block and comment appear a third time.
  4. Diff the three blocks against each other — they are character-for-character identical, confirming genuine triplication rather than superficially similar code.
  5. Confirm the surrounding code (range checks, *pc += 1) differs per-opcode, so only the immediate-byte-read logic itself is the duplicated unit, and it is exactly what a single immediateByte helper would replace.

This is scoped purely to code quality — reuse and maintainability — with no functional or security implication, so it should not block merging this PR.

Comment on lines 1526 to 1546
addStaleMeter.Mark(1)
case errors.Is(err, core.ErrNonceTooHigh):
addGappedMeter.Mark(1)
// Store the tx in memory, and revalidate later
from, _ := types.Sender(p.signer, tx)
allowance := p.gappedAllowance(from)
if allowance >= 1 && len(p.gapped) < maxGapped {
p.gapped[from] = append(p.gapped[from], tx)
p.gappedSource[tx.Hash()] = from
log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from]))
return nil
} else {
// if maxGapped is reached, it is better to give time to gapped
// transactions by keeping the old and dropping this one.
// Thus replacing a gapped transaction with another gapped transaction
// is discouraged.
log.Trace("no gapped blob queue allowance", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from]))
}
case errors.Is(err, core.ErrInsufficientFunds):
addOverdraftedMeter.Mark(1)
case errors.Is(err, txpool.ErrAccountLimitExceeded):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The gapped-blob admission check at blobpool.go:1532 (len(p.gapped) < maxGapped) caps the number of distinct sender addresses with a buffered gapped tx, not the total number of buffered transactions as the maxGapped doc comment promises (len(p.gappedSource) is the true total and is never checked). Since gappedAllowance() grants up to maxTxsPerAccount (16) gapped slots per high-nonce address, an attacker with up to 128 funded, elevated-nonce accounts can buffer well beyond the intended 128 total gapped blob transactions in memory (each holding a full blob sidecar) for up to gappedLifetime (1 minute). Fix by checking len(p.gappedSource) < maxGapped instead of len(p.gapped) < maxGapped.

Extended reasoning...

The bug: In BlobPool.addLocked() (core/txpool/blobpool/blobpool.go:1526-1546), when a blob transaction fails validation with core.ErrNonceTooHigh, it is buffered into the in-memory reorder queue p.gapped[from] under the guard:

if allowance >= 1 && len(p.gapped) < maxGapped {
    p.gapped[from] = append(p.gapped[from], tx)
    p.gappedSource[tx.Hash()] = from
    ...
}

p.gapped is declared as map[common.Address][]*types.Transaction, so len(p.gapped) counts the number of distinct sender addresses that currently have at least one gapped transaction — not the total number of buffered transactions. The pool also maintains p.gappedSource map[common.Hash]common.Address, which is incremented once per transaction and therefore holds the true total count, but this map is never consulted by the admission check.

Why this bypasses the intended limit: The constant's own doc comment states: maxGappedTxs is the maximum number of gapped transactions kept overall. This is a safety limit to avoid DoS vectors. The code does not enforce that invariant — it enforces a cap on distinct addresses instead. Per-address, gappedAllowance() (blobpool.go:1977 area) computes min(int(math.Log10(nonce+1)), maxTxsPerAccount-len(index[addr])) - len(gapped[addr]), where maxTxsPerAccount is 16. This means an address with a sufficiently high on-chain nonce (e.g. nonce ≥ 1000 gives allowance 3, nonce ≥ 100000 gives allowance 5, up to a cap of 16) can have multiple gapped transactions queued simultaneously, each retaining its full blob sidecar in memory (up to ~768KB for 6 blobs) since blob sidecars are only stripped after successful pool insertion via WithoutBlobTxSidecar().

Step-by-step proof:

  1. Attacker controls 127 funded accounts (just under maxGapped = 128), each with an on-chain nonce ≥ 100000 (cheap to reach on Bor given low gas fees and high throughput).
  2. For each account, gappedAllowance() returns up to 5 (or higher for higher nonces, capped at 16).
  3. Attacker sends 5 nonce-gapped blob transactions per account (skipping the immediate next nonce so they hit ErrNonceTooHigh).
  4. Each insertion checks len(p.gapped) < 128 — since p.gapped only grows by one new key per first gapped tx from a given address, all 5 txs per account are accepted under the same address-count budget.
  5. Total buffered blob transactions: 127 × 5 = 635, or up to 127 × 16 ≈ 2032 in the worst case — far beyond the documented cap of 128 — each holding a full blob sidecar in memory for up to gappedLifetime (1 minute).

Why existing code doesn't prevent it: The check only reads len(p.gapped), never len(p.gappedSource), so nothing in the admission path actually bounds the total transaction count; it only bounds the number of addresses participating in the gapped-reorder buffer.

Impact: This is a memory-amplification gap relative to the documented safety limit — an externally-triggerable path (blob txs arrive from the p2p network) that lets an attacker with a moderate number of funded, high-nonce accounts inflate blobpool memory usage well past the intended 128-transaction bound, though it is still bounded (by address count × per-account cap) and self-heals after the 1-minute gappedLifetime eviction window, and requires funding many accounts with elevated nonces to become meaningful.

Fix: Change the guard at blobpool.go:1532 to check the true total count:

if allowance >= 1 && len(p.gappedSource) < maxGapped {

This is a one-line, low-risk fix that aligns the enforcement with the documented invariant.

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.