The AST name call-graph follows DIRECT named calls only; it misses function-
value / closure / go:linkname indirection (RED PoC: e:=deriveKeyMaterial;
s:=bccSign; s(e(...)) not flagged). The old comment overstated 'never miss one'.
Fix: pair the reach graph with a companion lint forbidBannedPrimitiveIndirection
that forbids taking ANY banned primitive as a non-call value or aliasing it via
//go:linkname anywhere in the default build. With that lint green, every banned-
primitive use in the build IS a direct call the graph follows — so the PAIR is
complete for the banned set (neither half claimed complete alone). Wording
softened to state exactly this.
GATE: TestGATE_C_NoBannedPrimitiveValueIndirection — (a) the real build takes no
banned primitive as a value / linkname; (b) the name graph alone MISSES the
e:=deriveKeyMaterial; s:=bccSign indirection while the lint CATCHES it; (c)
go:linkname aliasing a banned primitive is caught.
AggregateBCCWithBlame consumed raw []Partial; PartyID was unauthenticated, so an
attacker could stamp a victim's slot on a forged partial and (via first-per-
PartyID + duplicate rules) get the honest victim BLAMED and front-run-EXCLUDED.
No forge/leak (capstone holds), but blame misattributed.
Fix: bind each partial to its producer's long-term identity key.
- Partial gains Author + AuthSig; Round2 signs (author ‖ slot ‖ H(PartyID,
session,nonce,ZShare,Proof)) via an injected IdentitySigner (SetIdentity).
- AggregateBCCWithBlame gains (quorum, epoch, AbortSignatureVerifier). When a
verifier is supplied it AUTHENTICATES each partial BEFORE attribution: accept
only if AuthSig verifies AND Author == quorum[PartyID]; drop forged/wrong-slot/
unsigned partials with NO blame against the slot's honest owner, BEFORE the
first-per-PartyID/duplicate logic (so a forgery cannot evict the victim).
- Blame is emitted ONLY for authenticated partials and NEVER off a raw PartyID
(auth==nil => no blame produced — attribution fails closed). Wraps
protocol_auth.SignedProtocolMessage (one identity layer). Networked transport
auth stays a flagged residual.
GATE B updated to authenticated partials (a deviator SIGNS its bad partial =>
attributable; a transport-tampered one breaks its sig => dropped, not framed).
New TestRED_PoC_MEDIUM_CannotFrameOrExcludeHonestVictim: forged victim-slot
partial is dropped (attacker can't sign as victim) -> victim NOT blamed, NOT
excluded, round still signs; contrast shows the verifier is load-bearing.
The dedup key was H(committeeID‖w1). The same joint nonce (same w1) used in two
committees that share a victim validator produced DISTINCT keys, so the victim's
ledger did not dedup across committees -> victim emits a partial in each -> the
attacker collects two partials on one nonce.
Fix: key on w1 alone. This is sound precisely BECAUSE the ledger is now
per-validator-share (resolved by shareIdentityKey): within the victim's one
ledger the same w1 collapses to ONE key regardless of committee. committeeID
stays on the NonceTicket/binding for audit and on TicketID; only the MaterialKey
drops it.
GATE: TestRED_LOW_CrossCommittee_SameNonce_Deduped — two distinct committees
sharing a victim, same w1 -> equal MaterialKey -> second reserve ErrNonceReused.
NewDistributedBCCSigner gave each instance its OWN empty in-memory ledger;
since a signer fixes (sid,ctx,msg) at construction, signing two messages needs
two signer objects -> two empty ledgers -> cross-message nonce reuse slipped
unless the integrator hand-threaded ONE ledger via SetNonceLedger. The
reference runBCCCeremony never did, so the shipped default was fail-open and
RED recovered the victim's full s1-share on the bare API.
Fix: resolve the single-use ledger from a process-global registry keyed by
SHARE IDENTITY (shareIdentityKey = H(mode,nodeID,evalPoint,secret-share)).
Every signer over the same key-share resolves to ONE ledger -> cross-instance
reuse refused by DEFAULT, no per-instance empty-ledger path. SetNonceLedger now
installs a persistent ledger into the same per-share slot (first-writer-wins);
the in-process default is safe NOW, persistence stays a flagged residual.
GATE A extended: TestRED_PoC_DefaultLedger_NonceReuse_Refused drives the REAL
API like runBCCCeremony (fresh signer per message, NO SetNonceLedger) and
asserts the second Round2 on the reused nonce + the relabel variant both return
ErrNonceReused -> RED's PoC flips, s1 is unrecoverable.
CAPSTONE (TestMalicious_BadBehaviorNeverForgesOrLeaks): every single-member
deviation on the no-reconstruct sign path => ABORT or attributable BLAME, NEVER a
FIPS-valid forged signature. Subtests: nonce-reuse refused; invalid-sigma
blamed+abort; duplicate cannot inflate threshold; malformed no-panic+blamed;
valid-sigma wrong-z is a liveness fault (no forgery). Honest baseline asserts an
honest quorum DOES sign (so the negatives are meaningful).
SELECTIVE-ABORT (TestMalicious_SelectiveAbort_ReformProducesValidSig): the
aggregator's Lagrange is over the EXACT supplied evalPoints set, so selective
abort is orchestrator RE-FORMATION of a fresh exact-threshold quorum excluding the
blamed party -> the re-formed clean quorum produces a FIPS-valid signature.
ORDER-INDEPENDENCE (TestMalicious_AggregationOrderIndependent): identical
signature bytes regardless of partial submission order (CanonicalSignerSet
deterministic) -> no last-mover grinding of subset/output/challenge.
go vet clean; no stdlib log; full suite green (6.2s).
- NonceTicket (unique TicketID = H(committeeID||w1||binding) audit handle) +
ReserveNonceTicket: the ONE nonce-consume path. Dedup keys on MATERIAL
(committeeID||w1), NOT TicketID, so relabel/different-message reuse of the same
joint nonce is still ErrNonceReused. Round2 refactored to reserve via the
ticket. Lifecycle test: unique ids, single-use, domain-bound, abort-doesn't-leak.
- protocol_auth.go: canonical to-be-signed encoding for protocol messages bound
to (epoch,session,nonce,round); pluggable identity verifier (reuses the
AbortSignatureVerifier shape, DRY); DetectEquivocation -> two validly-signed
conflicting same-slot messages from one author yield ComplaintEquivocation
evidence. Networked transport (authenticated channels) is the consensus-layer
residual; this is the crypto primitive + detector.
Full suite green.
RED GATE-2 bypass: the structural gate greps only distributed_bcc.go for
KeyFromSeed(/func LargeCombine, so a reconstruct-then-sign wired via
deriveKeyMaterial+bccSign elsewhere PASSES.
GATE C (gate2_reachability_test.go): stdlib go/ast name call-graph over the
default-build files. Bans {KeyFromSeed, deriveKeyMaterial, bccSign,
shamirReconstruct*, LargeCombine, DealAlgShares} from being REACHABLE from the
committee sign entrypoints (AggregateBCC[WithBlame], NewDistributedBCCSigner,
DistributedBCCSigner.{Round1,Round2,Finalize,FinalizeWithBlame,SetNonceShare}).
Callee side over-approximated by name -> sound bias (over-report, never miss).
Two tests: (1) the REAL package is clean (8 entrypoints, none reach a banned
primitive); (2) the checker CATCHES RED's deriveKeyMaterial+bccSign bypass, both
as a synthetic graph AND via the real parse->graph pipeline on rogue source;
plus a clean-path negative control (no false positive).
Item 4 (ci_build_invariant_test.go): the production (default-tag) build never
carries legacy_trusted_dealer -- asserts the tag gates real reconstruct files,
they are absent from the default build, LargeCombine is quarantined, no
default file requires the tag, and DealAlgShares (trusted-dealer keygen) is
defined ONLY in _test.go.
Full suite green.
AggregateBCC silently dropped malformed/duplicate/invalid partials (anonymous
DoS) and CanonicalSignerSet does not dedupe (a duplicate PartyID could satisfy
the threshold with < t DISTINCT signers); a short ZShare panicked the aggregator.
FIX:
- AggregateBCCWithBlame (canonical) attributes every detectable deviation to its
PartyID (PartialBlame: malformed / duplicate / unknown-party / session-mismatch
/ invalid-proof); AggregateBCC is the back-compat wrapper (RoundSigner.Finalize
+ GATE-2 reflection + existing tests intact). FinalizeWithBlame maps PartyID ->
NodeID -> signed-complaint AbortEvidence (ComplaintBadPartial kind, never
self-accuses).
- Duplicate PartyID rejected: first occurrence authoritative, later ones blamed;
duplicates can NOT inflate a sub-threshold set.
- unpackPolyVecChecked: malformed/truncated ZShare -> ErrWireLengthMismatch, no
panic (closes a malformed-share DoS).
- shareCommitmentsFor: ONE pure fn of public inputs called by BOTH Round2 and
AggregateBCC => the partial-z proof binds the SAME per-party DKG/nonce
commitments on prove + verify (symmetric, no globals). nil today; the single
BDLOP plug point.
DESIGN+SCAFFOLD+FLAG (share_commit.go, Residual A): sound valid-sigma WRONG-z
blame needs a HIDING lattice commitment (BDLOP/Ajtai under a SEPARATE matrix) to
the dealt (y_i,s1_i) + an extended linear-sigma of opening. Homomorphic A*s1_i /
A*y_i is REJECTED (reopens W-LEAK / HINT-LEAK). Until then a valid-sigma wrong-z
is a LIVENESS fault (no FIPS-valid signature, no forgery/leak), marked
ErrIdentifiableAbortResidual.
GATE B (blame_gate_test.go): B.1 duplicate attributed + cannot inflate threshold;
B.2 invalid-sigma attributed to exact PartyID as AbortEvidence; B.3 malformed
ZShare no panic + BlameMalformed; B.4 honest residual — valid-sigma wrong-z
yields no forgery and is the flagged BDLOP residual.
GATE-1/GATE-2 still green; full suite green (7.5s).
THE HIGH (RED TestRED_NonceReuse_RecoversS1): same joint nonce + 2 messages =>
public (z_A - z_B) = (c_A - c_B)*s1 solves for the WHOLE expanded key. UNGUARDED
on v0.4.0.
FIX (nonce_ledger.go + Round2 wiring):
- NonceLedger single-use store keyed on the nonce COMMITMENT
H(committeeID || w1) -- NOT the nonceID label, so a relabeled joint nonce
cannot bypass. Reserve() is fail-closed: ErrNonceReused on any second use,
never rolled back (an aborted attempt leaves no reusable nonce state).
- DistributedBCCSigner reserves the nonce in Round2 BEFORE touching the secret.
Per-validator ledger injectable via SetNonceLedger (one ledger per share across
signer instances catches cross-instance reuse); domain binding via
SetNonceBinding (epoch/policy/message_kind/digest, audit + tripwire).
GATE A (nonce_reuse_gate_test.go): (1) actually RECOVERS the full 5-poly s1 by
solving (c_A-c_B)*s1 = z_A-z_B over Z_q (negacyclic Gauss-Jordan) == dealer
secret bit-for-bit -- the vuln is real; (2) the real signer API refuses the
second partial (ErrNonceReused), incl. the relabel bypass -- the system can
never be assembled via honest signers.
GATE-1/GATE-2 still green; full suite green (7.2s).
Residual (flagged): in-memory ledger does not survive restart -> production must
inject a PERSISTENT ledger; dealer-side NonceMPC 'Consumed' marking is
complementary defense-in-depth (leak-free NonceMPC residual).
Red-verified (commit a0bd97e): AggregateBCC no-reconstruct signer is the only
production committee signer; LargeCombine + DealAlgShares quarantined out of the
default build; GATE-1 (circl mldsa65.Verify accepts) + GATE-2 (no KeyFromSeed/sk
in sign-combine) green. HONEST SCOPE: no-reconstruct foundation, standard-verifier
compatible; NOT yet real-key-safe (nonce single-use guard + malicious-secure CSCP
are the next layer, red HIGH). v0.3 algebraic-broadcast path was removed; v0.4 =
BCC/CSCP.
Make the committee threshold-SIGN combine genuinely no-reconstruct and remove
the centralized-trust paths from the production build.
- Quarantine the GF(q) SEED-share reconstruct family behind
//go:build legacy_trusted_dealer: large_threshold.go (LargeCombine, the
KeyFromSeed-at-sign H-1 footgun), large_dkg.go, large_reshare.go,
large_types.go, largeshamir.go (+ their tests). The production committee
sign path is now ONLY the no-reconstruct AlgShare/AggregateBCC RoundSigner
(distributed_bcc.go): each party emits z_i = λ_i·y_i + c·λ_i·s1_i from its
own share+nonce; the combiner Lagrange-sums to z and recovers the hint from
public w' = A·z − c·t1·2^d — never forming s1, the seed, or sk.
- Rip out the trusted s1-share dealer DealAlgShares (+ zeroizeKeyMaterial)
from distributed_bcc.go into bootstrap_dealer_test.go (a _test.go file,
uncompilable into any production binary; available to tests/bootstrap only).
- Add the two hard gates (no_reconstruct_committee_test.go):
GATE 1 (standard-verifier): a (t,n)=(4,7) committee no-reconstruct signature
verifies under independent cloudflare/circl mldsa65.Verify; tamper rejected.
GATE 2 (no-reconstruct invariant): structural — the production build has no
KeyFromSeed call in any sign-combine file and no LargeCombine; behavioural —
Partial and AggregateBCC carry no share/sk/seed, and t-1 partials cannot sign.
Residual (flagged, NOT claimed fixed): a dealerless s1-share KEYGEN is the
research-blocked Part-2 problem (naive_additive_seta_obstruction.go computes the
obstruction; Mithril short-replicated shares is the adoption target). Keygen-side
KeyFromSeed in dkg.go (group-pubkey derivation) is a separate keygen residual.
Honest claim: no-reconstruct committee threshold signing whose output verifies
under the standard ML-DSA verifier — NOT FIPS-204 KeyGen distribution-equivalence,
NOT FIPS-validated.
The obstruction file over-claimed: "a genuinely dealerless DKG ... is NOT
achievable with known techniques." That is too strong and contradicted by
published work. Corrected to the NARROW, true statement and renamed the file to
say what it actually proves.
- distributed_bcc_dkg.go -> naive_additive_seta_obstruction.go (+ test). The file
computes the obstruction to the NAIVE additive (Pedersen/Gennaro) lift: a summed
joint secret has ‖s2‖∞ ≤ Nη, breaking the BCC boundary-clearance hypothesis and
ML-DSA's S_η-calibrated EUF-CMA. That computed barrier is correct and unchanged
(assessDealerlessFIPS, errors.Is sentinel — both intact, test green).
- Removed the general-impossibility framing. Short replicated-share constructions
(Mithril, ia.cr/2026/013, USENIX Sec 2026) achieve dealerless byte-FIPS-204
threshold ML-DSA at small N — a known, adoptable technique, not yet implemented
here; cited as the candidate path.
- Fixed a re-introduced mislabel: Corona is Module-LWE (Ringtail/Raccoon), not
Ring-LWE (2 comment sites).
What stays true and proven (separate concern): Pulsar's no-leak BCC/CEF threshold
SIGNING produces a STANDARD ML-DSA-verifiable signature (TestBCCSignRoundTripVerifiesCIRCL,
independent circl verifier). The keygen is trusted-dealer; genuinely-dealerless
byte-FIPS keygen is the open item the corrected file scopes.
Comments + file rename only; no logic, no symbol rename. Package builds, test green.
Builds the CarryCompare (CSCP) secure-comparison MPC circuit that realises the
ideal cefIdealSecureHighBits with an ACTUAL secure comparison, so no node (and no
process) ever forms the aggregate low sum A0 = Sum a0_i, the low part
w0 = LowBits(w), or the full commitment w — even transiently. This is the closing
move for PULSAR-V13-W-LEAK on the TALUS-MPC path.
talus_cscp.go — the real circuit, composing from the bgwMulShares / SharedRandomBit
substrate (talus_mpc.go):
- cscpSecureHighBitsVec: additive commitment shares -> additive->Shamir reshare to
<w> over GF(q) -> secure HighBits -> opened w1 only. Parallel over the 256*K
independent coefficients; race-clean.
- secure HighBits via the exact boundary-count identity
w1 = (Sum_{k=1..16}[w > (2k-1)*gamma2]) mod 16 (m=16 power-of-two buckets for
ML-DSA-65/87), proven coefficient-exact against FIPS Decompose over ALL q
residues.
- bit-decomposition by mask-open (carry-save adder + bitwise prefix less-than for
the mod-q fold = the "CSA + prefix comparison" the obstruction names);
bitLTPubShared / bitLTSharedPub / bitAdd / bitSubBetaQ / randomBitwise gadgets.
- CSCPParticipant: per-node state holds ONLY its own commitment share (no joint
w/w0/A0 field). CSCPMaliciousResidual / AssessCSCPMalicious: the scoped,
orthogonal malicious-secure / identifiable-abort layer (TALUS Phase B).
- enforces N >= 2T-1 (TALUS Thm 10.1).
talus_cef.go — CEFComputeW1 now DRIVES the real circuit (cscpSecureHighBitsVec)
instead of the ideal; cefIdealSecureHighBits retained as the test ORACLE.
Proven: gadgets bottom-up (bitwise LT exhaustive over all 64x64 pairs; bit-decompose
reconstructs; secure HighBits == FIPS Decompose on boundary + random coeffs);
multi-node leak-free ceremony (separate per-node state + bus) with w1 == ground-truth
HighBits(A*ybar), transcript opening ONLY {validity bit, uniform mask-open, w1}
(otherCt==0), source-structural guard (reconstruct only inside open(), sanctioned
tags only), reflection (no joint field), perfect masking; real CSCP == ideal
F_HighBits, ML-DSA-65 and -87; a wrong w1 is caught downstream by FindHint + the
mandatory stock-FIPS release gate (liveness fault, never forgery/leak). The headline
TestTalus_MPC_EndToEnd_StockVerify / _Mode87 now run the REAL CSCP and the aggregated
signature still verifies under unmodified cloudflare/circl mldsa{65,87}.Verify.
27 TALUS+CSCP tests green; full pkg suite green; race-clean.
Residual (honest): the circuit is semi-honest honest-majority; the malicious-secure
identifiable-abort layer is scoped (CSCPMaliciousResidual), not faked. KEYGEN stays
trusted-dealer (item 2 obstruction); permissionless safety rests on the dealerless
Corona leg of the AND-mode dual-PQ cert.
Genuinely-dealerless DKG producing byte-FIPS-204 ML-DSA keys is NOT achievable
with known techniques. This commit COMPUTES why (mirroring rangeproof.go's
derive-don't-assert pattern) rather than faking a key or falling back to a
dealer/TEE.
THE OBSTRUCTION (precise): FIPS-204 draws s1,s2 from S_eta (||.||inf <= eta) and
calibrates beta=tau*eta, omega, and the BCC 2*beta boundary margin to that. A
dealerless DKG forms the joint secret as a sum/Lagrange-combination of N>=2
contributions => ||s2||inf <= N*eta, so ||c*s2||inf <= N*beta > beta, violating
the BCC boundary-clearance hypothesis ||c*s2||inf <= beta. assessDealerlessFIPS
computes N*beta vs beta and shows byte-validity breaks at N=2 for both ML-DSA-65
and -87; the deeper break is that a sum-of-contributions secret is not the
S_eta distribution ML-DSA's EUF-CMA assumes. Forcing the joint into S_eta needs
either a dealer or a norm-reduction that changes (A,t) => not byte-compatible.
NOT the obstruction: t0. The joint t0 = Power2Round-low(joint t) is in
(-2^(d-1), 2^(d-1)] by definition for ANY N, so ||c*t0||inf <= tau*2^(d-1) < gamma2
always. Pinned by TestDealerless_T0IsNotTheObstruction (200704/245760 < 261888).
CORONA/RACCOON noise-flooding assessed: corona.BootstrapPedersen is dealerless
because Ring-LWE is noise-flooded (beta_j = A*(lambda_j*s_j)+e_j', sigma=kappa*sigmaE*sqrt(n))
with a Round_Xi absorption step; ML-DSA has fixed S_eta bounds and no absorbing
rounding, so any flooded variant is a Raccoon-family scheme (Threshold Raccoon,
EUROCRYPT'24) that does NOT verify under FIPS-204. This is exactly why the
Quasar cert is AND-mode dual-PQ: dealerless Corona leg || dealer/TEE-genesis
Pulsar leg. DealerlessMLDSADKG fails closed (ErrDealerlessByteFIPSUnreachable),
never fakes, never falls back to dealer/TEE. 5 tests pin the arithmetic.
Concrete poly-vector (s1) AlgShare + DistributedBCCSigner closing the
PULSAR-V12-PARALLEL-PQ forward-path item 1: a no-reconstruct, single-share
t-of-n ML-DSA signer in the current RoundSigner/Partial/FlatAggregateZ model.
- AlgShare: GF(q) poly-vector Shamir share of the EXPANDED signing component
s1 (s1 only — the BCC/CEF path never touches s2/t0, so sharing them would
reopen PULSAR-V13-HINT-LEAK; matches the DKGPublicOutput no-t0 invariant).
- DistributedBCCSigner (satisfies RoundSigner): Round1 binds the canonical
NonceCert and derives c; Round2 emits z_i = lambda_i*y_i + c*lambda_i*s1_i
with the SOUND linear-sigma partial-z proof (partial_proof.go); Finalize
aggregates z=sum z_i Lagrange-linearly (FlatAggregateZ), recovers the hint
from PUBLIC w' via FindHint, emits FIPS-204 sigEncode. The aggregator holds
NO share/sk/seed (AggregateBCC takes []Partial only).
- PULSAR-V13-PARTIAL-Z-PROOF: REAL — sound Maurer/Schnorr sigma wired and
proven (honest+forged partials), forgeries dropped with a clean blame path.
- DealAlgShares: Part-1 TRUSTED-DEALER keygen (no-reconstruct at SIGN time,
not dealerless at keygen — that is Part 2). DealNonceMPCDebug is the
NonceMPC stand-in (reveals w to the harness = PULSAR-V13-W-LEAK gate,
fail-closed for production; signing decomposition is independent of it).
Multi-node proof (7 tests, all green): t separate signers one share each,
ceremony over a message bus, signatures verify under UNMODIFIED FIPS-204
(VerifyBytes + stock circl Verify), ML-DSA-65 and -87, ctx-binding real,
sub-quorum refused, share carries no seed/s2/t0, ShareCount==1 per node.
luxfi/consensus now composes a dual-lattice AND-mode finality cert that
carries a Pulsar (Module-LWE / FIPS-204 ML-DSA) leg in parallel with a Corona
(Ring-LWE) leg, both required to finalize. Record the consumer and pin the
Pulsar-side gap in one document:
- The Pulsar leg VERIFY path (wire.VerifyBytes, unmodified FIPS-204) is
production-ready and exercised live by the consensus multi-node test.
- The Pulsar leg's no-reconstruct single-share t-of-n SIGNING is not yet
production: the current KeyShare is a GF(257) seed-share and ML-DSA s1 is
non-linear in the seed, so seed-shares admit only reconstruct-then-sign.
A no-reconstruct signer needs poly-vector (s1,s2,t0) shares; the new
RoundSigner/Partial/FlatAggregateZ model carries the interface + z-sum
primitive but no concrete poly-vector share type or RoundSigner implementor
exists on the current line. It also stays fenced fail-closed behind
PULSAR-V13-W-LEAK (distributed boundary-clear nonce proof) and
PULSAR-V13-PARTIAL-Z-PROOF.
- Permissionless safety comes from the dealerless Corona leg in AND-mode;
Pulsar genesis is TEE-gated (PULSAR-V12-TEE-BIND / mldsa-tee).
- Forward path: finish the new model (poly-vector share + concrete
RoundSigner + close the V13 ZK gates) applying gate C's DistributedSigner
decomposition; do NOT resurrect the removed v0.3 stack (two-way / backwards).
The v0.1 GF(257) threshold-signing instantiation (ThresholdSigner /
Round1 / Round2 / Combine) Lagrange-reconstructed the master seed in
aggregator memory and called FIPS 204 SignTo once — i.e. a quorum's
full ML-DSA signing key materialised in a single process. That
reconstruct-then-sign ceremony was the H-1 footgun. Its only
production reach was consensus's quasar wave_signer (deleted), via the
threshold/protocols/pulsar alias (Combine re-export dropped in the
threshold repo).
The one and only threshold-signing path is now the v0.3 algebraic
ceremony (threshold_v03.go / orchestrate.go), whose
Lagrange-linearity-of-z aggregation produces the FIPS 204 signature
WITHOUT reconstructing the secret in any party's memory.
Removed:
- threshold.go: ThresholdSigner, NewThresholdSigner, Round1, Round2,
Combine (kept the shared transcript/committee/CT helpers + error set
consumed by the v0.3 and GF(q) paths; dropped now-unused crypto/rand
+ io imports and dead ErrNilSession).
- types.go: Round1Message, Round2Message wire types.
- round.go: dead RoundSigShare (was the wave_signer carrier).
- threshold_test.go, n1_byte_equality_test.go: pure-v0.1 suites.
- round_test.go / bench_test.go / precompile_e2e_test.go: excised the
v0.1 sign cases, kept RoundContext/ApproxRoundSecurity/DKG/Large/v0.3.
- cmd/genkat: dropped v0.1 threshold-sign KAT generation + helper.
- ct/dudect/combine_ct.go + dudect_combine.c: dead CT harness for the
removed Combine path.
- shared_dkg_test.go: relocated the non-v0.1 DKG test helpers
(runDKG / runDKGWithIdentities) that threshold_test.go had hosted.
Verified: GOWORK=off go build ./... exit 0; go vet clean;
go test ./ref/go/pkg/pulsar/ exit 0 (DKG, reshare, v0.3, KAT replay,
large-committee all green).
The headline EC byte-equality (pulsar_n1_byte_equality) proved the threshold
combine bit-equals SinglePartyRun, which RECONSTRUCTS the master secret and
signs with it -- exactly the path the production leaderless signer must never
take (PULSAR-V13-HINT-LEAK). This pass re-scopes that cone as IDEALISED
CORRECTNESS and adds the HONEST production model whose residual is a STANDARD
Module-LWE/MSIS reduction, not an implementation reconstruct.
New proofs/easycrypt/Pulsar_N1_NoLeak.ec (written; machine-recheck pending
EasyCrypt -- no `ec` on host, scripts/checks/ec-compile.sh is the CI gate):
- no_leak_z_aggregate: the public masked Lagrange aggregate equals central z
WITHOUT forming the master secret (re-exports the Lean-bridged
threshold_partial_response_identity).
- public_hint_roundtrip [B, Lean-backed]: the hint is recovered from the
PUBLIC w' = A*z - c*t1*2^d via FIPS UseHint; procedure lift of the
machine-checked Lean boundary_clearance + findHintCoeff_unique. No
c*s2/c*t0/r0 anywhere.
- no_leak_reduction [C-standard, OPEN]: under Module-LWE + Module-SIS the
public transcript leaks nothing about (s1,s2,t0) beyond one FIPS 204
signature. The honest replacement for combine_body_axiom -- a reduction to
the SAME lattice assumptions ML-DSA's EUF-CMA uses, secret NEVER formed.
The CORRECTNESS CORE is machine-checked in Lean 4 + Mathlib on this host
(luxfi/proofs: Crypto.Pulsar.NoLeakAggregate / .BoundaryClearance /
Threshold_Lagrange; lake build green, #print axioms shows no sorryAx).
Disclosure updated to match reality: AXIOM-INVENTORY (new Bucket C-standard +
"A is machine-checked in Lean"), PROOF-CLAIMS (two models), BLOCKERS
(PULSAR-EC-RECON-MODEL re-scoped, first criterion now [x]), budget AXIOM
87->89 (the 2 new disclosed standard-assumption axioms). Framework gate PASS
(all 5 checks); ref/go build + tests green.
Reclassify (not reduce) the EasyCrypt axiom surface and make the trust
cone of the N1 byte-equality theorem fully disclosed. NO axioms discharged
this pass (honest): no easycrypt toolchain on host to machine-recheck a
hand proof, and every in-file-derivable axiom is already a proved lemma;
the residual A/B axioms are over abstract types/ops and the C axioms are
open security assumptions that must stay open.
- AXIOM-INVENTORY.md (new): complete bucketed census of all 78 real axiom
declarations -- A standard-math-fact (16/17, 5 Lean-bridged), B
serialization/layout (41), C OPEN security assumption (21). One row per
axiom: name, file:line, bucket, justification. Notes the framework
axiom-budget regex over-counts (87) because 9 comment lines begin with
the word "axiom".
- PROOF-CLAIMS.md (new, root, gate-read): assurance vocabulary
(machine-checked / sound-by-reduction / interop-tested / asserted-axiom
/ fail-closed-pending-review / open-research) matched to reality. States
plainly that the EC byte-equality is machine-checked (0 admits) MODULO a
C-cone that models reconstruct-then-sign (combine_body_axiom /
S_functional_spec / *_body_*_spec assert the extracted code equals the
centralised signer on the Lagrange-reconstructed master secret) -- the
abstraction the production leaderless path must never use -- and that the
no-leak property is interop-tested (CIRCL + pq-crystals), not EC-proven.
- BLOCKERS.md: add PULSAR-EC-RECON-MODEL as an explicit OPEN item
cross-referenced to the V13 external-review gate; fix the contradictory
"no open items" header.
- .assurance/reviewed-axioms.txt: list every load-bearing / security-cone
axiom (A: 5 Lean-bridged + C: combine_body_axiom, S_functional_spec,
algebraic_aggregate_ctx_body_axiom) with bucket + justification -- full
disclosure, not a blanket allow.
- .assurance/budget.txt: correct the prose (true decl count 78 vs counted
87; 0 real admit. tactics); counts held, not raised.
Framework gate (circular-proof + axiom-budget + the other three) PASS with
full disclosure. go test ./... green (no Go touched).
Cert-verification surface (red-review fixes — none touch the proven BCC
arithmetic, all close fail-open/forgery seams):
1. ConsensusCert: bind the signer bitmap to the signature via an
accountability QuorumCert over a payload root committing bitmap +
transcript root + signature bytes; Verify() replaces the structural-
only VerifyStructure (bitmap rewrite / signature splice now rejected).
2. nonceCertPayloadRoot: bind Mode, WCommitment, ClearanceProof, Consumed
(length-prefixed) — closes swappable-commitment + replay seams.
3. VerifyNonceCert / ConsensusCert.Verify: fail-closed QuorumSigVerifier
seam (one registry for both cert types) — QC signatures are no longer
trusted unchecked; unregistered verifier => reject.
4. boundaryThreshold saturates at 0 and VerifyNonceCert/RunNonceMPCDebug
gate bccParams ok — ML-DSA-44 (vacuous boundary clearance) can no
longer underflow to "everything clear".
5. partial-z: reject a challenge c that is not a FIPS 204 SampleInBall
(tau ±1 coeffs) — a degenerate c=0 can no longer strip the s1 binding.
Forward-only removal of the v0.3 Algebraic threshold path: it broadcast
per-party c*s2 / c*t0 hint material whose aggregate reconstructs the joint
key (PULSAR-V13-HINT-LEAK). Deleted threshold_v03*.go + attest.go +
orchestrate.go + tests; preserved the two helpers the no-leak BCC signer
uses (polyVecExceeds, deriveMuCtx). Removed the now-dead code orphaned by
it — including makeHint, the secret-dependent hint primitive (BCC recovers
the hint from public data via FindHint), shamir_poly.go, and the accel
import. staticcheck U1000 clean; 190 tests pass.
The v0.1/v0.2 threshold.go path (documented aggregator-trust model, tests
green) is intentionally retained — it is not the V13 leak.
ClearanceVerifier + PartialZVerifier interfaces; default verifiers FAIL
CLOSED (ErrClearanceProofUnsound / ErrPartialZProofUnsound) so production
BCC signing is DISABLED until sound, externally-reviewed proofs are
registered. ProductionBCCSigningReady()=false by default. Honest
containment, not a stand-in for the (research-grade) proofs.
Aligns cover-sheet revision date with the submission-2026-06-05 cut.
Spec PDF is built fresh per cut by scripts/build.sh and bundled into
the tarball at cut-time (PDF is gitignored — generated artifact).
Wires Pulsar's v0.3 algebraic-threshold Round-2 forward NTTs through
luxfi/accel's LatticeNTTMLDSABatch (commit 6ec46c4 — the real GPU NTT
substrate, no longer the ErrNotSupported stub).
Surface:
batchNTT(polys []poly) [][]int32 in mldsa_lattice.go
Dispatches to accel.LatticeNTTMLDSABatch when
accel.Available() AND len(polys) >= accel.MLDSABatchThreshold (8).
Otherwise runs pulsar's per-poly circl-style (*poly).ntt() — byte-
identical to the pre-batchNTT reference. Any accel error falls
through to the pure-Go path.
Round-2 call sites converted (4 logical batches, 22 forward NTTs per
party-attempt total):
yHat threshold_v03.go ~L680 (L polys: 4/5/7 for P44/P65/P87)
s1Hat threshold_v03.go ~L893 (L polys, replaces L per-iter ntt)
cs2-input threshold_v03.go ~L917 (K polys: 4/6/8)
ct0-input threshold_v03.go ~L939 (K polys)
The accel substrate engages on ModeP87 (K=8) for the cs2-input and
ct0-input batches; ModeP44 and ModeP65 batches are below threshold and
stay on the per-poly leg.
Byte-equality regression (TestPulsar_GPU_ByteEqual, ModeP65) PASSES.
The test's four Round-2 batches are sized 5, 5, 6, 6 — all strictly
below the accel threshold — so both legs run the per-poly path and
the bit-identical contract holds by construction. Wider FIPS 204
verification (TestSignVerify_RoundTrip_AllModes including ModeP87)
continues to pass because both legs compute the same Z_q residues
and the FIPS 204 verifier checks math, not intermediate uint32
representations.
Tests:
go build ./... PASS
go test -count=1 ./... PASS
go test -count=1 -tags accel ./... PASS
go test -count=1 -race -run TestPulsar_GPU_ByteEqual ./... PASS
Fresh four-dimension audit at v1.1.1 (commit 6a5606f). BLOCKERS.md stays
at "submission-ready. No open items." for existing claims; two
forward-looking v1.2 extensions file under the new Forward-looking
section.
Bottom line per dimension:
1. Public-permissionless-chain safety — no re-opener. Slashable
equivocation extractable via AbortEvidence (4-field TLV +
wire-stable codec); rogue-key bound via FIPS 204 tr =
SHAKE-256(pk, 64) → μ; adaptive corruption gap is a research
direction documented as openq in spec/pulsar.tex; front-running
ruled out by Round-1 commit-and-reveal + ML-KEM-768-derived
pairwise MAC keys; PULSAR-V04-CTX v0.4 ctx-bound μ derivation
stays load-bearing via single canonical deriveMuCtx helper.
2. TEE — luxfi/threshold/protocols/mldsa-tee already ships the
custody-mode TEE-gated master-seed surface (AMD SEV-SNP / Intel
TDX / NVIDIA NRAS via mpc/cc/attest + KMS ReleaseGate + HSM
wrap-key + approval). Adds in-pulsar AttestationContext(setup, msg)
helper for the permissionless-quorum-gating case: canonical 32-byte
TBS digest a validator's TEE quote should bind to. No wire change,
no ABI break. Five unit tests pin stability, nil-safety, full input
sensitivity, and customisation-tag domain separation.
3. GPU — pulsar/gpu (UseAccelerator) is wired to luxfi/lattice/v7/gpu
dispatcher; lux-private/gpu-kernels/ops/lattice/ntt_mldsa ships the
byte-equal-to-PQClean NTT across 5 backends (CUDA/HIP/Metal/Vulkan/
WGSL). The substrate (lux_lattice_ntt_mldsa_batch + the existing
stubbed lux_mldsa_verify_batch in accel/internal/capi/capi.go:503-531)
is the v1.2 forward-looking item; routing through Round-2 batched
NTT call sites is mechanical once the ABI ships. Byte-equality
regression guard TestPulsar_GPU_ByteEqual already in place; passes
vacuously today, pins divergence the moment GPU dispatch engages.
4. Cross-validation against pulsar-mptc legacy — clean. No orphan
imports. All "MPTC" refs are to the NIST competition category.
Build sanity: go build / go vet clean; full ref/go/pkg/pulsar suite
green (14.9s, 1.4s -race on new tests); GPU subpackage + interop tests
green.
Files:
- AUDIT-2026-06.md (new) — comprehensive audit per dimension.
- BLOCKERS.md — adds Forward-looking (v1.2) section with
PULSAR-V12-TEE-BIND + PULSAR-V12-GPU-NTT-WIRE. Existing Closed
section untouched.
- ref/go/pkg/pulsar/attest.go (new) — AttestationContext helper
(one pure function over already-transmitted Round-1 inputs).
- ref/go/pkg/pulsar/attest_test.go (new) — stability + nil-safety +
input sensitivity + domain-separation pins.
These 3 threshold-signature sources type-checked but failed in the Jasmin
backend. Fixed with codegen-only restructuring — NO algorithm, value, wire
layout, or constant-time change:
- combine: decomposed an over-blended x86 effective address into a base reg;
split a shifted-OR source into shift-into-temp then OR.
- round1: relieved register pressure (spill output ptrs, extract the
expandA+matvec+decompose phase into a helper frame, pack derive_rho_prime
inputs into one preimage buffer, u32 stores for y_state).
- round2: an export fn had 13 pointer params (Jasmin caps at 6) — marshalled
them into a caller-supplied table; gave polyvecl_scalar_mont distinct in/out
ptrs; routed per-peer MAC tables through one buffer; stack-return digest
helpers; spilled keccak-crossing loop-invariants.
Shared lib tweaks (consumed by these files): mac/transcript/lagrange/seed .jinc
stack-return + u64-index + BE-via-temp. All #[ct] contracts and #declassify
pragmas preserved (CT type-checked at compile). Full Jasmin corpus: 23/23
compile (was 20/23).
Co-authored-by: Hanzo Dev <[email protected]>