mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
Add corona/gpu package — the single, decomplecting point where corona opts into the lattice library's per-SubRing GPU NTT dispatcher. Architecture (decomplected). The lattice library already owns ALL build-tag plumbing for GPU NTT: ring.SetGPUDispatchers (subring_ops.go) is the canonical hook; the lattice/gpu package installs it under `cgo && gpu` build tags and provides a real CPU fallback under !cgo or !gpu. Output is byte-equal to ring.SubRing.NTT by lattice's own contract. corona/gpu adds the corona-side bridge: UseAccelerator() flips a global flag, NewParams() across corona consults the flag via MaybeRegister and binds each created Ring's SubRings into the lattice GPU registry. Single source of truth for the opt-in; no build tags inside corona. Threshold gating (honest). Single-poly Metal NTT at corona's production N=256 is roughly 4-6x SLOWER than pure-Go ring.SubRing.NTT (measured: BenchmarkPulsarSign_ 5of7 force-GPU 7.1s vs CPU 1.1s; 14of21 force-GPU 23.5s vs CPU 5.9s). The GPU win exists only in BATCHED dispatch (many polynomials per kernel launch), which requires future engine-layer plumbing of lattice/gpu.MontgomeryNTTContext.Forward(data, batch>=4) bypassing the per-poly r.NTT() pinch point. Therefore UseAccelerator() picks defaultThreshold=1024 — above corona's N=256 — so the SubRing dispatch is armed but does not fire on single-poly NTT. The registry remains primed for any future batch caller (e.g. FHE bootstraps in thresholdvm sharing this library). UseAcceleratorForce() (threshold=1) is provided strictly for the correctness gate: every NTT call routes through the GPU so the byte-equality test in threshold/threshold_gpu_test.go exercises the GPU path end-to-end. Production callers use UseAccelerator() instead. Byte-equality. TestThresholdSign_CPU_vs_GPU_ByteIdentical runs the full 2-round Pulsar signing protocol with GPU dispatch off and forced-on (same deterministic dealer randomness, same message) and asserts byte-equal sig.C / sig.Z / sig.Delta. Passes under CGO_ENABLED=0, CGO_ENABLED=1, and CGO_ENABLED=1 -tags gpu. Existing TestDKG2_GPU_ByteEqual coverage extends across n=3,5,7,11,21 (production shape). Wiring. NewParams() in sign-bound packages — threshold, dkg2, dkg, reshare — calls corona/gpu.MaybeRegister(r) for the main Q ring. RXi and RNu are power-of-two moduli; the NTT path is not taken on them. Tests. Full corona test suite passes under both build modes: - CGO_ENABLED=0 go test ./... => all green - CGO_ENABLED=1 -tags gpu test => all green
232 lines
6.4 KiB
Go
232 lines
6.4 KiB
Go
// 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)
|
|
}
|
|
}
|
|
}
|