mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Red adversarial review of the PQ-finality gate. Dormant default verified a true no-op; these harden the ACTIVATED path before any forward-dated activation: H1 (HIGH) — bindCheck now binds cert.Epoch == cp.Epoch (and cert.Round). The gate resolves verification keys from the cert's epoch; an unbound epoch let a cert signed under a DIFFERENT validator-set era (e.g. a compromised RETIRED committee key) certify the current block, nullifying KeyEra rotation as a blast-radius bound. Honest certs match (producer signs Subject.Epoch==cp.Epoch). + wrong-epoch / wrong-round anti-replay test cases. M2 (MED) — activation is now HEIGHT-ONLY, deterministic. Removed time.Now() (and the ActivationConfig.Time field) from the accept path: wall-clock gating split finalization across validators with skewed clocks (some halting on a missing cert, others finalizing without one). A height is consensus-agreed; timestamp forward-dating is expressed by choosing the activation height. L5 (LOW) — an activated gate with a nil store/validator provider now fails closed with ErrGateMisconfigured instead of panicking in the accept hook (a panic would halt the chain uncontrollably). Off the dormant path. + test. M3 (doc) — made the StateRoot=0 transitive-through-BlockHash contract explicit in bindCheck (non-zero cert StateRoot already rejected + tested) so the producer follow-on cannot silently ship state-committing certs that self-halt. proposervm: verifyQuasarFinality short-circuits on nil gate BEFORE building the Checkpoint, so the default path makes zero block-accessor calls. 13 gate tests green under -race; full proposervm suite green; go build ./... exit 0. Deferred to follow-ons (gate is dormant + unwired in production): M4 cert- unavailability halt runbook + grace window, L6 MemCertStore eviction (no ingest caller yet), proposervm Accept-path integration test, positive valid-cert test (needs consensus to export cert encoders).
269 lines
11 KiB
Go
269 lines
11 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package quasar is the node-side integration of the luxfi/consensus Quasar
|
|
// post-quantum finality-certificate layer.
|
|
//
|
|
// luxd finalizes blocks on the classical Snow/Avalanche path (fast, every
|
|
// block). On top, at CHECKPOINTS (epoch boundaries — NOT every block), a sampled
|
|
// committee produces a QuasarCert over the finalized digest and validators
|
|
// VERIFY it. This package wires the VERIFY half: it consumes
|
|
// github.com/luxfi/consensus/protocol/quasar.VerifyConsensusCert as an OPTIONAL,
|
|
// FORWARD-DATED, DORMANT-BY-DEFAULT check in the block-accept path.
|
|
//
|
|
// # Safety contract
|
|
//
|
|
// The forward-dated dormant activation is the whole reason this package has the
|
|
// shape it does:
|
|
//
|
|
// - Pre-activation (the default): VerifyAccepted is a pure no-op. Classical
|
|
// Snow finality is UNCHANGED. A nil *Gate, a zero Gate, or an unset
|
|
// activation height all mean "dormant" — zero behavior change.
|
|
// - Post-activation (owner sets Activation.Height to a real, forward-dated
|
|
// height): at every checkpoint height the gate REQUIRES a valid QuasarCert
|
|
// bound to the just-finalized block and FAILS CLOSED — a missing or invalid
|
|
// cert returns an error from Accept(), halting the chain rather than
|
|
// finalizing a checkpoint without post-quantum evidence.
|
|
//
|
|
// Activation is therefore a deliberate switch the owner flips only AFTER the
|
|
// cert PRODUCER (the per-validator committee signer) is live and certs flow at
|
|
// the checkpoint cadence — otherwise every checkpoint would halt. See
|
|
// producer.go.
|
|
//
|
|
// # Default posture
|
|
//
|
|
// HYBRID_PQ = Beam(BLS) ∧ Pulsar (standard FIPS-204 threshold ML-DSA), at
|
|
// checkpoint cadence. Per the measured policy-tier benchmarks, Pulsar verify
|
|
// (~140µs) is cheaper than BLS itself and the cert is compact (~27KB) — the
|
|
// right default production finality posture. STRICT_DUAL_PQ (∧ Corona) and the
|
|
// POLARIS tiers (∧ Magnetar) are configurable for stricter mainnet finality.
|
|
package quasar
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
qcert "github.com/luxfi/consensus/protocol/quasar"
|
|
)
|
|
|
|
// ActivationConfig is the forward-dated activation switch.
|
|
//
|
|
// The zero value is DORMANT: Height == 0 means "never activate" and the gate is
|
|
// a no-op for every block. This mirrors the genesis upgrade discipline (a
|
|
// far-future / unset activation point cannot affect live finality).
|
|
//
|
|
// Activation is by HEIGHT ONLY, deliberately. A block height is agreed by
|
|
// consensus, so every honest validator enforces PQ finality at exactly the SAME
|
|
// checkpoints — there is no node-local decision. (A wall-clock gate would split
|
|
// finalization across validators with skewed clocks: some halting on a missing
|
|
// cert while others finalize without one. Timestamp-based forward-dating is
|
|
// expressed by choosing the activation HEIGHT at the target time.)
|
|
type ActivationConfig struct {
|
|
// Height is the block height at and above which PQ-finality verification is
|
|
// enforced at checkpoints. 0 == dormant (never).
|
|
Height uint64
|
|
}
|
|
|
|
// dormant reports whether the activation is unset (the default — never enforce).
|
|
func (a ActivationConfig) dormant() bool { return a.Height == 0 }
|
|
|
|
// active reports whether enforcement is live for a block at the given height.
|
|
// Deterministic: height-only, no wall clock. Dormant activation is never active.
|
|
func (a ActivationConfig) active(height uint64) bool {
|
|
return a.Height != 0 && height >= a.Height
|
|
}
|
|
|
|
// DefaultCheckpointInterval is the default checkpoint cadence in blocks. PQ
|
|
// certs ride epoch-boundary checkpoints, never every block (Magnetar sign is
|
|
// checkpoint-only; even the cheap Pulsar sign is checkpoint cadence). The owner
|
|
// overrides this to match the producer's cadence at activation time.
|
|
const DefaultCheckpointInterval uint64 = 256
|
|
|
|
// DefaultMode is the default Quasar posture: HYBRID_PQ (Beam ∧ Pulsar).
|
|
const DefaultMode = qcert.PolicyHybridPQCheckpoint
|
|
|
|
// Config is the node-surfaced PQ-finality configuration. Its zero value is
|
|
// dormant + HYBRID_PQ + default cadence.
|
|
type Config struct {
|
|
// ChainID is THIS chain's numeric identifier (the sovereign/EVM chain id),
|
|
// bound into every cert and checked against it. A per-chain constant sourced
|
|
// from chain config at gate construction — NOT pulled from a block, because
|
|
// the proposervm layer carries the 32-byte validator-set id, not the numeric
|
|
// chain id. Inert while dormant.
|
|
ChainID uint32
|
|
|
|
// Activation is the forward-dated dormant switch. Zero => dormant.
|
|
Activation ActivationConfig
|
|
|
|
// Mode is the Quasar evidence posture. Zero => DefaultMode (HYBRID_PQ).
|
|
Mode qcert.QuasarEvidenceMode
|
|
|
|
// MLDSAParam selects the ML-DSA parameter set for the Pulsar leg. 0 =>
|
|
// ML-DSA-65 (the consensus default).
|
|
MLDSAParam uint8
|
|
|
|
// Threshold is the BFT quorum floor (minimum aggregate signer weight) every
|
|
// leg's evidence must establish.
|
|
Threshold uint64
|
|
|
|
// CheckpointInterval is the checkpoint cadence in blocks. 0 =>
|
|
// DefaultCheckpointInterval.
|
|
CheckpointInterval uint64
|
|
}
|
|
|
|
// Checkpoint is the finalized-block position the accept hook hands the gate. It
|
|
// is the binding the cert must match (anti-replay): a valid cert for a DIFFERENT
|
|
// block must never satisfy THIS checkpoint. The chain id is gate-level config,
|
|
// not a per-block field.
|
|
type Checkpoint struct {
|
|
Epoch uint64
|
|
Height uint64
|
|
Round uint32
|
|
BlockID [32]byte
|
|
StateRoot [32]byte
|
|
}
|
|
|
|
// Gate enforces (or, dormant, ignores) PQ-finality at checkpoints. It is the
|
|
// single node-side seam between the classical accept path and the consensus
|
|
// Quasar verifier.
|
|
type Gate struct {
|
|
cfg Config
|
|
policy *qcert.QuasarEvidencePolicy
|
|
store CertStore
|
|
validators ValidatorSetProvider
|
|
}
|
|
|
|
// NewGate constructs a Gate. A Gate is meaningful even with a dormant Config:
|
|
// VerifyAccepted is a no-op until Activation.Height is set. store and validators
|
|
// are only consulted post-activation at checkpoints.
|
|
func NewGate(cfg Config, store CertStore, validators ValidatorSetProvider) *Gate {
|
|
mode := cfg.Mode
|
|
if mode == 0 {
|
|
mode = DefaultMode
|
|
}
|
|
if cfg.CheckpointInterval == 0 {
|
|
cfg.CheckpointInterval = DefaultCheckpointInterval
|
|
}
|
|
cfg.Mode = mode
|
|
return &Gate{
|
|
cfg: cfg,
|
|
policy: qcert.NewQuasarEvidencePolicy(mode, cfg.MLDSAParam, cfg.Threshold),
|
|
store: store,
|
|
validators: validators,
|
|
}
|
|
}
|
|
|
|
// VerifyAccepted is the accept-path hook and the SAFETY BOUNDARY.
|
|
//
|
|
// - g == nil OR dormant activation => returns nil immediately. This is the
|
|
// default and guarantees classical Snow finality is unchanged.
|
|
// - height below activation, or activation time not yet reached => nil.
|
|
// - not a checkpoint height => nil (certs ride checkpoints only).
|
|
// - checkpoint, activated => REQUIRE a valid cert bound to this block; FAIL
|
|
// CLOSED. A missing, mis-bound, or invalid cert is an error (the caller
|
|
// returns it from Accept, halting rather than finalizing without PQ
|
|
// evidence).
|
|
//
|
|
// It is intentionally nil-safe so the proposervm hook can call
|
|
// vm.quasarGate.VerifyAccepted(...) unconditionally with a nil gate.
|
|
func (g *Gate) VerifyAccepted(cp Checkpoint) error {
|
|
if g == nil || g.cfg.Activation.dormant() {
|
|
return nil
|
|
}
|
|
if !g.cfg.Activation.active(cp.Height) {
|
|
return nil
|
|
}
|
|
if !g.isCheckpoint(cp.Height) {
|
|
return nil
|
|
}
|
|
// Activated checkpoint: the gate MUST have its cert store + validator
|
|
// provider, or it cannot verify. Fail closed with a typed error rather than
|
|
// panic in the accept hook (a panic would halt the chain uncontrollably).
|
|
if g.store == nil || g.validators == nil {
|
|
return fmt.Errorf("%w: chain=%d height=%d", ErrGateMisconfigured, g.cfg.ChainID, cp.Height)
|
|
}
|
|
|
|
cert, ok := g.store.Lookup(g.cfg.ChainID, cp.Height, cp.BlockID)
|
|
if !ok || cert == nil {
|
|
return fmt.Errorf("%w: chain=%d height=%d block=%x", ErrFinalityCertMissing, g.cfg.ChainID, cp.Height, cp.BlockID[:8])
|
|
}
|
|
if err := bindCheck(cert, g.cfg.ChainID, cp); err != nil {
|
|
return err
|
|
}
|
|
|
|
vs, err := g.validators.ValidatorSet(g.cfg.ChainID, cert.Epoch)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: chain=%d epoch=%d: %v", ErrValidatorSetUnavailable, g.cfg.ChainID, cert.Epoch, err)
|
|
}
|
|
if err := qcert.VerifyConsensusCert(policyStore{policy: g.policy}, vs, cert); err != nil {
|
|
return fmt.Errorf("%w: chain=%d height=%d: %v", ErrFinalityCertInvalid, g.cfg.ChainID, cp.Height, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Activated reports whether enforcement is live for a block at the given height
|
|
// and the current wall clock. Used by the producer-request site to decide
|
|
// whether a cert is needed at a checkpoint.
|
|
func (g *Gate) Activated(height uint64) bool {
|
|
if g == nil {
|
|
return false
|
|
}
|
|
return g.cfg.Activation.active(height)
|
|
}
|
|
|
|
// IsCheckpoint reports whether the given height is a checkpoint under the gate's
|
|
// configured cadence. Exported so the producer-request site shares ONE cadence
|
|
// definition with the verify path (no second source of truth).
|
|
func (g *Gate) IsCheckpoint(height uint64) bool {
|
|
if g == nil {
|
|
return false
|
|
}
|
|
return g.isCheckpoint(height)
|
|
}
|
|
|
|
func (g *Gate) isCheckpoint(height uint64) bool {
|
|
iv := g.cfg.CheckpointInterval
|
|
if iv == 0 {
|
|
iv = DefaultCheckpointInterval
|
|
}
|
|
return height%iv == 0
|
|
}
|
|
|
|
// bindCheck pins the cert to the actual finalized block. Without this, a valid
|
|
// cert produced for a different (chain, height, block) could be replayed to
|
|
// satisfy this checkpoint. VerifyConsensusCert checks the cert's INTERNAL
|
|
// consistency and the validator-set/policy binding; bindCheck adds the external
|
|
// binding to THIS node's finalized position.
|
|
func bindCheck(cert *qcert.ConsensusCert, chainID uint32, cp Checkpoint) error {
|
|
if cert.ChainID != chainID {
|
|
return fmt.Errorf("%w: cert chain %d != finalized chain %d", ErrFinalityCertMismatch, cert.ChainID, chainID)
|
|
}
|
|
// Bind the epoch. The gate resolves the verification keys from the cert's
|
|
// epoch, so an UNBOUND epoch would let a cert signed under a DIFFERENT
|
|
// validator-set era (e.g. a compromised RETIRED committee's group key) certify
|
|
// the current block — nullifying KeyEra rotation as a blast-radius bound. The
|
|
// honest producer signs over Subject.Epoch == cp.Epoch, so honest certs match.
|
|
if cert.Epoch != cp.Epoch {
|
|
return fmt.Errorf("%w: cert epoch %d != finalized epoch %d", ErrFinalityCertMismatch, cert.Epoch, cp.Epoch)
|
|
}
|
|
if cert.Round != cp.Round {
|
|
return fmt.Errorf("%w: cert round %d != finalized round %d", ErrFinalityCertMismatch, cert.Round, cp.Round)
|
|
}
|
|
if cert.Height != cp.Height {
|
|
return fmt.Errorf("%w: cert height %d != finalized height %d", ErrFinalityCertMismatch, cert.Height, cp.Height)
|
|
}
|
|
if cert.BlockHash != cp.BlockID {
|
|
return fmt.Errorf("%w: cert block hash != finalized block id", ErrFinalityCertMismatch)
|
|
}
|
|
// StateRoot contract: at the proposervm layer the post-state root is
|
|
// committed TRANSITIVELY through BlockHash, so cp.StateRoot is zero and the
|
|
// cert MUST carry a zero StateRoot too. A non-zero cert StateRoot is rejected
|
|
// (no state to cross-check here) — the producer follow-on MUST emit
|
|
// StateRoot==0 at this layer; a chain that wants an explicit state binding
|
|
// plumbs cp.StateRoot AND signs it, and this check then enforces equality.
|
|
var zero [32]byte
|
|
if cert.StateRoot != zero && cert.StateRoot != cp.StateRoot {
|
|
return fmt.Errorf("%w: cert state root != finalized state root", ErrFinalityCertMismatch)
|
|
}
|
|
return nil
|
|
}
|