mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Add the canonical parallel post-quantum finality posture: every cert carries
a Pulsar (Module-LWE / FIPS-204 ML-DSA) leg AND a Corona (Ring-LWE, dealerless)
leg, in parallel, AND-mode (both required to finalize). Security is the safest
surviving leg: an adversary must break BOTH the Module-LWE leg and the Ring-LWE
leg in the same round (orthogonal hardness, conjunctive acceptance).
dualpq.go (production):
- DualPQRequiredLegs(paramSet): the canonical {Pulsar, Corona} AND-mode
required-leg set (matches config CertModeStrict+CertVariantStrict).
- ComposeDualPQEvidence(...): pure-function composition of the two
threshold-sig leg evidences from a real Corona threshold signature + a
real Pulsar FIPS-204 signature over the same envelope message. Composition
over inheritance — wraps each primitive's output, extends neither.
consensus_cert_dualpq_test.go (multi-node proof):
- Corona leg: genuine multi-node two-round t-of-n threshold ceremony — N
separate single-share Signers, Round1/Round2 over an explicit message bus,
aggregated by one node from Round2 DATA (never shares).
- Pulsar leg: real FIPS-204 ML-DSA-65 signature accepted byte-for-byte by
the on-chain verifier (verifyPulsarLeg -> pulsarwire.VerifyBytes).
- Proves: AND-mode (both legs required + verify), sub-quorum on EITHER leg
fails finality, corrupt either leg fails, composer refuses a half cert.
- Single-share invariant: compile-time witness that the only Corona signing
constructor takes EXACTLY ONE *KeyShare; aggregator takes Round2 DATA.
Permissionless-safe today via the dealerless Corona leg in AND-mode; the
Pulsar leg's no-reconstruct t-of-n threshold production is documented as
blocked on a poly-vector secret-share type (ML-DSA s1 is non-linear in the
GF(257) seed-share) — see luxfi/pulsar BLOCKERS.md PULSAR-V12-PARALLEL-PQ.
No fakes: the Pulsar leg signature is a real FIPS-204 group-key signature,
not a claimed threshold ceremony.
141 lines
6.5 KiB
Go
141 lines
6.5 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// dualpq.go — the canonical parallel-PQ (Pulsar ‖ Corona) AND-mode posture.
|
|
//
|
|
// Every Quasar finality cert in this posture carries TWO independent
|
|
// post-quantum threshold legs, in parallel, both REQUIRED to finalize:
|
|
//
|
|
// Pulsar leg — Module-LWE, FIPS-204 ML-DSA threshold signature.
|
|
// Verified by an unmodified FIPS-204 verifier
|
|
// (verifyPulsarLeg → pulsarwire.VerifyBytes). On-chain
|
|
// precompile: ML-DSA at 0x…2200 (C-Chain) / 0x…2300 (Q-Chain).
|
|
// Corona leg — Ring-LWE, dealerless 2-round threshold signature
|
|
// (Boschini et al, ePrint 2024/1113). Verified by
|
|
// verifyCoronaLeg → coronaThreshold.Verify. On-chain
|
|
// precompile: Corona at 0x5202… (C-Chain) / 0x5302… (Q-Chain).
|
|
//
|
|
// WHY AND-MODE, NOT OR. Finality security is the security of the SAFEST
|
|
// surviving leg, not the weakest. With OR-mode, an adversary who breaks
|
|
// EITHER lattice family forges a cert; with AND-mode it must break BOTH
|
|
// the Module-LWE leg AND the Ring-LWE leg in the SAME round. Two distinct
|
|
// hardness assumptions (MLWE ⟂ RLWE) and two distinct schemes (a hint-
|
|
// recovering FIPS-204 path ⟂ a Pedersen-DKG Ring-LWE path) must fall
|
|
// together. This is the cryptographic meaning of "parallel defense in
|
|
// depth": orthogonal assumptions, conjunctive acceptance.
|
|
//
|
|
// WHY THIS IS PERMISSIONLESS-SAFE TODAY. The permissionless guarantee
|
|
// rests on the Corona leg, whose group key is installed by a genuine
|
|
// DEALERLESS distributed key generation (no party ever holds the master
|
|
// secret; corona/keyera Pedersen DKG). Even where the Pulsar leg's genesis
|
|
// key is installed under a fenced trusted/TEE ceremony (see
|
|
// PULSAR_THRESHOLD_STATUS — a byte-FIPS-204-compatible dealerless ML-DSA
|
|
// DKG is research-grade and not yet shipped), an attacker who compromises
|
|
// that ceremony and forges a Pulsar leg STILL cannot finalize: AND-mode
|
|
// requires the Corona leg too, and Corona's dealerless key has no single
|
|
// point of forgery. The two legs are decomplected — neither leg's genesis
|
|
// trust assumption can weaken the other's. (Pulsar threshold-signing status:
|
|
// luxfi/pulsar BLOCKERS.md PULSAR-V12-PARALLEL-PQ — a byte-FIPS-204-compatible
|
|
// no-reconstruct ML-DSA threshold signer is gated on a poly-vector share type
|
|
// + the V13 ZK proofs and is not yet shipped.)
|
|
//
|
|
// DECOMPLECTED. This file owns ONLY the parallel-PQ leg-set definition and
|
|
// the pure-function composition of the two leg evidences. "Which legs are
|
|
// required" stays in config.CertPolicy.RequiredLegs() (the operator knob:
|
|
// CertModeStrict + CertVariantStrict ⇒ {Pulsar, Corona}); "does a leg
|
|
// verify" stays in the verify<Leg>Leg helpers; "is a leg required HERE"
|
|
// stays in the envelope. This is composition, not extension: a dual-PQ
|
|
// cert WRAPS a corona threshold output and a pulsar threshold output; it
|
|
// does not subclass either signer.
|
|
|
|
package quasar
|
|
|
|
import (
|
|
"errors"
|
|
|
|
coronaThreshold "github.com/luxfi/threshold/protocols/corona"
|
|
)
|
|
|
|
var (
|
|
// ErrDualPQMissingPulsar — the Pulsar leg signature is empty. A dual-PQ
|
|
// cert is AND-mode: a missing Pulsar leg cannot be composed.
|
|
ErrDualPQMissingPulsar = errors.New("quasar: dual-PQ cert requires a non-empty Pulsar (ML-DSA) leg")
|
|
|
|
// ErrDualPQMissingCorona — the Corona leg signature is nil. A dual-PQ
|
|
// cert is AND-mode: a missing Corona leg cannot be composed.
|
|
ErrDualPQMissingCorona = errors.New("quasar: dual-PQ cert requires a non-nil Corona (Ring-LWE) leg")
|
|
)
|
|
|
|
// DualPQRequiredLegs returns the canonical AND-mode dual-lattice required-leg
|
|
// set for the given parameter set: a Pulsar (Module-LWE / FIPS-204 ML-DSA)
|
|
// leg AND a Corona (Ring-LWE) leg. BOTH are required — this is the leg set a
|
|
// ConsensusCertPolicy.RequiredLegs() returns for the pure-PQ dual-lattice
|
|
// posture (config CertModeStrict + CertVariantStrict).
|
|
//
|
|
// Order is canonical: Pulsar first, then Corona. HashRequiredLegs binds the
|
|
// legs in the order returned, so the order is part of the committed
|
|
// RequiredLegsRoot — do not reorder.
|
|
func DualPQRequiredLegs(paramSet uint8) []LegSpec {
|
|
return []LegSpec{
|
|
{Kind: LegPulsarMLDSA, ParamSetID: paramSet},
|
|
{Kind: LegCoronaLattice, ParamSetID: paramSet},
|
|
}
|
|
}
|
|
|
|
// ComposeDualPQEvidence builds the two threshold-sig LegEvidence entries for a
|
|
// dual-PQ ConsensusCert from a real Pulsar (FIPS-204 ML-DSA) threshold
|
|
// signature and a real Corona (Ring-LWE) threshold signature, both produced
|
|
// over the SAME envelope message (consensusCertMessage), plus the shared
|
|
// accountability binding (signer root + aggregate weight).
|
|
//
|
|
// Pure function: same inputs ⇒ same evidence bytes. No mutable state, no
|
|
// randomness. Each leg ships its primitive's own output — pulsarSig is the
|
|
// PULS-framed FIPS-204 signature bytes (pulsarwire.Signature.MarshalBinary);
|
|
// coronaSig is the typed Corona threshold signature, wire-framed here via
|
|
// EncodeCoronaSig. The aggregator that calls this holds neither party's key
|
|
// share — it composes already-produced signatures (composition over
|
|
// inheritance).
|
|
//
|
|
// The returned slice is ordered Pulsar-then-Corona to match
|
|
// DualPQRequiredLegs; the envelope matches evidence to required legs by Kind,
|
|
// so order is not load-bearing for verification, but it is kept canonical.
|
|
func ComposeDualPQEvidence(
|
|
paramSet uint8,
|
|
signerRoot [32]byte,
|
|
aggregateWeight uint64,
|
|
pulsarSig []byte,
|
|
coronaSig *coronaThreshold.Signature,
|
|
) ([]LegEvidence, error) {
|
|
if len(pulsarSig) == 0 {
|
|
return nil, ErrDualPQMissingPulsar
|
|
}
|
|
if coronaSig == nil {
|
|
return nil, ErrDualPQMissingCorona
|
|
}
|
|
|
|
// One accountability binding, shared by both legs: both threshold quorums
|
|
// signed the SAME envelope message, which commits this signer root. The
|
|
// envelope cross-checks Accountability.SignerRoot == cert.SignerRoot and
|
|
// AggregateWeight >= policy.ThresholdWeight() per leg (I8).
|
|
acct := &ThresholdAccountability{SignerRoot: signerRoot, AggregateWeight: aggregateWeight}
|
|
|
|
return []LegEvidence{
|
|
{
|
|
Leg: LegSpec{Kind: LegPulsarMLDSA, ParamSetID: paramSet},
|
|
Mode: EvidenceThresholdSig,
|
|
Payload: encodeThresholdSigPayload(&ThresholdSigPayload{
|
|
Signature: pulsarSig,
|
|
Accountability: acct,
|
|
}),
|
|
},
|
|
{
|
|
Leg: LegSpec{Kind: LegCoronaLattice, ParamSetID: paramSet},
|
|
Mode: EvidenceThresholdSig,
|
|
Payload: encodeThresholdSigPayload(&ThresholdSigPayload{
|
|
Signature: EncodeCoronaSig(coronaSig),
|
|
Accountability: acct,
|
|
}),
|
|
},
|
|
}, nil
|
|
}
|