mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Add protocol/quasar/pulsar -- the consensus-side adapter that wires the
luxfi/pulsar threshold ML-DSA primitives into Quasar as a cert 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
inputs.
Pulsar owns CRYPTO: the two-round shape (Round1 binds the nonce,
Round2 emits a proof-carrying z partial), z aggregation mod q, the
public hint recovery (FindHint over w' = A*z - c*t1*2^d), and the
final FIPS 204 ML-DSA signature.
PulsarRoundSigner implements luxfi/pulsar's RoundSigner interface:
Round1 selects the canonical NonceCert via pulsar.CanonicalNonceIndex
over the session and refuses any non-canonical nonce
(anti-grind: a coordinator cannot pick w1, hence the
challenge, by choosing among boundary-clear certs after
seeing the message).
Round2 emits one signer's z Partial; pulsar.VerifyZPartial enforces
the public bindings + the registered ZK correctness check.
Finalize selects the canonical signer subset
(pulsar.CanonicalSignerSet), aggregates z
(pulsar.MergeAggregates), and assembles the two-certificate
ConsensusCert accountability artifact (signer bitmap +
transcript root).
The final FIPS 204 signature byte production is secret-bearing (needs
the module matrix A, t1, the challenge, and the per-coefficient hint),
all package-private to luxfi/pulsar, so it stays there: Finalize
delegates to a pluggable SignatureCore. The core is fail-closed until
luxfi/pulsar ships a reviewed Boundary-Cleared / Carry-Elimination
signer (pulsar.ProductionBCCSigningReady); until then Finalize 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.
Session binding (session.go): the 32-byte session id is a
domain-separated cSHAKE256 over chain_id, epoch, height, round,
block_hash, validator_set_root, joint_pk_id, dkg_transcript_root,
committee_id, signer_set_root, nonce_pool_root, protocol_version --
every field fixed-width for canonical (non-grindable) encoding. Validate
refuses a session with an all-zero security-relevant root (cross-round
replay guard).
NonceCert generation is a BACKGROUND validator subprotocol filling a
NonceCertPool (pool.go) -- never inline on the block hot path. An empty
pool yields ErrNonceCertPoolEmpty (fall back to Corona), never an inline
nonce.
The profile registers by the package convention (like wave_signer.go's
RoundSigner): a self-contained pulsar.RoundSigner whose ConsensusCert
Signature bytes feed the existing QuasarCert.Pulsar slot and verify via
the polaris.go verifyPulsarLeg / pulsarwire.VerifyBytes path already
wired. No change to polaris.go (cert assembly is a separate concern).
go.mod: replace github.com/luxfi/pulsar => ../luxfi/pulsar. The
RoundSigner / NonceCert / CertProfile API lands on the
fix/threshold-mldsa-hint-leak branch and is not in any published tag
(cached v1.1.1 predates it). Remove the replace once a tag carrying the
API ships.
Tests (protocol/quasar/pulsar, all green): contract implementation,
Round1->Round2->Finalize fail-closed fallback, stub-core success wiring,
empty-pool fallback, non-canonical-nonce rejection, full session-id
field binding, session validation. go build ./... green; existing
protocol/quasar suite unchanged and green.