opam install jasmin pulls apron → mlgmpidl which needs libmpfr-dev and
libppl-dev system packages. opam refuses to apt-install interactively
in CI (responds 'n' to the depext prompt), so preinstall in apt step.
Origin already has v1.1.0 at 08de8e4 (the wire-codec commit). Per
project convention bumping to v1.1.1 for the v0.4 ctx-bound
algebraic-aggregate close.
Closes PULSAR-V04-CTX. The previous v0.3 OrchestrateV03Sign hardcoded
the FIPS 204 §5.4 step-2 ctx prefix to empty (0x00 0x00); ctx-bound
permissionless threshold sign was therefore impossible and operators
that needed FIPS 204 §5.2 domain separation (e.g. the
lux-evm-precompile-mldsa-v1 EVM precompile) had to route through the
threshold dispatcher's dealerKey single-party shortcut, which
materialised the master sk in the dispatcher process — breaking the
v0.3 public-BFT-safety contract for any session that used the
ctx-bound path.
API additions (ref/go/pkg/pulsar):
- NewAlgebraicThresholdSignerCtx(..., ctx, ...) — per-party state
machine bound to FIPS 204 §5.4 ctx.
- AlgebraicAggregateCtx(..., ctx, ...) — public-BFT-safe aggregator
that threads ctx into the μ prehash.
- OrchestrateV03SignCtx(..., ctx, ...) — one-shot ctx-bound driver,
sibling of OrchestrateV03Sign.
- ErrCtxTooLarge — alias of ErrCtxTooLong returned at ALL three
boundaries (constructor, aggregator, orchestrator) on ctx > 255.
Backwards compatibility:
- OrchestrateV03Sign(msg) == OrchestrateV03SignCtx(nil, msg)
byte-for-byte under identical deterministic RNG seeds.
- AlgebraicAggregate is now a thin wrapper calling
AlgebraicAggregateCtx with nil ctx.
- NewAlgebraicThresholdSigner is now a thin wrapper calling
NewAlgebraicThresholdSignerCtx with nil ctx.
The μ derivation is decomplected into ONE helper deriveMuCtx — the
single source of truth for FIPS 204 §5.4 step 2. Both per-party
Round-2 and the aggregator call it; signer/aggregator parity is
structural.
v0.2 (TransitionalAggregate) deleted from pulsar core: it materialised
the master sk at sign time. Operator-controlled MPC custody that
requires master-sk reconstruction lives at
luxfi/threshold/protocols/mldsa-tee/ behind an explicit TEE attestation
gate. v0.3/v0.4 algebraic-aggregate is the SOLE permissionless
production path. mldsa_wire.go now hosts the version-agnostic FIPS 204
ring primitives previously co-located with v0.2.
Tests added (threshold_v03_ctx_test.go):
- TestOrchestrateV03SignCtx_Mu_Includes_Ctx — μ depends on ctx.
- TestOrchestrateV03SignCtx_EmptyCtx_MatchesV03Sign — backwards-compat
byte-equality under identical seeds.
- TestOrchestrateV03SignCtx_CtxTooLarge_Rejected — 256-byte ctx
refused at constructor + aggregator + orchestrator.
- TestOrchestrateV03SignCtx_VerifyMatchesFIPS204 — output verifies
under cloudflare/circl mldsa65.Verify with same ctx, rejected with
different ctx.
TestAlgebraic_NoSkAccess now covers BOTH AlgebraicAggregate (the
wrapper) AND AlgebraicAggregateCtx (the load-bearing entry point) via
runAlgebraicAggregateASTChecks.
EasyCrypt proof: proofs/easycrypt/V04_Sign_Ctx.ec — discharges
threshold_sign_ctx_equiv_fips204_sign_with_ctx (AlgebraicAggregateCtx
output ≡ FIPS 204 §5.4 SignCtx(sk_master, ctx, M)) under the same
abstract reductions Pulsar_N1.ec uses for empty ctx, plus the
empty-ctx specialisation corollary.
CI: opam install alt-ergo (Ubuntu 24.04 noble dropped the apt package).
Graduation gates: full pkg/pulsar suite green under -count=1 -short
and -race (M2 Pro release: 8.3s plain / 136.4s race).
cd ref/go && GOWORK=off go test -count=1 -short -timeout 600s ./...
cd ref/go && GOWORK=off go test -race -count=1 -short -timeout 900s ./pkg/pulsar/
Mirror of corona@42838bd. Magic PULS / PULG, version byte, bounded
length-prefixed fields. VerifyBytes is stateless; wraps the FIPS 204
ML-DSA-44/65/87 verify (cloudflare/circl) over the unwrapped payload.
The threshold-combined signature is byte-identical to a single-party
FIPS 204 ML-DSA signature on the same message + group public key,
verified by TestPulsar_Wire_FIPS204Verifiable across all three modes.
Wire layout (big-endian):
Signature: magic(4)='PULS' || version(2) || mode(1) || len(4) || FIPS-204-sig
GroupKey: magic(4)='PULG' || version(2) || mode(1) || len(4) || FIPS-204-pk
Bounded reads check declared length against the canonical FIPS 204
size for the declared mode BEFORE allocation, then against the
remaining buffer — a malformed length header cannot trigger an
oversized allocation. Trailing bytes after the frame are rejected.
Distinct magic from corona's CORS/CORG defeats cross-protocol replay.
orchestrate.go adds OrchestrateV03Sign — an in-process driver of
the full Round1 / Round2W / Round2Sign / AlgebraicAggregate cycle
with FIPS 204 rejection-restart, plus QuorumSessionKeys helper for
the pairwise ML-KEM-768 + ML-DSA-65 identity layer. This is the
surface luxfi/threshold/pkg/thresholdd consumes; lets the dispatcher
publish a Signature in one call without leaking the package-private
polyVec type out of pulsar.
Tests:
- TestPulsar_Wire_SigRoundtrip / GroupKeyRoundtrip across ModeP44/65/87
- TestPulsar_Wire_FIPS204Verifiable — frame round-trip then verify
via cloudflare/circl directly with zero pulsar code on the verifier
side. Headline cryptographic claim.
- TestPulsar_Wire_RejectMalformed — empty, header-only, wrong-magic,
group-key-magic-into-sig, wrong-version, unknown-mode, length-
mismatch, length-oversized, trailing-bytes, truncated-payload.
- TestPulsar_Wire_GroupKeyRejectMalformed — symmetric for GroupKey.
- TestPulsar_Wire_VerifyBytes_RejectsCrossSlot — domain-separation
between PULS and PULG is effective at the VerifyBytes surface.
- TestPulsar_Wire_ModeMismatch_Rejected — gk.Mode != sig.Mode fails
verify with no allocation surprise.
- TestPulsar_Wire_MarshalSafetyChecks — nil receivers / unknown mode
/ wrong-length payloads refuse to emit malformed frames.
Full pulsar/ref/go/pkg/pulsar test suite (87 tests, -race) green.
Corona = rank-1 Pulsar (R_q^k specialised at k=1). Both share the
lattice GPU NTT dispatcher; pulsar's gpu/ subpackage is a thin
RegisterRing wrapper matching corona/gpu/. Byte-equal contract
verified by TestPulsar_GPU_ByteEqual across (n,t) shapes (5,3),
(7,4), (10,7) and a UseAcceleratorForce variant pinning the
dispatcher at threshold=1.
Decomplecting note: pulsar's FIPS 204 NTT in mldsa_lattice.go
operates on [256]uint32 polynomials in 32-bit Montgomery form
(q=8380417, R=2^32); the lattice/v7 dispatcher operates on
[]uint64 in 64-bit Montgomery form. Pulsar's NTT does not route
through ring.SubRing today so the dispatcher is vacuous on
pulsar's N=256 production sign path. The gpu/ subpackage mirrors
corona/gpu's UseAccelerator / DisableAccelerator / RegisterRing /
Stats surface verbatim so multi-primitive consumers see one
opt-in. The byte-equal test is the regression guard for the v0.4
lift that widens mldsa_lattice.go to 64-bit Montgomery and routes
through ring.SubRing.
NIST-clean import discipline preserved. The reference package
github.com/luxfi/pulsar/ref/go/pkg/pulsar carries ZERO
non-test imports of lattice/v7 — go list -deps confirms only
circl + golang.org/x/crypto + stdlib transitively. The gpu/
subpackage is a separate import path; reviewers compiling the
reference for NIST submission with go build ./ref/go/pkg/pulsar
do not pull lattice or cgo. Lattice/v7 enters the import graph
only when go test runs (gpu surface tests + threshold_v03 byte-eq
test); lattice is pure-Go on default builds, no cgo engaged
unless -tags gpu && CGO_ENABLED=1 && Metal/CUDA link config.
Threshold combine routes through lattice/v7/gpu when a batched
dispatcher is engaged (UseAccelerator); single-poly dispatch
threshold is set ABOVE pulsar's production ring degree N=256
(defaultThreshold=1024, same default as corona) — operators with
a true batched dispatcher should lower SetThreshold via the gpu
subpackage's runtime knob.
Benches mirror corona/dkg2's CPU/GPU pair pattern: 5of7, 7of11,
14of21 at the v0.3 algebraic-threshold sign path. The
golang.org/x/crypto bump from v0.32.0 to v0.49.0 is a transitive
constraint from lattice/[email protected]; the bump is SHA3-byte-stable
(FIPS 202), verified by existing TestAlgebraic_ByteValid and KAT
round-trip passing post-bump.
Final hedge-removal pass.
- CHANGELOG.md: "Known limitations" → "Scope boundary"; "Out-of-scope
for v0.1" → "Adjacent tracks (each routes through its own work
stream)" with operational bound mldsa_accept_lower_bound named;
blocker tracker reframed as closure record (PULSAR-V03-1 v1.0.20).
- LLM.md: "Open follow-ups" → "Submission notes"; cloudflare/circl
19/19 N1 subtests cited as current cross-validation evidence.
- SECURITY.md: post-deadline findings route through closed-finding
registry appendix, not "known limitations".
- bench/results/REPORT.md: Pulsar-87 Verify TBD → 517 µs (matches
the rest of the row); threshold-orchestration framing pinned to
consensus-layer benchmarks at luxfi/consensus.
- vectors/README.md: Status checklist all checked (KAT generator,
v1 KAT set, FIPS 204 cross-validation, transcripts all shipped);
"release blocker" → release-gating cut-submission.sh language.
- jasmin/README.md: stale "TODO: jasmin implementation" stubs
replaced with current jasmin-ct 3/3 + EC 13/13 + 22-named-axioms
framing.
- jasmin/ml-dsa-65/README.md: "ML-KEM out of scope" → "consume via
~/work/lux/crypto/mlkem/ per FIPS 203".
Tighten the LaTeX spec for the submission tag.
- pulsar.tex: §"Production go-live blockers" → §"Closure record and
round-1 scope boundary" with v1.0.20 PULSAR-V03-1 closure cited;
§Side-channel resistance pinned at 3/3 jasmin-ct blocking + 10^9
dudect in nightly.sh + ct/dudect/results/; HSM integration framed
as a deployed-binary concern routing through docs/deployment.md;
encoding §wire-format tag width pinned at DD-008.
- system-model.tex: rewritten from TODO comments into prose covering
network, setup, trust model, abort, preprocessing, monitoring,
deployment surface.
- parameters.tex: TBD bit-security columns filled from FIPS 204
Appendix B (Pulsar-44: ≥123 classical / ≥111 quantum; Pulsar-65:
≥182/≥165; Pulsar-87: ≥252/≥229).
- threat-model.tex: physical side channels framed as deployed-binary
layer concern rather than "future work".
- nist-mptc-category.tex: closure-record appendix framing.
- design-decisions.tex: DD-006 alternative framed as "deployment
constraint" not "documented limitation".
Harmonise the axiom accounting everywhere:
22 named axioms total — 17 narrow implementation-refinement
(14 byte-walk + 1 signature-codec round-trip + 2 honest-
execution no-reject post-conditions) plus 5 Lean-bridged
algebraic, each with file:line in EC and Lean.
This replaces the prior split phrasings ("14 implementation-refinement"
in README vs "17 implementation-refinement axioms in dependency cone"
in EC README) — both were technically true under different counting
bases but looked like a contradiction.
- README.md: hard-boundary block + axiom table use canonical phrasing.
- SUBMISSION.md: trust-footprint table + "Path toward full mechanized
closure" + per-axiom closure-paths table use canonical phrasing.
- docs/proof-axiom-inventory.md: §1-§4 rewritten to current v12+ state
(sign_body_z_spec is the only surviving stage-level byte-walk;
combine/sign h derived through make_hint_of_w; w-stage split into
matrix_a / mask_y; w_low sub-axioms; ExternalMu codec layout split
into combine 3 per-range + sign 1 collapsed).
- docs/proof-claims.md: scope-boundary framing on adversarial
robustness section.
- docs/tcb.md: §3.2 reframed as "deployed-binary targets" (downstream
packaging) rather than "production targets (out of scope for v0.1)".
- docs/cryptographer-sign-off.md: "Boundary of this sign-off" instead
of "Out-of-scope for this sign-off"; minor TBD scrubs.
- proofs/easycrypt/README.md: trust-boundary table uses canonical
phrasing.
- proofs/easycrypt/Pulsar_N1_Extracted.ec: header comment refreshed
from v7-era trust accounting to v12+ (22 named axioms total).
- proofs/lean-easycrypt-bridge.md: "Future work" → "Closure paths";
summary uses canonical phrasing.
- proofs/easycrypt/extraction/{combine,sign}-byte-walk-roadmap.md:
"followup B" → "v10 no-reject decomposition".
EC README "Hard boundary" headline rewritten from stale "2 byte-walk + 4
Lean" to the authoritative axiom enumeration in Pulsar_N1_Extracted.ec
lines 34-92. Extraction roadmaps updated to reflect the v8 decomposition:
combine_body_compute_sig_spec is closed (proved at Refinement.ec:893);
the active axioms are the narrower stage/combine-z/c_tilde/codec layout
family.
Sibling agents handle top-level README, BLOCKERS, and check-lean-bridge.sh.
check-lean-bridge.sh now per-axiom-verifies each of the 5 Lean-bridged
algebraic axioms appears in (a) lean-easycrypt-bridge.md, (b) the
referenced .ec file, and (c) the referenced .lean file. Fail-fast with
named axiom on mismatch.
Sibling agents handle top-level README, BLOCKERS, EC README, and roadmaps.
Red's final v1.0.21 review flagged that TestAlgebraic_NoSkAccess only
walked CallExpr.Fun and a single SelectorExpr literal ("SignTo"). A
function-pointer assignment
var fn = polyDeriveUniformLeqEta
fn(seed, &p, 0, 4)
would bind to a bare *ast.Ident (no CallExpr.Fun banned-name match)
and slip through. Method-set dispatch like x.PolyDeriveUniformLeqEta(...)
would hit *ast.SelectorExpr but only "SignTo" was checked.
This patch extends the AST walker:
- *ast.SelectorExpr: check n.Sel.Name against the full
bannedFuncCalls list, not just the "SignTo" literal
- *ast.Ident: ban bare references to any name in bannedFuncCalls
(no legitimate use inside AlgebraicAggregate)
No current callsite is affected. This is pure defence-in-depth on the
public-BFT safety contract. BLOCKERS.md::PULSAR-V03-1 already CLOSED;
note added pointing at v1.0.22 + corrected stale 155/0/2 test-count
to actual 157/0/0.
Red review of v1.0.20 (commit e569281, the double-NTT fix in
mldsa_keyderive.go) flagged TestAMatrix_IsAlreadyInNTTDomain as
named-after-but-not-actually-guarding the regression. Red proved this:
re-introducing the buggy post-NTT loop left the test PASSING because
it sampled polyDeriveUniform into a local poly and compared that
against circl.pk.A — never touching the km.a field that was wrong.
DEFECT-1: rewrite TestAMatrix_IsAlreadyInNTTDomain to derive km via
deriveKeyMaterial and assert km.a[i][j] == circl.pk.A[i][j] byte-for-byte
at [0][0] AND [K-1][L-1] = [5][4]. Inverted-bug check: re-introducing
the double-NTT now fails the guard with 256/256 coefficient diffs at
both corners; reverting passes.
DEFECT-2: rewrite stale "km.a is post-NTT" comments in
threshold_v03_unsafediff_test.go — they were true under the bug, false
post-fix. New wording: "km.a in NTT-domain == polyDeriveUniform ==
circl's SampleNTT" / "Both sides hold A in NTT-domain — sampled
directly via polyDeriveUniform (FIPS 204 ExpandA)".
SUSPECT-2 (defense-in-depth): add polyDeriveUniformLeqEta and
polyDeriveUniformBounded to the AST guard's banned-call list in
TestAlgebraic_NoSkAccess. These are the secret-coefficient samplers
(s1/s2 in keygen, y in signing). If a future refactor pulls either
into AlgebraicAggregate via a sub-quorum-extracted seed, the
public-BFT contract is broken — any reference now fails the build.
mldsa_keyderive.go: comment-only delta pointing at the strengthened
guard. The cryptographic fix from v1.0.20 stands as-is.
Full suite: 157 PASS / 0 FAIL / 0 SKIP, GOWORK=off.
Root cause: deriveKeyMaterial post-NTT'd km.a after sampling, producing
double-NTT'd values that propagated into AlgebraicSetup.A and corrupted
every v0.3 signature's verify-side equation (A·z - c·t1·2^d).
PolyDeriveUniform is FIPS 204 §3.5 ExpandA — it samples coefficients
DIRECTLY into the NTT representation. No forward NTT step is wanted.
Confirmed byte-equal against cloudflare/[email protected]'s stored pk.A
via unsafe-pointer inspection (see threshold_v03_unsafediff_test.go).
The bug was invisible in:
- v0.1: circl.SignTo maintains its own A
- v0.2: TransitionalAggregate emits sig via circl.SignTo(SkBytes);
the wrong A only contaminated transcript-level commits which
remain self-consistent across parties
- keygen pub: t = A·s1 + s2 was computed BEFORE the spurious post-
NTT step, so the resulting t1 matches circl's
v0.3 was the first mode that propagated the wrong A all the way to
the output FIPS 204 signature.
Fix: drop the post-NTT loop in deriveKeyMaterial. Also drop the
equivalent spurious .ntt() on locally-sampled A in manualVerifyOnce
(threshold_v03_bytediff_test.go) and TestPrimDiff_VerifyPipeline.
New regression guards:
- threshold_v03_aprime_test.go: pins PolyDeriveUniform output ==
circl's pk.A[0][0] byte-for-byte (FIPS 204 ExpandA convention)
- threshold_v03_unsafediff_test.go: pins our km.a vs circl's pk.A
matrix-wide byte-equality
- threshold_v03_primdiff_test.go: end-to-end verify pipeline
diagnostic that walks each FIPS 204 verify step on a circl-
produced sig and confirms c̃' == c̃-from-sig
Test surface: 155 PASS / 0 FAIL / 2 SKIP (was 146/6/2). -race green.
Load-bearing safety contracts intact:
- TestAlgebraic_NoSkAccess PASS (no sk parameter reachable from
AlgebraicAggregate)
- TestTransitional_DependsOnSkBytes PASS (v0.2 still requires SkBytes)
- TestN1_ByteEquality_* PASS (Class N1 preserved)
Closes PULSAR-V03-1.
The two build-tagged files implemented a goroutine fan-out, NOT a GPU
dispatch. Real GPU NTT for the Pulsar ring math lives in
luxfi/lattice/v7/gpu via the consensus engine accel pipeline.
Collapse to a single file dkg_parallel.go with no build tag. Default OFF
(matching prior non-gpu build); tests opt in via SetDKGGPUForTest.
What's now PROVEN:
- TestPulsarSigPackRoundtrip PASS: Pulsar's polyPackLeGamma1 +
polyVecPackHint are byte-equal to circl's pack on a circl-produced
sig. Pack/unpack are NOT the bug.
- TestV03_CTildeConsistency PASS: all 3 parties compute the SAME c̃
as the aggregator (Σ w_i derivation is consistent). c̃ is NOT the
bug — every party uses the same c.
- TestV03_StageAlgebraicAndCheckSig: converges at attempt 6, hint
has 27 one-bits (well under omega=55), but circl Verify rejects.
What's RULED OUT:
- Pack/unpack byte layout (proven)
- c̃ derivation consistency across parties (proven)
- z computation (debug tests show z self-consistent with primitives)
- NTT/montReduce/decompose/le2qModQ (source-equal to circl)
- mu derivation (FIPS 204 §5.4 conformant: tr || 0x00 || |ctx| || ctx || M)
What's LIKELY:
- The HINT BITS encoded in the sig don't correctly bridge what the
verifier sees (A·z - c·t_1·2^d in coefficient form, decomposed,
hint-corrected) back to the prover's w_1 (HighBits of Σ w_j).
The makeHint algorithm, in source-by-source review, IS identical to
circl's. Same convention, same boundary check, same return values.
But somehow the bits emitted differ from what verify expects.
Next-session debug:
- Capture circl's sig (which verifies) and our sig (which doesn't)
for the SAME (master sk, msg, y_total, c). The y_total can be
injected by replacing per-party RNG with a stream that fully
determines y. With matching y and c, z and h should be byte-equal
between circl and us. First diverging byte localises the bug.
- Most likely culprit: subtle integer overflow in cs2/ct0 sum where
the per-party invNTT result has coefficients in [0, 2q) and the
aggregator's add() (no reduction) produces values that exceed the
expected bound before the final normalize. The hint compute
operates on these and emits wrong bits.
Per CLAUDE.md x.x.x+1.
Adds two diagnostic tests to chase the v0.3 byte-equality bug:
- TestAlgebraic_DebugVsCirclVerify: confirms v0.3 sig FAILS stock
mldsa65.Verify and logs c̃ fingerprints + SHA-256 of both our sig
and a circl sig (different y, so direct byte-compare uninformative).
- TestManualVerify_OnV01SigSanity: SANITY CHECK — runs our manual
Verify against a circl-produced sig that is KNOWN to pass circl
Verify. RESULT: our manual verify also REJECTS the circl sig.
This means our manual verify code itself has a bug, so the v0.3
"manual verify fails" signal is NOT authoritative — it does NOT
prove the v0.3 sig is wrong; only that one of (manual verify, v0.3
sig) is wrong.
- TestAlgebraic_ManualVerify (existing): retained, but the "FAIL"
result now needs interpretation through the sanity test lens.
Bugs found and fixed in the manual-verify code along the way:
- A matrix passed to polyDotHat must be NTT'd (was coefficient form)
- mulBy2toD arg order (was dst<-self, fixed to dst<-arg)
- useHint comparison was Q/2 vs Q (circl uses Q)
After all those fixes the manual verify still rejects circl sigs,
so at least one more bug remains in the manual verify code that
needs to be isolated before this can authoritatively diagnose v0.3.
The protocol-level public-BFT-safety contract (TestAlgebraic_NoSkAccess)
remains intact: AlgebraicAggregate has no sk parameter, no sk-bearing
primitive reachable. The byte-equality with stock circl mldsa65.Verify
is the v0.3 interop gap; tracked in BLOCKERS.md::PULSAR-V03-1.
Per CLAUDE.md x.x.x+1.
DEPLOYMENT-RUNBOOK now has TL;DR table mapping deployment scenarios to
the right Combine variant:
- Public BFT, no aggregator-host trust: AlgebraicAggregate (v0.3)
with caveat that v0.3 sig is not yet circl-Verify-accepted
- Public BFT, interop with stock FIPS 204: v0.1 Combine inside TEE
- M-Chain / A-Chain custody: v0.1 Combine, dealer/TEE in TCB by policy
BLOCKERS.md::PULSAR-V03-1 entry updated with:
- Narrow scope: the bug is byte-equality with circl's mldsa65.Verify
- Primitive-by-primitive source-review verification (NTT, mulHat,
montReduce, decompose, makeHint, packLeGamma1, packHint, mu, c̃,
sigEncode pack order) — all match circl source
- Next-session debug plan: side-by-side deterministic-mode sig
comparison to find first diverging byte
- Workaround: use TransitionalAggregate (v0.2) until closed
No code change; documentation only. Per CLAUDE.md x.x.x+1 rule.
The mu fix in v1.0.15 (Round2Sign + AlgebraicAggregate use FIPS 204
§5.4 mu = SHAKE256(tr || 0x00 || |ctx| || ctx || M)) made the
algebraic z computation byte-equal to circl's z. But the iso/debug
test helpers were still computing the OLD mu (tr || M without
domain separator), so they recomputed a "ghost z" that didn't
match Round2Sign's actual output.
This commit aligns the test helpers with the production mu:
- threshold_v03_iso_test.go line 98-105: add 0x00, 0x00 prefix
- threshold_v03_debug_test.go line 216-222: same
After this fix:
- TestAlgebraic_Debug_AlgebraicReconstruction PASS (zRecon vs zRef: 0 diffs)
- TestAlgebraic_Debug_ZSumVsCircl PASS (z matches circl byte-for-byte)
- TestAlgebraic_Debug_PartyZRecomputation PASS
Public-BFT-safety contract (load-bearing test):
- TestAlgebraic_NoSkAccess PASS — AlgebraicAggregate function signature
has NO sk parameter and function body NEVER references KeyFromSeed,
mldsaSign, or any sk-bearing primitive.
Other v0.3 tests still passing:
- TestAlgebraic_BadMAC_Detected, BadCommit_Detected, DishonestParty_BadZ
- TestAlgebraic_DealerReproducible, TminusOne_Fails, RestartConverges
Remaining FAIL (BLOCKERS.md::PULSAR-V03-1):
- TestAlgebraic_FullCycle_n5_t3, VariousQuorums, ByteValid, RealRNG_Smokes
- These all fail because the output signature, while ALGEBRAICALLY
CORRECT (debug tests confirm z, mu, c̃, c match circl), does not
pass circl's FIPS 204 Verify. The bug is somewhere subtle in:
(a) cs2 / ct0 sum across the per-party contributions
(b) The MakeHint args or boundary semantics
(c) The sigEncode pack at the byte level
- All primitive checks against circl source verified equivalent
(decompose, makeHint, packHint, packLeGamma1). Bug needs side-by-side
debugging with breakpoints comparing intermediate buffers; deferred
to a focused session.
The public-BFT-safety CONTRACT (no sk access) is satisfied today.
The byte-equality FOR INTEROP with stock FIPS 204 verifiers is the
remaining gap.
What's done:
- AlgebraicThresholdSigner, AlgebraicSetup, AlgebraicKeyShare types
- DealAlgebraicV03Shares (polynomial-vector Shamir over R_q)
- Round1 / Round2W / Round2Sign (each party computes z_i, cs2_i, ct0_i
LOCALLY from their share, broadcasts contributions; aggregator never
receives anything sk-derived)
- AlgebraicAggregate sums per-party contributions, computes c̃ from
Σ w_i, applies FIPS 204 rejection checks globally, packs σ via
sigEncode — NEVER touches setup.SkBytes (no master sk access)
Tests passing:
- TestAlgebraic_NoSkAccess — structural proof that AlgebraicAggregate's
call signature has no sk parameter and the function body does not
reference KeyFromSeed or any sk-derived path. THIS IS THE LOAD-BEARING
PUBLIC-BFT-SAFETY CONTRACT.
- TestAlgebraic_Debug_AlgebraicReconstruction — confirms Lagrange-share
reconstruction is mathematically correct (s_1 = Σ λ_i · s_1_i).
- TestAlgebraic_Debug_ZSumVsCircl — confirms Σ z_i byte-equals circl's
z when y is held fixed across paths. The algebraic z computation
works.
- TestAlgebraic_BadMAC_Detected / BadCommit_Detected — tamper detection.
Tests still failing (v0.3 graduation gate):
- TestAlgebraic_FullCycle_n5_t3 — output signature fails FIPS 204
Verify under circl. The algebraic z, cs2, ct0 sums are correct
(debug tests confirm) but the final (c̃, z, h) byte layout has a
discrepancy that doesn't yet pass circl's Verify equation. The
bug is somewhere in c̃ derivation, hint computation, or sigEncode
pack order — needs careful trace against circl's internal Sign.
What this means operationally:
- The PUBLIC-BFT-SAFETY CONTRACT is satisfied at the API surface:
no party ever materializes the master sk during signing.
- The signature is NOT YET acceptable to stock FIPS 204 verifiers.
- v0.2 Transitional (which does materialize sk at aggregator) remains
the wire path that produces FIPS 204 byte-equal sigs.
- v0.3 (this commit) is the path that REMOVES sk materialization;
finishing the byte-equality bug is the v1.0.16+ work tracked in
BLOCKERS.md::PULSAR-V03-1.
mu fix applied: FIPS 204 §5.4 requires μ = SHAKE-256(tr || 0x00 ||
|ctx| || ctx || M, 64); the previous derivation skipped the 0x00 ||
|ctx| domain separator. Both Round-2 and Aggregate now use the
correct form.
Per CLAUDE.md x.x.x+1 patch rule.
The v0.2 API previously named Algebraic* was materially misleading:
the file-header docstring claimed "AlgebraicCombine is a pure function
of public-only material; no access to any party's seed share or the
master ML-DSA private key" while the implementation calls
mldsaSign(setup.SkBytes, message, ...) against the master sk packed
in AlgebraicSetup.SkBytes. The aggregator TCB at sign time is
identical to v0.1 reconstruct-and-sign; only the parties' side of
the protocol is honestly algebraic.
v1.0.14 fixes the honesty defect without changing the algorithm.
Forward-only renames (no compat aliases):
- AlgebraicThresholdSigner → TransitionalThresholdSigner
- NewAlgebraicThresholdSigner → NewTransitionalThresholdSigner
- AlgebraicCombine → TransitionalAggregate
- AlgebraicSetup → TransitionalSetup
- AlgebraicRound1Message → TransitionalRound1Message
- AlgebraicRound2Message → TransitionalRound2Message
- DealAlgebraicShares → DealTransitionalShares
- ErrAlg* → ErrTransitional*
- TestV02_* → TestTransitional_*
Docstrings rewritten to honestly disclose the SkBytes dependency:
- threshold_v02.go file header opens with HONEST SCOPE.
- TransitionalAggregate docstring spells out TCB equivalence with v0.1.
- TransitionalSetup names SkBytes as the v0.2 TCB defect.
- types.go Signature docstring rewritten.
- DEPLOYMENT-RUNBOOK.md rewritten with "suitable / not suitable"
deployment guidance and a v0.3 milestone section.
New load-bearing test:
- TestTransitional_DependsOnSkBytes runs the full Round1/Round2
cycle, sets setup.SkBytes = nil, asserts TransitionalAggregate
returns ErrTransitionalNoSetup. Currently PASSES (because v0.2
depends on SkBytes). When v0.3 lands, this test FAILS — that
failure is the load-bearing red flag for v0.3 graduation.
New blocker tracker:
- BLOCKERS.md entry PULSAR-V03-1 documents the v0.3 algebraic-sign
work: port FIPS 204 sign internals to polynomial-share arithmetic,
drop SkBytes, rename Transitional* → Algebraic* (forward-only).
Wire protocol is byte-for-byte unchanged: PULSAR-ALG-* customisation
strings are preserved so existing v0.2 test vectors keep verifying.
Tests: full suite green (go test -count=1 -short -timeout 300s
./pkg/pulsar/ → ok 2.053s).
Adds AlgebraicThresholdSigner, AlgebraicSetup, PolyKeyShare, and
AlgebraicCombine — the v0.2 FROST-for-FSwA protocol surface where
parties hold polynomial-vector Shamir shares of (s_1, s_2, t_0) over
GF(q) rather than seed shares of a 32-byte master.
Protocol wire shape (commit / intermediate w-reveal / sign-contribute
/ aggregate) matches Boschini-Takahashi-Tibouchi (IACR 2024/1113) and
the FROST-RFC9591 analog. Round 1 emits Commit(w_i = A·y_i) under
MAC; Round 2 reveals w_i; AlgebraicCombine sums w to derive c̃ and
emits the signature.
v1.0.13 transitional inner sign. The Round-2 contributions
(W, Z, CS2, CT0) are exchanged on the wire and verified for
commit-bind plus per-pair MAC, then the inner FIPS 204 signature
emission delegates to circl's mldsa{44,65,87}.SignTo via the
AlgebraicSetup.SkBytes carrier. The trust model at the aggregator
side is therefore equivalent to v0.1's reveal-and-aggregate at sign
time. The PROTOCOL-side invariant — PARTIES never hold the master
seed in any form — is preserved by the polynomial-share carrier;
v0.3 closes the inner-sign gap by emitting from per-party (z_i, h_i)
contributions directly without touching SkBytes.
DEPLOYMENT-RUNBOOK adds the v0.2 algebraic-threshold section with
the two-path trust-model disclosure; types.go Signature docstring
updated to point new callers at AlgebraicCombine while keeping v0.1
Combine for TEE-bound custody use cases.
Tests (8 v0.2-specific, all green):
- TestV02_RoundTripAndVerify {5of3, 7of4, 10of7}
- TestV02_NoMasterKeyAccess
- TestV02_BadMAC_Detected
- TestV02_BadCommit_Detected
- TestV02_RestartConverges
- TestV02_DealerReproducible
- TestV02_AlgebraicAgreesWithSinglePartySign
- TestV02_RealRNG_Smokes
Full suite (-race) green. v0.1 Combine path unchanged.
Salvage the design-doc set the May 2026 pulsar-mptc consolidation
listed in its "Was here / Is now" table but never actually copied
over. Pulled from pulsar-mptc parent commit 7ee7183^ (the last
pre-deletion state) so the content is byte-equal to what the
archived repo carried.
Files imported:
- SUITE.md
- HANZO-CRYPTO-SUITE.md
- INFORMATION-ARCHITECTURE.md
- ROADMAP.md (80 KB)
- SINGLE-IMPL-PLAN.md
- SYNC-STATUS.md
- STATUS-SUBMISSION-READINESS.md
- docs/magnetar.md
- docs/x-wing-sig.md
Pulsar is now the single canonical home for impl + spec + proofs +
KAT vectors + cut tool + design docs. The pulsar-mptc GitHub remote
is archived (verified via gh repo view --json isArchived).
Reverts the TEE attestation surface added in v1.0.11 (TEEAttestation,
TEEQuoter, CombineWithAttestation, CombineReportData, tagCombineWindow,
tee_attestation_test.go). Pulsar is the threshold sign protocol used by
public-BFT consensus; it MUST be runnable without a TEE assumption
because adding one shifts the trust root from "honest supermajority"
(what Lux's BFT already guarantees) to "the TEE vendor's signing key"
— that's a regression for a public leaderless chain.
The honest fix for the master-key reconstruction concern is to move
to the v0.2 Lagrange-linearity sign path (each party signs its own
share locally; aggregator combines signature shares, never the key).
That target was already on the Pulsar roadmap before this rip; the
v0.1 reconstruction-aggregator path stays for single-operator-custody
and dev/test scenarios where the trust model already places the
aggregator inside the TCB.
Callers that want additional TEE binding (M-Chain bridge custody,
A-Chain compute providers under aivm) wire attestation at THEIR
layer — github.com/luxfi/ai/pkg/attestation is the canonical home
for SGX/SEV/TDX/nvtrust quoters. Pulsar stays TEE-agnostic so the
SAME protocol works on the public chain (no TEE) and on a
confidential-compute subnet (with TEE) without bifurcating the
signature wire format.
types.go Signature docstring is rewritten to reflect this layering:
v0.1 is dev/single-operator only; v0.2 is the production path for
public BFT; TEE wrapping happens at the consumer.
threshold.go / types.go: expose CombineWithAttestation() so the
aggregator can emit a TEE quote covering the brief master-key
reconstruction window inside Combine(). Signature.Attestation is
nil-by-default (no behavioural change for current consumers); when
populated the relying party can verify reconstruction happened
inside a measured enclave with sealed memory.
Signature.Bytes is unchanged stock FIPS 204 ML-DSA — any conformant
verifier accepts the threshold-produced signature without code
changes. The TEE attestation is an ADDITIONAL layer for relying
parties who do not trust the aggregator host.
The trust-model disclosure in Signature's docstring now states
explicitly: without TEE attestation the post-quantum guarantee
degrades to "trust the aggregator host" for the duration of the
Combine call. This was previously implicit; making it explicit
addresses the cryptographer review note on threshold-Combine
master-key reconstruction.
TEEQuoter interface lets the aggregator wire any platform: Intel SGX
DCAP, AMD SEV-SNP, Intel TDX, ARM CCA, AWS Nitro. CombineReportData
gives a stable 64-byte binding (sid, attempt, groupPubkey, message)
domain-separated under tagCombineWindow so a quote attesting to one
ceremony cannot be replayed as proof of a different one.
dkg.go / large_dkg.go: route shamirDealRandom{,Q} through the
accel-dispatched mirror (shamirDealRandom{GF,Q}Accel). Default build
is byte-identical to the historical CPU path; `-tags gpu` enables
the parallel byte-slot fan-out. Output is byte-equal across both
modes (TestDKG_GPU_ByteEqual pins this).
Go package is already named 'pulsar' (per imports in luxfi/consensus
quasar/witness.go). Only stale doc paths in LLM.md and gitignore
patterns carried the legacy 'pulsarm' identifier.
LLM.md: Updated all 'ref/go/pkg/pulsarm/' path references to
'ref/go/pkg/pulsar/' (matches actual filesystem).
.gitignore: Removed pulsarm-prefixed dudect lib patterns; kept the
pulsar-prefixed patterns (which match actual artifacts).
Formal academic name "Pulsar-M" remains in NIST MPTC submission
LaTeX (spec/pulsar-m.tex) and proofs/pulsar-m/ for paper-consistency.
Adds an INDEPENDENT verifier binding to the pq-crystals/dilithium
reference implementation so Pulsar signatures can be cross-checked
against the canonical FIPS 204 reference verifier. This is the same
discipline as the libjade Jasmin track: prove byte-equality / accept-
equivalence against an authoritative third-party impl.
The pq-crystals tree itself (~MB) is fetched on demand via fetch.sh
at a pinned commit and gitignored to keep history small. Only the Go
binding (verifier.go), C glue (pq_crystals_verify.{c,h}), class test
(pq_crystals_class_test.go), and the fetch script live in git.
WHY: closes the third-party-verifier gap for ML-DSA equivalence
testing without vendoring upstream sources.
Detailed invention disclosures are sensitive pre-filing material. Moved to
lux-private/patents to preserve international filing rights (EU/JP/KR/CN
absolute-novelty rule). Cross-ref: github.com/lux-private/patents/bundles/
Consolidated state: pulsar is self-contained with canonical Go ref
under ref/go/pkg/pulsar/. The vendor/ snapshot and the framework-side-
glue framing no longer apply; the reviewer-facing tree description
and the build instructions reflect the in-tree canonical sources.