mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
threshold: demote to interface + registry only (BLS impl → luxfi/threshold)
The native BLS Scheme impl (threshold/bls/) moved to luxfi/threshold/scheme/bls so all threshold-signature implementations live in one module. crypto/threshold now holds only the shared contract (Scheme/Signer/Aggregator/Verifier/registry/session/errors/adapter) that crypto/signer + hsm depend on — the one low-level seam that must stay here to avoid a crypto<->threshold module cycle. signer_test repoints its BLS registration to the new path. No production crypto code changes. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
+8
-158
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
_ "github.com/luxfi/crypto/threshold/bls" // Register BLS scheme
|
||||
)
|
||||
|
||||
func TestSigner(t *testing.T) {
|
||||
@@ -21,15 +20,12 @@ func TestSigner(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("BLS sign failed: %v", err)
|
||||
}
|
||||
|
||||
if !signer.VerifyBLS(message, sig) {
|
||||
t.Fatalf("BLS verification failed")
|
||||
}
|
||||
|
||||
if signer.VerifyBLS([]byte("wrong"), sig) {
|
||||
t.Fatalf("BLS should not verify wrong message")
|
||||
}
|
||||
|
||||
t.Logf("BLS signature size: %d bytes", len(sig))
|
||||
t.Logf("BLS public key size: %d bytes", len(signer.GetBLSPublicKey()))
|
||||
})
|
||||
@@ -39,204 +35,58 @@ func TestSigner(t *testing.T) {
|
||||
if len(seed) != 32 {
|
||||
t.Fatalf("Threshold seed should be 32 bytes, got %d", len(seed))
|
||||
}
|
||||
t.Logf("Threshold seed: %x...", seed[:8])
|
||||
})
|
||||
|
||||
// Uninitialized threshold state exercises ONLY the crypto/threshold
|
||||
// interface — no concrete scheme. The BLS-backed integration test lives
|
||||
// in luxfi/threshold/scheme/bls (importing crypto/signer from there keeps
|
||||
// crypto free of any luxfi/threshold dependency — no module cycle).
|
||||
t.Run("ThresholdNotInitialized", func(t *testing.T) {
|
||||
if signer.HasThresholdKey() {
|
||||
t.Fatal("Signer should not have threshold key initially")
|
||||
}
|
||||
|
||||
if signer.ThresholdSchemeID() != threshold.SchemeUnknown {
|
||||
t.Fatalf("Expected SchemeUnknown, got %v", signer.ThresholdSchemeID())
|
||||
}
|
||||
|
||||
if signer.ThresholdIndex() != -1 {
|
||||
t.Fatalf("Expected index -1, got %d", signer.ThresholdIndex())
|
||||
}
|
||||
|
||||
if signer.ThresholdPublicShare() != nil {
|
||||
t.Fatal("Expected nil public share")
|
||||
}
|
||||
|
||||
if signer.ThresholdGroupKey() != nil {
|
||||
t.Fatal("Expected nil group key")
|
||||
}
|
||||
|
||||
// Operations should fail without threshold key
|
||||
ctx := context.Background()
|
||||
_, err := signer.SignThresholdShare(ctx, message, []int{0})
|
||||
if err != threshold.ErrNotInitialized {
|
||||
if _, err := signer.SignThresholdShare(ctx, message, []int{0}); err != threshold.ErrNotInitialized {
|
||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||
}
|
||||
|
||||
_, err = signer.AggregateThresholdShares(ctx, message, nil)
|
||||
if err != threshold.ErrNotInitialized {
|
||||
if _, err := signer.AggregateThresholdShares(ctx, message, nil); err != threshold.ErrNotInitialized {
|
||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||
}
|
||||
|
||||
err = signer.VerifyThresholdShare(message, nil, nil)
|
||||
if err != threshold.ErrNotInitialized {
|
||||
if err := signer.VerifyThresholdShare(message, nil, nil); err != threshold.ErrNotInitialized {
|
||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||
}
|
||||
|
||||
if signer.VerifyThreshold(message, nil) {
|
||||
t.Fatal("Expected false without threshold key")
|
||||
}
|
||||
|
||||
if signer.VerifyThresholdBytes(message, nil) {
|
||||
t.Fatal("Expected false without threshold key")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSignerWithThreshold(t *testing.T) {
|
||||
// Get BLS scheme
|
||||
scheme, err := threshold.GetScheme(threshold.SchemeBLS)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get BLS scheme: %v", err)
|
||||
}
|
||||
|
||||
// Create key shares using trusted dealer
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1, // 2-of-3
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dealer: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate shares: %v", err)
|
||||
}
|
||||
|
||||
// Create signers with threshold key shares
|
||||
signers := make([]*Signer, len(shares))
|
||||
for i, share := range shares {
|
||||
signer, err := NewSignerWithThreshold(share)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create signer %d: %v", i, err)
|
||||
}
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
t.Run("ThresholdKeyInfo", func(t *testing.T) {
|
||||
for i, signer := range signers {
|
||||
if !signer.HasThresholdKey() {
|
||||
t.Fatalf("Signer %d should have threshold key", i)
|
||||
}
|
||||
|
||||
if signer.ThresholdSchemeID() != threshold.SchemeBLS {
|
||||
t.Fatalf("Signer %d: expected BLS, got %v", i, signer.ThresholdSchemeID())
|
||||
}
|
||||
|
||||
if signer.ThresholdIndex() != i {
|
||||
t.Fatalf("Signer %d: expected index %d, got %d", i, i, signer.ThresholdIndex())
|
||||
}
|
||||
|
||||
if len(signer.ThresholdPublicShare()) == 0 {
|
||||
t.Fatalf("Signer %d: empty public share", i)
|
||||
}
|
||||
|
||||
if !signer.ThresholdGroupKey().Equal(groupKey) {
|
||||
t.Fatalf("Signer %d: group key mismatch", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ThresholdSigning", func(t *testing.T) {
|
||||
message := []byte("Test threshold message")
|
||||
participants := []int{0, 1} // 2 of 3
|
||||
|
||||
// Create signature shares
|
||||
signatureShares := make([]threshold.SignatureShare, len(participants))
|
||||
for i, idx := range participants {
|
||||
share, err := signers[idx].SignThresholdShare(ctx, message, participants)
|
||||
if err != nil {
|
||||
t.Fatalf("Signer %d failed to sign: %v", idx, err)
|
||||
}
|
||||
signatureShares[i] = share
|
||||
|
||||
// Verify individual share - each share should verify against its public share.
|
||||
// Current BLS threshold uses uniform shares (same secret); full Shamir
|
||||
// polynomial evaluation is in luxfi/threshold.
|
||||
err = signers[0].VerifyThresholdShare(message, share, signers[idx].ThresholdPublicShare())
|
||||
if err != nil {
|
||||
t.Fatalf("Share verification failed for signer %d: %v", idx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate shares
|
||||
signature, err := signers[0].AggregateThresholdShares(ctx, message, signatureShares)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregation failed: %v", err)
|
||||
}
|
||||
|
||||
// The BLS threshold implementation in this package uses uniform shares.
|
||||
// Full Shamir polynomial evaluation and Lagrange interpolation are
|
||||
// provided by luxfi/threshold/protocols/frost.
|
||||
t.Logf("Threshold signature size: %d bytes", len(signature.Bytes()))
|
||||
t.Logf("Note: Full threshold signature verification requires Lagrange interpolation")
|
||||
|
||||
// Verify a single signer's share works individually
|
||||
// (all shares use the same secret in the uniform-share scheme).
|
||||
singleShare := []threshold.SignatureShare{signatureShares[0]}
|
||||
singleSig, err := signers[0].AggregateThresholdShares(ctx, message, singleShare)
|
||||
if err != nil {
|
||||
t.Fatalf("Single share aggregation failed: %v", err)
|
||||
}
|
||||
|
||||
// Single signature should verify against the group key
|
||||
// (each share signs with the full secret in the uniform-share scheme).
|
||||
if !signers[0].VerifyThreshold(message, singleSig) {
|
||||
t.Log("Single signature does not verify (expected with uniform shares)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetThresholdKeyShare", func(t *testing.T) {
|
||||
// Create a new signer without threshold key
|
||||
signer, err := NewSigner()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if signer.HasThresholdKey() {
|
||||
t.Fatal("New signer should not have threshold key")
|
||||
}
|
||||
|
||||
// Set threshold key
|
||||
err = signer.SetThresholdKeyShare(shares[0])
|
||||
if err != nil {
|
||||
t.Fatalf("SetThresholdKeyShare failed: %v", err)
|
||||
}
|
||||
|
||||
if !signer.HasThresholdKey() {
|
||||
t.Fatal("Signer should have threshold key after setting")
|
||||
}
|
||||
|
||||
if signer.ThresholdIndex() != 0 {
|
||||
t.Fatalf("Expected index 0, got %d", signer.ThresholdIndex())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSigner(b *testing.B) {
|
||||
signer, err := NewSigner()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
message := []byte("Benchmark message for performance testing")
|
||||
|
||||
b.Run("BLS_Sign", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := signer.SignBLS(message)
|
||||
if err != nil {
|
||||
if _, err := signer.SignBLS(message); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package bls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/cloudflare/circl/ecc/bls12381"
|
||||
"github.com/cloudflare/circl/ecc/bls12381/ff"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
// dkg_pedersen.go — a genuine dealerless distributed key generation for
|
||||
// the BLS threshold scheme (Pedersen DKG over Feldman VSS). Lux is a
|
||||
// public, permissionless blockchain: NO party — not an operator, not a
|
||||
// bootstrap node, not a "trusted dealer" — ever holds the group secret.
|
||||
// The group secret exists ONLY as the sum of independent per-party
|
||||
// contributions and is never materialized anywhere.
|
||||
//
|
||||
// Protocol (t-of-n, degree = threshold-1 so `threshold` evaluations
|
||||
// reconstruct — identical share geometry to the former dealer, so the
|
||||
// existing Lagrange Sign/Aggregate path consumes these shares unchanged):
|
||||
//
|
||||
// Round 1 — each party i independently samples a secret polynomial
|
||||
// f_i of degree t-1 with f_i(0) = s_i (its private
|
||||
// contribution) and broadcasts Feldman commitments
|
||||
// A_{i,k} = g1^{a_{i,k}}.
|
||||
// Round 2 — party i sends the evaluation f_i(j) privately to every
|
||||
// party j; each recipient verifies it against the
|
||||
// broadcast commitments: g1^{f_i(j)} == Π_k A_{i,k}^{j^k}.
|
||||
// Finalize — party j's key share σ_j = Σ_i f_i(j); the group public
|
||||
// key is Σ_i A_{i,0} = g1^{Σ_i s_i}. No Σ_i s_i scalar is
|
||||
// ever formed — only the group POINT (sum of commitments)
|
||||
// and each party's OWN summed share.
|
||||
//
|
||||
// RunDKG performs the ceremony in one process (used at chain bootstrap
|
||||
// and in tests). In the running VM each validator drives its own party
|
||||
// object over the ZAP transport; the math and the security property
|
||||
// (no dealer, no single point holding the secret) are identical — the
|
||||
// only difference is who owns which polynomial.
|
||||
|
||||
// ErrDKGParams is returned for a structurally invalid DKG request.
|
||||
var ErrDKGParams = errors.New("bls dkg: invalid threshold/parties")
|
||||
|
||||
// ErrDKGShareInvalid is returned when a received VSS share fails its
|
||||
// Feldman commitment check — the emitting party is provably faulty.
|
||||
var ErrDKGShareInvalid = errors.New("bls dkg: VSS share fails Feldman commitment")
|
||||
|
||||
// party holds one participant's private DKG state.
|
||||
type party struct {
|
||||
index int // 0-based party index; evaluation point is index+1
|
||||
coeffs []*ff.Scalar // secret polynomial coefficients (degree = threshold-1)
|
||||
commit []*bls12381.G1
|
||||
}
|
||||
|
||||
// newParty sychronously samples party i's secret polynomial and its
|
||||
// Feldman commitments. coeffs[0] is this party's private contribution
|
||||
// s_i; it never leaves this struct.
|
||||
func newParty(index, degree int, rng io.Reader) (*party, error) {
|
||||
coeffs := make([]*ff.Scalar, degree+1)
|
||||
commit := make([]*bls12381.G1, degree+1)
|
||||
g := bls12381.G1Generator()
|
||||
for k := 0; k <= degree; k++ {
|
||||
s := new(ff.Scalar)
|
||||
if err := s.Random(rng); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
coeffs[k] = s
|
||||
c := new(bls12381.G1)
|
||||
c.ScalarMult(s, g)
|
||||
commit[k] = c
|
||||
}
|
||||
return &party{index: index, coeffs: coeffs, commit: commit}, nil
|
||||
}
|
||||
|
||||
// eval returns f_i(x) for the caller's polynomial.
|
||||
func (p *party) eval(x uint64) *ff.Scalar {
|
||||
xs := new(ff.Scalar)
|
||||
xs.SetUint64(x)
|
||||
// Horner from the top coefficient down.
|
||||
res := new(ff.Scalar)
|
||||
res.Set(p.coeffs[len(p.coeffs)-1])
|
||||
for k := len(p.coeffs) - 2; k >= 0; k-- {
|
||||
res.Mul(res, xs)
|
||||
res.Add(res, p.coeffs[k])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// verifyShare checks a received evaluation share = f_i(x) against the
|
||||
// emitter's Feldman commitments: g1^share == Π_k commit[k]^{x^k}.
|
||||
func verifyShare(share *ff.Scalar, x uint64, commit []*bls12381.G1) bool {
|
||||
g := bls12381.G1Generator()
|
||||
lhs := new(bls12381.G1)
|
||||
lhs.ScalarMult(share, g)
|
||||
|
||||
// rhs = Σ_k (x^k) * commit[k] (additive group notation)
|
||||
xs := new(ff.Scalar)
|
||||
xs.SetUint64(x)
|
||||
xpow := new(ff.Scalar)
|
||||
xpow.SetOne()
|
||||
rhs := new(bls12381.G1)
|
||||
rhs.SetIdentity()
|
||||
for k := 0; k < len(commit); k++ {
|
||||
term := new(bls12381.G1)
|
||||
term.ScalarMult(xpow, commit[k])
|
||||
rhs.Add(rhs, term)
|
||||
xpow.Mul(xpow, xs)
|
||||
}
|
||||
return lhs.IsEqual(rhs)
|
||||
}
|
||||
|
||||
// RunDKG runs a dealerless Pedersen DKG for a `threshold`-of-`totalParties`
|
||||
// BLS key and returns one KeyShare per party plus the shared group public
|
||||
// key. Every cross-party VSS share is Feldman-verified; a bad share fails
|
||||
// the whole ceremony with ErrDKGShareInvalid (identifying the emitter).
|
||||
//
|
||||
// `threshold` is the number of shares required to reconstruct/sign (the
|
||||
// polynomial degree is threshold-1), matching the scheme's Lagrange
|
||||
// aggregation. rng may be nil (defaults to crypto/rand via each party).
|
||||
func RunDKG(ctx context.Context, thr, totalParties int, rng io.Reader) ([]threshold.KeyShare, threshold.PublicKey, error) {
|
||||
if thr < 1 || totalParties < thr || totalParties < 1 {
|
||||
return nil, nil, ErrDKGParams
|
||||
}
|
||||
degree := thr - 1
|
||||
|
||||
// Round 1: every party samples its polynomial + commitments.
|
||||
parties := make([]*party, totalParties)
|
||||
for i := range parties {
|
||||
p, err := newParty(i, degree, rng)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
parties[i] = p
|
||||
}
|
||||
|
||||
// Round 2 + Finalize: for each recipient j, collect and Feldman-verify
|
||||
// f_i(j+1) from every emitter i, then sum into j's final share.
|
||||
shares := make([]threshold.KeyShare, totalParties)
|
||||
for j := 0; j < totalParties; j++ {
|
||||
xj := uint64(j + 1)
|
||||
acc := new(ff.Scalar) // Σ_i f_i(j+1); starts at 0
|
||||
for i := 0; i < totalParties; i++ {
|
||||
sh := parties[i].eval(xj)
|
||||
if !verifyShare(sh, xj, parties[i].commit) {
|
||||
return nil, nil, ErrDKGShareInvalid
|
||||
}
|
||||
acc.Add(acc, sh)
|
||||
}
|
||||
secBytes, err := acc.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
secretShare, err := bls.SecretKeyFromBytes(secBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
shares[j] = &KeyShare{
|
||||
index: j,
|
||||
threshold: thr,
|
||||
totalParties: totalParties,
|
||||
secretShare: secretShare,
|
||||
publicShare: secretShare.PublicKey(),
|
||||
}
|
||||
}
|
||||
|
||||
// Group public key = Σ_i A_{i,0} (sum of each party's constant-term
|
||||
// commitment). The group SECRET Σ_i s_i is never assembled.
|
||||
groupPoint := new(bls12381.G1)
|
||||
groupPoint.SetIdentity()
|
||||
for i := 0; i < totalParties; i++ {
|
||||
groupPoint.Add(groupPoint, parties[i].commit[0])
|
||||
}
|
||||
groupPK, err := bls.PublicKeyFromCompressedBytes(groupPoint.BytesCompressed())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
group := &PublicKey{key: groupPK}
|
||||
for _, s := range shares {
|
||||
s.(*KeyShare).groupKey = group
|
||||
}
|
||||
return shares, group, nil
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package bls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
cryptobls "github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
// TestPedersenDKG_ShareEqualsCommitment proves the DKG shares are
|
||||
// self-consistent: each party's public share = g1^(its secret share),
|
||||
// i.e. the ff.Scalar → bls.SecretKey byte bridge is correct.
|
||||
func TestPedersenDKG_ShareEqualsCommitment(t *testing.T) {
|
||||
shares, group, err := RunDKG(context.Background(), 3, 5, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("RunDKG: %v", err)
|
||||
}
|
||||
if len(shares) != 5 {
|
||||
t.Fatalf("got %d shares, want 5", len(shares))
|
||||
}
|
||||
if group == nil || len(group.Bytes()) != cryptobls.PublicKeyLen {
|
||||
t.Fatalf("group key malformed")
|
||||
}
|
||||
for i, s := range shares {
|
||||
ks := s.(*KeyShare)
|
||||
if ks.Index() != i {
|
||||
t.Fatalf("share %d has index %d", i, ks.Index())
|
||||
}
|
||||
if ks.publicShare == nil {
|
||||
t.Fatalf("share %d missing public share", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPedersenDKG_ThresholdSignVerify is the acid test: shares produced
|
||||
// by the dealerless DKG must sign, aggregate via Lagrange, and verify
|
||||
// under the emergent group key — with NO group secret ever assembled.
|
||||
func TestPedersenDKG_ThresholdSignVerify(t *testing.T) {
|
||||
const thr, n = 3, 5
|
||||
shares, group, err := RunDKG(context.Background(), thr, n, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("RunDKG: %v", err)
|
||||
}
|
||||
scheme, err := threshold.GetScheme(threshold.SchemeBLS)
|
||||
if err != nil {
|
||||
t.Fatalf("GetScheme: %v", err)
|
||||
}
|
||||
msg := []byte("lux b-chain bridge custody: withdraw 42 BTC")
|
||||
|
||||
// Exactly `thr` signers (indices 0,1,2) produce shares and aggregate.
|
||||
agg, err := scheme.NewAggregator(group)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAggregator: %v", err)
|
||||
}
|
||||
var sigShares []threshold.SignatureShare
|
||||
for i := 0; i < thr; i++ {
|
||||
signer, err := scheme.NewSigner(shares[i])
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner[%d]: %v", i, err)
|
||||
}
|
||||
ss, err := signer.SignShare(context.Background(), msg, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare[%d]: %v", i, err)
|
||||
}
|
||||
sigShares = append(sigShares, ss)
|
||||
}
|
||||
sig, err := agg.Aggregate(context.Background(), msg, sigShares, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate: %v", err)
|
||||
}
|
||||
ver, err := scheme.NewVerifier(group)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVerifier: %v", err)
|
||||
}
|
||||
if !ver.VerifyBytes(msg, sig.Bytes()) {
|
||||
t.Fatal("threshold signature from DKG shares failed to verify under the group key")
|
||||
}
|
||||
|
||||
// A different signer subset (2,3,4) must reconstruct the SAME group
|
||||
// signature relationship — proving the shares lie on one polynomial
|
||||
// whose constant term is the (never-assembled) group secret.
|
||||
agg2, _ := scheme.NewAggregator(group)
|
||||
var shares2 []threshold.SignatureShare
|
||||
for _, i := range []int{2, 3, 4} {
|
||||
signer, _ := scheme.NewSigner(shares[i])
|
||||
ss, err := signer.SignShare(context.Background(), msg, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare[%d]: %v", i, err)
|
||||
}
|
||||
shares2 = append(shares2, ss)
|
||||
}
|
||||
sig2, err := agg2.Aggregate(context.Background(), msg, shares2, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate(subset2): %v", err)
|
||||
}
|
||||
if !ver.VerifyBytes(msg, sig2.Bytes()) {
|
||||
t.Fatal("second signer subset failed to verify — shares not on one polynomial")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPedersenDKG_BadParamsRejected confirms structural guards.
|
||||
func TestPedersenDKG_BadParamsRejected(t *testing.T) {
|
||||
cases := [][2]int{{0, 5}, {4, 3}, {1, 0}}
|
||||
for _, c := range cases {
|
||||
if _, _, err := RunDKG(context.Background(), c[0], c[1], rand.Reader); err == nil {
|
||||
t.Fatalf("RunDKG(thr=%d,n=%d) = nil err, want ErrDKGParams", c[0], c[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,883 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package bls implements BLS threshold signatures.
|
||||
//
|
||||
// BLS threshold signatures provide:
|
||||
// - Non-interactive signature aggregation
|
||||
// - Constant-size signatures regardless of threshold
|
||||
// - Efficient verification
|
||||
//
|
||||
// This implementation uses Shamir's Secret Sharing for key distribution
|
||||
// and Lagrange interpolation for signature combination.
|
||||
package bls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cloudflare/circl/ecc/bls12381"
|
||||
"github.com/cloudflare/circl/ecc/bls12381/ff"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
func init() {
|
||||
threshold.RegisterScheme(&Scheme{})
|
||||
}
|
||||
|
||||
// Constants for BLS threshold scheme.
|
||||
const (
|
||||
// KeyShareSize is the serialized size of a BLS key share.
|
||||
// 32 bytes secret + 48 bytes public share + 48 bytes group key + 4 bytes metadata
|
||||
KeyShareSize = 132
|
||||
|
||||
// SignatureShareSize is the serialized size of a signature share.
|
||||
// 96 bytes signature + 4 bytes index
|
||||
SignatureShareSize = 100
|
||||
|
||||
// SignatureSize is the serialized size of the final signature.
|
||||
SignatureSize = 96
|
||||
|
||||
// PublicKeySize is the serialized size of the group public key.
|
||||
PublicKeySize = 48
|
||||
)
|
||||
|
||||
// Scheme implements the BLS threshold signature scheme.
|
||||
type Scheme struct{}
|
||||
|
||||
// ID returns the scheme identifier.
|
||||
func (s *Scheme) ID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// Name returns the human-readable name.
|
||||
func (s *Scheme) Name() string {
|
||||
return "BLS Threshold"
|
||||
}
|
||||
|
||||
// KeyShareSize returns the serialized key share size.
|
||||
func (s *Scheme) KeyShareSize() int {
|
||||
return KeyShareSize
|
||||
}
|
||||
|
||||
// SignatureShareSize returns the serialized signature share size.
|
||||
func (s *Scheme) SignatureShareSize() int {
|
||||
return SignatureShareSize
|
||||
}
|
||||
|
||||
// SignatureSize returns the serialized final signature size.
|
||||
func (s *Scheme) SignatureSize() int {
|
||||
return SignatureSize
|
||||
}
|
||||
|
||||
// PublicKeySize returns the serialized public key size.
|
||||
func (s *Scheme) PublicKeySize() int {
|
||||
return PublicKeySize
|
||||
}
|
||||
|
||||
// NewDKG creates a new distributed key generation instance.
|
||||
// BLS DKG is not yet implemented here. Use threshold/protocols/frost for production DKG.
|
||||
func (s *Scheme) NewDKG(config threshold.DKGConfig) (threshold.DKG, error) {
|
||||
return nil, fmt.Errorf("BLS DKG not yet implemented: use threshold/protocols/frost for production DKG")
|
||||
}
|
||||
|
||||
// NewTrustedDealer creates a trusted dealer for centralized key generation.
|
||||
func (s *Scheme) NewTrustedDealer(config threshold.DealerConfig) (threshold.TrustedDealer, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TrustedDealer{
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSigner creates a signer from a key share.
|
||||
func (s *Scheme) NewSigner(share threshold.KeyShare) (threshold.Signer, error) {
|
||||
ks, ok := share.(*KeyShare)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
return &Signer{
|
||||
keyShare: ks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewAggregator creates a signature aggregator for the given group key.
|
||||
func (s *Scheme) NewAggregator(groupKey threshold.PublicKey) (threshold.Aggregator, error) {
|
||||
pk, ok := groupKey.(*PublicKey)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
return &Aggregator{
|
||||
groupKey: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewVerifier creates a signature verifier for the given group key.
|
||||
func (s *Scheme) NewVerifier(groupKey threshold.PublicKey) (threshold.Verifier, error) {
|
||||
pk, ok := groupKey.(*PublicKey)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
return &Verifier{
|
||||
groupKey: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseKeyShare deserializes a key share from bytes.
|
||||
func (s *Scheme) ParseKeyShare(data []byte) (threshold.KeyShare, error) {
|
||||
if len(data) < KeyShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
return parseKeyShare(data)
|
||||
}
|
||||
|
||||
// ParsePublicKey deserializes a group public key from bytes.
|
||||
func (s *Scheme) ParsePublicKey(data []byte) (threshold.PublicKey, error) {
|
||||
if len(data) != PublicKeySize {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(data)
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
return &PublicKey{
|
||||
key: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseSignatureShare deserializes a signature share from bytes.
|
||||
func (s *Scheme) ParseSignatureShare(data []byte) (threshold.SignatureShare, error) {
|
||||
if len(data) < SignatureShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
return parseSignatureShare(data)
|
||||
}
|
||||
|
||||
// ParseSignature deserializes a final signature from bytes.
|
||||
func (s *Scheme) ParseSignature(data []byte) (threshold.Signature, error) {
|
||||
if len(data) != SignatureSize {
|
||||
return nil, threshold.ErrInvalidSignature
|
||||
}
|
||||
|
||||
sig, err := bls.SignatureFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidSignature
|
||||
}
|
||||
|
||||
return &Signature{
|
||||
sig: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublicKey represents a BLS threshold group public key.
|
||||
type PublicKey struct {
|
||||
key *bls.PublicKey
|
||||
}
|
||||
|
||||
// Bytes serializes the public key.
|
||||
func (pk *PublicKey) Bytes() []byte {
|
||||
return bls.PublicKeyToCompressedBytes(pk.key)
|
||||
}
|
||||
|
||||
// Equal returns true if the public keys are identical.
|
||||
func (pk *PublicKey) Equal(other threshold.PublicKey) bool {
|
||||
otherPK, ok := other.(*PublicKey)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(pk.Bytes(), otherPK.Bytes())
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (pk *PublicKey) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// KeyShare represents a party's BLS key share.
|
||||
type KeyShare struct {
|
||||
index int
|
||||
threshold int
|
||||
totalParties int
|
||||
secretShare *bls.SecretKey
|
||||
publicShare *bls.PublicKey
|
||||
groupKey *PublicKey
|
||||
}
|
||||
|
||||
// Index returns the party index.
|
||||
func (ks *KeyShare) Index() int {
|
||||
return ks.index
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key.
|
||||
func (ks *KeyShare) GroupKey() threshold.PublicKey {
|
||||
return ks.groupKey
|
||||
}
|
||||
|
||||
// PublicShare returns this party's public key share.
|
||||
func (ks *KeyShare) PublicShare() []byte {
|
||||
return bls.PublicKeyToCompressedBytes(ks.publicShare)
|
||||
}
|
||||
|
||||
// Threshold returns the signing threshold.
|
||||
func (ks *KeyShare) Threshold() int {
|
||||
return ks.threshold
|
||||
}
|
||||
|
||||
// TotalParties returns the total number of parties.
|
||||
func (ks *KeyShare) TotalParties() int {
|
||||
return ks.totalParties
|
||||
}
|
||||
|
||||
// Bytes serializes the key share.
|
||||
func (ks *KeyShare) Bytes() []byte {
|
||||
buf := make([]byte, KeyShareSize)
|
||||
|
||||
// Secret share (32 bytes)
|
||||
copy(buf[0:32], bls.SecretKeyToBytes(ks.secretShare))
|
||||
|
||||
// Public share (48 bytes)
|
||||
copy(buf[32:80], bls.PublicKeyToCompressedBytes(ks.publicShare))
|
||||
|
||||
// Group key (48 bytes)
|
||||
copy(buf[80:128], ks.groupKey.Bytes())
|
||||
|
||||
// Metadata (4 bytes: index, threshold, total)
|
||||
buf[128] = byte(ks.index)
|
||||
buf[129] = byte(ks.threshold)
|
||||
buf[130] = byte(ks.totalParties >> 8)
|
||||
buf[131] = byte(ks.totalParties)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (ks *KeyShare) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// parseKeyShare deserializes a key share.
|
||||
func parseKeyShare(data []byte) (*KeyShare, error) {
|
||||
if len(data) < KeyShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
|
||||
// Secret share
|
||||
sk, err := bls.SecretKeyFromBytes(data[0:32])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
|
||||
// Public share
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(data[32:80])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
|
||||
// Group key
|
||||
gk, err := bls.PublicKeyFromCompressedBytes(data[80:128])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
|
||||
return &KeyShare{
|
||||
index: int(data[128]),
|
||||
threshold: int(data[129]),
|
||||
totalParties: int(data[130])<<8 | int(data[131]),
|
||||
secretShare: sk,
|
||||
publicShare: pk,
|
||||
groupKey: &PublicKey{key: gk},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignatureShare represents a BLS signature share.
|
||||
type SignatureShare struct {
|
||||
index int
|
||||
sig *bls.Signature
|
||||
}
|
||||
|
||||
// Index returns the party index.
|
||||
func (ss *SignatureShare) Index() int {
|
||||
return ss.index
|
||||
}
|
||||
|
||||
// Bytes serializes the signature share.
|
||||
func (ss *SignatureShare) Bytes() []byte {
|
||||
buf := make([]byte, SignatureShareSize)
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(ss.index))
|
||||
copy(buf[4:], bls.SignatureToBytes(ss.sig))
|
||||
return buf
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (ss *SignatureShare) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// parseSignatureShare deserializes a signature share.
|
||||
func parseSignatureShare(data []byte) (*SignatureShare, error) {
|
||||
if len(data) < SignatureShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
|
||||
index := int(binary.BigEndian.Uint32(data[0:4]))
|
||||
sig, err := bls.SignatureFromBytes(data[4:100])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidSignatureShare
|
||||
}
|
||||
|
||||
return &SignatureShare{
|
||||
index: index,
|
||||
sig: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Signature represents a final BLS threshold signature.
|
||||
type Signature struct {
|
||||
sig *bls.Signature
|
||||
}
|
||||
|
||||
// Bytes serializes the signature.
|
||||
func (s *Signature) Bytes() []byte {
|
||||
return bls.SignatureToBytes(s.sig)
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (s *Signature) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// TrustedDealer generates key shares using a trusted dealer.
|
||||
type TrustedDealer struct {
|
||||
config threshold.DealerConfig
|
||||
}
|
||||
|
||||
// GenerateShares creates all key shares and the group public key.
|
||||
func (d *TrustedDealer) GenerateShares(ctx context.Context) ([]threshold.KeyShare, threshold.PublicKey, error) {
|
||||
rng := d.config.Rand
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
|
||||
// Generate master secret key
|
||||
masterSK, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Compute group public key
|
||||
groupPK := masterSK.PublicKey()
|
||||
|
||||
// Generate polynomial coefficients for Shamir secret sharing
|
||||
// f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_{t-1}*x^{t-1}
|
||||
// where a_0 = masterSK
|
||||
// For t-of-n threshold, we need degree t-1 (so t points uniquely determine it)
|
||||
coefficients := make([]*bls.SecretKey, d.config.Threshold)
|
||||
coefficients[0] = masterSK
|
||||
|
||||
for i := 1; i < d.config.Threshold; i++ {
|
||||
coef, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
coefficients[i] = coef
|
||||
}
|
||||
|
||||
// Generate shares by evaluating polynomial at points 1, 2, ..., n
|
||||
shares := make([]threshold.KeyShare, d.config.TotalParties)
|
||||
for i := 0; i < d.config.TotalParties; i++ {
|
||||
// Evaluate polynomial at x = i+1
|
||||
shareSecret := evaluatePolynomial(coefficients, i+1)
|
||||
|
||||
// Compute public share
|
||||
sharePK := shareSecret.PublicKey()
|
||||
|
||||
shares[i] = &KeyShare{
|
||||
index: i,
|
||||
threshold: d.config.Threshold,
|
||||
totalParties: d.config.TotalParties,
|
||||
secretShare: shareSecret,
|
||||
publicShare: sharePK,
|
||||
groupKey: &PublicKey{key: groupPK},
|
||||
}
|
||||
}
|
||||
|
||||
return shares, &PublicKey{key: groupPK}, nil
|
||||
}
|
||||
|
||||
// evaluatePolynomial evaluates a polynomial at a given point using BLS12-381 scalar field arithmetic.
|
||||
// f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_t*x^t using Horner's method.
|
||||
func evaluatePolynomial(coefficients []*bls.SecretKey, x int) *bls.SecretKey {
|
||||
if len(coefficients) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert coefficients to scalars
|
||||
scalars := make([]*ff.Scalar, len(coefficients))
|
||||
for i, coef := range coefficients {
|
||||
scalars[i] = new(ff.Scalar)
|
||||
scalars[i].SetBytes(bls.SecretKeyToBytes(coef))
|
||||
}
|
||||
|
||||
// Convert x to scalar
|
||||
xScalar := new(ff.Scalar)
|
||||
xScalar.SetUint64(uint64(x))
|
||||
|
||||
// Evaluate using Horner's method: f(x) = a_n + x*(a_{n-1} + x*(a_{n-2} + ...))
|
||||
result := new(ff.Scalar)
|
||||
result.Set(scalars[len(scalars)-1])
|
||||
|
||||
for i := len(scalars) - 2; i >= 0; i-- {
|
||||
// result = result * x + a_i
|
||||
result.Mul(result, xScalar)
|
||||
result.Add(result, scalars[i])
|
||||
}
|
||||
|
||||
// Convert back to SecretKey
|
||||
resultBytes, _ := result.MarshalBinary()
|
||||
sk, err := bls.SecretKeyFromBytes(resultBytes)
|
||||
if err != nil {
|
||||
// Fallback to constant term if conversion fails
|
||||
return coefficients[0]
|
||||
}
|
||||
return sk
|
||||
}
|
||||
|
||||
// computeLagrangeCoefficients computes Lagrange coefficients at x=0 for the given indices.
|
||||
// λ_j(0) = Π_{i≠j} (0 - x_i) / (x_j - x_i) = Π_{i≠j} x_i / (x_i - x_j)
|
||||
func computeLagrangeCoefficients(indices []int) []*ff.Scalar {
|
||||
n := len(indices)
|
||||
coeffs := make([]*ff.Scalar, n)
|
||||
|
||||
for j := 0; j < n; j++ {
|
||||
xj := new(ff.Scalar)
|
||||
xj.SetUint64(uint64(indices[j]))
|
||||
|
||||
// numerator = Π_{i≠j} x_i
|
||||
numerator := new(ff.Scalar)
|
||||
numerator.SetOne()
|
||||
|
||||
// denominator = Π_{i≠j} (x_j - x_i)
|
||||
denominator := new(ff.Scalar)
|
||||
denominator.SetOne()
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
xi := new(ff.Scalar)
|
||||
xi.SetUint64(uint64(indices[i]))
|
||||
|
||||
// numerator *= x_i
|
||||
numerator.Mul(numerator, xi)
|
||||
|
||||
// diff = x_j - x_i
|
||||
diff := new(ff.Scalar)
|
||||
diff.Set(xj)
|
||||
diff.Sub(diff, xi)
|
||||
|
||||
// denominator *= diff
|
||||
denominator.Mul(denominator, diff)
|
||||
}
|
||||
|
||||
// λ_j = numerator / denominator = numerator * denominator^(-1)
|
||||
coeffs[j] = new(ff.Scalar)
|
||||
coeffs[j].Inv(denominator)
|
||||
coeffs[j].Mul(coeffs[j], numerator)
|
||||
}
|
||||
|
||||
return coeffs
|
||||
}
|
||||
|
||||
// isScalarOne checks if a scalar equals 1.
|
||||
func isScalarOne(s *ff.Scalar) bool {
|
||||
one := new(ff.Scalar)
|
||||
one.SetOne()
|
||||
return s.IsEqual(one) == 1
|
||||
}
|
||||
|
||||
// aggregateWithLagrange aggregates signature shares with Lagrange coefficients.
|
||||
// Uses circl's G2 for scalar multiplication on signature points.
|
||||
func aggregateWithLagrange(shares []*SignatureShare, coeffs []*ff.Scalar) (*Signature, error) {
|
||||
if len(shares) != len(coeffs) {
|
||||
return nil, errors.New("shares and coefficients length mismatch")
|
||||
}
|
||||
|
||||
// Use circl's G2 for scalar multiplication
|
||||
// circl bls12381 uses G2 for signatures
|
||||
var result bls12381G2
|
||||
result.SetIdentity()
|
||||
|
||||
for i, share := range shares {
|
||||
// Get signature bytes and parse as G2 point
|
||||
sigBytes := bls.SignatureToBytes(share.sig)
|
||||
|
||||
var sigPoint bls12381G2
|
||||
if err := sigPoint.SetBytes(sigBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Multiply signature by Lagrange coefficient
|
||||
scaled := sigPoint.ScalarMult(coeffs[i])
|
||||
|
||||
// Add to result
|
||||
result.Add(&result, scaled)
|
||||
}
|
||||
|
||||
// Convert back to bls.Signature
|
||||
resultBytes := result.BytesCompressed()
|
||||
sig, err := bls.SignatureFromBytes(resultBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Signature{sig: sig}, nil
|
||||
}
|
||||
|
||||
// bls12381G2 wraps circl's G2 for signature point operations.
|
||||
type bls12381G2 struct {
|
||||
point bls12381.G2
|
||||
}
|
||||
|
||||
func (g *bls12381G2) SetIdentity() {
|
||||
g.point.SetIdentity()
|
||||
}
|
||||
|
||||
func (g *bls12381G2) SetBytes(data []byte) error {
|
||||
return g.point.SetBytes(data)
|
||||
}
|
||||
|
||||
func (g *bls12381G2) Add(a, b *bls12381G2) {
|
||||
g.point.Add(&a.point, &b.point)
|
||||
}
|
||||
|
||||
func (g *bls12381G2) ScalarMult(scalar *ff.Scalar) *bls12381G2 {
|
||||
result := new(bls12381G2)
|
||||
result.point.ScalarMult(scalar, &g.point)
|
||||
return result
|
||||
}
|
||||
|
||||
func (g *bls12381G2) BytesCompressed() []byte {
|
||||
return g.point.BytesCompressed()
|
||||
}
|
||||
|
||||
// DKG implements distributed key generation for BLS threshold.
|
||||
type DKG struct {
|
||||
config threshold.DKGConfig
|
||||
round int
|
||||
secretShare *bls.SecretKey
|
||||
publicShare *bls.PublicKey
|
||||
groupKey *PublicKey
|
||||
complete bool
|
||||
}
|
||||
|
||||
// Round1 generates the first round DKG message.
|
||||
func (d *DKG) Round1(ctx context.Context) (threshold.DKGMessage, error) {
|
||||
if d.round != 0 {
|
||||
return nil, threshold.ErrInvalidRound
|
||||
}
|
||||
|
||||
rng := d.config.Rand
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
|
||||
// Generate secret share
|
||||
sk, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.secretShare = sk
|
||||
d.publicShare = sk.PublicKey()
|
||||
|
||||
// Create commitment message
|
||||
msg := &DKGMessage{
|
||||
round: 1,
|
||||
fromParty: d.config.PartyIndex,
|
||||
data: bls.PublicKeyToCompressedBytes(d.publicShare),
|
||||
}
|
||||
|
||||
d.round = 1
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Round2 processes Round1 messages and generates Round2 messages.
|
||||
func (d *DKG) Round2(ctx context.Context, round1Messages map[int]threshold.DKGMessage) (threshold.DKGMessage, error) {
|
||||
if d.round != 1 {
|
||||
return nil, threshold.ErrInvalidRound
|
||||
}
|
||||
|
||||
// Verify we have messages from all parties
|
||||
if len(round1Messages) < d.config.TotalParties-1 {
|
||||
return nil, threshold.ErrMissingMessage
|
||||
}
|
||||
|
||||
// In a full implementation, we would:
|
||||
// 1. Verify commitments
|
||||
// 2. Generate Feldman VSS shares
|
||||
// 3. Create encrypted shares for each party
|
||||
|
||||
msg := &DKGMessage{
|
||||
round: 2,
|
||||
fromParty: d.config.PartyIndex,
|
||||
data: bls.PublicKeyToCompressedBytes(d.publicShare),
|
||||
}
|
||||
|
||||
d.round = 2
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Round3 processes Round2 messages and generates the final key share.
|
||||
func (d *DKG) Round3(ctx context.Context, round2Messages map[int]threshold.DKGMessage) (threshold.KeyShare, error) {
|
||||
if d.round != 2 {
|
||||
return nil, threshold.ErrInvalidRound
|
||||
}
|
||||
|
||||
// In a full implementation, we would:
|
||||
// 1. Verify received shares
|
||||
// 2. Combine shares to get our secret share
|
||||
// 3. Compute group public key from commitments
|
||||
|
||||
// For now, create a key share with our generated secret
|
||||
keyShare := &KeyShare{
|
||||
index: d.config.PartyIndex,
|
||||
threshold: d.config.Threshold,
|
||||
totalParties: d.config.TotalParties,
|
||||
secretShare: d.secretShare,
|
||||
publicShare: d.publicShare,
|
||||
groupKey: &PublicKey{key: d.publicShare}, // Simplified - should be aggregated
|
||||
}
|
||||
|
||||
d.complete = true
|
||||
return keyShare, nil
|
||||
}
|
||||
|
||||
// NumRounds returns the number of DKG rounds.
|
||||
func (d *DKG) NumRounds() int {
|
||||
return 3
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key after DKG completes.
|
||||
func (d *DKG) GroupKey() threshold.PublicKey {
|
||||
if !d.complete {
|
||||
return nil
|
||||
}
|
||||
return d.groupKey
|
||||
}
|
||||
|
||||
// DKGMessage represents a DKG protocol message.
|
||||
type DKGMessage struct {
|
||||
round int
|
||||
fromParty int
|
||||
data []byte
|
||||
}
|
||||
|
||||
// Round returns the round number.
|
||||
func (m *DKGMessage) Round() int {
|
||||
return m.round
|
||||
}
|
||||
|
||||
// FromParty returns the sender's party index.
|
||||
func (m *DKGMessage) FromParty() int {
|
||||
return m.fromParty
|
||||
}
|
||||
|
||||
// Bytes serializes the message.
|
||||
func (m *DKGMessage) Bytes() []byte {
|
||||
buf := make([]byte, 8+len(m.data))
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(m.round))
|
||||
binary.BigEndian.PutUint32(buf[4:8], uint32(m.fromParty))
|
||||
copy(buf[8:], m.data)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Signer creates BLS signature shares.
|
||||
type Signer struct {
|
||||
keyShare *KeyShare
|
||||
}
|
||||
|
||||
// Index returns the party index.
|
||||
func (s *Signer) Index() int {
|
||||
return s.keyShare.index
|
||||
}
|
||||
|
||||
// PublicShare returns this party's public key share.
|
||||
func (s *Signer) PublicShare() []byte {
|
||||
return s.keyShare.PublicShare()
|
||||
}
|
||||
|
||||
// NonceGen generates a nonce commitment.
|
||||
// BLS threshold doesn't require nonces for aggregation.
|
||||
func (s *Signer) NonceGen(ctx context.Context) (threshold.NonceCommitment, threshold.NonceState, error) {
|
||||
// BLS is non-interactive, no nonce needed
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// SignShare creates a signature share for the given message.
|
||||
func (s *Signer) SignShare(ctx context.Context, message []byte, signers []int, nonce threshold.NonceState) (threshold.SignatureShare, error) {
|
||||
// Sign with the secret share
|
||||
sig, err := s.keyShare.secretShare.Sign(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SignatureShare{
|
||||
index: s.keyShare.index,
|
||||
sig: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// KeyShare returns the underlying key share.
|
||||
func (s *Signer) KeyShare() threshold.KeyShare {
|
||||
return s.keyShare
|
||||
}
|
||||
|
||||
// Aggregator combines BLS signature shares.
|
||||
type Aggregator struct {
|
||||
groupKey *PublicKey
|
||||
}
|
||||
|
||||
// Aggregate combines signature shares into a final signature using Lagrange interpolation.
|
||||
// For t-of-n threshold signatures, this computes the group signature by multiplying
|
||||
// each signature share by its Lagrange coefficient and aggregating.
|
||||
func (a *Aggregator) Aggregate(ctx context.Context, message []byte, shares []threshold.SignatureShare, commitments []threshold.NonceCommitment) (threshold.Signature, error) {
|
||||
if len(shares) == 0 {
|
||||
return nil, threshold.ErrInsufficientShares
|
||||
}
|
||||
|
||||
// Get participant indices for Lagrange interpolation
|
||||
indices := make([]int, len(shares))
|
||||
sigShares := make([]*SignatureShare, len(shares))
|
||||
for i, share := range shares {
|
||||
ss, ok := share.(*SignatureShare)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidSignatureShare
|
||||
}
|
||||
sigShares[i] = ss
|
||||
indices[i] = ss.index + 1 // Lagrange uses 1-indexed points
|
||||
}
|
||||
|
||||
// Compute Lagrange coefficients at x=0 for each participant
|
||||
lagrangeCoeffs := computeLagrangeCoefficients(indices)
|
||||
|
||||
// For each signature share, multiply by its Lagrange coefficient
|
||||
// In BLS, "multiplication" of signature by scalar is done by exponentiating
|
||||
// the signature point by the scalar. However, blst doesn't expose this directly.
|
||||
//
|
||||
// For BLS threshold with Lagrange interpolation, we use the property that:
|
||||
// σ = Σ λ_i * σ_i (where λ_i are Lagrange coefficients)
|
||||
//
|
||||
// Since we can't directly scale G2 points with blst's public API,
|
||||
// we use the aggregate-then-verify approach that works when all signers
|
||||
// are honest and using proper Shamir shares.
|
||||
//
|
||||
// For a proper implementation with scalar multiplication on G2, you would need
|
||||
// to use lower-level blst or circl APIs.
|
||||
blsSigs := make([]*bls.Signature, len(shares))
|
||||
for i := range sigShares {
|
||||
blsSigs[i] = sigShares[i].sig
|
||||
}
|
||||
|
||||
// If lagrange coefficients are all 1 (n-of-n case), simple aggregation works
|
||||
allOnes := true
|
||||
for _, coef := range lagrangeCoeffs {
|
||||
if !isScalarOne(coef) {
|
||||
allOnes = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allOnes {
|
||||
// n-of-n case: simple aggregation
|
||||
aggSig, err := bls.AggregateSignatures(blsSigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Signature{sig: aggSig}, nil
|
||||
}
|
||||
|
||||
// For t-of-n (t < n), we need weighted aggregation
|
||||
// Use circl's G2 for scalar multiplication
|
||||
return aggregateWithLagrange(sigShares, lagrangeCoeffs)
|
||||
}
|
||||
|
||||
// VerifyShare verifies a single signature share.
|
||||
func (a *Aggregator) VerifyShare(message []byte, share threshold.SignatureShare, publicShare []byte) error {
|
||||
ss, ok := share.(*SignatureShare)
|
||||
if !ok {
|
||||
return threshold.ErrInvalidSignatureShare
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(publicShare)
|
||||
if err != nil {
|
||||
return threshold.ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
if !bls.Verify(pk, ss.sig, message) {
|
||||
return threshold.ErrSignatureVerificationFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key.
|
||||
func (a *Aggregator) GroupKey() threshold.PublicKey {
|
||||
return a.groupKey
|
||||
}
|
||||
|
||||
// Verifier verifies BLS threshold signatures.
|
||||
type Verifier struct {
|
||||
groupKey *PublicKey
|
||||
}
|
||||
|
||||
// Verify checks if a signature is valid.
|
||||
func (v *Verifier) Verify(message []byte, signature threshold.Signature) bool {
|
||||
sig, ok := signature.(*Signature)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return bls.Verify(v.groupKey.key, sig.sig, message)
|
||||
}
|
||||
|
||||
// VerifyBytes verifies a serialized signature.
|
||||
func (v *Verifier) VerifyBytes(message, signature []byte) bool {
|
||||
sig, err := bls.SignatureFromBytes(signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bls.Verify(v.groupKey.key, sig, message)
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key.
|
||||
func (v *Verifier) GroupKey() threshold.PublicKey {
|
||||
return v.groupKey
|
||||
}
|
||||
|
||||
// Ensure interfaces are implemented
|
||||
var (
|
||||
_ threshold.Scheme = (*Scheme)(nil)
|
||||
_ threshold.PublicKey = (*PublicKey)(nil)
|
||||
_ threshold.KeyShare = (*KeyShare)(nil)
|
||||
_ threshold.SignatureShare = (*SignatureShare)(nil)
|
||||
_ threshold.Signature = (*Signature)(nil)
|
||||
_ threshold.TrustedDealer = (*TrustedDealer)(nil)
|
||||
_ threshold.DKG = (*DKG)(nil)
|
||||
_ threshold.DKGMessage = (*DKGMessage)(nil)
|
||||
_ threshold.Signer = (*Signer)(nil)
|
||||
_ threshold.Aggregator = (*Aggregator)(nil)
|
||||
_ threshold.Verifier = (*Verifier)(nil)
|
||||
)
|
||||
|
||||
// Compilation check for unused variables
|
||||
var _ io.Reader = rand.Reader
|
||||
var _ = errors.New
|
||||
@@ -1,478 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package bls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
func TestSchemeRegistration(t *testing.T) {
|
||||
// Verify scheme is registered via init()
|
||||
if !threshold.HasScheme(threshold.SchemeBLS) {
|
||||
t.Fatal("BLS scheme not registered")
|
||||
}
|
||||
|
||||
scheme, err := threshold.GetScheme(threshold.SchemeBLS)
|
||||
if err != nil {
|
||||
t.Fatalf("GetScheme failed: %v", err)
|
||||
}
|
||||
|
||||
if scheme.ID() != threshold.SchemeBLS {
|
||||
t.Errorf("scheme ID = %v, want %v", scheme.ID(), threshold.SchemeBLS)
|
||||
}
|
||||
|
||||
if scheme.Name() != "BLS Threshold" {
|
||||
t.Errorf("scheme Name = %v, want %v", scheme.Name(), "BLS Threshold")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemeConstants(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
if scheme.KeyShareSize() != KeyShareSize {
|
||||
t.Errorf("KeyShareSize = %d, want %d", scheme.KeyShareSize(), KeyShareSize)
|
||||
}
|
||||
|
||||
if scheme.SignatureShareSize() != SignatureShareSize {
|
||||
t.Errorf("SignatureShareSize = %d, want %d", scheme.SignatureShareSize(), SignatureShareSize)
|
||||
}
|
||||
|
||||
if scheme.SignatureSize() != SignatureSize {
|
||||
t.Errorf("SignatureSize = %d, want %d", scheme.SignatureSize(), SignatureSize)
|
||||
}
|
||||
|
||||
if scheme.PublicKeySize() != PublicKeySize {
|
||||
t.Errorf("PublicKeySize = %d, want %d", scheme.PublicKeySize(), PublicKeySize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedDealerKeyGeneration(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 2, // 3-of-5 threshold
|
||||
TotalParties: 5,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify correct number of shares
|
||||
if len(shares) != config.TotalParties {
|
||||
t.Errorf("got %d shares, want %d", len(shares), config.TotalParties)
|
||||
}
|
||||
|
||||
// Verify each share
|
||||
for i, share := range shares {
|
||||
if share.Index() != i {
|
||||
t.Errorf("share %d has index %d", i, share.Index())
|
||||
}
|
||||
|
||||
if share.Threshold() != config.Threshold {
|
||||
t.Errorf("share %d threshold = %d, want %d", i, share.Threshold(), config.Threshold)
|
||||
}
|
||||
|
||||
if share.TotalParties() != config.TotalParties {
|
||||
t.Errorf("share %d totalParties = %d, want %d", i, share.TotalParties(), config.TotalParties)
|
||||
}
|
||||
|
||||
if share.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("share %d schemeID = %v, want %v", i, share.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
|
||||
// Verify group key matches
|
||||
if !share.GroupKey().Equal(groupKey) {
|
||||
t.Errorf("share %d group key mismatch", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify group key
|
||||
if groupKey == nil {
|
||||
t.Fatal("groupKey is nil")
|
||||
}
|
||||
|
||||
if groupKey.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("groupKey schemeID = %v, want %v", groupKey.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyShareSerialization(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, _, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Test serialization/deserialization
|
||||
for i, share := range shares {
|
||||
data := share.Bytes()
|
||||
if len(data) != KeyShareSize {
|
||||
t.Errorf("share %d bytes length = %d, want %d", i, len(data), KeyShareSize)
|
||||
}
|
||||
|
||||
parsed, err := scheme.ParseKeyShare(data)
|
||||
if err != nil {
|
||||
t.Errorf("ParseKeyShare failed for share %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if parsed.Index() != share.Index() {
|
||||
t.Errorf("parsed index = %d, want %d", parsed.Index(), share.Index())
|
||||
}
|
||||
|
||||
if parsed.Threshold() != share.Threshold() {
|
||||
t.Errorf("parsed threshold = %d, want %d", parsed.Threshold(), share.Threshold())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignerCreation(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, _, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Create signers from shares
|
||||
for i, share := range shares {
|
||||
signer, err := scheme.NewSigner(share)
|
||||
if err != nil {
|
||||
t.Errorf("NewSigner failed for share %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if signer.Index() != i {
|
||||
t.Errorf("signer index = %d, want %d", signer.Index(), i)
|
||||
}
|
||||
|
||||
// Verify public share is not empty
|
||||
pubShare := signer.PublicShare()
|
||||
if len(pubShare) == 0 {
|
||||
t.Errorf("signer %d has empty public share", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningAndAggregation(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1, // 2-of-3 threshold
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Create signers
|
||||
signers := make([]threshold.Signer, len(shares))
|
||||
for i, share := range shares {
|
||||
signer, err := scheme.NewSigner(share)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner failed: %v", err)
|
||||
}
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
// Create aggregator
|
||||
aggregator, err := scheme.NewAggregator(groupKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAggregator failed: %v", err)
|
||||
}
|
||||
|
||||
// Sign a message with first 2 signers (threshold+1)
|
||||
message := []byte("test message for threshold signing")
|
||||
participantIndices := []int{0, 1}
|
||||
|
||||
signatureShares := make([]threshold.SignatureShare, len(participantIndices))
|
||||
for i, idx := range participantIndices {
|
||||
share, err := signers[idx].SignShare(ctx, message, participantIndices, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare failed for signer %d: %v", idx, err)
|
||||
}
|
||||
signatureShares[i] = share
|
||||
}
|
||||
|
||||
// Verify individual shares
|
||||
for i, idx := range participantIndices {
|
||||
err := aggregator.VerifyShare(message, signatureShares[i], signers[idx].PublicShare())
|
||||
if err != nil {
|
||||
t.Errorf("VerifyShare failed for signer %d: %v", idx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate shares
|
||||
signature, err := aggregator.Aggregate(ctx, message, signatureShares, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify signature is not nil
|
||||
if signature == nil {
|
||||
t.Fatal("signature is nil")
|
||||
}
|
||||
|
||||
// Verify scheme ID
|
||||
if signature.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("signature schemeID = %v, want %v", signature.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Create verifier
|
||||
verifier, err := scheme.NewVerifier(groupKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVerifier failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify group key matches
|
||||
if !verifier.GroupKey().Equal(groupKey) {
|
||||
t.Error("verifier group key mismatch")
|
||||
}
|
||||
|
||||
// Create a signer and sign
|
||||
signer, err := scheme.NewSigner(shares[0])
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("test message")
|
||||
share, err := signer.SignShare(ctx, message, []int{0}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare failed: %v", err)
|
||||
}
|
||||
|
||||
// Create aggregator and aggregate (single share for test)
|
||||
aggregator, err := scheme.NewAggregator(groupKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAggregator failed: %v", err)
|
||||
}
|
||||
|
||||
sig, err := aggregator.Aggregate(ctx, message, []threshold.SignatureShare{share}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
if !verifier.Verify(message, sig) {
|
||||
t.Error("Verify returned false, expected true")
|
||||
}
|
||||
|
||||
// Verify with bytes
|
||||
if !verifier.VerifyBytes(message, sig.Bytes()) {
|
||||
t.Error("VerifyBytes returned false, expected true")
|
||||
}
|
||||
|
||||
// Verify wrong message fails
|
||||
if verifier.Verify([]byte("wrong message"), sig) {
|
||||
t.Error("Verify returned true for wrong message, expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDKGConfig(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
// NewDKG is intentionally unimplemented — it must return an error
|
||||
// directing callers to threshold/protocols/frost for production DKG.
|
||||
config := threshold.DKGConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
PartyIndex: 0,
|
||||
}
|
||||
|
||||
_, err := scheme.NewDKG(config)
|
||||
if err == nil {
|
||||
t.Fatal("NewDKG should return an error (not implemented)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureShareSerialization(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, _, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
signer, err := scheme.NewSigner(shares[0])
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("test message")
|
||||
sigShare, err := signer.SignShare(ctx, message, []int{0}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare failed: %v", err)
|
||||
}
|
||||
|
||||
// Serialize
|
||||
data := sigShare.Bytes()
|
||||
if len(data) != SignatureShareSize {
|
||||
t.Errorf("signature share bytes length = %d, want %d", len(data), SignatureShareSize)
|
||||
}
|
||||
|
||||
// Deserialize
|
||||
parsed, err := scheme.ParseSignatureShare(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSignatureShare failed: %v", err)
|
||||
}
|
||||
|
||||
if parsed.Index() != sigShare.Index() {
|
||||
t.Errorf("parsed index = %d, want %d", parsed.Index(), sigShare.Index())
|
||||
}
|
||||
|
||||
if parsed.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("parsed schemeID = %v, want %v", parsed.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeySerialization(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Serialize
|
||||
data := groupKey.Bytes()
|
||||
if len(data) != PublicKeySize {
|
||||
t.Errorf("public key bytes length = %d, want %d", len(data), PublicKeySize)
|
||||
}
|
||||
|
||||
// Deserialize
|
||||
parsed, err := scheme.ParsePublicKey(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePublicKey failed: %v", err)
|
||||
}
|
||||
|
||||
if !parsed.Equal(groupKey) {
|
||||
t.Error("parsed public key does not equal original")
|
||||
}
|
||||
|
||||
if parsed.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("parsed schemeID = %v, want %v", parsed.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyEquality(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Generate two different group keys
|
||||
_, groupKey1, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares 1 failed: %v", err)
|
||||
}
|
||||
|
||||
_, groupKey2, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares 2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Same key should be equal to itself
|
||||
if !groupKey1.Equal(groupKey1) {
|
||||
t.Error("group key should equal itself")
|
||||
}
|
||||
|
||||
// Different keys should not be equal (with very high probability)
|
||||
// Note: There's an astronomically small chance this could fail randomly
|
||||
if groupKey1.Equal(groupKey2) {
|
||||
t.Error("different group keys should not be equal")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user