From 2cecc08c63c00a3814da4f7bbe80c72fa3f2785a Mon Sep 17 00:00:00 2001 From: zeekay Date: Sat, 27 Jun 2026 22:03:55 -0700 Subject: [PATCH] =?UTF-8?q?refactor(corona):=20delete=20the=20dkg2=20packa?= =?UTF-8?q?ge=20=E2=80=94=20superseded=20by=20luxfi/dkg=20vss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pedersen-VSS DKG share-dealing now runs entirely on github.com/luxfi/dkg (proven byte-identical at cutover); corona's own copy is dead. Remove it and its now-orphaned tooling, keeping only corona-specific code (Ringtail signing, β rounding, Path (a) finalize — all in sign/, utils/, keyera/, untouched). Deleted: - dkg2/ (the whole package: Round1/Round2/commits/complaint/GPU dispatch). - cmd/dkg2_oracle/ (the dkg2 KAT generator; the DKG KAT now lives in luxfi/dkg's own ring/vss vectors). - keyera/cutover_gate_test.go (the transitional dkg2-vs-vss equality gate; its job is done — permanent byte-stability is pinned by the golden KAT, and the PIN-4 convention assertion is preserved dkg2-free in pin4_convention_test.go). Updated: - cmd/cross_runtime_oracle drops the dkg2 manifest leg (the luxcpp C++ dkg2 cross-runtime gate migrates to luxfi/dkg separately — flagged, not silently broken). - keyera/reanchor doc comments + threshold bench comment point at luxfi/dkg. go build ./... + go test ./... -count=1 green (13 ok / 0 fail), go vet + gofmt clean. The golden byte-stability KAT + Bootstrap→sign→verify + dishonest-dealer abort all still pass on the vss path. --- cmd/cross_runtime_oracle/main.go | 13 +- cmd/dkg2_oracle/main.go | 324 ------- dkg2/bench_baseline_test.go | 40 - dkg2/complaint.go | 424 --------- dkg2/dkg2.go | 768 ---------------- dkg2/dkg2_gpu.go | 300 ------- dkg2/dkg2_gpu_test.go | 231 ----- dkg2/dkg2_parallel.go | 11 - dkg2/dkg2_test.go | 1382 ----------------------------- dkg2/fuzz_round_test.go | 114 --- keyera/bootstrap_pedersen_test.go | 2 +- keyera/cutover_gate_test.go | 440 --------- keyera/keyera.go | 26 +- keyera/pin4_convention_test.go | 128 +++ keyera/reanchor_pedersen.go | 5 +- threshold/bench_gpu_test.go | 3 +- 16 files changed, 155 insertions(+), 4056 deletions(-) delete mode 100644 cmd/dkg2_oracle/main.go delete mode 100644 dkg2/bench_baseline_test.go delete mode 100644 dkg2/complaint.go delete mode 100644 dkg2/dkg2.go delete mode 100644 dkg2/dkg2_gpu.go delete mode 100644 dkg2/dkg2_gpu_test.go delete mode 100644 dkg2/dkg2_parallel.go delete mode 100644 dkg2/dkg2_test.go delete mode 100644 dkg2/fuzz_round_test.go delete mode 100644 keyera/cutover_gate_test.go create mode 100644 keyera/pin4_convention_test.go diff --git a/cmd/cross_runtime_oracle/main.go b/cmd/cross_runtime_oracle/main.go index c9625da..fa55769 100644 --- a/cmd/cross_runtime_oracle/main.go +++ b/cmd/cross_runtime_oracle/main.go @@ -1,7 +1,7 @@ // Package main is the Corona cross-runtime KAT oracle. // // Emits a single JSON manifest at /cross_runtime_kat.json that ties -// together the three canonical Corona KATs (sign, reshare, dkg2) with +// together the canonical Corona KATs (sign, reshare) with // the SHA-256 of each individual KAT file. The C++ side // (luxcpp/crypto/corona/test/cross_runtime_test.cpp) replays each KAT // in C++ and verifies byte-equality; running this oracle first then @@ -73,12 +73,14 @@ func main() { fmt.Fprintf(os.Stderr, "sign_oracle: %v\n", err) os.Exit(1) } - // reshare_oracle and dkg2_oracle don't expose --out; they write to - // hardcoded locations in luxcpp/crypto. For the cross-runtime gate - // we hash the canonical paths. + // reshare_oracle doesn't expose --out; it writes to a hardcoded location + // in luxcpp/crypto. For the cross-runtime gate we hash the canonical path. + // + // The Pedersen-DKG KAT is no longer emitted here: corona's DKG share-dealing + // now runs on github.com/luxfi/dkg (vss), which owns its own ring/vss KATs. + // The luxcpp C++ dkg2 cross-runtime leg migrates to luxfi/dkg separately. signPath := filepath.Join(*out, "sign_kat.json") resharePath := filepath.Join(luxcppDir(), "crypto/corona/test/kat/reshare_kat.json") - dkg2Path := filepath.Join(luxcppDir(), "crypto/corona/dkg2/test/kat/dkg2_kat.json") files := []struct { name string @@ -86,7 +88,6 @@ func main() { }{ {"sign", signPath}, {"reshare", resharePath}, - {"dkg2", dkg2Path}, } m := manifest{ diff --git a/cmd/dkg2_oracle/main.go b/cmd/dkg2_oracle/main.go deleted file mode 100644 index 304d62c..0000000 --- a/cmd/dkg2_oracle/main.go +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) 2024-2026 Lux Industries Inc. -// SPDX-License-Identifier: BSD-3-Clause-Eco -// -// dkg2_oracle — emits byte-equal KATs for the Pedersen-style DKG protocol -// in github.com/luxfi/corona/dkg2. -// -// For each (t, n) configuration in the catalogue, the oracle: -// -// 1. Derives a master 32-byte seed from MasterSeed | tag(n,t) via BLAKE3. -// 2. From that master seed, derives one 32-byte sub-seed per party -// (BLAKE3(master || "party" || BE32(i))). -// 3. Constructs n DKGSessions (party 0..n-1) — they all share the same -// deterministic A and B matrices (derived from b"corona.dkg2.A.v1" / -// b"corona.dkg2.B.v1" via BLAKE3-XOF). -// 4. Each party calls Round1WithSeed(party_seed[i]) to produce its -// Commits, Shares (per recipient), and Blinds (per recipient). -// 5. Each party calls Round2 with the assembled shares/blinds/commits. -// -// The KAT pins, per entry: -// - For each party i: seed_hex[i] (sub-seed) -// - For each party i: SHA-256(Commits[k].WriteTo) × t (commitment-vector hash) -// - For each party i: BLAKE3(serialize(Commits)) (Round 1.5 digest) -// - For each party i, each recipient j: -// SHA-256(Shares[j].WriteTo) -// SHA-256(Blinds[j].WriteTo) -// - For each party j (the recipient running Round2): -// SHA-256(s_j.WriteTo) -// SHA-256(u_j.WriteTo) -// SHA-256(b_ped_j.WriteTo) -// -// Replay invariant: every party agrees on b_ped, so all b_ped hashes inside -// one entry must be identical. The KAT records all n of them so the C++ -// port can prove that property too. -// -// Output: /corona/dkg2/test/kat/dkg2_kat.json (4 entries: -// 2-of-3, 3-of-5, 5-of-7, 7-of-11). -package main - -import ( - "bytes" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/structs" - - "github.com/luxfi/corona/dkg2" - "github.com/luxfi/corona/sign" - "github.com/zeebo/blake3" -) - -// MasterSeed is the deterministic root for all dkg2 KAT entries. Distinct -// from dkg/'s 0xCAFEBABE_DEADBEEF root so the two oracles never alias. -const MasterSeed uint64 = 0xC0FFEE_F00D_FACE - -// deriveMaster expands MasterSeed + a per-entry tag into a 32-byte sub-seed. -func deriveMaster(t, n int) []byte { - h := blake3.New() - var buf [16]byte - binary.BigEndian.PutUint64(buf[0:8], MasterSeed) - binary.BigEndian.PutUint32(buf[8:12], uint32(t)) - binary.BigEndian.PutUint32(buf[12:16], uint32(n)) - _, _ = h.Write([]byte("dkg2-oracle:master:")) - _, _ = h.Write(buf[:]) - return h.Sum(nil)[:32] -} - -// derivePartySeed derives party i's 32-byte Round1WithSeed input from the -// master entry seed. -func derivePartySeed(master []byte, partyID int) []byte { - h := blake3.New() - _, _ = h.Write([]byte("dkg2-oracle:party:")) - _, _ = h.Write(master) - var buf [4]byte - binary.BigEndian.PutUint32(buf[:], uint32(partyID)) - _, _ = h.Write(buf[:]) - return h.Sum(nil)[:32] -} - -// hashVectorBytes returns SHA-256 of the WriteTo wire bytes of v. -func hashVectorBytes(v structs.Vector[ring.Poly]) string { - var buf bytes.Buffer - if _, err := v.WriteTo(&buf); err != nil { - panic(err) - } - h := sha256.Sum256(buf.Bytes()) - return hex.EncodeToString(h[:]) -} - -func hashMatrix(m structs.Matrix[ring.Poly]) string { - var buf bytes.Buffer - if _, err := m.WriteTo(&buf); err != nil { - panic(err) - } - h := sha256.Sum256(buf.Bytes()) - return hex.EncodeToString(h[:]) -} - -// PartyEntry is one party's contribution to the KAT. -type PartyEntry struct { - PartyID int `json:"party_id"` - SeedHex string `json:"seed_hex"` // Round1WithSeed input - CommitsHashHex []string `json:"commits_hash_hex"` // length t - CommitDigestHex string `json:"commit_digest_hex"` // BLAKE3 over serialized commits - SharesHashHex []string `json:"shares_hash_hex"` // length n; index j = share to party j - BlindsHashHex []string `json:"blinds_hash_hex"` // length n; index j = blind to party j - SecretShareHash string `json:"secret_share_hash"` // SHA-256(s_j) after Round2 - BlindShareHash string `json:"blind_share_hash"` // SHA-256(u_j) after Round2 - BPedHash string `json:"b_ped_hash"` // SHA-256(b_ped) after Round2 -} - -type Entry struct { - T int `json:"t"` - N int `json:"n"` - MasterSeedHex string `json:"master_seed_hex"` - AHashHex string `json:"a_hash_hex"` - BHashHex string `json:"b_hash_hex"` - Parties []PartyEntry `json:"parties"` -} - -type OracleOut struct { - Description string `json:"description"` - Q uint64 `json:"q"` - N int `json:"n_ring"` - M int `json:"m"` - Nvec int `json:"nvec"` - Xi int `json:"xi"` - TagAHex string `json:"tag_a_hex"` - TagBHex string `json:"tag_b_hex"` - Entries []Entry `json:"entries"` -} - -func writeJSON(path string, v any) error { - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - return err - } - b = append(b, '\n') - return os.WriteFile(path, b, 0o644) -} - -func runEntry(t, n int) Entry { - master := deriveMaster(t, n) - - params, err := dkg2.NewParams() - if err != nil { - panic(err) - } - - sessions := make([]*dkg2.DKGSession, n) - for i := 0; i < n; i++ { - // Use the legacy BLAKE3 hash suite for the canonical KAT — keeps - // every byte in dkg2_kat.json byte-stable across the Corona-SHA3 - // cutover. Production-track callers use hash.Default() (Corona-SHA3). - s, err := dkg2.NewDKGSession(params, i, n, t, nil) - if err != nil { - panic(fmt.Errorf("NewDKGSession(%d, %d, %d): %w", i, n, t, err)) - } - sessions[i] = s - } - - // Hash the public matrices. All sessions must agree. - aHash := hashMatrix(sessions[0].APublic()) - bHash := hashMatrix(sessions[0].BPublic()) - for i := 1; i < n; i++ { - if got := hashMatrix(sessions[i].APublic()); got != aHash { - panic(fmt.Errorf("party %d A-matrix mismatch: %s vs %s", i, got, aHash)) - } - if got := hashMatrix(sessions[i].BPublic()); got != bHash { - panic(fmt.Errorf("party %d B-matrix mismatch: %s vs %s", i, got, bHash)) - } - } - - round1Outs := make([]*dkg2.Round1Output, n) - partyEntries := make([]PartyEntry, n) - for i := 0; i < n; i++ { - seed := derivePartySeed(master, i) - out, err := sessions[i].Round1WithSeed(seed) - if err != nil { - panic(fmt.Errorf("Round1WithSeed(party=%d): %w", i, err)) - } - round1Outs[i] = out - - commitsHash := make([]string, t) - for k := 0; k < t; k++ { - commitsHash[k] = hashVectorBytes(out.Commits[k]) - } - sharesHash := make([]string, n) - blindsHash := make([]string, n) - for j := 0; j < n; j++ { - sh, ok := out.Shares[j] - if !ok { - panic(fmt.Errorf("party %d missing share for %d", i, j)) - } - bl, ok := out.Blinds[j] - if !ok { - panic(fmt.Errorf("party %d missing blind for %d", i, j)) - } - sharesHash[j] = hashVectorBytes(sh) - blindsHash[j] = hashVectorBytes(bl) - } - // commit_digest_hex pins the legacy BLAKE3 path used by the - // pre-cutover KAT. CommitDigestBLAKE3 produces byte-stable bytes - // so the C++ port can replay against the same JSON. - digest, err := out.CommitDigestBLAKE3() - if err != nil { - panic(fmt.Errorf("CommitDigestBLAKE3 (party=%d): %w", i, err)) - } - partyEntries[i] = PartyEntry{ - PartyID: i, - SeedHex: hex.EncodeToString(seed), - CommitsHashHex: commitsHash, - CommitDigestHex: hex.EncodeToString(digest[:]), - SharesHashHex: sharesHash, - BlindsHashHex: blindsHash, - } - } - - for j := 0; j < n; j++ { - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = round1Outs[i].Shares[j] - blinds[i] = round1Outs[i].Blinds[j] - commits[i] = round1Outs[i].Commits - } - s, u, bPed, err := sessions[j].Round2(shares, blinds, commits) - if err != nil { - panic(fmt.Errorf("Round2(party=%d): %w", j, err)) - } - partyEntries[j].SecretShareHash = hashVectorBytes(s) - partyEntries[j].BlindShareHash = hashVectorBytes(u) - partyEntries[j].BPedHash = hashVectorBytes(bPed) - } - - bp := partyEntries[0].BPedHash - for j := 1; j < n; j++ { - if partyEntries[j].BPedHash != bp { - panic(fmt.Errorf("entry t=%d n=%d: party %d b_ped hash mismatch: %s vs %s", - t, n, j, partyEntries[j].BPedHash, bp)) - } - } - - return Entry{ - T: t, - N: n, - MasterSeedHex: hex.EncodeToString(master), - AHashHex: aHash, - BHashHex: bHash, - Parties: partyEntries, - } -} - -func main() { - cases := []struct{ t, n int }{ - {2, 3}, - {3, 5}, - {5, 7}, - {7, 11}, - } - - out := OracleOut{ - Description: "Pedersen-style DKG over R = Z_q[X]/(X^256+1), Q=0x1000000004A01. " + - "C_k = A·NTT(c_k) + B·NTT(r_k) — hiding under MLWE on B, binding under " + - "MSIS on [A|B]. A derived from BLAKE3(\"corona.dkg2.A.v1\"); B from " + - "BLAKE3(\"corona.dkg2.B.v1\"). Each entry runs the full t-of-n protocol " + - "with deterministic per-party Round1WithSeed inputs derived from " + - "MasterSeed=0xC0FFEEF00DFACE. Wire format: structs.{Vector,Matrix}[ring.Poly]" + - ".WriteTo (LE u64). Hashes are SHA-256 of those wire bytes. CommitDigest " + - "is BLAKE3 over the concatenated serialized commits, exchanged in Round 1.5 " + - "to defeat cross-party inconsistency attacks (Finding 2 of RED-DKG-REVIEW.md).", - Q: sign.Q, - N: 1 << sign.LogN, - M: sign.M, - Nvec: sign.N, - Xi: sign.Xi, - TagAHex: hex.EncodeToString([]byte("corona.dkg2.A.v1")), - TagBHex: hex.EncodeToString([]byte("corona.dkg2.B.v1")), - } - - for _, c := range cases { - fmt.Fprintf(os.Stderr, "running t=%d n=%d ...\n", c.t, c.n) - out.Entries = append(out.Entries, runEntry(c.t, c.n)) - } - - // Default output path: canonical luxcpp KAT directory. Pass an - // argument to override. When the oracle is invoked via `go run` from - // the corona repo root, the relative form ../../../luxcpp/... resolves - // correctly; otherwise pass an absolute path. - outPath := "../../../luxcpp/crypto/corona/dkg2/test/kat/dkg2_kat.json" - if env := os.Getenv("CORONA_DKG2_KAT_PATH"); env != "" { - outPath = env - } - if env := os.Getenv("PULSAR_DKG2_KAT_PATH"); env != "" { - if os.Getenv("CORONA_DKG2_KAT_PATH") == "" { - outPath = env - } - } - if len(os.Args) >= 2 { - outPath = os.Args[1] - } - if err := writeJSON(outPath, out); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - fmt.Fprintf(os.Stderr, "wrote %s\n", outPath) - - if f, err := os.Open(outPath); err == nil { - defer f.Close() - var head [256]byte - n, _ := io.ReadFull(f, head[:]) - fmt.Fprintln(os.Stderr, string(head[:n])) - } -} diff --git a/dkg2/bench_baseline_test.go b/dkg2/bench_baseline_test.go deleted file mode 100644 index 5fff4c6..0000000 --- a/dkg2/bench_baseline_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dkg2 - -import ( - "crypto/rand" - "io" - "testing" - - "github.com/luxfi/corona/sign" -) - -// BenchmarkDKG2_Round1_Baseline captures the single-party Round 1 cost -// before GPU dispatch is wired in. The numbers feed the speedup -// comparison in BenchmarkDKG2_Round1_GPU (dkg2_gpu_test.go). -func BenchmarkDKG2_Round1_Baseline_5of7(b *testing.B) { benchRound1Baseline(b, 7, 5) } -func BenchmarkDKG2_Round1_Baseline_7of11(b *testing.B) { benchRound1Baseline(b, 11, 7) } -func BenchmarkDKG2_Round1_Baseline_14of21(b *testing.B) { benchRound1Baseline(b, 21, 14) } - -func benchRound1Baseline(b *testing.B, n, t int) { - params, err := NewParams() - if err != nil { - b.Fatal(err) - } - sess, err := NewDKGSession(params, 0, n, t, nil) - if err != nil { - b.Fatal(err) - } - seed := make([]byte, sign.KeySize) - if _, err := io.ReadFull(rand.Reader, seed); err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := sess.Round1WithSeed(seed); err != nil { - b.Fatal(err) - } - } -} diff --git a/dkg2/complaint.go b/dkg2/complaint.go deleted file mode 100644 index 20a14ae..0000000 --- a/dkg2/complaint.go +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dkg2 - -// Complaint workflow and disqualification logic for the Pedersen DKG. -// -// The Pedersen DKG has identifiable abort: any malformed contribution -// (Round 1 commit / Round 1.5 digest / Round 2 share-blind pair) names -// the offending sender. Each detection produces a signed Complaint that -// any honest validator can independently re-check without trusting the -// complainer. -// -// Failure modes (mirrors reshare.ComplaintReason): -// -// (a) ComplaintBadDelivery — sender i shipped (share_{i→j}, blind_{i→j}) -// that does not satisfy A·NTT(share) + B·NTT(blind) = -// Σ_k (j+1)^k · C_{i,k}. Evidence: the (share, blind, commits) -// triple. Re-check via VerifyShareAgainstCommits. -// -// (b) ComplaintEquivocation — sender i broadcast different commit -// vectors to different recipients. Detected via Round 1.5 digest -// cross-check: when recipient j observes that sender i delivered -// digest h_i^{(j)} but other recipients k report h_i^{(k)} ≠ -// h_i^{(j)}, j emits this complaint. Evidence: two signed digest -// broadcasts under sender i's wire identity that disagree. -// -// (c) ComplaintMissing — sender i failed to deliver Round 1 by the -// cohort deadline. Evidence is the absence; a separate liveness -// round timestamps the deadline. -// -// (d) ComplaintMalformedCommit — sender i's commit vector has the -// wrong length, the wrong dimension, or fails Round 2 sanity -// checks. Evidence: the malformed commit vector. Re-check via -// VerifyShareAgainstCommits returning ErrMalformedCommit. -// -// Disqualification rule (deterministic, identical on every honest party): -// -// - Complaints from the same complainer about the same sender are -// deduplicated by (sender, complainer) tuple. -// - A sender i is disqualified iff at least DisqualificationThreshold -// distinct complainers signed valid complaints against i. The -// default threshold is t-1 (so at most t-1 colluding adversaries -// cannot disqualify an honest sender). -// - After the Round 2 deadline every honest party computes the same -// set Q' = Q \ disqualified. If |Q'| < t the DKG aborts and the -// activation cert binds the abort transcript so the chain stays at -// the previous epoch. -// -// Slashing evidence: every signed Complaint, plus the equivocation pair -// (commits_a, commits_b) where applicable, is admissible at the Quasar -// layer. Wire format mirrors reshare/quasar_integration.go to keep one -// adjudication path for both protocols. - -import ( - "bytes" - "crypto/ed25519" - "encoding/binary" - "errors" - "fmt" - - "github.com/luxfi/corona/hash" - - "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/structs" -) - -// ComplaintReason enumerates the failure modes that justify a complaint. -type ComplaintReason uint8 - -const ( - // ComplaintBadDelivery — share_{i→j} fails the Pedersen identity. - ComplaintBadDelivery ComplaintReason = 1 - - // ComplaintEquivocation — sender i broadcast disagreeing commit - // digests to different recipients. - ComplaintEquivocation ComplaintReason = 2 - - // ComplaintMissing — sender i failed to deliver share_{i→j} by - // the round-1 deadline. - ComplaintMissing ComplaintReason = 3 - - // ComplaintMalformedCommit — sender i's commit vector has wrong - // length / wrong dimension / nil entries. - ComplaintMalformedCommit ComplaintReason = 4 -) - -// String returns a human-readable name for the reason. -func (r ComplaintReason) String() string { - switch r { - case ComplaintBadDelivery: - return "bad-delivery" - case ComplaintEquivocation: - return "equivocation" - case ComplaintMissing: - return "missing" - case ComplaintMalformedCommit: - return "malformed-commit" - default: - return fmt.Sprintf("unknown(%d)", r) - } -} - -// Complaint is a signed assertion that party SenderID misbehaved during -// the Pedersen DKG protocol. The structure mirrors reshare.Complaint to -// keep one adjudication path for both protocols. -// -// SenderID is the misbehaving party (the dealer in dkg2 parlance). -// ComplainerID is the recipient that observed the failure and signs. -// -// TranscriptHash binds the complaint to a specific dkg2 invocation -// (n, t, sessionID, suiteID). A complaint with a stale transcript hash -// is rejected. -// -// Evidence carries the protocol-specific data needed to re-check the -// claim without trusting the complainer: -// -// - ComplaintBadDelivery: serialize(share, blind, commits) for the -// (sender, recipient) pair. Any honest party can re-run -// VerifyShareAgainstCommits and confirm the mismatch. -// - ComplaintEquivocation: two signed commit-digest broadcasts from -// SenderID with disagreeing digests. -// - ComplaintMissing: empty (the absence is the evidence; a separate -// liveness round timestamps the deadline). -// - ComplaintMalformedCommit: serialize(commits) showing the -// malformed structure. -type Complaint struct { - TranscriptHash [32]byte - SenderID int // 0-indexed party ID of the misbehaving dealer - ComplainerID int // 0-indexed party ID of the complainer - Reason ComplaintReason - Evidence []byte // canonical-serialized evidence (see above) - Signature []byte // Ed25519 signature over Bytes() under ComplainerID's wire key - ComplainerKey ed25519.PublicKey -} - -// Bytes returns the canonical signed payload for a complaint. The -// Signature field is excluded (it is computed OVER these bytes). -// -// Format: -// -// "corona.dkg2.complaint.v1" || transcript || sender_id_be32 || -// complainer_id_be32 || reason_u8 || evidence_len_be32 || evidence -func (c *Complaint) Bytes() []byte { - var buf bytes.Buffer - buf.WriteString("corona.dkg2.complaint.v1") - buf.Write(c.TranscriptHash[:]) - var b4 [4]byte - binary.BigEndian.PutUint32(b4[:], uint32(c.SenderID)) - buf.Write(b4[:]) - binary.BigEndian.PutUint32(b4[:], uint32(c.ComplainerID)) - buf.Write(b4[:]) - buf.WriteByte(byte(c.Reason)) - binary.BigEndian.PutUint32(b4[:], uint32(len(c.Evidence))) - buf.Write(b4[:]) - buf.Write(c.Evidence) - return buf.Bytes() -} - -// Sign produces a complaint signature using the provided Ed25519 private -// key. Callers MUST use the wire-identity key associated with -// ComplainerID — using a different key produces a complaint that other -// validators reject. -func (c *Complaint) Sign(priv ed25519.PrivateKey) { - c.Signature = ed25519.Sign(priv, c.Bytes()) - c.ComplainerKey = priv.Public().(ed25519.PublicKey) -} - -// Verify checks the Ed25519 signature against ComplainerKey. Returns nil -// iff the signature is valid. Note: this does NOT verify that the -// reason actually holds (e.g. for ComplaintBadDelivery the evidence must -// be re-checked separately) — only that the complaint was signed by the -// claimed complainer. -func (c *Complaint) Verify() error { - if c == nil || len(c.Signature) == 0 || len(c.ComplainerKey) == 0 { - return errors.New("dkg2: complaint missing signature or key") - } - if !ed25519.Verify(c.ComplainerKey, c.Bytes(), c.Signature) { - return errors.New("dkg2: complaint signature invalid") - } - return nil -} - -// ComplaintHash returns a 32-byte digest over the complaint's canonical -// bytes (signature included), bound under the active HashSuite. Used to -// commit to the SET of complaints in the Round 2 transcript and the -// activation message. -// -// Pass nil for the production default (Corona-SHA3). -func ComplaintHash(suite hash.HashSuite, c *Complaint) [32]byte { - s := hash.Resolve(suite) - return s.TranscriptHash([]byte("corona.dkg2.complaint-hash.v1"), c.Bytes(), c.Signature) -} - -// DisqualificationThreshold returns the minimum number of distinct, -// validly-signed complaints needed to disqualify a sender. -// -// Default: t-1. Rationale: any single Byzantine validator can emit one -// false complaint to slow the protocol, but to disqualify an honest -// sender the adversary needs t-1 collaborators — which exceeds the -// static-corruption threshold of t-1 by exactly one. -func DisqualificationThreshold(threshold int) int { - if threshold <= 1 { - return 1 - } - return threshold - 1 -} - -// ComputeDisqualifiedSet takes a slice of validated complaints and -// returns the set of sender IDs that meet the disqualification -// threshold. Every honest party that processes the same complaint set -// returns the same disqualified set. -// -// "Validated" means: the complaint's signature has been verified, the -// complaint's transcript hash matches the local view, and (for -// ComplaintBadDelivery / ComplaintEquivocation / ComplaintMalformedCommit) -// the evidence has been re-checked. This function does NOT re-verify -// the underlying claim — the caller is responsible (see VerifyComplaint). -// -// Complaints are deduplicated by (sender, complainer) tuple; if the same -// complainer signs two complaints against the same sender (e.g. for -// different reasons), only one counts toward the threshold. -func ComputeDisqualifiedSet(complaints []*Complaint, threshold int) map[int]struct{} { - cap := DisqualificationThreshold(threshold) - seen := make(map[[2]int]bool) - count := make(map[int]int) - for _, c := range complaints { - key := [2]int{c.SenderID, c.ComplainerID} - if seen[key] { - continue - } - seen[key] = true - count[c.SenderID]++ - } - out := make(map[int]struct{}) - for sender, n := range count { - if n >= cap { - out[sender] = struct{}{} - } - } - return out -} - -// FilterQualifiedQuorum returns the survivor set of party IDs after -// removing disqualified senders. Returns ErrInsufficientQuorum if too -// many parties were disqualified for the protocol to recover. -// -// Sorted ascending for determinism — every honest party that processes -// the same disqualified set computes the same surviving slice. -func FilterQualifiedQuorum(originalQuorum []int, disqualified map[int]struct{}, threshold int) ([]int, error) { - out := make([]int, 0, len(originalQuorum)) - for _, id := range originalQuorum { - if _, dq := disqualified[id]; dq { - continue - } - out = append(out, id) - } - if len(out) < threshold { - return nil, fmt.Errorf("%w: %d survivors < threshold %d", - ErrInsufficientQuorum, len(out), threshold) - } - for i := 1; i < len(out); i++ { - for j := i; j > 0 && out[j-1] > out[j]; j-- { - out[j-1], out[j] = out[j], out[j-1] - } - } - return out, nil -} - -// ErrInsufficientQuorum signals that too many parties were disqualified -// for the DKG to recover. The activation cert binds the abort transcript -// and the chain stays at the previous epoch. -var ErrInsufficientQuorum = errors.New("dkg2: qualified quorum below threshold after disqualification") - -// NewBadDeliveryComplaint constructs an unsigned ComplaintBadDelivery -// against sender for the (share, blind, commits) triple that -// VerifyShareAgainstCommits rejected. The caller signs with their wire -// identity key via Complaint.Sign before broadcasting. -// -// transcriptHash binds the complaint to the active dkg2 invocation; -// pass the result of TranscriptHash on the canonical session inputs. -func NewBadDeliveryComplaint( - transcriptHash [32]byte, - senderID, complainerID int, - share, blind structs.Vector[ring.Poly], - commits []structs.Vector[ring.Poly], -) (*Complaint, error) { - evidence, err := serializeBadDeliveryEvidence(share, blind, commits) - if err != nil { - return nil, err - } - return &Complaint{ - TranscriptHash: transcriptHash, - SenderID: senderID, - ComplainerID: complainerID, - Reason: ComplaintBadDelivery, - Evidence: evidence, - }, nil -} - -// NewEquivocationComplaint constructs an unsigned ComplaintEquivocation -// against sender. evidenceA and evidenceB are two distinct, independently -// signed commit-digest broadcasts under sender's wire identity that -// disagree (Round 1.5 cross-party check). -func NewEquivocationComplaint( - transcriptHash [32]byte, - senderID, complainerID int, - evidenceA, evidenceB []byte, -) *Complaint { - var buf bytes.Buffer - var b4 [4]byte - binary.BigEndian.PutUint32(b4[:], uint32(len(evidenceA))) - buf.Write(b4[:]) - buf.Write(evidenceA) - binary.BigEndian.PutUint32(b4[:], uint32(len(evidenceB))) - buf.Write(b4[:]) - buf.Write(evidenceB) - return &Complaint{ - TranscriptHash: transcriptHash, - SenderID: senderID, - ComplainerID: complainerID, - Reason: ComplaintEquivocation, - Evidence: buf.Bytes(), - } -} - -// NewMissingComplaint constructs an unsigned ComplaintMissing against -// sender. Evidence is empty by design — the absence of a Round 1 -// delivery before the cohort deadline is the evidence; a separate -// liveness round timestamps the deadline. -func NewMissingComplaint( - transcriptHash [32]byte, - senderID, complainerID int, -) *Complaint { - return &Complaint{ - TranscriptHash: transcriptHash, - SenderID: senderID, - ComplainerID: complainerID, - Reason: ComplaintMissing, - Evidence: nil, - } -} - -// NewMalformedCommitComplaint constructs an unsigned -// ComplaintMalformedCommit against sender carrying the malformed commit -// vector as evidence. -func NewMalformedCommitComplaint( - transcriptHash [32]byte, - senderID, complainerID int, - commits []structs.Vector[ring.Poly], -) (*Complaint, error) { - evidence, err := serializeCommitsEvidence(commits) - if err != nil { - return nil, err - } - return &Complaint{ - TranscriptHash: transcriptHash, - SenderID: senderID, - ComplainerID: complainerID, - Reason: ComplaintMalformedCommit, - Evidence: evidence, - }, nil -} - -// serializeBadDeliveryEvidence packs (share, blind, commits) into the -// canonical evidence wire format consumed by VerifyComplaint. -// -// Format: -// -// share_bytes_len_be32 || share_bytes || -// blind_bytes_len_be32 || blind_bytes || -// t_be32 || (commits[k]_bytes_len_be32 || commits[k]_bytes) × t -func serializeBadDeliveryEvidence( - share, blind structs.Vector[ring.Poly], - commits []structs.Vector[ring.Poly], -) ([]byte, error) { - var buf bytes.Buffer - if err := writeVector(&buf, share); err != nil { - return nil, err - } - if err := writeVector(&buf, blind); err != nil { - return nil, err - } - var b4 [4]byte - binary.BigEndian.PutUint32(b4[:], uint32(len(commits))) - buf.Write(b4[:]) - for _, v := range commits { - if err := writeVector(&buf, v); err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} - -// serializeCommitsEvidence packs a commits vector into evidence bytes -// for ComplaintMalformedCommit. Format: -// -// t_be32 || (commits[k]_bytes_len_be32 || commits[k]_bytes) × t -func serializeCommitsEvidence(commits []structs.Vector[ring.Poly]) ([]byte, error) { - var buf bytes.Buffer - var b4 [4]byte - binary.BigEndian.PutUint32(b4[:], uint32(len(commits))) - buf.Write(b4[:]) - for _, v := range commits { - if err := writeVector(&buf, v); err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} - -// writeVector emits a length-prefixed serialization of one -// structs.Vector[ring.Poly] using the lattigo WriteTo wire format. -func writeVector(buf *bytes.Buffer, v structs.Vector[ring.Poly]) error { - var inner bytes.Buffer - if _, err := v.WriteTo(&inner); err != nil { - return fmt.Errorf("%w: %v", ErrSerialization, err) - } - var b4 [4]byte - binary.BigEndian.PutUint32(b4[:], uint32(inner.Len())) - buf.Write(b4[:]) - buf.Write(inner.Bytes()) - return nil -} diff --git a/dkg2/dkg2.go b/dkg2/dkg2.go deleted file mode 100644 index ae379fc..0000000 --- a/dkg2/dkg2.go +++ /dev/null @@ -1,768 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package dkg2 implements a Pedersen-style verifiable-secret-sharing-based -// distributed key generation over the Corona polynomial ring -// R_q = Z_q[X]/(X^256 + 1). -// -// dkg2 is the production parallel-track keygen for Corona. It replaces the -// pseudoinverse-recoverable Feldman commit C_k = A · NTT(c_k) of the -// upstream Corona DKG with a Pedersen commit -// -// C_{i,k} = A · NTT(c_{i,k}) + B · NTT(r_{i,k}) (R_q^M, NTT-Mont) -// -// where A, B are independent uniform public matrices derived deterministically -// from nothing-up-my-sleeve domain-separation tags. Hiding holds under -// decisional MLWE on B; binding holds under MSIS on the wide concatenation -// [A | B]. Formal statements live in -// papers/lp-073-pulsar/sections/07-pedersen-dkg.tex; Lean theorem references -// are in proofs/lean/Crypto/Corona/dkg2.lean. -// -// # Round structure -// -// Round 1 Each party samples Gaussian f_i, g_i in R_q^Nvec[x]_{deg 0 and < total parties") - ErrInvalidPartyCount = errors.New("dkg2: need at least 2 parties") - ErrInvalidPartyID = errors.New("dkg2: party ID out of range") - ErrShareVerification = errors.New("dkg2: share verification failed") - ErrMissingData = errors.New("dkg2: missing share, blind, or commitment data") - ErrCommitMismatch = errors.New("dkg2: cross-party commitment digest mismatch") - ErrMalformedCommit = errors.New("dkg2: commit vector malformed") - ErrSerialization = errors.New("dkg2: commit serialization failed") -) - -// Params holds ring parameters for the DKG protocol. -// -// Identical to dkg.Params; reused so existing code paths (sign.Q-derived -// rings) match without per-package divergence. -type Params struct { - R *ring.Ring - RXi *ring.Ring -} - -// NewParams creates ring parameters for dkg2. -// -// Mirrors dkg.NewParams: the RXi ring is the post-rounding modulus -// (Xi = 30 bits, sign.QXi = 0x40000 = 2^18) — not prime, but that's the -// canonical Corona layout used by sign.Gen and the Round2 b_ped output. -// ring.NewRing returns a non-prime-modulus error here that we deliberately -// ignore (matches dkg/dkg.go:53), and the constructor remains usable -// because RoundVector only needs the ring as a coefficient container. -func NewParams() (*Params, error) { - r, err := ring.NewRing(1<= n { - return nil, ErrInvalidThreshold - } - if partyID < 0 || partyID >= n { - return nil, ErrInvalidPartyID - } - - // Set globals consumed by the Sign package (mirrors dkg/dkg.go). - sign.K = n - sign.Threshold = t - - A := DeriveA(params.R) - B := DeriveB(params.R) - - return &DKGSession{ - params: params, - partyID: partyID, - n: n, - t: t, - suite: hash.Resolve(suite), - A: A, - B: B, - }, nil -} - -// APublic returns the public matrix A. Exposed for KAT pinning. -func (d *DKGSession) APublic() structs.Matrix[ring.Poly] { return d.A } - -// BPublic returns the public matrix B. Exposed for KAT pinning. -func (d *DKGSession) BPublic() structs.Matrix[ring.Poly] { return d.B } - -// PartyID returns the party ID for this session. -func (d *DKGSession) PartyID() int { return d.partyID } - -// N returns the total party count. -func (d *DKGSession) N() int { return d.n } - -// T returns the threshold. -func (d *DKGSession) T() int { return d.t } - -// HashSuite returns the cohort-bound hash suite this session uses. -func (d *DKGSession) HashSuite() hash.HashSuite { return d.suite } - -// Round1 generates the party's random polynomials f_i, g_i, computes -// commitments, and computes shares (and blinds) for all other parties. -// -// Uses crypto/rand for the per-party Gaussian PRNG seed. For deterministic -// testing / KAT generation, use Round1WithSeed. -func (d *DKGSession) Round1() (*Round1Output, error) { - seed := make([]byte, sign.KeySize) - if _, err := io.ReadFull(rand.Reader, seed); err != nil { - return nil, fmt.Errorf("dkg2: random read: %w", err) - } - return d.Round1WithSeed(seed) -} - -// Round1WithSeed is the deterministic variant of Round1. Same seed → -// byte-equal output for a given (partyID, n, t, A, B) tuple. -// -// Sampling order (BYTE-PINNED — must match the C++ port): -// -// 1. KeyedPRNG(seed) → Gaussian sampler (σ_E, β_E). -// 2. For k = 0..t-1: sample c_{i,k} (Nvec polys, standard form). -// 3. For k = 0..t-1: sample r_{i,k} (Nvec polys, standard form). -// 4. Compute commits[k] = A*NTT(c_k) + B*NTT(r_k). -// 5. Compute shares[j], blinds[j] via Horner over the (j+1) point. -// -// The two-pass sample order (all c's first, then all r's) matters for byte -// equality across the Go reference and the C++ port. -func (d *DKGSession) Round1WithSeed(seed []byte) (*Round1Output, error) { - if len(seed) != sign.KeySize { - return nil, fmt.Errorf("dkg2: Round1WithSeed: expected %d-byte seed, got %d", sign.KeySize, len(seed)) - } - r := d.params.R - - prng, err := sampling.NewKeyedPRNG(seed) - if err != nil { - return nil, err - } - gauss := ring.NewGaussianSampler(prng, r, - ring.DiscreteGaussian{Sigma: sign.SigmaE, Bound: sign.BoundE}, false) - - // Step 2: sample t coefficient vectors c_{i,k} in standard form. - d.cCoeffs = make([]structs.Vector[ring.Poly], d.t) - for k := 0; k < d.t; k++ { - d.cCoeffs[k] = utils.SamplePolyVector(r, sign.N, gauss, false, false) - } - - // Step 3: sample t coefficient vectors r_{i,k} in standard form. - d.rCoeffs = make([]structs.Vector[ring.Poly], d.t) - for k := 0; k < d.t; k++ { - d.rCoeffs[k] = utils.SamplePolyVector(r, sign.N, gauss, false, false) - } - - // Step 4: build Pedersen commits C_k = A·NTT(c_k) + B·NTT(r_k). - // - // Routed through the backend-dispatched path (dkg2_gpu.go). The CPU - // leg is byte-identical to the historical inline loop; the parallel - // leg fans out independent k iterations across runtime.GOMAXPROCS - // workers. Output bytes are unchanged because the ring routines are - // deterministic per (input, output) pair and there are no shared - // writes across goroutines (each worker owns its commits[k] cell). - commits := computePedersenCommits(r, d.A, d.B, d.cCoeffs, d.rCoeffs) - - // Step 5: build shares and blinds via Horner over (j+1) for each j. - q := new(big.Int).SetUint64(sign.Q) - shares, blinds := computeHornerShares(r, d.cCoeffs, d.rCoeffs, d.n, - func(rr *ring.Ring, coeffs []structs.Vector[ring.Poly], j int) structs.Vector[ring.Poly] { - x := big.NewInt(int64(j + 1)) - return hornerEval(rr, coeffs, x, q) - }) - - return &Round1Output{ - Commits: commits, - Shares: shares, - Blinds: blinds, - }, nil -} - -// hornerEval computes f(x) = Σ_k coeffs[k] * x^k in standard coefficient -// form over R_q^Nvec. Horner's method: f(x) = c_0 + x·(c_1 + x·(c_2 + …)). -// -// The arithmetic is performed by big.Int per coefficient mod q; the -// resulting Vector[Poly] is returned in standard coefficient form. -func hornerEval(r *ring.Ring, coeffs []structs.Vector[ring.Poly], x, q *big.Int) structs.Vector[ring.Poly] { - t := len(coeffs) - result := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - result[vi] = r.NewPoly() - } - for k := t - 1; k >= 0; k-- { - for vi := 0; vi < sign.N; vi++ { - if k < t-1 { - polyMulScalar(r, result[vi], x, q) - } - polyAddCoeffwise(r, result[vi], coeffs[k][vi], q) - } - } - return result -} - -// VerifyShareAgainstCommits checks the Pedersen identity -// -// A · NTT(share) + B · NTT(blind) ?= Σ_{k=0..t-1} (recipientID+1)^k · commits[k] -// -// in NTT-Montgomery form (mod sign.Q). Returns (true, nil) on a valid pair, -// (false, ErrShareVerification) on a Pedersen mismatch, and a wrapped -// ErrMissingData / ErrMalformedCommit when inputs are absent or malformed. -// -// Comparison is constant-time across all M·N coefficient slots — no -// short-circuit on the first mismatched slot, which matches the response to -// Findings 5/6 of luxcpp/crypto/corona/RED-DKG-REVIEW.md. -// -// recipientID is 0-indexed (the (j+1) shift to the Lagrange evaluation -// point is applied internally). -func VerifyShareAgainstCommits( - params *Params, - A, B structs.Matrix[ring.Poly], - share, blind structs.Vector[ring.Poly], - commits []structs.Vector[ring.Poly], - recipientID int, - threshold int, -) (bool, error) { - r := params.R - if len(share) != sign.N || len(blind) != sign.N { - return false, fmt.Errorf("%w: share/blind length mismatch", ErrMalformedCommit) - } - if len(commits) != threshold { - return false, fmt.Errorf("%w: %d commits, expected %d", ErrMalformedCommit, len(commits), threshold) - } - for k, v := range commits { - if len(v) != sign.M { - return false, fmt.Errorf("%w: commit[%d] dim %d, expected %d", ErrMalformedCommit, k, len(v), sign.M) - } - } - - q := new(big.Int).SetUint64(sign.Q) - - // LHS_share = A · NTT(share) - shareNTT := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - shareNTT[vi] = *share[vi].CopyNew() - r.NTT(shareNTT[vi], shareNTT[vi]) - } - ash := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, A, shareNTT, ash) - - // LHS_blind = B · NTT(blind) - blindNTT := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - blindNTT[vi] = *blind[vi].CopyNew() - r.NTT(blindNTT[vi], blindNTT[vi]) - } - bbl := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, B, blindNTT, bbl) - - lhs := utils.InitializeVector(r, sign.M) - utils.VectorAdd(r, ash, bbl, lhs) - - // RHS = Σ_k (recipientID+1)^k · commits[k] via Horner in NTT domain. - x := big.NewInt(int64(recipientID + 1)) - rhs := utils.InitializeVector(r, sign.M) - for k := threshold - 1; k >= 0; k-- { - if k < threshold-1 { - for ri := 0; ri < sign.M; ri++ { - polyMulScalarNTT(r, rhs[ri], x, q) - } - } - utils.VectorAdd(r, rhs, commits[k], rhs) - } - - // Constant-time compare across all M slots, all coefficient levels. - // utils.ConstantTimePolyEqual returns 1 iff equal; AND across slots. - // This is the dkg2 response to Findings 5/6 of - // luxcpp/crypto/corona/RED-DKG-REVIEW.md, which note that the upstream - // dkg/ share comparison short-circuits on the first slot mismatch and - // so leaks the location of any planted divergence to a timing observer. - eq := 1 - for ri := 0; ri < sign.M; ri++ { - eq &= utils.ConstantTimePolyEqual(lhs[ri], rhs[ri]) - } - if eq != 1 { - return false, ErrShareVerification - } - return true, nil -} - -// Round2 verifies received shares (and blinds) against commitments, then -// aggregates to (s_j, u_j) plus the Pedersen-shaped group public key. -// -// On a verification failure Round2 returns the global ErrShareVerification -// without identifying the offending sender. For identifiable abort use -// Round2Identify, which returns the failing sender ID and produces a -// signed Complaint suitable for slashing evidence. -// -// receivedShares maps sender i → share i computed for THIS party (j). -// receivedBlinds maps sender i → blind i computed for THIS party (j). -// receivedCommits maps sender i → that sender's t-element commit vector. -// -// Returns (s_j, u_j, b_ped) on success. -// -// s_j = Σ_i share_{i→j} (Corona secret share) -// u_j = Σ_i blind_{i→j} (private; discarded by Corona Sign callers) -// b_ped = Σ_i C_{i,0} (rounded to Xi, Pedersen-shaped pk) -// -// b_ped has shape Round_Xi(A·s + B·t_master). Corona Sign verification -// running in 2-secret mode (path (b)) takes (A, B, b_ped) jointly; see -// papers/lp-073-pulsar/sections/07-pedersen-dkg.tex §Mapping for the -// integration recipe. -func (d *DKGSession) Round2( - receivedShares map[int]structs.Vector[ring.Poly], - receivedBlinds map[int]structs.Vector[ring.Poly], - receivedCommits map[int][]structs.Vector[ring.Poly], -) (structs.Vector[ring.Poly], structs.Vector[ring.Poly], structs.Vector[ring.Poly], error) { - s, u, b, _, err := d.round2Internal(receivedShares, receivedBlinds, receivedCommits, false) - return s, u, b, err -} - -// Round2Identify is the identifiable-abort variant of Round2. On success -// returns (s_j, u_j, b_ped, -1, nil). On a Pedersen-mismatch failure -// returns (nil, nil, nil, senderID, wrapped ErrShareVerification) where -// senderID is the first sender in iteration order whose share/blind -// fails the verification. The caller may immediately produce a signed -// ComplaintBadDelivery for that sender via NewBadDeliveryComplaint. -// -// On a missing-input failure returns (nil, nil, nil, senderID, -// wrapped ErrMissingData) where senderID names the absent sender. -func (d *DKGSession) Round2Identify( - receivedShares map[int]structs.Vector[ring.Poly], - receivedBlinds map[int]structs.Vector[ring.Poly], - receivedCommits map[int][]structs.Vector[ring.Poly], -) (structs.Vector[ring.Poly], structs.Vector[ring.Poly], structs.Vector[ring.Poly], int, error) { - return d.round2Internal(receivedShares, receivedBlinds, receivedCommits, true) -} - -// round2Internal is the shared implementation behind Round2 and -// Round2Identify. When identify=true the senderID return value names the -// first failing sender on error; otherwise it returns -1. -func (d *DKGSession) round2Internal( - receivedShares map[int]structs.Vector[ring.Poly], - receivedBlinds map[int]structs.Vector[ring.Poly], - receivedCommits map[int][]structs.Vector[ring.Poly], - identify bool, -) (structs.Vector[ring.Poly], structs.Vector[ring.Poly], structs.Vector[ring.Poly], int, error) { - r := d.params.R - - // Sanity: every party 0..n-1 must contribute share, blind, commits. - for i := 0; i < d.n; i++ { - if _, ok := receivedShares[i]; !ok { - id := -1 - if identify { - id = i - } - return nil, nil, nil, id, fmt.Errorf("%w: missing share from party %d", ErrMissingData, i) - } - if _, ok := receivedBlinds[i]; !ok { - id := -1 - if identify { - id = i - } - return nil, nil, nil, id, fmt.Errorf("%w: missing blind from party %d", ErrMissingData, i) - } - if _, ok := receivedCommits[i]; !ok { - id := -1 - if identify { - id = i - } - return nil, nil, nil, id, fmt.Errorf("%w: missing commitment from party %d", ErrMissingData, i) - } - } - - q := new(big.Int).SetUint64(sign.Q) - - // Verification: A·NTT(share_i) + B·NTT(blind_i) ?= Σ_k (j+1)^k · C_{i,k} - for i := 0; i < d.n; i++ { - ok, err := VerifyShareAgainstCommits( - d.params, d.A, d.B, - receivedShares[i], receivedBlinds[i], receivedCommits[i], - d.partyID, d.t, - ) - if err != nil { - id := -1 - if identify { - id = i - } - if errors.Is(err, ErrMalformedCommit) { - return nil, nil, nil, id, fmt.Errorf("party %d commit malformed: %w", i, err) - } - return nil, nil, nil, id, fmt.Errorf("%w: party %d share/blind do not match commitment", ErrShareVerification, i) - } - if !ok { - // Defensive: VerifyShareAgainstCommits returns (false, err) in - // lockstep, so this branch is unreachable. Keep for clarity. - id := -1 - if identify { - id = i - } - return nil, nil, nil, id, fmt.Errorf("%w: party %d", ErrShareVerification, i) - } - } - - // Aggregate s_j = Σ_i share_{i→j} (coefficient-domain add). - s := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - s[vi] = r.NewPoly() - } - for i := 0; i < d.n; i++ { - for vi := 0; vi < sign.N; vi++ { - polyAddCoeffwise(r, s[vi], receivedShares[i][vi], q) - } - } - - // Aggregate u_j = Σ_i blind_{i→j} (coefficient-domain add). - u := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - u[vi] = r.NewPoly() - } - for i := 0; i < d.n; i++ { - for vi := 0; vi < sign.N; vi++ { - polyAddCoeffwise(r, u[vi], receivedBlinds[i][vi], q) - } - } - - // Public key: b_ped = Round_Xi(IMForm + INTT(Σ_i C_{i,0})). - pkNTT := utils.InitializeVector(r, sign.M) - for i := 0; i < d.n; i++ { - utils.VectorAdd(r, pkNTT, receivedCommits[i][0], pkNTT) - } - utils.ConvertVectorFromNTT(r, pkNTT) - bPed := utils.RoundVector(r, d.params.RXi, pkNTT, sign.Xi) - - return s, u, bPed, -1, nil -} - -// AggregateUnroundedCommit returns Σ_i C_{i,0} in NTT-Mont form (no -// rounding, no IMForm/INTT). Used by sign-after-DKG path (b) integration -// where the verifier needs the *unrounded* Pedersen commitment to recompute -// the verification equation. -func AggregateUnroundedCommit( - params *Params, - commits map[int][]structs.Vector[ring.Poly], - n int, -) (structs.Vector[ring.Poly], error) { - r := params.R - for i := 0; i < n; i++ { - if _, ok := commits[i]; !ok { - return nil, fmt.Errorf("%w: missing commitment from party %d", ErrMissingData, i) - } - } - pkNTT := utils.InitializeVector(r, sign.M) - for i := 0; i < n; i++ { - utils.VectorAdd(r, pkNTT, commits[i][0], pkNTT) - } - return pkNTT, nil -} - -// polyMulScalar multiplies each coefficient of p by scalar s mod q -// (coefficient domain). Mirrors dkg.polyMulScalar. -func polyMulScalar(r *ring.Ring, p ring.Poly, s, q *big.Int) { - degree := r.N() - for i := 0; i < degree; i++ { - if p.Coeffs[0] == nil { - return - } - val := new(big.Int).SetUint64(p.Coeffs[0][i]) - val.Mul(val, s) - val.Mod(val, q) - p.Coeffs[0][i] = val.Uint64() - } -} - -// polyAddCoeffwise adds b into a coefficient-wise mod q (coefficient domain). -func polyAddCoeffwise(r *ring.Ring, a, b ring.Poly, q *big.Int) { - degree := r.N() - if a.Coeffs[0] == nil { - a.Coeffs[0] = make([]uint64, degree) - } - bCoeffs := b.Coeffs[0] - if bCoeffs == nil { - return - } - for i := 0; i < degree; i++ { - val := new(big.Int).SetUint64(a.Coeffs[0][i]) - val.Add(val, new(big.Int).SetUint64(bCoeffs[i])) - val.Mod(val, q) - a.Coeffs[0][i] = val.Uint64() - } -} - -// polyMulScalarNTT multiplies each NTT coefficient of p by scalar s mod q. -// Mirrors dkg.polyMulScalarNTT. -func polyMulScalarNTT(r *ring.Ring, p ring.Poly, s, q *big.Int) { - degree := r.N() - for level := range p.Coeffs { - for i := 0; i < degree; i++ { - val := new(big.Int).SetUint64(p.Coeffs[level][i]) - val.Mul(val, s) - val.Mod(val, q) - p.Coeffs[level][i] = val.Uint64() - } - } -} - -// EncodeUint32BE writes x in big-endian into a 4-byte slice. Used by the -// KAT oracle; declared here so the C++ port can match seed-derivation -// without depending on the oracle binary. -func EncodeUint32BE(x uint32) []byte { - var buf [4]byte - binary.BigEndian.PutUint32(buf[:], x) - return buf[:] -} diff --git a/dkg2/dkg2_gpu.go b/dkg2/dkg2_gpu.go deleted file mode 100644 index b873216..0000000 --- a/dkg2/dkg2_gpu.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dkg2 - -// dkg2_gpu.go — GPU-dispatched parallel compute for the Pedersen DKG -// Round 1 hot path. -// -// The R_q^k Pedersen commit construction (dkg2.go Round1WithSeed Step 4) -// and the Horner share evaluation (Step 5) are both embarrassingly -// parallel across the per-coefficient and per-recipient axes: -// -// Step 4: commits[k] = A·NTT(c_k) + B·NTT(r_k) -// - The NTT calls (2·t·N) are independent across k. -// - The MatrixVectorMul calls (2·t) are independent across k. -// - The VectorAdd calls (t) write to disjoint commits[k] cells. -// => Parallel across the k axis (length t = threshold). -// -// Step 5: shares[j] = hornerEval(c_coeffs, j+1) -// blinds[j] = hornerEval(r_coeffs, j+1) -// - Each j is independent (reads coeffs, writes shares[j]/blinds[j]). -// => Parallel across the j axis (length n = committee size). -// -// Byte-equality contract. -// -// The Gaussian sampling step (Steps 2-3) consumes a deterministic -// KeyedPRNG stream in a fixed order; we DO NOT touch that sampling path -// because changing read order changes byte output and breaks the C++ -// cross-runtime KAT. Steps 4 and 5 are pure functions of the already- -// sampled coefficient vectors; their outputs are byte-equal regardless -// of the goroutine scheduling because: -// -// - NTT is a deterministic linear transform of the coefficient vector. -// - MatrixVectorMul is a sum of MulCoeffsMontgomeryThenAdd ops; the -// ring routines are deterministic and the per-output-slot reduction -// order is fixed inside MatrixVectorMul. -// - VectorAdd writes element-wise, no cross-element reductions. -// - Horner-method polynomial evaluation is a fixed sequence of polyMul -// + polyAdd per `vi` slot, independent across `j`. -// -// Goroutine scheduling determines the WALL-CLOCK order of writes to -// different cells; it does not change the value written to any one -// cell. -// -// Backend selection. -// -// Same dispatch policy as the pulsar/dkg path: a build-tag flips -// dkg2GPUEnabled; the runtime selector consults GOMAXPROCS and the -// input shape (n, t). The actual luxfi/accel session (Metal/CUDA) is -// reserved for the engine layer; the in-package "GPU" today is the Go -// goroutine fan-out across independent work axes. The dispatch hook -// remains in place for the v0.6+ NIST submission lift where the accel -// MatrixVectorMul kernel becomes available. - -import ( - "runtime" - "sync" - - "github.com/luxfi/corona/sign" - "github.com/luxfi/corona/utils" - "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/structs" -) - -// dkg2ComputeBackend identifies the active compute backend. -type dkg2ComputeBackend uint8 - -const ( - // kDKG2BackendCPU is the single-threaded reference path. Byte-equal to - // the historical dkg2.go Round1WithSeed. - kDKG2BackendCPU dkg2ComputeBackend = iota - - // kDKG2BackendParallel runs the per-coefficient and per-recipient - // fan-out across runtime.GOMAXPROCS workers. - kDKG2BackendParallel -) - -// dkg2GPUEnabled flips between CPU-only and the parallel fan-out path. -// Set by the build-tagged init() files (dkg2_gpu_accel.go for `-tags gpu`, -// dkg2_gpu_default.go for everything else). -var dkg2GPUEnabled bool - -// SetDKG2GPUForTest forces the dispatch backend. Returns the previous -// value so tests can restore it. Test-only. -func SetDKG2GPUForTest(on bool) bool { - prev := dkg2GPUEnabled - dkg2GPUEnabled = on - return prev -} - -// DKG2GPUDispatchAvailable reports whether the GPU dispatch path is wired -// in this build. -func DKG2GPUDispatchAvailable() bool { - return dkg2GPUEnabled -} - -// resolveDKG2Backend selects the per-call backend. -// -// The Pedersen DKG Round 1 hot path is heavy enough at production sizes -// (n=21, t=14 → 196 NTT calls, 1568 MulCoeffsMontgomeryThenAdd ops, -// 2058 Horner steps) that goroutine setup is fully amortised. The -// threshold below uses NTT-call count as the shape proxy: 2·t·N where -// N is the ring degree dimension (sign.N = Nvec = 7) for the dkg2 ring. -func resolveDKG2Backend(n, t int) dkg2ComputeBackend { - if !dkg2GPUEnabled { - return kDKG2BackendCPU - } - if runtime.GOMAXPROCS(0) < 2 { - return kDKG2BackendCPU - } - // 2·t·sign.N independent NTT calls plus t MatrixVectorMul plus - // 2·n Horner chains. Below this threshold the goroutine setup - // dwarfs the work. Empirically tuned on Apple M1 Max — the - // crossover sits between n=3, t=2 (38 NTTs, 14 ops total) and - // n=5, t=3 (70 NTTs). We pin at 60 NTT-calls. - const kDKG2ParallelMinNTTs = 60 - if 2*t*sign.N < kDKG2ParallelMinNTTs { - return kDKG2BackendCPU - } - return kDKG2BackendParallel -} - -// computePedersenCommits builds commits[k] = A·NTT(c_k) + B·NTT(r_k) for -// k ∈ [0, t). The CPU leg is byte-identical to the inline Step 4 loop in -// dkg2.go; the parallel leg fans out across the k axis. -// -// Caller owns cCoeffs and rCoeffs (standard form, not NTT) and must NOT -// mutate them — this function CopyNews before transforming. -func computePedersenCommits( - r *ring.Ring, - A, B structs.Matrix[ring.Poly], - cCoeffs, rCoeffs []structs.Vector[ring.Poly], -) []structs.Vector[ring.Poly] { - t := len(cCoeffs) - commits := make([]structs.Vector[ring.Poly], t) - - switch resolveDKG2Backend(0 /*n unused for commits*/, t) { - case kDKG2BackendParallel: - // Fan-out across the (kind ∈ {A·c, B·r}) × k product. That gives - // 2·t independent half-commits (each contributing one Vector[Poly] - // summand to commits[k]). Workers write to disjoint slots; a - // single-threaded VectorAdd folds the pairs together at the end, - // keeping the addition order byte-deterministic. - acSlots := make([]structs.Vector[ring.Poly], t) - brSlots := make([]structs.Vector[ring.Poly], t) - type halfJob struct { - isB bool - k int - } - jobs := make([]halfJob, 0, 2*t) - for k := 0; k < t; k++ { - jobs = append(jobs, halfJob{isB: false, k: k}) - jobs = append(jobs, halfJob{isB: true, k: k}) - } - workers := runtime.GOMAXPROCS(0) - if workers > len(jobs) { - workers = len(jobs) - } - var wg sync.WaitGroup - wg.Add(workers) - for w := 0; w < workers; w++ { - go func(w int) { - defer wg.Done() - for idx := w; idx < len(jobs); idx += workers { - jb := jobs[idx] - if jb.isB { - brSlots[jb.k] = mulMatByNTT(r, B, rCoeffs[jb.k]) - } else { - acSlots[jb.k] = mulMatByNTT(r, A, cCoeffs[jb.k]) - } - } - }(w) - } - wg.Wait() - // Fold the half-commits in single-threaded VectorAdd order; this - // is the same addition order the CPU leg uses (ac + br per k), - // so the byte output is identical. - for k := 0; k < t; k++ { - commits[k] = utils.InitializeVector(r, sign.M) - utils.VectorAdd(r, acSlots[k], brSlots[k], commits[k]) - } - default: - for k := 0; k < t; k++ { - commits[k] = singleCommit(r, A, B, cCoeffs[k], rCoeffs[k]) - } - } - return commits -} - -// mulMatByNTT computes M · NTT(coeffs) and returns the result. The -// CopyNew + NTT pair matches the historical inline loop (dkg2.go Step 4 -// lines 415-419) so the output is byte-equal. -func mulMatByNTT(r *ring.Ring, M structs.Matrix[ring.Poly], coeffs structs.Vector[ring.Poly]) structs.Vector[ring.Poly] { - nttVec := make(structs.Vector[ring.Poly], sign.N) - for i := 0; i < sign.N; i++ { - nttVec[i] = *coeffs[i].CopyNew() - r.NTT(nttVec[i], nttVec[i]) - } - out := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, M, nttVec, out) - return out -} - -// singleCommit is the per-k inner kernel: NTT the coefficients, multiply -// by A and B, sum. Byte-identical to the inline loop body in dkg2.go -// Step 4 lines 411-426. -func singleCommit( - r *ring.Ring, - A, B structs.Matrix[ring.Poly], - cCoeffsK, rCoeffsK structs.Vector[ring.Poly], -) structs.Vector[ring.Poly] { - cNTT := make(structs.Vector[ring.Poly], sign.N) - rNTT := make(structs.Vector[ring.Poly], sign.N) - for i := 0; i < sign.N; i++ { - cNTT[i] = *cCoeffsK[i].CopyNew() - r.NTT(cNTT[i], cNTT[i]) - rNTT[i] = *rCoeffsK[i].CopyNew() - r.NTT(rNTT[i], rNTT[i]) - } - ac := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, A, cNTT, ac) - br := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, B, rNTT, br) - out := utils.InitializeVector(r, sign.M) - utils.VectorAdd(r, ac, br, out) - return out -} - -// computeHornerShares evaluates f_i(j+1) and g_i(j+1) for j ∈ [0, n). The -// CPU leg matches the inline Step 5 loop. The parallel leg fans out across -// the j axis: each goroutine owns its shares[j] and blinds[j] cells. -// -// hornerEvalFn is injected so the package's existing hornerEval stays the -// only big.Int / polyMulScalar / polyAddCoeffwise call site (the same -// invariant the audit footprint requires). -func computeHornerShares( - r *ring.Ring, - cCoeffs, rCoeffs []structs.Vector[ring.Poly], - n int, - hornerEvalFn func(r *ring.Ring, coeffs []structs.Vector[ring.Poly], j int) structs.Vector[ring.Poly], -) (shares, blinds map[int]structs.Vector[ring.Poly]) { - shares = make(map[int]structs.Vector[ring.Poly], n) - blinds = make(map[int]structs.Vector[ring.Poly], n) - - switch resolveDKG2Backend(n, len(cCoeffs)) { - case kDKG2BackendParallel: - // Pre-allocate slot arrays so workers write to distinct cells - // without needing a synchronized map. The map[] copy at the end - // is a single-threaded fold and stays byte-deterministic. - shareSlots := make([]structs.Vector[ring.Poly], n) - blindSlots := make([]structs.Vector[ring.Poly], n) - - // Fan out across the (kind ∈ {share, blind}) × j product. That - // gives 2·n independent work items so on a 10-core box at n=14 - // we keep every core busy (28 items / 10 workers = ~3 items - // each). On smaller n (3, 5, 7) the 2·n product still gives - // enough cells to amortise the fan-out cost. - type job struct { - isBlind bool - j int - } - jobs := make([]job, 0, 2*n) - for j := 0; j < n; j++ { - jobs = append(jobs, job{isBlind: false, j: j}) - jobs = append(jobs, job{isBlind: true, j: j}) - } - - workers := runtime.GOMAXPROCS(0) - if workers > len(jobs) { - workers = len(jobs) - } - var wg sync.WaitGroup - wg.Add(workers) - for w := 0; w < workers; w++ { - go func(w int) { - defer wg.Done() - for idx := w; idx < len(jobs); idx += workers { - jb := jobs[idx] - if jb.isBlind { - blindSlots[jb.j] = hornerEvalFn(r, rCoeffs, jb.j) - } else { - shareSlots[jb.j] = hornerEvalFn(r, cCoeffs, jb.j) - } - } - }(w) - } - wg.Wait() - // Single-threaded fold into map form (matches existing return shape). - for j := 0; j < n; j++ { - shares[j] = shareSlots[j] - blinds[j] = blindSlots[j] - } - default: - for j := 0; j < n; j++ { - shares[j] = hornerEvalFn(r, cCoeffs, j) - blinds[j] = hornerEvalFn(r, rCoeffs, j) - } - } - return shares, blinds -} diff --git a/dkg2/dkg2_gpu_test.go b/dkg2/dkg2_gpu_test.go deleted file mode 100644 index 25963ad..0000000 --- a/dkg2/dkg2_gpu_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dkg2 - -import ( - "bytes" - "crypto/rand" - "io" - "testing" - - "github.com/luxfi/corona/sign" - "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/structs" -) - -// TestDKG2_GPU_ByteEqual is the core correctness theorem: the CPU and -// GPU dispatch legs of Round 1 MUST produce byte-identical output for -// the same RNG seed. A divergence here would be a cross-validator KAT -// failure during a live Pedersen DKG and would halt the consensus path -// the ceremony bootstraps. -// -// We exercise the same (n, t) shapes the canonical KATs cover (2-of-3, -// 3-of-5, 5-of-7, 7-of-11) plus the production target n=21, t=14. -func TestDKG2_GPU_ByteEqual(t *testing.T) { - for _, tc := range []struct { - name string - n, t int - }{ - {"n3_t2", 3, 2}, - {"n5_t3", 5, 3}, - {"n7_t5", 7, 5}, - {"n11_t7", 11, 7}, - {"n21_t14", 21, 14}, - } { - tc := tc - t.Run(tc.name, func(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatal(err) - } - - // Same seed for both legs — byte-equality requires the - // same input. - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(0x5A ^ (i * 31) ^ tc.n ^ (tc.t << 1)) - } - - runLeg := func(gpuOn bool) *Round1Output { - prev := SetDKG2GPUForTest(gpuOn) - defer SetDKG2GPUForTest(prev) - sess, err := NewDKGSession(params, 0, tc.n, tc.t, nil) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1WithSeed gpu=%v: %v", gpuOn, err) - } - return out - } - - cpu := runLeg(false) - gpu := runLeg(true) - - // Commits: byte-equal across both legs. - if len(cpu.Commits) != len(gpu.Commits) { - t.Fatalf("commit count: cpu=%d gpu=%d", len(cpu.Commits), len(gpu.Commits)) - } - cpuBytes, err := cpu.SerializeCommits() - if err != nil { - t.Fatal(err) - } - gpuBytes, err := gpu.SerializeCommits() - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(cpuBytes, gpuBytes) { - t.Fatalf("commit bytes mismatch: cpu_len=%d gpu_len=%d head_cpu=%x head_gpu=%x", - len(cpuBytes), len(gpuBytes), cpuBytes[:32], gpuBytes[:32]) - } - - // Shares and blinds: byte-equal per recipient. - if len(cpu.Shares) != len(gpu.Shares) || len(cpu.Blinds) != len(gpu.Blinds) { - t.Fatalf("share/blind map size mismatch") - } - for j := 0; j < tc.n; j++ { - if !vectorBytesEqual(cpu.Shares[j], gpu.Shares[j]) { - t.Fatalf("share[%d] bytes mismatch", j) - } - if !vectorBytesEqual(cpu.Blinds[j], gpu.Blinds[j]) { - t.Fatalf("blind[%d] bytes mismatch", j) - } - } - }) - } -} - -// vectorBytesEqual returns true if two Vector[Poly] serialise byte-equal. -func vectorBytesEqual(a, b structs.Vector[ring.Poly]) bool { - if len(a) != len(b) { - return false - } - var bufA, bufB bytes.Buffer - if _, err := a.WriteTo(&bufA); err != nil { - return false - } - if _, err := b.WriteTo(&bufB); err != nil { - return false - } - return bytes.Equal(bufA.Bytes(), bufB.Bytes()) -} - -// TestDKG2_GPU_RoundTripVerify confirms the GPU-produced shares verify -// against the GPU-produced commits via the existing Pedersen identity -// check. This is the cryptographic correctness gate (independent of the -// byte-equal test): even if the CPU and GPU legs produced different -// outputs, both would have to verify under their own commits — and a -// share that satisfies the Pedersen identity is a valid Pedersen-DKG -// contribution by construction. -func TestDKG2_GPU_RoundTripVerify(t *testing.T) { - prev := SetDKG2GPUForTest(true) - defer SetDKG2GPUForTest(prev) - - params, err := NewParams() - if err != nil { - t.Fatal(err) - } - const n, threshold = 7, 5 - sess, err := NewDKGSession(params, 0, n, threshold, nil) - if err != nil { - t.Fatal(err) - } - seed := make([]byte, sign.KeySize) - if _, err := io.ReadFull(rand.Reader, seed); err != nil { - t.Fatal(err) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatal(err) - } - - // Every (share[j], blind[j]) MUST verify against the commit vector. - A := sess.APublic() - B := sess.BPublic() - for j := 0; j < n; j++ { - ok, err := VerifyShareAgainstCommits(params, A, B, - out.Shares[j], out.Blinds[j], out.Commits, j, threshold) - if err != nil { - t.Fatalf("VerifyShareAgainstCommits j=%d: %v", j, err) - } - if !ok { - t.Fatalf("GPU-produced share[%d] FAILED Pedersen verify under GPU-produced commits", j) - } - } -} - -// TestDKG2_GPU_FullCeremony runs the canonical multi-party Round 1 → -// Round 2 ceremony with GPU dispatch on. Hooks into the same harness -// used by TestDKG2_2of3 etc; only difference is the dispatch toggle. -func TestDKG2_GPU_FullCeremony(t *testing.T) { - prev := SetDKG2GPUForTest(true) - defer SetDKG2GPUForTest(prev) - runPedersenDKG(t, 7, 5) -} - -// BenchmarkDKG2_GPU_Round1_*_CPU and _GPU measure the speedup of the -// parallel byte-slot fan-out at production sizes. -// -// On Apple M1 Max with 10 GOMAXPROCS the expected speedup is roughly -// linear in the t·N axis (NTT calls are the dominant cost); the n axis -// also benefits but Horner is cheaper per step than the NTT path. -func BenchmarkDKG2_GPU_Round1_5of7_CPU(b *testing.B) { - prev := SetDKG2GPUForTest(false) - defer SetDKG2GPUForTest(prev) - benchRound1(b, 7, 5) -} - -func BenchmarkDKG2_GPU_Round1_5of7_GPU(b *testing.B) { - prev := SetDKG2GPUForTest(true) - defer SetDKG2GPUForTest(prev) - benchRound1(b, 7, 5) -} - -func BenchmarkDKG2_GPU_Round1_7of11_CPU(b *testing.B) { - prev := SetDKG2GPUForTest(false) - defer SetDKG2GPUForTest(prev) - benchRound1(b, 11, 7) -} - -func BenchmarkDKG2_GPU_Round1_7of11_GPU(b *testing.B) { - prev := SetDKG2GPUForTest(true) - defer SetDKG2GPUForTest(prev) - benchRound1(b, 11, 7) -} - -// 14-of-21: the production Lux consensus committee shape (n=21, t=14) -// is the headline benchmark the task asks for. -func BenchmarkDKG2_GPU_Round1_14of21_CPU(b *testing.B) { - prev := SetDKG2GPUForTest(false) - defer SetDKG2GPUForTest(prev) - benchRound1(b, 21, 14) -} - -func BenchmarkDKG2_GPU_Round1_14of21_GPU(b *testing.B) { - prev := SetDKG2GPUForTest(true) - defer SetDKG2GPUForTest(prev) - benchRound1(b, 21, 14) -} - -func benchRound1(b *testing.B, n, t int) { - params, err := NewParams() - if err != nil { - b.Fatal(err) - } - sess, err := NewDKGSession(params, 0, n, t, nil) - if err != nil { - b.Fatal(err) - } - seed := make([]byte, sign.KeySize) - if _, err := io.ReadFull(rand.Reader, seed); err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := sess.Round1WithSeed(seed); err != nil { - b.Fatal(err) - } - } -} diff --git a/dkg2/dkg2_parallel.go b/dkg2/dkg2_parallel.go deleted file mode 100644 index 8c41d1c..0000000 --- a/dkg2/dkg2_parallel.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dkg2 - -// dkg2GPUEnabled is the runtime toggle for the goroutine fan-out path of -// the Pedersen DKG hot loop. Default off; tests opt in via -// SetDKG2GPUForTest. This is not a GPU dispatch — real GPU NTT for the -// underlying ring math lives in luxfi/lattice/v7/gpu and is reached via -// the consensus engine accel pipeline. -func init() { dkg2GPUEnabled = false } diff --git a/dkg2/dkg2_test.go b/dkg2/dkg2_test.go deleted file mode 100644 index 593344f..0000000 --- a/dkg2/dkg2_test.go +++ /dev/null @@ -1,1382 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package dkg2 - -import ( - "bytes" - "crypto/ed25519" - cryptorand "crypto/rand" - "errors" - "math/big" - "testing" - - "github.com/luxfi/corona/hash" - "github.com/luxfi/corona/sign" - "github.com/luxfi/corona/utils" - - "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/sampling" - "github.com/luxfi/lattice/v7/utils/structs" -) - -// TestDKG2_2of3 runs the smallest meaningful Pedersen DKG. -func TestDKG2_2of3(t *testing.T) { runPedersenDKG(t, 3, 2) } - -// TestDKG2_3of5 stresses the Horner evaluation loop slightly more. -func TestDKG2_3of5(t *testing.T) { runPedersenDKG(t, 5, 3) } - -// TestDKG2_5of7 — same parameter family as the upstream dkg/ test suite. -func TestDKG2_5of7(t *testing.T) { runPedersenDKG(t, 7, 5) } - -// TestDKG2_7of11 — biggest config in the canonical KAT. -func TestDKG2_7of11(t *testing.T) { runPedersenDKG(t, 11, 7) } - -// TestDKG2_InvalidParams checks bounds-checking on session construction. -func TestDKG2_InvalidParams(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - cases := []struct { - name string - partyID int - n, t int - expectedErr error - }{ - {"threshold equals n", 0, 3, 3, ErrInvalidThreshold}, - {"threshold zero", 0, 3, 0, ErrInvalidThreshold}, - {"n equals 1", 0, 1, 1, ErrInvalidPartyCount}, - {"party out of range", 5, 3, 2, ErrInvalidPartyID}, - {"negative party", -1, 3, 2, ErrInvalidPartyID}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - _, err := NewDKGSession(params, c.partyID, c.n, c.t, nil) - if err != c.expectedErr { - t.Fatalf("expected %v, got %v", c.expectedErr, err) - } - }) - } -} - -// TestDKG2_DeterministicMatrices proves DeriveA/DeriveB are reproducible — -// the byte-equal C++ port relies on these being identical. -func TestDKG2_DeterministicMatrices(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - A1 := DeriveA(params.R) - A2 := DeriveA(params.R) - B1 := DeriveB(params.R) - B2 := DeriveB(params.R) - if !matEqual(params.R, A1, A2) { - t.Fatal("DeriveA is non-deterministic") - } - if !matEqual(params.R, B1, B2) { - t.Fatal("DeriveB is non-deterministic") - } - if matEqual(params.R, A1, B1) { - t.Fatal("A and B collided — domain separation broken") - } -} - -// TestDKG2_CommitDigestConsistency validates that CommitDigest is stable -// for the same Round1 output and changes when the commits change. This is -// the hash exchanged in Round 1.5 to defeat the cross-party-inconsistency -// attack (Finding 2 of RED-DKG-REVIEW.md). -// -// Runs against both supported hash suites (Corona-SHA3 default and -// Corona-BLAKE3 legacy) so neither the signature surface nor the byte -// stability silently regresses across the cutover. -func TestDKG2_CommitDigestConsistency(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - - suites := []struct { - name string - s hash.HashSuite - }{ - {"default", nil}, - {"sha3", hash.NewCoronaSHA3()}, - {"blake3", hash.NewCoronaBLAKE3()}, - } - - for _, sc := range suites { - t.Run(sc.name, func(t *testing.T) { - sess, err := NewDKGSession(params, 0, 3, 2, sc.s) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(i) - } - out1, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1: %v", err) - } - out2, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1 (replay): %v", err) - } - d1, err := out1.CommitDigest(sc.s) - if err != nil { - t.Fatalf("CommitDigest: %v", err) - } - d2, err := out2.CommitDigest(sc.s) - if err != nil { - t.Fatalf("CommitDigest (replay): %v", err) - } - if d1 != d2 { - t.Fatal("CommitDigest non-deterministic for same seed — KAT replay would fail") - } - - c0 := *out1.Commits[0][0].CopyNew() - c0.Coeffs[0][0] ^= 1 - out1.Commits[0][0] = c0 - d3, err := out1.CommitDigest(sc.s) - if err != nil { - t.Fatalf("CommitDigest (tampered): %v", err) - } - if d3 == d2 { - t.Fatal("CommitDigest invariant under tampering — useless against Finding 2 attack") - } - }) - } -} - -// TestDKG2_HashSuiteCrossProfile verifies that two distinct HashSuite -// implementations produce distinct CommitDigest bytes for the same commit -// vector. This is the dkg2 invariant for the suite-ID-bound transcript: a -// SHA3 commitment and a BLAKE3 commitment can never collide on the same -// cohort body, so a stale/legacy participant cannot impersonate a current -// participant via the digest channel. -func TestDKG2_HashSuiteCrossProfile(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - sess, err := NewDKGSession(params, 0, 3, 2, hash.NewCoronaSHA3()) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(i ^ 0x55) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1: %v", err) - } - dSHA3, err := out.CommitDigest(hash.NewCoronaSHA3()) - if err != nil { - t.Fatalf("CommitDigest sha3: %v", err) - } - dBLAKE3, err := out.CommitDigest(hash.NewCoronaBLAKE3()) - if err != nil { - t.Fatalf("CommitDigest blake3: %v", err) - } - if dSHA3 == dBLAKE3 { - t.Fatal("SHA3 and BLAKE3 digests collided on same commits — suite-ID binding broken") - } - - // Legacy BLAKE3 path — no suite-ID binding, kept byte-stable for KAT - // replay against the pre-cutover oracle. - dLegacy, err := out.CommitDigestBLAKE3() - if err != nil { - t.Fatalf("CommitDigestBLAKE3: %v", err) - } - if dLegacy == dSHA3 || dLegacy == dBLAKE3 { - t.Fatal("legacy BLAKE3 digest collided with suite-bound digest — domain separation broken") - } -} - -// TestDKG2_PseudoinverseAttack is the headline negative-test: prove that -// Pedersen-style DKG resists the broken-Feldman recovery attack. -// -// Setup: run a single party's Round1 to produce (Commits, Shares, Blinds) -// where commits = A·NTT(c_k) + B·NTT(r_k). The "passive observer" applies -// the pseudoinverse recovery that BREAKS the Feldman-only scheme: solve -// M·z = C_0 via least-squares slot-wise. -// -// In dkg2 the recovered z is contaminated by B·NTT(r_0); we assert the -// pseudoinverse output disagrees with NTT(c_0) on every coefficient by a -// margin much larger than any β_E-Gaussian width. The threshold is 99% of -// slots; the implementation typically reaches 100%. -func TestDKG2_PseudoinverseAttack(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - sess, err := NewDKGSession(params, 0, 3, 2, nil) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(0xAB ^ i) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1: %v", err) - } - - r := params.R - A := sess.A - - cTrueNTT := make(structs.Vector[ring.Poly], sign.N) - for i := 0; i < sign.N; i++ { - cTrueNTT[i] = *sess.cCoeffs[0][i].CopyNew() - r.NTT(cTrueNTT[i], cTrueNTT[i]) - } - - C0 := out.Commits[0] - q := r.Modulus() - - Aplain := cloneAndIMForm(r, A) - C0plain := cloneVecAndIMForm(r, C0) - _ = q - - phi := r.N() - guesses := make([][]uint64, sign.N) - for vi := range guesses { - guesses[vi] = make([]uint64, phi) - } - - for slot := 0; slot < phi; slot++ { - Aslot := make([][]*big.Int, sign.M) - for i := 0; i < sign.M; i++ { - Aslot[i] = make([]*big.Int, sign.N) - for j := 0; j < sign.N; j++ { - Aslot[i][j] = new(big.Int).SetUint64(Aplain[i][j].Coeffs[0][slot]) - } - } - yslot := make([]*big.Int, sign.M) - for i := 0; i < sign.M; i++ { - yslot[i] = new(big.Int).SetUint64(C0plain[i].Coeffs[0][slot]) - } - - AtA, Aty := buildNormalEquations(Aslot, yslot, q) - zSlot, ok := solveLinearModQ(AtA, Aty, q) - if !ok { - continue - } - for vi := 0; vi < sign.N; vi++ { - guesses[vi][slot] = zSlot[vi].Uint64() - } - } - - totalDiffSlots := 0 - for vi := 0; vi < sign.N; vi++ { - for slot := 0; slot < phi; slot++ { - if guesses[vi][slot] != cTrueNTT[vi].Coeffs[0][slot] { - totalDiffSlots++ - } - } - } - totalSlots := sign.N * phi - t.Logf("Pseudoinverse attack: %d/%d NTT slots disagree with true NTT(c_0)", - totalDiffSlots, totalSlots) - if totalDiffSlots*100 < totalSlots*99 { - t.Fatalf("Pseudoinverse attack recovered %d/%d slots — hiding broken!", - totalSlots-totalDiffSlots, totalSlots) - } -} - -// TestDKG2_TamperedShareRejected — verifies cross-party share verification -// rejects a tampered share via the constant-time verifier. -func TestDKG2_TamperedShareRejected(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = out[i].Shares[0] - blinds[i] = out[i].Blinds[0] - commits[i] = out[i].Commits - } - shares[1][0].Coeffs[0][0] ^= 1 - _, _, _, err = sessions[0].Round2(shares, blinds, commits) - if err == nil { - t.Fatal("expected verification failure on tampered share, got success") - } - if !errors.Is(err, ErrShareVerification) { - t.Fatalf("expected ErrShareVerification, got %v", err) - } - t.Logf("rejected as expected: %v", err) -} - -// TestDKG2_Round2Identify — Round2Identify names the offending sender. -func TestDKG2_Round2Identify(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 4, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = out[i].Shares[0] - blinds[i] = out[i].Blinds[0] - commits[i] = out[i].Commits - } - - // Tamper with sender 2's share-to-0. - shares[2][3].Coeffs[0][7] ^= 0x80 - _, _, _, badID, err := sessions[0].Round2Identify(shares, blinds, commits) - if err == nil { - t.Fatal("expected verification failure") - } - if badID != 2 { - t.Fatalf("expected bad sender=2, got %d", badID) - } - if !errors.Is(err, ErrShareVerification) { - t.Fatalf("expected ErrShareVerification, got %v", err) - } -} - -// TestDKG2_MalformedCommitRejected — recipient rejects sender with a -// commit vector of wrong length. -func TestDKG2_MalformedCommitRejected(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = out[i].Shares[0] - blinds[i] = out[i].Blinds[0] - commits[i] = out[i].Commits - } - // Truncate sender 1's commit vector to 1 element (expected: t=2). - commits[1] = commits[1][:1] - _, _, _, badID, err := sessions[0].Round2Identify(shares, blinds, commits) - if err == nil { - t.Fatal("expected malformed-commit rejection") - } - if badID != 1 { - t.Fatalf("expected bad sender=1, got %d", badID) - } - if !errors.Is(err, ErrMalformedCommit) { - t.Fatalf("expected ErrMalformedCommit, got %v", err) - } -} - -// TestDKG2_MissingDataRejected confirms missing inputs cause clear errors. -func TestDKG2_MissingDataRejected(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 1; i < n; i++ { - shares[i] = out[i].Shares[0] - blinds[i] = out[i].Blinds[0] - commits[i] = out[i].Commits - } - _, _, _, err = sessions[0].Round2(shares, blinds, commits) - if err == nil { - t.Fatal("expected ErrMissingData, got success") - } - if !errors.Is(err, ErrMissingData) { - t.Fatalf("expected ErrMissingData, got %v", err) - } -} - -// TestDKG2_CommitDigestEquivocation — Round 1.5 cross-party check -// detects a sender that delivers different commit vectors to different -// recipients. -// -// Scenario: sender 1 in a 3-party DKG ships commit-vector C^a to recipient -// 0 and a tampered C^b to recipient 2. Round 1.5 runs: each recipient -// computes the digest of the C they received and broadcasts. Recipients -// 0 and 2 disagree → equivocation detected → ComplaintEquivocation -// emitted by either honest recipient. -func TestDKG2_CommitDigestEquivocation(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - // Sender 1 produces a "real" Round1 output. - seed := make([]byte, sign.KeySize) - for k := range seed { - seed[k] = 0x11 - } - outReal, err := sessions[1].Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1 sender 1: %v", err) - } - - // Sender 1 fakes a *second* Round1 output (different seed) to deliver - // to recipient 2. - seedFake := make([]byte, sign.KeySize) - for k := range seedFake { - seedFake[k] = 0x22 - } - outFake, err := sessions[1].Round1WithSeed(seedFake) - if err != nil { - t.Fatalf("Round1 sender 1 fake: %v", err) - } - if outReal.Commits[0][0].Coeffs[0][0] == outFake.Commits[0][0].Coeffs[0][0] { - t.Fatal("real and fake commits accidentally identical — pick a fresh seed") - } - - // Recipient 0 sees outReal.Commits, computes its digest. - digestForR0, err := outReal.CommitDigest(nil) - if err != nil { - t.Fatalf("CommitDigest real: %v", err) - } - // Recipient 2 sees outFake.Commits, computes its digest. - digestForR2, err := outFake.CommitDigest(nil) - if err != nil { - t.Fatalf("CommitDigest fake: %v", err) - } - if digestForR0 == digestForR2 { - t.Fatal("equivocation undetectable — Round 1.5 digest channel broken") - } - - // Either honest recipient now emits a ComplaintEquivocation. Build it - // from the two disagreeing digest broadcasts (here represented by the - // raw digests; in production they would be wire-signed under sender - // 1's identity key). - transcript := [32]byte{} - for i := range transcript { - transcript[i] = byte(i) - } - complaint := NewEquivocationComplaint( - transcript, - 1, // misbehaving sender - 0, // complainer - digestForR0[:], digestForR2[:], - ) - - priv, _, err := genWireKey(t) - if err != nil { - t.Fatalf("genWireKey: %v", err) - } - complaint.Sign(priv) - if err := complaint.Verify(); err != nil { - t.Fatalf("complaint.Verify: %v", err) - } - if complaint.Reason != ComplaintEquivocation { - t.Fatalf("expected ComplaintEquivocation, got %v", complaint.Reason) - } -} - -// TestDKG2_BadDeliveryComplaint — a recipient that detects a Pedersen -// mismatch produces a signed ComplaintBadDelivery with re-checkable -// evidence. Any honest validator can re-run VerifyShareAgainstCommits on -// the evidence and confirm the misbehaviour. -func TestDKG2_BadDeliveryComplaint(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - - // Tamper sender 1's share-to-0. - tamperedShare := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - tamperedShare[vi] = *out[1].Shares[0][vi].CopyNew() - } - tamperedShare[0].Coeffs[0][3] ^= 0x42 - - ok, err := VerifyShareAgainstCommits( - params, sessions[0].A, sessions[0].B, - tamperedShare, out[1].Blinds[0], out[1].Commits, - 0, th, - ) - if ok { - t.Fatal("VerifyShareAgainstCommits accepted tampered share") - } - if !errors.Is(err, ErrShareVerification) { - t.Fatalf("expected ErrShareVerification, got %v", err) - } - - transcript := [32]byte{} - for i := range transcript { - transcript[i] = byte(i + 1) - } - complaint, err := NewBadDeliveryComplaint( - transcript, - 1, // misbehaving sender - 0, // complainer - tamperedShare, out[1].Blinds[0], out[1].Commits, - ) - if err != nil { - t.Fatalf("NewBadDeliveryComplaint: %v", err) - } - priv, _, err := genWireKey(t) - if err != nil { - t.Fatalf("genWireKey: %v", err) - } - complaint.Sign(priv) - if err := complaint.Verify(); err != nil { - t.Fatalf("complaint.Verify: %v", err) - } - if complaint.SenderID != 1 || complaint.Reason != ComplaintBadDelivery { - t.Fatalf("complaint mis-shaped: sender=%d reason=%v", complaint.SenderID, complaint.Reason) - } -} - -// TestDKG2_DisqualificationThreshold — t-1 distinct complainers -// disqualify a sender; t-2 do not. -func TestDKG2_DisqualificationThreshold(t *testing.T) { - const th = 4 - cap := DisqualificationThreshold(th) - if cap != 3 { - t.Fatalf("DisqualificationThreshold(4) = %d, expected 3", cap) - } - - mkComplaint := func(sender, complainer int) *Complaint { - return &Complaint{ - SenderID: sender, - ComplainerID: complainer, - Reason: ComplaintBadDelivery, - } - } - - // 3 distinct complainers against sender 0 → disqualified. - dq := ComputeDisqualifiedSet([]*Complaint{ - mkComplaint(0, 1), mkComplaint(0, 2), mkComplaint(0, 3), - }, th) - if _, ok := dq[0]; !ok { - t.Fatal("expected sender 0 disqualified at 3 complainers, was not") - } - - // 2 distinct complainers → NOT disqualified. - dq = ComputeDisqualifiedSet([]*Complaint{ - mkComplaint(0, 1), mkComplaint(0, 2), - }, th) - if _, ok := dq[0]; ok { - t.Fatal("sender 0 disqualified at 2 complainers — threshold misapplied") - } - - // 3 complaints from the SAME complainer → still NOT disqualified. - dq = ComputeDisqualifiedSet([]*Complaint{ - mkComplaint(0, 1), mkComplaint(0, 1), mkComplaint(0, 1), - }, th) - if _, ok := dq[0]; ok { - t.Fatal("sender 0 disqualified by single complainer — dedup broken") - } -} - -// TestDKG2_FilterQualifiedQuorum — quorum filtering removes disqualified, -// returns ErrInsufficientQuorum when survivors fall below threshold. -func TestDKG2_FilterQualifiedQuorum(t *testing.T) { - q, err := FilterQualifiedQuorum([]int{0, 1, 2, 3, 4}, map[int]struct{}{1: {}}, 3) - if err != nil { - t.Fatalf("FilterQualifiedQuorum: %v", err) - } - if got, want := q, []int{0, 2, 3, 4}; !equalIntSlice(got, want) { - t.Fatalf("got %v, want %v", got, want) - } - - _, err = FilterQualifiedQuorum([]int{0, 1, 2}, map[int]struct{}{0: {}, 1: {}}, 3) - if !errors.Is(err, ErrInsufficientQuorum) { - t.Fatalf("expected ErrInsufficientQuorum, got %v", err) - } -} - -// TestDKG2_PedersenIdentity (integration sketch — path (b)): -// -// Demonstrate that the Pedersen-shaped public key b_ped satisfies the -// signing-time identity: -// -// A · NTT(s) + B · NTT(t_master) ?= Σ_i C_{i,0} (mod q) -// -// where s = Σ_j λ_j · s_j is the reconstructed Corona secret and -// t_master = Σ_j λ_j · u_j is the reconstructed blinding scalar (Lagrange -// recombination over an arbitrary t-subset T). -// -// This proves end-to-end algebraic consistency: the dealer's commit, the -// distributed share aggregation, and the Lagrange recombination all close. -func TestDKG2_PedersenIdentity(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - r := params.R - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - seed := make([]byte, sign.KeySize) - seed[0] = byte(i) - out[i], err = sessions[i].Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - - sShares := make([]structs.Vector[ring.Poly], n) - uShares := make([]structs.Vector[ring.Poly], n) - for j := 0; j < n; j++ { - sj, uj, _, err := sessions[j].Round2( - gather(out, n, j, func(o *Round1Output) structs.Vector[ring.Poly] { return o.Shares[j] }), - gather(out, n, j, func(o *Round1Output) structs.Vector[ring.Poly] { return o.Blinds[j] }), - gatherCommits(out, n), - ) - if err != nil { - t.Fatalf("Round2(%d): %v", j, err) - } - sShares[j] = sj - uShares[j] = uj - } - - T := make([]int, th) - for i := 0; i < th; i++ { - T[i] = i - } - q := new(big.Int).SetUint64(sign.Q) - lambdas := lagrangeAtZero(T, q) - - sRecon := lagrangeRecombine(r, sShares, T, lambdas, q) - tMasterRecon := lagrangeRecombine(r, uShares, T, lambdas, q) - - sNTT := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - sNTT[vi] = *sRecon[vi].CopyNew() - r.NTT(sNTT[vi], sNTT[vi]) - } - tNTT := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - tNTT[vi] = *tMasterRecon[vi].CopyNew() - r.NTT(tNTT[vi], tNTT[vi]) - } - - A := sessions[0].A - B := sessions[0].B - asN := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, A, sNTT, asN) - btN := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, B, tNTT, btN) - lhs := utils.InitializeVector(r, sign.M) - utils.VectorAdd(r, asN, btN, lhs) - - rhs := utils.InitializeVector(r, sign.M) - for i := 0; i < n; i++ { - utils.VectorAdd(r, rhs, out[i].Commits[0], rhs) - } - - for ri := 0; ri < sign.M; ri++ { - if !r.Equal(lhs[ri], rhs[ri]) { - t.Fatalf("Pedersen identity broken at index %d", ri) - } - } - t.Log("Pedersen identity holds: A·NTT(s) + B·NTT(t_master) == Σ_i C_{i,0}") -} - -// TestDKG2_AggregateUnroundedCommit — exposes the commit aggregator used -// by the path (b) sign-after-DKG integration. -func TestDKG2_AggregateUnroundedCommit(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - commits[i] = out[i].Commits - } - agg, err := AggregateUnroundedCommit(params, commits, n) - if err != nil { - t.Fatalf("AggregateUnroundedCommit: %v", err) - } - if len(agg) != sign.M { - t.Fatalf("aggregated commit dim %d, expected %d", len(agg), sign.M) - } - - // Missing data rejection. - delete(commits, 1) - _, err = AggregateUnroundedCommit(params, commits, n) - if !errors.Is(err, ErrMissingData) { - t.Fatalf("expected ErrMissingData, got %v", err) - } -} - -// TestDKG2_VerifyShareAgainstCommits_Pure — the public verifier function -// matches the in-Round2 inline check, accepts honest pairs and rejects -// tampered ones with constant-time comparison semantics. -func TestDKG2_VerifyShareAgainstCommits_Pure(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - sess, err := NewDKGSession(params, 0, 3, 2, nil) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(0x33 + i) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1: %v", err) - } - for j := 0; j < 3; j++ { - ok, err := VerifyShareAgainstCommits(params, sess.A, sess.B, - out.Shares[j], out.Blinds[j], out.Commits, j, 2) - if err != nil || !ok { - t.Fatalf("recipient %d: %v / %v", j, ok, err) - } - } - - // Reject on tampered share. - tampered := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - tampered[vi] = *out.Shares[1][vi].CopyNew() - } - tampered[0].Coeffs[0][0] ^= 1 - ok, err := VerifyShareAgainstCommits(params, sess.A, sess.B, - tampered, out.Blinds[1], out.Commits, 1, 2) - if ok { - t.Fatal("tampered share accepted") - } - if !errors.Is(err, ErrShareVerification) { - t.Fatalf("expected ErrShareVerification, got %v", err) - } - - // Reject on tampered blind. - bTampered := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - bTampered[vi] = *out.Blinds[1][vi].CopyNew() - } - bTampered[2].Coeffs[0][3] ^= 0x80 - ok, err = VerifyShareAgainstCommits(params, sess.A, sess.B, - out.Shares[1], bTampered, out.Commits, 1, 2) - if ok { - t.Fatal("tampered blind accepted") - } - if !errors.Is(err, ErrShareVerification) { - t.Fatalf("expected ErrShareVerification, got %v", err) - } -} - -// TestDKG2_SignIntegration_PathC — closes the loop from DKG2 output to a -// Corona Sign-compatible public key via the recommended path (c) of -// papers/lp-073-pulsar/sections/08a-pedersen-dkg.tex. -// -// Path (c) in production: run dkg2, recombine s = Σ_j λ_j s_j over a -// t-subset, sample fresh Gaussian e, build b = A·s + e, round to bTilde, -// then run Corona Sign as normal under bTilde. This test mechanises the -// "DKG-output → Corona-shaped pk" leg and confirms (i) recombined s has -// the expected dimension and lattice shape, (ii) the b = A·s + e -// construction yields a Corona-shaped bTilde of the right shape, and -// (iii) the round-trip RestoreVector(RoundVector(b)) deviates from b by -// at most the Xi rounding tolerance — i.e., Corona Sign Verify's -// L2-norm check would accept a signature produced under (A, bTilde). -// -// The full Sign1/Sign2/Combine path uses sign.Gen which generates s -// internally; injecting an externally-supplied DKG s into Sign requires -// the small refactor of sign.Gen to accept an external secret. That -// refactor is independent of dkg2 and tracked in corona/sign; the -// algebraic compatibility this test confirms is the binding contract -// between dkg2 and Sign. -func TestDKG2_SignIntegration_PathC(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - const n, th = 3, 2 - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, th, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - r := params.R - - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - seed := make([]byte, sign.KeySize) - seed[0] = byte(0x77 + i) - out[i], err = sessions[i].Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - } - - // Run Round2 from each recipient so we have s_j shares. - sShares := make([]structs.Vector[ring.Poly], n) - for j := 0; j < n; j++ { - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = out[i].Shares[j] - blinds[i] = out[i].Blinds[j] - commits[i] = out[i].Commits - } - sj, _, _, err := sessions[j].Round2(shares, blinds, commits) - if err != nil { - t.Fatalf("Round2(%d): %v", j, err) - } - sShares[j] = sj - } - - // Recombine s = Σ_j∈T λ_j · s_j over T = {0, 1}. - T := []int{0, 1} - q := new(big.Int).SetUint64(sign.Q) - lambdas := lagrangeAtZero(T, q) - sRecon := lagrangeRecombine(r, sShares, T, lambdas, q) - if len(sRecon) != sign.N { - t.Fatalf("recombined s dim %d, expected %d", len(sRecon), sign.N) - } - - // Path (c) noise-flooding: build b = A · NTT(s) + e for a fresh - // small Gaussian e under SigmaE. Then bTilde = Round_Xi(b). - A := sessions[0].A - sNTT := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - sNTT[vi] = *sRecon[vi].CopyNew() - r.NTT(sNTT[vi], sNTT[vi]) - r.MForm(sNTT[vi], sNTT[vi]) - } - asN := utils.InitializeVector(r, sign.M) - utils.MatrixVectorMul(r, A, sNTT, asN) - utils.ConvertVectorFromNTT(r, asN) - - // Sample fresh small e — uses the same parameters Corona Sign Gen - // uses (sign.go:66), so the resulting (A, b) pair is statistically - // identical to a trusted-dealer Corona setup. - eSeed := make([]byte, sign.KeySize) - for i := range eSeed { - eSeed[i] = byte(0xE5 ^ i) - } - prng, err := sampling.NewKeyedPRNG(eSeed) - if err != nil { - t.Fatalf("KeyedPRNG: %v", err) - } - gauss := ring.NewGaussianSampler(prng, r, - ring.DiscreteGaussian{Sigma: sign.SigmaE, Bound: sign.BoundE}, false) - e := utils.SamplePolyVector(r, sign.M, gauss, false, false) - - b := utils.InitializeVector(r, sign.M) - for ri := 0; ri < sign.M; ri++ { - r.Add(asN[ri], e[ri], b[ri]) - } - - // Round and restore — the deviation must be within the Xi rounding - // tolerance. The Corona Sign verify path does exactly this round-trip - // (sign/sign.go:284-285). - bTilde := utils.RoundVector(r, params.RXi, b, sign.Xi) - bRestored := utils.RestoreVector(r, params.RXi, bTilde, sign.Xi) - - // Sanity: bRestored differs from b by at most 2^Xi per coefficient. - tolerance := uint64(1) << sign.Xi - for ri := 0; ri < sign.M; ri++ { - for ci := 0; ci < r.N(); ci++ { - diff := int64(b[ri].Coeffs[0][ci]) - int64(bRestored[ri].Coeffs[0][ci]) - if diff < 0 { - diff = -diff - } - if uint64(diff) > tolerance { - t.Fatalf("path-c rounding tolerance exceeded at [%d][%d]: diff=%d, tol=%d", - ri, ci, diff, tolerance) - } - } - } - if len(bTilde) != sign.M { - t.Fatalf("bTilde dim %d, expected %d", len(bTilde), sign.M) - } - t.Logf("path (c) integration: dkg2 → A·s+e → bTilde shape verified (M=%d, Xi=%d)", sign.M, sign.Xi) -} - -// TestDKG2_KAT — pin the canonical KAT digest values shipped with the -// luxcpp port. Any change to seed handling, sampler order, A/B derivation, -// or wire format flips these bytes — make sure the KAT JSON moves in -// lockstep. -func TestDKG2_KAT(t *testing.T) { - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - // Same fixed seed used in TestDKG2_CommitDigestConsistency. - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(i) - } - sess, err := NewDKGSession(params, 0, 3, 2, nil) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1WithSeed: %v", err) - } - body, err := out.SerializeCommits() - if err != nil { - t.Fatalf("SerializeCommits: %v", err) - } - if len(body) == 0 { - t.Fatal("empty serialized commits") - } - - dBLAKE3, err := out.CommitDigestBLAKE3() - if err != nil { - t.Fatalf("CommitDigestBLAKE3: %v", err) - } - dSHA3, err := out.CommitDigest(hash.NewCoronaSHA3()) - if err != nil { - t.Fatalf("CommitDigest sha3: %v", err) - } - - // Re-derive on a fresh session and seed to catch hidden global state. - sess2, err := NewDKGSession(params, 0, 3, 2, nil) - if err != nil { - t.Fatalf("NewDKGSession (2): %v", err) - } - out2, err := sess2.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1WithSeed (2): %v", err) - } - dBLAKE3b, _ := out2.CommitDigestBLAKE3() - dSHA3b, _ := out2.CommitDigest(hash.NewCoronaSHA3()) - if dBLAKE3 != dBLAKE3b { - t.Fatal("CommitDigestBLAKE3 not stable across sessions") - } - if dSHA3 != dSHA3b { - t.Fatal("CommitDigest(SHA3) not stable across sessions") - } -} - -// runPedersenDKG runs a full t-of-n protocol and verifies all parties agree -// on the public key. -func runPedersenDKG(t *testing.T, n, threshold int) { - t.Helper() - params, err := NewParams() - if err != nil { - t.Fatalf("NewParams: %v", err) - } - sessions := make([]*DKGSession, n) - for i := 0; i < n; i++ { - sessions[i], err = NewDKGSession(params, i, n, threshold, nil) - if err != nil { - t.Fatalf("NewDKGSession(%d): %v", i, err) - } - } - out := make([]*Round1Output, n) - for i := 0; i < n; i++ { - out[i], err = sessions[i].Round1() - if err != nil { - t.Fatalf("Round1(%d): %v", i, err) - } - if got := len(out[i].Commits); got != threshold { - t.Fatalf("party %d: %d commits, expected %d", i, got, threshold) - } - if got := len(out[i].Shares); got != n { - t.Fatalf("party %d: %d shares, expected %d", i, got, n) - } - if got := len(out[i].Blinds); got != n { - t.Fatalf("party %d: %d blinds, expected %d", i, got, n) - } - } - - // Round 1.5 — every recipient computes its sender digests under the - // active suite. In a real cohort each digest is broadcast and cross- - // checked; here we assert each digest is well-formed and stable. - digests := make([][32]byte, n) - for i := 0; i < n; i++ { - d, err := out[i].CommitDigest(nil) - if err != nil { - t.Fatalf("CommitDigest(%d): %v", i, err) - } - digests[i] = d - } - for i := 0; i < n; i++ { - dCheck, err := out[i].CommitDigest(nil) - if err != nil { - t.Fatalf("CommitDigest(%d): %v", i, err) - } - if dCheck != digests[i] { - t.Fatalf("party %d: digest non-deterministic", i) - } - } - - pubKeys := make([]structs.Vector[ring.Poly], n) - for j := 0; j < n; j++ { - shares := map[int]structs.Vector[ring.Poly]{} - blinds := map[int]structs.Vector[ring.Poly]{} - commits := map[int][]structs.Vector[ring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = out[i].Shares[j] - blinds[i] = out[i].Blinds[j] - commits[i] = out[i].Commits - } - _, _, pk, err := sessions[j].Round2(shares, blinds, commits) - if err != nil { - t.Fatalf("Round2(%d): %v", j, err) - } - pubKeys[j] = pk - } - - r := params.R - for j := 1; j < n; j++ { - if !vecEqual(r, pubKeys[0], pubKeys[j]) { - t.Fatalf("party 0 and party %d disagree on b_ped", j) - } - } - t.Logf("DKG2 %d-of-%d complete; all %d parties agree on b_ped", threshold, n, n) -} - -// ---------------------- helpers ---------------------- - -func matEqual(r *ring.Ring, a, b structs.Matrix[ring.Poly]) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if len(a[i]) != len(b[i]) { - return false - } - for j := range a[i] { - if !r.Equal(a[i][j], b[i][j]) { - return false - } - } - } - return true -} - -func vecEqual(r *ring.Ring, a, b structs.Vector[ring.Poly]) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i].N() != b[i].N() { - return false - } - for level := range a[i].Coeffs { - for k := range a[i].Coeffs[level] { - if a[i].Coeffs[level][k] != b[i].Coeffs[level][k] { - return false - } - } - } - } - return true -} - -// gather builds a per-sender map for recipient j by extracting sub-data -// from each sender's Round1 output via the supplied accessor. -func gather(out []*Round1Output, n, j int, - pick func(*Round1Output) structs.Vector[ring.Poly], -) map[int]structs.Vector[ring.Poly] { - m := make(map[int]structs.Vector[ring.Poly], n) - for i := 0; i < n; i++ { - m[i] = pick(out[i]) - } - return m -} - -func gatherCommits(out []*Round1Output, n int) map[int][]structs.Vector[ring.Poly] { - m := make(map[int][]structs.Vector[ring.Poly], n) - for i := 0; i < n; i++ { - m[i] = out[i].Commits - } - return m -} - -// cloneAndIMForm copies a Mont-NTT matrix and converts to plain NTT form. -func cloneAndIMForm(r *ring.Ring, M structs.Matrix[ring.Poly]) structs.Matrix[ring.Poly] { - out := make(structs.Matrix[ring.Poly], len(M)) - for i := range M { - out[i] = make([]ring.Poly, len(M[i])) - for j := range M[i] { - cp := M[i][j].CopyNew() - r.IMForm(*cp, *cp) - out[i][j] = *cp - } - } - return out -} - -// cloneVecAndIMForm copies a Mont-NTT vector and converts to plain NTT form. -func cloneVecAndIMForm(r *ring.Ring, v structs.Vector[ring.Poly]) structs.Vector[ring.Poly] { - out := make(structs.Vector[ring.Poly], len(v)) - for i := range v { - cp := v[i].CopyNew() - r.IMForm(*cp, *cp) - out[i] = *cp - } - return out -} - -// buildNormalEquations builds A^T A and A^T y mod q for a per-slot system. -func buildNormalEquations(A [][]*big.Int, y []*big.Int, q *big.Int) ([][]*big.Int, []*big.Int) { - M := len(A) - N := len(A[0]) - AtA := make([][]*big.Int, N) - for i := range AtA { - AtA[i] = make([]*big.Int, N) - for j := range AtA[i] { - AtA[i][j] = big.NewInt(0) - } - } - Aty := make([]*big.Int, N) - for i := range Aty { - Aty[i] = big.NewInt(0) - } - for k := 0; k < N; k++ { - for j := 0; j < N; j++ { - s := big.NewInt(0) - for r := 0; r < M; r++ { - t := new(big.Int).Mul(A[r][k], A[r][j]) - s.Add(s, t) - s.Mod(s, q) - } - AtA[k][j] = s - } - } - for k := 0; k < N; k++ { - s := big.NewInt(0) - for r := 0; r < M; r++ { - t := new(big.Int).Mul(A[r][k], y[r]) - s.Add(s, t) - s.Mod(s, q) - } - Aty[k] = s - } - return AtA, Aty -} - -// solveLinearModQ solves N·z = b mod q via Gauss-Jordan. Returns (z, true) -// on full rank; (nil, false) if singular. Pure big.Int. -func solveLinearModQ(N [][]*big.Int, b []*big.Int, q *big.Int) ([]*big.Int, bool) { - n := len(N) - aug := make([][]*big.Int, n) - for i := range aug { - aug[i] = make([]*big.Int, n+1) - for j := 0; j < n; j++ { - aug[i][j] = new(big.Int).Set(N[i][j]) - } - aug[i][n] = new(big.Int).Set(b[i]) - } - for col := 0; col < n; col++ { - piv := -1 - for row := col; row < n; row++ { - if aug[row][col].Sign() != 0 { - piv = row - break - } - } - if piv < 0 { - return nil, false - } - aug[col], aug[piv] = aug[piv], aug[col] - inv := new(big.Int).ModInverse(aug[col][col], q) - if inv == nil { - return nil, false - } - for j := col; j <= n; j++ { - aug[col][j].Mul(aug[col][j], inv).Mod(aug[col][j], q) - } - for row := 0; row < n; row++ { - if row == col || aug[row][col].Sign() == 0 { - continue - } - factor := new(big.Int).Set(aug[row][col]) - for j := col; j <= n; j++ { - t := new(big.Int).Mul(factor, aug[col][j]) - aug[row][j].Sub(aug[row][j], t).Mod(aug[row][j], q) - } - } - } - z := make([]*big.Int, n) - for i := 0; i < n; i++ { - z[i] = new(big.Int).Mod(aug[i][n], q) - if z[i].Sign() < 0 { - z[i].Add(z[i], q) - } - } - return z, true -} - -// lagrangeAtZero returns the Lagrange weights λ_i evaluated at x=0 over -// the subset T of points {T[0]+1, T[1]+1, ...} (1-indexed convention used -// by Round1 share evaluation). Result mod q, as big.Int. -func lagrangeAtZero(T []int, q *big.Int) []*big.Int { - t := len(T) - lambdas := make([]*big.Int, t) - for i := 0; i < t; i++ { - xi := big.NewInt(int64(T[i] + 1)) - num := big.NewInt(1) - den := big.NewInt(1) - for j := 0; j < t; j++ { - if i == j { - continue - } - xj := big.NewInt(int64(T[j] + 1)) - num.Mul(num, new(big.Int).Neg(xj)).Mod(num, q) - den.Mul(den, new(big.Int).Sub(xi, xj)).Mod(den, q) - } - denInv := new(big.Int).ModInverse(den, q) - l := new(big.Int).Mul(num, denInv) - l.Mod(l, q) - if l.Sign() < 0 { - l.Add(l, q) - } - lambdas[i] = l - } - return lambdas -} - -// lagrangeRecombine computes Σ_i λ_i · share_{T[i]} in standard -// coefficient form. -func lagrangeRecombine(r *ring.Ring, - shares []structs.Vector[ring.Poly], - T []int, lambdas []*big.Int, q *big.Int, -) structs.Vector[ring.Poly] { - out := make(structs.Vector[ring.Poly], sign.N) - for vi := 0; vi < sign.N; vi++ { - out[vi] = r.NewPoly() - } - for k, partyIdx := range T { - share := shares[partyIdx] - for vi := 0; vi < sign.N; vi++ { - tmp := r.NewPoly() - polyAddCoeffwise(r, tmp, share[vi], q) - polyMulScalar(r, tmp, lambdas[k], q) - polyAddCoeffwise(r, out[vi], tmp, q) - } - } - return out -} - -func equalIntSlice(a, b []int) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} - -func genWireKey(t *testing.T) (ed25519.PrivateKey, ed25519.PublicKey, error) { - t.Helper() - pub, priv, err := ed25519.GenerateKey(cryptorand.Reader) - return priv, pub, err -} - -// ensure bytes import is exercised (for KAT format hooks below). -var _ = bytes.Equal diff --git a/dkg2/fuzz_round_test.go b/dkg2/fuzz_round_test.go deleted file mode 100644 index 49182b4..0000000 --- a/dkg2/fuzz_round_test.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// dkg2 wire-format fuzz harnesses. -// -// FuzzDKG2Round1Output — Round1Output.SerializeCommits bytes (Vector[Vector[Poly]]) -// FuzzDKG2Round2Output — Round-2 share vector bytes (Vector[Poly]) -// -// Property: every decoder path under fuzzMaxRawSize never escapes a -// panic to the caller, even on attacker-controlled length-prefix -// inputs (luxfi/lattice#2 DoS surface). - -package dkg2 - -import ( - "bytes" - "fmt" - "testing" - - "github.com/luxfi/corona/utils" - - "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/structs" -) - -// fuzzMaxRawSize bounds the input handed to the lattigo decoder so -// the unpatched-upstream-lattigo DoS path (luxfi/lattice#2) is -// neutralized by the recover boundary in <1ms. See -// corona/threshold/fuzz_round_test.go for the rationale. -const fuzzMaxRawSize = 1024 - -// decodeVectorWithRecover decodes a Vector[Poly] from raw bytes with -// the same defense-in-depth stack as -// corona/threshold/fuzz_round_test.go: hard byte cap + recover. -func decodeVectorWithRecover(raw []byte) (err error) { - if len(raw) > fuzzMaxRawSize { - return fmt.Errorf("input exceeds fuzzMaxRawSize") - } - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("decode panic recovered: %v", r) - } - }() - r, perr := ring.NewRing(1<<10, []uint64{0x7ffffd8001}) - if perr != nil { - return perr - } - v := utils.InitializeVector(r, 8) - _, derr := v.ReadFrom(bytes.NewReader(raw)) - _ = structs.Vector[ring.Poly](v) // type-assertion for clarity - return derr -} - -// addSmallSeeds adds bounded-size seeds. Real protocol data is -// exercised by TestFuzzCorpus_*Replay, NOT here. -func addSmallSeeds(f *testing.F) { - f.Add([]byte{}) - f.Add([]byte{0x00}) - f.Add([]byte{0x01, 0x02, 0x03, 0x04}) - f.Add(bytes.Repeat([]byte{0xff}, 32)) - f.Add(bytes.Repeat([]byte{0xaa}, 64)) - f.Add(append([]byte{0x10, 0x00, 0x00, 0x00}, bytes.Repeat([]byte{0xcc}, 24)...)) -} - -// FuzzDKG2Round1Output fuzzes the Round-1 commitment-vector decoder. -// dkg2.Round1Output.Commits is broadcast publicly in the dkg2 protocol; -// any peer can supply attacker-controlled bytes for this lane. -func FuzzDKG2Round1Output(f *testing.F) { - addSmallSeeds(f) - - f.Fuzz(func(t *testing.T, raw []byte) { - _ = decodeVectorWithRecover(raw) - }) -} - -// FuzzDKG2Round2Output fuzzes the Round-2 share-vector decoder. -// dkg2 Round-2 outputs are private but transit on an authenticated -// channel; an attacker who compromises a single point-to-point link -// must not be able to crash the receiver via malformed bytes. -func FuzzDKG2Round2Output(f *testing.F) { - addSmallSeeds(f) - - f.Fuzz(func(t *testing.T, raw []byte) { - _ = decodeVectorWithRecover(raw) - }) -} - -// TestFuzzCorpus_DKG2Round1Replay confirms the small-seed corpus is -// at least decodable to the point of producing a clean error (or -// nil) rather than a panic. -func TestFuzzCorpus_DKG2Round1Replay(t *testing.T) { - for _, raw := range [][]byte{ - {}, - {0x00}, - {0x01, 0x02, 0x03, 0x04}, - bytes.Repeat([]byte{0xff}, 32), - } { - if err := decodeVectorWithRecover(raw); err == nil { - // nil is fine — empty/zero input may decode to empty vector - continue - } - } -} - -// TestFuzzCorpus_DKG2Round2Replay mirrors the Round-1 replay. -func TestFuzzCorpus_DKG2Round2Replay(t *testing.T) { - for _, raw := range [][]byte{ - {}, - {0x00}, - bytes.Repeat([]byte{0xaa}, 64), - } { - _ = decodeVectorWithRecover(raw) - } -} diff --git a/keyera/bootstrap_pedersen_test.go b/keyera/bootstrap_pedersen_test.go index 6a03840..ca27061 100644 --- a/keyera/bootstrap_pedersen_test.go +++ b/keyera/bootstrap_pedersen_test.go @@ -259,7 +259,7 @@ func TestBootstrapPedersen_NoMasterSecretInMemory(t *testing.T) { // "mastersecret" or "masters". lower := lowercase(f.Name) if contains(lower, "mastersecret") || contains(lower, "fullsecret") { - t.Fatalf("dkg2.DKGSession has a forbidden field %q — master-secret state leaks", f.Name) + t.Fatalf("dkgvss.Party has a forbidden field %q — master-secret state leaks", f.Name) } } diff --git a/keyera/cutover_gate_test.go b/keyera/cutover_gate_test.go deleted file mode 100644 index 9224706..0000000 --- a/keyera/cutover_gate_test.go +++ /dev/null @@ -1,440 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package keyera - -// cutover_gate_test.go — the Phase-3a cutover gate for replacing corona/dkg2's -// hand-rolled Pedersen-VSS DKG with the shared github.com/luxfi/dkg library -// (vss + ring), parameterized by ring.Ringtail(). -// -// Before any rewire, this gate proves — byte-for-byte — that luxfi/dkg's -// Ringtail binding reproduces corona/dkg2's KAT-pinned arithmetic: -// -// (A) the public Pedersen matrices A, B are byte-identical; -// (B) vss's REAL Round1 produces byte-identical Pedersen commits for the same -// per-party sampling seed (same χ sampler, same NTT/Mont convention); -// (C) the secret share f_i(j) (Horner eval) is byte-identical; -// (D) the group-key NTT convention: dkg2's Round2 b_ped uses IMForm+INTT -// (R^{-1}-scaled), vss's group commit T uses INTT-only (TRUE A·s1+B·u); -// the two differ by EXACTLY the Montgomery factor R. corona's PRODUCTION -// group key (keyera Path-(a) noise flooding) is the TRUE A·s+e'' — it does -// NOT use dkg2's b_ped — so it is convention-aligned with vss's TRUE T. -// -// (A)–(C) are the share-dealing core: if they hold, a vss-backed Bootstrap that -// keeps corona's own Path-(a) finalize preserves the group key + shares -// byte-for-byte. (D) pins PIN 4 of HANDOFF-PHASE3.md against ground truth. - -import ( - "bytes" - "crypto/rand" - "io" - "testing" - - "github.com/luxfi/corona/dkg2" - "github.com/luxfi/corona/hash" - "github.com/luxfi/corona/sign" - - dkgchannel "github.com/luxfi/dkg/channel" - dkgring "github.com/luxfi/dkg/ring" - dkgvss "github.com/luxfi/dkg/vss" - - lring "github.com/luxfi/lattice/v7/ring" - "github.com/luxfi/lattice/v7/utils/structs" -) - -// polysEqual reports byte-equality of two single-modulus polynomials over all -// coefficients (Coeffs[0]); the corona ring is single-level so this is total. -func polysEqual(a, b lring.Poly) bool { - if len(a.Coeffs) != len(b.Coeffs) { - return false - } - for lvl := range a.Coeffs { - if len(a.Coeffs[lvl]) != len(b.Coeffs[lvl]) { - return false - } - for i := range a.Coeffs[lvl] { - if a.Coeffs[lvl][i] != b.Coeffs[lvl][i] { - return false - } - } - } - return true -} - -func vecsEqual(a, b structs.Vector[lring.Poly]) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if !polysEqual(a[i], b[i]) { - return false - } - } - return true -} - -// TestCutover_RingtailMatricesByteIdentical — gate (A). luxfi/dkg's -// ring.Ringtail() must derive the SAME public Pedersen matrices A, B as -// corona/dkg2.DeriveA/DeriveB (both BLAKE3("corona.dkg2.{A,B}.v1") → KeyedPRNG -// → uniform → NTT-Mont). This is the cutover gate: matrix divergence would make -// every commit, share, and group key diverge. -func TestCutover_RingtailMatricesByteIdentical(t *testing.T) { - dkgParams, err := dkg2.NewParams() - if err != nil { - t.Fatalf("dkg2.NewParams: %v", err) - } - coronaA := dkg2.DeriveA(dkgParams.R) - coronaB := dkg2.DeriveB(dkgParams.R) - - prof, err := dkgring.Ringtail() - if err != nil { - t.Fatalf("ring.Ringtail: %v", err) - } - - if len(prof.A) != len(coronaA) || len(prof.B) != len(coronaB) { - t.Fatalf("matrix row mismatch: A %d/%d B %d/%d", - len(prof.A), len(coronaA), len(prof.B), len(coronaB)) - } - for i := range coronaA { - for j := range coronaA[i] { - if !polysEqual(prof.A[i][j], coronaA[i][j]) { - t.Fatalf("A[%d][%d] byte-divergent between Ringtail() and dkg2.DeriveA", i, j) - } - if !polysEqual(prof.B[i][j], coronaB[i][j]) { - t.Fatalf("B[%d][%d] byte-divergent between Ringtail() and dkg2.DeriveB", i, j) - } - } - } - t.Logf("Ringtail() A,B byte-identical to dkg2.DeriveA/DeriveB over all %dx%d slots", len(coronaA), len(coronaA[0])) -} - -// seededGateRng returns a reader that yields seed first (the 32 bytes vss -// Party.Round1 consumes as its sampling seed) then a deterministic tail (KEM -// encapsulation randomness — does NOT affect the commits, which are computed -// before any sealing). This makes vss's χ draws identical to a corona/dkg2 -// Round1WithSeed(seed) run. -func seededGateRng(seed []byte) io.Reader { - tail := make([]byte, 1<<16) - // deterministic, distinct from seed; KEM randomness only. - for i := range tail { - tail[i] = byte(0x5A ^ (i & 0xFF)) - } - return io.MultiReader(bytes.NewReader(seed), bytes.NewReader(tail)) -} - -// buildVSSParty0 constructs party 0 of an n-party vss committee with a directory -// of n fresh identities (identities affect only the sealed envelopes, never the -// public commits). Returns the party ready for Round1. -func buildVSSParty0(t *testing.T, prof *dkgring.Profile, n, thr int) *dkgvss.Party { - return buildVSSPartyAt(t, prof, 0, n, thr) -} - -// buildVSSPartyAt constructs party idx of an n-party vss committee with a fresh -// identity directory. Identities affect only sealed envelopes; gate E drives -// Round3 with DealerInputs built directly from dkg2's (gate-B/C-proven identical) -// cleartext contributions, so envelope decryption is not exercised — only vss's -// real verifyPedersen + aggregateShare. -func buildVSSPartyAt(t *testing.T, prof *dkgring.Profile, idx, n, thr int) *dkgvss.Party { - t.Helper() - ids := make([]*dkgchannel.IdentityKey, n) - nodes := make([]dkgchannel.NodeID, n) - entries := make(map[dkgchannel.NodeID]*dkgchannel.IdentityPublicKey, n) - for i := 0; i < n; i++ { - id, err := dkgchannel.GenerateIdentity(rand.Reader) - if err != nil { - t.Fatalf("GenerateIdentity[%d]: %v", i, err) - } - ids[i] = id - nodes[i] = dkgchannel.NodeID{byte(i + 1), 0xC0} - entries[nodes[i]] = id.PublicKey() - } - dir, err := dkgchannel.NewIdentityDirectory(entries) - if err != nil { - t.Fatalf("NewIdentityDirectory: %v", err) - } - p, err := dkgvss.NewParty(prof, nodes[idx], ids[idx], idx, n, thr, nodes, dir, [32]byte{0xCA, 0xFE}) - if err != nil { - t.Fatalf("NewParty: %v", err) - } - return p -} - -// TestCutover_Round1CommitsByteIdentical — gate (B). vss's REAL Party.Round1 -// (which internally samples χ, builds Pedersen commits C_k = A·NTT(c_k)+B·NTT(r_k)) -// must produce commits byte-identical to corona/dkg2.Round1WithSeed for the same -// 32-byte sampling seed. This exercises vss's actual computeCommits path, not a -// reimplementation. -func TestCutover_Round1CommitsByteIdentical(t *testing.T) { - const n, thr = 5, 3 - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(0x11*i + 7) - } - - // corona/dkg2 reference. - dkgParams, err := dkg2.NewParams() - if err != nil { - t.Fatalf("dkg2.NewParams: %v", err) - } - sess, err := dkg2.NewDKGSession(dkgParams, 0, n, thr, hash.NewCoronaSHA3()) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - coronaOut, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1WithSeed: %v", err) - } - - // vss real Round1 with the same sampling seed. - prof, err := dkgring.Ringtail() - if err != nil { - t.Fatalf("ring.Ringtail: %v", err) - } - party := buildVSSParty0(t, prof, n, thr) - vssOut, err := party.Round1(seededGateRng(seed)) - if err != nil { - t.Fatalf("vss Party.Round1: %v", err) - } - - if len(vssOut.Commits) != len(coronaOut.Commits) { - t.Fatalf("commit count: vss=%d corona=%d", len(vssOut.Commits), len(coronaOut.Commits)) - } - for k := range coronaOut.Commits { - if !vecsEqual(vssOut.Commits[k], coronaOut.Commits[k]) { - t.Fatalf("commit[%d] byte-divergent between vss Round1 and dkg2 Round1WithSeed", k) - } - } - t.Logf("vss Party.Round1 commits byte-identical to dkg2.Round1WithSeed over all %d Pedersen commits", len(coronaOut.Commits)) -} - -// TestCutover_HornerShareByteIdentical — gate (C). The secret share f_i(j) = -// Σ_k c_{i,k}·(j+1)^k must be byte-identical. corona/dkg2 computes it with a -// big.Int Horner (polyMulScalar/polyAddCoeffwise); vss computes it with the -// lattice ring's Barrett-reduced ScalarMulVec + VecAdd. Both yield the canonical -// representative in [0,q). This replicates vss's hornerEval (3 exported-op lines) -// because vss seals the share into an envelope rather than exposing it; the -// arithmetic under test is vss's own ring.ScalarMulVec / ring.VecAdd. -func TestCutover_HornerShareByteIdentical(t *testing.T) { - const n, thr = 5, 3 - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(0x09*i + 3) - } - - dkgParams, err := dkg2.NewParams() - if err != nil { - t.Fatalf("dkg2.NewParams: %v", err) - } - sess, err := dkg2.NewDKGSession(dkgParams, 0, n, thr, hash.NewCoronaSHA3()) - if err != nil { - t.Fatalf("NewDKGSession: %v", err) - } - coronaOut, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1WithSeed: %v", err) - } - - // Re-sample the SAME c_{i,k} via luxfi/dkg's Ringtail χ sampler (identical - // PRNG, σ, bound, order) and Horner-evaluate with vss's exported ring ops. - prof, err := dkgring.Ringtail() - if err != nil { - t.Fatalf("ring.Ringtail: %v", err) - } - r := prof.Ring - prng, err := dkgring.NewKeyedPRNG(seed) - if err != nil { - t.Fatalf("NewKeyedPRNG: %v", err) - } - cCoeffs := make([]dkgring.Vector, thr) - for k := 0; k < thr; k++ { - cCoeffs[k] = prof.SampleSecretVec(prng) - } - // (skip the r_{i,k} draws — not needed for the share value, but keep the - // PRNG honest for any future blind comparison.) - rCoeffs := make([]dkgring.Vector, thr) - for k := 0; k < thr; k++ { - rCoeffs[k] = prof.SampleSecretVec(prng) - } - _ = rCoeffs - - // hornerEval at every recipient point j+1, exactly as vss/party.go does. - for j := 0; j < n; j++ { - x := uint64(j + 1) - share := dkgring.NewVec(r, prof.L) - for k := thr - 1; k >= 0; k-- { - if k < thr-1 { - dkgring.ScalarMulVec(r, share, x, share) - } - dkgring.VecAdd(r, share, cCoeffs[k], share) - } - if !vecsEqual(share, coronaOut.Shares[j]) { - t.Fatalf("share to recipient %d byte-divergent (vss Horner vs dkg2 Horner)", j) - } - } - t.Logf("vss-style Horner shares byte-identical to dkg2 shares for all %d recipients", n) -} - -// TestCutover_GroupKeyConvention — gate (D), PIN 4. Pins the exact NTT-domain -// relationship between the two group-commit conventions against ground truth: -// -// - dkg2.Round2 b_ped = RoundXi( ConvertVectorFromNTT(ΣC) ) [IMForm + INTT] -// - vss group commit T = RoundXi( ConvertVecFromNTT(ΣC) ) [INTT only] -// -// Because dkg2's commits are plain-NTT (Montgomery cancels in the commit -// multiply), the extra IMForm injects exactly one R^{-1}. So dkg2's pre-round -// vector is vss's TRUE A·s1+B·u scaled by R^{-1}; equivalently MForm(dkg2) == vss. -// This asserts that identity coefficient-wise — proving the two conventions -// differ by EXACTLY the Montgomery factor R, not by anything message-dependent. -// -// The corona PRODUCTION group key is NEITHER of these: keyera Path-(a) noise -// flooding multiplies NTT-Mont operands (A · NTT-Mont(λ_j·s_j)), so the surviving -// R is correctly removed by ConvertVectorFromNTT, yielding the TRUE A·s+e” -// (convention-aligned with vss's TRUE T). dkg2's R^{-1}-scaled b_ped is an -// internal Round2 value the production path discards. -func TestCutover_GroupKeyConvention(t *testing.T) { - prof, err := dkgring.Ringtail() - if err != nil { - t.Fatalf("ring.Ringtail: %v", err) - } - r := prof.Ring - - // Deterministic plain-NTT "aggregated commit" ΣC ∈ R_q^K: derive it as a - // real commit A·NTT(c) + B·NTT(r) so it lives in the exact commit domain. - seed := make([]byte, sign.KeySize) - for i := range seed { - seed[i] = byte(0x33*i + 1) - } - prng, err := dkgring.NewKeyedPRNG(seed) - if err != nil { - t.Fatalf("NewKeyedPRNG: %v", err) - } - c := prof.SampleSecretVec(prng) - rr := prof.SampleSecretVec(prng) - cN := dkgring.CopyVec(c) - rN := dkgring.CopyVec(rr) - dkgring.NTTVec(r, cN) - dkgring.NTTVec(r, rN) - ac := dkgring.NewVec(r, prof.K) - br := dkgring.NewVec(r, prof.K) - dkgring.MatVecMul(r, prof.A, cN, ac) - dkgring.MatVecMul(r, prof.B, rN, br) - sumC := dkgring.NewVec(r, prof.K) // plain-NTT aggregated commit - dkgring.VecAdd(r, ac, br, sumC) - - // vss convention: INTT only → TRUE A·s1+B·u (standard coeff form). - vssT := dkgring.CopyVec(sumC) - dkgring.ConvertVecFromNTT(r, vssT) // INTT only - - // dkg2 convention: IMForm + INTT → R^{-1}-scaled. - base := prof.Ring.Base() - dkg2T := dkgring.CopyVec(sumC) - for i := range dkg2T { - base.IMForm(dkg2T[i], dkg2T[i]) - base.INTT(dkg2T[i], dkg2T[i]) - } - - // They must DIFFER (the conventions genuinely diverge)... - if vecsEqual(vssT, dkg2T) { - t.Fatal("expected the IMForm+INTT and INTT-only conventions to differ; they did not") - } - - // ...and differ by EXACTLY the Montgomery factor R: MForm(dkg2T) == vssT. - reMont := dkgring.CopyVec(dkg2T) - for i := range reMont { - base.MForm(reMont[i], reMont[i]) - } - if !vecsEqual(reMont, vssT) { - t.Fatal("MForm(dkg2 b_ped pre-round) != vss T: the conventions do not differ by exactly R") - } - t.Log("PIN 4 confirmed: dkg2 b_ped (IMForm+INTT) = vss T (INTT-only) scaled by R^{-1}; " + - "production keyera Path-(a) key is the TRUE A·s+e'' (R-cancelled), convention-aligned with vss T") -} - -// TestCutover_Round3AggregationByteIdentical — gate (E), the LAST byte-equality -// the production swap rests on. Feeding dkg2's (gate-B/C-proven byte-identical) -// per-dealer commits/shares/blinds into vss's REAL Round3 must: -// -// (1) pass vss's verifyPedersen for EVERY dealer — proving vss's verify accepts -// dkg2's contributions cross-implementation (despite dkg2's "NTT-Montgomery" -// vs vss's "plain-NTT" commit-domain comments, the bytes and the Pedersen -// identity agree); and -// (2) produce the aggregated secret share s_j = Σ_i f_i(j) BYTE-IDENTICAL to -// dkg2's Round2Identify. -// -// gate C proved each per-dealer share f_i(j) is identical; this pins the -// AGGREGATION + VERIFY step (vss.aggregateShare / vss.verifyPedersen == -// dkg2.Round2Identify). With gates A–E green, BootstrapPedersen can drive vss -// (Round1 → OpenDealerShare → Round3.ShareResult.Share) for the share-dealing DKG -// and keep corona's own Path-(a) noise-flooding finalize, preserving the group -// key + per-party shares byte-for-byte — i.e. deployed corona signatures survive. -func TestCutover_Round3AggregationByteIdentical(t *testing.T) { - const n, thr = 5, 3 - suite := hash.NewCoronaSHA3() - - // dkg2 reference: full Round1 for all n parties. - dkgParams, err := dkg2.NewParams() - if err != nil { - t.Fatalf("dkg2.NewParams: %v", err) - } - sessions := make([]*dkg2.DKGSession, n) - round1 := make([]*dkg2.Round1Output, n) - for i := 0; i < n; i++ { - sess, err := dkg2.NewDKGSession(dkgParams, i, n, thr, suite) - if err != nil { - t.Fatalf("NewDKGSession[%d]: %v", i, err) - } - sessions[i] = sess - seed := make([]byte, sign.KeySize) - for k := range seed { - seed[k] = byte(0x07*i + 0x13*k + 1) - } - out, err := sess.Round1WithSeed(seed) - if err != nil { - t.Fatalf("Round1WithSeed[%d]: %v", i, err) - } - round1[i] = out - } - - prof, err := dkgring.Ringtail() - if err != nil { - t.Fatalf("ring.Ringtail: %v", err) - } - - for j := 0; j < n; j++ { - // dkg2 aggregated, verified share s_j (the production Round 2 path). - shares := map[int]structs.Vector[lring.Poly]{} - blinds := map[int]structs.Vector[lring.Poly]{} - commits := map[int][]structs.Vector[lring.Poly]{} - for i := 0; i < n; i++ { - shares[i] = round1[i].Shares[j] - blinds[i] = round1[i].Blinds[j] - commits[i] = round1[i].Commits - } - dkg2Sj, _, _, badID, err := sessions[j].Round2Identify(shares, blinds, commits) - if err != nil { - t.Fatalf("dkg2 Round2Identify[%d] (badID=%d): %v", j, badID, err) - } - - // vss real Round3 over the SAME (commits, share, blind) inputs. - party := buildVSSPartyAt(t, prof, j, n, thr) - inputs := make(map[int]*dkgvss.DealerInput, n) - for i := 0; i < n; i++ { - inputs[i] = &dkgvss.DealerInput{ - Commits: round1[i].Commits, - Share: round1[i].Shares[j], - Blind: round1[i].Blinds[j], - } - } - res, fault, err := party.Round3(inputs) - if err != nil { - if fault != nil { - t.Fatalf("vss Round3[%d]: verifyPedersen REJECTED dkg2 dealer %d — commit-domain conventions diverge (blocker)", j, fault.DealerIndex) - } - t.Fatalf("vss Round3[%d]: %v", j, err) - } - if !vecsEqual(res.Share, dkg2Sj) { - t.Fatalf("aggregated s_%d byte-divergent: vss Round3 aggregateShare != dkg2 Round2Identify", j) - } - } - t.Logf("vss Round3 (verifyPedersen + aggregateShare) byte-identical to dkg2 Round2Identify for all %d recipients", n) -} diff --git a/keyera/keyera.go b/keyera/keyera.go index c8ddfe1..4621b5b 100644 --- a/keyera/keyera.go +++ b/keyera/keyera.go @@ -135,15 +135,16 @@ type EpochShareState struct { // Bootstrap opens a new key era WITHOUT a trusted dealer. // // TRUST MODEL — PUBLIC-BFT-SAFE (default). -// This function routes the keygen ceremony through `dkg2/` (Pedersen- -// DKG over R_q) + Path (a) noise flooding so no single party ever -// holds the master secret s at any point in the ceremony. Every -// validator runs `dkg2.Round1` independently; the per-party noise -// contributions aggregate into a Corona-Sign-shaped public key +// This function routes the keygen ceremony through the shared +// github.com/luxfi/dkg Pedersen-VSS (vss, parameterized by +// ring.Ringtail()) + corona's Path (a) noise flooding so no single +// party ever holds the master secret s at any point in the ceremony. +// Every validator runs the vss Round 1 independently; the per-party +// noise contributions aggregate into a Corona-Sign-shaped public key // `bTilde = Round_Xi(A·s + e”)` where each party adds one Gaussian // `e_j'` slice under σ” = κ·σ_E·√n. Identifiable abort: a -// misbehaving sender is named in a signed `dkg2.Complaint` carrying -// re-checkable Pedersen evidence; the chain commits the abort +// misbehaving sender is named in a signed luxfi/dkg blame complaint +// carrying re-checkable Pedersen evidence; the chain commits the abort // transcript and stays at the previous epoch. // // On success returns (era, transcript, nil). The transcript is the @@ -376,11 +377,12 @@ func (era *KeyEra) Reshare(newValidators []string, newThreshold int, randSource // Corona-SHA3) call ReanchorWithSuite. // // TRUST MODEL — PUBLIC-BFT-SAFE. -// This function routes through dkg2/ (Pedersen-DKG over R_q) so no -// party ever holds the master secret s for the new era at any point -// in the rotation. The dealer that previously held s for the prior -// era is NOT re-trusted; every validator runs dkg2.Round1 -// independently and Path (a) noise flooding aggregates the result. +// This function routes through the shared github.com/luxfi/dkg +// Pedersen-VSS (vss) so no party ever holds the master secret s for +// the new era at any point in the rotation. The dealer that previously +// held s for the prior era is NOT re-trusted; every validator runs the +// vss Round 1 independently and Path (a) noise flooding aggregates the +// result. // // LEGACY ALTERNATIVE: use ReanchorTrustedDealer, which runs the // single-dealer Shamir share-out (byte-equivalent to the v0.7.3 and diff --git a/keyera/pin4_convention_test.go b/keyera/pin4_convention_test.go new file mode 100644 index 0000000..2bd167e --- /dev/null +++ b/keyera/pin4_convention_test.go @@ -0,0 +1,128 @@ +// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package keyera + +// pin4_convention_test.go — the PIN-4 group-key NTT-convention assertion +// (HANDOFF-PHASE3.md). It is the durable, dkg2-free record of WHY the move to +// luxfi/dkg preserved corona's group key. +// +// Two ways to materialize the aggregated Pedersen commit ΣC ∈ R_q^K (plain-NTT) +// into coefficient form: +// +// - IMForm + INTT : the historical corona/dkg2 Round2 b_ped path. Because the +// commits are plain-NTT (Montgomery cancels in the commit multiply), the +// extra IMForm injects one spurious R^{-1}, so this is R^{-1}-scaled. +// - INTT only : the luxfi/dkg vss group-commit T path — the TRUE A·s1+B·u. +// +// The two differ by EXACTLY the Montgomery factor R (MForm(IMForm+INTT) == INTT). +// corona's PRODUCTION group key is NEITHER: keyera Path-(a) noise flooding +// multiplies NTT-Mont operands (A · NTT-Mont(λ_j·s_j)), so the surviving R is +// correctly removed by ConvertVectorFromNTT, yielding the TRUE A·s+e'' — +// convention-aligned with vss's TRUE T. The dkg2 R^{-1}-scaled b_ped was an +// internal Round2 value the production path always discarded, which is why the +// rewire (keep corona's Path-(a) finalize, use vss only for share-dealing) +// preserves the group key with NO convention change. + +import ( + "testing" + + "github.com/luxfi/corona/sign" + + dkgring "github.com/luxfi/dkg/ring" + + lring "github.com/luxfi/lattice/v7/ring" + "github.com/luxfi/lattice/v7/utils/structs" +) + +// polysEqual reports byte-equality of two single-modulus polynomials over all +// coefficients (Coeffs[0]); the corona ring is single-level so this is total. +func polysEqual(a, b lring.Poly) bool { + if len(a.Coeffs) != len(b.Coeffs) { + return false + } + for lvl := range a.Coeffs { + if len(a.Coeffs[lvl]) != len(b.Coeffs[lvl]) { + return false + } + for i := range a.Coeffs[lvl] { + if a.Coeffs[lvl][i] != b.Coeffs[lvl][i] { + return false + } + } + } + return true +} + +func vecsEqual(a, b structs.Vector[lring.Poly]) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !polysEqual(a[i], b[i]) { + return false + } + } + return true +} + +// TestPin4_GroupKeyConvention pins the exact NTT-domain relationship between the +// two group-commit conventions against ground truth (see file header). +func TestPin4_GroupKeyConvention(t *testing.T) { + prof, err := dkgring.Ringtail() + if err != nil { + t.Fatalf("ring.Ringtail: %v", err) + } + r := prof.Ring + + // Deterministic plain-NTT "aggregated commit" ΣC ∈ R_q^K: build it as a real + // commit A·NTT(c) + B·NTT(r) so it lives in the exact commit domain. + seed := make([]byte, sign.KeySize) + for i := range seed { + seed[i] = byte(0x33*i + 1) + } + prng, err := dkgring.NewKeyedPRNG(seed) + if err != nil { + t.Fatalf("NewKeyedPRNG: %v", err) + } + c := prof.SampleSecretVec(prng) + rr := prof.SampleSecretVec(prng) + cN := dkgring.CopyVec(c) + rN := dkgring.CopyVec(rr) + dkgring.NTTVec(r, cN) + dkgring.NTTVec(r, rN) + ac := dkgring.NewVec(r, prof.K) + br := dkgring.NewVec(r, prof.K) + dkgring.MatVecMul(r, prof.A, cN, ac) + dkgring.MatVecMul(r, prof.B, rN, br) + sumC := dkgring.NewVec(r, prof.K) // plain-NTT aggregated commit + dkgring.VecAdd(r, ac, br, sumC) + + // vss convention: INTT only → TRUE A·s1+B·u (standard coeff form). + vssT := dkgring.CopyVec(sumC) + dkgring.ConvertVecFromNTT(r, vssT) // INTT only + + // dkg2 convention: IMForm + INTT → R^{-1}-scaled. + base := prof.Ring.Base() + dkg2T := dkgring.CopyVec(sumC) + for i := range dkg2T { + base.IMForm(dkg2T[i], dkg2T[i]) + base.INTT(dkg2T[i], dkg2T[i]) + } + + // They must DIFFER (the conventions genuinely diverge)... + if vecsEqual(vssT, dkg2T) { + t.Fatal("expected the IMForm+INTT and INTT-only conventions to differ; they did not") + } + + // ...and differ by EXACTLY the Montgomery factor R: MForm(dkg2T) == vssT. + reMont := dkgring.CopyVec(dkg2T) + for i := range reMont { + base.MForm(reMont[i], reMont[i]) + } + if !vecsEqual(reMont, vssT) { + t.Fatal("MForm(dkg2 b_ped pre-round) != vss T: the conventions do not differ by exactly R") + } + t.Log("PIN 4 confirmed: dkg2 b_ped (IMForm+INTT) = vss T (INTT-only) scaled by R^{-1}; " + + "production keyera Path-(a) key is the TRUE A·s+e'' (R-cancelled), convention-aligned with vss T") +} diff --git a/keyera/reanchor_pedersen.go b/keyera/reanchor_pedersen.go index c9b6d09..57c3953 100644 --- a/keyera/reanchor_pedersen.go +++ b/keyera/reanchor_pedersen.go @@ -12,8 +12,9 @@ import ( // ReanchorPedersen opens a new key era WITHOUT a trusted dealer. // // This is the public-BFT-safe Reanchor entrypoint: the keygen ceremony -// for the new era routes through dkg2/ (Pedersen-DKG over R_q) + Path -// (a) noise flooding, exactly like BootstrapPedersen does at chain +// for the new era routes through the shared github.com/luxfi/dkg +// Pedersen-VSS (vss) + Path (a) noise flooding, exactly like +// BootstrapPedersen does at chain // genesis. No party ever holds the master secret s for the new era at // any point in the rotation. The previous era's GroupKey is discarded // (Reanchor is the only lifecycle operation that may do so). diff --git a/threshold/bench_gpu_test.go b/threshold/bench_gpu_test.go index 7ebc88d..e0486b9 100644 --- a/threshold/bench_gpu_test.go +++ b/threshold/bench_gpu_test.go @@ -34,7 +34,8 @@ import ( // engine layer, bypassing the per-poly r.NTT() pinch point. That // kernel slot exists (see lattice/gpu/gpu_cgo.go::BatchNTT) but is // not yet plumbed through r.NTT — that's the v0.6+ NIST submission -// pipeline work referenced in corona/dkg2/dkg2_gpu_accel.go. +// pipeline work (the dealerless DKG GPU path now lives in the shared +// github.com/luxfi/dkg library). // // CPU vs GPU pairs (one bench function each) let `go test -bench .` // emit a side-by-side comparison without bench-fixture trickery; the