Repository navigation
Call SetFinality when processing a milestone - #2226
sduchesneau wants to merge 4 commits into
Conversation
|
@claude review once |
|
Claude finished @pratikspatil024's task in 1m 33s —— View job Review: Call SetFinalized when processing a milestone
Overall: This is a well-motivated, clean fix. The PR description accurately identifies the gap — Bor's milestone finality was never written to CorrectnessLooks good. The change is placed after Thread safety is fine: Persistence across restarts
Suggestions (non-blocking)1. Consider also calling In the Engine API path ( For Bor, milestones represent the strongest finality signal, so the finalized block is by definition also "safe." Adding if header := eth.blockchain.GetHeaderByHash(hash); header != nil {
eth.blockchain.SetFinalized(header)
eth.blockchain.SetSafe(header)
}2. Re-processing on the 1-second ticker
3. No monotonicity check There's no check that the new finalized block number is ≥ the current finalized block number. In practice this should always hold because milestones are monotonically increasing, but a defensive check would make the invariant explicit: if header := eth.blockchain.GetHeaderByHash(hash); header != nil {
if current := eth.blockchain.CurrentFinalBlock(); current == nil || header.Number.Uint64() > current.Number.Uint64() {
eth.blockchain.SetFinalized(header)
}
}Again, non-blocking — the milestone protocol already guarantees monotonicity. No issues found
LGTM with the optional |
There was a problem hiding this comment.
LGTM — small, defensive 5-line change that mirrors the existing ProcessMilestone call.
Extended reasoning...
Overview
Single-file, 5-line addition in eth/handler_bor.go. After the existing h.downloader.ProcessMilestone(num, hash) call in handleMilestone, the PR fetches the corresponding header via GetHeaderByHash and, if non-nil, calls eth.blockchain.SetFinalized(header). This brings bor's finality signal into the canonical geth BlockChain API (CurrentFinalBlock()), which was previously always nil on bor since the only upstream writer is the (unused) Engine API.
Security risks
None identified. SetFinalized is a simple setter that stores the header pointer, writes the finalized hash to rawdb, and updates a metrics gauge — no consensus or state-transition impact. GetHeaderByHash is a safe read and returns nil for unknown hashes; the unknown-hash case is handled as a defensive no-op. No new external inputs, auth, or crypto surface area.
Level of scrutiny
Low. The change is purely additive metadata propagation: it makes existing geth machinery (metrics, the SetHead safety guard, tracer-visible finality) reflect a value bor already computes elsewhere (getFinalizedBlockNumber via the downloader whitelist). The two observable effects called out in the description (gauge advancing, SetHead invalidated finalized block log becoming reachable on rewinds below a milestone) are intended and benign.
Other factors
SonarQube quality gate passed. Maintainer (pratikspatil024) explicitly requested review on this PR. The change has been manually tested on mainnet per the PR description. No bugs flagged by the bug hunting system.
|
|
This PR is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
|
This is still valid, PR approved was just missing a merge window. |
|
@claude review once |
There was a problem hiding this comment.
LGTM — the change is a small, well-reviewed addition that wires milestone processing into BlockChain.SetFinalized. The three findings are all nits (a stale comment, a non-blocking monotonicity suggestion already raised and accepted as non-blocking in the original review, and a subtle safe==finalized resurfacing on restart via the pre-existing loadLastState path) — none affect correctness or introduce security/consensus risk, and the core placement/nil-checks are correct.
Extended reasoning...
Overview
The diff adds a single call, eth.blockchain.SetFinalized(block.Header()), in handleMilestone (eth/handler_bor.go), right after ProcessMilestone succeeds. It re-resolves the milestone's end block by number and checks the hash matches before publishing, guarding against a reorg between verification and this write. This wires Heimdall milestone finality into the standard geth BlockChain finality bookkeeping (atomic pointer + headFinalizedBlockGauge + rawdb.WriteFinalizedBlockHash), which was previously always nil/zero on Bor since only the (unused) Engine API called SetFinalized upstream.
Security risks
No new external input surface is introduced — the milestone data was already verified by verifier.verify before this call executes, and the block is re-resolved defensively by canonical number->hash mapping rather than trusting a stale reference. The only consumers of CurrentFinalBlock() are metrics, live tracers, and downstream tooling — Bor's actual reorg-safety mechanism (the whitelist finality.IsValidChain) is untouched. The RPC eth_getBlockByNumber("finalized") path is also unchanged (still goes through the downloader whitelist). I confirmed the three inline findings are all real relative to the current code (checked SetFinalized/SetSafe/loadLastState in core/blockchain.go), but all are Nit-severity: none allow an external actor to corrupt state, bypass validation, or crash the node.
Level of scrutiny
This is a narrowly-scoped, single-function change (7 added lines) touching a non-consensus-critical bookkeeping path (finality metrics/API surface, not block validation or whitelist logic). It underwent a full review cycle with two maintainers (kamuikatsurgi, maoueh) explicitly debating and resolving the SetSafe question, and a maintainer reconfirmed on 2026-07-13 that it remains valid and was previously approved pending a merge window. Given the limited blast radius and the depth of prior human review, a light-touch confirmation is appropriate here rather than blocking again on nits that don't change behavior in any harmful way.
Other factors
All three inline findings are explicitly non-blocking suggestions (documentation staleness, a defensive monotonicity check already proposed and consciously deferred in the original review, and a restart-time edge case in pre-existing upstream loadLastState logic that this PR merely activates for the first time on Bor). None require a design decision beyond what's already been discussed in the thread.
|
This PR is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
|
This is still valid and was accepted previously. |
|
This PR is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
|
Hi @sduchesneau - could you please merge/rebase develop? |
|
This PR is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
Yes will do |
bc0fd91 to
5f202cf
Compare
|
@pratikspatil024 Branch should now be mergeable again |
|
@claude review |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I also checked whether the rawdb.WriteFinalizedBlockHash call inside SetFinalized could introduce a new crash path on a transient KV-store write error — it uses the same existing log.Crit-on-write-failure pattern already used by other Write* accessors in core/rawdb, so this isn't a novel risk introduced by this PR.
Extended reasoning...
The diff adds a post-milestone call to BlockChain.SetFinalized in eth/handler_bor.go, re-resolving the milestone block by number and checking its hash before promoting it to finalized; this is security/consensus-adjacent (finality state used by RPC and gauges) but small and already reviewed. A confirmed finding (safe block gauge/tag going stale after restart because loadLastState copies the persisted finalized hash into currentSafeBlock) is the same mechanism I flagged in my prior review on this PR, so I did not restate it. I additionally checked whether SetFinalized's rawdb write could newly crash the node on a transient error, and ruled that out since it reuses an existing log.Crit pattern common to other rawdb Write* functions, which is new information worth recording briefly.
|
@maoueh - the PR looks good, thank you! |
write time handleMilestone now resolves the milestone block via GetBlockByNumber and verifies the canonical hash matches milestone.Hash before calling SetFinalized/SetSafe to avoid promoting a stale header if a reorg lands before this write. Calls SetSafe alongside SetFinalized so both advance in real time.
|
@pratikspatil024 Done, I've fixed the commits to be co-authored by me. |
|
@maoueh Thanks! The change looks sound; I found no consensus-safety blocker. Could you add tests for the finality update to fix diffguard? One edge case: |
Milestones only advance the finalized block, but loadLastState seeded the safe block from the persisted finalized hash on restart, leaving "safe" frozen at the restart-time block. Skip that seeding on Bor chains so safe stays consistently absent, and ignore milestones that are not ahead of the current finalized block so finality never moves backward.
|
@pratikspatil024 Added requested changes |



