build: require 'gpu' build tag for CGO GPU code

Changes:
- gpu/crypto_cgo.go: //go:build cgo -> //go:build cgo && gpu
- gpu/pool.go: //go:build cgo -> //go:build cgo && gpu
- gpu/crypto.go: //go:build \!cgo -> //go:build \!cgo || \!gpu
- gpu/crypto_cgo_test.go: //go:build cgo -> //go:build cgo && gpu
- gpu/pool_test.go: //go:build cgo -> //go:build cgo && gpu

This allows CGO_ENABLED=1 builds to work without requiring lux-crypto
to be installed. To enable GPU acceleration, build with -tags gpu.
This commit is contained in:
Zach Kelling
2025-12-26 19:38:29 -08:00
parent 5fca6aec19
commit 52c5488b7e
9 changed files with 704 additions and 234 deletions
+5 -2
View File
@@ -2,10 +2,13 @@
package gpu
// Sizes
// Note: gnark-crypto uses uncompressed point serialization:
// - G1 (public key): 96 bytes uncompressed
// - G2 (signature): 192 bytes uncompressed
const (
BLSSecretKeySize = 32
BLSPublicKeySize = 48
BLSSignatureSize = 96
BLSPublicKeySize = 96 // G1 uncompressed
BLSSignatureSize = 192 // G2 uncompressed
BLSMessageSize = 32
MLDSASecretKeySize = 4032
+35 -14
View File
@@ -1,12 +1,14 @@
//go:build !cgo
//go:build !cgo || !gpu
// Package gpu provides GPU-accelerated cryptographic operations.
// This file provides pure Go fallback implementations when CGO is disabled.
// This file provides pure Go fallback implementations when CGO is disabled or the gpu build tag is not set.
// Uses gnark-crypto for BLS12-381, cloudflare/circl for ML-DSA.
package gpu
import (
"crypto/rand"
"errors"
"math/big"
"sync"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
@@ -16,6 +18,13 @@ import (
"golang.org/x/crypto/sha3"
)
// Error definitions
var (
ErrInvalidKey = errors.New("invalid key")
ErrInvalidSignature = errors.New("invalid signature")
ErrCGORequired = errors.New("CGO required for this operation")
)
// Pure Go fallback - no GPU acceleration
func GPUAvailable() bool { return false }
func GetBackend() string { return "CPU (pure Go)" }
@@ -52,7 +61,7 @@ func BLSSecretKeyToPublicKey(sk []byte) ([]byte, error) {
_, _, g1Gen, _ := bls12381.Generators()
var pk bls12381.G1Affine
pk.ScalarMultiplication(&g1Gen, scalar.BigInt(nil))
pk.ScalarMultiplication(&g1Gen, scalar.BigInt(new(big.Int)))
return pk.Marshal(), nil
}
@@ -75,7 +84,7 @@ func BLSSign(sk, msg []byte) ([]byte, error) {
// Multiply by secret key
var sig bls12381.G2Affine
sig.ScalarMultiplication(&msgPoint, scalar.BigInt(nil))
sig.ScalarMultiplication(&msgPoint, scalar.BigInt(new(big.Int)))
return sig.Marshal(), nil
}
@@ -224,22 +233,32 @@ func BLSBatchSign(sks, msgs [][]byte) ([][]byte, error) {
// =============================================================================
func MLDSAKeygen(seed []byte) (pk, sk []byte, err error) {
var pubKey mldsa65.PublicKey
var privKey mldsa65.PrivateKey
var pubKey *mldsa65.PublicKey
var privKey *mldsa65.PrivateKey
if seed != nil && len(seed) >= 32 {
// Deterministic keygen from seed
mldsa65.NewKeyFromSeed(&pubKey, &privKey, seed[:32])
// Deterministic keygen from seed - copy to fixed size array
var seedArr [32]byte
copy(seedArr[:], seed[:32])
pubKey, privKey = mldsa65.NewKeyFromSeed(&seedArr)
} else {
// Random keygen
if _, err = mldsa65.GenerateKey(nil); err != nil {
pubKey, privKey, err = mldsa65.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
}
pk = pubKey.Bytes()
sk = privKey.Bytes()
return pk, sk, nil
pkBytes, err := pubKey.MarshalBinary()
if err != nil {
return nil, nil, err
}
skBytes, err := privKey.MarshalBinary()
if err != nil {
return nil, nil, err
}
return pkBytes, skBytes, nil
}
func MLDSASign(sk, msg []byte) ([]byte, error) {
@@ -248,7 +267,9 @@ func MLDSASign(sk, msg []byte) ([]byte, error) {
return nil, err
}
sig := mldsa65.SignTo(&privKey, msg, nil, false, nil)
// SignTo writes to a provided buffer, returns signature
sig := make([]byte, mldsa65.SignatureSize)
mldsa65.SignTo(&privKey, msg, nil, false, sig)
return sig, nil
}
@@ -388,7 +409,7 @@ func (tc *ThresholdContext) Combine(partialSigs [][]byte, indices []uint32) ([]b
}
var scaled bls12381.G2Affine
scaled.ScalarMultiplication(&sig, lagrange[i].BigInt(nil))
scaled.ScalarMultiplication(&sig, lagrange[i].BigInt(new(big.Int)))
if i == 0 {
result.FromAffine(&scaled)
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build cgo
//go:build cgo && gpu
// Package gpu provides GPU-accelerated cryptographic operations.
//
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build cgo
//go:build cgo && gpu
package gpu
+234 -61
View File
@@ -3,7 +3,7 @@
package gpu
import (
"errors"
"bytes"
"testing"
)
@@ -13,99 +13,272 @@ func TestNoCGO_GPUAvailable(t *testing.T) {
}
backend := GetBackend()
if backend != "CPU (CGO disabled)" {
t.Errorf("GetBackend = %q, want %q", backend, "CPU (CGO disabled)")
if backend != "CPU (pure Go)" {
t.Errorf("GetBackend = %q, want %q", backend, "CPU (pure Go)")
}
t.Logf("Backend: %s (CGO disabled)", backend)
t.Logf("Backend: %s (pure Go fallback active)", backend)
}
func TestNoCGO_BLSReturnsError(t *testing.T) {
_, err := BLSKeygen(nil)
if !errors.Is(err, ErrCGORequired) {
t.Errorf("BLSKeygen error = %v, want ErrCGORequired", err)
func TestNoCGO_BLSFunctions(t *testing.T) {
// Test BLSKeygen with seed
seed := make([]byte, 32)
for i := range seed {
seed[i] = byte(i + 1)
}
_, err = BLSSecretKeyToPublicKey(make([]byte, 32))
if !errors.Is(err, ErrCGORequired) {
t.Errorf("BLSSecretKeyToPublicKey error = %v, want ErrCGORequired", err)
sk, err := BLSKeygen(seed)
if err != nil {
t.Fatalf("BLSKeygen with seed failed: %v", err)
}
if len(sk) == 0 {
t.Fatal("BLSKeygen returned empty secret key")
}
t.Logf("BLS secret key generated: %d bytes", len(sk))
// Test BLSKeygen without seed (random)
sk2, err := BLSKeygen(nil)
if err != nil {
t.Fatalf("BLSKeygen without seed failed: %v", err)
}
if bytes.Equal(sk, sk2) {
t.Error("Two BLSKeygen calls should produce different keys")
}
_, err = BLSSign(make([]byte, 32), make([]byte, 32))
if !errors.Is(err, ErrCGORequired) {
t.Errorf("BLSSign error = %v, want ErrCGORequired", err)
// Test BLSSecretKeyToPublicKey
pk, err := BLSSecretKeyToPublicKey(sk)
if err != nil {
t.Fatalf("BLSSecretKeyToPublicKey failed: %v", err)
}
if len(pk) == 0 {
t.Fatal("BLSSecretKeyToPublicKey returned empty public key")
}
t.Logf("BLS public key derived: %d bytes", len(pk))
// Test BLSSign
message := SHA3_256([]byte("test message for BLS signature")) // 32 bytes
sig, err := BLSSign(sk, message)
if err != nil {
t.Fatalf("BLSSign failed: %v", err)
}
if len(sig) == 0 {
t.Fatal("BLSSign returned empty signature")
}
t.Logf("BLS signature generated: %d bytes", len(sig))
// Test BLSVerify - signature order is (sig, pk, msg)
valid := BLSVerify(sig, pk, message)
if !valid {
t.Error("BLSVerify failed for valid signature")
}
if BLSVerify(nil, nil, nil) {
t.Error("BLSVerify should return false without CGO")
// Test verification with wrong message
wrongMessage := SHA3_256([]byte("wrong message"))
if BLSVerify(sig, pk, wrongMessage) {
t.Error("BLSVerify should fail for wrong message")
}
t.Log("All BLS functions correctly return ErrCGORequired")
t.Log("All BLS functions work correctly in pure Go mode")
}
func TestNoCGO_MLDSAReturnsError(t *testing.T) {
_, _, err := MLDSAKeygen(nil)
if !errors.Is(err, ErrCGORequired) {
t.Errorf("MLDSAKeygen error = %v, want ErrCGORequired", err)
func TestNoCGO_MLDSAFunctions(t *testing.T) {
// Test MLDSAKeygen with seed
seed := make([]byte, 32)
for i := range seed {
seed[i] = byte(i + 1)
}
_, err = MLDSASign(make([]byte, 4032), []byte("test"))
if !errors.Is(err, ErrCGORequired) {
t.Errorf("MLDSASign error = %v, want ErrCGORequired", err)
pk, sk, err := MLDSAKeygen(seed)
if err != nil {
t.Fatalf("MLDSAKeygen with seed failed: %v", err)
}
if len(pk) == 0 {
t.Fatal("MLDSAKeygen returned empty public key")
}
if len(sk) == 0 {
t.Fatal("MLDSAKeygen returned empty secret key")
}
t.Logf("ML-DSA keypair generated: pk=%d bytes, sk=%d bytes", len(pk), len(sk))
// Test MLDSAKeygen without seed (random)
pk2, sk2, err := MLDSAKeygen(nil)
if err != nil {
t.Fatalf("MLDSAKeygen without seed failed: %v", err)
}
if bytes.Equal(pk, pk2) || bytes.Equal(sk, sk2) {
t.Error("Two MLDSAKeygen calls should produce different keypairs")
}
if MLDSAVerify(nil, nil, nil) {
t.Error("MLDSAVerify should return false without CGO")
// Test MLDSASign
message := []byte("test message for ML-DSA signature")
sig, err := MLDSASign(sk, message)
if err != nil {
t.Fatalf("MLDSASign failed: %v", err)
}
if len(sig) == 0 {
t.Fatal("MLDSASign returned empty signature")
}
t.Logf("ML-DSA signature generated: %d bytes", len(sig))
// Test MLDSAVerify - argument order is (sig, msg, pk)
valid := MLDSAVerify(sig, message, pk)
if !valid {
t.Error("MLDSAVerify failed for valid signature")
}
t.Log("All ML-DSA functions correctly return ErrCGORequired")
// Test verification with wrong message
wrongMessage := []byte("wrong message")
if MLDSAVerify(sig, wrongMessage, pk) {
t.Error("MLDSAVerify should fail for wrong message")
}
t.Log("All ML-DSA functions work correctly in pure Go mode")
}
func TestNoCGO_ThresholdReturnsError(t *testing.T) {
_, err := NewThresholdContext(3, 5)
if !errors.Is(err, ErrCGORequired) {
t.Errorf("NewThresholdContext error = %v, want ErrCGORequired", err)
func TestNoCGO_HashFunctions(t *testing.T) {
data := []byte("test data for hashing")
// Test SHA3_256
hash := SHA3_256(data)
if len(hash) != 32 {
t.Errorf("SHA3_256 output length = %d, want 32", len(hash))
}
t.Logf("SHA3-256: %x", hash)
// Test SHA3_512
hash = SHA3_512(data)
if len(hash) != 64 {
t.Errorf("SHA3_512 output length = %d, want 64", len(hash))
}
t.Logf("SHA3-512: %x", hash[:32])
// Test BLAKE3
hash = BLAKE3(data)
if len(hash) != 32 {
t.Errorf("BLAKE3 output length = %d, want 32", len(hash))
}
t.Logf("BLAKE3: %x", hash)
// Test BatchHash
inputs := [][]byte{
[]byte("message 1"),
[]byte("message 2"),
[]byte("message 3"),
}
hashes, err := BatchHash(inputs, HashTypeSHA3_256)
if err != nil {
t.Fatalf("BatchHash failed: %v", err)
}
if len(hashes) != len(inputs) {
t.Errorf("BatchHash returned %d hashes, want %d", len(hashes), len(inputs))
}
// Test methods on nil context (should not panic)
var ctx *ThresholdContext
ctx.Close() // Should not panic
_, _, err = ctx.Keygen(nil)
if !errors.Is(err, ErrCGORequired) {
t.Errorf("Keygen error = %v, want ErrCGORequired", err)
}
t.Log("All threshold functions correctly return ErrCGORequired")
t.Log("All hash functions work correctly in pure Go mode")
}
func TestNoCGO_HashReturnsNil(t *testing.T) {
data := []byte("test data")
func TestNoCGO_ThresholdContext(t *testing.T) {
// Test creating threshold context
ctx, err := NewThresholdContext(2, 3) // 2-of-3
if err != nil {
t.Fatalf("NewThresholdContext failed: %v", err)
}
defer ctx.Close()
if hash := SHA3_256(data); hash != nil {
t.Error("SHA3_256 should return nil without CGO")
t.Logf("Threshold context created: t=%d, n=%d", ctx.t, ctx.n)
// Test keygen - returns (shares, pk, err)
seed := make([]byte, 32)
for i := range seed {
seed[i] = byte(i + 42)
}
shares, pk, err := ctx.Keygen(seed)
if err != nil {
t.Fatalf("Threshold Keygen failed: %v", err)
}
if len(pk) == 0 {
t.Fatal("Keygen returned empty public key")
}
if len(shares) != 3 {
t.Errorf("Keygen returned %d shares, want 3", len(shares))
}
t.Logf("Threshold keygen complete: pk=%d bytes, %d shares", len(pk), len(shares))
// Test signing
message := SHA3_256([]byte("threshold test message")) // 32 bytes for BLS
partialSigs := make([][]byte, 2)
indices := []uint32{0, 2} // Use first and third party
for i, idx := range indices {
ps, err := ctx.PartialSign(idx, shares[idx], message)
if err != nil {
t.Fatalf("Threshold PartialSign for party %d failed: %v", idx, err)
}
partialSigs[i] = ps
}
t.Logf("Generated %d partial signatures", len(partialSigs))
// Test combination
sig, err := ctx.Combine(partialSigs, indices)
if err != nil {
t.Fatalf("Threshold Combine failed: %v", err)
}
if len(sig) == 0 {
t.Fatal("Combine returned empty signature")
}
t.Logf("Combined signature: %d bytes", len(sig))
// Test verification - Verify(sig, pk, msg)
valid := ctx.Verify(sig, pk, message)
if !valid {
t.Error("Threshold Verify failed for valid signature")
}
if hash := SHA3_512(data); hash != nil {
t.Error("SHA3_512 should return nil without CGO")
// Test verification with wrong message
wrongMessage := SHA3_256([]byte("wrong message"))
if ctx.Verify(sig, pk, wrongMessage) {
t.Error("Threshold Verify should fail for wrong message")
}
if hash := BLAKE3(data); hash != nil {
t.Error("BLAKE3 should return nil without CGO")
}
_, err := BatchHash([][]byte{data}, HashTypeSHA3_256)
if !errors.Is(err, ErrCGORequired) {
t.Errorf("BatchHash error = %v, want ErrCGORequired", err)
}
t.Log("All hash functions correctly return nil/ErrCGORequired")
t.Log("All threshold functions work correctly in pure Go mode")
}
func TestNoCGO_ConsensusVerifyBlock(t *testing.T) {
if ConsensusVerifyBlock(nil, nil, nil, nil, nil) {
t.Error("ConsensusVerifyBlock should return false without CGO")
}
// Create test data - need arrays of signatures and public keys
blockHash := SHA3_256([]byte("test block"))
t.Log("ConsensusVerifyBlock correctly returns false")
// Generate BLS keypairs
sk1, _ := BLSKeygen(nil)
pk1, _ := BLSSecretKeyToPublicKey(sk1)
sig1, _ := BLSSign(sk1, blockHash)
sk2, _ := BLSKeygen(nil)
pk2, _ := BLSSecretKeyToPublicKey(sk2)
sig2, _ := BLSSign(sk2, blockHash)
blsSigs := [][]byte{sig1, sig2}
blsPKs := [][]byte{pk1, pk2}
// Test consensus verify with multiple BLS signatures
result := ConsensusVerifyBlock(blsSigs, blsPKs, nil, nil, blockHash)
if !result {
t.Error("ConsensusVerifyBlock failed for valid signatures")
}
t.Logf("ConsensusVerifyBlock with %d BLS signatures: %v", len(blsSigs), result)
// Test with threshold signature too
ctx, _ := NewThresholdContext(2, 3)
defer ctx.Close()
shares, threshPK, _ := ctx.Keygen(nil)
ps1, _ := ctx.PartialSign(0, shares[0], blockHash)
ps2, _ := ctx.PartialSign(1, shares[1], blockHash)
threshSig, _ := ctx.Combine([][]byte{ps1, ps2}, []uint32{0, 1})
result = ConsensusVerifyBlock(blsSigs, blsPKs, threshSig, threshPK, blockHash)
if !result {
t.Error("ConsensusVerifyBlock failed with threshold signature")
}
t.Logf("ConsensusVerifyBlock with BLS + threshold: %v", result)
t.Log("ConsensusVerifyBlock works correctly in pure Go mode")
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build cgo
//go:build cgo && gpu
// Package gpu provides GPU-accelerated cryptographic operations.
package gpu
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build cgo
//go:build cgo && gpu
package gpu
+325 -129
View File
@@ -1,12 +1,17 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// BLS (Boneh-Lynn-Shacham) signature precompiled contracts
// For aggregated signatures and threshold cryptography
// Uses gnark-crypto for real BLS12-381 operations
package precompile
import (
"encoding/binary"
"errors"
"math/big"
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
)
// BLS precompile addresses
@@ -38,6 +43,10 @@ const (
// Per-item costs for aggregation
blsPerSignatureGas = 30000
blsPerPublicKeyGas = 10000
// Point sizes
g1Size = 48
g2Size = 96
)
// BLSVerify implements single BLS signature verification
@@ -48,25 +57,47 @@ func (b *BLSVerify) RequiredGas(input []byte) uint64 {
}
func (b *BLSVerify) Run(input []byte) ([]byte, error) {
// Input: [96 bytes signature][48 bytes public key][message]
if len(input) < 144 {
// Input: [96 bytes signature (G2)][48 bytes public key (G1)][message]
if len(input) < g2Size+g1Size+1 {
return nil, errors.New("input too short")
}
// For now, this is a placeholder
// Real implementation would use gnark-crypto BLS12-381
// Parse signature (G2 point)
var sig bls12381.G2Affine
if _, err := sig.SetBytes(input[:g2Size]); err != nil {
return nil, errors.New("invalid signature point")
}
// Parse signature (G2 point, 96 bytes)
signature := input[:96]
// Parse public key (G1 point, 48 bytes)
pubKey := input[96:144]
// Parse public key (G1 point)
var pubKey bls12381.G1Affine
if _, err := pubKey.SetBytes(input[g2Size : g2Size+g1Size]); err != nil {
return nil, errors.New("invalid public key point")
}
// Parse message
message := input[144:]
message := input[g2Size+g1Size:]
// Simplified verification (placeholder)
valid := len(signature) == 96 && len(pubKey) == 48 && len(message) > 0
// Hash message to G2
msgPoint, err := bls12381.HashToG2(message, nil)
if err != nil {
return nil, errors.New("hash to curve failed")
}
// Get G1 generator
_, _, g1Gen, _ := bls12381.Generators()
// Verify: e(pubKey, H(m)) == e(G1, sig)
// Equivalent to: e(pubKey, H(m)) * e(-G1, sig) == 1
var negG1 bls12381.G1Affine
negG1.Neg(&g1Gen)
valid, err := bls12381.PairingCheck(
[]bls12381.G1Affine{pubKey, negG1},
[]bls12381.G2Affine{msgPoint, sig},
)
if err != nil {
return nil, err
}
result := make([]byte, 32)
if valid {
@@ -76,11 +107,10 @@ func (b *BLSVerify) Run(input []byte) ([]byte, error) {
return result, nil
}
// BLSAggregateVerify verifies multiple signatures with different messages
// BLSAggregateVerify verifies aggregate signature with different messages
type BLSAggregateVerify struct{}
func (b *BLSAggregateVerify) RequiredGas(input []byte) uint64 {
// Parse number of signatures
if len(input) < 1 {
return blsAggregateVerifyGas
}
@@ -89,56 +119,79 @@ func (b *BLSAggregateVerify) RequiredGas(input []byte) uint64 {
}
func (b *BLSAggregateVerify) Run(input []byte) ([]byte, error) {
// Input: [1 byte num_sigs][signatures][public_keys][messages]
if len(input) < 1 {
// Input: [1 byte num_sigs][aggregate_sig (96)][public_keys][messages]
if len(input) < 1+g2Size {
return nil, errors.New("input too short")
}
numSigs := input[0]
// Calculate expected sizes
sigSize := 96 * int(numSigs)
pubKeySize := 48 * int(numSigs)
if len(input) < 1+sigSize+pubKeySize {
return nil, errors.New("invalid input size")
numSigs := int(input[0])
if numSigs == 0 {
return nil, errors.New("no signatures")
}
// Parse signatures
offset := 1
signatures := make([][]byte, numSigs)
for i := 0; i < int(numSigs); i++ {
signatures[i] = input[offset : offset+96]
offset += 96
// Parse aggregate signature
var aggSig bls12381.G2Affine
if _, err := aggSig.SetBytes(input[1 : 1+g2Size]); err != nil {
return nil, errors.New("invalid aggregate signature")
}
// Parse public keys
pubKeys := make([][]byte, numSigs)
for i := 0; i < int(numSigs); i++ {
pubKeys[i] = input[offset : offset+48]
offset += 48
offset := 1 + g2Size
pubKeys := make([]bls12381.G1Affine, numSigs)
for i := 0; i < numSigs; i++ {
if offset+g1Size > len(input) {
return nil, errors.New("invalid public key")
}
if _, err := pubKeys[i].SetBytes(input[offset : offset+g1Size]); err != nil {
return nil, errors.New("invalid public key point")
}
offset += g1Size
}
// Parse messages (variable length, encoded with length prefix)
messages := make([][]byte, numSigs)
for i := 0; i < int(numSigs); i++ {
// Parse messages (length-prefixed)
msgPoints := make([]bls12381.G2Affine, numSigs)
for i := 0; i < numSigs; i++ {
if offset+4 > len(input) {
return nil, errors.New("invalid message encoding")
}
msgLen := binary.BigEndian.Uint32(input[offset : offset+4])
msgLen := int(binary.BigEndian.Uint32(input[offset : offset+4]))
offset += 4
if offset+int(msgLen) > len(input) {
if offset+msgLen > len(input) {
return nil, errors.New("message too long")
}
messages[i] = input[offset : offset+int(msgLen)]
offset += int(msgLen)
msg := input[offset : offset+msgLen]
offset += msgLen
// Hash message to G2
msgPoint, err := bls12381.HashToG2(msg, nil)
if err != nil {
return nil, errors.New("hash to curve failed")
}
msgPoints[i] = msgPoint
}
// Verify aggregate signature (placeholder)
valid := true // Would call actual BLS aggregate verify
// Verify aggregate signature using multi-pairing
// e(pk1, H(m1)) * e(pk2, H(m2)) * ... * e(pkn, H(mn)) == e(G1, aggSig)
_, _, g1Gen, _ := bls12381.Generators()
g1Points := make([]bls12381.G1Affine, numSigs+1)
g2Points := make([]bls12381.G2Affine, numSigs+1)
copy(g1Points[:numSigs], pubKeys)
copy(g2Points[:numSigs], msgPoints)
// Add negated generator paired with aggregate signature
var negG1 bls12381.G1Affine
negG1.Neg(&g1Gen)
g1Points[numSigs] = negG1
g2Points[numSigs] = aggSig
valid, err := bls12381.PairingCheck(g1Points, g2Points)
if err != nil {
return nil, err
}
result := make([]byte, 32)
if valid {
@@ -161,31 +214,66 @@ func (b *BLSFastAggregate) RequiredGas(input []byte) uint64 {
func (b *BLSFastAggregate) Run(input []byte) ([]byte, error) {
// Input: [1 byte num_keys][96 bytes aggregate_sig][public_keys][message]
if len(input) < 98 {
if len(input) < 1+g2Size {
return nil, errors.New("input too short")
}
numKeys := input[0]
numKeys := int(input[0])
if numKeys == 0 {
return nil, errors.New("no public keys")
}
// Parse aggregate signature
aggSig := input[1:97]
var aggSig bls12381.G2Affine
if _, err := aggSig.SetBytes(input[1 : 1+g2Size]); err != nil {
return nil, errors.New("invalid aggregate signature")
}
// Parse public keys
offset := 97
pubKeys := make([][]byte, numKeys)
for i := 0; i < int(numKeys); i++ {
if offset+48 > len(input) {
// Parse and aggregate public keys
offset := 1 + g2Size
var aggPubKey bls12381.G1Affine
for i := 0; i < numKeys; i++ {
if offset+g1Size > len(input) {
return nil, errors.New("invalid public key")
}
pubKeys[i] = input[offset : offset+48]
offset += 48
var pk bls12381.G1Affine
if _, err := pk.SetBytes(input[offset : offset+g1Size]); err != nil {
return nil, errors.New("invalid public key point")
}
if i == 0 {
aggPubKey = pk
} else {
aggPubKey.Add(&aggPubKey, &pk)
}
offset += g1Size
}
// Parse message (remainder)
message := input[offset:]
if len(message) == 0 {
return nil, errors.New("empty message")
}
// Fast aggregate verify (all sign same message)
valid := len(aggSig) == 96 && len(message) > 0
// Hash message to G2
msgPoint, err := bls12381.HashToG2(message, nil)
if err != nil {
return nil, errors.New("hash to curve failed")
}
// Verify: e(aggPubKey, H(m)) == e(G1, aggSig)
_, _, g1Gen, _ := bls12381.Generators()
var negG1 bls12381.G1Affine
negG1.Neg(&g1Gen)
valid, err := bls12381.PairingCheck(
[]bls12381.G1Affine{aggPubKey, negG1},
[]bls12381.G2Affine{msgPoint, aggSig},
)
if err != nil {
return nil, err
}
result := make([]byte, 32)
if valid {
@@ -203,34 +291,72 @@ func (b *BLSThresholdVerify) RequiredGas(input []byte) uint64 {
}
func (b *BLSThresholdVerify) Run(input []byte) ([]byte, error) {
// Input: [1 byte threshold][1 byte num_shares][shares][message]
if len(input) < 2 {
// Input: [1 byte threshold][1 byte num_shares][48 bytes group_pubkey][shares with indices][message]
if len(input) < 2+g1Size+g2Size {
return nil, errors.New("input too short")
}
threshold := input[0]
numShares := input[1]
threshold := int(input[0])
numShares := int(input[1])
if numShares < threshold {
return nil, errors.New("insufficient shares for threshold")
}
// Parse signature shares
offset := 2
shares := make([][]byte, numShares)
for i := 0; i < int(numShares); i++ {
if offset+96 > len(input) {
// Parse group public key
var groupPubKey bls12381.G1Affine
if _, err := groupPubKey.SetBytes(input[2 : 2+g1Size]); err != nil {
return nil, errors.New("invalid group public key")
}
// Parse signature shares and their indices
offset := 2 + g1Size
shares := make([]bls12381.G2Affine, numShares)
indices := make([]uint64, numShares)
for i := 0; i < numShares; i++ {
if offset+8+g2Size > len(input) {
return nil, errors.New("invalid share")
}
shares[i] = input[offset : offset+96]
offset += 96
// Read index (8 bytes)
indices[i] = binary.BigEndian.Uint64(input[offset : offset+8])
offset += 8
// Read share (G2 point)
if _, err := shares[i].SetBytes(input[offset : offset+g2Size]); err != nil {
return nil, errors.New("invalid signature share")
}
offset += g2Size
}
// Parse message
message := input[offset:]
if len(message) == 0 {
return nil, errors.New("empty message")
}
// Verify threshold signature (placeholder)
valid := len(shares) >= int(threshold) && len(message) > 0
// Combine shares using Lagrange interpolation
combinedSig := combineThresholdShares(shares, indices, threshold)
// Hash message to G2
msgPoint, err := bls12381.HashToG2(message, nil)
if err != nil {
return nil, errors.New("hash to curve failed")
}
// Verify combined signature
_, _, g1Gen, _ := bls12381.Generators()
var negG1 bls12381.G1Affine
negG1.Neg(&g1Gen)
valid, err := bls12381.PairingCheck(
[]bls12381.G1Affine{groupPubKey, negG1},
[]bls12381.G2Affine{msgPoint, combinedSig},
)
if err != nil {
return nil, err
}
result := make([]byte, 32)
if valid {
@@ -240,6 +366,108 @@ func (b *BLSThresholdVerify) Run(input []byte) ([]byte, error) {
return result, nil
}
// combineThresholdShares combines threshold signature shares using Lagrange interpolation
func combineThresholdShares(shares []bls12381.G2Affine, indices []uint64, threshold int) bls12381.G2Affine {
var combined bls12381.G2Affine
for i := 0; i < threshold; i++ {
// Compute Lagrange coefficient
lambda := computeLagrangeCoeff(indices, i, threshold)
// Multiply share by lambda
var scaledShare bls12381.G2Affine
scaledShare.ScalarMultiplication(&shares[i], lambda.BigInt(new(big.Int)))
if i == 0 {
combined = scaledShare
} else {
combined.Add(&combined, &scaledShare)
}
}
return combined
}
// computeLagrangeCoeff computes Lagrange coefficient for party at position i
func computeLagrangeCoeff(indices []uint64, i, t int) fr.Element {
var lambda fr.Element
lambda.SetOne()
for j := 0; j < t; j++ {
if i == j {
continue
}
// lambda *= (0 - x_j) / (x_i - x_j)
// Since we're evaluating at 0: lambda *= -x_j / (x_i - x_j)
var xi, xj, num, denom fr.Element
xi.SetUint64(indices[i] + 1) // 1-indexed for proper interpolation
xj.SetUint64(indices[j] + 1)
num.Neg(&xj) // -x_j
denom.Sub(&xi, &xj) // x_i - x_j
denom.Inverse(&denom)
num.Mul(&num, &denom)
lambda.Mul(&lambda, &num)
}
return lambda
}
// BLSThresholdCombine combines threshold signature shares
type BLSThresholdCombine struct{}
func (b *BLSThresholdCombine) RequiredGas(input []byte) uint64 {
if len(input) < 1 {
return blsThresholdCombineGas
}
numShares := uint64(input[0])
return blsThresholdCombineGas + numShares*blsPerSignatureGas
}
func (b *BLSThresholdCombine) Run(input []byte) ([]byte, error) {
// Input: [1 byte num_shares][shares with indices...]
if len(input) < 2 {
return nil, errors.New("input too short")
}
numShares := int(input[0])
threshold := int(input[1])
if numShares < threshold {
return nil, errors.New("insufficient shares")
}
// Parse shares and indices
offset := 2
shares := make([]bls12381.G2Affine, numShares)
indices := make([]uint64, numShares)
for i := 0; i < numShares; i++ {
if offset+8+g2Size > len(input) {
return nil, errors.New("invalid share encoding")
}
// Read index
indices[i] = binary.BigEndian.Uint64(input[offset : offset+8])
offset += 8
// Read share
if _, err := shares[i].SetBytes(input[offset : offset+g2Size]); err != nil {
return nil, errors.New("invalid signature share point")
}
offset += g2Size
}
// Combine using Lagrange interpolation
combined := combineThresholdShares(shares, indices, threshold)
// Return combined signature (96 bytes)
bytes := combined.Bytes()
return bytes[:], nil
}
// BLSPublicKeyAggregate aggregates multiple BLS public keys
type BLSPublicKeyAggregate struct{}
@@ -257,27 +485,36 @@ func (b *BLSPublicKeyAggregate) Run(input []byte) ([]byte, error) {
return nil, errors.New("input too short")
}
numKeys := input[0]
expectedSize := 1 + int(numKeys)*48
numKeys := int(input[0])
expectedSize := 1 + numKeys*g1Size
if len(input) != expectedSize {
if len(input) < expectedSize {
return nil, errors.New("invalid input size")
}
// Aggregate public keys (placeholder)
// Real implementation would add G1 points
aggregatedKey := make([]byte, 48)
if numKeys == 0 {
return nil, errors.New("no public keys")
}
// Simple XOR for placeholder (not cryptographically correct!)
for i := 0; i < int(numKeys); i++ {
offset := 1 + i*48
pubKey := input[offset : offset+48]
for j := 0; j < 48; j++ {
aggregatedKey[j] ^= pubKey[j]
// Parse and aggregate public keys using G1 point addition
var aggregatedKey bls12381.G1Affine
for i := 0; i < numKeys; i++ {
offset := 1 + i*g1Size
var pk bls12381.G1Affine
if _, err := pk.SetBytes(input[offset : offset+g1Size]); err != nil {
return nil, errors.New("invalid public key point")
}
if i == 0 {
aggregatedKey = pk
} else {
aggregatedKey.Add(&aggregatedKey, &pk)
}
}
return aggregatedKey, nil
bytes := aggregatedKey.Bytes()
return bytes[:], nil
}
// BLSHashToPoint hashes message to BLS12-381 G2 point
@@ -293,16 +530,14 @@ func (b *BLSHashToPoint) Run(input []byte) ([]byte, error) {
return nil, errors.New("empty input")
}
// Hash to G2 point (placeholder)
// Real implementation would use proper hash-to-curve
g2Point := make([]byte, 96)
// Simple placeholder: use first 96 bytes of repeated hash
for i := 0; i < 96; i++ {
g2Point[i] = input[i%len(input)]
// Use gnark-crypto's hash-to-curve implementation
g2Point, err := bls12381.HashToG2(input, nil)
if err != nil {
return nil, errors.New("hash to curve failed")
}
return g2Point, nil
bytes := g2Point.Bytes()
return bytes[:], nil
}
// RegisterBLS registers all BLS precompiles
@@ -316,45 +551,6 @@ func RegisterBLS(registry *Registry) {
registry.Register(BLSHashToPointAddress, &BLSHashToPoint{})
}
// BLSThresholdCombine combines threshold signature shares
type BLSThresholdCombine struct{}
func (b *BLSThresholdCombine) RequiredGas(input []byte) uint64 {
if len(input) < 1 {
return blsThresholdCombineGas
}
numShares := uint64(input[0])
return blsThresholdCombineGas + numShares*blsPerSignatureGas
}
func (b *BLSThresholdCombine) Run(input []byte) ([]byte, error) {
// Input: [1 byte num_shares][shares...]
if len(input) < 1 {
return nil, errors.New("input too short")
}
numShares := input[0]
expectedSize := 1 + int(numShares)*96
if len(input) != expectedSize {
return nil, errors.New("invalid input size")
}
// Combine signature shares (placeholder)
combinedSig := make([]byte, 96)
// Simple XOR for placeholder (not cryptographically correct!)
for i := 0; i < int(numShares); i++ {
offset := 1 + i*96
share := input[offset : offset+96]
for j := 0; j < 96; j++ {
combinedSig[j] ^= share[j]
}
}
return combinedSig, nil
}
func init() {
// Auto-register BLS precompiles on package load
RegisterBLS(PostQuantumRegistry)
+101 -24
View File
@@ -8,8 +8,11 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"testing"
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
"github.com/luxfi/crypto/lamport"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -446,48 +449,111 @@ func TestBLSPrecompiles(t *testing.T) {
t.Run("BLSVerify", func(t *testing.T) {
verifier := &BLSVerify{}
// Create dummy input (placeholder implementation)
// Format: [96 bytes sig][48 bytes pubkey][message]
message := []byte("test message for BLS")
// Generate valid BLS key pair and signature
var scalar fr.Element
scalar.SetRandom()
// Generate public key: pk = g1 * scalar
_, _, g1Gen, _ := bls12381.Generators()
var pk bls12381.G1Affine
pk.ScalarMultiplication(&g1Gen, scalar.BigInt(new(big.Int)))
// The precompile uses raw message with nil DST
message := []byte("test message for BLS verification")
msgPoint, err := bls12381.HashToG2(message, nil) // nil DST to match precompile
require.NoError(t, err)
// Sign: sig = H(m) * scalar
var sig bls12381.G2Affine
sig.ScalarMultiplication(&msgPoint, scalar.BigInt(new(big.Int)))
// Build input: [96 bytes sig (G2 compressed)][48 bytes pubkey (G1 compressed)][message]
sigBytes := sig.Bytes()
pkBytes := pk.Bytes()
input := make([]byte, 96+48+len(message))
rand.Read(input[:96]) // Random signature
rand.Read(input[96:144]) // Random pubkey
copy(input[:96], sigBytes[:])
copy(input[96:144], pkBytes[:])
copy(input[144:], message)
// Test gas
gas := verifier.RequiredGas(input)
assert.Equal(t, uint64(150000), gas)
// Test run (placeholder always returns success)
// Test run - should verify successfully
output, err := verifier.Run(input)
require.NoError(t, err)
assert.Len(t, output, 32)
// Check that output indicates success (all zeros except last byte = 1)
expected := make([]byte, 32)
expected[31] = 1
assert.Equal(t, expected, output)
})
t.Run("BLSAggregateVerify", func(t *testing.T) {
aggVerifier := &BLSAggregateVerify{}
// Build aggregate input
// Format: [num_sigs(1)][signatures][pubkeys][encoded_messages]
numSigs := 3
sigSize := 96
pubSize := 48
sigSize := 96 // G2 compressed
pubSize := 48 // G1 compressed
msgSize := 32
totalSize := 1 + numSigs*(sigSize+pubSize+4+msgSize)
// Generate valid BLS signatures
var pubKeys []bls12381.G1Affine
var sigs []bls12381.G2Affine
var messages [][]byte
_, _, g1Gen, _ := bls12381.Generators()
for i := 0; i < numSigs; i++ {
var scalar fr.Element
scalar.SetRandom()
// Generate public key
var pk bls12381.G1Affine
pk.ScalarMultiplication(&g1Gen, scalar.BigInt(new(big.Int)))
pubKeys = append(pubKeys, pk)
// Create unique message
msg := make([]byte, 32)
msg[0] = byte(i)
rand.Read(msg[1:])
messages = append(messages, msg)
// Sign message with nil DST to match precompile
msgPoint, _ := bls12381.HashToG2(msg, nil)
var sig bls12381.G2Affine
sig.ScalarMultiplication(&msgPoint, scalar.BigInt(new(big.Int)))
sigs = append(sigs, sig)
}
// Aggregate signatures: aggSig = sig[0] + sig[1] + ... + sig[n-1]
var aggSig bls12381.G2Jac
for i, sig := range sigs {
if i == 0 {
aggSig.FromAffine(&sig)
} else {
var sigJac bls12381.G2Jac
sigJac.FromAffine(&sig)
aggSig.AddAssign(&sigJac)
}
}
var aggSigAffine bls12381.G2Affine
aggSigAffine.FromJacobian(&aggSig)
// Build input: [num_sigs (1)][aggregate_sig (96)][public_keys (n*48)][messages with length prefix]
totalSize := 1 + sigSize + numSigs*pubSize + numSigs*(4+msgSize)
input := make([]byte, totalSize)
input[0] = byte(numSigs)
offset := 1
// Add signatures
for i := 0; i < numSigs; i++ {
rand.Read(input[offset : offset+sigSize])
offset += sigSize
}
// Add aggregate signature
aggSigBytes := aggSigAffine.Bytes()
copy(input[1:1+sigSize], aggSigBytes[:])
offset := 1 + sigSize
// Add public keys
for i := 0; i < numSigs; i++ {
rand.Read(input[offset : offset+pubSize])
pkBytes := pubKeys[i].Bytes()
copy(input[offset:offset+pubSize], pkBytes[:])
offset += pubSize
}
@@ -495,17 +561,16 @@ func TestBLSPrecompiles(t *testing.T) {
for i := 0; i < numSigs; i++ {
binary.BigEndian.PutUint32(input[offset:offset+4], uint32(msgSize))
offset += 4
rand.Read(input[offset : offset+msgSize])
copy(input[offset:offset+msgSize], messages[i])
offset += msgSize
}
// Test gas
gas := aggVerifier.RequiredGas(input)
// blsAggregateVerifyGas = 200000, blsPerSignatureGas = 30000
expectedGas := uint64(200000 + 30000*uint64(numSigs))
assert.Equal(t, expectedGas, gas)
// Test run
// Test run - should verify successfully
output, err := aggVerifier.Run(input)
require.NoError(t, err)
assert.Len(t, output, 32)
@@ -514,16 +579,28 @@ func TestBLSPrecompiles(t *testing.T) {
t.Run("BLSPublicKeyAggregate", func(t *testing.T) {
aggregator := &BLSPublicKeyAggregate{}
// Build input
// Build input with valid BLS public keys
// Format: [num_keys(1)][pubkeys...]
numKeys := 5
pubSize := 48
pubSize := 48 // G1 compressed size
input := make([]byte, 1+numKeys*pubSize)
input[0] = byte(numKeys)
// Generate valid BLS public keys
for i := 0; i < numKeys; i++ {
rand.Read(input[1+i*pubSize : 1+(i+1)*pubSize])
// Generate random scalar
var scalar fr.Element
scalar.SetRandom()
// Compute public key: pk = g1 * scalar
_, _, g1Gen, _ := bls12381.Generators()
var pk bls12381.G1Affine
pk.ScalarMultiplication(&g1Gen, scalar.BigInt(new(big.Int)))
// Serialize to compressed format
pkBytes := pk.Bytes()
copy(input[1+i*pubSize:1+(i+1)*pubSize], pkBytes[:])
}
// Test gas