mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
refactor(corona): delete the dkg2 package — superseded by luxfi/dkg vss
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.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// Package main is the Corona cross-runtime KAT oracle.
|
// Package main is the Corona cross-runtime KAT oracle.
|
||||||
//
|
//
|
||||||
// Emits a single JSON manifest at <out>/cross_runtime_kat.json that ties
|
// Emits a single JSON manifest at <out>/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
|
// the SHA-256 of each individual KAT file. The C++ side
|
||||||
// (luxcpp/crypto/corona/test/cross_runtime_test.cpp) replays each KAT
|
// (luxcpp/crypto/corona/test/cross_runtime_test.cpp) replays each KAT
|
||||||
// in C++ and verifies byte-equality; running this oracle first then
|
// 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)
|
fmt.Fprintf(os.Stderr, "sign_oracle: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// reshare_oracle and dkg2_oracle don't expose --out; they write to
|
// reshare_oracle doesn't expose --out; it writes to a hardcoded location
|
||||||
// hardcoded locations in luxcpp/crypto. For the cross-runtime gate
|
// in luxcpp/crypto. For the cross-runtime gate we hash the canonical path.
|
||||||
// we hash the canonical paths.
|
//
|
||||||
|
// 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")
|
signPath := filepath.Join(*out, "sign_kat.json")
|
||||||
resharePath := filepath.Join(luxcppDir(), "crypto/corona/test/kat/reshare_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 {
|
files := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -86,7 +88,6 @@ func main() {
|
|||||||
}{
|
}{
|
||||||
{"sign", signPath},
|
{"sign", signPath},
|
||||||
{"reshare", resharePath},
|
{"reshare", resharePath},
|
||||||
{"dkg2", dkg2Path},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m := manifest{
|
m := manifest{
|
||||||
|
|||||||
@@ -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: <luxcpp/crypto>/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]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
-768
@@ -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<t},
|
|
||||||
// broadcasts {C_{i,k}}_{k=0..t-1}, sends (share_{i→j}, blind_{i→j})
|
|
||||||
// privately to each recipient j over an authenticated p2p channel.
|
|
||||||
//
|
|
||||||
// Round 1.5 Every party broadcasts H_i = HashSuite.TranscriptHash(serialize
|
|
||||||
// (C_{i,0}) || ... || serialize(C_{i,t-1})). Recipients compare
|
|
||||||
// digests received from each sender across the cohort. Mismatch
|
|
||||||
// → equivocation → signed Complaint (ComplaintEquivocation) →
|
|
||||||
// sender disqualified.
|
|
||||||
//
|
|
||||||
// Round 2 Each recipient j verifies, for every sender i,
|
|
||||||
// A · NTT(share_{i→j}) + B · NTT(blind_{i→j})
|
|
||||||
// ?= Σ_{k=0..t-1} (j+1)^k · C_{i,k} (mod q)
|
|
||||||
// Verification is exact: both sides are Z_q-linear and NTT is
|
|
||||||
// bijective. Comparison is constant-time across all M·N_vec slots
|
|
||||||
// (subtle.ConstantTimeCompare). On mismatch, recipient emits a
|
|
||||||
// signed Complaint (ComplaintBadDelivery) naming the sender and
|
|
||||||
// carrying (share, blind, commits) as evidence.
|
|
||||||
//
|
|
||||||
// Aggregation: s_j = Σ_i share_{i→j}, u_j = Σ_i blind_{i→j},
|
|
||||||
// b_ped = Round_Xi(IMForm + INTT(Σ_i C_{i,0})).
|
|
||||||
//
|
|
||||||
// # Hash suite
|
|
||||||
//
|
|
||||||
// dkg2 routes every cohort-bound digest through the canonical
|
|
||||||
// hash.HashSuite (Corona-SHA3 in production; Corona-BLAKE3 retained for
|
|
||||||
// byte-equality with pre-cutover KATs). NewDKGSession accepts a HashSuite;
|
|
||||||
// nil resolves to the production default. Matrix derivation (A, B) uses a
|
|
||||||
// dedicated, version-pinned BLAKE3 path to keep KAT bytes stable across
|
|
||||||
// the SHA3 cutover — the matrix derivation is structural, not transcript-
|
|
||||||
// bound, so it has its own version tag (corona.dkg2.A.v1 / .B.v1). The
|
|
||||||
// Round 1.5 commit digest, by contrast, is HashSuite-bound and uses the
|
|
||||||
// CORONA-TRANSCRIPT-v1 customization of the active suite.
|
|
||||||
//
|
|
||||||
// # Identifiable abort
|
|
||||||
//
|
|
||||||
// Round1.5 and Round 2 produce signed Complaint records (Ed25519, mirroring
|
|
||||||
// reshare.Complaint). Aggregating any DisqualificationThreshold complaints
|
|
||||||
// against the same sender disqualifies that sender deterministically; every
|
|
||||||
// honest party that processes the same complaint set computes the same
|
|
||||||
// disqualified set (FilterQualifiedQuorum).
|
|
||||||
//
|
|
||||||
// # File-level invariants
|
|
||||||
//
|
|
||||||
// - All ring arithmetic uses sign.Q (the Corona 48-bit prime).
|
|
||||||
// - Sampler parameters reuse sign.SigmaE / sign.BoundE for both c_{i,k}
|
|
||||||
// and r_{i,k}, mirroring the Corona secret distribution.
|
|
||||||
// - A is derived from the 16-byte tag b"corona.dkg2.A.v1" via BLAKE3-XOF
|
|
||||||
// (KAT-pinned).
|
|
||||||
// - B is derived from the 16-byte tag b"corona.dkg2.B.v1" via BLAKE3-XOF
|
|
||||||
// (KAT-pinned).
|
|
||||||
// - Commits are stored in NTT-Montgomery form (matches A, B).
|
|
||||||
// - Shares are stored in standard coefficient form (NTT=false, mont=false).
|
|
||||||
// - Round 2 verifier comparison is constant-time across the M-element LHS/
|
|
||||||
// RHS pair (constant-time AND across all M coefficient blobs).
|
|
||||||
//
|
|
||||||
// # KAT contract
|
|
||||||
//
|
|
||||||
// Round1WithSeed pins every byte of the protocol output for byte-equal C++
|
|
||||||
// porting. See cmd/dkg2_oracle for the canonical generator and
|
|
||||||
// luxcpp/crypto/corona/dkg2/test/kat/dkg2_kat.json for the 4 reference
|
|
||||||
// entries (2-of-3, 3-of-5, 5-of-7, 7-of-11).
|
|
||||||
package dkg2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
cgpu "github.com/luxfi/corona/gpu"
|
|
||||||
"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"
|
|
||||||
|
|
||||||
"github.com/zeebo/blake3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Domain-separation tags used to derive the public matrices A, B.
|
|
||||||
//
|
|
||||||
// The bytes themselves are nothing-up-my-sleeve: an ASCII string identifying
|
|
||||||
// the matrix and a version suffix. Changing either tag invalidates every
|
|
||||||
// KAT and every group public key derived in dkg2 — bump the version when
|
|
||||||
// breaking compatibility.
|
|
||||||
//
|
|
||||||
// Matrix derivation is BLAKE3 directly, NOT the active HashSuite. This keeps
|
|
||||||
// public-matrix bytes stable across the Corona-SHA3 cutover (the matrices
|
|
||||||
// are structural, not transcript-bound; their version tag covers any future
|
|
||||||
// rotation).
|
|
||||||
var (
|
|
||||||
tagA = []byte("corona.dkg2.A.v1")
|
|
||||||
tagB = []byte("corona.dkg2.B.v1")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Customization tag bound into the Round 1.5 commit-digest under the active
|
|
||||||
// HashSuite. The suite ID is bound into the digest input as well so two
|
|
||||||
// suites can never produce a colliding digest for the same commit vector.
|
|
||||||
const tagCommitDigest = "CORONA-DKG2-COMMIT-DIGEST-v1"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidThreshold = errors.New("dkg2: threshold must be > 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<<sign.LogN, []uint64{sign.Q})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rXi, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QXi})
|
|
||||||
// Best-effort GPU NTT registration when corona/gpu has been opted
|
|
||||||
// into. No-op when disabled or on non-GPU builds; identical bytes.
|
|
||||||
cgpu.MaybeRegister(r)
|
|
||||||
return &Params{R: r, RXi: rXi}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// derivePublicMatrix builds an M×Nvec uniform matrix in R_q from a
|
|
||||||
// nothing-up-my-sleeve tag using BLAKE3 → KeyedPRNG → UniformSampler.
|
|
||||||
//
|
|
||||||
// Returned matrix is in NTT-Montgomery form (matches the convention used by
|
|
||||||
// the Sign Gen path at sign/sign.go:49). BLAKE3 is wired directly here —
|
|
||||||
// see the package-level documentation for why matrix derivation is
|
|
||||||
// deliberately suite-independent.
|
|
||||||
func derivePublicMatrix(r *ring.Ring, tag []byte) (structs.Matrix[ring.Poly], error) {
|
|
||||||
h := blake3.New()
|
|
||||||
if _, err := h.Write(tag); err != nil {
|
|
||||||
return nil, fmt.Errorf("dkg2: derivePublicMatrix: %w", err)
|
|
||||||
}
|
|
||||||
seed := h.Sum(nil)[:sign.KeySize]
|
|
||||||
prng, err := sampling.NewKeyedPRNG(seed)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("dkg2: derivePublicMatrix: %w", err)
|
|
||||||
}
|
|
||||||
uniform := ring.NewUniformSampler(prng, r)
|
|
||||||
return utils.SamplePolyMatrix(r, sign.M, sign.N, uniform, true, true), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveA returns the canonical Pedersen-DKG matrix A in NTT-Mont form.
|
|
||||||
func DeriveA(r *ring.Ring) structs.Matrix[ring.Poly] {
|
|
||||||
m, err := derivePublicMatrix(r, tagA)
|
|
||||||
if err != nil {
|
|
||||||
// derivePublicMatrix failure means BLAKE3 / KeyedPRNG construction
|
|
||||||
// failed under fully-controlled inputs — a deterministic crash here
|
|
||||||
// is correct (cf. sign/local.go's startup posture).
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveB returns the canonical Pedersen-DKG matrix B in NTT-Mont form.
|
|
||||||
func DeriveB(r *ring.Ring) structs.Matrix[ring.Poly] {
|
|
||||||
m, err := derivePublicMatrix(r, tagB)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// Round1Output is one party's contribution to Round 1.
|
|
||||||
//
|
|
||||||
// Commits[k] is the public Pedersen commitment to (c_{i,k}, r_{i,k}); it is
|
|
||||||
// broadcast to every other party. Shares[j] and Blinds[j] are the secret
|
|
||||||
// share-pair sent privately to party j over an authenticated point-to-point
|
|
||||||
// channel.
|
|
||||||
type Round1Output struct {
|
|
||||||
// Commits[k] = A * NTT(c_{i,k}) + B * NTT(r_{i,k}), length t.
|
|
||||||
// Stored in NTT-Montgomery form.
|
|
||||||
Commits []structs.Vector[ring.Poly]
|
|
||||||
|
|
||||||
// Shares[j] = f_i(j+1), length n. Standard coefficient form.
|
|
||||||
Shares map[int]structs.Vector[ring.Poly]
|
|
||||||
|
|
||||||
// Blinds[j] = g_i(j+1), length n. Standard coefficient form.
|
|
||||||
// Sent over the same private channel as Shares[j].
|
|
||||||
Blinds map[int]structs.Vector[ring.Poly]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SerializeCommits returns the canonical wire bytes of Commits. Used as the
|
|
||||||
// hashing pre-image for Round 1.5 digests, equivocation evidence, and KAT
|
|
||||||
// pinning. Errors on the underlying lattigo WriteTo are surfaced as
|
|
||||||
// ErrSerialization.
|
|
||||||
func (r *Round1Output) SerializeCommits() ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for _, v := range r.Commits {
|
|
||||||
if _, err := v.WriteTo(&buf); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSerialization, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitDigest returns the Round 1.5 cross-party-consistency digest under
|
|
||||||
// the supplied HashSuite. Passing nil resolves to the production default
|
|
||||||
// (Corona-SHA3); pass hash.NewCoronaBLAKE3() for byte-equal replay against
|
|
||||||
// the canonical KATs.
|
|
||||||
//
|
|
||||||
// Format (suite-agnostic):
|
|
||||||
//
|
|
||||||
// suite.TranscriptHash([]byte(tagCommitDigest), []byte(suite.ID()),
|
|
||||||
// serialize(Commits[0]) || ... || serialize(Commits[t-1]))
|
|
||||||
//
|
|
||||||
// The suite ID is bound in so two suites can never collide on a single
|
|
||||||
// commit vector. Errors only on serialization failure.
|
|
||||||
func (r *Round1Output) CommitDigest(suite hash.HashSuite) ([32]byte, error) {
|
|
||||||
s := hash.Resolve(suite)
|
|
||||||
body, err := r.SerializeCommits()
|
|
||||||
if err != nil {
|
|
||||||
return [32]byte{}, err
|
|
||||||
}
|
|
||||||
return s.TranscriptHash([]byte(tagCommitDigest), []byte(s.ID()), body), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitDigestBLAKE3 returns the legacy BLAKE3 commit digest used by the
|
|
||||||
// pre-SHA3-cutover KAT (corona/dkg2 oracle, luxcpp dkg2_kat.json). Format:
|
|
||||||
//
|
|
||||||
// BLAKE3(serialize(Commits[0]) || ... || serialize(Commits[t-1]))[:32]
|
|
||||||
//
|
|
||||||
// Kept for byte-stable replay only — production code paths SHOULD use
|
|
||||||
// CommitDigest(hash.Default()).
|
|
||||||
func (r *Round1Output) CommitDigestBLAKE3() ([32]byte, error) {
|
|
||||||
body, err := r.SerializeCommits()
|
|
||||||
if err != nil {
|
|
||||||
return [32]byte{}, err
|
|
||||||
}
|
|
||||||
h := blake3.New()
|
|
||||||
if _, err := h.Write(body); err != nil {
|
|
||||||
return [32]byte{}, fmt.Errorf("%w: %v", ErrSerialization, err)
|
|
||||||
}
|
|
||||||
var out [32]byte
|
|
||||||
copy(out[:], h.Sum(nil)[:32])
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DKGSession tracks the state of one party in the dkg2 protocol.
|
|
||||||
type DKGSession struct {
|
|
||||||
params *Params
|
|
||||||
partyID int
|
|
||||||
n int
|
|
||||||
t int
|
|
||||||
suite hash.HashSuite
|
|
||||||
|
|
||||||
A structs.Matrix[ring.Poly] // public uniform M×Nvec, NTT-Mont
|
|
||||||
B structs.Matrix[ring.Poly] // public uniform M×Nvec, NTT-Mont
|
|
||||||
|
|
||||||
// Stashed across Round1 → Round2 for self-consistency checks.
|
|
||||||
// Standard coefficient form (NTT=false, mont=false).
|
|
||||||
cCoeffs []structs.Vector[ring.Poly] // f_i polynomial coeffs (length t)
|
|
||||||
rCoeffs []structs.Vector[ring.Poly] // g_i polynomial coeffs (length t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDKGSession initializes a Pedersen DKG session for the given party.
|
|
||||||
//
|
|
||||||
// suite parameterizes the cohort-bound hash routines (Round 1.5 commit
|
|
||||||
// digest, complaint transcripts). nil resolves to the production default
|
|
||||||
// (Corona-SHA3). Public-matrix derivation is deliberately HashSuite-
|
|
||||||
// independent — it uses a dedicated BLAKE3 path so KAT bytes stay stable
|
|
||||||
// across the Corona-SHA3 cutover (see package documentation).
|
|
||||||
//
|
|
||||||
// Mirrors dkg.NewDKGSession exactly except that the matrices A, B are
|
|
||||||
// derived from public domain-separated tags via BLAKE3, removing the
|
|
||||||
// "all-zero seedKey" footgun present in dkg/dkg.go:99-105.
|
|
||||||
func NewDKGSession(params *Params, partyID, n, t int, suite hash.HashSuite) (*DKGSession, error) {
|
|
||||||
if n < 2 {
|
|
||||||
return nil, ErrInvalidPartyCount
|
|
||||||
}
|
|
||||||
if t < 1 || t >= 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[:]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
-1382
File diff suppressed because it is too large
Load Diff
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -259,7 +259,7 @@ func TestBootstrapPedersen_NoMasterSecretInMemory(t *testing.T) {
|
|||||||
// "mastersecret" or "masters".
|
// "mastersecret" or "masters".
|
||||||
lower := lowercase(f.Name)
|
lower := lowercase(f.Name)
|
||||||
if contains(lower, "mastersecret") || contains(lower, "fullsecret") {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
+14
-12
@@ -135,15 +135,16 @@ type EpochShareState struct {
|
|||||||
// Bootstrap opens a new key era WITHOUT a trusted dealer.
|
// Bootstrap opens a new key era WITHOUT a trusted dealer.
|
||||||
//
|
//
|
||||||
// TRUST MODEL — PUBLIC-BFT-SAFE (default).
|
// TRUST MODEL — PUBLIC-BFT-SAFE (default).
|
||||||
// This function routes the keygen ceremony through `dkg2/` (Pedersen-
|
// This function routes the keygen ceremony through the shared
|
||||||
// DKG over R_q) + Path (a) noise flooding so no single party ever
|
// github.com/luxfi/dkg Pedersen-VSS (vss, parameterized by
|
||||||
// holds the master secret s at any point in the ceremony. Every
|
// ring.Ringtail()) + corona's Path (a) noise flooding so no single
|
||||||
// validator runs `dkg2.Round1` independently; the per-party noise
|
// party ever holds the master secret s at any point in the ceremony.
|
||||||
// contributions aggregate into a Corona-Sign-shaped public key
|
// 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
|
// `bTilde = Round_Xi(A·s + e”)` where each party adds one Gaussian
|
||||||
// `e_j'` slice under σ” = κ·σ_E·√n. Identifiable abort: a
|
// `e_j'` slice under σ” = κ·σ_E·√n. Identifiable abort: a
|
||||||
// misbehaving sender is named in a signed `dkg2.Complaint` carrying
|
// misbehaving sender is named in a signed luxfi/dkg blame complaint
|
||||||
// re-checkable Pedersen evidence; the chain commits the abort
|
// carrying re-checkable Pedersen evidence; the chain commits the abort
|
||||||
// transcript and stays at the previous epoch.
|
// transcript and stays at the previous epoch.
|
||||||
//
|
//
|
||||||
// On success returns (era, transcript, nil). The transcript is the
|
// 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.
|
// Corona-SHA3) call ReanchorWithSuite.
|
||||||
//
|
//
|
||||||
// TRUST MODEL — PUBLIC-BFT-SAFE.
|
// TRUST MODEL — PUBLIC-BFT-SAFE.
|
||||||
// This function routes through dkg2/ (Pedersen-DKG over R_q) so no
|
// This function routes through the shared github.com/luxfi/dkg
|
||||||
// party ever holds the master secret s for the new era at any point
|
// Pedersen-VSS (vss) so no party ever holds the master secret s for
|
||||||
// in the rotation. The dealer that previously held s for the prior
|
// the new era at any point in the rotation. The dealer that previously
|
||||||
// era is NOT re-trusted; every validator runs dkg2.Round1
|
// held s for the prior era is NOT re-trusted; every validator runs the
|
||||||
// independently and Path (a) noise flooding aggregates the result.
|
// vss Round 1 independently and Path (a) noise flooding aggregates the
|
||||||
|
// result.
|
||||||
//
|
//
|
||||||
// LEGACY ALTERNATIVE: use ReanchorTrustedDealer, which runs the
|
// LEGACY ALTERNATIVE: use ReanchorTrustedDealer, which runs the
|
||||||
// single-dealer Shamir share-out (byte-equivalent to the v0.7.3 and
|
// single-dealer Shamir share-out (byte-equivalent to the v0.7.3 and
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -12,8 +12,9 @@ import (
|
|||||||
// ReanchorPedersen opens a new key era WITHOUT a trusted dealer.
|
// ReanchorPedersen opens a new key era WITHOUT a trusted dealer.
|
||||||
//
|
//
|
||||||
// This is the public-BFT-safe Reanchor entrypoint: the keygen ceremony
|
// This is the public-BFT-safe Reanchor entrypoint: the keygen ceremony
|
||||||
// for the new era routes through dkg2/ (Pedersen-DKG over R_q) + Path
|
// for the new era routes through the shared github.com/luxfi/dkg
|
||||||
// (a) noise flooding, exactly like BootstrapPedersen does at chain
|
// 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
|
// 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
|
// any point in the rotation. The previous era's GroupKey is discarded
|
||||||
// (Reanchor is the only lifecycle operation that may do so).
|
// (Reanchor is the only lifecycle operation that may do so).
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import (
|
|||||||
// engine layer, bypassing the per-poly r.NTT() pinch point. That
|
// engine layer, bypassing the per-poly r.NTT() pinch point. That
|
||||||
// kernel slot exists (see lattice/gpu/gpu_cgo.go::BatchNTT) but is
|
// 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
|
// 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 .`
|
// CPU vs GPU pairs (one bench function each) let `go test -bench .`
|
||||||
// emit a side-by-side comparison without bench-fixture trickery; the
|
// emit a side-by-side comparison without bench-fixture trickery; the
|
||||||
|
|||||||
Reference in New Issue
Block a user