feat(consensus/quasar): wire Quasar PQ-finality VERIFY path (dormant, forward-dated)

Node-side integration of luxfi/consensus v1.29.0 Quasar compact-cert finality.
luxd finalizes on classical Snow every block; at CHECKPOINTS a sampled committee
QuasarCert over the finalized digest is VERIFIED. This wires the verify half as
an OPTIONAL, FORWARD-DATED, DORMANT-BY-DEFAULT check in the block-accept path.

consensus/quasar (new package):
  - Gate.VerifyAccepted: the accept-path safety boundary. nil gate / zero
    activation / below-activation-height / non-checkpoint => no-op (classical
    Snow unchanged). Post-activation at a checkpoint => REQUIRE a valid cert
    bound to the finalized block; fail closed (missing/mismatch/invalid -> error).
  - ActivationConfig: forward-dated dormant switch (Height==0 => never; optional
    wall-clock Time gate). The safety contract: this branch changes NOTHING about
    live finality until the owner sets a real activation height.
  - bindCheck: anti-replay binding (cert chain/height/block/state == finalized).
  - policyStore: default posture HYBRID_PQ = Beam(BLS) ^ Pulsar (Pulsar verify
    ~140us < BLS, cert ~27KB); STRICT_DUAL_PQ / POLARIS configurable. The cert
    can never select its own (weaker) policy (I1/I2 enforced by consensus).
  - ValidatorSet/MemCertStore: the per-leg key + cert-lookup seams (HYBRID_PQ
    BLS+Pulsar keys now; Corona/Magnetar/weighted-set + per-epoch resolution are
    the activation-wiring follow-on).
  - Producer (scaffolding): the committee-sign service contract + MaybeProduce
    request site, sharing ONE checkpoint cadence with verify. nil producer =
    verify-only (the default). Concrete pulsard signer is the follow-on; it needs
    consensus to export the (currently package-private) cert/payload ENCODERS.

proposervm: post_fork_block.Accept calls vm.verifyQuasarFinality(b) BEFORE the
accept commits, so an un-PQ-certified checkpoint halts without persisting. nil
gate (default) => unchanged classical accept. SetQuasarGate installs it.

