magnetar v0.5.0: public-BFT framing + standalone+aggregate + dkg2 skeleton

The user's verbatim position: "we can't do a trusted dealer obv. it
HAS to be freking done right for public chains bro"

WHAT SHIPPED

1. PRIMARY PUBLIC-BFT PRIMITIVE (standalone.go + aggregate.go):
   - PerValidatorKeypair / ValidatorSign / ValidatorBatchVerify
   - BuildAggregateCert / VerifyAggregateCert
   - This is the ARCHITECTURALLY CORRECT pattern for SLH-DSA in
     public BFT: each validator has its own keypair (no DKG),
     signs independently, consensus collects N signatures. Mirrors
     what consensus.QuasarCert.MLDSAProof already does for ML-DSA
     via Z-Chain Groth16 rollup. SLH-DSA's lack of algebraic
     structure (Cozzo-Smart 2019, Bonte-Smart-Tan 2023) means true
     threshold SLH-DSA WITHOUT a dealer/TEE/MPC is research-grade;
     per-validator + aggregation is the honest answer.

2. THBS SUBPACKAGE DEMOTED (thbs/dealer.go + thbs.go docstrings):
   - Renamed to make the dealer-backed v1 trust assumption explicit
     at the API surface. "NOT public-BFT-safe" headers added.
   - Kept for M-Chain bridge custody / single-operator TCB scenarios
     where the dealer is in the TCB by policy.

3. DKG2 SKELETON (thbs/dkg2/):
   - PVSS layer (pvss.go, complaint.go, consensus.go) — each party
     dealer-shares contribution r_{j,e} for every secret element e
     via verifiable shares. No single party ever holds x_e = Σ r_{j,e}.
   - MPC root layer (root.go) STUB — returns ErrMPCRootNotImpl.
     This is the v0.6+ research deliverable: jointly compute
     H(x_e) for every WOTS+/FORS leaf without revealing x_e.
   - README + doc.go cite the literature honestly (Schoenmakers PVSS
     1999, Gurkan aggregatable DKG 2021, SPDZ MPC 2012, Boyle-Gilboa-
     Ishai FSS 2015, MP-SPDZ framework, McGrew et al. threshold HBS
     IACR 2019/793, Bonte-Smart-Tan threshold SPHINCS+ 2023).
   - Tests pass (skeleton-only assertions, MPC root stub returns
     the documented sentinel).

DEPLOYMENT-RUNBOOK.md REWRITTEN — clear guidance:
- PUBLIC BFT CONSENSUS → magnetar.ValidatorSign + VerifyAggregateCert
- M-CHAIN CUSTODY (TEE/dealer in TCB) → CombineWithSeedReconstruction
  or thbs.DealerDKG + SignShare + Aggregate
- RESEARCH/FUTURE → thbs/dkg2/ when MPC root layer lands

BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1 — open research entry:
Public DKG for HBS requires MPC over SHA-256/SHAKE to compute public
roots from secret-shared leaves. Estimated multi-hour per signature
at current MPC framework throughput (MP-SPDZ ~hundred-ms per SHA-256
block × 750K SHAKE evaluations for SLH-DSA-SHAKE-192s public key).
v0.6+ target.

CHANGELOG.md entry for v0.5.0.

Tests green:
- ref/go/pkg/magnetar/ ok (105s, includes standalone + aggregate)
- ref/go/pkg/thbs/ ok (dealer-backed THBS, slot guard, equiv evidence)
- ref/go/pkg/thbs/dkg2/ ok (skeleton with documented stub)

