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"
|
"testing"
|
||||||
|
|
||||||
"github.com/luxfi/crypto/threshold"
|
"github.com/luxfi/crypto/threshold"
|
||||||
_ "github.com/luxfi/crypto/threshold/bls" // Register BLS scheme
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSigner(t *testing.T) {
|
func TestSigner(t *testing.T) {
|
||||||
@@ -21,15 +20,12 @@ func TestSigner(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BLS sign failed: %v", err)
|
t.Fatalf("BLS sign failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !signer.VerifyBLS(message, sig) {
|
if !signer.VerifyBLS(message, sig) {
|
||||||
t.Fatalf("BLS verification failed")
|
t.Fatalf("BLS verification failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.VerifyBLS([]byte("wrong"), sig) {
|
if signer.VerifyBLS([]byte("wrong"), sig) {
|
||||||
t.Fatalf("BLS should not verify wrong message")
|
t.Fatalf("BLS should not verify wrong message")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("BLS signature size: %d bytes", len(sig))
|
t.Logf("BLS signature size: %d bytes", len(sig))
|
||||||
t.Logf("BLS public key size: %d bytes", len(signer.GetBLSPublicKey()))
|
t.Logf("BLS public key size: %d bytes", len(signer.GetBLSPublicKey()))
|
||||||
})
|
})
|
||||||
@@ -39,204 +35,58 @@ func TestSigner(t *testing.T) {
|
|||||||
if len(seed) != 32 {
|
if len(seed) != 32 {
|
||||||
t.Fatalf("Threshold seed should be 32 bytes, got %d", len(seed))
|
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) {
|
t.Run("ThresholdNotInitialized", func(t *testing.T) {
|
||||||
if signer.HasThresholdKey() {
|
if signer.HasThresholdKey() {
|
||||||
t.Fatal("Signer should not have threshold key initially")
|
t.Fatal("Signer should not have threshold key initially")
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.ThresholdSchemeID() != threshold.SchemeUnknown {
|
if signer.ThresholdSchemeID() != threshold.SchemeUnknown {
|
||||||
t.Fatalf("Expected SchemeUnknown, got %v", signer.ThresholdSchemeID())
|
t.Fatalf("Expected SchemeUnknown, got %v", signer.ThresholdSchemeID())
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.ThresholdIndex() != -1 {
|
if signer.ThresholdIndex() != -1 {
|
||||||
t.Fatalf("Expected index -1, got %d", signer.ThresholdIndex())
|
t.Fatalf("Expected index -1, got %d", signer.ThresholdIndex())
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.ThresholdPublicShare() != nil {
|
if signer.ThresholdPublicShare() != nil {
|
||||||
t.Fatal("Expected nil public share")
|
t.Fatal("Expected nil public share")
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.ThresholdGroupKey() != nil {
|
if signer.ThresholdGroupKey() != nil {
|
||||||
t.Fatal("Expected nil group key")
|
t.Fatal("Expected nil group key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Operations should fail without threshold key
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, err := signer.SignThresholdShare(ctx, message, []int{0})
|
if _, err := signer.SignThresholdShare(ctx, message, []int{0}); err != threshold.ErrNotInitialized {
|
||||||
if err != threshold.ErrNotInitialized {
|
|
||||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||||
}
|
}
|
||||||
|
if _, err := signer.AggregateThresholdShares(ctx, message, nil); err != threshold.ErrNotInitialized {
|
||||||
_, err = signer.AggregateThresholdShares(ctx, message, nil)
|
|
||||||
if err != threshold.ErrNotInitialized {
|
|
||||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||||
}
|
}
|
||||||
|
if err := signer.VerifyThresholdShare(message, nil, nil); err != threshold.ErrNotInitialized {
|
||||||
err = signer.VerifyThresholdShare(message, nil, nil)
|
|
||||||
if err != threshold.ErrNotInitialized {
|
|
||||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.VerifyThreshold(message, nil) {
|
if signer.VerifyThreshold(message, nil) {
|
||||||
t.Fatal("Expected false without threshold key")
|
t.Fatal("Expected false without threshold key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.VerifyThresholdBytes(message, nil) {
|
if signer.VerifyThresholdBytes(message, nil) {
|
||||||
t.Fatal("Expected false without threshold key")
|
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) {
|
func BenchmarkSigner(b *testing.B) {
|
||||||
signer, err := NewSigner()
|
signer, err := NewSigner()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
message := []byte("Benchmark message for performance testing")
|
message := []byte("Benchmark message for performance testing")
|
||||||
|
|
||||||
b.Run("BLS_Sign", func(b *testing.B) {
|
b.Run("BLS_Sign", func(b *testing.B) {
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_, err := signer.SignBLS(message)
|
if _, err := signer.SignBLS(message); err != nil {
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
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