core, miner, consensus/bor, eth, triedb/pathdb: pipelined state root computation for block import#2180
core, miner, consensus/bor, eth, triedb/pathdb: pipelined state root computation for block import#2180pratikspatil024 wants to merge 70 commits into
Conversation
…oved the post tx execution buffer time
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
Code ReviewFound 6 issues: 4 bugs and 2 security concerns. Bugs
Security Concerns
|
Code ReviewFound 5 issues in miner/worker.go and miner/pipeline.go. Checked for bugs and CLAUDE.md compliance. 1. Bug: writeElapsed always ~0ns (miner/worker.go L1116-L1123) writeElapsed := time.Since(writeStart) is computed immediately after writeStart := time.Now(), before either WriteBlockAndSetHeadPipelined or WriteBlockAndSetHead executes. writeBlockAndSetHeadTimer always records ~0, and workerMgaspsTimer (line 1148) reports inflated MGas/s. Fix: move writeElapsed := time.Since(writeStart) to after the if/else block. 2. Bug: nil pointer dereference (miner/pipeline.go L379-L384) When chainHead is nil, the || short-circuits into the if-body, but chainHead.Number.Uint64() in log.Error dereferences nil and panics. Per CLAUDE.md: No panics in consensus, sync, or block production paths. Fix: split into two if-checks. 3. Bug: unchecked type assertion (miner/pipeline.go L335-L341) borEngine, _ := w.engine.(*bor.Bor) discards the ok boolean. If w.engine is not *bor.Bor, borEngine is nil and borEngine.AssembleBlock(...) panics. The same assertion at line 96 correctly checks ok. Fix: check ok and return early. 4. Bug: goroutine leak on 5 return paths (miner/pipeline.go L293-L345) initialFillDone channel (line 293) goroutine is not drained on return paths at lines 345, 357, 371, 373, 383. Only WaitForSRC error (line 331) and happy path (line 390) drain it. Fix: defer drain after line 293. 5. Bug: trie DB race after SpawnSRCGoroutine (miner/pipeline.go L206-L229) SpawnSRCGoroutine called at line 213 launches a goroutine doing CommitWithUpdate. If StateAtWithFlatDiff fails (line 219) or GetHeader returns nil (line 228), fallbackToSequential does IntermediateRoot inline on the same parent root concurrently. The comments at lines 206-211 identify this as causing missing trie node / layer stale errors but only guard the Prepare() case. Fix: WaitForSRC() before fallbackToSequential, or move spawn after preconditions. |
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (45.88%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## develop #2180 +/- ##
===========================================
+ Coverage 54.07% 54.09% +0.01%
===========================================
Files 907 911 +4
Lines 162012 165335 +3323
===========================================
+ Hits 87615 89442 +1827
- Misses 68984 70380 +1396
- Partials 5413 5513 +100
... and 20 files with indirect coverage changes
🚀 New features to boost your workflow:
|
I am okay with the idea of removing the remaining 100ms. We already reduced this buffer from 500ms to 100ms in v2.7.1, and from what we have seen so far, this remaining time looks small enough that removing it seems reasonable. My main concern is not the removal of the 100ms itself. My concern is the cost of pipelining SRC with the next block production. In other words: by doing SRC in parallel with block building, how much do we impact SRC time itself? Do we expect SRC to remain roughly the same, or does it become meaningfully slower because it is now competing with the next block production? That is the part I would like to understand better. I think this is basically a TPS vs finality question:
So I am supportive of the direction, but I think the key question is still: How much TPS do we gain, and how much finality do we lose, if any, by making SRC fully pipelined with block production? If the impact on SRC time is only slight, then the tradeoff is probably clearly worth it. But if SRC time increases materially once it is pipelined with block production, then we should make that tradeoff explicit |
I think SRC will be roughly the same, because the time consuming part, trie nodes prefetching, is already running at the same time with tx execution today, and this PR doesn't change this behavior. |
…r block import Overlap SRC(N) with execution of block N+1 on importing/RPC nodes. After executing block N, defer IntermediateRoot + CommitWithUpdate to a background SRC goroutine and immediately proceed to block N+1 using a FlatDiff overlay for state reads. Cross-call persistence allows the SRC to run across insertChain boundaries. Key changes: - Pipeline path in insertChainWithWitnesses with ValidateStateCheap - FlatDiff overlay in StateAt, StateAtWithReaders, PostExecutionStateAt - Path DB reader chained fallback for concurrent layer flattening - Trie-only reader for SRC witness generation (no flat reader bypass) - WIT handler waits for pipelined witness before returning empty - WitnessReadyEvent for announcing witnesses to stateless peers - PropagateReadsTo in checkAndCommitSpan for witness completeness - Feature gated: --pipeline.enable-import-src
Adds TestPipelinedImportSRC_SelfDestruct to verify that the FlatDiff Destructs check in getStateObject correctly handles self-destructed contracts during pipelined import.
Two fixes for prefetcher errors during pipelined state root computation:
1. Storage root mismatch: FlatDiff accounts had storage roots from block
N's post-state, but the prefetcher's NodeReader was at the committed
parent root (grandparent). Add prefetchRoot field to stateObject that
stores the grandparent's storage root, read from the flat state reader
when loading from FlatDiff. Use it consistently across all prefetcher
interactions.
2. Layer stale during trie node resolution: SRC's cap() flattens diff
layers concurrently with prefetcher trie walks. Add nodeFallback to
reader.Node(), mirroring the existing accountFallback/storageFallback
pattern — retries via the current base disk layer on errSnapshotStale.
A series of fixes for pipelined SRC under EIP-2935/BLOCKHASH aborts and
abort-heavy devnet load:
1. Skip pipeline pre-Rio.
Pre-Rio speculative Prepare walks unsigned speculative headers and can hit
ecrecover failures on zero-seal Extra data. Disable pipelined SRC before
Rio so the miner stays on the safe sequential path there.
2. Move slot waiting fully to Seal and keep abort rebuilds in-slot.
The miner now always builds block bodies early and uses the slot for tx
selection, while Bor holds propagation until the target time in Seal().
Abort-recovery headers carry a miner-local AbortRecovery flag so late
speculative rebuilds stay in-slot instead of getting pushed to the next
slot by minBlockBuildTime.
3. Isolate block-build timeout state per build environment.
Sequential builds and speculative fills previously shared a worker-global
timeout flag, so one build's timer could interrupt another build's tx
selection. Move timeout state onto each environment and make timer cancel
stop the timer without poisoning the build as timed out.
4. Improve speculative fill behavior and fix DAG metadata on refill.
Speculative blocks now take a late refill pass when they are still under
about 75% full by gas and there is at least 300ms left before the slot,
not only when fully empty. Keep tx dependency DAG state on the block
environment across refill passes so multi-pass speculative fills do not
restart dependency indices from zero and drop metadata with
non-sequential transaction index errors.
5. Harden abort recovery and mined-block propagation.
After speculative aborts, requeue normal work through the standard worker
path instead of re-entering commitWork recursively. On the networking
side, mined inline blocks now still announce correctly when witness data
is already cached but the async block write is not yet visible in the DB.
6. Add regression coverage and clean up logs.
Add tests for Bor timing behavior, speculative refill decisions,
per-build interrupt isolation, DAG metadata persistence across refill
passes, cached-witness announcement, and BLOCKHASH(N) abort-flag
behavior. Also remove duplicate EIP-2935 abort logs and fix negative
seal-delay logging so slightly-late blocks no longer print huge wrapped
unsigned delays.
Wires a complete metrics suite for A/B comparing pipelined vs non-pipelined
import and block production on mainnet.
New pipelined metrics (import):
- chain/imports/pipelined/{hit,miss,root_mismatch,enabled}
- chain/imports/witness_ready_end_to_end — apples-to-apples end-to-end timer,
fires in both modes (primary A/B KPI)
New pipelined metrics (build):
- worker/pipelineSpeculativeCommitted, pipelineSRCWait, pipelineSealDuration
- worker/pipelineAnnounceEarlinessMs (signed ms — PIP-66 earliness signal)
- worker/pipelineSpeculativeAborts/{blockhash,src_failed,fallback}
- worker/build_to_announce — producer-side end-to-end, both modes
- worker/pipeline/enabled
Parity wiring for legacy metrics so dashboards work in both modes:
- chain/inserts, account/storage read + hash + update + commit timers,
snapshot/triedb commits, stateCommitTimer, blockBatchWriteTimer,
witnessEncode/DbWrite — emitted from the pipelined branch (main statedb or
SRC goroutine's tmpDB as appropriate)
- worker/writeBlockAndSetHead — emitted from inlineSealAndBroadcast's async
write goroutine
- pipelineAnnounceEarlinessMs and pipelineSpeculativeCommittedCounter also
emitted from resultLoop for the sealBlockViaTaskCh path
Throughput and overlay observability:
- chain/{gas_used_per_block,txs_per_block,mgasps} + chain/witness/size_bytes
- worker/chain/{gas_used_per_block,txs_per_block}
- state/flatdiff/{account_hits,storage_hits} — FlatDiff overlay effectiveness
Metrics that have no clean pipelined semantic (chain/validation, chain/write,
worker/commit, worker/finalizeAndAssemble, worker/intermediateRoot) are left
unemitted in pipelined mode with inline comments documenting the reason and
pointing to the closest pipeline equivalent.
Code ReviewFound 3 issues in
|
Inline Review CommentsSince inline comments could not be posted via the review API, here are the detailed findings with line references: Issue 1 (HIGH): Missing The non-pipelined path (lines 3618-3631) calls Fix: Add the same per-block CLAUDE.md: blockchain-security.md and consensus-security.md Issue 2 (HIGH):
Fix: Acquire CLAUDE.md: security-common.md — "Shared mutable state protected by mutex or atomic operations" Issue 3 (HIGH):
Fix: Replace CLAUDE.md: security-common.md — "Error values checked — never discard errors with _ in security-sensitive paths" |
…ions for diffguard compliance
Decompose large pipelined-src-authored functions into focused helpers so
every function owned by this branch sits under diffguard's 50-line /
complexity-10 limits. Pure structural refactor — no behavior change.
miner/pipeline.go:
- commitSpeculativeWork (599) → orchestrator (35) + specSession struct
with ~18 methods (setupInitial, waitForSRCAndSealBlockN, runOneIteration,
prepareNextIteration, sealCurrentAndAdvance, shiftToNext, etc.)
- inlineSealAndBroadcast (100) → 35 + sealViaPrivateChannel,
rebindReceiptsToSealedBlock, announceInlineSealedBlock
- commitPipelined (59) → 37 + buildSpeculativeReq, spawnSRCForFinalBlock
- sealBlockViaTaskCh (52) → 48 (reuses spawnSRCForFinalBlock)
miner/worker.go:
- fillTransactions (59) → 47 + commitTxMaps
- makeEnv (51) → 38 + resolveStateFor
- updateTxDependencyMetadata (68) → 32 + buildTxDependencyArray
Pre-existing develop functions where pipelined-src had grown the body
are reduced back close to or below their develop size by extracting the
added branches:
- commitWork (67 → 36) via clearPendingWorkOnExit + maybeStartPrefetch
- resultLoop (191 → 124; develop was 123) via emitExecutionMetrics,
emitCommitMetrics, writeTaskBlock, announceTaskBlock
- mainLoop (135 → 120; develop was 116) via handleSpeculativeWork
- buildAndCommitBlock (93 → 83; develop was 80) via submitForSealing
core/state/statedb.go:
- CommitSnapshot (95, complexity 40) → 30 + captureMutation,
captureObjectStorage, captureReadOnlyAccount, captureNonExistentRead
- ApplyFlatDiffForCommit (49, complexity 20) → 16 + applyFlatMutation
- ApplyFlatDiff (36, complexity 11) → 13 + applyFlatAccountOverlay
- TouchAllAddresses (25, complexity 11) → 12 + touchAddressAndStorage,
mutatedStorageKeys
core/blockchain.go:
- SpawnSRCGoroutine (127, complexity 35) → 13 + runSRCCompute,
openSRCStateDB, preloadFlatDiffReads, emitSRCStateDBMetrics,
encodeAndCachePendingWitness
- writeBlockAndSetHeadPipelined (108, complexity 29) → 16 +
writePipelinedBlockBatch, writeBorStateSyncLogs, resolveWriteStatus,
emitPipelinedWriteEvents
- handleImportTrieGC (52, complexity 16) → 21 + capTrieIfDirty,
maybeFlushChosen, dereferenceUpTo
- waitForPipelinedWitness (complexity 11) → 9 + waitForPendingSRCWitness,
pollWitnessCache
core/evm.go:
- SpeculativeGetHashFn (complexity 12) → 17 + newPendingBlockNResolver
core/blockchain.go insertChainWithWitnesses pipelined branch (had grown
+222 lines on top of develop's 452) → +42 via buildPipelineImportOpts,
persistPipelinedImport, collectPrevImportSRCIfAny, emitStateSyncFeed,
runImportAutoCollection, verifyImportSRCRoot, publishImportWitness,
emitPipelinedImportParityMetrics.
core/blockchain.go ProcessBlock pipelined branches (+22 lines) → +4
via pipelineReaderRoot, applyFlatDiffOverlayToAll, validateStateForPipeline.
eth/peer.go:
- doWitnessRequest (pipelined-src pushed from 38 → 65) → 32 +
awaitWitnessResponse extracting the goroutine body
eth/handler_wit.go:
- handleGetWitness (pipelined-src pushed from 70 → 91) → 66 +
resolveWitnessSizes consolidating per-hash size resolution (rawdb +
header-existence DoS guard + SRC cache fallback)
tests/bor/helper.go:
- InitMinerWithPipelinedSRC (65) → 32 + newPipelineTestNode (17),
importValidatorKey (11)
- InitImporterWithPipelinedSRC (64) → 31 (same helpers)
Mutation coverage. Ran diffguard in diff-scoped mode (-base develop
-include-paths <module>) across every module pipelined-src touches and
filled the gaps it surfaced:
- core/state: adds core/state/statedb_pipeline_mutations_test.go with 41
targeted tests that kill 24 of 28 mutation survivors in pipelined-src
FlatDiff code (statedb.go lines 2031-2330, 2492-2499). The 4 remaining
are equivalent mutants — Finalise removes destructed addrs before the
guarded branches can fire (2114, 2163), a zero-length loop produces
the same output with or without the guard (2141), and an empty-slice
map entry is observationally equivalent to a missing entry (2150).
Covers CommitSnapshot and its capture helpers, ApplyFlatDiff +
applyFlatAccountOverlay, ApplyFlatDiffForCommit + applyFlatMutation,
NewWithFlatBase, TouchAllAddresses + touchAddressAndStorage +
mutatedStorageKeys, WasStorageSlotRead, and PropagateReadsTo — 14 of
15 functions at 100% line coverage (captureReadOnlyAccount at 90.9%).
- core/stateless: extends witness_test.go with 3 tests targeting
ValidateWitnessPreState's expectedBlock guard (ParentHash and Number
checks that defend against a malicious peer substituting a witness
for a different block / fork). Previous tests all passed nil for
expectedBlock, leaving the entire anti-forgery branch uncovered.
- eth/filters: adds TestResolveBlockNumForRangeCheck and
TestCheckBlockRangeLimit (16 subcases) to api_test.go covering the
RPC range-limit DoS guard at the unit level (sentinel resolution,
span-at-limit boundary, sum-vs-span distinction). Extends
TestInvalidGetRangeLogsRequest in filter_system_test.go to also
exercise GetBorBlockLogs with an inverted range — previously only
GetLogs was covered.
Per-module mutation scores after this coverage: miner 96%, consensus/bor
100% (41/41), core 100% (447/447), core/state 86% (24/28 equivalent),
core/stateless 100% (8/8), core/txpool 100% (12/12), tests/bor 100%
(43/43), triedb/pathdb 100% (20/20). eth at 53% — remaining survivors
are in auto-generated gen_config.go boilerplate (36), P2P dispatcher
cancel-channel plumbing awaitWitnessResponse goroutine cleanup
(3); documented as accepted gaps requiring complex mock infrastructure
for diminishing security return.
Remaining diffguard violations in miner and core are pre-existing
develop functions (commitTransactions, insertChainWithWitnesses,
newWorkLoop, NewBlockChain, ProcessBlock, writeBlockWithState, etc.)
that were over threshold on develop before pipelined-src. Their
pipelined-src deltas are now small (+1 to +42 lines) and out of scope
for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames (reviewer nits):
- PostExecutionStateAt → PostExecState (BlockChain + txpool/legacypool/
blobpool interfaces + test mocks).
- ResetSpeculativeState → SetSpeculativeState, SpeculativeResetter →
SpeculativeSetter (the method overwrites, it doesn't revert).
Dedup between writeBlockAndSetHead and writeBlockAndSetHeadPipelined.
Both paths now share resolvePostWriteStatus(block, stateless) for
fork-choice + reorg (stateless flag preserves the errInvalidNewChain
escape for fast-forward sync), emitPostWriteEvents for the feed sends,
and writeBorStateSyncLogs for the pre-Madhugiri bor receipt. ~80 lines
of duplicated fork-choice + event logic removed; writeBlockAndSetHead
drops from ~70 to ~11 lines. Batch bodies intentionally not merged —
witness source (statedb.Witness() vs pre-encoded bytes) and trie-commit
timing genuinely differ.
Miner coinbase unification: extracted resolveCoinbase(blockNumber,
fallback) used by makeHeader (fallback=genParams.coinbase) and the
speculative header builders (fallback=etherbase()). Divergence between
the speculative and real header would cause a state root mismatch, so
single-sourcing this is security-meaningful. Rest of buildInitialSpecHeader
kept separate from makeHeader (placeholder parent, deterministic bor
period timestamp, static GasCeil, no engine.Prepare); comment documents
why unifying further would hurt readability.
Pipelined import correctness fixes (core/blockchain.go):
1. persistPipelinedImport now runs the Heimdall milestone/checkpoint
ValidateReorg guard before writeBlockAndSetHeadPipelined — mirrors
the non-pipelined path. Without it, a milestone whitelisted during
block execution could be bypassed.
2. verifyImportSRCRoot wraps its writeHeadBlock revert in chainmu
TryLock/Unlock. The call ran in the auto-collection goroutine
without the mutex, racing any concurrent InsertChain on head state.
Skips + warns if chainmu is closed (shutdown).
3. flushPendingImportSRC error in insertChain's ProcessBlock error
path no longer discarded with `_`; logged like the other two call
sites.
Linter: dropped two `tc := tc` loop-var copies in eth/filters/api_test.go
(copyloopvar, redundant since Go 1.22).
|
…port Verify the read-only RPC surface on a node importing with pipelined SRC, and close the one gap found: handlers that open tries directly by root (eth_getProof, debug_storageRangeAt) failed transiently when queried at the head whose SRC hadn't committed yet, since proofs need trie nodes the FlatDiff overlay cannot provide. - WaitForPipelinedStateCommit: bounded wait on the pending import's collectedCh; no-op for any root other than the pending head's. Wired into eth_getProof (via a new ethapi.Backend method) and debug_storageRangeAt, converting the transient error into a few ms of latency. Overlay-served value reads are untouched. - core/pipelined_window_test.go: holds the SRC goroutine open via a test-only hook to pin window semantics deterministically — overlay reads correct, raw trie opens fail, wait gate blocks then releases, post-settle values identical, proofs verify. Both schemes, race-clean. - tests/bor/pipelined_rpc_test.go: BP -> pipelined importer sync with a live tx stream; strict 12-method battery at pinned heights and latest (getProof now strict), sync-time vs settled response identity, full per-height parity vs a non-pipelined BP incl. cryptographic account proof verification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict: consensus/bor/bor.go — #2314 extracted commitSprintWork and our branch added FinalizeForPipeline/AssembleBlock at the same location; both kept verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@claude review |
There was a problem hiding this comment.
No new bugs found this run. Given the scale and consensus-critical nature of this pipelined state-root PR (73 files, block-import head-advance-before-root-verify, witness protocol changes), this still needs human sign-off rather than a shadow approval.
What was reviewed: the FlatDiff/PostExecState overlay and prefetch-root consistency paths, the pipelined-import auto-collection/root-verification flow, and the WIT-protocol witness-serving changes (resolveWitnessSizes, GetWitness pipelined wait, minedBroadcastLoop goroutine lifecycle, warm-snapshot Verkle handling) flagged by Copilot — none of these rose to a confirmed bug: the witness-wait/size-resolution paths are bounded by the header-existence + 64-block recency gate already added, and the goroutine-lifecycle/Verkle concerns didn't reproduce a concrete failure on inspection.
Extended reasoning...
No bugs were found by the bug-hunting system in this run. This PR (pipelined SRC for block import) is large (73 files) and touches consensus-critical paths: block head advancement before state-root verification, FlatDiff overlay masking for txpool/RPC reads, witness generation/serving over the WIT protocol, and prefetcher root handling. Prior runs found several real bugs (missing ValidateReorg guard, writeHeadBlock without chainmu, discarded flushPendingImportSRC error, nil-deref in pipeline.go, stale witness header copy) which the author has since fixed per their replies in the thread.
This run's bug hunters specifically re-examined the four still-open Copilot findings (resolveWitnessSizes read-before-budget-check and cache pollution, GetWitness's unbounded wait, minedBroadcastLoop's untracked goroutine, warm-snapshot behavior under Verkle) and did not confirm any of them as bugs beyond what the author already addressed via the header-existence/64-block-recency gate. That gate bounds the size-resolution and wait paths to a single in-flight SRC block rather than arbitrary hashes, which is why these did not reproduce as a real DoS or correctness issue on inspection.
Given the severity classification in this repo's own security rules (peer/RPC-triggerable issues in consensus/sync paths are escalated), and the sheer surface area of this diff (state package internals, bor consensus timing, witness protocol, txpool interaction), a human reviewer with deep context on the pipelined-SRC design should make the final call, especially since this is a new consensus-adjacent subsystem being enabled by a feature flag for the first time.
Bring the new-code diffguard complexity violations under threshold and remove production-code duplication: - statedb: resolveFlatMutationObject extracted from applyFlatMutationFast; the two witness-collection loops in IntermediateRoot deduped into addObjectWitness/addWitnessNodes. - blockchain: preload read-surface histograms + preloadFlatDiffReads extracted from runSRCCompute into recordAndPreloadSRCWitnessReads. - bor: FinalizeForPipeline now reuses commitSprintWork (extracted on develop by #2314) instead of duplicating the sprint-start span and state-sync commits; this also sets BorConsensusTime on the pipelined path, matching FinalizeAndAssemble. Behavior-preserving; legacy-grown giants (insertChainWithWitnesses, IntermediateRoot, getStateObject, updateTrie) are declared as explicit deviations in the PR description. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 73 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
eth/handler_wit.go:143
- resolveWitnessSizes calls Chain().GetWitness(hash) to fetch witnesses when the persisted size index is missing. GetWitness() populates the chain witness cache (core/blockchain_reader.go:166-183) and may load a full witness before the per-request memory budget checks in handleGetWitness, so peer traffic can evict import-critical cache entries and can force large allocations even for requests that would later be rejected.
Consider adding an uncached “wait for pipelined witness” read path (e.g. extend GetWitnessUncached to also wait, but never cache), and use that here; alternatively, avoid prefetching full witnesses in size resolution and only fetch on-demand after passing the memory-budget guard.
- PostExecState: match the cached FlatDiff by state root in addition to block number (zero stored root — the miner's speculative path — keeps the number-only match), so a same-height reorg can't serve a stale overlay. - waitForPendingSRCWitness: cap the collectedCh wait so a stalled SRC goroutine can't pin peer/RPC handlers indefinitely. - resolveWitnessSizes: fetch via new GetWitnessUncachedWait (bounded SRC wait without inserting peer-driven reads into the witness cache) and cap retained prefetch bytes at MaximumResponseSize. - updateTrie: surface witness re-read GetStorage failures via setError instead of silently producing an incomplete witness. - minedBroadcastLoop: track the announceMinedBlock goroutine on h.wg so handler shutdown doesn't race peer close. - SetSpeculativeState: publish NewTxsEvent after releasing pool.mu, matching the regular promotion flow. - pipelined_rpc_test: assert fdlimit.Raise succeeds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 73 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
consensus/bor/bor_test.go:5120
- This subtest’s fixed 100ms lower bound can be flaky, and it doesn’t directly assert the intended property (that Prepare(waitOnPrepare=true) waits until the cached parent boundary). Prefer asserting that the call returns at/after parentActual (with parentActual set far enough in the future to tolerate CI variability).
require.NoError(t, err)
require.GreaterOrEqual(t, elapsed, 100*time.Millisecond)
require.NotZero(t, header.Time, "Prepare should still populate header time")
The three pipelined integration tests failed on CI with both nodes at zero peers for the full 60s window. Root cause: the dial scheduler dedupes static nodes by ID (addStaticCh handling ignores re-adds), so when the first AddPeer captures a port-0 enode — Self() publishes its TCP port asynchronously after the listener starts — the "self-healing" re-add loop never updates the record and the dialer keeps dialing a dead address forever. On top of that, a transiently failed dial parks the target in dial history for 35s, so a 60s deadline covers barely one retry. connectAndWaitForPeers now waits for both enodes to publish a real port before the first AddPeer and allows several history windows. Also addresses review findings: - bor_test: the waitOnPrepare subtests assert against the parent slot boundary instead of fixed elapsed-time bounds, so loaded runners can't flake them. - pathdb: nodeFallback goes straight to tree.bottom() — nodeWalk's primary attempt already is the entry-layer walk, so retrying it deterministically hits the same stale disk layer (unlike account/storage fallbacks, whose primary attempt is the lookup-index path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 73 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
tests/bor/pipelined_rpc_test.go:86
- Same as above: waiting for importer listener port without a timeout can hang the test indefinitely on startup failures. Please add a bounded wait.
- speculative_chain_reader: guard the parent-number subtraction in GetTd against a genesis parent (can't happen on the speculative path, but an underflow to MaxUint64 is worth a two-line guard). - pipelined_rpc_test: handle crypto.GenerateKey errors; drop the unbounded listener-wait loops — connectAndWaitForPeers now waits for published listener ports under a deadline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| // warmKey identifies a trie node by its containing trie's owner (zero for | ||
| // the account trie, account hash for storage tries), its path within the | ||
| // trie, and the node hash itself. The hash field disambiguates entries that | ||
| // share owner+path across different blocks/states. | ||
| // warmKey identifies a trie node by owner, path and content hash. It is a | ||
| // comparable value type built on the stack — a string-keyed variant would | ||
| // allocate on every Lookup, since Go's string(bytes) map-index optimization | ||
| // does not apply to struct-literal keys. MPT paths are at most 64 nibbles; | ||
| // longer paths cannot be produced by the trie and are simply not indexed | ||
| // (Lookup misses fall through to the underlying reader). |


Description
Overlaps state-root computation (SRC) of block N with transaction execution of block N+1 during block import. Execution of N+1 opens state at the last committed trie root plus an in-memory
FlatDiffoverlay of block N's mutations, while a background SRC goroutine commits block N and computes its root. Disabled by default — enabled with[pipeline] enable-import-src = true.Built on top of the delayed SRC PoC: instead of just deferring SRC, it pipelines SRC with the next block's work.
Measured impact
Mainnet full node (n2d-standard-16, path scheme, BlockSTM enforce), ~64k-block catch-up windows, block-weight and traffic-character controlled across adjacent weekday ranges:
producewitnessesproducewitnessesPer-block overhead beyond raw execution dropped from ~45–50ms to ~18ms. Zero
root_mismatch, pipeline fallbacks, or execution errors across the benchmark campaign (~500k blocks imported under the pipeline).How it works — import
When importing block N (pipeline active):
ValidateStateCheap(gas, bloom, receipt root — noIntermediateRoot)FlatDiffviaCommitSnapshot(~1ms, no trie hashing)StateAt/RPC reads (eth_call,eth_estimateGas, pending reads) are correct during the pipeline windowTwo supporting optimizations ship with the pipeline:
triedb/pathdb/lookup_nodes.go): a reference-counted(owner, path, hash) → blobmap over live diff layers.reader.Noderesolves in one probe instead of walking up to 128 diff layers per read (the walk measured ~44% of process CPU during catch-up). The hash is part of the key, so a hit is correct by construction; a definitive miss reads the disk layer directly. This index is always on (not gated by the pipeline flag) — it is a read-path cache whose misses fall through to the existing walk, adding ~250MB steady-state at 128 diff layers.core/state/warm_snapshot.go,pipeline.warm-snapshot, default on): when witnesses are produced, the execution-side trie prefetcher is detached and handed to SRC, which builds an immutable hash-verified snapshot of the loaded trie nodes and consults it before pathdb. No effect when witnesses are off.Review-sensitive properties
ValidateStateCheapgates insertion; the full root check happens asynchronously in the SRC goroutine. A mismatch fireschain/imports/pipelined/root_mismatch(a hard alarm that must stay 0) and errors the pipeline. Sync compensates:hasPendingPipelinedHeadStatekeeps the node in full-sync while a head-state commit is in flight.WitnessReadyEventnow push-announces witness availability to stateless peers (replacing a 10s poll);GetWitnessserving waits for in-flight SRC (bounded, with a header-existence DoS guard) and reads uncached so peer traffic can't evict import-critical cache entries.NewTrieOnly) so every read walks the MPT and proof paths land in the witness; parity tests assert proof-node-set equality and stateless replay (ProcessBlockWithWitnesses) across all configurations.PropagateReadsToincheckAndCommitSpancaptures the validator-contract proof nodes read via a copied statedb; EIP-2935 accounts are touched into the read set.bc.wg; pending SRC is flushed on reorg, gap, or error.BlockChainVersionunchanged); nothing forces a resync.Production-side (miner) pipelining: landed but disabled
miner/pipeline.gocontains the speculative-sealing counterpart (FlatDiff extraction afterFinalizeForPipeline, background SRC, speculative N+1 build, async chain write). It is hard-disabled:isPipelineEligiblereturnsfalseunconditionally and no config exposes it (theworker/pipeline/enabledgauge is always 0). Pre-Rio seal-recovery interaction makes speculativePreparefail; re-enablement is future work and the gating logic is preserved in comments. Reviewers can treat the miner path as dormant code with test coverage.Config
Notable negative results baked into the design (so nobody re-litigates them): the exec-side trie prefetcher and the speculative block prefetcher are load-bearing via shared-cache warming — disabling either regresses 25–75% — so neither is configurable (
pipeline.exec-prefetchexisted briefly on the feature branch and was removed).Executed tests
-race): pipelined vs sequential roots/hashes, witness proof-node-set parity + stateless replay, FlatDiff mutation/overlay suites, self-destruct integration, pathdb node-index unit tests (fork refcounting, deletion markers, definitive-miss semantics),tests/borintegration.core/pipelined_window_test.go, SRC goroutine held open via test hook) pins the overlay/trie semantics of the "head advanced, root not yet committed" interval; an end-to-end battery (tests/bor/pipelined_rpc_test.go) drives 12 read methods against a live-syncing importer at pinned heights andlatest, checks sync-time vs settled response identity, and verifies full per-height parity vs a non-pipelined BP including cryptographic account-proof verification. Handlers that need committed trie nodes (eth_getProof,debug_storageRangeAt) gate onWaitForPipelinedStateCommitinstead of erroring transiently inside the window.Quality gates — declared deviations
Three CI gates fail for structural reasons; each is intentional and listed here rather than suppressed:
insertChainWithWitnesses,IntermediateRoot,getStateObject, andupdateTrieexceed thresholds. All four are legacy hot-path giants that this PR extends in place; refactoring them is high-risk consensus-path churn that belongs in a dedicated cleanup, not a feature PR. New-code violations were extracted below thresholds (applyFlatMutationFast,runSRCCompute,FinalizeForPipeline, witness-collection loops deduped intoaddObjectWitness).nodeWalkis a rename of pre-existing upstream logic. The reportedcore -> coredependency cycle is a tool artifact (self-cycle).addObjectWitness,commitSprintWorkreuse);ValidateStateCheapintentionally mirrorsValidateState's shape.tests/bor, kurtosis). The 90% patch bar on a 12.7k-line PR spanning hot paths is not attainable without duplicating e2e coverage as unit tests.Rollout notes
Backwards-compatible; off by default; no coordinated upgrade required. Not consensus-affecting: the pipeline computes the identical root on the identical commit path, one block later in wall-clock, and aborts on mismatch. Operator-facing changes when enabled: witness-ready latency trails the block by one SRC cycle (~210ms end-to-end vs ~143ms inline). Related: #2312 implements an alternative trie-node index on develop; this PR lands first and #2312 rebases.