mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
pulsar-m: NIST MPTC submission readiness — benchmarks + CI gates
Final v0.1 polish:
bench_test.go — reproducible benchmark harness covering KeyGen,
Sign, Verify across all three FIPS 204 parameter
sets, plus DKG, ThresholdSign, and Reshare at
representative committee sizes for ML-DSA-65.
The benchmark harness drives scripts/bench.sh; results land in
bench/results/{fingerprint.txt,go-bench.txt}. Apple M1 Max baseline
(Go 1.26.2, -benchtime=2s -count=3):
KeyGen P44 ~ 0.3 ms, P65 ~ 0.5 ms, P87 ~ 0.7 ms
Sign P44 ~ 1.6 ms, P65 ~ 2.5 ms, P87 ~ 3.0 ms
Verify P44 ~ 0.3 ms, P65 ~ 0.5 ms, P87 ~ 0.7 ms
DKG (5,3) ~ 3.5 ms, (7,4) ~ 7 ms, (10,7) ~ 12 ms
Threshold (5,3) ~ 2.2 ms, (7,4) ~ 4.5 ms, (10,7) ~ 7 ms
Reshare (5→5,t=3) ~ 0.5 ms
The threshold path is dominated by FIPS 204 Sign latency for small
quorums; at (n=10, t=7) the 2-round overhead becomes comparable to
the FIPS 204 cost.
This commit closes the v0.1 reference impl gates:
- scripts/build.sh passes
- scripts/test.sh passes (61 tests, race-clean)
- scripts/gen_vectors.sh passes; KATs deterministic
- scripts/bench.sh passes
- Class N1 interchangeability demonstrated by every threshold KAT
in vectors/threshold-sign.json passing under pulsarm.Verify
(i.e. under unmodified mldsa{44,65,87}.Verify)
docs/known-limitations.md updated to reflect v0.1 reality: the
reconstruction-aggregator path ships in v0.1; the Lagrange-linearity
sign and R_q^k Pedersen are documented v0.2 paths.
Signed-off-by: Lux Industries <[email protected]>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsarm
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkKeyGen_P44(b *testing.B) { benchKeyGen(b, ModeP44) }
|
||||
func BenchmarkKeyGen_P65(b *testing.B) { benchKeyGen(b, ModeP65) }
|
||||
func BenchmarkKeyGen_P87(b *testing.B) { benchKeyGen(b, ModeP87) }
|
||||
|
||||
func benchKeyGen(b *testing.B, mode Mode) {
|
||||
params := MustParamsFor(mode)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateKey(params, rand.Reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSign_P44(b *testing.B) { benchSign(b, ModeP44) }
|
||||
func BenchmarkSign_P65(b *testing.B) { benchSign(b, ModeP65) }
|
||||
func BenchmarkSign_P87(b *testing.B) { benchSign(b, ModeP87) }
|
||||
|
||||
func benchSign(b *testing.B, mode Mode) {
|
||||
params := MustParamsFor(mode)
|
||||
sk, _ := GenerateKey(params, rand.Reader)
|
||||
msg := []byte("benchmark message — measures FIPS 204 Sign latency end-to-end")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := Sign(params, sk, msg, nil, true, rand.Reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerify_P44(b *testing.B) { benchVerify(b, ModeP44) }
|
||||
func BenchmarkVerify_P65(b *testing.B) { benchVerify(b, ModeP65) }
|
||||
func BenchmarkVerify_P87(b *testing.B) { benchVerify(b, ModeP87) }
|
||||
|
||||
func benchVerify(b *testing.B, mode Mode) {
|
||||
params := MustParamsFor(mode)
|
||||
sk, _ := GenerateKey(params, rand.Reader)
|
||||
msg := []byte("benchmark message — measures FIPS 204 Verify latency end-to-end")
|
||||
sig, _ := Sign(params, sk, msg, nil, true, rand.Reader)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := Verify(params, sk.Pub, msg, sig); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDKG_5of3_P65(b *testing.B) { benchDKG(b, 5, 3, ModeP65) }
|
||||
func BenchmarkDKG_7of4_P65(b *testing.B) { benchDKG(b, 7, 4, ModeP65) }
|
||||
func BenchmarkDKG_10of7_P65(b *testing.B) { benchDKG(b, 10, 7, ModeP65) }
|
||||
|
||||
func benchDKG(b *testing.B, n, t int, mode Mode) {
|
||||
params := MustParamsFor(mode)
|
||||
committee := makeCommittee(n)
|
||||
b.ResetTimer()
|
||||
for iter := 0; iter < b.N; iter++ {
|
||||
sessions := make([]*DKGSession, n)
|
||||
for i := range sessions {
|
||||
rng := deterministicReader([]byte{byte(iter), byte(i)})
|
||||
s, _ := NewDKGSession(params, committee, t, committee[i], rng)
|
||||
sessions[i] = s
|
||||
}
|
||||
r1 := make([]*DKGRound1Msg, n)
|
||||
for i, s := range sessions {
|
||||
r1[i], _ = s.Round1()
|
||||
}
|
||||
r2 := make([]*DKGRound2Msg, n)
|
||||
for i, s := range sessions {
|
||||
r2[i], _ = s.Round2(r1)
|
||||
}
|
||||
for _, s := range sessions {
|
||||
_, _ = s.Round3(r1, r2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkThresholdSign_5of3_P65(b *testing.B) { benchThresholdSign(b, 5, 3, ModeP65) }
|
||||
func BenchmarkThresholdSign_7of4_P65(b *testing.B) { benchThresholdSign(b, 7, 4, ModeP65) }
|
||||
func BenchmarkThresholdSign_10of7_P65(b *testing.B) { benchThresholdSign(b, 10, 7, ModeP65) }
|
||||
|
||||
func benchThresholdSign(b *testing.B, n, t int, mode Mode) {
|
||||
params := MustParamsFor(mode)
|
||||
committee := makeCommittee(n)
|
||||
// Set up DKG once outside the timer.
|
||||
sessions := make([]*DKGSession, n)
|
||||
for i := range sessions {
|
||||
rng := deterministicReader([]byte{byte(i), 0xBE})
|
||||
s, _ := NewDKGSession(params, committee, t, committee[i], rng)
|
||||
sessions[i] = s
|
||||
}
|
||||
r1 := make([]*DKGRound1Msg, n)
|
||||
for i, s := range sessions {
|
||||
r1[i], _ = s.Round1()
|
||||
}
|
||||
r2 := make([]*DKGRound2Msg, n)
|
||||
for i, s := range sessions {
|
||||
r2[i], _ = s.Round2(r1)
|
||||
}
|
||||
outputs := make([]*DKGOutput, n)
|
||||
for i, s := range sessions {
|
||||
outputs[i], _ = s.Round3(r1, r2)
|
||||
}
|
||||
pub := outputs[0].GroupPubkey
|
||||
shares := make([]*KeyShare, n)
|
||||
for i := range outputs {
|
||||
shares[i] = outputs[i].SecretShare
|
||||
}
|
||||
msg := []byte("benchmark threshold sign — Class N1 round trip")
|
||||
quorum := make([]NodeID, t)
|
||||
for i := 0; i < t; i++ {
|
||||
quorum[i] = shares[i].NodeID
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for iter := 0; iter < b.N; iter++ {
|
||||
var sid [16]byte
|
||||
sid[0] = byte(iter)
|
||||
sid[1] = byte(iter >> 8)
|
||||
signers := make([]*ThresholdSigner, t)
|
||||
for i := 0; i < t; i++ {
|
||||
rng := deterministicReader([]byte{byte(iter), byte(i)})
|
||||
signers[i], _ = NewThresholdSigner(params, sid, 1, quorum, shares[i], msg, rng)
|
||||
}
|
||||
sr1 := make([]*Round1Message, t)
|
||||
for i, s := range signers {
|
||||
sr1[i], _ = s.Round1(msg)
|
||||
}
|
||||
sr2 := make([]*Round2Message, t)
|
||||
for i, s := range signers {
|
||||
sr2[i], _, _ = s.Round2(sr1)
|
||||
}
|
||||
_, err := Combine(params, pub, msg, nil, false, sid, 1, quorum, t, sr1, sr2, shares)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReshare_5to5_t3_P65(b *testing.B) { benchReshare(b, 5, 3, 5, 3, ModeP65) }
|
||||
|
||||
func benchReshare(b *testing.B, oldN, oldT, newN, newT int, mode Mode) {
|
||||
params := MustParamsFor(mode)
|
||||
oldCommittee := makeCommittee(oldN)
|
||||
newCommittee := makeCommittee(newN)
|
||||
// Set up DKG to get the old shares.
|
||||
sessions := make([]*DKGSession, oldN)
|
||||
for i := range sessions {
|
||||
rng := deterministicReader([]byte{byte(i), 0xCD})
|
||||
s, _ := NewDKGSession(params, oldCommittee, oldT, oldCommittee[i], rng)
|
||||
sessions[i] = s
|
||||
}
|
||||
r1 := make([]*DKGRound1Msg, oldN)
|
||||
for i, s := range sessions {
|
||||
r1[i], _ = s.Round1()
|
||||
}
|
||||
r2 := make([]*DKGRound2Msg, oldN)
|
||||
for i, s := range sessions {
|
||||
r2[i], _ = s.Round2(r1)
|
||||
}
|
||||
outputs := make([]*DKGOutput, oldN)
|
||||
for i, s := range sessions {
|
||||
outputs[i], _ = s.Round3(r1, r2)
|
||||
}
|
||||
oldShares := make([]*KeyShare, oldN)
|
||||
for i := range outputs {
|
||||
oldShares[i] = outputs[i].SecretShare
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for iter := 0; iter < b.N; iter++ {
|
||||
resh := make([]*ReshareSession, 0)
|
||||
for i := 0; i < oldN; i++ {
|
||||
var os *KeyShare
|
||||
if i < oldT {
|
||||
os = oldShares[i]
|
||||
}
|
||||
s, _ := NewReshareSession(params, oldCommittee, oldT, newCommittee, newT,
|
||||
oldCommittee[i], os, nil, deterministicReader([]byte{byte(iter), byte(i)}))
|
||||
resh = append(resh, s)
|
||||
}
|
||||
rr1 := []*DKGRound1Msg{}
|
||||
for _, s := range resh {
|
||||
if !s.InReshareQuorum() {
|
||||
continue
|
||||
}
|
||||
m, _ := s.Round1()
|
||||
rr1 = append(rr1, m)
|
||||
}
|
||||
rr2 := []*DKGRound2Msg{}
|
||||
for _, s := range resh {
|
||||
if !s.InReshareQuorum() {
|
||||
continue
|
||||
}
|
||||
m, _ := s.Round2(rr1)
|
||||
rr2 = append(rr2, m)
|
||||
}
|
||||
for _, s := range resh {
|
||||
_, _, _ = s.Round3(rr1, rr2)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user