polygon/sync, stagedsync: add forkchoice finality changeset optimization#121
Open
madumas wants to merge 1 commit into0xPolygon:release/3.2from
Open
polygon/sync, stagedsync: add forkchoice finality changeset optimization#121madumas wants to merge 1 commit into0xPolygon:release/3.2from
madumas wants to merge 1 commit into0xPolygon:release/3.2from
Conversation
Add --experimental.use-forkchoice-finality flag that skips generating changesets for blocks finalized by Polygon milestones. This reduces sync overhead near the chain head by leveraging milestone-based finality instead of keeping changesets for the full MaxReorgDepth (512 blocks). How it works: - Track lastFinalizedBlockNum in the sync layer as milestones are validated (both at-tip and ahead-of-tip milestones) - In commitExecution(), resolve the finalized header post-flush: if tip <= lastFinalizedBlockNum, use newTip as finalized; otherwise fetch the milestone end block header from DB - Write finalized hash before execution stage runs so shouldGenerateChangeSets() can read it - Cache getFinalizedBlockNum before execution loop (1 DB read) - When UseForkchoiceFinality is enabled, skip changeset generation for blocks at or below the finalized block - Use aggressive pruning for finalized blocks, matching initial-sync behavior - Downgrade noisy "no unwindable block" warning to Debug WARNING: If finality is later reverted (e.g., faulty milestone purged by hard fork), the node will require a chaindata reset to recover.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Polygon PoS, Heimdall milestones provide deterministic finality: once a milestone is validated, the covered blocks can never be reorged. Today, erigon still generates changesets (undo data for reorgs) for these finalized blocks during
syncToTipand event loop processing. This is wasted work. On a typical sync cycle of 200+ blocks, ~95% are already finalized by milestones.This PR adds an opt-in
--experimental.use-forkchoice-finalityflag that skips changeset generation for blocks at or below the finalized milestone, and enables aggressive pruning for those blocks (matching initial-sync behavior).How it works
Sync layer (
polygon/sync/sync.go):lastFinalizedBlockNumas milestones are validated, both at-tip and ahead-of-tip. The value is updated monotonically viamax()to handle out-of-order processing.commitExecution(), resolve the best finalized header post-flush and pass it toUpdateForkChoice().lastFinalizedBlockNumfrom the latest waypoint ininitialiseCcb().syncToTipyields no waypoint (already at tip), fetch the latest milestone from Heimdall.Execution layer (
execution/stagedsync/exec3.go):getFinalizedBlockNum()reads the finalized hash from DB once before the execution loop (1 DB read, not per-block).shouldGenerateChangeSets()returnsfalsefor blocks <= finalized when the flag is enabled.GreedyPruneHistory) and don't trigger the "batch can be pruned" early break, allowing larger batches likeinitialCycle.Forkchoice (
execution/eth1/forkchoice.go):ForkchoiceFinalizedhash before the execution stage runs, soshouldGenerateChangeSets()can read it.Design decisions
Opt-in via
--experimental.use-forkchoice-finality. This is an aggressive optimization with an irreversible trade-off (see warning below). Theexperimentalprefix signals this.Finalized block cached once before execution loop. The finalized hash doesn't change mid-cycle (written in
forkchoice.gobefore execution starts), so one DB read is sufficient. The parallel executor (exec3_parallel.go) still reads per-call since it runs concurrently.Monotonic
lastFinalizedBlockNumviamax(). Milestones can arrive out of order (an at-tip milestone for block 1000 may be processed after an ahead-of-tip milestone for block 1012). Usingmax()prevents regressing the finalized mark.Aggressive pruning matches
initialCyclebehavior. When we skip changesets, commitment history accumulates faster. Matching the initial-cycle pruning strategy (longer timeout, greedy history prune) prevents long-term commit degradation."No unwindable block" warning downgraded to Debug. With fewer changesets generated, this warning fires frequently and is expected behavior, not an error condition.
Warning: chaindata reset required if finality is reverted
Without changesets, the node cannot unwind finalized blocks. If a milestone is later invalidated, the node will require a full chaindata reset to recover.
This is not a theoretical risk. On September 10, 2025, a faulty Heimdall milestone pushed Bor nodes onto divergent forks. The Polygon Foundation resolved it via hard fork (Bor v2.2.11-beta2, Heimdall v0.3.1) that deleted the faulty milestone from node databases. A node running with this flag during that incident would have needed a chaindata reset after the hard fork.
In practice, resetting chaindata on an archive node with snapshots takes minutes, and in such a scenario the node would be down anyway waiting for the hard fork binary. This is why the trade-off is acceptable as an opt-in flag.
Why not upstream?
This optimization is specific to Polygon's finality model. Heimdall milestones are a sidechain consensus mechanism that validates spans of blocks with deterministic finality. The plumbing here (
lastFinalizedBlockNumtracking, milestone-to-forkchoice bridging insync.go) is entirely Polygon-specific. The execution-layer changesshouldGenerateChangeSets, pruning) are generic but gated behind the flag, which has no effect without the Polygon sync layer populating the finalized block.