Files
consensus/engine
zeekayandHanzo Dev 25048fa5e0 engine/chain: reconcile VM to the certified block instead of killing the validator
On a `vm.SetPreference(certified)` refusal in the accept path — the EVM's "cannot
orphan finalized block" guard, which fires when the VM's own accepted head ran
ahead of / diverged from consensus finality — the engine used to `log.Crit`
(= os.Exit(1)), killing an otherwise-healthy validator. That neither undoes the
finality the ledger already recorded nor repairs the VM; it just removes a node.

Replace it with a safety-gated reconcile. `reconcileVMToCertified` classifies the
VM's diverged head against the finality ledger (the sole authority, one canonical
per height):

- head above the finalized frontier, or a losing sibling of a finalized block
  (its canonical is NOT the ledger's finalized canonical at its height) ⇒ the tip
  is provably UNCERTIFIED ⇒ reconcile the VM to the certified block (via the new
  optional PreferenceReconciler VM interface) or, if the VM cannot reconcile live,
  surface the divergence and defer to the existing offline core.FinalizeRewind
  recovery. Either way the node keeps its correct consensus finality and does not
  crash.
- head IS the ledger's finalized canonical at its height (and `certified`, a
  different canonical, would orphan it) ⇒ a genuine two-certified-blocks
  double-finalization ⇒ HALT fail-closed.

Safety: a tip is dropped ONLY when its canonical is provably absent from the
byHeight ledger, so no certified block is ever orphaned. `certified` is always the
block consensus just finalized — on the single canonical finalized chain — so a
reconcile can only move the VM along/forward on that chain. Liveness is untouched:
the reconciler reads only the Nova ledger, never the Quasar frontier, and the
finalize path (ApplyCert → applyBranchFinalization) is byte-identical; a 3/5 Nova
majority finalizes exactly as before, with no new ⅔-Quasar wait. The
PreferenceReconciler contract adds a second, independent VM-side ⅔-Quasar-floor
gate as defense-in-depth. On the honest 5-node BFT path this divergence cannot
arise at all (byHeight guard + single-vote-per-height + coupled ApplyCert→Accept);
it requires a second accept authority (automining — which structurally forces K=1;
admin_importChain; or a proposervm crash/state-sync desync), where the reconcile
still drops only uncertified state.

Hardening from the adversarial consensus-safety review (LOW-1): the fail-closed
halt is now logger-independent. `log.Crit` is fatal only under a real logger and
the engine defaults to log.Noop() (a silent Crit), so the sole safety halt now
follows the Crit with an explicit os.Exit(1) — it can never fall through to
ForcePreference regardless of WithLogger wiring.

Tests (engine/chain/orphan_reconcile_test.go): every classifier branch —
uncertified-sibling reconcile, build-ahead-above-frontier reconcile, no-op,
unreadable-head defer, no-live-reconcile defer, and the load-bearing
CertifiedHead_HaltsFailClosed (a certified head returns false / halts, and
ReconcilePreference is NOT called) — plus the end-to-end
AcceptWithCert_OrphanRefusal path that drives the real
acceptWithCertCore → ApplyCert → applyBranchFinalization → SetPreference sequence
with a VM that refuses with the exact EVM orphan error, asserting a clean finalize
(no os.Exit) and the VM reconciled to the certified block. Full engine/chain suite
green. Reviewed adversarially: verdict SAFE (no CRITICAL/HIGH; no certified block
ever orphaned, liveness unweakened, double-finalization still halts fail-closed).

Companion live-recovery follow-up (not in this commit): implement
PreferenceReconciler on the evm plugin VM (SetLastAcceptedBlockDirect /
FinalizeRewind, refusing below eth.ClampedQuasarHeight) so a diverged node
recovers without a restart; today it defers non-fatally to the offline rewind.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-16 14:26:42 -07:00
..
2025-12-19 08:49:11 -08:00