scheme/bls: home the BLS threshold Scheme impl here (was crypto/threshold/bls)

Consolidates all threshold-signature IMPLEMENTATIONS under luxfi/threshold
(alongside cmp/frost/lss/corona/pulsar). The Scheme/Signer/Aggregator/
Verifier INTERFACE stays in luxfi/crypto/threshold — it is the shared
low-level contract that crypto/signer + hsm depend on, so it cannot move
up without a module cycle. One home for impls, one home for the contract.

scheme/bls/ carries the native BLS Scheme + the dealerless Pedersen-VSS
DKG (RunDKG). It implements crypto/threshold.Scheme using crypto/bls
primitives. Consumers blank-import luxfi/threshold/scheme/bls to register.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-03 14:34:58 -07:00
co-authored by Hanzo Dev
parent b4b124eeff
commit 2f9bfb1b09
5 changed files with 1740 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bls
import (
"context"
"crypto/rand"
"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
}
if rng == nil {
rng = rand.Reader
}
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
}
+114
View File
@@ -0,0 +1,114 @@
// 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])
}
}
}
+883
View File
@@ -0,0 +1,883 @@
// 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
+478
View File
@@ -0,0 +1,478 @@
// 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")
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bls_test
// Integration test for luxfi/crypto/signer's threshold-signing feature
// backed by the BLS Scheme. It lives HERE (not in luxfi/crypto) because it
// needs a concrete threshold scheme: importing crypto/signer from
// luxfi/threshold is the allowed dependency direction (threshold -> crypto),
// whereas the reverse would create a crypto <-> threshold module cycle.
import (
"context"
"testing"
"github.com/luxfi/crypto/signer"
"github.com/luxfi/crypto/threshold"
bls "github.com/luxfi/threshold/scheme/bls" // BLS Scheme (init registers it) + RunDKG
)
func TestSignerWithThresholdBLS(t *testing.T) {
// BLS Scheme must be registered (the bls import's init did this).
if _, err := threshold.GetScheme(threshold.SchemeBLS); err != nil {
t.Fatalf("GetScheme(BLS): %v", err)
}
// Dealerless keygen: Pedersen-VSS DKG, no trusted dealer.
shares, groupKey, err := bls.RunDKG(context.Background(), 2, 3, nil)
if err != nil {
t.Fatalf("RunDKG: %v", err)
}
signers := make([]*signer.Signer, len(shares))
for i, share := range shares {
s, err := signer.NewSignerWithThreshold(share)
if err != nil {
t.Fatalf("NewSignerWithThreshold[%d]: %v", i, err)
}
signers[i] = s
}
t.Run("KeyInfo", func(t *testing.T) {
for i, s := range signers {
if !s.HasThresholdKey() {
t.Fatalf("signer %d missing threshold key", i)
}
if s.ThresholdSchemeID() != threshold.SchemeBLS {
t.Fatalf("signer %d: scheme %v, want BLS", i, s.ThresholdSchemeID())
}
if s.ThresholdIndex() != i {
t.Fatalf("signer %d: index %d", i, s.ThresholdIndex())
}
if !s.ThresholdGroupKey().Equal(groupKey) {
t.Fatalf("signer %d: group key mismatch", i)
}
}
})
t.Run("SetThresholdKeyShare", func(t *testing.T) {
s, err := signer.NewSigner()
if err != nil {
t.Fatal(err)
}
if s.HasThresholdKey() {
t.Fatal("fresh signer should have no threshold key")
}
if err := s.SetThresholdKeyShare(shares[0]); err != nil {
t.Fatalf("SetThresholdKeyShare: %v", err)
}
if !s.HasThresholdKey() || s.ThresholdIndex() != 0 {
t.Fatal("threshold key not set correctly")
}
})
}