Conversation
…he new head The fork-choice decision and the XDPoS head side effects are needed in more than one place, so pull them out before the known-block adoption lands on top: - blockBeatsHead holds the rule writeBlockWithState applied inline (a higher total difficulty, or an equal one with a higher number), and writeBlockWithState now calls it, so any later known-block path can never adopt a chain under a rule an executed block would not be allowed to. - isGapBlock, notifyEpochSwitchBlock and cacheSigningTxs collect the side effects that were duplicated in insertChain, insertBlock, writeBlockWithState and reorg. - reorg no longer writes the new head itself: its callers do it after it returns. Doing it in both places wrote the same canonical markers twice and ran UpdateM1 twice for a gap block. The doc comment states the contract, and the stale marker cleanup starts one block lower to match. - insertSidechain is renamed to insertSideChain, as upstream did. No behaviour change: the extracted rule is the one that was applied inline, the helpers are the same code, and the new reorg contract is pinned by a test.
|
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 ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
🟡 Changes recommended
Known-block adoption omits ChainEvent, leaving new-head subscribers and chain indexers temporarily stale.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes stalled synchronization by allowing known blocks above the current head to advance the canonical chain.
Changes:
- Adds shared fork-choice and known-block adoption logic.
- Preserves reorg side effects, logs, caches, and XDPoS snapshots.
- Adds regression and consensus tests.
File summaries
| File | Description |
|---|---|
core/blockchain.go |
Implements known-block adoption and updates reorg handling. |
core/blockchain_test.go |
Tests fork choice, reorgs, logs, and gap detection. |
consensus/tests/engine_v2_tests/known_block_test.go |
Tests XDPoS snapshot restoration. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
d429084 to
d2593a8
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Log-delivery detection can both omit first-adoption logs and duplicate intermediate rollback logs.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
core/blockchain.go:1957
GetCanonicalHashreflects the header chain’s canonical table, not whether this full block was previously adopted and its logs delivered (core/blockchain_reader.go:237-239,core/headerchain.go:171-196). If header sync has already canonicalized an executed side fork before these known blocks advance the full-block head, this comparison is false and the block’s live logs are silently omitted. Please use full-block/event history for this distinction, or normalize rollback/reorg log semantics so every canonical transition can emit logs safely.
promoted := bc.GetCanonicalHash(block.NumberU64()) != block.Hash()
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
…inFinOrg#2535 Port geth ethereum#19417 (c113723). Its writeKnownBlock half is the fix for XinFinOrg#2535; the reorg and naming changes below come along from the same upstream commit. insertChain only skipped known blocks while they sat at or below the head. A block already stored with its state but sitting above the head left err as ErrKnownBlock, so the import loop, which requires err == nil, never ran and the head stayed where it was. Archive nodes hit this constantly: once a block has been executed and its state committed without being adopted, it stays known forever and every later attempt stops on it. On a catching-up mainnet node the head crawled two or three blocks per round while each round downloaded and rewrote a thousand, and the same range was fetched again and again. Adopt the upstream handling: - writeKnownBlock makes such a block the head. It reorgs when the block does not sit on the current head, then moves the head, and the import loop tolerates ErrKnownBlock instead of re-executing the block. A gap block reaching the head this way refreshes the masternode set too, mirroring writeBlockWithState, otherwise its snapshot would never be written. - reorg stops one block short of the new head and leaves it to the caller, which in this port is both writeBlockWithState and the new writeKnownBlock. Applying the head inside reorg as well wrote the same canonical markers twice and, for a gap block, ran UpdateM1 twice. - insertSidechain is renamed to insertSideChain, as upstream did. Three points upstream leaves open are closed here, because XDPoS chains adopt blocks without snapshots and have a wider set of head side effects: - The fork-choice rule lives in blockBeatsHead and writeBlockWithState reuses it, so a known block is adopted exactly when an executed block would be. An edit to one path can no longer silently diverge from the other. - A known block that was not canonical yet is adopted for the first time, so its logs were never delivered; insertChain now delivers them with the rest of the batch. A rollback re-import is the opposite case: the block was canonical before, its logs already went out, and they are not sent a second time. - Adopting a known block also has to announce it with a ChainEvent, not merely deliver its logs. The head moved, and the subscribers that follow the head, eth_subscribe("newHeads") among them, hear about a new head through that event alone. Leaving it out kept them a block behind until an executed block arrived, which on a node catching up over known blocks could take a long time. The event carries the logs of a block promoted for the first time and none for a rollback re-import, matching what the logs feed sends. Known-block adoption also refreshes the blocks-hash cache and the signing-tx cache like the canonical import path does, and the gap-block predicate is shared through isGapBlock so a zero epoch can no longer divide by zero. Tests: TestInsertChainAdvancesHeadOverKnownBlocks rolls the head back over blocks that keep their state and asserts that re-inserting them moves the head to the tip. TestReorgLeavesNewHeadToCaller pins the reorg contract. TestWriteKnownBlockLogDelivery asserts that a promoted side chain delivers its logs exactly once while a rollback re-import delivers none. TestWriteKnownBlockChainEvents asserts that every known block the head moves over is announced on the chain feed, with logs on first promotion only. TestWriteBlockWithStateRejectsUnknownParentTd keeps the unknown-parent error that sharing the fork-choice rule could otherwise swallow. Closes XinFinOrg#2535 Refs ethereum#19417
d2593a8 to
b7993ed
Compare
|
replaced by #2566 |
Problem
insertChainonly skips known blocks while they sit at or below the head:A block that is already stored with its state but sits above the head leaves
err == ErrKnownBlock. The main import loop requireserr == nil, so it never runs and the head does not move.Upstream go-ethereum handles this with
writeKnownBlock, which reorgs when the block does not sit on the current head and then moves the head, driven byskipBlockin the import loop. NeitherwriteKnownBlocknorskipBlockexists in this repository.For
--gcmode archivethis is severe: once a block has been executed and its state committed without being adopted, it stays known forever, so every later attempt stops on it. Measured on a catching-up mainnet node (#2535): the head advanced 5114 blocks in 58 minutes (~88 blocks/min), with 692 sidechain imports of 50–1160 blocks each andadd=2for 284 of the 457Extend chainevents — roughly 1000 downloaded and rewritten blocks per ~6 blocks of progress, and consecutive rounds re-fetch almost the same range.Changes
core/blockchain.go: port geth core: import known blocks ethereum/go-ethereum#19417 (c113723f).writeKnownBlock: reorgs when the known block does not sit on the current head, then moves the head; the import loop now toleratesErrKnownBlockinstead of re-executing the block. A gap block reaching the head this way refreshes the masternode set, mirroringwriteBlockWithState, otherwise its snapshot would never be written.reorgstops one block short of the new head and leaves it to the caller (now bothwriteBlockWithStateandwriteKnownBlock), because applying it insidereorgas well wrote the same canonical markers twice and, for a gap block, ranUpdateM1twice.insertSidechainis renamed toinsertSideChain, as upstream did.blockBeatsHeadandwriteBlockWithStatereuses it, so a known block is adopted exactly when an executed block would be; an edit to one path can no longer silently diverge from the other. An unknown parent total difficulty is treated as not beating the head, so the batch keeps the current chain.insertChainnow delivers them with the rest of the batch. A rollback re-import is the opposite case — the block was canonical before, its logs already went out, and they are not sent a second time.isGapBlockso a zero epoch can no longer divide by zero.Tests
core/blockchain_test.goTestInsertChainAdvancesHeadOverKnownBlocks: rolls the head back over blocks that keep their state, asserts that re-inserting them moves the head to the tip.TestReorgLeavesNewHeadToCaller: pins the reorg contract.TestWriteKnownBlockLogDelivery: a promoted side chain delivers its logs exactly once, a rollback re-import delivers none.TestWriteBlockWithStateRejectsUnknownParentTd: keeps the unknown-parent error that sharing the fork-choice rule could otherwise swallow.consensus/tests/engine_v2_tests/known_block_test.goTestInsertChainKnownGapBlockRefreshesSnapshot: rolls the head back below the gap block, drops its snapshot and re-imports the already executed blocks; the known-block path must move the head forward and rewrite the snapshot.Trade-off
GetTdlookups and, on a gap block,UpdateM1. Those blocks were previously imported for nothing at all, so the cost replaces zero progress.reorgno longer writes the new head, so every caller must do it. There are exactly two (writeBlockWithState,writeKnownBlock) and both do.chainmuis held for the reorg; this is the same cost an executed block of the same segment already pays.Verification
make all,make quick-test,make tidy,make generateall pass.TestInsertChainAdvancesHeadOverKnownBlocksfails on the pre-fix code (the head stays on the rolled-back block) and passes after.geth PR: ethereum#19417
Closes #2535