mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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]>