Per CLAUDE.md, minor bump for new public API surface
(magnetar.ValidatorSign + dkg2/ subpackage).
This commit is contained in:
Hanzo AI
2026-05-22 00:17:54 -07:00
parent 5a13eda0e9
commit 40618d99f9
14 changed files with 2554 additions and 73 deletions
+104 -2
View File
@@ -4,13 +4,19 @@
> **full Tier A** (Pulsar-equivalent) on the submission-readiness
> ladder.
>
> **As of v0.3.0**: Tier C → Tier B blockers (BLK-1, BLK-2, BLK-3)
> **As of v0.5.0**: Tier C → Tier B blockers (BLK-1, BLK-2, BLK-3)
> are **CLOSED** at v0.2.0. BLK-5 (KAT vectors), BLK-8 (submission
> package documentation shape) are **CLOSED** at v0.3.0. BLK-4
> (reference impl + v0.4 lifecycle additions), BLK-6 (cross-validation
> harness), BLK-7 (proof artifacts), BLK-9 (independent
> cryptographer review) are partially or fully open; see status
> below. See `SPEC.md` for the selected construction,
> below. **MAGNETAR-PUBLIC-DKG-1** (public-DKG threshold HBS) is the
> v0.6+ research-grade open task; the v0.5.0 release ships the PVSS
> skeleton at `ref/go/pkg/thbs/dkg2/` and demotes the threshold form
> to the M-Chain custody / TEE path while promoting per-validator
> standalone SLH-DSA (`magnetar.ValidatorSign` +
> `magnetar.VerifyAggregateCert`) as the public-BFT primary primitive.
> See `SPEC.md` for the selected construction,
> `SUBMISSION-STATUS.md` for tier definitions and the phased plan,
> `CRYPTOGRAPHER-SIGN-OFF.md` Gates section for the full Tier A
> gate inventory.
@@ -180,6 +186,102 @@ External engagement is blocked on:
- GATE-3 (dudect 10⁹ samples, v0.6.0)
- GATE-5 (v0.4 lifecycle additions for the deployment posture)
## Research path: public-DKG threshold HBS
### MAGNETAR-PUBLIC-DKG-1 — Public DKG for threshold HBS → **OPEN (research-grade, v0.6+ candidate)**
**Context**: the `thbs/` subpackage ships true-threshold HBS per
McGrew et al. (IACR ePrint 2019/793) but the v1 setup is
DEALER-BACKED — a single dealer generates the WOTS+ chain heads
and FORS secret leaves at setup, Shamir-shares them, and erases the
master seed. Dealer-backed threshold HBS is **NOT public-BFT-safe**
(the dealer learns every secret element at setup time). The user's
hard architectural position:
> "We can't do a trusted dealer obv. It HAS to be freaking done
> right for public chains bro."
>
> "For HBS, the hard part is that public keys are hashes/Merkle
> roots of many secret values. Hashes are nonlinear... a practical
> public-safe DKG must generate and commit to shared leaf/chain
> material, not merely share a seed."
>
> "DKG = PVSS for secret shares + MPC/public verification for
> derived roots."
**What's needed** (per the user spec):
1. **PVSS layer (Ingredient A)** — each party j ∈ [n] samples a
contribution `r_{j,e}` for every secret element e, Shamir-shares
`r_{j,e}` across the committee at threshold t. The joint secret
is `x_e = Σ_j r_{j,e}`; no single party ever holds `x_e`. **A
SKELETON of this layer ships at `ref/go/pkg/thbs/dkg2/` in
v0.5.0** (`pvss.go`, `complaint.go`, `consensus.go`).
2. **MPC-root layer (Ingredient B)** — MPC over SHA-256/SHAKE to
compute the public chain endpoints `W_e = H^{w-1}(x_e)` from
secret-shared `x_e` without revealing any `x_e`. **THIS IS THE
HARD FRONTIER.** Candidate frameworks:
- SPDZ-style MPC (Damgård-Pastro-Smart-Zakarias CRYPTO 2012)
- Garbled circuits + OT (Wang-Ranellucci-Katz CCS 2017)
- Function Secret Sharing (Boyle-Gilboa-Ishai EUROCRYPT 2015)
- MP-SPDZ integration (https://github.com/data61/MP-SPDZ)
For SLH-DSA-SHAKE-192s with the v1 thbs parameter set this is
~750K SHAKE evaluations per DKG ceremony — multi-hour to
multi-day per ceremony at current framework performance. The
cryptographer team selects the production MPC framework when
integrating; the v0.6+ candidate target is "honest cost
estimate + working demo at small parameters."
**Status of the skeleton** (`ref/go/pkg/thbs/dkg2/`):
-`doc.go` — package doc, scope, literature (Schoenmakers PVSS,
SPDZ, garbled-circuits, function-secret-sharing).
-`pvss.go` — Deal / Verify wire shape; `ErrSkeletonOnly` stubs
prevent production consumption.
-`complaint.go` — Complaint round wire shape (BadShare,
MissingDelivery, CommitmentMalformed); FileComplaint / DealerDefend
stubs.
-`consensus.go` — Qualified-set agreement + Run orchestrator;
`RootMPC` stub returns `ErrMPCRootNotImpl` with explicit pointer
to this BLOCKERS entry.
-`README.md` — public-facing scope + research path.
- ✅ Skeleton tests pin the `ErrSkeletonOnly` / `ErrMPCRootNotImpl`
sentinels.
**What blocks shipping a working dkg2** (the v0.6+ checklist):
1. Select the production MPC framework (MP-SPDZ, EMP-toolkit,
custom).
2. Implement the SHAKE-256 chain circuit under the chosen MPC.
3. Benchmark per-element cost; choose parameter set + committee
size that fits the deployment budget.
4. Write production `root.go` (compute joint W_e + public Merkle
root from secret-shared x_e).
5. Wire-format spec for the public root computation transcript
(auditability across re-runs).
6. Integrate with parent `thbs.PublicKey` so the resulting threshold
HBS public key is BYTE-EQUAL to a dealer-DKG'd public key for
the same elements.
7. Re-prove the construction (security argument under PVSS + MPC
composition; the user's "PVSS + MPC/public verification for
derived roots" framing).
8. Independent cryptographer review (BLK-9-analog gate).
9. Independent MPC engineer review of the SHAKE-circuit
implementation.
**Until MAGNETAR-PUBLIC-DKG-1 closes**:
- PUBLIC-BFT CONSENSUS uses `magnetar.PerValidatorKeypair` +
`magnetar.ValidatorSign` + `magnetar.VerifyAggregateCert`
(per-validator standalone SLH-DSA, no DKG, no dealer, no
aggregator-as-TCB). See `DEPLOYMENT-RUNBOOK.md` §9.
- M-CHAIN CUSTODY uses `magnetar.CombineWithSeedReconstruction`
OR `thbs.DealerDKG` inside a TEE-attested host (the dealer/
aggregator is in the TCB by policy). See `DEPLOYMENT-RUNBOOK.md`
§1.
## Non-blockers
These are NOT blockers — they are deliberate non-claims that
+133
View File
@@ -10,6 +10,139 @@ Magnetar adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
(Nothing pending at this writing.)
## [0.5.0] — 2026-05-21 — Per-validator standalone is the primary public-BFT primitive; THBS demoted to TEE-only
### Architectural decomplecting (HONEST framing)
This release re-architects Magnetar for **public-BFT-safety**. The
previous v0.4.x messaging conflated two distinct trust models:
threshold-HBS (custody, requires dealer/aggregator-in-TCB) and
N-of-N collected signatures (public-BFT, no TCB). v0.5.0 ships the
canonical separation:
- **PRIMARY public-BFT primitive**: per-validator standalone
SLH-DSA via `magnetar.PerValidatorKeypair` +
`magnetar.ValidatorSign` + `magnetar.VerifyAggregateCert`
(`ref/go/pkg/magnetar/standalone.go`). Per-validator keypair, no
DKG, no dealer, no aggregator-in-TCB. The consensus layer
collects N validator signatures and counts valid signers; the
quorum policy decision lives at the consumer. Z-Chain Groth16
rollup compresses N × |σ| to ~192 bytes.
- **CUSTODY (TEE required)**: `magnetar.CombineWithSeedReconstruction`
(reveal-and-aggregate) OR `thbs.DealerDKG` + `thbs.SignShare` +
`thbs.Aggregate` (true threshold HBS). The dealer / aggregator
is in the TCB by policy — both REQUIRE TEE attestation.
- **RESEARCH (v0.6+)**: `thbs/dkg2/` skeleton ships the PVSS
layer; MPC-root layer is OPEN RESEARCH tracked at
`BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1`.
The honest architectural truth: SLH-DSA is hash-based and has no
algebraic structure that admits FROST-style threshold aggregation
(Cozzo-Smart EUROCRYPT 2019; Bonte-Smart-Tan 2023; NIST IR 8214).
True-threshold SLH-DSA without TEE/dealer requires full MPC over
the SHA-256/SHAKE hash tree — research-grade. For Lux's PUBLIC-BFT
consensus, per-validator standalone is the right primitive; for
M-Chain custody (where a TEE-attested host can be in the TCB by
policy), the threshold form is correct.
### Added
- **`ref/go/pkg/magnetar/standalone.go`** — the public-BFT-safe
PRIMARY primitive surface:
- `PerValidatorKeypair(params, rng) → (sk, pk, error)` — standalone
FIPS 205 keypair generator. Semantic alias for
`GenerateValidatorKey`; explicit name for the public-BFT path.
- `ValidatorSign(sk, rng, message) → ([]byte, error)` — canonical
public-BFT signing primitive. Returns raw FIPS 205 signature
bytes. Deterministic when rng is nil (FIPS 205
SignDeterministic).
- `ValidatorBatchVerify(params, pubs, msgs, sigs) → ([]bool, error)`
— parallel-CPU batch verify returning per-signer bitmap.
Routes through the same goroutine fork-join pattern as
`luxfi/crypto/slhdsa.VerifyBatch`.
- `ValidatorAggregateCert{Mode, Signers, PubKeys, Sigs}`
parallel-slices wire envelope for N per-validator signatures
over a single message. Sibling shape to `AggregatedSignature`
(aggregate.go) but maps directly to Z-Chain Groth16 rollup
witness layout.
- `BuildAggregateCert(params, signers, pubKeys, sigs)`
constructor with shape + mode validation and defensive copies.
- `VerifyAggregateCert(cert, message, knownValidators) → (count, err)`
— verifies every signature against the registered public key
and returns the count of valid signers. Unknown / pubkey-
mismatched signers are counted as INVALID (not fatal — defense
against impersonation that does not abort the entire batch).
- **`ref/go/pkg/magnetar/standalone_test.go`** — 7 tests covering
round-trip + determinism + independence, parallel batch verify,
bad-sig-counted-out, unknown-validator rejection,
duplicate-validator dedupe-and-count-once, GPU/parallel
provenance assertion, all-invalid returns-zero, and shape checks.
- **`ref/go/pkg/thbs/dkg2/`** — research-grade SKELETON of a
public DKG for threshold HBS. v1 ships the PVSS layer; the
MPC-root layer is OPEN RESEARCH tracked at
`BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1`. Files:
- `doc.go` — package doc, scope, literature (Schoenmakers PVSS,
Damgård-Pastro-Smart-Zakarias SPDZ, Boyle-Gilboa-Ishai FSS,
Wang-Ranellucci-Katz MPC).
- `pvss.go` — Deal / Verify wire shape with `ErrSkeletonOnly` /
`ErrMPCRootNotImpl` sentinels preventing production
consumption.
- `complaint.go` — Complaint round wire shape (BadShare,
MissingDelivery, CommitmentMalformed) + dealer Defense.
- `consensus.go` — qualified-set agreement + `Run` orchestrator +
`RootMPC` stub. Every public function returns
`ErrSkeletonOnly` or `ErrMPCRootNotImpl`.
- `README.md` — public-facing scope, literature, v0.6+ checklist.
- `dkg2_test.go` — sentinel-pinning tests.
- **`BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1`** — open-research entry
for public DKG over HBS. PVSS skeleton shipped; MPC-root layer
open. 9-step v0.6+ checklist.
### Changed
- **`ref/go/pkg/thbs/thbs.go`** — package doc rewritten to honestly
demote the threshold form to "DEALER-BACKED v1 — NOT public-BFT-
safe". Directs callers to `magnetar.ValidatorSign` for public BFT
and to `thbs.DealerDKG` only for M-Chain custody where the
dealer is in the TCB by TEE attestation. Cites the
`BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1` research path.
- **`ref/go/pkg/thbs/dealer.go`** — renamed `DKG``DealerDKG` and
`DKGAll``DealerDKGAll`. The function bodies are unchanged;
the rename honestly reflects that v1 has a dealer. Callers in
`thbs_test.go` updated. `DKGConfig` / `DKGOptions` retained
(config struct shape applies to both v1 dealer and a future v2
public DKG).
- **`DEPLOYMENT-RUNBOOK.md` §0** — rewritten to direct PUBLIC BFT
CONSENSUS users to `magnetar.ValidatorSign` +
`magnetar.VerifyAggregateCert` as the primary primitive; demotes
`CombineWithSeedReconstruction` and `thbs.DealerDKG` to the
M-Chain custody / TEE-attested path; cites `thbs/dkg2/` as the
v0.6+ research path. New §9 documents the
`ValidatorSign`/`VerifyAggregateCert` primary primitive in detail.
- **`BLOCKERS.md`** — header updated to v0.5.0 framing; new
`MAGNETAR-PUBLIC-DKG-1` research entry.
### Honest framing
- The threshold form of Magnetar (both `CombineWithSeedReconstruction`
and `thbs.DealerDKG`) is FUNDAMENTALLY not public-BFT-safe. This
is not a Magnetar choice; it is a property of hash-based
signatures. The literature is clear (Cozzo-Smart, Bonte-Smart-Tan,
NIST IR 8214). The honest fix is the architectural pivot in
v0.5.0: use the threshold form for custody (where TEE
attestation puts the dealer/aggregator in the TCB by policy) and
use per-validator standalone for public consensus.
- The Lux consensus stack ALREADY does the right thing for ML-DSA
via `QuasarCert.MLDSAProof` (per-validator standalone ML-DSA
signatures collected into a multi-signer cert). The
`ValidatorSign` + `VerifyAggregateCert` API makes the same
pattern explicit and canonical for SLH-DSA.
- The `thbs/dkg2/` skeleton is honestly research-grade. It exists
to make the v0.6+ research direction concrete without pretending
to ship a working public DKG. Every function returns a sentinel
error pointing at the BLOCKERS entry. A production caller
CANNOT accidentally consume the unfinished pipeline.
## [0.4.2] — 2026-05-21 — THBS v1 + Public-BFT-safe aggregate-signatures API
This release introduces TWO new signing modes:
+187 -47
View File
@@ -5,64 +5,92 @@
## 0. Choose your mode — the most important decision
Magnetar ships **two distinct signing modes** with different trust
Magnetar ships **three distinct signing surfaces** with different trust
models. Picking the wrong one is the single largest deployment risk.
| Property | `AggregateSignatures` (public-BFT) | `CombineWithSeedReconstruction` (custody) |
|---|---|---|
| Output | N separate FIPS 205 signatures | One FIPS 205 signature |
| Aggregator-as-TCB | NO (no party holds the master seed) | YES (master seed live in aggregator) |
| Public-BFT-safe | YES | NO — requires TEE |
| Validator key model | Per-validator, independent keypairs | Shared via DKG + Shamir-split seed |
| DKG required | NO | YES |
| Wire size | N × \|σ\| (e.g. 21 × 16 KiB = 336 KiB at L3) | \|σ\| (16 KiB at L3) |
| Z-Chain Groth16-compressible | YES (~192 B proof) | N/A (already 1 σ) |
| Module | `aggregate.go` | `combine.go` |
| File | `ref/go/pkg/magnetar/aggregate.go` | `ref/go/pkg/magnetar/combine.go` |
| Use case | Lux public-BFT validator quorum | M-Chain custody w/ TEE attestation |
| Surface | Trust model | Public-BFT safe | Use case |
|---|---|---|---|
| `ValidatorSign` + `VerifyAggregateCert` | Per-validator standalone keys | **YES** | **Lux public-BFT validator quorum (PRIMARY)** |
| `AggregateSignatures` (SignedBundle form) | Per-validator standalone keys | YES | Same as above, SDK-friendly bundle shape |
| `CombineWithSeedReconstruction` | Aggregator-as-TCB (TEE required) | NO | M-Chain custody w/ TEE attestation |
| `thbs.DealerDKG` + `thbs.SignShare` + `thbs.Aggregate` | Trusted-dealer-as-TCB (TEE required) | NO | M-Chain bridge custody w/ TEE attestation |
| `thbs/dkg2/` | Public PVSS + MPC (research) | YES (when complete) | Future research; v0.6+ candidate |
### TL;DR
- **PUBLIC BFT CONSENSUS** (validator quorum, untrusted aggregator
host) → `magnetar.PerValidatorKeypair` + `magnetar.ValidatorSign` +
`magnetar.VerifyAggregateCert` (`ref/go/pkg/magnetar/standalone.go`).
This is the **CANONICAL primary primitive for Lux public-BFT
post-quantum consensus**. Per-validator standalone SLH-DSA keypairs;
no DKG; no dealer; no aggregator-as-TCB. The consensus layer
collects N validator signatures and a relying party calls
`VerifyAggregateCert` to obtain the count of valid signers. Wire
cost is N × |σ|; Z-Chain Groth16 rollup compresses to ~192 bytes.
See §10.
- **PUBLIC BFT, SDK SHAPE** (same model as above, but the
consumer prefers the nested-bundle wire format) →
`magnetar.AggregateSignatures` + `magnetar.VerifyAggregated`
(`ref/go/pkg/magnetar/aggregate.go`). Byte-equivalent to the
standalone form; differs only in wire encoding shape.
- **M-CHAIN CUSTODY with TEE** (M-Chain thresholdvm host with
TDX/SEV-SNP/SGX attestation, the LP-134 thresholdvm M-Chain mode)
→ EITHER:
* `magnetar.CombineWithSeedReconstruction` (reveal-and-aggregate;
master seed reconstructed in aggregator memory; trust caveat in
§1 applies), OR
* `thbs.DealerDKG` + `thbs.SignShare` + `thbs.Aggregate` (true
threshold HBS per McGrew et al.; dealer learns secrets at
setup time; per-signature reveals only the SELECTED elements).
Both REQUIRE TEE attestation of the dealer/aggregator host.
- **RESEARCH / FUTURE** (public-DKG threshold HBS, no dealer, no
TEE) → `thbs/dkg2/` ships the PVSS layer as a skeleton. The
MPC-root layer is OPEN RESEARCH; see `BLOCKERS.md::MAGNETAR-PUBLIC-
DKG-1`. Will be the public-BFT-safe THRESHOLD path in v0.6+
once an MPC framework or MPC-friendly hash is selected.
- **Anything else** → `magnetar.ValidatorSign`. The reveal-and-
aggregate and dealer-backed-threshold paths are NOT safe for
public deployments.
### Decision tree
```
──────────────────────────────────┐
│ Who needs to verify the σ?
──────────────────────────────────┘
┌────────────────────────────────────────────────┐
WHO NEEDS TO VERIFY THIS SIGNATURE?
└────────────────────────────────────────────────┘
┌───────────────────┴───────────────────┐
│ │
Public, untrusted Internal,
verifiers (BFT chain, pre-attested
external relayers, etc.) TCB
PUBLIC, INTERNAL,
untrusted verifiers pre-attested TCB
(BFT chain, (M-Chain custody,
external relayers) thresholdvm)
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
AggregateSignatures │ │ Is an aggregator
(public-BFT) │ host attested by TEE│
│ (TDX/SEV-SNP/SGX) ? │
+ Z-Chain Groth16 ─────────────────────┘
rollup for size │
compression │ ┌──────────────┴──────────────┐
└─────────────────────┘ │ │
YES NO
│ │
┌─────────────────────┐ ┌─────────────────────┐
│ CombineWithSeed- AggregateSignatures
│ Reconstruction │ — do NOT use Combine
│ (custody / M-Chain) WithSeedReconstr.
└─────────────────────┘ │ outside TEE!
└─────────────────────┘
┌─────────────────────────┐ ┌─────────────────────────────┐
magnetar. │ Is an aggregator/dealer host
ValidatorSign + attested by TEE
VerifyAggregateCert │ │ (TDX/SEV-SNP/SGX)?
│ └─────────────────────────────┘
(PRIMARY public-BFT- │
safe primitive; │ ┌──────────────────────┐
│ per-validator
│ standalone keys) YES NO
│ + Z-Chain Groth16 ▼ ▼
rollup compresses │ ┌─────────────────────┐ ┌─────────────────────┐
N × |σ| → ~192 B │ Pick one of: │ │ magnetar.ValidatorSign
└─────────────────────────┘ │ - magnetar.Combine- │ │ — DO NOT use the
│ WithSeedReconstr. │ │ dealer / reveal-and-
│ - thbs.DealerDKG + │ │ aggregate paths
│ thbs.SignShare │ │ outside TEE! │
└─────────────────────┘ └──────────────────────┘
```
### TL;DR
- **PUBLIC BFT** (validator quorum, untrusted aggregator host) →
`AggregateSignatures` (N separate sigs). See §10.
- **M-CHAIN CUSTODY with TEE** (M-Chain thresholdvm host with
TDX/SEV-SNP/SGX attestation) → `CombineWithSeedReconstruction`.
Trust caveat in §1 applies — operator MUST enforce the §1 mitigations.
- **Anything else** → `AggregateSignatures`. The reveal-and-aggregate
path is NOT safe for public deployments.
## 1. `CombineWithSeedReconstruction` (v0.1 reveal-and-aggregate) trust caveat
**The Magnetar `CombineWithSeedReconstruction` aggregator process holds the reconstructed master SLH-DSA seed in memory for the duration of one call.**
@@ -169,10 +197,122 @@ Operators deploying Magnetar v0.1 do so at their own risk and SHOULD layer it wi
## 8. Cross-references
- `SPEC.md` — full protocol specification
- `BLOCKERS.md` — Tier B → A path
- `BLOCKERS.md` — Tier B → A path (incl. `MAGNETAR-PUBLIC-DKG-1` for the public-DKG research path)
- `SUBMISSION-STATUS.md` — NIST MPTC submission status
- `THBS-SPEC.md` — true threshold HBS subpackage scope and v1/v2/v3 roadmap
- `ref/go/pkg/thbs/dkg2/README.md` — public-DKG skeleton + research path
- [`luxfi/pulsar/DEPLOYMENT-RUNBOOK.md`](https://github.com/luxfi/pulsar/blob/main/DEPLOYMENT-RUNBOOK.md) — sibling runbook for the threshold M-LWE primitive
## 9. Public-BFT primary primitive: `ValidatorSign` + `VerifyAggregateCert`
This is the **CANONICAL primary primitive** for using Magnetar in a
Lux public-BFT validator quorum. It does NOT require an aggregator
or dealer in the TCB. **This is the path you SHOULD pick** unless
§0's decision tree points elsewhere.
### Construction summary
1. **Per-validator keygen** (one-time, off-chain): each validator `i`
independently runs `magnetar.PerValidatorKeypair(params, rng)` to
produce its own (sk_i, pk_i). **No DKG.** **No shared seed.** **No
resharing ceremony.** The validator-set registry binds
`(ValidatorID_i, pk_i)` so peers can verify this validator's
signatures.
2. **Per-validator sign** (one signature per block): each validator
`i` calls `magnetar.ValidatorSign(sk_i, nil, message)` to produce
the raw FIPS 205 signature bytes (length = params.SignatureSize).
The signature is byte-identical to single-party FIPS 205
SignDeterministic and verifies under unmodified `slhdsa.Verify`.
Bind chain-id / block-height into `message` upstream of this call
(the consensus layer's transcript-hash pattern handles this; see
`luxfi/consensus` `QuasarCert`).
3. **Build aggregate cert** (consensus layer): collect N
per-validator signatures and assemble into a
`ValidatorAggregateCert{Mode, Signers, PubKeys, Sigs}` via
`magnetar.BuildAggregateCert(params, signers, pubKeys, sigs)`.
The cert's parallel-slice shape maps directly onto a Z-Chain
Groth16 rollup witness — N parallel signatures become N parallel
circuit inputs.
4. **Verify aggregate cert** (relying party): a verifier calls
`magnetar.VerifyAggregateCert(cert, message, knownValidators)`
which returns the COUNT of valid signers. The verifier compares
the count against its quorum threshold to make the policy
decision. Unknown / pubkey-mismatched signers are counted as
INVALID but are NOT fatal — the cert may carry a stale-registry
tail that the verifier should not abort on.
### Security model
| Property | `ValidatorSign` + `VerifyAggregateCert` | Notes |
|---|---|---|
| **Unforgeability** | FIPS 205-grade per signer | Each σ_i is a standard FIPS 205 signature; forgery reduces to the FIPS 205 EUF-CMA assumption per signer. |
| **No single-host TCB** | YES | No party ever holds N validators' secret material together. Compromising one validator host leaks ONE sk_i, not the group key. |
| **No dealer-in-TCB** | YES | Each validator generates its keypair INDEPENDENTLY; no party ever sees another validator's secret material. |
| **No aggregator-in-TCB** | YES | The aggregator is a pure-public-data shape-checker; the cert's Verify step does not require the aggregator to be trusted. |
| **Quorum byzantine tolerance** | Yes (consensus-layer policy) | The count returned by `VerifyAggregateCert` is compared against the consensus threshold (e.g. 2f+1 for f Byzantine faults). |
| **Per-validator slashing** | YES | Each σ_i is independently attributable to ValidatorID_i; the consensus layer can publish proof-of-double-sign on (ValidatorID_i, σ_a, σ_b, m_a, m_b). |
| **Post-quantum hardness** | FIPS 205 (~192-bit at SHAKE-192s) | All N signatures use the SAME FIPS 205 parameter set; no hybrid weakening. |
| **Replay resistance** | Caller-bound message | Bind chain-id / block-height into `message` upstream. |
### Why this is the right primary primitive for SLH-DSA in public BFT
SLH-DSA is HASH-BASED. It has no algebraic structure that admits
FROST-style threshold aggregation. The literature confirms this is
fundamental (Cozzo-Smart EUROCRYPT 2019; Bonte-Smart-Tan 2023; NIST
IR 8214). Any true-threshold SLH-DSA construction requires either:
- A **dealer-in-TCB** (the v1 `CombineWithSeedReconstruction` and
`thbs.DealerDKG` paths — REQUIRES TEE), or
- A **full MPC over the SHA-256/SHAKE hash tree** (research-grade,
multi-second to multi-hour per signature; tracked at
`BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1`).
For PUBLIC BFT consensus, the correct architecture is "N separate
signatures, count valid ones, apply policy threshold". The
`ValidatorSign` + `VerifyAggregateCert` path expresses exactly this.
The wire cost is N × |σ|; this is the irreducible cost of being
TCB-free on a hash-based signature scheme.
The Z-Chain Groth16 rollup compresses N × |σ| to ~192 bytes when the
relying party cannot consume the raw N-bundle envelope. The rollup
is a separate primitive (lux-zchain spec); Magnetar produces its
input but does NOT consume or verify the proof.
### Operational checklist
- [ ] Generate each validator's keypair on the validator host with a
hardware RNG. NEVER share sk_i across hosts.
- [ ] Encrypt sk_i at rest with a host-bound key (TPM-sealed or
KMS-wrapped). NEVER write sk_i to disk in plaintext.
- [ ] Publish (ValidatorID_i, pk_i) to the validator-set registry
via the same on-chain registration flow you'd use for
classical BLS validators.
- [ ] The consensus layer SHOULD parallelize verify by routing
`cert` bytes through `luxfi/crypto/slhdsa.VerifyBatch` for GPU
substrate acceleration (see `crypto/slhdsa/gpu.go`). Magnetar's
goroutine-parallel CPU path is the floor; the upstream GPU
substrate is the ceiling.
- [ ] On Byzantine fault detection (double-sign), produce slashing
evidence at the consensus layer.
### Honest non-warranties
- **No formal proof** of the aggregate construction beyond FIPS
205's per-signature security. The construction is a thin layer
over N independent FIPS 205 signatures — its security argument is
"N × FIPS 205 EUF-CMA", which is sufficient but not separately
formalized in EasyCrypt/Lean.
- **No dudect on `VerifyAggregateCert`.** The function dispatches
over per-validator data which is public (NodeID + PublicKey are
network-observable). The signature verify call inherits the
dudect coverage of single-party FIPS 205 verify.
- **No Groth16 verifier ships in this package.** The Z-Chain rollup
is a separate primitive — Magnetar produces the input to it but
does not consume or verify the proof.
## 10. Public-BFT aggregate mode (`AggregateSignatures`)
The aggregate mode is the **public-BFT-safe path** for using Magnetar in a validator quorum. It does NOT require the aggregator to be in the TCB and does NOT rely on the reveal-and-aggregate construction. This is the path you SHOULD pick unless §0's decision tree points elsewhere.
@@ -223,6 +363,6 @@ The aggregate mode is the **public-BFT-safe path** for using Magnetar in a valid
**Document metadata**
- File: `DEPLOYMENT-RUNBOOK.md`
- Version: v0.4.2 (adds §0 mode chooser + §10 aggregate mode; renames §1 to `CombineWithSeedReconstruction`)
- Version: v0.5.0 (rewrites §0 mode chooser to direct PUBLIC BFT users to `ValidatorSign` + `VerifyAggregateCert`; adds §9 documenting the new primary primitive; honestly demotes `CombineWithSeedReconstruction` and `thbs.DealerDKG` to the M-Chain custody / TEE-attested path; cites `thbs/dkg2/` skeleton + `BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1` as the public-DKG research path)
- Date: 2026-05-21
- Status: operator-facing trust-model disclosure
+613
View File
@@ -0,0 +1,613 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package magnetar
// standalone.go — Per-validator standalone SLH-DSA: the PRIMARY,
// CANONICAL primitive for public-BFT post-quantum consensus.
//
// HONEST ARCHITECTURE FRAMING
// ===========================
//
// SLH-DSA (FIPS 205) is HASH-BASED. It has no algebraic structure
// that admits FROST-style threshold aggregation. The literature
// confirms this is fundamental:
//
// - Cozzo & Smart (EUROCRYPT 2019), "Sharing the LUOV": every
// internal SHAKE/SHA-2 evaluation in an HBS scheme is a
// non-linear function of the secret seed. Secret-sharing the
// seed forces every hash to be evaluated under MPC.
// - Bonte, Smart, Tan (2023), "Threshold SPHINCS+": exhaustive
// infeasibility analysis. There is no efficient t-of-n SLH-DSA
// scheme producing a single FIPS 205-shaped signature without
// either (a) reconstructing the seed (dealer or aggregator in
// TCB) or (b) running full MPC over the SHA-256/SHAKE hash tree
// (multi-second per signature, multi-megabyte of comms).
// - NIST IR 8214 / MPTC submission notes: SLH-DSA is classified
// as "highest threshold-MPC cost" among FIPS 20{3,4,5}.
// - FIPS 205 §6 (slh_sign_internal): direct inspection confirms
// no Lagrange-aggregatable response.
//
// THE PUBLIC-BFT-SAFE PRIMARY PRIMITIVE for SLH-DSA is therefore
// "N separate signatures" — each validator i holds its OWN keypair
// (sk_i, pk_i), each produces a single-party FIPS 205 σ_i over the
// message, and the consensus layer collects N of them into a
// ValidatorAggregateCert:
//
// ValidatorAggregateCert = (Mode, Signers, PubKeys, Sigs)
//
// Verification:
//
// valid_count = Σ_i 1[FIPS 205 Verify(pk_i, msg, σ_i) ∧ pk_i = registry(i)]
// accept iff valid_count ≥ quorum_threshold
//
// Wire size: N × |σ|. For SHAKE-192s (16,224-byte σ) and a 21-validator
// quorum that's ~332 KiB. A Z-Chain Groth16 rollup compresses this to
// ~192 bytes; the rollup is a SEPARATE primitive (lux-zchain spec),
// not part of this API.
//
// WHY NOT THRESHOLD HBS FOR PUBLIC CONSENSUS
// ==========================================
//
// The thbs subpackage (ref/go/pkg/thbs/) ships TRUE threshold HBS
// per McGrew et al. ePrint 2019/793. Its v1 setup is DEALER-BACKED:
// a single dealer generates the per-element secrets and Shamir-shares
// them. Dealer-backed threshold HBS is RESEARCHED but NOT public-BFT-
// safe (the dealer learns every secret element at setup time).
//
// A truly public-DKG threshold HBS requires:
// (A) PVSS distribution of per-element entropy contributions:
// x_{slot,e} = Σ_j r_{j,slot,e}; no party holds x.
// (B) MPC over the SHA-256/SHAKE hash tree to compute the public
// Merkle root from secret-shared leaves WITHOUT REVEALING the
// leaves. This is the hard frontier (Damgård-Pastro-Smart-
// Zakarias SPDZ 2012, Wang-Ranellucci-Katz "Global-Scale Secure
// Multiparty Computation" 2017, Boyle-Gilboa-Ishai "Function
// Secret Sharing" 2015).
//
// (A) is shipped at thbs/dkg2/ as a skeleton. (B) is open research
// tracked at BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1. Until (B) lands,
// thbs is APPROPRIATE for M-Chain bridge custody (where a TEE'd
// dealer is in the TCB by policy) but NOT for public BFT.
//
// FOR PUBLIC BFT: USE THIS FILE.
//
// API CONTRACT
// ============
//
// - PerValidatorKeypair generates a STANDALONE FIPS 205 keypair.
// No DKG. No dealer. No shared seed. Each validator runs this
// ONCE, persists (sk_i, pk_i), and registers pk_i in the
// validator-set commitment.
//
// - ValidatorSign is the canonical signing primitive. Equivalent
// to single-party FIPS 205 SignDeterministic on (sk_i, message).
// The output is a raw |σ|-byte slice ready for the consensus
// layer's wire format. Pass rng != nil only if you want the
// FIPS 205 RANDOMIZED variant; the default (rng == nil) is the
// deterministic variant matching SignDeterministic byte-for-byte.
//
// - ValidatorBatchVerify verifies N (pub, msg, sig) triples in
// parallel via the same goroutine fork-join pattern used in
// luxfi/crypto/slhdsa.VerifyBatch. Returns a bitmap of
// valid/invalid per signer.
//
// - ValidatorAggregateCert is the wire envelope shipped in a
// QuasarCert or rolled up via Z-Chain Groth16. Carries N
// independently-verifiable signatures.
//
// - VerifyAggregateCert verifies the entire bundle against a
// known-validator allowlist. Signatures from unknown validators
// are counted as INVALID (defense against impersonation). The
// function returns the COUNT of valid signers; the quorum
// decision lives at the consumer.
//
// DECOMPLECTING DISCIPLINE
// ========================
//
// - The signing primitive (ValidatorSign) takes ONE secret key and
// produces ONE signature. It does NOT reach for any aggregator,
// ceremony state, or shared material. A single-validator signing
// a single message is a single function call with NO surrounding
// protocol context.
//
// - The verification primitive (ValidatorBatchVerify) takes N
// PUBLIC inputs and returns N independent booleans. No
// cross-signer coupling.
//
// - The cert primitive (ValidatorAggregateCert / VerifyAggregateCert)
// adds REGISTRY BINDING (knownValidators) on top of batch verify.
// The registry is the consensus layer's authoritative validator-
// set; the cert primitive consumes it but does not own it.
//
// - The quorum decision (count ≥ threshold) lives at the CONSUMER.
// This primitive REPORTS facts; policy gates filter them.
//
// COMPOSITION WITH THE EXISTING AGGREGATE API
// ===========================================
//
// The aggregate.go API (SignedBundle, AggregateSignatures,
// VerifyAggregated, etc.) ships the SAME computational graph under
// a "bundle / aggregate" shape: bundles carry self-describing
// PublicKey bytes in the envelope; AggregateSignatures dedupes and
// shape-checks; VerifyAggregated cross-checks against a registry.
// That API STAYS — it is the SDK-friendly form.
//
// The standalone.go API is the EXPLICIT-SEMANTIC form for the
// consensus layer: ValidatorSign returns raw σ bytes that flow
// straight into a wire envelope without going through SignedBundle;
// ValidatorAggregateCert carries the (PubKeys, Sigs, Signers)
// triple as parallel slices ready to be Groth16-compressed.
//
// Both APIs are byte-compatible: a ValidatorAggregateCert built
// from N ValidatorSign outputs verifies under VerifyAggregated
// after a trivial conversion (see toSignedBundles below). The
// duplication is DELIBERATE — bundles are the SDK shape;
// aggregate-cert is the consensus-wire shape.
import (
"errors"
"io"
"runtime"
)
// (crypto/rand is reached via Sign indirectly; this file does not
// directly source randomness.)
// Errors returned by the standalone (public-BFT) API.
var (
// ErrAggregateCertEmpty is returned by VerifyAggregateCert when
// the cert carries zero signers.
ErrAggregateCertEmpty = errors.New("magnetar: aggregate cert has no signers")
// ErrAggregateCertShape is returned when the parallel slices
// (Signers, PubKeys, Sigs) have mismatched lengths or any
// individual entry has the wrong byte length for the cert's Mode.
ErrAggregateCertShape = errors.New("magnetar: aggregate cert has mismatched slice shapes")
// ErrAggregateCertMode is returned when a cert carries a Mode
// the params do not match.
ErrAggregateCertMode = errors.New("magnetar: aggregate cert mode mismatch")
)
// PerValidatorKeypair generates a STANDALONE SLH-DSA keypair for
// one validator.
//
// Public-BFT contract:
//
// - No DKG, no shares, no dealer, no MPC.
// - Each validator runs this INDEPENDENTLY on its own host with
// its own RNG.
// - The validator persists (sk, pk) — losing sk means dropping
// out of the quorum until rotating to a fresh keypair.
// - The consensus layer's validator-set registry binds
// (ValidatorID, pk) so peers can verify this validator's
// signatures.
//
// This is the FIPS 205 keygen primitive: the returned PublicKey is
// byte-identical to what circl/slhdsa.Scheme().DeriveKey would emit
// on the same seed; the returned PrivateKey carries the seed and
// can be used with ValidatorSign or any other Magnetar sign path.
//
// rng may be nil (crypto/rand is used). Pass a deterministic
// reader for KAT-reproducible key generation.
//
// Equivalent semantics to GenerateValidatorKey in aggregate.go;
// this is the EXPLICIT-SEMANTIC name for the public-BFT path.
func PerValidatorKeypair(params *Params, rng io.Reader) (*PrivateKey, *PublicKey, error) {
return GenerateValidatorKey(params, rng)
}
// ValidatorSign is the canonical public-BFT signing primitive.
//
// Returns the raw FIPS 205 signature bytes (length =
// params.SignatureSize for sk's mode). The signature verifies under
// unmodified circl/slhdsa.Verify against the validator's public key
// (sk.Pub.Bytes).
//
// Semantics: this is ONE validator's contribution to a multi-
// validator certificate. The consensus layer collects N
// ValidatorSign outputs into a ValidatorAggregateCert via
// BuildAggregateCert.
//
// Determinism: when rng is nil, the output is FIPS 205
// SignDeterministic — byte-identical across calls for any (sk,
// message) pair. This is the recommended mode for the Lux
// validator path because it lets the consensus layer dedupe
// duplicate broadcasts at the wire layer.
//
// When rng is non-nil, the output is FIPS 205 SignRandomized with
// n bytes of fresh randomness drawn from rng. Use the randomized
// variant only if you specifically need the hedged-against-bad-RNG
// property; the deterministic variant is the standard recommendation
// for consensus.
//
// ctx is intentionally omitted: this primitive is the wire-format
// canonical sign. Bind chain-id / block-height into the message
// upstream of this call (the consensus layer's transcript-hash
// pattern handles this; see luxfi/consensus QuasarCert).
//
// If you need an explicit ctx, use Sign in sign.go directly.
func ValidatorSign(sk *PrivateKey, rng io.Reader, message []byte) ([]byte, error) {
if sk == nil {
return nil, ErrNilKey
}
params, err := ParamsFor(sk.Mode)
if err != nil {
return nil, err
}
sig, err := Sign(params, sk, message, nil, rng != nil, rng)
if err != nil {
return nil, err
}
return sig.Bytes, nil
}
// ValidatorBatchVerify verifies N (pub, msg, sig) triples and
// returns a bitmap of valid/invalid per signer.
//
// Parallelism: this routes through the same goroutine fork-join
// pattern as VerifyAggregated (above the
// verifyAggregatedConcurrentThreshold the work is forked across
// GOMAXPROCS workers). The pattern mirrors
// luxfi/crypto/slhdsa.VerifyBatch — when magnetar is embedded in a
// system that exports a GPU substrate (e.g. consensus quorum
// verification routing through crypto/slhdsa.VerifyBatch with the
// luxcpp/lux-accel SLH-DSA GPU plugin loaded), callers SHOULD
// route their batch through that upstream primitive for the GPU
// upper bound. This package ships the goroutine-parallel CPU
// floor.
//
// Returns:
// - bitmap[i] = true iff slhdsa.Verify(pubs[i].Bytes, msgs[i], sigs[i])
// accepts. Per-signer verify failures are reported as bitmap[i] =
// false, NOT as an error.
// - err = nil on success, even when some signatures fail; err is
// non-nil only for STRUCTURAL violations (length mismatches,
// mode mismatches, nil inputs).
//
// Provenance: the dispatch tier (serial vs goroutine-parallel) is
// observable via LastVerifyAggregatedTier — the same hook the
// aggregate.go path uses, since ValidatorBatchVerify and
// VerifyAggregated share the same parallel-CPU dispatch.
func ValidatorBatchVerify(params *Params, pubs []*PublicKey, msgs [][]byte, sigs [][]byte) ([]bool, error) {
if err := params.Validate(); err != nil {
return nil, err
}
n := len(pubs)
if n != len(msgs) || n != len(sigs) {
return nil, ErrAggregateCertShape
}
out := make([]bool, n)
if n == 0 {
return out, nil
}
// Shape + mode checks first; any structural failure is fatal so
// the caller does not consume a half-validated bitmap.
for i := 0; i < n; i++ {
if pubs[i] == nil {
return nil, ErrNilPublicKey
}
if pubs[i].Mode != params.Mode {
return nil, ErrModeMismatch
}
if len(pubs[i].Bytes) != params.PublicKeySize {
return nil, ErrPublicKeyWrongSize
}
if len(sigs[i]) != params.SignatureSize {
return nil, ErrSignatureWrongSize
}
}
// Build the per-bundle AggregatedSignature view so we can reuse
// the existing parallel-CPU dispatch in VerifyAggregated. We
// intentionally do NOT pass a knownValidators map here — the
// bitmap path is for the lower-level case where the registry
// binding is handled by the caller (VerifyAggregateCert is the
// registry-binding wrapper).
//
// We dispatch directly so we get the same provenance hook
// (LastVerifyAggregatedTier) without paying the dedupe pass —
// in this primitive the caller has already chosen the input
// order.
dispatchTier := verifyAggregatedSerial
if n >= verifyAggregatedConcurrentThreshold {
dispatchTier = verifyAggregatedConcurrent
validatorBatchVerifyConcurrent(params, pubs, msgs, sigs, out)
} else {
for i := 0; i < n; i++ {
out[i] = slhVerify(params.ID, pubs[i].Bytes, msgs[i], nil, sigs[i])
}
}
recordVerifyAggregatedTier(dispatchTier)
return out, nil
}
// validatorBatchVerifyConcurrent runs per-signer Verify across
// GOMAXPROCS goroutines for the message-batch case (each signer
// gets its own message). Pure function per signer, no shared
// state — same byte-equality contract as the serial path and as
// VerifyAggregated's parallel dispatch.
func validatorBatchVerifyConcurrent(params *Params, pubs []*PublicKey, msgs [][]byte, sigs [][]byte, out []bool) {
n := len(pubs)
workers := runtimeGOMAXPROCS()
if workers > n {
workers = n
}
if workers < 2 {
for i := 0; i < n; i++ {
out[i] = slhVerify(params.ID, pubs[i].Bytes, msgs[i], nil, sigs[i])
}
return
}
type job struct {
lo, hi int
}
jobs := make([]job, 0, workers)
chunk := (n + workers - 1) / workers
for w := 0; w < workers; w++ {
start := w * chunk
if start >= n {
break
}
end := start + chunk
if end > n {
end = n
}
jobs = append(jobs, job{lo: start, hi: end})
}
done := make(chan struct{}, len(jobs))
for _, j := range jobs {
go func(lo, hi int) {
for i := lo; i < hi; i++ {
out[i] = slhVerify(params.ID, pubs[i].Bytes, msgs[i], nil, sigs[i])
}
done <- struct{}{}
}(j.lo, j.hi)
}
for i := 0; i < len(jobs); i++ {
<-done
}
}
// runtimeGOMAXPROCS wraps runtime.GOMAXPROCS(0) so callers don't
// re-import runtime here.
func runtimeGOMAXPROCS() int {
return runtime.GOMAXPROCS(0)
}
// ValidatorAggregateCert is the wire envelope for N per-validator
// SLH-DSA signatures over a SINGLE message.
//
// Wire layout (canonical, parallel-slices form):
//
// Mode || N || Signers[0] || ... || Signers[N-1]
// || PubKeys[0] || ... || PubKeys[N-1]
// || Sigs[0] || ... || Sigs[N-1]
//
// where:
// - Mode is one byte.
// - N is a 4-byte big-endian count.
// - Each Signers[i] is 32 bytes (NodeID).
// - Each PubKeys[i] is params.PublicKeySize bytes.
// - Each Sigs[i] is params.SignatureSize bytes.
//
// (Magnetar's reference impl does NOT pin a specific transport
// encoding — that's the responsibility of the consensus layer.
// RLP / protobuf / canonical binary are all reasonable choices.
// The parallel-slices form is INTENTIONALLY chosen over a nested
// SignedBundle list because it maps directly to Z-Chain Groth16
// rollup input where the prover's wire shape is N parallel
// witnesses, not N records.)
//
// All N signatures are over the SAME message. If the consensus
// layer needs distinct per-signer messages (e.g. per-signer
// transcript binding), use ValidatorBatchVerify directly.
//
// This shape is the SIBLING of AggregatedSignature (aggregate.go):
// - AggregatedSignature: nested SignedBundle list, SDK-friendly,
// self-describing per-bundle.
// - ValidatorAggregateCert: parallel-slices, consensus-wire
// friendly, Z-Chain-rollup-friendly.
//
// Both are byte-equivalent under conversion (see toSignedBundles).
type ValidatorAggregateCert struct {
// Mode is the FIPS 205 parameter set every signature targets.
Mode Mode
// Signers carries each signer's canonical 32-byte validator
// identifier (NodeID).
Signers []NodeID
// PubKeys carries each signer's FIPS 205 public-key bytes.
// Length per entry = params.PublicKeySize. Length of slice =
// len(Signers).
PubKeys [][]byte
// Sigs carries each signer's FIPS 205 signature bytes. Length
// per entry = params.SignatureSize. Length of slice =
// len(Signers).
Sigs [][]byte
}
// BuildAggregateCert constructs a ValidatorAggregateCert from N
// parallel slices.
//
// The function performs shape + mode validation and a defensive
// copy of each PubKeys[i] / Sigs[i] (the caller's slices may be
// aliased; the cert is wire-stable and must not move under the
// caller).
//
// Returns ErrAggregateCertShape on any shape mismatch.
func BuildAggregateCert(params *Params, signers []NodeID, pubKeys [][]byte, sigs [][]byte) (*ValidatorAggregateCert, error) {
if err := params.Validate(); err != nil {
return nil, err
}
n := len(signers)
if n != len(pubKeys) || n != len(sigs) {
return nil, ErrAggregateCertShape
}
if n == 0 {
return nil, ErrAggregateCertEmpty
}
outPubs := make([][]byte, n)
outSigs := make([][]byte, n)
for i := 0; i < n; i++ {
if len(pubKeys[i]) != params.PublicKeySize {
return nil, ErrAggregateCertShape
}
if len(sigs[i]) != params.SignatureSize {
return nil, ErrAggregateCertShape
}
pk := make([]byte, params.PublicKeySize)
copy(pk, pubKeys[i])
sg := make([]byte, params.SignatureSize)
copy(sg, sigs[i])
outPubs[i] = pk
outSigs[i] = sg
}
signersCopy := make([]NodeID, n)
copy(signersCopy, signers)
return &ValidatorAggregateCert{
Mode: params.Mode,
Signers: signersCopy,
PubKeys: outPubs,
Sigs: outSigs,
}, nil
}
// VerifyAggregateCert verifies every signature in cert against the
// registered public key for that signer and returns the COUNT of
// valid signers.
//
// Verification rules (each enforced per signer):
//
// 1. cert.Signers[i] must appear in knownValidators. If not, that
// signer is COUNTED AS INVALID. This is a defense-in-depth choice
// against impersonation: an attacker who slips a SignedBundle
// from an unknown validator into the cert does NOT crash the
// verifier, but does NOT gain a count either. (Compare to
// VerifyAggregated which returns ErrUnknownValidator as a fatal
// error; the standalone form is more permissive on registry
// misses because the cert may be Groth16-rolled-up over a
// stale registry and the verifier should not abort the entire
// batch on one stale entry.)
//
// 2. cert.PubKeys[i] must byte-equal knownValidators[cert.Signers[i]].
// A pubkey mismatch is also counted as INVALID (NOT fatal) —
// same rationale as (1).
//
// 3. The signature must verify under the REGISTERED public key (not
// the cert's embedded copy, to be paranoid).
//
// 4. Duplicate signers are deduped: the FIRST occurrence wins and
// subsequent occurrences are silently dropped (NOT counted twice,
// NOT counted as invalid — the consensus layer can collect proof
// of double-signing separately via per-signer Sigs comparison).
//
// The count is the load-bearing output: a quorum policy decision
// ("is N ≥ threshold?") lives at the consumer, NOT in this
// primitive. This is the same decomplecting discipline the rest of
// the Lux post-quantum stack uses.
//
// Returns:
// - validCount: number of signers that verified, in [0, len(cert.Signers)].
// - err: nil on success (including 0 valid signers), or one of
// ErrAggregateCertEmpty / ErrAggregateCertShape /
// ErrAggregateCertMode when the cert is structurally malformed.
func VerifyAggregateCert(cert *ValidatorAggregateCert, message []byte, knownValidators map[NodeID][]byte) (int, error) {
if cert == nil {
return 0, ErrAggregateCertEmpty
}
params, err := ParamsFor(cert.Mode)
if err != nil {
return 0, ErrAggregateCertMode
}
n := len(cert.Signers)
if n == 0 {
return 0, ErrAggregateCertEmpty
}
if n != len(cert.PubKeys) || n != len(cert.Sigs) {
return 0, ErrAggregateCertShape
}
for i := 0; i < n; i++ {
if len(cert.PubKeys[i]) != params.PublicKeySize {
return 0, ErrAggregateCertShape
}
if len(cert.Sigs[i]) != params.SignatureSize {
return 0, ErrAggregateCertShape
}
}
// Phase 1: dedupe Signers, build the verify queue, and
// classify each entry as (verifiable, against-which-pk) or
// (skip — unknown signer / pubkey mismatch).
type entry struct {
idx int
registered []byte
verifiable bool
}
entries := make([]entry, 0, n)
seen := make(map[NodeID]struct{}, n)
for i := 0; i < n; i++ {
if _, dup := seen[cert.Signers[i]]; dup {
continue
}
seen[cert.Signers[i]] = struct{}{}
known, ok := knownValidators[cert.Signers[i]]
if !ok {
entries = append(entries, entry{idx: i, verifiable: false})
continue
}
if len(known) != params.PublicKeySize {
entries = append(entries, entry{idx: i, verifiable: false})
continue
}
if !ctEqualBytes(cert.PubKeys[i], known) {
entries = append(entries, entry{idx: i, verifiable: false})
continue
}
entries = append(entries, entry{idx: i, registered: known, verifiable: true})
}
// Phase 2: per-signer Verify, parallel above the concurrent
// threshold. We build flat slices for the parallel dispatch.
verifiableIdx := make([]int, 0, len(entries))
for _, e := range entries {
if e.verifiable {
verifiableIdx = append(verifiableIdx, e.idx)
}
}
results := make(map[int]bool, len(verifiableIdx))
if len(verifiableIdx) > 0 {
pubs := make([]*PublicKey, len(verifiableIdx))
msgs := make([][]byte, len(verifiableIdx))
sigs := make([][]byte, len(verifiableIdx))
for k, idx := range verifiableIdx {
pubs[k] = &PublicKey{Mode: params.Mode, Bytes: cert.PubKeys[idx]}
msgs[k] = message
sigs[k] = cert.Sigs[idx]
}
bitmap, vErr := ValidatorBatchVerify(params, pubs, msgs, sigs)
if vErr != nil {
return 0, vErr
}
for k, idx := range verifiableIdx {
results[idx] = bitmap[k]
}
}
// Phase 3: tally. Verifiable + valid = count++; verifiable +
// invalid = count untouched; non-verifiable = count untouched.
count := 0
for _, e := range entries {
if !e.verifiable {
continue
}
if results[e.idx] {
count++
}
}
return count, nil
}
+429
View File
@@ -0,0 +1,429 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package magnetar
// standalone_test.go — Tests for the public-BFT-safe per-validator
// standalone primitive (standalone.go).
//
// These tests pin the canonical public-BFT path: each validator
// holds its OWN SLH-DSA keypair, signs independently, and the
// consensus layer collects N signatures into a
// ValidatorAggregateCert. No DKG. No dealer. No shared seed.
import (
"bytes"
"testing"
)
// TestPerValidatorKeypair_RoundTrip pins the round-trip:
// - PerValidatorKeypair produces a fresh standalone keypair.
// - ValidatorSign over the message produces a valid signature.
// - That signature verifies under the validator's pk via stock
// FIPS 205 Verify (the byte-equality claim).
// - Two independent calls with distinct seeds produce DISTINCT
// keypairs (no shared seed, no DKG).
func TestPerValidatorKeypair_RoundTrip(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
sk, pk, err := PerValidatorKeypair(params, newDetReader([]byte("standalone-rt-1")))
if err != nil {
t.Fatalf("PerValidatorKeypair: %v", err)
}
if sk == nil || pk == nil {
t.Fatalf("PerValidatorKeypair returned nil (sk=%v pk=%v)", sk, pk)
}
if sk.Mode != params.Mode || pk.Mode != params.Mode {
t.Fatalf("mode mismatch: sk=%v pk=%v want=%v", sk.Mode, pk.Mode, params.Mode)
}
if !sk.Pub.Equal(pk) {
t.Fatalf("PerValidatorKeypair: sk.Pub != returned pk")
}
msg := []byte("Magnetar standalone-mode per-validator round-trip")
sig, err := ValidatorSign(sk, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign: %v", err)
}
if len(sig) != params.SignatureSize {
t.Fatalf("sig len = %d, want %d", len(sig), params.SignatureSize)
}
// Round-trip under stock FIPS 205 dispatch.
if !slhVerify(params.ID, pk.Bytes, msg, nil, sig) {
t.Fatalf("slhVerify rejected ValidatorSign output")
}
// Determinism: two ValidatorSign calls under the same (sk, msg)
// must be byte-identical when rng is nil (FIPS 205
// SignDeterministic).
sig2, err := ValidatorSign(sk, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign (2nd): %v", err)
}
if !bytes.Equal(sig, sig2) {
t.Fatalf("ValidatorSign nil-rng output is non-deterministic")
}
// Independence: a second PerValidatorKeypair call with a
// DIFFERENT seed produces a distinct keypair. "No DKG, no shared
// seed" property.
sk2, pk2, err := PerValidatorKeypair(params, newDetReader([]byte("standalone-rt-2")))
if err != nil {
t.Fatalf("PerValidatorKeypair (2nd): %v", err)
}
if bytes.Equal(sk.Bytes, sk2.Bytes) {
t.Fatalf("two PerValidatorKeypair calls produced identical sk")
}
if pk.Equal(pk2) {
t.Fatalf("two PerValidatorKeypair calls produced identical pk")
}
}
// TestValidatorBatchVerify_5of5_AllValid pins the parallel-CPU
// batch-verify path. Five validators sign the same message under
// their own keypairs; ValidatorBatchVerify returns true for every
// signer.
func TestValidatorBatchVerify_5of5_AllValid(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
const N = 5
pubs, msgs, sigs := buildStandaloneFixture(t, params, N, []byte("standalone-batch-5"))
bitmap, err := ValidatorBatchVerify(params, pubs, msgs, sigs)
if err != nil {
t.Fatalf("ValidatorBatchVerify: %v", err)
}
if len(bitmap) != N {
t.Fatalf("bitmap len = %d, want %d", len(bitmap), N)
}
for i, ok := range bitmap {
if !ok {
t.Fatalf("ValidatorBatchVerify[%d] = false, want true", i)
}
}
}
// TestValidatorBatchVerify_BadSig_Counted pins the
// "counted-out, not fatal" property for tampered signatures.
func TestValidatorBatchVerify_BadSig_Counted(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
const N = 5
pubs, msgs, sigs := buildStandaloneFixture(t, params, N, []byte("standalone-batch-bad"))
// Tamper bitmap[2].
sigs[2] = append([]byte(nil), sigs[2]...)
sigs[2][len(sigs[2])/2] ^= 0x01
bitmap, err := ValidatorBatchVerify(params, pubs, msgs, sigs)
if err != nil {
t.Fatalf("ValidatorBatchVerify: %v", err)
}
if len(bitmap) != N {
t.Fatalf("bitmap len = %d, want %d", len(bitmap), N)
}
for i, ok := range bitmap {
if i == 2 {
if ok {
t.Fatalf("ValidatorBatchVerify[%d] = true, want false (tampered)", i)
}
continue
}
if !ok {
t.Fatalf("ValidatorBatchVerify[%d] = false, want true", i)
}
}
}
// TestVerifyAggregateCert_UnknownValidator_Rejected pins the
// known-validator allowlist defense. A signer whose ValidatorID is
// not in knownValidators is COUNTED AS INVALID (not fatal, by
// standalone.go contract).
func TestVerifyAggregateCert_UnknownValidator_Rejected(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
const N = 3
known := make(map[NodeID][]byte, N)
signers := make([]NodeID, N)
pubKeys := make([][]byte, N)
sigs := make([][]byte, N)
msg := []byte("standalone-cert-unknown-validator")
for i := 0; i < N; i++ {
sk, pk, err := PerValidatorKeypair(params, newDetReader([]byte{byte(i), 'U', 'N'}))
if err != nil {
t.Fatalf("PerValidatorKeypair[%d]: %v", i, err)
}
var id NodeID
id[0] = byte(i + 1)
copy(id[1:], []byte("CERT-UNKNOWN"))
signers[i] = id
pubKeys[i] = pk.Bytes
s, err := ValidatorSign(sk, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign[%d]: %v", i, err)
}
sigs[i] = s
// Only register the first two validators in knownValidators.
// The third signer is "unknown" to the registry.
if i < 2 {
known[id] = pk.Bytes
}
}
cert, err := BuildAggregateCert(params, signers, pubKeys, sigs)
if err != nil {
t.Fatalf("BuildAggregateCert: %v", err)
}
count, err := VerifyAggregateCert(cert, msg, known)
if err != nil {
t.Fatalf("VerifyAggregateCert: %v", err)
}
if count != 2 {
t.Fatalf("VerifyAggregateCert count = %d, want 2 (one unknown signer)", count)
}
}
// TestVerifyAggregateCert_DuplicateValidator_DedupedAndCountedOnce
// pins dedupe-by-ValidatorID semantics. The same signer appearing
// twice in the cert is counted ONCE.
func TestVerifyAggregateCert_DuplicateValidator_DedupedAndCountedOnce(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
known := make(map[NodeID][]byte, 2)
signers := make([]NodeID, 0, 3)
pubKeys := make([][]byte, 0, 3)
sigs := make([][]byte, 0, 3)
msg := []byte("standalone-cert-duplicate")
// Validator A.
skA, pkA, err := PerValidatorKeypair(params, newDetReader([]byte("dup-A")))
if err != nil {
t.Fatalf("PerValidatorKeypair[A]: %v", err)
}
var idA NodeID
idA[0] = 1
copy(idA[1:], []byte("DUP-A"))
known[idA] = pkA.Bytes
// Validator B.
skB, pkB, err := PerValidatorKeypair(params, newDetReader([]byte("dup-B")))
if err != nil {
t.Fatalf("PerValidatorKeypair[B]: %v", err)
}
var idB NodeID
idB[0] = 2
copy(idB[1:], []byte("DUP-B"))
known[idB] = pkB.Bytes
sigA, err := ValidatorSign(skA, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign[A]: %v", err)
}
sigB, err := ValidatorSign(skB, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign[B]: %v", err)
}
// Build a cert with A appearing TWICE plus B once.
signers = append(signers, idA, idA, idB)
pubKeys = append(pubKeys, pkA.Bytes, pkA.Bytes, pkB.Bytes)
sigs = append(sigs, sigA, sigA, sigB)
cert, err := BuildAggregateCert(params, signers, pubKeys, sigs)
if err != nil {
t.Fatalf("BuildAggregateCert: %v", err)
}
count, err := VerifyAggregateCert(cert, msg, known)
if err != nil {
t.Fatalf("VerifyAggregateCert: %v", err)
}
if count != 2 {
t.Fatalf("VerifyAggregateCert count = %d, want 2 (A deduped, B counted once)", count)
}
}
// TestVerifyAggregateCert_GPU_Provenance pins that the cert
// verification path routes through the parallel-CPU dispatch
// (the goroutine fork-join that mirrors
// luxfi/crypto/slhdsa.VerifyBatch). Confirms a batch above the
// concurrent threshold takes the parallel tier.
func TestVerifyAggregateCert_GPU_Provenance(t *testing.T) {
skipUnderRace(t)
if testing.Short() {
t.Skip("skip: -short does not run the cert-provenance check (5 SLH-DSA verifies)")
}
params := MustParamsFor(ModeM192s)
const N = 5
known := make(map[NodeID][]byte, N)
signers := make([]NodeID, N)
pubKeys := make([][]byte, N)
sigs := make([][]byte, N)
msg := []byte("standalone-cert-provenance")
for i := 0; i < N; i++ {
sk, pk, err := PerValidatorKeypair(params, newDetReader([]byte{byte(i), 'P', 'V'}))
if err != nil {
t.Fatalf("PerValidatorKeypair[%d]: %v", i, err)
}
var id NodeID
id[0] = byte(i + 1)
copy(id[1:], []byte("PROV"))
signers[i] = id
pubKeys[i] = pk.Bytes
known[id] = pk.Bytes
s, err := ValidatorSign(sk, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign[%d]: %v", i, err)
}
sigs[i] = s
}
cert, err := BuildAggregateCert(params, signers, pubKeys, sigs)
if err != nil {
t.Fatalf("BuildAggregateCert: %v", err)
}
count, err := VerifyAggregateCert(cert, msg, known)
if err != nil {
t.Fatalf("VerifyAggregateCert: %v", err)
}
if count != N {
t.Fatalf("VerifyAggregateCert count = %d, want %d", count, N)
}
// Provenance: above the concurrent threshold the parallel tier
// MUST engage. This is the standalone analog of the
// aggregate.go BatchVerify provenance test — observable
// evidence that the per-bundle verify uses the goroutine
// fork-join pattern (the slhdsa.VerifyBatch parallel path).
tier := LastVerifyAggregatedTier()
if tier != verifyAggregatedConcurrent {
t.Fatalf("LastVerifyAggregatedTier = %s, want %s — cert verify did not dispatch parallel",
tier, verifyAggregatedConcurrent)
}
}
// TestValidatorBatchVerify_ShapeChecks pins the fail-fast structural
// errors. Bad inputs do NOT silently produce a half-validated
// bitmap.
func TestValidatorBatchVerify_ShapeChecks(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
const N = 3
pubs, msgs, sigs := buildStandaloneFixture(t, params, N, []byte("standalone-shape"))
// Mismatched slice lengths.
if _, err := ValidatorBatchVerify(params, pubs, msgs[:N-1], sigs); err != ErrAggregateCertShape {
t.Fatalf("ValidatorBatchVerify msgs-short err = %v, want ErrAggregateCertShape", err)
}
// Wrong signature byte length.
badSigs := make([][]byte, N)
for i := range badSigs {
badSigs[i] = sigs[i]
}
badSigs[1] = make([]byte, params.SignatureSize-1)
if _, err := ValidatorBatchVerify(params, pubs, msgs, badSigs); err != ErrSignatureWrongSize {
t.Fatalf("ValidatorBatchVerify wrong-sig-size err = %v, want ErrSignatureWrongSize", err)
}
// Wrong pubkey byte length.
badPub := *pubs[1]
badPub.Bytes = make([]byte, params.PublicKeySize-1)
badPubs := append([]*PublicKey(nil), pubs...)
badPubs[1] = &badPub
if _, err := ValidatorBatchVerify(params, badPubs, msgs, sigs); err != ErrPublicKeyWrongSize {
t.Fatalf("ValidatorBatchVerify wrong-pk-size err = %v, want ErrPublicKeyWrongSize", err)
}
// Mode mismatch.
wrongMode := *pubs[1]
wrongMode.Mode = ModeM256s
wrongModePubs := append([]*PublicKey(nil), pubs...)
wrongModePubs[1] = &wrongMode
if _, err := ValidatorBatchVerify(params, wrongModePubs, msgs, sigs); err != ErrModeMismatch {
t.Fatalf("ValidatorBatchVerify wrong-mode err = %v, want ErrModeMismatch", err)
}
}
// TestVerifyAggregateCert_AllInvalid_ReturnsZero pins the "fact-
// reporting" property: when no signer is valid, the count is 0
// and the error is nil (the caller's quorum threshold check will
// reject; that is not this primitive's job).
func TestVerifyAggregateCert_AllInvalid_ReturnsZero(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
const N = 3
known := make(map[NodeID][]byte, N)
signers := make([]NodeID, N)
pubKeys := make([][]byte, N)
sigs := make([][]byte, N)
msg := []byte("standalone-cert-all-invalid")
for i := 0; i < N; i++ {
sk, pk, err := PerValidatorKeypair(params, newDetReader([]byte{byte(i), 'I', 'V'}))
if err != nil {
t.Fatalf("PerValidatorKeypair[%d]: %v", i, err)
}
var id NodeID
id[0] = byte(i + 1)
copy(id[1:], []byte("ALL-INVALID"))
signers[i] = id
pubKeys[i] = pk.Bytes
known[id] = pk.Bytes
s, err := ValidatorSign(sk, nil, msg)
if err != nil {
t.Fatalf("ValidatorSign[%d]: %v", i, err)
}
// Corrupt every signature.
s[len(s)/2] ^= 0x01
sigs[i] = s
}
cert, err := BuildAggregateCert(params, signers, pubKeys, sigs)
if err != nil {
t.Fatalf("BuildAggregateCert: %v", err)
}
count, err := VerifyAggregateCert(cert, msg, known)
if err != nil {
t.Fatalf("VerifyAggregateCert: %v", err)
}
if count != 0 {
t.Fatalf("VerifyAggregateCert count = %d, want 0 (all invalid)", count)
}
}
// TestBuildAggregateCert_EmptyAndShape pins the BuildAggregateCert
// shape checks.
func TestBuildAggregateCert_EmptyAndShape(t *testing.T) {
skipUnderRace(t)
params := MustParamsFor(ModeM192s)
if _, err := BuildAggregateCert(params, nil, nil, nil); err != ErrAggregateCertEmpty {
t.Fatalf("BuildAggregateCert empty err = %v, want ErrAggregateCertEmpty", err)
}
// Mismatched lengths.
signers := []NodeID{{1}, {2}}
pubs := [][]byte{make([]byte, params.PublicKeySize)}
sigs := [][]byte{make([]byte, params.SignatureSize), make([]byte, params.SignatureSize)}
if _, err := BuildAggregateCert(params, signers, pubs, sigs); err != ErrAggregateCertShape {
t.Fatalf("BuildAggregateCert mismatch err = %v, want ErrAggregateCertShape", err)
}
}
// buildStandaloneFixture is a test helper that produces N valid
// (pk, msg, sig) triples under deterministic per-validator
// keypairs. Returns parallel slices ready to feed into
// ValidatorBatchVerify or BuildAggregateCert.
func buildStandaloneFixture(t *testing.T, params *Params, n int, seed []byte) ([]*PublicKey, [][]byte, [][]byte) {
t.Helper()
pubs := make([]*PublicKey, n)
msgs := make([][]byte, n)
sigs := make([][]byte, n)
for i := 0; i < n; i++ {
mix := append([]byte{byte(i)}, seed...)
sk, pk, err := PerValidatorKeypair(params, newDetReader(mix))
if err != nil {
t.Fatalf("PerValidatorKeypair[%d]: %v", i, err)
}
pubs[i] = pk
msgs[i] = append([]byte{}, seed...)
s, err := ValidatorSign(sk, nil, msgs[i])
if err != nil {
t.Fatalf("ValidatorSign[%d]: %v", i, err)
}
sigs[i] = s
}
return pubs, msgs, sigs
}
+24 -10
View File
@@ -54,7 +54,10 @@ type DKGOptions struct {
DealerSeed []byte
}
// DKG runs the v1 dealer-backed DKG.
// DealerDKG runs the v1 DEALER-BACKED DKG. The dealer learns every
// secret element at setup time and Shamir-shares each across the
// committee. NOT public-BFT-safe — see the package doc comment in
// thbs.go for the framing.
//
// Returns the threshold public key, the calling party's PrivateShare,
// and any error.
@@ -63,13 +66,17 @@ type DKGOptions struct {
// generates EVERY party's PrivateShare. The function is wired to
// produce ALL shares; in a real deployment the dealer would split
// the returned share-set across parties and zeroise the rest before
// the dealer-host shuts down. See DKGAll for that path.
// the dealer-host shuts down. See DealerDKGAll for that path.
//
// This helper signature returns the PrivateShare for the FIRST
// participant only, to match the user-spec signature. To obtain every
// party's share, call DKGAll.
func DKG(config DKGConfig) (PublicKey, PrivateShare, error) {
pk, shares, err := DKGAll(config, DKGOptions{})
// party's share, call DealerDKGAll.
//
// The renamed-from-DKG marker is deliberate: the v1 path IS dealer-
// backed and that property is load-bearing for the deployment
// decision. v0.6+ candidate public DKG lives in subpackage dkg2/.
func DealerDKG(config DKGConfig) (PublicKey, PrivateShare, error) {
pk, shares, err := DealerDKGAll(config, DKGOptions{})
if err != nil {
return PublicKey{}, PrivateShare{}, err
}
@@ -79,11 +86,18 @@ func DKG(config DKGConfig) (PublicKey, PrivateShare, error) {
return pk, shares[0], nil
}
// DKGAll is the same as DKG but returns one PrivateShare per
// participant (indexed by position in config.Participants). Use this in
// tests and in a real dealer host where the dealer process needs all
// shares to distribute them.
func DKGAll(config DKGConfig, opts DKGOptions) (PublicKey, []PrivateShare, error) {
// DealerDKGAll is the same as DealerDKG but returns one PrivateShare
// per participant (indexed by position in config.Participants). Use
// this in tests and in a real dealer host where the dealer process
// needs all shares to distribute them.
//
// HONEST FRAMING: the dealer process holds the master seed and every
// per-element secret in memory for the duration of this call. The
// deferred zeroize call below wipes the master seed before return;
// per-element secrets are wiped inline as each slot is processed.
// The dealer MUST run in a TEE (or be the same process as the
// custody host) if public-BFT safety is required.
func DealerDKGAll(config DKGConfig, opts DKGOptions) (PublicKey, []PrivateShare, error) {
if err := validateDKGConfig(config); err != nil {
return PublicKey{}, nil, err
}
+148
View File
@@ -0,0 +1,148 @@
# thbs/dkg2 — Public DKG for Threshold HBS (research skeleton)
> **DO NOT USE IN PRODUCTION.** This subpackage is a research-grade
> skeleton. The PVSS layer ships; the MPC-root layer is **open research**
> and tracked in `BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1`. Ship a working
> public DKG in v0.6+ once an MPC-friendly hash is selected OR an MPC
> framework (MP-SPDZ, EMP, etc.) is integrated.
## What this is
`dkg2` is the **public-DKG** counterpart to the dealer-backed v1 DKG
in the parent `thbs` package. The two paths are:
| Path | Setup | Public-BFT safe | Status |
|---|---|---|---|
| `thbs.DealerDKG` | Trusted dealer | No (dealer learns secrets) | Ships, v1 stable |
| `dkg2` (this dir) | PVSS + MPC | Yes (no party learns secrets) | Skeleton only |
For the **public-BFT-safe primary primitive** today, use
**per-validator standalone SLH-DSA**: `magnetar.PerValidatorKeypair` +
`magnetar.ValidatorSign` + `magnetar.VerifyAggregateCert`
(`ref/go/pkg/magnetar/standalone.go`). That path needs no DKG and is
the correct primitive for public consensus.
`dkg2` is the future path for **threshold** HBS (one signature, not N
collected) **without** a trusted dealer.
## Why this is hard
Hash-Based Signatures (LMS, XMSS, SLH-DSA) commit public keys to
**Merkle roots over many WOTS+ chain endpoints**. Each endpoint
`W_{slot, j} = H^{w-1}(x_{slot, j})` is derived from a SECRET chain head
`x_{slot, j}` by repeated hashing.
A truly **public** DKG (no trusted dealer, no TEE) needs TWO ingredients:
### (A) PVSS distribution of per-element entropy
For every secret element `x_e`, the protocol collects randomness
contributions `r_{j,e}` from each party `j ∈ [n]` and Shamir-shares each
`r_{j,e}` across the committee at threshold `t`. The secret element is
`x_e = Σ_j r_{j,e}`; **no single party** ever holds `x_e` in cleartext.
**Status: this is what the skeleton implements** in `pvss.go`,
`complaint.go`, `consensus.go`.
### (B) MPC over the SHA-256 / SHAKE hash function
To produce a PUBLIC chain endpoint `W_e = H^{w-1}(x_e)` from a SECRET-
SHARED `x_e`, every party participates in an MPC that evaluates the hash
function on the shared input WITHOUT revealing it. Hash functions are
**non-linear**, so this is one of:
- **SPDZ-style arithmetic MPC**
(Damgård-Pastro-Smart-Zakarias, "Multiparty Computation from Somewhat
Homomorphic Encryption", CRYPTO 2012)
- **Garbled circuits + OT**
(Wang-Ranellucci-Katz, "Global-Scale Secure Multiparty Computation",
CCS 2017)
- **Function Secret Sharing**
(Boyle-Gilboa-Ishai, "Function Secret Sharing", EUROCRYPT 2015)
For SLH-DSA-SHAKE-192s with the v1 thbs parameter set this is
**~750k SHAKE evaluations per DKG ceremony**. At current MPC framework
performance (MP-SPDZ, EMP-toolkit, SCALE-MAMBA) that's **multi-hour to
multi-day** per ceremony.
**Status: NOT IMPLEMENTED.** `RootMPC` returns `ErrMPCRootNotImpl`.
## Literature
### PVSS (the layer this skeleton implements)
- Schoenmakers, "A Simple Publicly Verifiable Secret Sharing Scheme and
its Application to Electronic Voting" (CRYPTO 1999)
- Heidarvand, Villar, "Public Verifiability from Pairings in Secret
Sharing Schemes" (SAC 2008)
- Gurkan et al., "Aggregatable Distributed Key Generation"
(EUROCRYPT 2021)
### MPC over hash functions (the layer not yet implemented)
- Damgård, Pastro, Smart, Zakarias, "Multiparty Computation from Somewhat
Homomorphic Encryption" (CRYPTO 2012) — SPDZ
- Wang, Ranellucci, Katz, "Global-Scale Secure Multiparty Computation"
(CCS 2017)
- Boyle, Gilboa, Ishai, "Function Secret Sharing" (EUROCRYPT 2015)
- MP-SPDZ implementation: <https://github.com/data61/MP-SPDZ>
### Threshold HBS context
- McGrew, Fluhrer, Gazdag, Kampanakis, Morton, Westerbaan, "Coalition
and Threshold Hash-Based Signatures" (IACR ePrint 2019/793)
- Bonte, Smart, Tan, "Threshold SPHINCS+" (2023)
## What ships in v1
```
dkg2/
├── doc.go — package doc, scope, literature
├── pvss.go — PVSS deal/verify wire shape, ErrSkeletonOnly stub
├── complaint.go — Complaint round wire shape, ErrSkeletonOnly stub
├── consensus.go — Qualified-set agreement + orchestrator + ErrMPCRootNotImpl
└── README.md — this file
```
Every public function returns `ErrSkeletonOnly` (orchestration) or
`ErrMPCRootNotImpl` (the MPC root step). This makes it **impossible**
for a production caller to accidentally consume the unfinished pipeline.
## What blocks shipping a working dkg2
See `BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1` in the magnetar repo root.
In short:
1. Select the MPC framework (MP-SPDZ, EMP, custom).
2. Implement the SHAKE-256 chain circuit under the chosen MPC.
3. Benchmark per-element cost; choose the parameter set + committee
size that fits the deployment budget.
4. Write the production root.go.
5. Wire-format spec the public root computation transcript.
6. Integrate with the parent `thbs` `PublicKey` shape so the resulting
threshold HBS public key is **byte-equal** to a dealer-DKG'd public
key for the same elements (auditability).
7. Re-prove the construction (security argument under PVSS + MPC
composition).
8. Independent cryptographer review (BLK-9-analog gate).
9. Independent MPC engineer review of the SHAKE-circuit implementation.
## When to use the dealer-backed v1 instead
The dealer-backed `thbs.DealerDKG` is the **right primitive** when:
- The deployment is **M-Chain bridge custody** (`thresholdvm` M-Chain
mode per LP-134) and a TEE-attested host runs the dealer.
- The TCB explicitly includes the dealer process (Intel TDX / AMD
SEV-SNP / Intel SGX attestation).
- The operator has performed the §1 hardening from
`DEPLOYMENT-RUNBOOK.md` (mlock, ptrace-off, short-lived process).
It is the **wrong primitive** for:
- Public-BFT consensus (validator quorum, external relayers).
- Settings where the dealer cannot be in the TCB.
For those, use `magnetar.ValidatorSign` + `VerifyAggregateCert`
(per-validator standalone SLH-DSA).
+139
View File
@@ -0,0 +1,139 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dkg2
// complaint.go — Complaint round + dealer disqualification.
//
// SKELETON: this file ships the wire shape of the complaint round
// that pairs with pvss.go's Deal/Verify. A recipient that detects
// ErrCommitmentMismatch on a received share emits a Complaint
// against the dealer; the consensus round (consensus.go) tallies
// complaints and produces the QualifiedSet Q ⊆ [n] of dealers whose
// contributions survive.
//
// Bad-delivery model:
//
// 1. Dealer D broadcasts a DealtBundle whose PublicCommitments
// promise per-recipient shares consistent with the dealer's
// polynomial.
// 2. Recipient R checks the share against the public commitments.
// If the check fails, R publishes a Complaint{Accuser=R,
// Accused=D, Element=e, Witness=share} so any third party can
// replay the verification.
// 3. The dealer D may RESPOND with a Defense{Element=e, OpenedShare=...}
// that lets every party re-verify R's complaint against D's
// opened share. If the defense holds, the complaint is dropped;
// if D fails to defend (or defends incorrectly), D is added to
// the disqualified set.
// 4. Consensus rounds (consensus.go) reach agreement on the
// qualified set Q via the chain's BFT layer.
// ComplaintKind taxonomy. Wire-stable; do not renumber.
type ComplaintKind uint8
const (
// ComplaintBadShare: accused dealer's share for the accuser does
// not verify against the accused's public commitment vector.
ComplaintBadShare ComplaintKind = 1
// ComplaintMissingDelivery: accused dealer did not deliver any
// share to the accuser within the round timeout.
ComplaintMissingDelivery ComplaintKind = 2
// ComplaintCommitmentMalformed: accused dealer's public
// commitment vector is structurally invalid (wrong length,
// wrong group element, etc.).
ComplaintCommitmentMalformed ComplaintKind = 3
)
// String returns the canonical name of the complaint kind.
func (k ComplaintKind) String() string {
switch k {
case ComplaintBadShare:
return "bad-share"
case ComplaintMissingDelivery:
return "missing-delivery"
case ComplaintCommitmentMalformed:
return "commitment-malformed"
default:
return "unknown"
}
}
// Complaint is the wire envelope a recipient broadcasts when it
// detects deviation by a dealer during the PVSS round.
//
// Wire layout:
// Kind || Accuser || Accused || Element || Witness
//
// The Witness payload is kind-specific:
// - ComplaintBadShare: serialized ContributionShare for the
// pair (Accused -> Accuser, Element)
// - ComplaintMissingDelivery: empty
// - ComplaintCommitmentMalformed: serialized commitment vector
// bytes the accuser received
//
// Third parties replay the witness against the accused's broadcast
// DealtBundle to confirm the complaint is valid; the chain BFT
// layer decides which complaints to honour.
type Complaint struct {
Kind ComplaintKind
Accuser PartyID
Accused PartyID
Element ElementID
Witness []byte
}
// Defense is the dealer-side response to a Complaint. The dealer
// re-opens the share for the disputed element so any third party
// can confirm whether the accuser's complaint holds.
//
// Wire layout:
// Dealer || Element || OpenedShare
type Defense struct {
Dealer PartyID
Element ElementID
OpenedShare []uint16
}
// FileComplaint is the recipient-side step: scan received
// DealtBundles, detect mismatches, emit Complaint envelopes.
//
// SKELETON: returns nil + ErrSkeletonOnly. Real implementation:
//
// 1. Run Verify() on every received DealtBundle.
// 2. For each (element, dealer) that fails verification, emit
// Complaint{Kind: ComplaintBadShare, Accuser: self,
// Accused: dealer, Element: element,
// Witness: serialized share}.
// 3. For dealers that did not deliver any share within the timeout,
// emit Complaint{Kind: ComplaintMissingDelivery, ...}.
// 4. Return the list of complaints; caller broadcasts them.
func FileComplaint(cfg Config, self PartyID, bundles []*DealtBundle) ([]*Complaint, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}
return nil, ErrSkeletonOnly
}
// DealerDefend is the dealer-side step: process incoming Complaints,
// emit a Defense for each VALID complaint (or accept disqualification
// for INVALID complaints).
//
// SKELETON: returns nil + ErrSkeletonOnly. Real implementation:
//
// 1. For each Complaint against THIS dealer, re-derive the
// contribution polynomial f_e at the accuser's EvalPoint.
// 2. If the dealer's opened share matches the value the dealer
// committed to via PublicCommitments, emit a Defense and rely
// on consensus to drop the complaint.
// 3. If the dealer cannot defend (e.g. genuine misdeal), do not
// emit a Defense; the consensus round will disqualify the
// dealer.
func DealerDefend(cfg Config, self PartyID, complaints []*Complaint) ([]*Defense, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}
return nil, ErrSkeletonOnly
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dkg2
// consensus.go — Agreement on the qualified set Q ⊆ [n] of dealers
// whose contributions survive the complaint round, and orchestrator
// for the public DKG.
//
// SKELETON: this file ships the wire shape of the qualified-set
// consensus step + the top-level Run orchestrator. Real
// implementation:
//
// 1. After PVSS Round 1 (deal) and Complaint Round (file +
// defend), every party computes its LOCAL view of the
// qualified set Q_self = {dealers d | no surviving complaint
// against d}.
// 2. The committee runs a BFT consensus round on Q. In Lux this
// means broadcasting a hash of Q_self and waiting for
// 2f+1 matching signatures; the chain layer drives this. In a
// standalone deployment this can be a single-round broadcast +
// local intersection.
// 3. The agreed Q is the basis for x_e = Σ_{j ∈ Q} r_{j,e} (the
// joint contribution sum, per element).
// 4. The PUBLIC chain endpoints W_e = H^{w-1}(x_e) and the public
// Merkle root over all W_e are derived via the MPC layer at
// RootMPC (which is OPEN RESEARCH; see pvss.go ErrMPCRootNotImpl
// and root.go).
// QualifiedSet is a compact dense bitmap over the committee. Bit i
// is set iff dealer with EvalPoint (i+1) is in the qualified set.
type QualifiedSet []byte
// Set marks bit i.
func (q QualifiedSet) Set(i int) {
if i>>3 < len(q) {
q[i>>3] |= 1 << uint(i&7)
}
}
// Has reports whether bit i is set.
func (q QualifiedSet) Has(i int) bool {
if i>>3 >= len(q) {
return false
}
return q[i>>3]&(1<<uint(i&7)) != 0
}
// NewQualifiedSet allocates a QualifiedSet sized for `n` parties.
func NewQualifiedSet(n int) QualifiedSet {
return make(QualifiedSet, (n+7)/8)
}
// AgreementProposal is the per-party broadcast for the qualified-set
// consensus round: each party emits its local view of Q.
//
// Wire layout:
// Author || Epoch || QHash
//
// QHash is a hash of the canonical-ordered Q bitmap; the canonical
// order is by dealer EvalPoint ascending.
type AgreementProposal struct {
Author PartyID
Epoch uint64
QHash [32]byte
}
// AgreementOutput is the output of the qualified-set consensus step:
// the agreed Q bitmap plus the witnesses (signed proposals) for
// auditability.
type AgreementOutput struct {
Epoch uint64
Q QualifiedSet
Proposals []*AgreementProposal
}
// ProposeQ is the per-party step: scan received complaints +
// defenses, compute the LOCAL view of Q, broadcast the hash.
//
// SKELETON: returns nil + ErrSkeletonOnly.
func ProposeQ(cfg Config, self PartyID, complaints []*Complaint, defenses []*Defense) (*AgreementProposal, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}
return nil, ErrSkeletonOnly
}
// AgreeQ is the consensus step: collect proposals, find the majority
// agreement, and return the agreed Q.
//
// SKELETON: returns nil + ErrSkeletonOnly. Real implementation
// requires plugging into the chain's BFT layer.
func AgreeQ(cfg Config, proposals []*AgreementProposal) (*AgreementOutput, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}
return nil, ErrSkeletonOnly
}
// Run is the top-level orchestrator for the public DKG. It composes
// PVSS deal -> verify -> complaint -> defense -> agreement -> MPC
// root computation.
//
// SKELETON: returns ErrSkeletonOnly because the MPC-root step is not
// implemented. A v1 caller MUST NOT consume Run; use
// thbs.DealerDKG for the dealer-backed v1 path or
// magnetar.PerValidatorKeypair + magnetar.ValidatorSign for the
// per-validator standalone path.
//
// When the MPC-root layer lands (v0.6+ candidate), Run will:
//
// 1. Call Deal once per dealer; collect DealtBundles.
// 2. Call Verify on every party for every bundle.
// 3. Collect FileComplaint outputs from every party; broadcast
// DealerDefend outputs from dealers.
// 4. Each party calls ProposeQ; AgreeQ produces the qualified set.
// 5. RootMPC consumes the qualified-set shares + dealer
// contributions and computes the PUBLIC Merkle root, returning
// a (root, helper_data, transcript) tuple consumable by the
// parent thbs verifier.
//
// The output of Run, when implemented, is a (PublicKey, PerParty
// shares) tuple analogous to thbs.DealerDKGAll's output — but
// PRODUCED WITHOUT A DEALER. That is the public-BFT-safe path the
// user's spec calls "DKG = PVSS for secret shares + MPC/public
// verification for derived roots."
func Run(cfg Config) error {
if err := validateConfig(cfg); err != nil {
return err
}
return ErrSkeletonOnly
}
// RootMPC is the unfinished STUB for the public Merkle root
// computation. Given the qualified-set shares + dealer
// contributions, it must compute the public chain endpoints W_e =
// H^{w-1}(x_e) and the public Merkle root over them WITHOUT
// REVEALING any x_e to any single party.
//
// SKELETON: returns ErrMPCRootNotImpl. See doc.go and
// BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1 for the open-research framing.
//
// The right answer is one of:
//
// - SPDZ-style MPC over SHA-256/SHAKE (Damgård-Pastro-Smart-Zakarias
// 2012). Multi-second per element on current MP-SPDZ; for SLH-DSA-
// SHAKE-192s with ~750K hashes per public key this is multi-hour
// to multi-day per DKG ceremony.
// - Garbled-circuit MPC (Wang-Ranellucci-Katz 2017). Similar order
// of cost.
// - Function Secret Sharing for SHA-256 specifically (Boyle-Gilboa-
// Ishai 2015). Open research for the specific hash circuit.
// - A different HBS instantiation with MPC-friendly hash (e.g.
// LowMC, Poseidon, Rescue). Would NOT produce FIPS 205-
// compatible signatures.
//
// The choice depends on the deployment economics. The cryptographer
// team selects when integrating; v0.6+ candidate.
func RootMPC(cfg Config, agreed *AgreementOutput) error {
if err := validateConfig(cfg); err != nil {
return err
}
return ErrMPCRootNotImpl
}
+199
View File
@@ -0,0 +1,199 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dkg2
// dkg2_test.go — Skeleton-tier tests. These do NOT exercise the
// (open-research) MPC-root layer; they pin the wire-shape contract
// and the ErrSkeletonOnly / ErrMPCRootNotImpl sentinels so a
// production caller cannot consume the unfinished pipeline by
// accident.
import (
"errors"
"testing"
)
func testCommittee(n int) []Participant {
out := make([]Participant, n)
for i := 0; i < n; i++ {
var id PartyID
id[0] = byte(i + 1)
copy(id[1:], []byte("DKG2-TEST"))
out[i] = Participant{ID: id, EvalPoint: uint16(i + 1)}
}
return out
}
func testConfig() Config {
parts := testCommittee(3)
return Config{
ChainID: []byte("lux-magnetar-dkg2-test"),
Epoch: 1,
Threshold: 2,
Participants: parts,
Elements: []ElementID{
{Type: 1, Slot: 0, ChainIdx: 0},
{Type: 1, Slot: 0, ChainIdx: 1},
{Type: 2, Slot: 0, TreeIdx: 0, LeafIdx: 0},
},
}
}
// TestDeal_SkeletonOnly pins the Deal-returns-ErrSkeletonOnly
// contract. A production caller invoking Deal must hit the
// sentinel; if a future commit removes the sentinel WITHOUT
// implementing the MPC layer, this test catches it.
func TestDeal_SkeletonOnly(t *testing.T) {
cfg := testConfig()
_, err := Deal(cfg, cfg.Participants[0].ID)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("Deal err = %v, want ErrSkeletonOnly", err)
}
}
// TestVerify_SkeletonOnly pins the Verify-returns-ErrSkeletonOnly
// contract on a non-nil bundle.
func TestVerify_SkeletonOnly(t *testing.T) {
cfg := testConfig()
bundle := &DealtBundle{}
err := Verify(cfg, cfg.Participants[0].ID, bundle)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("Verify err = %v, want ErrSkeletonOnly", err)
}
}
// TestFileComplaint_SkeletonOnly pins the complaint-round stub.
func TestFileComplaint_SkeletonOnly(t *testing.T) {
cfg := testConfig()
_, err := FileComplaint(cfg, cfg.Participants[0].ID, nil)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("FileComplaint err = %v, want ErrSkeletonOnly", err)
}
}
// TestDealerDefend_SkeletonOnly pins the defense stub.
func TestDealerDefend_SkeletonOnly(t *testing.T) {
cfg := testConfig()
_, err := DealerDefend(cfg, cfg.Participants[0].ID, nil)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("DealerDefend err = %v, want ErrSkeletonOnly", err)
}
}
// TestProposeQ_SkeletonOnly pins the agreement-proposal stub.
func TestProposeQ_SkeletonOnly(t *testing.T) {
cfg := testConfig()
_, err := ProposeQ(cfg, cfg.Participants[0].ID, nil, nil)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("ProposeQ err = %v, want ErrSkeletonOnly", err)
}
}
// TestAgreeQ_SkeletonOnly pins the agreement stub.
func TestAgreeQ_SkeletonOnly(t *testing.T) {
cfg := testConfig()
_, err := AgreeQ(cfg, nil)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("AgreeQ err = %v, want ErrSkeletonOnly", err)
}
}
// TestRun_SkeletonOnly pins the top-level orchestrator stub.
func TestRun_SkeletonOnly(t *testing.T) {
cfg := testConfig()
err := Run(cfg)
if !errors.Is(err, ErrSkeletonOnly) {
t.Fatalf("Run err = %v, want ErrSkeletonOnly", err)
}
}
// TestRootMPC_MPCRootNotImpl pins the MPC-root unfinished marker.
// This is the SPECIFIC sentinel pointing at the open research task.
// A production caller CANNOT consume RootMPC; the error explicitly
// names the BLOCKERS.md entry.
func TestRootMPC_MPCRootNotImpl(t *testing.T) {
cfg := testConfig()
err := RootMPC(cfg, nil)
if !errors.Is(err, ErrMPCRootNotImpl) {
t.Fatalf("RootMPC err = %v, want ErrMPCRootNotImpl", err)
}
}
// TestValidateConfig_BadThreshold pins the structural validation.
func TestValidateConfig_BadThreshold(t *testing.T) {
cfg := testConfig()
cfg.Threshold = 0
if err := validateConfig(cfg); !errors.Is(err, ErrInvalidPVSSConfig) {
t.Fatalf("validateConfig threshold=0 err = %v, want ErrInvalidPVSSConfig", err)
}
cfg = testConfig()
cfg.Threshold = uint16(len(cfg.Participants) + 1)
if err := validateConfig(cfg); !errors.Is(err, ErrInvalidPVSSConfig) {
t.Fatalf("validateConfig threshold>n err = %v, want ErrInvalidPVSSConfig", err)
}
}
// TestValidateConfig_EmptyElements pins that a DKG over an empty
// element set is rejected (catches caller bugs).
func TestValidateConfig_EmptyElements(t *testing.T) {
cfg := testConfig()
cfg.Elements = nil
if err := validateConfig(cfg); !errors.Is(err, ErrInvalidPVSSConfig) {
t.Fatalf("validateConfig empty-elements err = %v, want ErrInvalidPVSSConfig", err)
}
}
// TestValidateConfig_ZeroEvalPoint pins that EvalPoint=0 (the
// Shamir master-secret point) is rejected.
func TestValidateConfig_ZeroEvalPoint(t *testing.T) {
cfg := testConfig()
cfg.Participants[1].EvalPoint = 0
if err := validateConfig(cfg); !errors.Is(err, ErrInvalidPVSSConfig) {
t.Fatalf("validateConfig zero-EvalPoint err = %v, want ErrInvalidPVSSConfig", err)
}
}
// TestValidateConfig_DuplicateEvalPoint pins distinct-EvalPoint
// enforcement.
func TestValidateConfig_DuplicateEvalPoint(t *testing.T) {
cfg := testConfig()
cfg.Participants[1].EvalPoint = cfg.Participants[0].EvalPoint
if err := validateConfig(cfg); !errors.Is(err, ErrInvalidPVSSConfig) {
t.Fatalf("validateConfig dup-EvalPoint err = %v, want ErrInvalidPVSSConfig", err)
}
}
// TestQualifiedSet_SetHas pins the bitmap behaviour.
func TestQualifiedSet_SetHas(t *testing.T) {
q := NewQualifiedSet(8)
if q.Has(0) {
t.Fatalf("fresh QualifiedSet should have no bits set")
}
q.Set(3)
q.Set(7)
if !q.Has(3) || !q.Has(7) {
t.Fatalf("Set bits not retrievable via Has")
}
if q.Has(2) || q.Has(4) {
t.Fatalf("Unset bits returned true from Has")
}
}
// TestComplaintKind_String pins the wire-stable string mapping.
func TestComplaintKind_String(t *testing.T) {
cases := []struct {
k ComplaintKind
want string
}{
{ComplaintBadShare, "bad-share"},
{ComplaintMissingDelivery, "missing-delivery"},
{ComplaintCommitmentMalformed, "commitment-malformed"},
{ComplaintKind(99), "unknown"},
}
for _, c := range cases {
if got := c.k.String(); got != c.want {
t.Errorf("ComplaintKind(%d).String() = %q, want %q", c.k, got, c.want)
}
}
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package dkg2 is the RESEARCH-GRADE SKELETON of a public DKG for
// threshold HBS. v1 in this directory ships the PVSS layer; the
// MPC-root layer is OPEN RESEARCH and tracked in BLOCKERS.md::
// MAGNETAR-PUBLIC-DKG-1.
//
// =====================================================================
// !! SKELETON / RESEARCH ONLY — DO NOT USE IN PRODUCTION !!
// =====================================================================
//
// HBS schemes (LMS, XMSS, SLH-DSA) commit to public keys via Merkle
// roots over many WOTS+ chain endpoints (one per slot, per chain).
// Each chain endpoint W_{slot, j} = H^{w-1}(x_{slot, j}) is derived
// from a SECRET chain head x_{slot, j} by repeated hashing.
//
// A truly PUBLIC DKG (no trusted dealer, no TEE) for threshold HBS
// therefore needs TWO ingredients:
//
// (A) PVSS distribution of per-element entropy. For every secret
// element x_e (WOTS+ chain head or FORS leaf), the protocol
// collects randomness contributions r_{j,e} from each party
// j ∈ [n] and SHAMIR-SHARES each r_{j,e} across the committee
// at threshold t. The secret element is x_e = Σ_j r_{j,e};
// NO single party ever holds x_e in cleartext.
//
// This file implements (A) as a SKELETON: see pvss.go.
//
// (B) MPC over the SHA-256 / SHAKE hash function to compute the
// PUBLIC chain endpoint W_e = H^{w-1}(x_e) from the SECRET-
// SHARED x_e — WITHOUT REVEALING x_e.
//
// This is the HARD FRONTIER. Hash functions are non-linear:
// evaluating one SHA-256 / SHAKE block on a secret-shared input
// requires either (i) garbled circuits + oblivious transfer
// (multi-second, multi-megabyte per evaluation) or (ii) SPDZ-
// style arithmetic MPC (similar cost, plus offline triples) or
// (iii) function secret sharing for the specific hash circuit
// (open research). For a single SLH-DSA-SHAKE-192s public key
// we need O(2^h × WOTSChains × (w-1)) hash evaluations, where
// h=10, WOTSChains=51, w=16 → ~750 thousand SHAKE evaluations.
// Multi-hour at current MPC frameworks (MP-SPDZ, EMP, etc.).
//
// (B) IS NOT IMPLEMENTED. See pvss.go's TODO + the SkeletonOnly
// sentinel returned by RootMPC.
//
// LITERATURE
// ==========
//
// On PVSS (the layer this skeleton implements):
//
// - Schoenmakers, "A Simple Publicly Verifiable Secret Sharing
// Scheme and its Application to Electronic Voting" (CRYPTO 1999)
// - Heidarvand, Villar, "Public Verifiability from Pairings in
// Secret Sharing Schemes" (SAC 2008)
// - Gurkan et al., "Aggregatable Distributed Key Generation"
// (EUROCRYPT 2021)
//
// On the MPC layer for the public root computation (the layer this
// skeleton does NOT yet implement):
//
// - Damgård, Pastro, Smart, Zakarias, "Multiparty Computation from
// Somewhat Homomorphic Encryption" (CRYPTO 2012) — SPDZ
// - Wang, Ranellucci, Katz, "Global-Scale Secure Multiparty
// Computation" (CCS 2017)
// - Boyle, Gilboa, Ishai, "Function Secret Sharing" (EUROCRYPT 2015)
// - The MP-SPDZ implementation (https://github.com/data61/MP-SPDZ)
// ships SHA-256 over MASCOT/SPDZ at multi-hundred-ms per block
// across 3-party LANs.
//
// On threshold HBS overall (the McGrew et al. line of work that
// motivates the entire thbs package):
//
// - McGrew, Fluhrer, Gazdag, Kampanakis, Morton, Westerbaan,
// "Coalition and Threshold Hash-Based Signatures" (IACR ePrint
// 2019/793) — the v1 dealer-backed setup, with a v2 public-DKG
// discussion that leaves the MPC layer unspecified.
// - Bonte, Smart, Tan, "Threshold SPHINCS+" (2023) — infeasibility
// analysis confirming the per-signature MPC cost dominates.
//
// SCOPE OF THIS SKELETON
// ======================
//
// What ships in dkg2/ v1:
//
// - pvss.go: per-element Shamir VSS over GF(257), per-recipient
// PEdersen-style commitment + share. Each party deals their
// contribution r_{j,e} to every element e via verifiable shares.
// - complaint.go: complaint round + dealer disqualification
// mechanics. A party that fails to deliver a valid share or
// that delivers an inconsistent share is identifiably aborted.
// - consensus.go: agreement on the qualified set Q ⊆ [n] of
// parties whose contributions survive the complaint round. The
// joint secret is x_e = Σ_{j ∈ Q} r_{j,e} (per element).
//
// What does NOT ship:
//
// - root.go: STUB. The MPC-root computation that would derive
// the public chain endpoints W_e from the secret-shared x_e
// WITHOUT REVEALING any x_e. RootMPC returns ErrMPCRootNotImpl.
// This is the v0.6+ candidate work; see BLOCKERS.md::
// MAGNETAR-PUBLIC-DKG-1.
//
// USAGE
// =====
//
// This package is a STUB. Callers should NOT attempt to use it for
// production signing. The intended use is:
//
// 1. The PVSS layer is exercised by the cryptographer team to
// validate the wire shape against future MPC-root integration.
// 2. The README.md in this directory documents the open research
// path and serves as a placeholder for the v0.6+ implementation.
//
// For a working THRESHOLD HBS construction TODAY, see the parent
// package (github.com/luxfi/magnetar/ref/go/pkg/thbs) which ships
// the DEALER-BACKED v1 path.
//
// For a working PUBLIC-BFT-SAFE SLH-DSA primitive TODAY, see
// github.com/luxfi/magnetar.ValidatorSign +
// VerifyAggregateCert (per-validator standalone SLH-DSA, no DKG).
package dkg2
+234
View File
@@ -0,0 +1,234 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dkg2
// pvss.go — Publicly Verifiable Secret Sharing layer for the public
// DKG.
//
// SKELETON: this file ships the WIRE SHAPE of the PVSS-based public
// DKG for HBS. It implements:
//
// - Per-element contribution sharing: every party j ∈ [n] samples
// a random per-element contribution r_{j,e} for every secret
// element e ∈ E (the WOTS+ chain heads + FORS leaves), and
// Shamir-shares r_{j,e} across the committee at threshold t.
// - Per-share Pedersen-style commitments so the share's
// consistency with the dealt commitment vector is publicly
// verifiable.
// - Per-recipient envelope with KEM wrapping placeholder
// (ML-KEM-768 in production; placeholder slot here).
//
// What this file does NOT do:
//
// - It does NOT compute the public chain endpoints W_e or the
// public Merkle root from the secret-shared elements. That
// requires the MPC layer at root.go which is OPEN RESEARCH.
// - It does NOT consume from a CryptoSuite or wire-format spec
// beyond the parent thbs's GF(257) Shamir. Future work pins
// a chain-friendly curve commitment (Pedersen on BLS12-381
// scalar field) for aggregatable PVSS — current shape uses
// in-package hash-based commitments only.
import (
"errors"
)
// PartyID is the canonical party identifier, mirroring thbs.PartyID.
// Re-declared here so dkg2 has no import cycle into thbs.
type PartyID [32]byte
// ElementID is dkg2's flat key for a threshold-shared secret element.
// Mirrors thbs.ElementID but is local to avoid an import cycle.
type ElementID struct {
Type byte
Slot uint32
ChainIdx uint16
TreeIdx uint16
LeafIdx uint32
}
// Participant identifies one party in the PVSS committee.
type Participant struct {
ID PartyID
EvalPoint uint16
}
// Config parameterises a PVSS-based public DKG ceremony.
type Config struct {
ChainID []byte
Epoch uint64
Threshold uint16
Participants []Participant
// Elements is the set of secret elements the DKG produces shares
// for. Caller derives this from the HBS parameter set:
// - one ElementID per (slot, ElementWOTS, chain_idx) tuple
// - one ElementID per (slot, ElementFORS, tree_idx, leaf_idx)
// For SLH-DSA-SHAKE-192s with the v1 thbs parameter set this is
// ~1.6M elements per fresh DKG — see THBS-SPEC.md.
Elements []ElementID
}
// ContributionShare is one party's PVSS share of one dealer's
// contribution to one element. Wire-stable, signable.
type ContributionShare struct {
// Dealer is the party that contributed the randomness.
Dealer PartyID
// Recipient is the party this share is addressed to.
Recipient PartyID
// Element is the secret element this share pertains to.
Element ElementID
// EvalPoint is the recipient's Shamir x-coordinate.
EvalPoint uint16
// Share is the per-byte GF(257) Shamir share of the dealer's
// contribution r_{Dealer, Element}. Length matches the
// underlying secret element byte width.
Share []uint16
// Commitment is the per-coefficient Pedersen-style commitment
// vector so the recipient can verify the share is consistent
// with what was publicly committed.
//
// SKELETON NOTE: the v1 commitment here is a placeholder
// hash-based binding tag. A production PVSS layer would use a
// pairing-friendly Pedersen commitment over BLS12-381 to admit
// publicly verifiable shares. This skeleton documents the wire
// slot; the cryptographer team selects the production
// commitment scheme when integrating with the (open-research)
// MPC-root layer.
Commitment [][]byte
}
// DealtBundle is one dealer's full output for a PVSS round: a
// commitment vector PUBLIC to the committee plus per-recipient
// share envelopes.
type DealtBundle struct {
// Dealer is the party that produced this bundle.
Dealer PartyID
// PublicCommitments is the dealer-broadcast commitment vector
// (one Pedersen-style commitment per polynomial coefficient,
// per element). Every party verifies share consistency against
// this vector.
PublicCommitments map[ElementID][][]byte
// PerRecipient maps each recipient PartyID to that party's
// per-element shares. Total size = n_recipients × |Elements|
// per dealer — for the v1 thbs parameter set this dominates
// the DKG wire cost.
PerRecipient map[PartyID][]ContributionShare
}
// Errors returned by the PVSS layer.
var (
// ErrInvalidPVSSConfig means the Config is malformed: bad
// threshold, zero eval point, duplicate participants, or empty
// element set.
ErrInvalidPVSSConfig = errors.New("dkg2: invalid PVSS config")
// ErrCommitmentMismatch means a recipient's verification of a
// dealt share against the dealer's public commitment failed.
// Triggers the complaint round (complaint.go).
ErrCommitmentMismatch = errors.New("dkg2: PVSS share commitment mismatch")
// ErrMPCRootNotImpl is the sentinel returned by RootMPC. The
// MPC layer that computes public Merkle roots from
// secret-shared leaves is open research; v0.6+ candidate.
// See BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1.
ErrMPCRootNotImpl = errors.New("dkg2: MPC-root computation not implemented (see BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1)")
// ErrSkeletonOnly is the umbrella sentinel returned by
// orchestrator functions that compose PVSS + complaint +
// consensus + MPC root. As of the skeleton ship the
// orchestrator returns this to make it impossible for a
// production caller to accidentally consume the unfinished
// pipeline.
ErrSkeletonOnly = errors.New("dkg2: skeleton only — production caller MUST NOT consume (use thbs.DealerDKG for v1 or magnetar.ValidatorSign for public BFT)")
)
// Deal is the dealer-side PVSS step.
//
// SKELETON: ships the function signature + a stub body that returns
// ErrSkeletonOnly. Real implementation:
//
// 1. For each element e ∈ Elements:
// a. Sample contribution r_e ∈ {0,1}^{element_byte_size}.
// b. Generate a random degree-(t-1) polynomial f_e(X) with
// f_e(0) = r_e (per-byte GF(257) Shamir, matching the
// parent thbs Shamir).
// c. Compute the per-coefficient commitment vector
// C_e = [Comm(f_{e,0}), Comm(f_{e,1}), ..., Comm(f_{e,t-1})].
// d. For each recipient j with EvalPoint x_j:
// Share_{e,j} = f_e(x_j) (per byte).
//
// 2. Broadcast (commitments, per-recipient shares) as a
// DealtBundle.
//
// 3. Wrap each per-recipient envelope under ML-KEM-768 to the
// recipient's long-term identity public key (BLOCKERS.md::CR-8
// pattern, matches Pulsar v0.3+).
//
// What does NOT happen here:
//
// - The public chain-endpoint W_e is NOT computed from the
// contribution. That requires the MPC layer at RootMPC.
// - The dealer does NOT see or hold the JOINT secret element
// x_e = Σ_j r_{j,e}; only its OWN contribution r_{self,e}.
func Deal(cfg Config, self PartyID) (*DealtBundle, error) {
if err := validateConfig(cfg); err != nil {
return nil, err
}
return nil, ErrSkeletonOnly
}
// Verify is the recipient-side PVSS step. Each party invokes Verify
// on every received DealtBundle to detect bad-delivery / commitment-
// mismatch (and feed evidence into the complaint round).
//
// SKELETON: returns ErrSkeletonOnly.
//
// Real implementation:
//
// 1. For every (element, share) pair in bundle.PerRecipient[self]:
// a. Evaluate the dealer's public commitment polynomial at
// self's EvalPoint x_self using the bundle's
// PublicCommitments[element] vector.
// b. Verify the commitment evaluation matches the per-byte
// share. If not: return ErrCommitmentMismatch and the
// caller raises a complaint (complaint.go).
// 2. Persist (dealer, element, share) for the consensus step.
func Verify(cfg Config, self PartyID, bundle *DealtBundle) error {
if err := validateConfig(cfg); err != nil {
return err
}
if bundle == nil {
return ErrSkeletonOnly
}
return ErrSkeletonOnly
}
// validateConfig pins the structural invariants on Config.
func validateConfig(cfg Config) error {
if cfg.Threshold < 1 || int(cfg.Threshold) > len(cfg.Participants) {
return ErrInvalidPVSSConfig
}
if len(cfg.Elements) == 0 {
return ErrInvalidPVSSConfig
}
seen := make(map[uint16]struct{}, len(cfg.Participants))
for _, p := range cfg.Participants {
if p.EvalPoint == 0 {
return ErrInvalidPVSSConfig
}
if _, dup := seen[p.EvalPoint]; dup {
return ErrInvalidPVSSConfig
}
seen[p.EvalPoint] = struct{}{}
}
return nil
}
+50 -7
View File
@@ -1,12 +1,53 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package thbs implements Magnetar-THBS v1: true threshold hash-based
// signatures in the sense of McGrew, Fluhrer, Gazdag, Kampanakis, Morton,
// Westerbaan, "Coalition and Threshold Hash-Based Signatures"
// Package thbs implements Magnetar-THBS v1: TRUE threshold hash-based
// signatures in the sense of McGrew, Fluhrer, Gazdag, Kampanakis,
// Morton, Westerbaan, "Coalition and Threshold Hash-Based Signatures"
// (IACR ePrint 2019/793 and the IRTF draft-mcgrew-hash-sigs line of
// work).
//
// =====================================================================
// !! DEALER-BACKED v1 — NOT PUBLIC-BFT-SAFE !!
// =====================================================================
//
// THIS PACKAGE IS A DEALER-BACKED THRESHOLD HBS CONSTRUCTION.
//
// - A SINGLE DEALER generates the WOTS+ chain heads and FORS secret
// leaves at setup, Shamir-shares each across the committee, and
// erases the master seed. The dealer LEARNS every secret element
// at setup time.
// - For PUBLIC-BFT consensus (Lux validator quorums, external
// relayers, any setting where the dealer cannot be in the trusted
// computing base) this is the WRONG primitive.
// - For PUBLIC-BFT consensus on SLH-DSA, use:
//
// github.com/luxfi/magnetar.ValidatorSign +
// github.com/luxfi/magnetar.VerifyAggregateCert
//
// (See ref/go/pkg/magnetar/standalone.go. Per-validator
// standalone SLH-DSA keypairs. No DKG. No dealer. No shared
// secret. Each validator signs independently; the consensus
// layer collects N signatures into a ValidatorAggregateCert
// and optionally compresses via Z-Chain Groth16.)
//
// THIS PACKAGE IS APPROPRIATE for the M-Chain bridge custody
// pattern where a TEE-attested host (Intel TDX / AMD SEV-SNP / Intel
// SGX) acts as the dealer by policy, and the TEE root-of-trust is
// already in the deployment TCB. For that pattern see
// DEPLOYMENT-RUNBOOK.md §0 "Choose your mode".
//
// THE PUBLIC-DKG PATH (no dealer, no TEE, public-BFT-safe) is
// research-grade and tracked in BLOCKERS.md::MAGNETAR-PUBLIC-DKG-1.
// A SKELETON ships at github.com/luxfi/magnetar/ref/go/pkg/thbs/dkg2/
// — that subpackage implements the PVSS layer (each party
// contributes randomness to every secret element) but DOES NOT yet
// solve the MPC-root layer (computing the public Merkle root from
// secret-shared leaves without revealing the leaves). The MPC-root
// layer is open research; v0.6+ candidate.
//
// =====================================================================
//
// CRITICAL DISTINCTION (this is what makes the package "true" threshold):
//
// - For HBS schemes a signature reveals SELECTED secret elements
@@ -21,12 +62,14 @@
// signature.
//
// This is NOT "collect N independent SLH-DSA signatures and average
// them"; that's the wrong thing. This is the right thing.
// them"; that's the wrong thing. This is the right thing — but ONLY
// when the dealer is in the TCB.
//
// V1 SCOPE — honest:
// V1 SCOPE — HONEST:
//
// - Setup is DEALER-BACKED. A trusted dealer generates the shared
// WOTS+/FORS material and distributes shares. (Public DKG = v2.)
// WOTS+/FORS material and distributes shares.
// (Public DKG = research, see dkg2/.)
// - Helper data (the Merkle authentication paths and the public-key
// chain endpoints) is shipped alongside the public key. McGrew et
// al. allow this.
@@ -40,7 +83,7 @@
//
// HARD INVARIANT (cf. THBS-SPEC.md):
//
// OK: ReconstructElement(slot, elementID, shares)
// OK: reconstructElement(slot, elementID, shares) [unexported]
// Forbidden: ReconstructSeed, ReconstructPrivateKey,
// ExpandPrivateKey, DeriveAllFutureElements
//
+7 -7
View File
@@ -54,12 +54,12 @@ func runDealerDKG(t *testing.T, threshold, n int) (PublicKey, []PrivateShare) {
}
// Deterministic DKG for stable test vectors.
seed := bytes.Repeat([]byte{0xA1}, 64)
pk, shares, err := DKGAll(cfg, DKGOptions{DealerSeed: seed})
pk, shares, err := DealerDKGAll(cfg, DKGOptions{DealerSeed: seed})
if err != nil {
t.Fatalf("DKGAll: %v", err)
t.Fatalf("DealerDKGAll: %v", err)
}
if len(shares) != n {
t.Fatalf("DKGAll returned %d shares, want %d", len(shares), n)
t.Fatalf("DealerDKGAll returned %d shares, want %d", len(shares), n)
}
return pk, shares
}
@@ -72,7 +72,7 @@ func TestTHBS_DKG_NoSeedExposure(t *testing.T) {
_, shares := runDealerDKG(t, 2, 3)
// We inspect every byte of every PrivateShare and assert no part
// of it is the dealer seed. Because the dealer seed is wiped
// before DKGAll returns, we use a structural check: the share
// before DealerDKGAll returns, we use a structural check: the share
// must not contain a "seed" field, and ElementShares must consist
// of per-element entries.
for i, s := range shares {
@@ -670,7 +670,7 @@ func TestTHBS_DKGConfig_Validation(t *testing.T) {
Params: testParams(),
}
tc.mut(&cfg)
_, _, err := DKGAll(cfg, DKGOptions{})
_, _, err := DealerDKGAll(cfg, DKGOptions{})
if !errors.Is(err, tc.want) {
t.Errorf("got %v, want %v", err, tc.want)
}
@@ -689,9 +689,9 @@ func TestTHBS_RandomSeed(t *testing.T) {
Participants: parts,
Params: testParams(),
}
pk, shares, err := DKGAll(cfg, DKGOptions{Rng: rand.Reader})
pk, shares, err := DealerDKGAll(cfg, DKGOptions{Rng: rand.Reader})
if err != nil {
t.Fatalf("DKGAll(random): %v", err)
t.Fatalf("DealerDKGAll(random): %v", err)
}
msg := []byte("random-seed-test")
const slot Slot = 0