BLS Signatures
Boneh-Lynn-Shacham signature scheme with aggregation and threshold support
BLS Signatures
BLS (Boneh-Lynn-Shacham) signatures are a critical component of Lux's consensus mechanism, enabling efficient signature aggregation for validator voting and threshold cryptography. This implementation uses BLS12-381 curve for 128-bit security.
Overview
BLS signatures offer unique properties that make them ideal for blockchain consensus:
- Signature Aggregation: Combine multiple signatures into a single constant-size signature
- Threshold Signatures: Enable k-of-n signature schemes
- Non-interactive Aggregation: No additional communication between signers
- Deterministic Verification: Signature verification is deterministic and efficient
Technical Details
Cryptographic Parameters
| Parameter | Value | Description |
|---|---|---|
| Curve | BLS12-381 | Pairing-friendly elliptic curve |
| Security Level | 128-bit | Equivalent to 3072-bit RSA |
| Public Key Size | 48 bytes | Compressed G1 point |
| Private Key Size | 32 bytes | Scalar field element |
| Signature Size | 96 bytes | Compressed G2 point |
| Aggregated Signature Size | 96 bytes | Constant size regardless of signers |
Mathematical Foundation
BLS signatures are based on bilinear pairings over elliptic curves:
e: G1 × G2 → GTWhere:
G1,G2are cyclic groups of prime orderrGTis the target groupeis the bilinear pairing function
Implementation
Key Generation
Generate a new BLS key pair for validators:
package main
import (
"fmt"
"log"
"github.com/luxfi/crypto/bls"
)
func main() {
// Generate new private key
privateKey, err := bls.NewSecretKey()
if err != nil {
log.Fatal("Failed to generate private key:", err)
}
// Derive public key
publicKey := bls.PublicKeyFromSecretKey(privateKey)
// Serialize for storage
privBytes := bls.SecretKeyToBytes(privateKey)
pubBytes := bls.PublicKeyToCompressedBytes(publicKey)
fmt.Printf("Private Key: %x\n", privBytes)
fmt.Printf("Public Key: %x\n", pubBytes)
}Signing Messages
Sign messages for consensus voting:
func signMessage(privateKey *bls.SecretKey, message []byte) (*bls.Signature, error) {
// Sign the message
signature := bls.Sign(privateKey, message)
// Serialize signature for transmission
sigBytes := bls.SignatureToBytes(signature)
// Verify signature locally (optional)
publicKey := bls.PublicKeyFromSecretKey(privateKey)
if !bls.Verify(publicKey, message, signature) {
return nil, errors.New("signature verification failed")
}
return signature, nil
}Signature Verification
Verify individual signatures from validators:
func verifySignature(publicKey *bls.PublicKey, message []byte, signature *bls.Signature) bool {
// Simple verification
return bls.Verify(publicKey, message, signature)
}
// Batch verification for efficiency
func batchVerify(publicKeys []*bls.PublicKey, messages [][]byte, signatures []*bls.Signature) bool {
if len(publicKeys) != len(messages) || len(messages) != len(signatures) {
return false
}
for i := range publicKeys {
if !bls.Verify(publicKeys[i], messages[i], signatures[i]) {
return false
}
}
return true
}Signature Aggregation
The key feature of BLS signatures is efficient aggregation:
Basic Aggregation
Aggregate multiple signatures on the same message:
func aggregateSignatures(signatures []*bls.Signature) (*bls.AggregateSignature, error) {
if len(signatures) == 0 {
return nil, bls.ErrNoSignatures
}
// Aggregate all signatures
aggregated, err := bls.AggregateSignatures(signatures)
if err != nil {
return nil, fmt.Errorf("aggregation failed: %w", err)
}
return aggregated, nil
}
// Verify aggregated signature
func verifyAggregated(publicKeys []*bls.PublicKey, message []byte, aggregatedSig *bls.AggregateSignature) bool {
return bls.VerifyAggregateCommon(publicKeys, message, aggregatedSig)
}Proof of Possession
Prevent rogue key attacks with proof of possession:
func generateProofOfPossession(privateKey *bls.SecretKey) *bls.Signature {
publicKey := bls.PublicKeyFromSecretKey(privateKey)
publicKeyBytes := bls.PublicKeyToCompressedBytes(publicKey)
// Sign own public key as proof of possession
pop := bls.Sign(privateKey, publicKeyBytes)
return pop
}
func verifyProofOfPossession(publicKey *bls.PublicKey, pop *bls.Signature) bool {
publicKeyBytes := bls.PublicKeyToCompressedBytes(publicKey)
return bls.Verify(publicKey, publicKeyBytes, pop)
}Threshold Signatures
Implement k-of-n threshold signatures for distributed consensus:
// Threshold signature share generation
type ThresholdSigner struct {
index int
privateKey *bls.SecretKey
publicKeys []*bls.PublicKey
threshold int
}
func (ts *ThresholdSigner) SignShare(message []byte) *bls.Signature {
return bls.Sign(ts.privateKey, message)
}
// Combine threshold signature shares
func combineThresholdSignatures(shares []*bls.Signature, indices []int, threshold int) (*bls.Signature, error) {
if len(shares) < threshold {
return nil, fmt.Errorf("insufficient shares: need %d, got %d", threshold, len(shares))
}
// Use first k shares (simplified - actual implementation uses Lagrange interpolation)
validShares := shares[:threshold]
// Aggregate the shares
combined, err := bls.AggregateSignatures(validShares)
if err != nil {
return nil, err
}
return combined, nil
}Performance Benchmarks
Performance characteristics on modern hardware (Apple M1):
| Operation | Time | Throughput | Notes |
|---|---|---|---|
| Key Generation | 150 μs | 6,666 ops/sec | One-time per validator |
| Sign | 1.2 ms | 833 ops/sec | Per message signing |
| Verify | 2.5 ms | 400 ops/sec | Single signature |
| Aggregate (100 sigs) | 0.5 ms | 2,000 ops/sec | Constant time |
| Verify Aggregate (100) | 250 ms | 4 ops/sec | Batch verification available |
| Pairing | 2.1 ms | 476 ops/sec | Core operation |
Optimization Techniques
- Signature Caching: Cache verified signatures to avoid repeated verification
- Batch Verification: Verify multiple signatures in a single operation
- Parallel Verification: Use goroutines for concurrent signature verification
- Precomputed Tables: Use precomputed pairing tables for faster verification
Security Considerations
Best Practices
-
Key Generation
// Always use cryptographically secure randomness privateKey, err := bls.NewSecretKey() // Uses crypto/rand internally // Never use deterministic key generation in production -
Proof of Possession
// Always verify proof of possession before accepting public keys if !verifyProofOfPossession(publicKey, pop) { return errors.New("invalid proof of possession") } -
Message Domain Separation
// Use domain separation for different message types func signWithDomain(sk *bls.SecretKey, message []byte, domain string) *bls.Signature { prefixedMsg := append([]byte(domain), message...) return bls.Sign(sk, prefixedMsg) }
Attack Vectors and Mitigations
| Attack | Description | Mitigation |
|---|---|---|
| Rogue Key Attack | Adversary creates key to cancel honest signatures | Proof of Possession |
| Related Key Attack | Exploiting algebraic relationships | Domain separation |
| Small Subgroup Attack | Using points from small subgroups | Subgroup checks |
| Side Channel Attack | Timing/power analysis | Constant-time implementation |
Integration Examples
Consensus Voting
Implement efficient consensus voting with BLS aggregation:
type ConsensusVote struct {
Height uint64
BlockHash []byte
Signature *bls.Signature
}
type VoteAggregator struct {
votes map[string]*ConsensusVote
publicKeys map[string]*bls.PublicKey
}
func (va *VoteAggregator) AddVote(voterID string, vote *ConsensusVote) error {
// Verify individual vote
pubKey := va.publicKeys[voterID]
message := append(toBytes(vote.Height), vote.BlockHash...)
if !bls.Verify(pubKey, message, vote.Signature) {
return errors.New("invalid vote signature")
}
va.votes[voterID] = vote
return nil
}
func (va *VoteAggregator) GetQuorumCertificate(threshold int) (*QuorumCertificate, error) {
if len(va.votes) < threshold {
return nil, errors.New("insufficient votes")
}
// Collect signatures
var signatures []*bls.Signature
var signers []string
for voterID, vote := range va.votes {
signatures = append(signatures, vote.Signature)
signers = append(signers, voterID)
}
// Aggregate signatures
aggregatedSig, err := bls.AggregateSignatures(signatures)
if err != nil {
return nil, err
}
return &QuorumCertificate{
Height: va.votes[signers[0]].Height,
BlockHash: va.votes[signers[0]].BlockHash,
Signature: aggregatedSig,
Signers: signers,
}, nil
}Multi-Signature Wallets
Implement multi-signature wallets with threshold BLS:
type MultiSigWallet struct {
threshold int
publicKeys []*bls.PublicKey
pending map[string][]*bls.Signature
}
func (w *MultiSigWallet) SubmitSignature(txHash []byte, signature *bls.Signature, signerIndex int) error {
key := hex.EncodeToString(txHash)
// Verify signature
if !bls.Verify(w.publicKeys[signerIndex], txHash, signature) {
return errors.New("invalid signature")
}
w.pending[key] = append(w.pending[key], signature)
// Check if threshold reached
if len(w.pending[key]) >= w.threshold {
return w.executeTx(txHash)
}
return nil
}Testing
Comprehensive testing for BLS signatures:
func TestBLSAggregation(t *testing.T) {
// Generate validators
validators := make([]*bls.SecretKey, 100)
publicKeys := make([]*bls.PublicKey, 100)
for i := range validators {
validators[i], _ = bls.NewSecretKey()
publicKeys[i] = bls.PublicKeyFromSecretKey(validators[i])
}
// Sign message
message := []byte("consensus block at height 12345")
signatures := make([]*bls.Signature, len(validators))
for i, validator := range validators {
signatures[i] = bls.Sign(validator, message)
}
// Aggregate
aggregated, err := bls.AggregateSignatures(signatures)
require.NoError(t, err)
// Verify aggregated signature
valid := bls.VerifyAggregateCommon(publicKeys, message, aggregated)
require.True(t, valid)
// Test with subset (threshold)
threshold := 67 // 2/3 + 1
subsetSigs := signatures[:threshold]
subsetPubs := publicKeys[:threshold]
subsetAgg, err := bls.AggregateSignatures(subsetSigs)
require.NoError(t, err)
valid = bls.VerifyAggregateCommon(subsetPubs, message, subsetAgg)
require.True(t, valid)
}Advanced Features
Aggregate Verification
Verify multiple messages with different signers efficiently:
func aggregateVerifyDistinct(publicKeys []*bls.PublicKey, messages [][]byte, signatures []*bls.Signature) bool {
// Each signer signs a different message
if len(publicKeys) != len(messages) || len(messages) != len(signatures) {
return false
}
// Aggregate signatures
aggregated, err := bls.AggregateSignatures(signatures)
if err != nil {
return false
}
// Verify using distinct message aggregation
return bls.VerifyAggregateDistinct(publicKeys, messages, aggregated)
}Distributed Key Generation (DKG)
Setup for threshold signatures without trusted dealer:
type DKGParticipant struct {
index int
secret *bls.SecretKey
shares map[int]*bls.SecretKey
publicPoly []*bls.PublicKey
}
func (p *DKGParticipant) GenerateShares(threshold, total int) {
// Generate polynomial coefficients
coeffs := make([]*bls.SecretKey, threshold)
coeffs[0] = p.secret
for i := 1; i < threshold; i++ {
coeffs[i], _ = bls.NewSecretKey()
}
// Generate shares for each participant
for j := 1; j <= total; j++ {
share := evaluatePolynomial(coeffs, j)
p.shares[j] = share
}
// Publish public polynomial
for _, coeff := range coeffs {
p.publicPoly = append(p.publicPoly, bls.PublicKeyFromSecretKey(coeff))
}
}