Files
deb599d614 quasar: add the Pulsar threshold ML-DSA certificate profile (#12)
Add protocol/quasar/pulsar -- the consensus-side adapter that wires
luxfi/pulsar's threshold ML-DSA RoundSigner into Quasar as a certificate
profile. Consensus consumes Pulsar; Pulsar never consumes consensus.

Decomplected ownership (the load-bearing boundary):

  Consensus owns ORCHESTRATION: validator-set membership, committee
  sampling, session binding, canonical nonce selection, QC aggregation,
  signer bitmap, ConsensusCert assembly, retries, fallback, slashing.

  Pulsar owns CRYPTO: the two-round shape (Round1 binds the nonce,
  Round2 emits a proof-carrying z partial), z aggregation mod q, public
  hint recovery (FindHint over w' = A*z - c*t1*2^d), and the FIPS 204
  ML-DSA signature.

PulsarRoundSigner implements pulsar.RoundSigner (Round1/Round2/Finalize).
The secret-bearing FIPS 204 assembly stays package-private to luxfi/pulsar
and is injected as a pluggable core; until a reviewed Boundary-Cleared /
Carry-Elimination signer ships (pulsar.ProductionBCCSigningReady), Finalize
fails closed -- it returns a structurally-complete ConsensusCert with an
EMPTY signature and ErrProfileNotReady, and the orchestrator falls back to
Corona. No secret residual is ever opened on this path.

This forward-ports the profile onto the v1.35 release line. It consumes the
pulsar v1.9.0 pkg/pulsar package already pinned on this line -- no go.mod /
go.sum change, no dependency bump, and the new import adds zero transitive
sums. Purely additive: six new files, no existing file touched, so no
existing exported consensus symbol changes and no other consumer is affected.
The strict-PQ triple gate (engine.go generateCert: BLS + Corona + ML-DSA) is
untouched and still refuses to finalize without all three legs.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-07 17:43:43 -07:00

305 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// round_signer_test.go -- focused tests that the Pulsar cert profile builds,
// implements the luxfi/pulsar RoundSigner contract, and that a round-trip stub
// (Round1 -> Round2 -> Finalize) drives the consensus-owned orchestration
// correctly using the exported pulsar primitives.
//
// These tests deliberately exercise ONLY the consensus-side orchestration this
// package owns. The secret-bearing FIPS 204 signature assembly lives inside
// luxfi/pulsar and is fail-closed until its BCC/CEF ZK proofs are reviewed; we
// assert the fail-closed fallback and (via a stub core) the success wiring,
// without claiming a real threshold signature.
package pulsar
import (
"bytes"
"testing"
pulsarlib "github.com/luxfi/pulsar/pkg/pulsar"
)
func init() {
// The hot path's Round2 calls pulsar.VerifyZPartial, which delegates the
// zero-knowledge check to the registered PartialZVerifier (fail-closed by
// default). For the orchestration round-trip we register an accept-bindings
// verifier exactly as luxfi/pulsar's own partial_test.go does; the public
// bindings (party/session/nonce/z) are still enforced unconditionally.
pulsarlib.RegisterPartialZVerifier(acceptBindingsOnly{})
}
type acceptBindingsOnly struct{}
func (acceptBindingsOnly) VerifyPartial(*pulsarlib.Partial, []byte, []byte, []byte) error {
return nil
}
// stubCore is a TEST-ONLY signature core: it returns a deterministic,
// non-empty Signature so we can exercise the Finalize success path. It is NOT a
// real threshold ML-DSA signer (that must live in luxfi/pulsar). It proves the
// wiring: Finalize hands the core the PUBLIC aggregate + nonce cert + session,
// and threads the returned Signature into the ConsensusCert.
type stubCore struct{ sawZSumLen int }
func (c *stubCore) AssembleSignature(
_ PulsarSession,
cert pulsarlib.NonceCert,
agg pulsarlib.Aggregate,
) (pulsarlib.Signature, error) {
c.sawZSumLen = len(agg.ZSum)
return pulsarlib.Signature{Mode: pulsarlib.ModeP65, Bytes: []byte("stub-sig")}, nil
}
// fullSession returns a PulsarSession with every security-relevant root set
// (so Validate passes) and a NoncePoolRoot matching the supplied pool root.
func fullSession(poolRoot [32]byte) PulsarSession {
mk := func(b byte) [32]byte {
var x [32]byte
for i := range x {
x[i] = b
}
return x
}
return PulsarSession{
ChainID: mk(0x11),
Epoch: 7,
Height: 42,
Round: 3,
BlockHash: mk(0x22),
ValidatorSetRoot: mk(0x33),
JointPKID: mk(0x44),
DKGTranscriptRoot: mk(0x55),
CommitteeID: mk(0x66),
SignerSetRoot: mk(0x77),
NoncePoolRoot: poolRoot,
ProtocolVersion: 1,
}
}
// makePool builds a one-cert in-memory pool whose single NonceCert carries a
// non-zero transcript root (so the ConsensusCert transcript binding is real).
func makePool() (*MemNonceCertPool, [32]byte) {
var poolRoot [32]byte
for i := range poolRoot {
poolRoot[i] = 0xAB
}
var nonceID, trRoot [32]byte
for i := range nonceID {
nonceID[i] = 0xC0
trRoot[i] = 0x0D
}
cert := pulsarlib.NonceCert{NonceID: nonceID, NonceTranscriptRoot: trRoot}
return NewMemNonceCertPool([]pulsarlib.NonceCert{cert}, poolRoot), nonceID
}
// TestPulsarRoundSignerImplementsContract is a compile-time + runtime check
// that the profile satisfies luxfi/pulsar's RoundSigner and reports the right
// profile value.
func TestPulsarRoundSignerImplementsContract(t *testing.T) {
var rs pulsarlib.RoundSigner = &PulsarRoundSigner{}
if got := rs.Profile(); got != pulsarlib.ProfilePulsar {
t.Fatalf("Profile() = %v, want ProfilePulsar", got)
}
}
// TestRoundTripFailClosed drives Round1 -> Round2 -> Finalize with NO signature
// core: the orchestration completes (canonical nonce, canonical signer subset,
// z-aggregate, ConsensusCert), but the FIPS 204 signature is empty and the
// error is the Corona-fallback signal.
func TestRoundTripFailClosed(t *testing.T) {
pool, nonceID := makePool()
sess := fullSession(pool.Root())
if err := sess.Validate(); err != nil {
t.Fatalf("session Validate: %v", err)
}
sid := sess.SessionID()
s := &PulsarRoundSigner{
Session: sess,
Pool: pool,
Threshold: 2,
ValidatorSetSize: 8,
L: 5, // ML-DSA-65 secret dimension â„“
Core: nil,
}
// Round1: bind the canonical nonce.
canonical, ok := pool.At(pulsarlib.CanonicalNonceIndex(sid, pool.Root(), pool.Size()))
if !ok {
t.Fatal("pool.At canonical index failed")
}
r1, err := s.Round1(sid, nonceID, canonical)
if err != nil {
t.Fatalf("Round1: %v", err)
}
if r1.SessionID != sid || r1.NonceID != nonceID {
t.Fatal("Round1 did not bind session/nonce")
}
// Round2: two signers' z-partials. ZShare length must match L*N*polyBytes
// only matters to the real core; for aggregation we just need consistent
// non-empty packed vectors.
partials := make([]pulsarlib.Partial, 0, 2)
for _, party := range []uint32{2, 5} {
in := pulsarlib.PartialInput{
PartyID: party,
ZShare: bytes.Repeat([]byte{byte(party)}, 5*256*4),
}
p, err := s.Round2(r1, in)
if err != nil {
t.Fatalf("Round2 party %d: %v", party, err)
}
partials = append(partials, p)
}
// Finalize: structurally complete cert, empty signature, fallback error.
agg, cert, err := s.Finalize(r1, partials)
if err != ErrProfileNotReady {
t.Fatalf("Finalize err = %v, want ErrProfileNotReady", err)
}
if len(cert.Signature.Bytes) != 0 {
t.Fatal("expected empty signature on fail-closed path")
}
// The ConsensusCert must still be a valid accountability artifact:
// quorum=2 over the canonical 2-signer bitmap, all bits in-set.
// Published pulsar folds full attestation (AccountabilityQC + quorum sigs)
// into ConsensusCert.Verify; the fail-closed path deliberately leaves the QC
// unset (consensus owns QC aggregation, not this profile). Assert the
// structural invariants Finalize DOES guarantee: bitmap weight == threshold
// and every set bit indexes a validator.
weight := 0
for i := 0; i < len(cert.SignerBitmap)*8; i++ {
if cert.SignerBitmap[i/8]&(1<<(uint(i)%8)) != 0 {
if i >= s.ValidatorSetSize {
t.Fatalf("signer bit %d out of validator set (size %d)", i, s.ValidatorSetSize)
}
weight++
}
}
if weight != 2 {
t.Fatalf("signer-bitmap weight = %d, want threshold 2", weight)
}
if cert.TranscriptRoot != canonical.NonceTranscriptRoot {
t.Fatal("ConsensusCert did not bind the nonce transcript root")
}
if cert.Epoch != sess.Epoch || cert.Height != sess.Height || cert.Round != sess.Round {
t.Fatal("ConsensusCert did not bind the consensus round")
}
if len(agg.ZSum) == 0 {
t.Fatal("aggregate z-sum is empty")
}
}
// TestRoundTripWithStubCore proves the success wiring: with a (stub) core
// registered, Finalize threads the assembled signature into the cert and hands
// the core the public aggregate.
func TestRoundTripWithStubCore(t *testing.T) {
pool, nonceID := makePool()
sess := fullSession(pool.Root())
sid := sess.SessionID()
core := &stubCore{}
s := &PulsarRoundSigner{
Session: sess, Pool: pool, Threshold: 2, ValidatorSetSize: 8, L: 5, Core: core,
}
canonical, _ := pool.At(pulsarlib.CanonicalNonceIndex(sid, pool.Root(), pool.Size()))
r1, err := s.Round1(sid, nonceID, canonical)
if err != nil {
t.Fatalf("Round1: %v", err)
}
var partials []pulsarlib.Partial
for _, party := range []uint32{1, 4} {
p, err := s.Round2(r1, pulsarlib.PartialInput{PartyID: party, ZShare: bytes.Repeat([]byte{byte(party)}, 5*256*4)})
if err != nil {
t.Fatalf("Round2: %v", err)
}
partials = append(partials, p)
}
_, cert, err := s.Finalize(r1, partials)
if err != nil {
t.Fatalf("Finalize: %v", err)
}
if string(cert.Signature.Bytes) != "stub-sig" {
t.Fatal("Finalize did not thread the core's signature into the cert")
}
if core.sawZSumLen == 0 {
t.Fatal("core was not handed the aggregated z-sum")
}
}
// TestEmptyPoolFallsBack: an empty NonceCert pool yields the Corona-fallback
// signal, never an inline nonce.
func TestEmptyPoolFallsBack(t *testing.T) {
var root [32]byte
root[0] = 1
empty := NewMemNonceCertPool(nil, root)
sess := fullSession(root)
s := &PulsarRoundSigner{Session: sess, Pool: empty, Threshold: 2, ValidatorSetSize: 8, L: 5}
var nid [32]byte
nid[0] = 9
if _, err := s.Round1(sess.SessionID(), nid, pulsarlib.NonceCert{NonceID: nid}); err != ErrNonceCertPoolEmpty {
t.Fatalf("Round1 on empty pool err = %v, want ErrNonceCertPoolEmpty", err)
}
}
// TestNonCanonicalNonceRejected: a cert that is not the canonical pool
// selection for the session is refused (anti-grind).
func TestNonCanonicalNonceRejected(t *testing.T) {
pool, _ := makePool()
sess := fullSession(pool.Root())
s := &PulsarRoundSigner{Session: sess, Pool: pool, Threshold: 2, ValidatorSetSize: 8, L: 5}
var bogus [32]byte
bogus[0] = 0xFF // not the canonical nonce id
if _, err := s.Round1(sess.SessionID(), bogus, pulsarlib.NonceCert{NonceID: bogus}); err != ErrNonCanonicalNonce {
t.Fatalf("Round1 with non-canonical nonce err = %v, want ErrNonCanonicalNonce", err)
}
}
// TestSessionIDBindsAllFields: changing any one binding field changes the
// session id (the binding is load-bearing across the whole round context).
func TestSessionIDBindsAllFields(t *testing.T) {
var root [32]byte
root[0] = 0xAB
base := fullSession(root)
baseID := base.SessionID()
mutators := map[string]func(*PulsarSession){
"chain_id": func(s *PulsarSession) { s.ChainID[0] ^= 1 },
"epoch": func(s *PulsarSession) { s.Epoch ^= 1 },
"height": func(s *PulsarSession) { s.Height ^= 1 },
"round": func(s *PulsarSession) { s.Round ^= 1 },
"block_hash": func(s *PulsarSession) { s.BlockHash[0] ^= 1 },
"validator_set_root": func(s *PulsarSession) { s.ValidatorSetRoot[0] ^= 1 },
"joint_pk_id": func(s *PulsarSession) { s.JointPKID[0] ^= 1 },
"dkg_transcript_root": func(s *PulsarSession) { s.DKGTranscriptRoot[0] ^= 1 },
"committee_id": func(s *PulsarSession) { s.CommitteeID[0] ^= 1 },
"signer_set_root": func(s *PulsarSession) { s.SignerSetRoot[0] ^= 1 },
"nonce_pool_root": func(s *PulsarSession) { s.NoncePoolRoot[0] ^= 1 },
"protocol_version": func(s *PulsarSession) { s.ProtocolVersion ^= 1 },
}
for name, mut := range mutators {
s := base
mut(&s)
if s.SessionID() == baseID {
t.Fatalf("mutating %q did not change the session id", name)
}
}
}
// TestSessionValidateRejectsUnsetRoots: an all-zero security-relevant root is
// refused.
func TestSessionValidateRejectsUnsetRoots(t *testing.T) {
var root [32]byte
root[0] = 1
good := fullSession(root)
if err := good.Validate(); err != nil {
t.Fatalf("full session must validate, got %v", err)
}
bad := good
bad.DKGTranscriptRoot = [32]byte{} // unbind a field
if err := bad.Validate(); err != ErrSessionFieldUnset {
t.Fatalf("unset DKG transcript root err = %v, want ErrSessionFieldUnset", err)
}
}