Description
bc.CurrentFinalBlock()is currently alwaysnilon bor nodes, because the only writers ofblockchain.SetFinalizedin upstream geth are the Engine API (eth/catalyst), which bor does not use. As a result:headFinalizedBlockGauge/headSafeBlockGaugemetrics never advance.BlockChainAPI to read finality — live tracers (core.BlockchainLogger), external indexers, downstream tooling — sees no finality on bor, even though Heimdall milestones are by definition the chain's strongest finality signal.Bor already knows what's finalized:
eth/api.go:getFinalizedBlockNumberderives it fromDownloader().GetWhitelistedMilestone()to answer theeth_getBlockByNumber("finalized")RPC. This PR closes the gap by also writing that information into the canonical place (bc.SetFinalized) the moment a milestone has been successfully processed inethHandler.handleMilestone, right after the existingProcessMilestonecall. The header is fetched defensively viaGetHeaderByHash; an unknown hash is a no-op.This PR only writes to
CurrentFinalBlock()from milestones, because milestones are the strongest/live signal. The RPCfinalized-tag path ingetFinalizedBlockNumber(milestone → checkpoint fallback) is unchanged.Changes
Breaking changes
None at the protocol or RPC level. Two observable effects:
headFinalizedBlockGaugeandheadSafeBlockGaugewill now advance on bor nodes (they were stuck at 0). Dashboards may need to acknowledge this.SetHead invalidated finalized blocklog path incore/blockchain.gobecomes reachable. By construction it only triggers on a rewind below a confirmed Heimdall milestone, which is already a pathological situation — making it visible is the desired behavior.he JSON-RPC
eth_getBlockByNumber("finalized")response is unchanged: it still flows throughgetFinalizedBlockNumberand the downloader whitelist.Testing
eth_getBlockByNumber("finalized")continues to return the latest milestoned block (unchanged).headFinalizedBlockGaugeadvances after the first milestone is processed (new behavior).BlockChain.CurrentFinalBlock()returns the milestone header (new behavior, previouslynil).