Files
consensus/engine/chain
zeekay 0a36c12cbd fix(chain): self-finalize proposer at proposal time (v1.25.2)
The v1.25.1 IsOwnProposal handleVote trust path closed the proposer's
re-Verify-flip-to-Reject gap, but only along the code path where peer
Chits actually arrive on the proposer. Production validation on
lux-devnet v1.27.26 (K=3 alpha=2) shows the gap reappears when peer
Chits do not reach the proposer in a timely fashion — the proposer's
acceptVotes stays at 1 (lone self-vote), below alpha; PendingBlock
stays Decided=false; VM.Accept is never called; the proposer freezes
at its proposal height while peers fast-follow and advance.

Empirical signal from devnet logs:
  - Proposer: "proposed block to validators"
  - Followers: "fast-follow: accepting block from validator" + advance
  - Proposer: NO chit / fast-follow / finalize event; height stays
  - Stuck until a non-proposer round produces the next block

This is independent of the v1.25.1 IsOwnProposal logic (which is
correct and still required for the path where Chits do arrive but
re-Verify flips them to Reject). The v1.25.2 fix routes the proposer
through a separate self-finalize path at the end of buildBlocksLocked,
once Propose + RequestVotes have queued the network broadcast.

Surgery (3 files):

  consensus.go: add ForceAccept(blockID) — consensus-level counterpart
  of ForcePreference, marks block accepted bypassing alpha-of-K. Only
  authorized caller is engine.finalizeOwnProposal.

  engine.go: extract finalizePendingLocked shared finalization tail
  (VMBlock.Accept + SetPreference + signalPipeline + idempotency
  guard) shared by both tryFinalizeBlock and the new
  finalizeOwnProposal. The new helper is gated on
  pending.IsOwnProposal=true && !pending.Decided && proposer != nil
  so follower-tracked entries (HandleIncomingBlock slow path,
  IsOwnProposal=false) and unit-test paths (no Proposer wired) still
  require natural alpha-of-K quorum.

  proposer_self_accept_test.go: three new regression tests with
  recordingProposer (zero-network Proposer):
   - TestProposerSelfAccept_FinalizesWithoutPeerChits: zero peer
     votes, Accept must fire exactly once at proposal time.
   - TestProposerSelfAccept_IdempotentUnderLateChits: late vote.Accept=false
     arrivals must not drive a second Accept call.
   - TestFollower_StillRequiresAlphaQuorum_AfterSelfAcceptFix: direct
     call to finalizeOwnProposal on an IsOwnProposal=false entry must
     be a no-op.

Safety argument:
 1. The proposer ran Verify locally before AddBlock (engine.go:1001).
    A successful Verify is the proposer's commitment that the block
    is well-formed against its current view of state.
 2. Under honest-validator + deterministic-Verify assumption (devnet,
    all production deployments) peers running the same Verify reach
    the same decision; structurally no divergence.
 3. Cluster authoritative finality is unaffected: the BLS+Pulsar
    witness is the canonical record, slashing evidence flags a
    byzantine proposer that commits a block peers reject.
 4. Idempotent under concurrent invocation: pending.Decided=true
    + delete(pendingBlocks) guard in finalizePendingLocked prevent
    double-Accept across pipeline tick, vote handler, and the new
    self-finalize path.
 5. Tightly scoped: only fires for IsOwnProposal=true entries with
   a wired Proposer; the v1.25.1 IsOwnProposal trust path in
   handleVote is preserved and complementary.

Test results: all 6 proposer_self_accept tests PASS under -race;
full engine/chain suite PASS (without -race; pre-existing test leak
in TestNotifyPendingTxsTriggersBuildBlock blocks -race run, not
introduced by this commit).

No API surface change. No backwards-compat shim. No new dependency.
2026-05-30 21:28:51 -07:00
..