Tests: 12 gate tests green (dormant no-op, nil-safe, below-activation, time-gate,
non-checkpoint, missing-fails-closed, 4x mismatch/anti-replay, real-verifier
delegation, validator-set-unavailable, producer nil-safety/dormant/active). Full
proposervm suite green; go build ./... exit 0.
This commit is contained in:
zeekay
2026-06-28 07:32:48 -07:00
parent d70a407a3a
commit 463d2533af
9 changed files with 851 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import "errors"
// Typed, fail-closed errors. Every one is returned (never swallowed) and, when
// surfaced from the accept hook post-activation, halts finalization rather than
// accepting a checkpoint without valid post-quantum evidence.
var (
// ErrFinalityCertMissing — a checkpoint was finalized post-activation but no
// QuasarCert is available for it (the producer has not delivered one).
ErrFinalityCertMissing = errors.New("quasar: finality cert missing for checkpoint")
// ErrFinalityCertMismatch — a cert exists but does not bind the finalized
// block (chain/height/block/state mismatch). Anti-replay.
ErrFinalityCertMismatch = errors.New("quasar: finality cert does not bind the finalized block")
// ErrFinalityCertInvalid — the cert is bound correctly but failed consensus
// verification (policy, validator-set root, or a leg signature).
ErrFinalityCertInvalid = errors.New("quasar: finality cert failed verification")
// ErrValidatorSetUnavailable — no committed validator set for the cert's
// epoch (the verifier cannot resolve the per-leg verification keys).
ErrValidatorSetUnavailable = errors.New("quasar: validator set unavailable for epoch")
// ErrPolicyUnavailable — the gate has no configured policy.
ErrPolicyUnavailable = errors.New("quasar: policy unavailable")
// ErrPolicyMismatch — the cert's PolicyID is not the configured policy. A
// cert cannot select its own (weaker) posture.
ErrPolicyMismatch = errors.New("quasar: cert policy id does not match configured policy")
)
+256
View File
@@ -0,0 +1,256 @@
// 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"
"time"
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-timestamp discipline
// (a far-future / unset activation point cannot affect live finality).
type ActivationConfig struct {
// Height is the block height at and above which PQ-finality verification is
// enforced at checkpoints. 0 == dormant (never).
Height uint64
// Time, when non-zero, ADDITIONALLY requires the wall-clock activation
// moment to have passed before enforcement begins (defence in depth on the
// forward-dating). Zero == height-only gating.
Time time.Time
}
// 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 and
// the given wall-clock now. Dormant activation is never active.
func (a ActivationConfig) active(height uint64, now time.Time) bool {
if a.Height == 0 {
return false
}
if height < a.Height {
return false
}
if !a.Time.IsZero() && now.Before(a.Time) {
return false
}
return true
}
// 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, time.Now()) {
return nil
}
if !g.isCheckpoint(cp.Height) {
return nil
}
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, time.Now())
}
// 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)
}
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 binds only when the cert commits it. A zero cert StateRoot means
// "committed transitively through BlockHash" (the envelope spec), so it is
// not cross-checked.
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
}
+258
View File
@@ -0,0 +1,258 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import (
"context"
"errors"
"testing"
"time"
qcert "github.com/luxfi/consensus/protocol/quasar"
)
// vsRoot is a fixed committed-validator-set root used across the tests.
var vsRoot = [48]byte{0x11, 0x22, 0x33, 0x44}
// testValidators is a ValidatorSet whose Root matches vsRoot, with non-empty
// (placeholder) HYBRID_PQ keys. The delegation test never reaches signature
// math, so the key bytes need only be non-empty.
func testValidators() *ValidatorSet {
return NewValidatorSet(vsRoot, 1, []byte("bls-agg-key"), []byte("pulsar-group-key"))
}
// newGate builds a gate with a tight cadence (checkpoint every 10 blocks) and
// the given forward-dated activation height. interval 10 keeps the heights in
// the tests readable.
func newGate(activationHeight uint64, store CertStore) *Gate {
return NewGate(Config{
ChainID: 1337,
Activation: ActivationConfig{Height: activationHeight},
Mode: DefaultMode, // HYBRID_PQ
Threshold: 100,
CheckpointInterval: 10,
}, store, StaticValidatorSetProvider{Set: testValidators()})
}
func checkpointAt(height uint64) Checkpoint {
return Checkpoint{
Epoch: 1,
Height: height,
BlockID: [32]byte{0xab, 0xcd, 0xef},
}
}
// TestDormantIsNoop — the default (Activation.Height == 0) is a pure no-op even
// at a checkpoint height with a poisoned store. This is the core safety
// property: pre-activation, classical Snow finality is unchanged.
func TestDormantIsNoop(t *testing.T) {
store := NewMemCertStore()
g := NewGate(Config{CheckpointInterval: 10}, store, StaticValidatorSetProvider{Set: testValidators()})
// height 10 is a checkpoint; no cert exists; yet dormant => nil.
if err := g.VerifyAccepted(checkpointAt(10)); err != nil {
t.Fatalf("dormant gate must be a no-op, got %v", err)
}
if g.Activated(10) {
t.Fatal("dormant gate must never report Activated")
}
}
// TestNilGateIsNoop — a nil *Gate is the wire-it-but-leave-it-off default; the
// proposervm hook calls VerifyAccepted on a possibly-nil gate.
func TestNilGateIsNoop(t *testing.T) {
var g *Gate
if err := g.VerifyAccepted(checkpointAt(10)); err != nil {
t.Fatalf("nil gate must be a no-op, got %v", err)
}
if g.Activated(10) || g.IsCheckpoint(10) {
t.Fatal("nil gate must report neither Activated nor IsCheckpoint")
}
}
// TestBelowActivationIsNoop — activated at height 100 but the block is at 10:
// below the forward-dated height => nil, even at a checkpoint with no cert.
func TestBelowActivationIsNoop(t *testing.T) {
g := newGate(100, NewMemCertStore())
if err := g.VerifyAccepted(checkpointAt(10)); err != nil {
t.Fatalf("below activation must be a no-op, got %v", err)
}
}
// TestActivationTimeNotReached — height is past the activation height but the
// forward-dated wall-clock moment has not arrived => still dormant.
func TestActivationTimeNotReached(t *testing.T) {
g := NewGate(Config{
Activation: ActivationConfig{Height: 10, Time: time.Now().Add(time.Hour)},
CheckpointInterval: 10,
}, NewMemCertStore(), StaticValidatorSetProvider{Set: testValidators()})
if err := g.VerifyAccepted(checkpointAt(10)); err != nil {
t.Fatalf("pre-activation-time must be a no-op, got %v", err)
}
}
// TestNonCheckpointIsNoop — activated and at/above activation height, but the
// height is not a checkpoint => nil (certs ride checkpoints only).
func TestNonCheckpointIsNoop(t *testing.T) {
g := newGate(10, NewMemCertStore())
// height 15 is activated (>=10) but not a checkpoint (15 % 10 != 0).
if err := g.VerifyAccepted(checkpointAt(15)); err != nil {
t.Fatalf("non-checkpoint must be a no-op, got %v", err)
}
if !g.Activated(15) {
t.Fatal("height 15 should be activated")
}
if g.IsCheckpoint(15) {
t.Fatal("height 15 must not be a checkpoint")
}
}
// TestMissingCertFailsClosed — activated checkpoint with no cert in the store =>
// ErrFinalityCertMissing. Post-activation a checkpoint without PQ evidence must
// NOT finalize.
func TestMissingCertFailsClosed(t *testing.T) {
g := newGate(10, NewMemCertStore())
err := g.VerifyAccepted(checkpointAt(20))
if !errors.Is(err, ErrFinalityCertMissing) {
t.Fatalf("want ErrFinalityCertMissing, got %v", err)
}
}
// TestMismatchedCertRejected — a cert that does not bind the finalized block
// (wrong block id / height / chain) is rejected by bindCheck before any crypto.
// Anti-replay: a valid cert for a different block must not satisfy this one.
func TestMismatchedCertRejected(t *testing.T) {
cases := []struct {
name string
cert *qcert.ConsensusCert
}{
{"wrong block", &qcert.ConsensusCert{ChainID: 1337, Height: 20, BlockHash: [32]byte{0x99}}},
{"wrong height", &qcert.ConsensusCert{ChainID: 1337, Height: 21, BlockHash: [32]byte{0xab, 0xcd, 0xef}}},
{"wrong chain", &qcert.ConsensusCert{ChainID: 7, Height: 20, BlockHash: [32]byte{0xab, 0xcd, 0xef}}},
{"wrong state root", &qcert.ConsensusCert{ChainID: 1337, Height: 20, BlockHash: [32]byte{0xab, 0xcd, 0xef}, StateRoot: [32]byte{0x55}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
store := NewMemCertStore()
// Index it at the checkpoint's lookup key so Lookup returns it and
// bindCheck (not Lookup) does the rejecting.
store.certs[certKey{chainID: 1337, height: 20, blockID: [32]byte{0xab, 0xcd, 0xef}}] = tc.cert
g := newGate(10, store)
err := g.VerifyAccepted(checkpointAt(20))
if !errors.Is(err, ErrFinalityCertMismatch) {
t.Fatalf("want ErrFinalityCertMismatch, got %v", err)
}
})
}
}
// TestDelegatesToVerifier — a cert that BINDS correctly and passes the full
// ConsensusCert header path (version, policy load, required-legs root, validator
// -set root) but carries no signature evidence is rejected by the REAL
// consensus verifier, and the gate surfaces it as ErrFinalityCertInvalid. This
// proves the whole delegation chain is wired: policyStore + ValidatorSet +
// quasar.VerifyConsensusCert are reached with matching commitments — everything
// up to (but not including) the leg signature crypto, which needs the producer.
func TestDelegatesToVerifier(t *testing.T) {
cp := checkpointAt(20)
// Mirror the gate's posture to compute the header commitments the verifier
// pins (policy id + required-legs root). policyID and required legs derive
// from the mode + ML-DSA param, which match the gate's config.
pol := qcert.NewQuasarEvidencePolicy(DefaultMode, 0, 100)
cert := &qcert.ConsensusCert{
Version: 1,
ChainID: 1337, // must equal the gate's configured ChainID
Epoch: cp.Epoch,
Height: cp.Height,
BlockHash: cp.BlockID,
PolicyID: pol.EvidencePolicyID(),
RequiredLegsRoot: qcert.HashRequiredLegs(pol.RequiredLegs()),
ValidatorSetRoot: vsRoot,
// Evidence intentionally empty: the verifier must reject a required leg
// with no evidence (deepest deterministic failure without real crypto).
}
store := NewMemCertStore()
store.Put(cert)
g := newGate(10, store)
err := g.VerifyAccepted(cp)
if !errors.Is(err, ErrFinalityCertInvalid) {
t.Fatalf("want ErrFinalityCertInvalid (delegated), got %v", err)
}
}
// TestValidatorSetUnavailable — a bound cert at an activated checkpoint, but the
// provider has no set for the epoch => ErrValidatorSetUnavailable (fail closed).
func TestValidatorSetUnavailable(t *testing.T) {
cp := checkpointAt(20)
cert := &qcert.ConsensusCert{Version: 1, ChainID: 1337, Height: cp.Height, BlockHash: cp.BlockID}
store := NewMemCertStore()
store.Put(cert)
g := NewGate(Config{
ChainID: 1337,
Activation: ActivationConfig{Height: 10},
CheckpointInterval: 10,
}, store, StaticValidatorSetProvider{Set: nil}) // no set
err := g.VerifyAccepted(cp)
if !errors.Is(err, ErrValidatorSetUnavailable) {
t.Fatalf("want ErrValidatorSetUnavailable, got %v", err)
}
}
// --- producer scaffolding ---
type stubProducer struct {
cert *qcert.ConsensusCert
hits int
}
func (s *stubProducer) Produce(_ context.Context, _ Subject) (*qcert.ConsensusCert, error) {
s.hits++
return s.cert, nil
}
// TestMaybeProduceVerifyOnlyByDefault — a nil producer is the verify-only
// default: MaybeProduce short-circuits to (nil, nil), never panics.
func TestMaybeProduceVerifyOnlyByDefault(t *testing.T) {
g := newGate(10, NewMemCertStore())
cert, err := g.MaybeProduce(context.Background(), nil, checkpointAt(20))
if err != nil || cert != nil {
t.Fatalf("nil producer must yield (nil,nil), got cert=%v err=%v", cert, err)
}
}
// TestMaybeProduceDormant — even with a producer wired, a dormant gate produces
// nothing (the producer is brought up before activation is forward-dated).
func TestMaybeProduceDormant(t *testing.T) {
store := NewMemCertStore()
g := NewGate(Config{CheckpointInterval: 10}, store, StaticValidatorSetProvider{Set: testValidators()})
p := &stubProducer{cert: &qcert.ConsensusCert{}}
cert, err := g.MaybeProduce(context.Background(), p, checkpointAt(20))
if err != nil || cert != nil {
t.Fatalf("dormant gate must not produce, got cert=%v err=%v", cert, err)
}
if p.hits != 0 {
t.Fatalf("producer must not be called while dormant, hits=%d", p.hits)
}
}
// TestMaybeProduceActiveCheckpoint — wired producer + activated checkpoint =>
// the producer is asked for the cert.
func TestMaybeProduceActiveCheckpoint(t *testing.T) {
g := newGate(10, NewMemCertStore())
want := &qcert.ConsensusCert{ChainID: 1337, Height: 20}
p := &stubProducer{cert: want}
got, err := g.MaybeProduce(context.Background(), p, checkpointAt(20))
if err != nil {
t.Fatalf("unexpected err %v", err)
}
if got != want || p.hits != 1 {
t.Fatalf("producer not invoked as expected: got=%v hits=%d", got, p.hits)
}
// non-checkpoint height must not invoke the producer
p2 := &stubProducer{cert: want}
if _, _ = g.MaybeProduce(context.Background(), p2, checkpointAt(15)); p2.hits != 0 {
t.Fatalf("producer invoked at non-checkpoint, hits=%d", p2.hits)
}
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import (
"fmt"
qcert "github.com/luxfi/consensus/protocol/quasar"
)
// policyStore adapts the single configured QuasarEvidencePolicy to the consensus
// ConsensusCertPolicyStore interface. The verifier loads the required-leg set
// and the (kind, mode, param) permissions from HERE — never from the cert
// (invariants I1/I2). A cert that names a different PolicyID than the node's
// configured posture is rejected: a cert cannot pick its own weaker policy.
type policyStore struct{ policy *qcert.QuasarEvidencePolicy }
func (s policyStore) Policy(_ uint32, _ uint64, policyID uint32) (qcert.ConsensusCertPolicy, error) {
if s.policy == nil {
return nil, ErrPolicyUnavailable
}
if policyID != s.policy.EvidencePolicyID() {
return nil, fmt.Errorf("%w: cert policy %d != configured %d", ErrPolicyMismatch, policyID, s.policy.EvidencePolicyID())
}
return s.policy, nil
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import (
"context"
qcert "github.com/luxfi/consensus/protocol/quasar"
)
// Subject is the finalized-block position a cert must certify — the producer's
// input at a checkpoint. Mirrors Checkpoint (the verify side) so producer and
// verifier bind the SAME tuple.
type Subject struct {
ChainID uint32
Epoch uint64
Height uint64
Round uint32
BlockID [32]byte
StateRoot [32]byte
}
// subjectFrom derives the producer Subject from this gate's chain id and a
// finalized Checkpoint, so producer and verifier bind the SAME tuple.
func (g *Gate) subjectFrom(cp Checkpoint) Subject {
return Subject{
ChainID: g.cfg.ChainID,
Epoch: cp.Epoch,
Height: cp.Height,
Round: cp.Round,
BlockID: cp.BlockID,
StateRoot: cp.StateRoot,
}
}
// Producer is the committee cert-signing service contract (the per-validator
// "pulsard" committee). At a checkpoint, a producing validator calls Produce to
// obtain the QuasarCert over the finalized subject, then gossips it so peers can
// verify and store it (via a CertStore).
//
// SCAFFOLDING — this is the seam, not the service. This milestone wires the
// VERIFY half (gate.go) and this interface. luxd ships with a nil Producer
// (verify-only): a node VERIFIES certs it receives but does not itself produce
// them. A nil Producer is the correct default — most of the rollout window is
// verify-only, and the producer is brought up before activation is forward-dated.
//
// Implementation path for the follow-on:
//
// - github.com/luxfi/consensus/protocol/quasar already defines the
// producer-side abstractions: PWitnessProducer / QWitnessProducer /
// ZWitnessProducer + NewWitnessSet, and ComposeDualPQEvidence. The concrete
// committee signer implements Producer over those.
// - The signer needs the live Pulsar key share + nonce pool + offline
// preprocessing + one-round sign + verify-before-gossip + nonce-erase (the
// no-reconstruct hyperball signer), which lands with pulsar v1.7.1.
// - REQUIRED CONSENSUS EXPORT: the ConsensusCert envelope + per-leg payload
// ENCODERS are package-private in consensus v1.29.0 (only the verifiers are
// exported). An external producer — and any end-to-end "valid cert verifies
// through the gate" test — needs those encoders exported (a small, additive
// consensus change). The verify path here needs no such export: it consumes
// a fully-formed *ConsensusCert.
type Producer interface {
Produce(ctx context.Context, subject Subject) (*qcert.ConsensusCert, error)
}
// MaybeProduce is the checkpoint producer-request site. It is nil-safe and
// activation-aware so the accept hook can call it unconditionally: a nil gate,
// dormant activation, a non-checkpoint height, or a nil producer all short-
// circuit to (nil, nil) — the verify-only default. When a producer IS wired and
// the checkpoint is live, it requests the cert; the caller gossips/stores it.
//
// This keeps producer cadence and verify cadence on ONE definition (g.IsCheckpoint),
// so producer and verifier can never disagree on which heights carry certs.
func (g *Gate) MaybeProduce(ctx context.Context, producer Producer, cp Checkpoint) (*qcert.ConsensusCert, error) {
if g == nil || producer == nil {
return nil, nil
}
if !g.Activated(cp.Height) || !g.IsCheckpoint(cp.Height) {
return nil, nil
}
return producer.Produce(ctx, g.subjectFrom(cp))
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import (
"sync"
qcert "github.com/luxfi/consensus/protocol/quasar"
)
// CertStore resolves the QuasarCert that certifies a finalized block. In
// production it is filled by the cert gossip/ingest path (the producer
// follow-on, producer.go); the verify gate only READS from it. Keyed by the
// finalized position (chainID, height, blockID) so a cert can never be returned
// for the wrong block.
type CertStore interface {
Lookup(chainID uint32, height uint64, blockID [32]byte) (*qcert.ConsensusCert, bool)
}
type certKey struct {
chainID uint32
height uint64
blockID [32]byte
}
// MemCertStore is an in-memory CertStore keyed by (chainID, height, blockID). It
// is the ingest sink the cert-gossip handler writes into (Put) and the gate
// reads from (Lookup). Safe for concurrent use.
type MemCertStore struct {
mu sync.RWMutex
certs map[certKey]*qcert.ConsensusCert
}
// NewMemCertStore returns an empty in-memory cert store.
func NewMemCertStore() *MemCertStore {
return &MemCertStore{certs: make(map[certKey]*qcert.ConsensusCert)}
}
// Put indexes a cert by its own (ChainID, Height, BlockHash). The ingest path
// MUST verify a cert before Put (verify-before-store), exactly as the gossip
// layer verifies before re-gossip; the gate re-verifies at the checkpoint so a
// store poisoned by an unverified Put still cannot finalize an invalid cert.
func (m *MemCertStore) Put(cert *qcert.ConsensusCert) {
if cert == nil {
return
}
k := certKey{chainID: cert.ChainID, height: cert.Height, blockID: cert.BlockHash}
m.mu.Lock()
m.certs[k] = cert
m.mu.Unlock()
}
// Lookup returns the cert for the finalized position, or (nil, false).
func (m *MemCertStore) Lookup(chainID uint32, height uint64, blockID [32]byte) (*qcert.ConsensusCert, bool) {
k := certKey{chainID: chainID, height: height, blockID: blockID}
m.mu.RLock()
c, ok := m.certs[k]
m.mu.RUnlock()
return c, ok
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import (
qcert "github.com/luxfi/consensus/protocol/quasar"
)
// ValidatorSetProvider resolves the committed validator set the verifier pins a
// cert against, for a (chain, epoch). Post-activation the gate calls this once
// per verified checkpoint.
type ValidatorSetProvider interface {
ValidatorSet(chainID uint32, epoch uint64) (qcert.ConsensusValidatorSet, error)
}
// ValidatorSet is a concrete ConsensusValidatorSet for one epoch: the committed
// weighted-validator-set root plus the per-leg verification keys the cert legs
// verify against (the classical BLS aggregate key for the Beam leg and the
// Pulsar ML-DSA threshold group key for the Pulsar leg — the HYBRID_PQ pair).
//
// Production wiring (the activation seam): in production these fields are
// populated from the P-Chain-pinned validator set (Root, Epoch) and the active
// KeyEra group keys for the epoch. That population is era/rotation-coupled and
// lands with the producer + KeyEra-registry wiring (see producer.go). Corona
// (STRICT_DUAL_PQ) and Magnetar/P3Q (POLARIS / RECOVERY) group keys + the
// weighted-sig-set config are populated by that same follow-on; until then this
// set serves the HYBRID_PQ pair and reports "no key" for the other lanes.
type ValidatorSet struct {
root [48]byte
epoch uint64
blsAggKey []byte // classical BLS-12-381 aggregate pubkey (Beam leg)
pulsarGroup []byte // Pulsar ML-DSA threshold group pubkey (Pulsar leg)
}
var _ qcert.ConsensusValidatorSet = (*ValidatorSet)(nil)
// NewValidatorSet builds a committed validator set for one epoch from its root
// and the HYBRID_PQ verification keys.
func NewValidatorSet(root [48]byte, epoch uint64, blsAggKey, pulsarGroup []byte) *ValidatorSet {
return &ValidatorSet{root: root, epoch: epoch, blsAggKey: blsAggKey, pulsarGroup: pulsarGroup}
}
// Root returns the 48-byte weighted-validator-set commitment.
func (v *ValidatorSet) Root() [48]byte { return v.root }
// Epoch returns the epoch this set was committed under.
func (v *ValidatorSet) Epoch() uint64 { return v.epoch }
// WeightedConfig returns the QuorumVerifierConfig for the WeightedSigSet
// evidence mode. HYBRID_PQ does not use weighted-sig-set legs; the zero config
// is correct here and is populated by the POLARIS / RECOVERY follow-on.
func (v *ValidatorSet) WeightedConfig() qcert.QuorumVerifierConfig {
return qcert.QuorumVerifierConfig{}
}
// WeightedEnvelope returns the round-digest posture axes for the inner
// WeightedQuorumCert. Zero for HYBRID_PQ (no weighted-sig-set leg); populated by
// the POLARIS / RECOVERY follow-on.
func (v *ValidatorSet) WeightedEnvelope() qcert.QuorumMessageEnvelope {
return qcert.QuorumMessageEnvelope{}
}
// ThresholdGroupKey returns the threshold-signature group public key for a leg
// kind. Serves the Pulsar (ML-DSA) lane; reports (zero, false) for the others
// until their group keys are wired by the follow-on.
func (v *ValidatorSet) ThresholdGroupKey(kind qcert.LegKind) (qcert.ThresholdGroupKey, bool) {
if kind == qcert.LegPulsarMLDSA && len(v.pulsarGroup) > 0 {
return qcert.ThresholdGroupKey{Kind: qcert.LegPulsarMLDSA, PulsarGroupKey: v.pulsarGroup}, true
}
return qcert.ThresholdGroupKey{}, false
}
// ClassicalAggregateKey returns the classical aggregate verification key for a
// scheme. Serves the BLS-12-381 Beam leg.
func (v *ValidatorSet) ClassicalAggregateKey(scheme qcert.ClassicalScheme) ([]byte, bool) {
if scheme == qcert.ClassicalSchemeBLS12381 && len(v.blsAggKey) > 0 {
return v.blsAggKey, true
}
return nil, false
}
// StaticValidatorSetProvider returns the same committed set for every (chain,
// epoch). It is the single-era / test provider; the production provider resolves
// per-epoch sets from the P-Chain validator manager + KeyEra registry.
type StaticValidatorSetProvider struct{ Set qcert.ConsensusValidatorSet }
// ValidatorSet implements ValidatorSetProvider.
func (p StaticValidatorSetProvider) ValidatorSet(_ uint32, _ uint64) (qcert.ConsensusValidatorSet, error) {
if p.Set == nil {
return nil, ErrValidatorSetUnavailable
}
return p.Set, nil
}