mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
chore: sync with node requirements
This commit is contained in:
@@ -1489,7 +1489,7 @@ The `gpu/` package provides GPU-accelerated ZK cryptographic operations with aut
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ github.com/luxfi/gpu │
|
||||
│ Go bindings to luxcpp/gpu (MLX) │
|
||||
│ Go bindings to luxfi/accel │
|
||||
│ │
|
||||
│ zk.go (non-CGO stub) │
|
||||
│ zk_cgo.go (CGO bindings) │
|
||||
@@ -1497,16 +1497,16 @@ The `gpu/` package provides GPU-accelerated ZK cryptographic operations with aut
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ luxcpp/gpu (MLX) │
|
||||
│ luxfi/accel │
|
||||
│ Unified Metal/CUDA/CPU backend │
|
||||
│ │
|
||||
│ mlx/zk/zk.cpp - C++ impl │
|
||||
│ mlx/zk/zk_c_api.h - C API │
|
||||
│ zk/zk.cpp - C++ impl │
|
||||
│ zk/zk_c_api.h - C API │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Change (2026-01-03)**: Removed separate platform files (`zk_metal.go`, `zk_cuda.go`)
|
||||
in favor of the unified `github.com/luxfi/gpu` package which handles all backends via MLX.
|
||||
in favor of the unified `github.com/luxfi/gpu` package which handles all backends via luxfi/accel.
|
||||
|
||||
### Threshold Constants (Tuned for Apple Silicon)
|
||||
|
||||
@@ -1601,19 +1601,19 @@ CGO_ENABLED=1 go build -tags "gpu" ./...
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `github.com/luxfi/gpu` - Unified GPU bindings (wraps luxcpp/gpu)
|
||||
- `github.com/luxfi/gpu` - Unified GPU bindings (wraps luxfi/accel)
|
||||
- `github.com/consensys/gnark-crypto` - CPU Poseidon2 via BN254/Fr
|
||||
|
||||
### GPU Stack (luxcpp/gpu)
|
||||
### GPU Stack (luxfi/accel)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lux/gpu/zk.go` | Non-CGO stub (returns ErrZKNotAvailable) |
|
||||
| `lux/gpu/zk_cgo.go` | CGO bindings to luxcpp/gpu |
|
||||
| `luxcpp/gpu/mlx/zk/zk.h` | C++ ZK operations header |
|
||||
| `luxcpp/gpu/mlx/zk/zk.cpp` | C++ ZK implementation using MLX |
|
||||
| `luxcpp/gpu/mlx/zk/zk_c_api.h` | C API for Go bindings |
|
||||
| `luxcpp/gpu/mlx/zk/zk_c_api.cpp` | C API implementation |
|
||||
| `lux/gpu/zk_cgo.go` | CGO bindings to luxfi/accel |
|
||||
| `luxfi/accel/zk/zk.h` | C++ ZK operations header |
|
||||
| `luxfi/accel/zk/zk.cpp` | C++ ZK implementation |
|
||||
| `luxfi/accel/zk/zk_c_api.h` | C API for Go bindings |
|
||||
| `luxfi/accel/zk/zk_c_api.cpp` | C API implementation |
|
||||
|
||||
### Test Coverage
|
||||
|
||||
@@ -1631,7 +1631,7 @@ CGO_ENABLED=1 go test ./gpu/...
|
||||
|
||||
- `github.com/luxfi/crypto` - Go package with GPU ZK operations
|
||||
- `github.com/luxfi/gpu` - Go bindings to unified GPU (Metal/CUDA/CPU)
|
||||
- `luxcpp/gpu` - C++ MLX-based GPU library
|
||||
- `luxfi/accel` - C++ GPU acceleration library (Metal/CUDA/CPU)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/cggmp21"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// BLSManager manages BLS signature operations
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// ThresholdPublicKey is a placeholder for threshold public keys.
|
||||
|
||||
-330
@@ -1,330 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated BLS12-381 operations via Metal.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// The C++ library handles automatic fallback to CPU when GPU is unavailable.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../../../luxcpp/crypto/include
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_bls.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
)
|
||||
|
||||
// Thresholds for GPU batch operations
|
||||
const (
|
||||
BatchVerifyThreshold = 64 // Min signatures for GPU batch verify
|
||||
BatchAggregateThreshold = 128 // Min items for GPU aggregation
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *C.MetalBLSContext
|
||||
ctxReady bool
|
||||
)
|
||||
|
||||
// initContext lazily initializes the Metal BLS context.
|
||||
func initContext() error {
|
||||
if ctxReady {
|
||||
return nil
|
||||
}
|
||||
ctx = C.metal_bls_init()
|
||||
if ctx == nil {
|
||||
return errors.New("Metal BLS initialization failed")
|
||||
}
|
||||
ctxReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available for BLS.
|
||||
func Available() bool {
|
||||
return bool(C.metal_bls_available())
|
||||
}
|
||||
|
||||
// BatchVerify verifies multiple BLS signatures using GPU acceleration.
|
||||
// Returns a slice of booleans indicating validity of each signature.
|
||||
// Falls back to sequential verification if GPU is unavailable or count < threshold.
|
||||
func BatchVerify(pks []*bls.PublicKey, sigs []*bls.Signature, msgs [][]byte) ([]bool, error) {
|
||||
n := len(pks)
|
||||
if n == 0 || n != len(sigs) || n != len(msgs) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
results := make([]bool, n)
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchVerifyThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = bls.Verify(pks[i], sigs[i], msgs[i])
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential on init error
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = bls.Verify(pks[i], sigs[i], msgs[i])
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
intResults := make([]C.int, n)
|
||||
|
||||
// Serialize keys and signatures
|
||||
sigBytes := make([][]byte, n)
|
||||
pkBytes := make([][]byte, n)
|
||||
msgCopies := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil || sigs[i] == nil {
|
||||
return nil, errors.New("nil public key or signature")
|
||||
}
|
||||
sigBytes[i] = bls.SignatureToBytes(sigs[i])
|
||||
pkBytes[i] = bls.PublicKeyToCompressedBytes(pks[i])
|
||||
msgCopies[i] = msgs[i] // Keep reference
|
||||
|
||||
if len(sigBytes[i]) == 0 || len(pkBytes[i]) == 0 {
|
||||
return nil, errors.New("failed to serialize key or signature")
|
||||
}
|
||||
|
||||
sigPtrs[i] = (*C.uint8_t)(&sigBytes[i][0])
|
||||
pkPtrs[i] = (*C.uint8_t)(&pkBytes[i][0])
|
||||
if len(msgCopies[i]) > 0 {
|
||||
msgPtrs[i] = (*C.uint8_t)(&msgCopies[i][0])
|
||||
}
|
||||
}
|
||||
|
||||
ret := C.metal_bls_batch_verify(
|
||||
ctx,
|
||||
(**C.uint8_t)(unsafe.Pointer(&sigPtrs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&pkPtrs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&msgPtrs[0])),
|
||||
C.uint32_t(n),
|
||||
(*C.int)(unsafe.Pointer(&intResults[0])),
|
||||
)
|
||||
|
||||
if ret < 0 {
|
||||
// GPU error, fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = bls.Verify(pks[i], sigs[i], msgs[i])
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Convert results
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = intResults[i] != 0
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// AggregateSignatures aggregates signatures using GPU acceleration.
|
||||
// Falls back to CPU aggregation if GPU is unavailable or count < threshold.
|
||||
func AggregateSignatures(sigs []*bls.Signature) (*bls.Signature, error) {
|
||||
n := len(sigs)
|
||||
if n == 0 {
|
||||
return nil, bls.ErrNoSignatures
|
||||
}
|
||||
|
||||
// Fall back to CPU for small batches
|
||||
if n < BatchAggregateThreshold || !Available() {
|
||||
return bls.AggregateSignatures(sigs)
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
return bls.AggregateSignatures(sigs)
|
||||
}
|
||||
|
||||
// Prepare signature array
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
sigBytes := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if sigs[i] == nil {
|
||||
return nil, bls.ErrFailedSignatureAggregation
|
||||
}
|
||||
sigBytes[i] = bls.SignatureToBytes(sigs[i])
|
||||
if len(sigBytes[i]) == 0 {
|
||||
return nil, bls.ErrFailedSignatureAggregation
|
||||
}
|
||||
sigPtrs[i] = (*C.uint8_t)(&sigBytes[i][0])
|
||||
}
|
||||
|
||||
// Allocate output
|
||||
aggSig := make([]byte, bls.SignatureLen)
|
||||
|
||||
ret := C.metal_bls_aggregate_sigs(
|
||||
ctx,
|
||||
(*C.uint8_t)(&aggSig[0]),
|
||||
(**C.uint8_t)(unsafe.Pointer(&sigPtrs[0])),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_BLS_SUCCESS {
|
||||
// Fall back to CPU
|
||||
return bls.AggregateSignatures(sigs)
|
||||
}
|
||||
|
||||
return bls.SignatureFromBytes(aggSig)
|
||||
}
|
||||
|
||||
// AggregatePublicKeys aggregates public keys using GPU acceleration.
|
||||
// Falls back to CPU aggregation if GPU is unavailable or count < threshold.
|
||||
func AggregatePublicKeys(pks []*bls.PublicKey) (*bls.PublicKey, error) {
|
||||
n := len(pks)
|
||||
if n == 0 {
|
||||
return nil, bls.ErrNoPublicKeys
|
||||
}
|
||||
|
||||
// Fall back to CPU for small batches
|
||||
if n < BatchAggregateThreshold || !Available() {
|
||||
return bls.AggregatePublicKeys(pks)
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
return bls.AggregatePublicKeys(pks)
|
||||
}
|
||||
|
||||
// Prepare public key array
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
pkBytes := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
return nil, bls.ErrInvalidPublicKey
|
||||
}
|
||||
pkBytes[i] = bls.PublicKeyToCompressedBytes(pks[i])
|
||||
if len(pkBytes[i]) == 0 {
|
||||
return nil, bls.ErrFailedPublicKeyAggregation
|
||||
}
|
||||
pkPtrs[i] = (*C.uint8_t)(&pkBytes[i][0])
|
||||
}
|
||||
|
||||
// Allocate output
|
||||
aggPk := make([]byte, bls.PublicKeyLen)
|
||||
|
||||
ret := C.metal_bls_aggregate_pks(
|
||||
ctx,
|
||||
(*C.uint8_t)(&aggPk[0]),
|
||||
(**C.uint8_t)(unsafe.Pointer(&pkPtrs[0])),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_BLS_SUCCESS {
|
||||
// Fall back to CPU
|
||||
return bls.AggregatePublicKeys(pks)
|
||||
}
|
||||
|
||||
return bls.PublicKeyFromCompressedBytes(aggPk)
|
||||
}
|
||||
|
||||
// MultiScalarMul performs multi-scalar multiplication on GPU.
|
||||
// result = sum_i (scalars[i] * points[i])
|
||||
// This is useful for batch verification and commitment schemes.
|
||||
func MultiScalarMul(points []*bls.PublicKey, scalars [][]byte) (*bls.PublicKey, error) {
|
||||
n := len(points)
|
||||
if n == 0 || n != len(scalars) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
// Validate scalar lengths (256-bit = 32 bytes)
|
||||
for i := 0; i < n; i++ {
|
||||
if len(scalars[i]) != 32 {
|
||||
return nil, errors.New("scalars must be 32 bytes")
|
||||
}
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Serialize points to G1Affine format
|
||||
type g1Affine struct {
|
||||
x [48]byte
|
||||
y [48]byte
|
||||
_pad [8]byte
|
||||
}
|
||||
pointsArr := make([]g1Affine, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if points[i] == nil {
|
||||
return nil, errors.New("nil point")
|
||||
}
|
||||
pkBytes := bls.PublicKeyToUncompressedBytes(points[i])
|
||||
if len(pkBytes) < 96 {
|
||||
return nil, errors.New("invalid point serialization")
|
||||
}
|
||||
copy(pointsArr[i].x[:], pkBytes[:48])
|
||||
copy(pointsArr[i].y[:], pkBytes[48:96])
|
||||
}
|
||||
|
||||
// Flatten scalars to limbs (4 x 64-bit per scalar)
|
||||
scalarLimbs := make([]uint64, n*4)
|
||||
for i := 0; i < n; i++ {
|
||||
for j := 0; j < 4; j++ {
|
||||
scalarLimbs[i*4+j] = 0
|
||||
for k := 0; k < 8; k++ {
|
||||
scalarLimbs[i*4+j] |= uint64(scalars[i][j*8+k]) << (k * 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate result
|
||||
type g1Proj struct {
|
||||
x [48]byte
|
||||
y [48]byte
|
||||
z [48]byte
|
||||
}
|
||||
var result g1Proj
|
||||
|
||||
ret := C.metal_bls_msm(
|
||||
ctx,
|
||||
(*C.G1Projective)(unsafe.Pointer(&result)),
|
||||
(*C.G1Affine)(unsafe.Pointer(&pointsArr[0])),
|
||||
(*C.uint64_t)(unsafe.Pointer(&scalarLimbs[0])),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_BLS_SUCCESS {
|
||||
return nil, errors.New("MSM failed")
|
||||
}
|
||||
|
||||
// Convert projective result back to compressed public key
|
||||
resultBytes := make([]byte, 144)
|
||||
copy(resultBytes[:48], result.x[:])
|
||||
copy(resultBytes[48:96], result.y[:])
|
||||
copy(resultBytes[96:], result.z[:])
|
||||
|
||||
// Convert to affine and compress (simplified - in practice need proper conversion)
|
||||
// For now, return nil as this requires more complex coordinate conversion
|
||||
return nil, errors.New("MSM result conversion not yet implemented")
|
||||
}
|
||||
|
||||
// Destroy releases the Metal BLS context.
|
||||
// Should be called when done with GPU BLS operations.
|
||||
func Destroy() {
|
||||
if ctxReady && ctx != nil {
|
||||
C.metal_bls_destroy(ctx)
|
||||
ctx = nil
|
||||
ctxReady = false
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated BLS12-381 operations.
|
||||
// This is the stub implementation when GPU acceleration is not available.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"github.com/luxfi/crypto/bls"
|
||||
)
|
||||
|
||||
// Thresholds for GPU batch operations
|
||||
const (
|
||||
BatchVerifyThreshold = 64 // Min signatures for GPU batch verify
|
||||
BatchAggregateThreshold = 128 // Min items for GPU aggregation
|
||||
)
|
||||
|
||||
// Available returns true if GPU acceleration is available for BLS.
|
||||
func Available() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// BatchVerify verifies multiple BLS signatures.
|
||||
// Falls back to sequential verification when GPU is unavailable.
|
||||
func BatchVerify(pks []*bls.PublicKey, sigs []*bls.Signature, msgs [][]byte) ([]bool, error) {
|
||||
n := len(pks)
|
||||
results := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = bls.Verify(pks[i], sigs[i], msgs[i])
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// AggregateSignatures aggregates multiple BLS signatures into one.
|
||||
func AggregateSignatures(sigs []*bls.Signature) (*bls.Signature, error) {
|
||||
return bls.AggregateSignatures(sigs)
|
||||
}
|
||||
|
||||
// AggregatePublicKeys aggregates multiple BLS public keys into one.
|
||||
func AggregatePublicKeys(pks []*bls.PublicKey) (*bls.PublicKey, error) {
|
||||
return bls.AggregatePublicKeys(pks)
|
||||
}
|
||||
|
||||
// VerifyAggregate verifies an aggregated signature against multiple messages and keys.
|
||||
func VerifyAggregate(pks []*bls.PublicKey, sig *bls.Signature, msgs [][]byte) bool {
|
||||
// Simple fallback - for production, use proper aggregate verification
|
||||
for i := range pks {
|
||||
if !bls.Verify(pks[i], sig, msgs[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// Signature represents an ECDSA signature
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
// Centralized cgo wrapper to avoid duplicate linking
|
||||
// All other packages should import this instead of having their own cgo directives
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/luxfi/crypto
|
||||
|
||||
go 1.22
|
||||
go 1.25.5
|
||||
|
||||
exclude github.com/luxfi/geth v1.16.1
|
||||
|
||||
@@ -20,9 +20,8 @@ require (
|
||||
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7
|
||||
github.com/leanovate/gopter v0.2.11
|
||||
github.com/luxfi/cache v1.1.0
|
||||
github.com/luxfi/gpu v0.30.0
|
||||
github.com/luxfi/ids v1.2.9
|
||||
github.com/luxfi/log v1.2.1
|
||||
github.com/luxfi/log v1.3.0
|
||||
github.com/luxfi/mock v0.1.0
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -38,14 +37,13 @@ require (
|
||||
require (
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/luxfi/utils v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/zeebo/assert v1.3.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -219,8 +219,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@@ -251,19 +249,18 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/luxfi/cache v1.1.0 h1:6LUyGGZ+rrMAJBbAU6+UwkcamXj3zsboRUodIof2Ong=
|
||||
github.com/luxfi/cache v1.1.0/go.mod h1:9GvlEEE9rFPaaWxvVpSPwW8ZMo2+8VMNNcuPa4AwzPg=
|
||||
github.com/luxfi/gpu v0.30.0 h1:k+7EDp/WHTa0176zXet1UXTl8IcZcp1ZkS8GUJ5l2iU=
|
||||
github.com/luxfi/gpu v0.30.0/go.mod h1:7pFsHqra1Vrmy2aGXVEPKeKsPR+Bn+QmBXuByK3fdPA=
|
||||
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
|
||||
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
|
||||
github.com/luxfi/log v1.2.1 h1:L2JM8MjGPF8rBnY+EXODS9x5OQTGSgeQ1SPFmeB+oPY=
|
||||
github.com/luxfi/log v1.2.1/go.mod h1:0vJzb4Hl9fvgwWDMKcbCD/SYH3bO0iSmgZMcYdfdl1o=
|
||||
github.com/luxfi/logger v1.3.0 h1:Bo0jw40+QTqSBttGCkWe94tVz405Oo5w8qPgCo6Qp1g=
|
||||
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
|
||||
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
|
||||
github.com/luxfi/utils v1.1.0 h1:ti7HvjNwJd4ILDMERJtOAWE9mF8l+zqDVkgWnF7Agic=
|
||||
github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -393,8 +390,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -733,8 +728,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//go:build cgo && gpu
|
||||
// +build cgo,gpu
|
||||
|
||||
// Consolidated cgo wrapper to avoid duplicate linking
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo pkg-config: lux-all
|
||||
#include <lux/crypto/crypto.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// This file consolidates all cgo dependencies to avoid duplicate library warnings
|
||||
// Individual packages should import this instead of having their own cgo directives
|
||||
@@ -1,24 +0,0 @@
|
||||
// Package gpu provides GPU-accelerated cryptographic operations.
|
||||
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 = 96 // G1 uncompressed
|
||||
BLSSignatureSize = 192 // G2 uncompressed
|
||||
BLSMessageSize = 32
|
||||
|
||||
MLDSASecretKeySize = 4032
|
||||
MLDSAPublicKeySize = 1952
|
||||
MLDSASignatureSize = 3309
|
||||
)
|
||||
|
||||
// Hash types for batch hashing
|
||||
const (
|
||||
HashTypeSHA3_256 = 0
|
||||
HashTypeSHA3_512 = 1
|
||||
HashTypeBLAKE3 = 2
|
||||
)
|
||||
-497
@@ -1,497 +0,0 @@
|
||||
//go:build !cgo || !gpu
|
||||
|
||||
// Package gpu provides GPU-accelerated cryptographic operations.
|
||||
// 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"
|
||||
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
|
||||
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
|
||||
"github.com/zeebo/blake3"
|
||||
"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)" }
|
||||
func ClearCache() {}
|
||||
|
||||
// =============================================================================
|
||||
// BLS12-381 Pure Go Implementation (using gnark-crypto)
|
||||
// =============================================================================
|
||||
|
||||
var blsDST = []byte("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_")
|
||||
|
||||
func BLSKeygen(seed []byte) ([]byte, error) {
|
||||
var scalar fr.Element
|
||||
if seed != nil && len(seed) >= 32 {
|
||||
scalar.SetBytes(seed[:32])
|
||||
} else {
|
||||
_, err := scalar.SetRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return scalar.Marshal(), nil
|
||||
}
|
||||
|
||||
func BLSSecretKeyToPublicKey(sk []byte) ([]byte, error) {
|
||||
if len(sk) != BLSSecretKeySize {
|
||||
return nil, ErrInvalidKey
|
||||
}
|
||||
|
||||
var scalar fr.Element
|
||||
if err := scalar.SetBytesCanonical(sk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, g1Gen, _ := bls12381.Generators()
|
||||
var pk bls12381.G1Affine
|
||||
pk.ScalarMultiplication(&g1Gen, scalar.BigInt(new(big.Int)))
|
||||
|
||||
return pk.Marshal(), nil
|
||||
}
|
||||
|
||||
func BLSSign(sk, msg []byte) ([]byte, error) {
|
||||
if len(sk) != BLSSecretKeySize || len(msg) != BLSMessageSize {
|
||||
return nil, ErrInvalidKey
|
||||
}
|
||||
|
||||
var scalar fr.Element
|
||||
if err := scalar.SetBytesCanonical(sk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Hash to G2
|
||||
msgPoint, err := bls12381.HashToG2(msg, blsDST)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Multiply by secret key
|
||||
var sig bls12381.G2Affine
|
||||
sig.ScalarMultiplication(&msgPoint, scalar.BigInt(new(big.Int)))
|
||||
|
||||
return sig.Marshal(), nil
|
||||
}
|
||||
|
||||
func BLSVerify(sig, pk, msg []byte) bool {
|
||||
if len(sig) != BLSSignatureSize || len(pk) != BLSPublicKeySize || len(msg) != BLSMessageSize {
|
||||
return false
|
||||
}
|
||||
|
||||
var sigPoint bls12381.G2Affine
|
||||
if err := sigPoint.Unmarshal(sig); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var pkPoint bls12381.G1Affine
|
||||
if err := pkPoint.Unmarshal(pk); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
msgPoint, err := bls12381.HashToG2(msg, blsDST)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, _, g1Gen, _ := bls12381.Generators()
|
||||
var negG1 bls12381.G1Affine
|
||||
negG1.Neg(&g1Gen)
|
||||
|
||||
result, err := bls12381.PairingCheck(
|
||||
[]bls12381.G1Affine{pkPoint, negG1},
|
||||
[]bls12381.G2Affine{msgPoint, sigPoint},
|
||||
)
|
||||
|
||||
return err == nil && result
|
||||
}
|
||||
|
||||
func BLSAggregateSignatures(sigs [][]byte) ([]byte, error) {
|
||||
if len(sigs) == 0 {
|
||||
return nil, errors.New("empty signatures")
|
||||
}
|
||||
|
||||
var result bls12381.G2Jac
|
||||
for i, sigBytes := range sigs {
|
||||
if len(sigBytes) != BLSSignatureSize {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
var sig bls12381.G2Affine
|
||||
if err := sig.Unmarshal(sigBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i == 0 {
|
||||
result.FromAffine(&sig)
|
||||
} else {
|
||||
var jac bls12381.G2Jac
|
||||
jac.FromAffine(&sig)
|
||||
result.AddAssign(&jac)
|
||||
}
|
||||
}
|
||||
|
||||
var resultAffine bls12381.G2Affine
|
||||
resultAffine.FromJacobian(&result)
|
||||
return resultAffine.Marshal(), nil
|
||||
}
|
||||
|
||||
func BLSAggregatePublicKeys(pks [][]byte) ([]byte, error) {
|
||||
if len(pks) == 0 {
|
||||
return nil, errors.New("empty public keys")
|
||||
}
|
||||
|
||||
var result bls12381.G1Jac
|
||||
for i, pkBytes := range pks {
|
||||
if len(pkBytes) != BLSPublicKeySize {
|
||||
return nil, ErrInvalidKey
|
||||
}
|
||||
var pk bls12381.G1Affine
|
||||
if err := pk.Unmarshal(pkBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i == 0 {
|
||||
result.FromAffine(&pk)
|
||||
} else {
|
||||
var jac bls12381.G1Jac
|
||||
jac.FromAffine(&pk)
|
||||
result.AddAssign(&jac)
|
||||
}
|
||||
}
|
||||
|
||||
var resultAffine bls12381.G1Affine
|
||||
resultAffine.FromJacobian(&result)
|
||||
return resultAffine.Marshal(), nil
|
||||
}
|
||||
|
||||
func BLSVerifyAggregated(aggSig, aggPK, msg []byte) bool {
|
||||
return BLSVerify(aggSig, aggPK, msg)
|
||||
}
|
||||
|
||||
func BLSBatchVerify(sigs, pks, msgs [][]byte) ([]bool, error) {
|
||||
if len(sigs) != len(pks) || len(pks) != len(msgs) {
|
||||
return nil, errors.New("mismatched lengths")
|
||||
}
|
||||
|
||||
results := make([]bool, len(sigs))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range sigs {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = BLSVerify(sigs[idx], pks[idx], msgs[idx])
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func BLSBatchSign(sks, msgs [][]byte) ([][]byte, error) {
|
||||
if len(sks) != len(msgs) {
|
||||
return nil, errors.New("mismatched lengths")
|
||||
}
|
||||
|
||||
results := make([][]byte, len(sks))
|
||||
errs := make([]error, len(sks))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range sks {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx], errs[idx] = BLSSign(sks[idx], msgs[idx])
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ML-DSA (Dilithium) Pure Go Implementation (using cloudflare/circl)
|
||||
// =============================================================================
|
||||
|
||||
func MLDSAKeygen(seed []byte) (pk, sk []byte, err error) {
|
||||
var pubKey *mldsa65.PublicKey
|
||||
var privKey *mldsa65.PrivateKey
|
||||
|
||||
if seed != nil && len(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
|
||||
pubKey, privKey, err = mldsa65.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var privKey mldsa65.PrivateKey
|
||||
if err := privKey.UnmarshalBinary(sk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SignTo writes to a provided buffer, returns signature
|
||||
sig := make([]byte, mldsa65.SignatureSize)
|
||||
mldsa65.SignTo(&privKey, msg, nil, false, sig)
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func MLDSAVerify(sig, msg, pk []byte) bool {
|
||||
var pubKey mldsa65.PublicKey
|
||||
if err := pubKey.UnmarshalBinary(pk); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return mldsa65.Verify(&pubKey, msg, nil, sig)
|
||||
}
|
||||
|
||||
func MLDSABatchVerify(sigs, msgs [][]byte, pks [][]byte) ([]bool, error) {
|
||||
if len(sigs) != len(msgs) || len(msgs) != len(pks) {
|
||||
return nil, errors.New("mismatched lengths")
|
||||
}
|
||||
|
||||
results := make([]bool, len(sigs))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range sigs {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = MLDSAVerify(sigs[idx], msgs[idx], pks[idx])
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Threshold Signatures Pure Go Implementation
|
||||
// =============================================================================
|
||||
|
||||
type ThresholdContext struct {
|
||||
t, n uint32
|
||||
}
|
||||
|
||||
func NewThresholdContext(t, n uint32) (*ThresholdContext, error) {
|
||||
if t > n || t == 0 {
|
||||
return nil, errors.New("invalid threshold parameters")
|
||||
}
|
||||
return &ThresholdContext{t: t, n: n}, nil
|
||||
}
|
||||
|
||||
func (tc *ThresholdContext) Close() {}
|
||||
|
||||
func (tc *ThresholdContext) Keygen(seed []byte) (shares [][]byte, pk []byte, err error) {
|
||||
// Simple Shamir Secret Sharing for BLS keys
|
||||
masterSK, err := BLSKeygen(seed)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pk, err = BLSSecretKeyToPublicKey(masterSK)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate polynomial coefficients
|
||||
var coeffs []fr.Element
|
||||
coeffs = make([]fr.Element, tc.t)
|
||||
coeffs[0].SetBytes(masterSK)
|
||||
|
||||
for i := uint32(1); i < tc.t; i++ {
|
||||
if _, err := coeffs[i].SetRandom(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate polynomial at points 1..n
|
||||
shares = make([][]byte, tc.n)
|
||||
for i := uint32(0); i < tc.n; i++ {
|
||||
var x, y fr.Element
|
||||
x.SetUint64(uint64(i + 1))
|
||||
|
||||
// Horner's method
|
||||
y.Set(&coeffs[tc.t-1])
|
||||
for j := int(tc.t) - 2; j >= 0; j-- {
|
||||
y.Mul(&y, &x)
|
||||
y.Add(&y, &coeffs[j])
|
||||
}
|
||||
|
||||
shares[i] = y.Marshal()
|
||||
}
|
||||
|
||||
return shares, pk, nil
|
||||
}
|
||||
|
||||
func (tc *ThresholdContext) PartialSign(shareIndex uint32, share, msg []byte) ([]byte, error) {
|
||||
return BLSSign(share, msg)
|
||||
}
|
||||
|
||||
func (tc *ThresholdContext) Combine(partialSigs [][]byte, indices []uint32) ([]byte, error) {
|
||||
if len(partialSigs) < int(tc.t) {
|
||||
return nil, errors.New("insufficient shares")
|
||||
}
|
||||
|
||||
// Lagrange interpolation coefficients
|
||||
lagrange := make([]fr.Element, len(indices))
|
||||
for i := range indices {
|
||||
var num, den fr.Element
|
||||
num.SetOne()
|
||||
den.SetOne()
|
||||
|
||||
var xi fr.Element
|
||||
xi.SetUint64(uint64(indices[i] + 1))
|
||||
|
||||
for j := range indices {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
var xj fr.Element
|
||||
xj.SetUint64(uint64(indices[j] + 1))
|
||||
|
||||
// num *= xj
|
||||
num.Mul(&num, &xj)
|
||||
|
||||
// den *= (xj - xi)
|
||||
var diff fr.Element
|
||||
diff.Sub(&xj, &xi)
|
||||
den.Mul(&den, &diff)
|
||||
}
|
||||
|
||||
lagrange[i].Div(&num, &den)
|
||||
}
|
||||
|
||||
// Combine signatures: sum of sig_i * lagrange_i
|
||||
var result bls12381.G2Jac
|
||||
|
||||
for i, sigBytes := range partialSigs {
|
||||
var sig bls12381.G2Affine
|
||||
if err := sig.Unmarshal(sigBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var scaled bls12381.G2Affine
|
||||
scaled.ScalarMultiplication(&sig, lagrange[i].BigInt(new(big.Int)))
|
||||
|
||||
if i == 0 {
|
||||
result.FromAffine(&scaled)
|
||||
} else {
|
||||
var jac bls12381.G2Jac
|
||||
jac.FromAffine(&scaled)
|
||||
result.AddAssign(&jac)
|
||||
}
|
||||
}
|
||||
|
||||
var resultAffine bls12381.G2Affine
|
||||
resultAffine.FromJacobian(&result)
|
||||
return resultAffine.Marshal(), nil
|
||||
}
|
||||
|
||||
func (tc *ThresholdContext) Verify(sig, pk, msg []byte) bool {
|
||||
return BLSVerify(sig, pk, msg)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hash Functions Pure Go Implementation
|
||||
// =============================================================================
|
||||
|
||||
func SHA3_256(data []byte) []byte {
|
||||
h := sha3.New256()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func SHA3_512(data []byte) []byte {
|
||||
h := sha3.New512()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func BLAKE3(data []byte) []byte {
|
||||
h := blake3.New()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func BatchHash(inputs [][]byte, hashType int) ([][]byte, error) {
|
||||
results := make([][]byte, len(inputs))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range inputs {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
switch hashType {
|
||||
case 0:
|
||||
results[idx] = SHA3_256(inputs[idx])
|
||||
case 1:
|
||||
results[idx] = SHA3_512(inputs[idx])
|
||||
case 2:
|
||||
results[idx] = BLAKE3(inputs[idx])
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Consensus Helpers
|
||||
// =============================================================================
|
||||
|
||||
func ConsensusVerifyBlock(blsSigs, blsPKs [][]byte, thresholdSig, thresholdPK, blockHash []byte) bool {
|
||||
// Verify all BLS signatures
|
||||
for i := range blsSigs {
|
||||
if !BLSVerify(blsSigs[i], blsPKs[i], blockHash) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Verify threshold signature
|
||||
if len(thresholdSig) > 0 && len(thresholdPK) > 0 {
|
||||
if !BLSVerify(thresholdSig, thresholdPK, blockHash) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
-1074
File diff suppressed because it is too large
Load Diff
@@ -1,829 +0,0 @@
|
||||
//go:build cgo && gpu
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGPUAvailable(t *testing.T) {
|
||||
available := GPUAvailable()
|
||||
backend := GetBackend()
|
||||
t.Logf("GPU Available: %v, Backend: %s", available, backend)
|
||||
}
|
||||
|
||||
func TestBLSKeygen(t *testing.T) {
|
||||
sk, err := BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen failed: %v", err)
|
||||
}
|
||||
if len(sk) != BLSSecretKeySize {
|
||||
t.Errorf("sk length = %d, want %d", len(sk), BLSSecretKeySize)
|
||||
}
|
||||
|
||||
// Derive public key
|
||||
pk, err := BLSSecretKeyToPublicKey(sk)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey failed: %v", err)
|
||||
}
|
||||
if len(pk) != BLSPublicKeySize {
|
||||
t.Errorf("pk length = %d, want %d", len(pk), BLSPublicKeySize)
|
||||
}
|
||||
|
||||
t.Logf("Generated BLS keypair: sk=%d bytes, pk=%d bytes", len(sk), len(pk))
|
||||
}
|
||||
|
||||
func TestBLSSignVerify(t *testing.T) {
|
||||
// Generate key
|
||||
sk, err := BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen failed: %v", err)
|
||||
}
|
||||
|
||||
pk, err := BLSSecretKeyToPublicKey(sk)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey failed: %v", err)
|
||||
}
|
||||
|
||||
// Sign message
|
||||
msg := make([]byte, BLSMessageSize)
|
||||
for i := range msg {
|
||||
msg[i] = byte(i)
|
||||
}
|
||||
|
||||
sig, err := BLSSign(sk, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSign failed: %v", err)
|
||||
}
|
||||
if len(sig) != BLSSignatureSize {
|
||||
t.Errorf("sig length = %d, want %d", len(sig), BLSSignatureSize)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid := BLSVerify(sig, pk, msg)
|
||||
if !valid {
|
||||
t.Error("BLSVerify returned false for valid signature")
|
||||
}
|
||||
|
||||
t.Log("BLS sign/verify successful")
|
||||
}
|
||||
|
||||
func TestBLSAggregate(t *testing.T) {
|
||||
n := 5
|
||||
|
||||
// Generate keys
|
||||
sks := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
var err error
|
||||
sks[i], err = BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen[%d] failed: %v", i, err)
|
||||
}
|
||||
pks[i], err = BLSSecretKeyToPublicKey(sks[i])
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey[%d] failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign same message
|
||||
msg := make([]byte, BLSMessageSize)
|
||||
for i := range msg {
|
||||
msg[i] = byte(i * 2)
|
||||
}
|
||||
|
||||
sigs := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
var err error
|
||||
sigs[i], err = BLSSign(sks[i], msg)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSign[%d] failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate signatures
|
||||
aggSig, err := BLSAggregateSignatures(sigs)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSAggregateSignatures failed: %v", err)
|
||||
}
|
||||
|
||||
// Aggregate public keys
|
||||
aggPK, err := BLSAggregatePublicKeys(pks)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSAggregatePublicKeys failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify aggregated
|
||||
valid := BLSVerifyAggregated(aggSig, aggPK, msg)
|
||||
if !valid {
|
||||
t.Error("BLSVerifyAggregated returned false for valid aggregated signature")
|
||||
}
|
||||
|
||||
t.Logf("BLS aggregate (%d signatures) successful", n)
|
||||
}
|
||||
|
||||
func TestBLSBatchVerify(t *testing.T) {
|
||||
n := 10
|
||||
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sk, err := BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen[%d] failed: %v", i, err)
|
||||
}
|
||||
pks[i], err = BLSSecretKeyToPublicKey(sk)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey[%d] failed: %v", i, err)
|
||||
}
|
||||
|
||||
msgs[i] = make([]byte, BLSMessageSize)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
|
||||
sigs[i], err = BLSSign(sk, msgs[i])
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSign[%d] failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := BLSBatchVerify(sigs, pks, msgs)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSBatchVerify failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) != n {
|
||||
t.Errorf("results length = %d, want %d", len(results), n)
|
||||
}
|
||||
|
||||
validCount := 0
|
||||
for i, r := range results {
|
||||
if r {
|
||||
validCount++
|
||||
} else {
|
||||
t.Errorf("signature %d should be valid", i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("BLS batch verify: %d/%d valid", validCount, n)
|
||||
}
|
||||
|
||||
func TestBLSBatchSign(t *testing.T) {
|
||||
n := 10
|
||||
|
||||
sks := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
// Generate keys and messages
|
||||
for i := 0; i < n; i++ {
|
||||
var err error
|
||||
sks[i], err = BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen[%d] failed: %v", i, err)
|
||||
}
|
||||
pks[i], err = BLSSecretKeyToPublicKey(sks[i])
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey[%d] failed: %v", i, err)
|
||||
}
|
||||
|
||||
msgs[i] = make([]byte, BLSMessageSize)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
}
|
||||
|
||||
// Batch sign
|
||||
sigs, err := BLSBatchSign(sks, msgs)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSBatchSign failed: %v", err)
|
||||
}
|
||||
|
||||
if len(sigs) != n {
|
||||
t.Errorf("sigs length = %d, want %d", len(sigs), n)
|
||||
}
|
||||
|
||||
// Verify all signatures
|
||||
validCount := 0
|
||||
for i := 0; i < n; i++ {
|
||||
if len(sigs[i]) != BLSSignatureSize {
|
||||
t.Errorf("sig[%d] length = %d, want %d", i, len(sigs[i]), BLSSignatureSize)
|
||||
continue
|
||||
}
|
||||
if BLSVerify(sigs[i], pks[i], msgs[i]) {
|
||||
validCount++
|
||||
} else {
|
||||
t.Errorf("signature %d should be valid", i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("BLS batch sign: %d/%d valid", validCount, n)
|
||||
}
|
||||
|
||||
func TestBLSBatchSignErrors(t *testing.T) {
|
||||
// Test empty inputs
|
||||
_, err := BLSBatchSign(nil, nil)
|
||||
if err != ErrNullPointer {
|
||||
t.Errorf("expected ErrNullPointer for nil inputs, got %v", err)
|
||||
}
|
||||
|
||||
_, err = BLSBatchSign([][]byte{}, [][]byte{})
|
||||
if err != ErrNullPointer {
|
||||
t.Errorf("expected ErrNullPointer for empty inputs, got %v", err)
|
||||
}
|
||||
|
||||
// Test mismatched lengths
|
||||
sk, _ := BLSKeygen(nil)
|
||||
msg := make([]byte, BLSMessageSize)
|
||||
_, err = BLSBatchSign([][]byte{sk}, [][]byte{msg, msg})
|
||||
if err != ErrNullPointer {
|
||||
t.Errorf("expected ErrNullPointer for mismatched lengths, got %v", err)
|
||||
}
|
||||
|
||||
// Test invalid key
|
||||
_, err = BLSBatchSign([][]byte{[]byte("short")}, [][]byte{msg})
|
||||
if err != ErrInvalidKey {
|
||||
t.Errorf("expected ErrInvalidKey for short key, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSAKeygen(t *testing.T) {
|
||||
pk, sk, err := MLDSAKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("MLDSAKeygen failed: %v", err)
|
||||
}
|
||||
|
||||
if len(pk) != MLDSAPublicKeySize {
|
||||
t.Errorf("pk length = %d, want %d", len(pk), MLDSAPublicKeySize)
|
||||
}
|
||||
if len(sk) != MLDSASecretKeySize {
|
||||
t.Errorf("sk length = %d, want %d", len(sk), MLDSASecretKeySize)
|
||||
}
|
||||
|
||||
t.Logf("Generated ML-DSA keypair: pk=%d bytes, sk=%d bytes", len(pk), len(sk))
|
||||
}
|
||||
|
||||
func TestMLDSASignVerify(t *testing.T) {
|
||||
pk, sk, err := MLDSAKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("MLDSAKeygen failed: %v", err)
|
||||
}
|
||||
|
||||
msg := []byte("Hello, ML-DSA post-quantum signatures!")
|
||||
|
||||
sig, err := MLDSASign(sk, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("MLDSASign failed: %v", err)
|
||||
}
|
||||
|
||||
valid := MLDSAVerify(sig, msg, pk)
|
||||
if !valid {
|
||||
t.Error("MLDSAVerify returned false for valid signature")
|
||||
}
|
||||
|
||||
t.Logf("ML-DSA sign/verify successful: sig=%d bytes", len(sig))
|
||||
}
|
||||
|
||||
func TestThreshold(t *testing.T) {
|
||||
threshold := uint32(3)
|
||||
total := uint32(5)
|
||||
|
||||
ctx, err := NewThresholdContext(threshold, total)
|
||||
if err != nil {
|
||||
t.Fatalf("NewThresholdContext failed: %v", err)
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
// Generate shares - may not be supported in stub implementation
|
||||
shares, pk, err := ctx.Keygen(nil)
|
||||
if err == ErrNotSupported {
|
||||
t.Skip("Threshold keygen not supported in stub implementation")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Keygen failed: %v", err)
|
||||
}
|
||||
|
||||
if uint32(len(shares)) != total {
|
||||
t.Errorf("shares count = %d, want %d", len(shares), total)
|
||||
}
|
||||
if len(pk) != BLSPublicKeySize {
|
||||
t.Errorf("pk length = %d, want %d", len(pk), BLSPublicKeySize)
|
||||
}
|
||||
|
||||
t.Logf("Generated %d shares, pk=%d bytes", len(shares), len(pk))
|
||||
|
||||
// Create partial signatures
|
||||
msg := make([]byte, BLSMessageSize)
|
||||
for i := range msg {
|
||||
msg[i] = byte(i * 3)
|
||||
}
|
||||
|
||||
partialSigs := make([][]byte, threshold)
|
||||
indices := make([]uint32, threshold)
|
||||
for i := uint32(0); i < threshold; i++ {
|
||||
var err error
|
||||
partialSigs[i], err = ctx.PartialSign(i, shares[i], msg)
|
||||
if err != nil {
|
||||
t.Fatalf("PartialSign[%d] failed: %v", i, err)
|
||||
}
|
||||
indices[i] = i
|
||||
}
|
||||
|
||||
// Combine
|
||||
sig, err := ctx.Combine(partialSigs, indices)
|
||||
if err != nil {
|
||||
t.Fatalf("Combine failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify
|
||||
valid := ctx.Verify(sig, pk, msg)
|
||||
if !valid {
|
||||
t.Error("Verify returned false for valid threshold signature")
|
||||
}
|
||||
|
||||
t.Logf("Threshold %d-of-%d signature successful", threshold, total)
|
||||
}
|
||||
|
||||
func TestSHA3_256(t *testing.T) {
|
||||
data := []byte("test data for SHA3-256")
|
||||
hash := SHA3_256(data)
|
||||
|
||||
if len(hash) != 32 {
|
||||
t.Errorf("hash length = %d, want 32", len(hash))
|
||||
}
|
||||
|
||||
// Same input should produce same output
|
||||
hash2 := SHA3_256(data)
|
||||
if !bytes.Equal(hash, hash2) {
|
||||
t.Error("SHA3_256 is not deterministic")
|
||||
}
|
||||
|
||||
t.Logf("SHA3-256: %x", hash)
|
||||
}
|
||||
|
||||
func TestSHA3_512(t *testing.T) {
|
||||
data := []byte("test data for SHA3-512")
|
||||
hash := SHA3_512(data)
|
||||
|
||||
if len(hash) != 64 {
|
||||
t.Errorf("hash length = %d, want 64", len(hash))
|
||||
}
|
||||
|
||||
t.Logf("SHA3-512: %x", hash)
|
||||
}
|
||||
|
||||
func TestBLAKE3(t *testing.T) {
|
||||
data := []byte("test data for BLAKE3")
|
||||
hash := BLAKE3(data)
|
||||
|
||||
if len(hash) != 32 {
|
||||
t.Errorf("hash length = %d, want 32", len(hash))
|
||||
}
|
||||
|
||||
t.Logf("BLAKE3: %x", hash)
|
||||
}
|
||||
|
||||
func TestBatchHash(t *testing.T) {
|
||||
inputs := [][]byte{
|
||||
[]byte("input 1"),
|
||||
[]byte("input 2"),
|
||||
[]byte("input 3"),
|
||||
[]byte("input 4"),
|
||||
}
|
||||
|
||||
// Test SHA3-256
|
||||
outputs, err := BatchHash(inputs, HashTypeSHA3_256)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchHash(SHA3-256) failed: %v", err)
|
||||
}
|
||||
|
||||
if len(outputs) != len(inputs) {
|
||||
t.Errorf("outputs count = %d, want %d", len(outputs), len(inputs))
|
||||
}
|
||||
|
||||
for i, out := range outputs {
|
||||
if len(out) != 32 {
|
||||
t.Errorf("outputs[%d] length = %d, want 32", i, len(out))
|
||||
}
|
||||
// Verify against single hash
|
||||
expected := SHA3_256(inputs[i])
|
||||
if !bytes.Equal(out, expected) {
|
||||
t.Errorf("outputs[%d] mismatch: got %x, want %x", i, out, expected)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("BatchHash: %d hashes computed", len(outputs))
|
||||
}
|
||||
|
||||
func TestConsensusVerifyBlock(t *testing.T) {
|
||||
n := 5
|
||||
|
||||
// Generate validator keys
|
||||
sks := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
var err error
|
||||
sks[i], err = BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen[%d] failed: %v", i, err)
|
||||
}
|
||||
pks[i], err = BLSSecretKeyToPublicKey(sks[i])
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey[%d] failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Block hash
|
||||
blockHash := make([]byte, BLSMessageSize)
|
||||
for i := range blockHash {
|
||||
blockHash[i] = byte(i)
|
||||
}
|
||||
|
||||
// Sign with validators
|
||||
sigs := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
var err error
|
||||
sigs[i], err = BLSSign(sks[i], blockHash)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSign[%d] failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify block (without threshold sig)
|
||||
valid := ConsensusVerifyBlock(sigs, pks, nil, nil, blockHash)
|
||||
if !valid {
|
||||
t.Error("ConsensusVerifyBlock returned false for valid block")
|
||||
}
|
||||
|
||||
t.Log("ConsensusVerifyBlock successful")
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
|
||||
func BenchmarkBLSSign(b *testing.B) {
|
||||
sk, _ := BLSKeygen(nil)
|
||||
msg := make([]byte, BLSMessageSize)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSSign(sk, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBLSVerify(b *testing.B) {
|
||||
sk, _ := BLSKeygen(nil)
|
||||
pk, _ := BLSSecretKeyToPublicKey(sk)
|
||||
msg := make([]byte, BLSMessageSize)
|
||||
sig, _ := BLSSign(sk, msg)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
BLSVerify(sig, pk, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBLSBatchVerify(b *testing.B) {
|
||||
n := 100
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sk, _ := BLSKeygen(nil)
|
||||
pks[i], _ = BLSSecretKeyToPublicKey(sk)
|
||||
msgs[i] = make([]byte, BLSMessageSize)
|
||||
sigs[i], _ = BLSSign(sk, msgs[i])
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSBatchVerify(sigs, pks, msgs)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBLSBatchSign(b *testing.B) {
|
||||
n := 100
|
||||
sks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sks[i], _ = BLSKeygen(nil)
|
||||
msgs[i] = make([]byte, BLSMessageSize)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSBatchSign(sks, msgs)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBLSSequentialSign(b *testing.B) {
|
||||
// Benchmark sequential signing for comparison with BLSBatchSign
|
||||
n := 100
|
||||
sks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sks[i], _ = BLSKeygen(nil)
|
||||
msgs[i] = make([]byte, BLSMessageSize)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < n; j++ {
|
||||
_, _ = BLSSign(sks[j], msgs[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSHA3_256(b *testing.B) {
|
||||
data := make([]byte, 1024)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
SHA3_256(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBatchHash(b *testing.B) {
|
||||
n := 100
|
||||
inputs := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
inputs[i] = make([]byte, 1024)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BatchHash(inputs, HashTypeSHA3_256)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks for pool allocation reduction
|
||||
// Run with: go test -bench=BenchmarkPool -benchmem
|
||||
|
||||
func BenchmarkPoolBLSSignWithHash(b *testing.B) {
|
||||
// BLSSign with message that requires hashing (tests pool32 usage)
|
||||
sk, _ := BLSKeygen(nil)
|
||||
msg := make([]byte, 64) // Not 32 bytes, so will be hashed internally
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSSign(sk, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPoolBLSVerifyWithHash(b *testing.B) {
|
||||
// BLSVerify with message that requires hashing
|
||||
sk, _ := BLSKeygen(nil)
|
||||
pk, _ := BLSSecretKeyToPublicKey(sk)
|
||||
msg := make([]byte, 64)
|
||||
sig, _ := BLSSign(sk, msg)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
BLSVerify(sig, pk, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPoolBLSBatchVerifyWithHash(b *testing.B) {
|
||||
// Batch verify with messages requiring hashing
|
||||
n := 100
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sk, _ := BLSKeygen(nil)
|
||||
pks[i], _ = BLSSecretKeyToPublicKey(sk)
|
||||
msgs[i] = make([]byte, 64) // Requires hashing
|
||||
sigs[i], _ = BLSSign(sk, msgs[i])
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSBatchVerify(sigs, pks, msgs)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPoolSHA3_256(b *testing.B) {
|
||||
// SHA3_256 allocates output buffer (not pooled - caller keeps it)
|
||||
data := make([]byte, 1024)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SHA3_256(data)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Parallel vs Sequential Hashing Benchmarks
|
||||
// =============================================================================
|
||||
|
||||
// BenchmarkBLSBatchVerifyParallel benchmarks parallel batch verify (default).
|
||||
func BenchmarkBLSBatchVerifyParallel(b *testing.B) {
|
||||
n := 100
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sk, _ := BLSKeygen(nil)
|
||||
pks[i], _ = BLSSecretKeyToPublicKey(sk)
|
||||
// Use variable-length messages to require hashing
|
||||
msgs[i] = make([]byte, 128+i)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
sigs[i], _ = BLSSign(sk, msgs[i])
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSBatchVerify(sigs, pks, msgs)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBLSBatchVerifySequential benchmarks sequential batch verify.
|
||||
func BenchmarkBLSBatchVerifySequential(b *testing.B) {
|
||||
n := 100
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sk, _ := BLSKeygen(nil)
|
||||
pks[i], _ = BLSSecretKeyToPublicKey(sk)
|
||||
// Use variable-length messages to require hashing
|
||||
msgs[i] = make([]byte, 128+i)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
sigs[i], _ = BLSSign(sk, msgs[i])
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = BLSBatchVerifySequential(sigs, pks, msgs)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMLDSABatchVerifyParallel benchmarks parallel ML-DSA batch verify.
|
||||
func BenchmarkMLDSABatchVerifyParallel(b *testing.B) {
|
||||
n := 10 // Smaller batch for ML-DSA (larger keys/sigs)
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
pk, sk, _ := MLDSAKeygen(nil)
|
||||
pks[i] = pk
|
||||
msgs[i] = make([]byte, 100)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
sigs[i], _ = MLDSASign(sk, msgs[i])
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = MLDSABatchVerify(sigs, msgs, pks)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMLDSABatchVerifySequential benchmarks sequential ML-DSA batch verify.
|
||||
func BenchmarkMLDSABatchVerifySequential(b *testing.B) {
|
||||
n := 10
|
||||
sigs := make([][]byte, n)
|
||||
pks := make([][]byte, n)
|
||||
msgs := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
pk, sk, _ := MLDSAKeygen(nil)
|
||||
pks[i] = pk
|
||||
msgs[i] = make([]byte, 100)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
sigs[i], _ = MLDSASign(sk, msgs[i])
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = MLDSABatchVerifySequential(sigs, msgs, pks)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHashMessagesParallel benchmarks parallel message hashing only.
|
||||
func BenchmarkHashMessagesParallel(b *testing.B) {
|
||||
n := 100
|
||||
msgs := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
msgs[i] = make([]byte, 1024)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
}
|
||||
hashes := make([][]byte, n)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hashMessagesParallel(msgs, hashes)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHashMessagesSequential benchmarks sequential message hashing only.
|
||||
func BenchmarkHashMessagesSequential(b *testing.B) {
|
||||
n := 100
|
||||
msgs := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
msgs[i] = make([]byte, 1024)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
}
|
||||
hashes := make([][]byte, n)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hashMessagesSequential(msgs, hashes)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParallelHashingCorrectness verifies parallel hashing produces same results.
|
||||
func TestParallelHashingCorrectness(t *testing.T) {
|
||||
n := 100
|
||||
msgs := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
msgs[i] = make([]byte, 64+i)
|
||||
for j := range msgs[i] {
|
||||
msgs[i][j] = byte(i + j)
|
||||
}
|
||||
}
|
||||
|
||||
seqHashes := make([][]byte, n)
|
||||
parHashes := make([][]byte, n)
|
||||
|
||||
hashMessagesSequential(msgs, seqHashes)
|
||||
hashMessagesParallel(msgs, parHashes)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if !bytes.Equal(seqHashes[i], parHashes[i]) {
|
||||
t.Errorf("hash mismatch at index %d: seq=%x, par=%x", i, seqHashes[i], parHashes[i])
|
||||
}
|
||||
}
|
||||
t.Logf("Parallel hashing correctness verified for %d messages", n)
|
||||
}
|
||||
|
||||
// TestPoolReturnCorrectness verifies pooled buffers work correctly
|
||||
func TestPoolReturnCorrectness(t *testing.T) {
|
||||
// Sign with message requiring hash (uses pool internally)
|
||||
sk, err := BLSKeygen(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSKeygen failed: %v", err)
|
||||
}
|
||||
|
||||
pk, err := BLSSecretKeyToPublicKey(sk)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSecretKeyToPublicKey failed: %v", err)
|
||||
}
|
||||
|
||||
// Use message > 32 bytes to trigger internal hashing with pooled buffer
|
||||
msg := []byte("This is a longer message that will require SHA3 hashing internally")
|
||||
|
||||
sig, err := BLSSign(sk, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSign failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify should also use pooled buffer for hash
|
||||
if !BLSVerify(sig, pk, msg) {
|
||||
t.Error("BLSVerify failed for valid signature")
|
||||
}
|
||||
|
||||
// Run multiple times to exercise pool reuse
|
||||
for i := 0; i < 100; i++ {
|
||||
sig2, err := BLSSign(sk, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("BLSSign iteration %d failed: %v", i, err)
|
||||
}
|
||||
if !BLSVerify(sig2, pk, msg) {
|
||||
t.Errorf("BLSVerify iteration %d failed", i)
|
||||
}
|
||||
}
|
||||
|
||||
t.Log("Pool correctness test passed")
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
//go:build !cgo
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoCGO_GPUAvailable(t *testing.T) {
|
||||
if GPUAvailable() {
|
||||
t.Error("GPUAvailable should return false without CGO")
|
||||
}
|
||||
|
||||
backend := GetBackend()
|
||||
if backend != "CPU (pure Go)" {
|
||||
t.Errorf("GetBackend = %q, want %q", backend, "CPU (pure Go)")
|
||||
}
|
||||
|
||||
t.Logf("Backend: %s (pure Go fallback active)", backend)
|
||||
}
|
||||
|
||||
func TestNoCGO_BLSFunctions(t *testing.T) {
|
||||
// Test BLSKeygen with seed
|
||||
seed := make([]byte, 32)
|
||||
for i := range seed {
|
||||
seed[i] = byte(i + 1)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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 work correctly in pure Go mode")
|
||||
}
|
||||
|
||||
func TestNoCGO_MLDSAFunctions(t *testing.T) {
|
||||
// Test MLDSAKeygen with seed
|
||||
seed := make([]byte, 32)
|
||||
for i := range seed {
|
||||
seed[i] = byte(i + 1)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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_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))
|
||||
}
|
||||
|
||||
t.Log("All hash functions work correctly in pure Go mode")
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
t.Log("All threshold functions work correctly in pure Go mode")
|
||||
}
|
||||
|
||||
func TestNoCGO_ConsensusVerifyBlock(t *testing.T) {
|
||||
// Create test data - need arrays of signatures and public keys
|
||||
blockHash := SHA3_256([]byte("test block"))
|
||||
|
||||
// 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,239 +0,0 @@
|
||||
// gpu_crypto.h - GPU-accelerated cryptographic operations
|
||||
// Self-contained header with runtime backend detection
|
||||
//
|
||||
// Backends (auto-detected at runtime):
|
||||
// - Metal (Apple Silicon M1/M2/M3/M4)
|
||||
// - CUDA (NVIDIA GPUs)
|
||||
// - CPU (fallback, uses Accelerate on macOS)
|
||||
|
||||
#ifndef LUX_GPU_CRYPTO_H
|
||||
#define LUX_GPU_CRYPTO_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Backend types
|
||||
typedef enum {
|
||||
GPU_BACKEND_NONE = 0,
|
||||
GPU_BACKEND_CPU = 1,
|
||||
GPU_BACKEND_METAL = 2,
|
||||
GPU_BACKEND_CUDA = 3
|
||||
} gpu_backend_t;
|
||||
|
||||
// Error codes
|
||||
typedef enum {
|
||||
GPU_OK = 0,
|
||||
GPU_ERR_INVALID_KEY = -1,
|
||||
GPU_ERR_INVALID_SIG = -2,
|
||||
GPU_ERR_NULL_PTR = -3,
|
||||
GPU_ERR_GPU = -4,
|
||||
GPU_ERR_THRESHOLD = -5,
|
||||
GPU_ERR_HASH = -6,
|
||||
GPU_ERR_NOT_SUPPORTED = -7
|
||||
} gpu_error_t;
|
||||
|
||||
// Sizes
|
||||
#define BLS_SECRET_KEY_SIZE 32
|
||||
#define BLS_PUBLIC_KEY_SIZE 48
|
||||
#define BLS_SIGNATURE_SIZE 96
|
||||
#define MLDSA_SECRET_KEY_SIZE 4032
|
||||
#define MLDSA_PUBLIC_KEY_SIZE 1952
|
||||
#define MLDSA_SIGNATURE_SIZE 3309
|
||||
|
||||
// Global state
|
||||
static gpu_backend_t g_backend = GPU_BACKEND_NONE;
|
||||
static bool g_initialized = false;
|
||||
|
||||
// Backend detection
|
||||
static inline gpu_backend_t detect_backend(void) {
|
||||
if (g_initialized) return g_backend;
|
||||
|
||||
#if defined(USE_METAL) && defined(__APPLE__)
|
||||
// Check for Metal support via Apple frameworks
|
||||
// In production, would use dlopen to check Metal.framework
|
||||
g_backend = GPU_BACKEND_METAL;
|
||||
#elif defined(USE_CUDA) && defined(__linux__)
|
||||
// Check for CUDA support
|
||||
// In production, would use dlopen to check libcuda.so
|
||||
g_backend = GPU_BACKEND_CPU; // Default to CPU on Linux for now
|
||||
#else
|
||||
g_backend = GPU_BACKEND_CPU;
|
||||
#endif
|
||||
|
||||
g_initialized = true;
|
||||
return g_backend;
|
||||
}
|
||||
|
||||
static inline bool gpu_available(void) {
|
||||
gpu_backend_t backend = detect_backend();
|
||||
return backend == GPU_BACKEND_METAL || backend == GPU_BACKEND_CUDA;
|
||||
}
|
||||
|
||||
static inline const char* gpu_backend_name(void) {
|
||||
switch (detect_backend()) {
|
||||
case GPU_BACKEND_METAL: return "Metal";
|
||||
case GPU_BACKEND_CUDA: return "CUDA";
|
||||
case GPU_BACKEND_CPU: return "CPU";
|
||||
default: return "None";
|
||||
}
|
||||
}
|
||||
|
||||
static inline void gpu_clear_cache(void) {
|
||||
// Clear any GPU memory caches
|
||||
}
|
||||
|
||||
// BLS signatures (CPU fallback implementations)
|
||||
// In production, these would call optimized libraries
|
||||
|
||||
static inline gpu_error_t bls_keygen(const uint8_t* seed, size_t seed_len,
|
||||
uint8_t* sk_out, uint8_t* pk_out) {
|
||||
if (!sk_out || !pk_out) return GPU_ERR_NULL_PTR;
|
||||
// Placeholder: in production, use blst or similar
|
||||
// Generate deterministic test key from seed or random
|
||||
memset(sk_out, 0x42, BLS_SECRET_KEY_SIZE);
|
||||
memset(pk_out, 0x43, BLS_PUBLIC_KEY_SIZE);
|
||||
if (seed && seed_len > 0) {
|
||||
memcpy(sk_out, seed, seed_len < BLS_SECRET_KEY_SIZE ? seed_len : BLS_SECRET_KEY_SIZE);
|
||||
}
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline gpu_error_t bls_sk_to_pk(const uint8_t* sk, uint8_t* pk_out) {
|
||||
if (!sk || !pk_out) return GPU_ERR_NULL_PTR;
|
||||
// Placeholder
|
||||
memset(pk_out, 0, BLS_PUBLIC_KEY_SIZE);
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline gpu_error_t bls_sign(const uint8_t* sk, const uint8_t* msg, size_t msg_len,
|
||||
uint8_t* sig_out) {
|
||||
if (!sk || !msg || !sig_out) return GPU_ERR_NULL_PTR;
|
||||
// Placeholder
|
||||
memset(sig_out, 0, BLS_SIGNATURE_SIZE);
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline bool bls_verify(const uint8_t* sig, const uint8_t* pk,
|
||||
const uint8_t* msg, size_t msg_len) {
|
||||
if (!sig || !pk || !msg) return false;
|
||||
// Placeholder - always returns true for testing
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline gpu_error_t bls_aggregate_sigs(const uint8_t** sigs, size_t n,
|
||||
uint8_t* agg_out) {
|
||||
if (!sigs || !agg_out || n == 0) return GPU_ERR_NULL_PTR;
|
||||
memset(agg_out, 0, BLS_SIGNATURE_SIZE);
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline gpu_error_t bls_aggregate_pks(const uint8_t** pks, size_t n,
|
||||
uint8_t* agg_out) {
|
||||
if (!pks || !agg_out || n == 0) return GPU_ERR_NULL_PTR;
|
||||
memset(agg_out, 0, BLS_PUBLIC_KEY_SIZE);
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline bool bls_verify_aggregated(const uint8_t* agg_sig, const uint8_t* agg_pk,
|
||||
const uint8_t* msg, size_t msg_len) {
|
||||
return bls_verify(agg_sig, agg_pk, msg, msg_len);
|
||||
}
|
||||
|
||||
// ML-DSA signatures (CPU fallback)
|
||||
|
||||
static inline gpu_error_t mldsa_keygen(const uint8_t* seed, size_t seed_len,
|
||||
uint8_t* pk_out, uint8_t* sk_out) {
|
||||
if (!pk_out || !sk_out) return GPU_ERR_NULL_PTR;
|
||||
// Placeholder: generate deterministic test keys
|
||||
memset(pk_out, 0x44, MLDSA_PUBLIC_KEY_SIZE);
|
||||
memset(sk_out, 0x45, MLDSA_SECRET_KEY_SIZE);
|
||||
if (seed && seed_len > 0) {
|
||||
memcpy(sk_out, seed, seed_len < 32 ? seed_len : 32);
|
||||
}
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline gpu_error_t mldsa_sign(const uint8_t* sk, const uint8_t* msg, size_t msg_len,
|
||||
uint8_t* sig_out) {
|
||||
if (!sk || !msg || !sig_out) return GPU_ERR_NULL_PTR;
|
||||
memset(sig_out, 0, MLDSA_SIGNATURE_SIZE);
|
||||
return GPU_OK;
|
||||
}
|
||||
|
||||
static inline bool mldsa_verify(const uint8_t* sig, const uint8_t* msg, size_t msg_len,
|
||||
const uint8_t* pk) {
|
||||
if (!sig || !msg || !pk) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hash functions
|
||||
|
||||
static inline void sha3_256(const uint8_t* data, size_t len, uint8_t* out) {
|
||||
if (!data || !out) return;
|
||||
// Placeholder - zero output
|
||||
memset(out, 0, 32);
|
||||
}
|
||||
|
||||
static inline void sha3_512(const uint8_t* data, size_t len, uint8_t* out) {
|
||||
if (!data || !out) return;
|
||||
memset(out, 0, 64);
|
||||
}
|
||||
|
||||
static inline void blake3_hash(const uint8_t* data, size_t len, uint8_t* out) {
|
||||
if (!data || !out) return;
|
||||
memset(out, 0, 32);
|
||||
}
|
||||
|
||||
// Threshold context
|
||||
typedef struct {
|
||||
uint32_t threshold;
|
||||
uint32_t total;
|
||||
} threshold_ctx_t;
|
||||
|
||||
static inline threshold_ctx_t* threshold_new(uint32_t t, uint32_t n) {
|
||||
threshold_ctx_t* ctx = (threshold_ctx_t*)malloc(sizeof(threshold_ctx_t));
|
||||
if (ctx) {
|
||||
ctx->threshold = t;
|
||||
ctx->total = n;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static inline void threshold_free(threshold_ctx_t* ctx) {
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
static inline gpu_error_t threshold_keygen(threshold_ctx_t* ctx,
|
||||
const uint8_t* seed, size_t seed_len,
|
||||
uint8_t** shares_out, size_t* share_size,
|
||||
uint8_t* pk_out) {
|
||||
return GPU_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static inline gpu_error_t threshold_partial_sign(threshold_ctx_t* ctx,
|
||||
uint32_t idx,
|
||||
const uint8_t* share,
|
||||
const uint8_t* msg, size_t msg_len,
|
||||
uint8_t* partial_out) {
|
||||
return GPU_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static inline gpu_error_t threshold_combine(threshold_ctx_t* ctx,
|
||||
const uint8_t** partials,
|
||||
const uint32_t* indices, size_t n,
|
||||
uint8_t* sig_out) {
|
||||
return GPU_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // LUX_GPU_CRYPTO_H
|
||||
@@ -1,235 +0,0 @@
|
||||
// Package gpu provides GPU-accelerated cryptographic operations.
|
||||
// This file implements precomputed NTT twiddle factor caching for polynomial operations.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ML-DSA NTT parameters (FIPS 204 / Dilithium)
|
||||
const (
|
||||
// MLDSA_Q is the ML-DSA prime modulus: 2^23 - 2^13 + 1 = 8380417
|
||||
MLDSA_Q int32 = 8380417
|
||||
|
||||
// MLDSA_N is the ML-DSA polynomial degree
|
||||
MLDSA_N = 256
|
||||
|
||||
// MLDSA_QINV is Q^(-1) mod 2^32 for Montgomery reduction
|
||||
MLDSA_QINV int32 = 58728449
|
||||
|
||||
// MLDSA_MONT is 2^32 mod Q for Montgomery form
|
||||
MLDSA_MONT int32 = -4186625
|
||||
|
||||
// MLDSA_ROOT is primitive 512th root of unity: 1753 (zeta in FIPS 204)
|
||||
MLDSA_ROOT int32 = 1753
|
||||
|
||||
// invNTTScaleFactor is mont^2/256 = 41978 for inverse NTT scaling
|
||||
invNTTScaleFactor int32 = 41978
|
||||
)
|
||||
|
||||
// NTTCache holds precomputed twiddle factors for NTT operations.
|
||||
// Thread-safe after initialization. Immutable during use.
|
||||
type NTTCache struct {
|
||||
N int // Polynomial degree
|
||||
Q int32 // Prime modulus
|
||||
Zetas [256]int32 // Forward NTT twiddle factors
|
||||
}
|
||||
|
||||
// Global cache for ML-DSA (N=256)
|
||||
var (
|
||||
mldsaCache *NTTCache
|
||||
mldsaCacheOnce sync.Once
|
||||
)
|
||||
|
||||
// Precomputed zetas table from ML-DSA reference (FIPS 204).
|
||||
// These are powers of the primitive 512th root of unity in a specific order
|
||||
// for the Cooley-Tukey NTT algorithm.
|
||||
var mldsaZetas = [256]int32{
|
||||
0, 25847, -2608894, -518909, 237124, -777960, -876248, 466468,
|
||||
1826347, 2353451, -359251, -2091905, 3119733, -2884855, 3111497, 2680103,
|
||||
2725464, 1024112, -1079900, 3585928, -549488, -1119584, 2619752, -2108549,
|
||||
-2118186, -3859737, -1399561, -3277672, 1757237, -19422, 4010497, 280005,
|
||||
2706023, 95776, 3077325, 3530437, -1661693, -3592148, -2537516, 3915439,
|
||||
-3861115, -3043716, 3574422, -2867647, 3539968, -300467, 2348700, -539299,
|
||||
-1699267, -1643818, 3505694, -3821735, 3507263, -2140649, -1600420, 3699596,
|
||||
811944, 531354, 954230, 3881043, 3900724, -2556880, 2071892, -2797779,
|
||||
-3930395, -1528703, -3677745, -3041255, -1452451, 3475950, 2176455, -1585221,
|
||||
-1257611, 1939314, -4083598, -1000202, -3190144, -3157330, -3632928, 126922,
|
||||
3412210, -983419, 2147896, 2715295, -2967645, -3693493, -411027, -2477047,
|
||||
-671102, -1228525, -22981, -1308169, -381987, 1349076, 1852771, -1430430,
|
||||
-3343383, 264944, 508951, 3097992, 44288, -1100098, 904516, 3958618,
|
||||
-3724342, -8578, 1653064, -3249728, 2389356, -210977, 759969, -1316856,
|
||||
189548, -3553272, 3159746, -1851402, -2409325, -177440, 1315589, 1341330,
|
||||
1285669, -1584928, -812732, -1439742, -3019102, -3881060, -3628969, 3839961,
|
||||
2091667, 3407706, 2316500, 3817976, -3342478, 2244091, -2446433, -3562462,
|
||||
266997, 2434439, -1235728, 3513181, -3520352, -3759364, -1197226, -3193378,
|
||||
900702, 1859098, 909542, 819034, 495491, -1613174, -43260, -522500,
|
||||
-655327, -3122442, 2031748, 3207046, -3556995, -525098, -768622, -3595838,
|
||||
342297, 286988, -2437823, 4108315, 3437287, -3342277, 1735879, 203044,
|
||||
2842341, 2691481, -2590150, 1265009, 4055324, 1247620, 2486353, 1595974,
|
||||
-3767016, 1250494, 2635921, -3548272, -2994039, 1869119, 1903435, -1050970,
|
||||
-1333058, 1237275, -3318210, -1430225, -451100, 1312455, 3306115, -1962642,
|
||||
-1279661, 1917081, -2546312, -1374803, 1500165, 777191, 2235880, 3406031,
|
||||
-542412, -2831860, -1671176, -1846953, -2584293, -3724270, 594136, -3776993,
|
||||
-2013608, 2432395, 2454455, -164721, 1957272, 3369112, 185531, -1207385,
|
||||
-3183426, 162844, 1616392, 3014001, 810149, 1652634, -3694233, -1799107,
|
||||
-3038916, 3523897, 3866901, 269760, 2213111, -975884, 1717735, 472078,
|
||||
-426683, 1723600, -1803090, 1910376, -1667432, -1104333, -260646, -3833893,
|
||||
-2939036, -2235985, -420899, -2286327, 183443, -976891, 1612842, -3545687,
|
||||
-554416, 3919660, -48306, -1362209, 3937738, 1400424, -846154, 1976782,
|
||||
}
|
||||
|
||||
// GetMLDSACache returns the precomputed NTT cache for ML-DSA (N=256, Q=8380417).
|
||||
// Lazily initialized on first call. Thread-safe.
|
||||
func GetMLDSACache() *NTTCache {
|
||||
mldsaCacheOnce.Do(func() {
|
||||
mldsaCache = &NTTCache{
|
||||
N: MLDSA_N,
|
||||
Q: MLDSA_Q,
|
||||
Zetas: mldsaZetas,
|
||||
}
|
||||
})
|
||||
return mldsaCache
|
||||
}
|
||||
|
||||
// GetNTTCache returns a precomputed NTT cache for the given polynomial size.
|
||||
// Currently only size 256 (ML-DSA) is supported.
|
||||
func GetNTTCache(n int) *NTTCache {
|
||||
if n == 256 {
|
||||
return GetMLDSACache()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NTT performs forward Number Theoretic Transform in-place.
|
||||
// Uses Cooley-Tukey decimation-in-time algorithm.
|
||||
// Output is in bit-reversed order.
|
||||
// Matches FIPS 204 reference implementation exactly.
|
||||
func (c *NTTCache) NTT(a []int32) {
|
||||
if len(a) != c.N {
|
||||
return
|
||||
}
|
||||
|
||||
k := 0
|
||||
for length := 128; length > 0; length >>= 1 {
|
||||
for start := 0; start < c.N; start += 2 * length {
|
||||
k++
|
||||
zeta := c.Zetas[k]
|
||||
for j := start; j < start+length; j++ {
|
||||
t := montgomeryReduce(int64(zeta) * int64(a[j+length]))
|
||||
a[j+length] = a[j] - t
|
||||
a[j] = a[j] + t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InvNTT performs inverse Number Theoretic Transform in-place.
|
||||
// Uses Gentleman-Sande decimation-in-frequency algorithm.
|
||||
// Output is multiplied by Montgomery factor 2^32.
|
||||
// Matches FIPS 204 reference implementation exactly.
|
||||
func (c *NTTCache) InvNTT(a []int32) {
|
||||
if len(a) != c.N {
|
||||
return
|
||||
}
|
||||
|
||||
k := 256
|
||||
for length := 1; length < c.N; length <<= 1 {
|
||||
for start := 0; start < c.N; start += 2 * length {
|
||||
k--
|
||||
zeta := -c.Zetas[k]
|
||||
for j := start; j < start+length; j++ {
|
||||
t := a[j]
|
||||
a[j] = t + a[j+length]
|
||||
a[j+length] = t - a[j+length]
|
||||
a[j+length] = montgomeryReduce(int64(zeta) * int64(a[j+length]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale by mont^2/256
|
||||
for j := 0; j < c.N; j++ {
|
||||
a[j] = montgomeryReduce(int64(invNTTScaleFactor) * int64(a[j]))
|
||||
}
|
||||
}
|
||||
|
||||
// NTTBatch performs forward NTT on multiple polynomials in parallel.
|
||||
func (c *NTTCache) NTTBatch(polys [][]int32) {
|
||||
var wg sync.WaitGroup
|
||||
for i := range polys {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
c.NTT(polys[idx])
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// InvNTTBatch performs inverse NTT on multiple polynomials in parallel.
|
||||
func (c *NTTCache) InvNTTBatch(polys [][]int32) {
|
||||
var wg sync.WaitGroup
|
||||
for i := range polys {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
c.InvNTT(polys[idx])
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// PolyMulNTT multiplies two polynomials in NTT domain.
|
||||
// Both a and b must already be in NTT form.
|
||||
// Result is written to r (can be same as a or b).
|
||||
func (c *NTTCache) PolyMulNTT(r, a, b []int32) {
|
||||
if len(a) != c.N || len(b) != c.N || len(r) != c.N {
|
||||
return
|
||||
}
|
||||
for i := 0; i < c.N; i++ {
|
||||
r[i] = montgomeryReduce(int64(a[i]) * int64(b[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce32 reduces coefficient to range (-Q, Q).
|
||||
func Reduce32(a int32) int32 {
|
||||
t := (a + (1 << 22)) >> 23
|
||||
t = a - t*MLDSA_Q
|
||||
return t
|
||||
}
|
||||
|
||||
// CAddQ adds Q if coefficient is negative.
|
||||
func CAddQ(a int32) int32 {
|
||||
a += (a >> 31) & MLDSA_Q
|
||||
return a
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Montgomery Arithmetic
|
||||
// =============================================================================
|
||||
|
||||
// montgomeryReduce computes Montgomery reduction: a * R^(-1) mod Q
|
||||
// where R = 2^32. For ML-DSA Q = 8380417.
|
||||
// Input: -Q*2^31 <= a < Q*2^31
|
||||
// Output: -Q < result < Q
|
||||
func montgomeryReduce(a int64) int32 {
|
||||
t := int32(a) * MLDSA_QINV
|
||||
return int32((a - int64(t)*int64(MLDSA_Q)) >> 32)
|
||||
}
|
||||
|
||||
// ToMontgomery converts a to Montgomery form: a * 2^32 mod Q.
|
||||
func ToMontgomery(a int32) int32 {
|
||||
return montgomeryReduce(int64(a) * int64(MLDSA_MONT) * int64(MLDSA_MONT))
|
||||
}
|
||||
|
||||
// FromMontgomery converts from Montgomery form: a * 2^(-32) mod Q.
|
||||
func FromMontgomery(a int32) int32 {
|
||||
return montgomeryReduce(int64(a))
|
||||
}
|
||||
|
||||
// ClearNTTCaches releases all cached NTT contexts.
|
||||
// Primarily for testing. New caches will be created on next access.
|
||||
func ClearNTTCaches() {
|
||||
mldsaCacheOnce = sync.Once{}
|
||||
mldsaCache = nil
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Reference zetas from ML-DSA (FIPS 204) for verification.
|
||||
// First 16 values from the official table.
|
||||
var referenceZetas = []int32{
|
||||
0, 25847, -2608894, -518909, 237124, -777960, -876248, 466468,
|
||||
1826347, 2353451, -359251, -2091905, 3119733, -2884855, 3111497, 2680103,
|
||||
}
|
||||
|
||||
func TestNTTCacheConstants(t *testing.T) {
|
||||
// Verify ML-DSA parameters
|
||||
if MLDSA_Q != 8380417 {
|
||||
t.Errorf("MLDSA_Q = %d, want 8380417", MLDSA_Q)
|
||||
}
|
||||
if MLDSA_N != 256 {
|
||||
t.Errorf("MLDSA_N = %d, want 256", MLDSA_N)
|
||||
}
|
||||
|
||||
// Verify Q = 2^23 - 2^13 + 1
|
||||
expected := int32(1<<23 - 1<<13 + 1)
|
||||
if MLDSA_Q != expected {
|
||||
t.Errorf("MLDSA_Q = %d, want 2^23 - 2^13 + 1 = %d", MLDSA_Q, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMLDSACache(t *testing.T) {
|
||||
cache := GetMLDSACache()
|
||||
if cache == nil {
|
||||
t.Fatal("GetMLDSACache returned nil")
|
||||
}
|
||||
|
||||
if cache.N != 256 {
|
||||
t.Errorf("cache.N = %d, want 256", cache.N)
|
||||
}
|
||||
if cache.Q != MLDSA_Q {
|
||||
t.Errorf("cache.Q = %d, want %d", cache.Q, MLDSA_Q)
|
||||
}
|
||||
|
||||
// Verify zetas match reference
|
||||
for i, want := range referenceZetas {
|
||||
if cache.Zetas[i] != want {
|
||||
t.Errorf("cache.Zetas[%d] = %d, want %d", i, cache.Zetas[i], want)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify cache is singleton (same pointer on second call)
|
||||
cache2 := GetMLDSACache()
|
||||
if cache != cache2 {
|
||||
t.Error("GetMLDSACache should return same instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNTTZeroPolynomial(t *testing.T) {
|
||||
cache := GetMLDSACache()
|
||||
|
||||
// Zero polynomial should remain zero
|
||||
poly := make([]int32, cache.N)
|
||||
|
||||
cache.NTT(poly)
|
||||
cache.InvNTT(poly)
|
||||
|
||||
for i := range poly {
|
||||
if poly[i] != 0 {
|
||||
t.Errorf("zero poly[%d] = %d after roundtrip", i, poly[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNTTConstantPolynomial(t *testing.T) {
|
||||
cache := GetMLDSACache()
|
||||
|
||||
// f(x) = 1 means just a[0] = 1 in coefficient representation
|
||||
// After NTT and InvNTT, we should get back 1 * (mont factor)
|
||||
poly := make([]int32, cache.N)
|
||||
poly[0] = 1
|
||||
|
||||
cache.NTT(poly)
|
||||
cache.InvNTT(poly)
|
||||
|
||||
// The result is scaled by mont^2/256 from invNTT
|
||||
// So poly[0] should be montgomeryReduce(1 * mont^2/256) = 1 * mont / 256
|
||||
// Actually for the identity function, we need to account for Montgomery form
|
||||
|
||||
// Just verify all coefficients are properly reduced
|
||||
for i := range poly {
|
||||
if poly[i] <= -MLDSA_Q || poly[i] >= MLDSA_Q {
|
||||
t.Errorf("poly[%d] = %d out of range", i, poly[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNTTCache(t *testing.T) {
|
||||
// Test supported size
|
||||
cache := GetNTTCache(256)
|
||||
if cache == nil {
|
||||
t.Error("GetNTTCache(256) returned nil")
|
||||
}
|
||||
if cache.N != 256 {
|
||||
t.Errorf("GetNTTCache(256).N = %d", cache.N)
|
||||
}
|
||||
|
||||
// Test unsupported sizes
|
||||
for _, size := range []int{128, 512, 1024} {
|
||||
cache := GetNTTCache(size)
|
||||
if cache != nil {
|
||||
t.Errorf("GetNTTCache(%d) should return nil", size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMontgomeryReduce(t *testing.T) {
|
||||
// Test Montgomery reduction correctness
|
||||
testCases := []struct {
|
||||
a int64
|
||||
want int32
|
||||
}{
|
||||
{0, 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
got := montgomeryReduce(tc.a)
|
||||
if got != tc.want {
|
||||
t.Errorf("montgomeryReduce(%d) = %d, want %d", tc.a, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReduce32(t *testing.T) {
|
||||
testCases := []int32{0, 1, -1, MLDSA_Q, -MLDSA_Q, MLDSA_Q / 2, 2 * MLDSA_Q}
|
||||
|
||||
for _, a := range testCases {
|
||||
r := Reduce32(a)
|
||||
// Result should be in range (-Q, Q)
|
||||
if r <= -MLDSA_Q || r >= MLDSA_Q {
|
||||
t.Errorf("Reduce32(%d) = %d, out of range", a, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCAddQ(t *testing.T) {
|
||||
testCases := []struct {
|
||||
a int32
|
||||
want int32
|
||||
}{
|
||||
{0, 0},
|
||||
{1, 1},
|
||||
{-1, MLDSA_Q - 1},
|
||||
{MLDSA_Q - 1, MLDSA_Q - 1},
|
||||
{-MLDSA_Q + 1, 1},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
got := CAddQ(tc.a)
|
||||
if got != tc.want {
|
||||
t.Errorf("CAddQ(%d) = %d, want %d", tc.a, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNTTPolyMulIdentity tests that multiplying by 1 gives identity in NTT domain.
|
||||
// This is the key correctness test for NTT.
|
||||
func TestNTTPolyMulIdentity(t *testing.T) {
|
||||
t.Skip("NTT polynomial operations need further validation - core crypto ops work")
|
||||
cache := GetMLDSACache()
|
||||
|
||||
// Create polynomial f(x) = 1 + 2x + 3x^2 + ...
|
||||
f := make([]int32, cache.N)
|
||||
for i := range f {
|
||||
f[i] = int32(i + 1)
|
||||
}
|
||||
|
||||
// Create polynomial g(x) = 1 (identity for multiplication in NTT domain)
|
||||
g := make([]int32, cache.N)
|
||||
g[0] = 1
|
||||
|
||||
// Transform both
|
||||
fNTT := make([]int32, cache.N)
|
||||
gNTT := make([]int32, cache.N)
|
||||
copy(fNTT, f)
|
||||
copy(gNTT, g)
|
||||
cache.NTT(fNTT)
|
||||
cache.NTT(gNTT)
|
||||
|
||||
// Multiply in NTT domain
|
||||
result := make([]int32, cache.N)
|
||||
cache.PolyMulNTT(result, fNTT, gNTT)
|
||||
|
||||
// Transform back
|
||||
cache.InvNTT(result)
|
||||
|
||||
// Compare with original after applying same transforms
|
||||
original := make([]int32, cache.N)
|
||||
copy(original, f)
|
||||
cache.NTT(original)
|
||||
cache.InvNTT(original)
|
||||
|
||||
// They should match
|
||||
for i := range result {
|
||||
if CAddQ(Reduce32(result[i])) != CAddQ(Reduce32(original[i])) {
|
||||
t.Errorf("result[%d] = %d, want %d", i,
|
||||
CAddQ(Reduce32(result[i])), CAddQ(Reduce32(original[i])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNTTPolyMulCommutative tests that polynomial multiplication is commutative.
|
||||
func TestNTTPolyMulCommutative(t *testing.T) {
|
||||
t.Skip("NTT polynomial operations need further validation - core crypto ops work")
|
||||
cache := GetMLDSACache()
|
||||
|
||||
// Create two random polynomials
|
||||
f := make([]int32, cache.N)
|
||||
g := make([]int32, cache.N)
|
||||
for i := range f {
|
||||
f[i] = rand.Int31n(1000) - 500
|
||||
g[i] = rand.Int31n(1000) - 500
|
||||
}
|
||||
|
||||
// Compute f * g
|
||||
fNTT := make([]int32, cache.N)
|
||||
gNTT := make([]int32, cache.N)
|
||||
copy(fNTT, f)
|
||||
copy(gNTT, g)
|
||||
cache.NTT(fNTT)
|
||||
cache.NTT(gNTT)
|
||||
|
||||
fg := make([]int32, cache.N)
|
||||
cache.PolyMulNTT(fg, fNTT, gNTT)
|
||||
cache.InvNTT(fg)
|
||||
|
||||
// Compute g * f
|
||||
gf := make([]int32, cache.N)
|
||||
cache.PolyMulNTT(gf, gNTT, fNTT)
|
||||
cache.InvNTT(gf)
|
||||
|
||||
// Results should be equal
|
||||
for i := range fg {
|
||||
fgNorm := CAddQ(Reduce32(fg[i]))
|
||||
gfNorm := CAddQ(Reduce32(gf[i]))
|
||||
if fgNorm != gfNorm {
|
||||
t.Errorf("f*g[%d] = %d, g*f[%d] = %d", i, fgNorm, i, gfNorm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNTTSingleElement tests NTT of a single non-zero element.
|
||||
func TestNTTSingleElement(t *testing.T) {
|
||||
cache := GetMLDSACache()
|
||||
|
||||
// f(x) = 5 (constant polynomial)
|
||||
poly := make([]int32, cache.N)
|
||||
poly[0] = 5
|
||||
|
||||
cache.NTT(poly)
|
||||
|
||||
// In NTT domain, constant polynomial should have all elements equal to 5
|
||||
// (NTT of constant is constant in each slot, modulo Montgomery factors)
|
||||
// Just verify all elements are non-zero (not all mapped to 0)
|
||||
nonZero := 0
|
||||
for _, v := range poly {
|
||||
if v != 0 {
|
||||
nonZero++
|
||||
}
|
||||
}
|
||||
if nonZero == 0 {
|
||||
t.Error("NTT of constant 5 should have non-zero elements")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNTTDistributive tests that NTT is linear: NTT(a+b) = NTT(a) + NTT(b).
|
||||
func TestNTTDistributive(t *testing.T) {
|
||||
t.Skip("NTT polynomial operations need further validation - core crypto ops work")
|
||||
cache := GetMLDSACache()
|
||||
|
||||
// Create two polynomials
|
||||
a := make([]int32, cache.N)
|
||||
b := make([]int32, cache.N)
|
||||
for i := range a {
|
||||
a[i] = rand.Int31n(100)
|
||||
b[i] = rand.Int31n(100)
|
||||
}
|
||||
|
||||
// Compute NTT(a+b)
|
||||
sum := make([]int32, cache.N)
|
||||
for i := range sum {
|
||||
sum[i] = a[i] + b[i]
|
||||
}
|
||||
cache.NTT(sum)
|
||||
|
||||
// Compute NTT(a) + NTT(b)
|
||||
aNTT := make([]int32, cache.N)
|
||||
bNTT := make([]int32, cache.N)
|
||||
copy(aNTT, a)
|
||||
copy(bNTT, b)
|
||||
cache.NTT(aNTT)
|
||||
cache.NTT(bNTT)
|
||||
|
||||
// Compare
|
||||
for i := range sum {
|
||||
expected := Reduce32(aNTT[i] + bNTT[i])
|
||||
got := Reduce32(sum[i])
|
||||
if got != expected {
|
||||
t.Errorf("NTT(a+b)[%d] = %d, NTT(a)+NTT(b)[%d] = %d", i, got, i, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Benchmarks
|
||||
// =============================================================================
|
||||
|
||||
func BenchmarkNTTForward256(b *testing.B) {
|
||||
cache := GetMLDSACache()
|
||||
poly := make([]int32, cache.N)
|
||||
for i := range poly {
|
||||
poly[i] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.NTT(poly)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNTTInverse256(b *testing.B) {
|
||||
cache := GetMLDSACache()
|
||||
poly := make([]int32, cache.N)
|
||||
for i := range poly {
|
||||
poly[i] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
cache.NTT(poly) // Pre-transform
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.InvNTT(poly)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNTTRoundTrip256(b *testing.B) {
|
||||
cache := GetMLDSACache()
|
||||
poly := make([]int32, cache.N)
|
||||
for i := range poly {
|
||||
poly[i] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.NTT(poly)
|
||||
cache.InvNTT(poly)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNTTBatch10(b *testing.B) {
|
||||
cache := GetMLDSACache()
|
||||
polys := make([][]int32, 10)
|
||||
for i := range polys {
|
||||
polys[i] = make([]int32, cache.N)
|
||||
for j := range polys[i] {
|
||||
polys[i][j] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.NTTBatch(polys)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNTTBatch100(b *testing.B) {
|
||||
cache := GetMLDSACache()
|
||||
polys := make([][]int32, 100)
|
||||
for i := range polys {
|
||||
polys[i] = make([]int32, cache.N)
|
||||
for j := range polys[i] {
|
||||
polys[i][j] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.NTTBatch(polys)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPolyMulNTT256(b *testing.B) {
|
||||
cache := GetMLDSACache()
|
||||
a := make([]int32, cache.N)
|
||||
bv := make([]int32, cache.N)
|
||||
r := make([]int32, cache.N)
|
||||
for i := range a {
|
||||
a[i] = rand.Int31n(MLDSA_Q)
|
||||
bv[i] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.PolyMulNTT(r, a, bv)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkNTTCached vs BenchmarkNTTCacheMiss shows speedup from caching
|
||||
func BenchmarkNTTCached(b *testing.B) {
|
||||
cache := GetMLDSACache() // Cached twiddle factors
|
||||
poly := make([]int32, cache.N)
|
||||
for i := range poly {
|
||||
poly[i] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.NTT(poly)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNTTCacheMiss(b *testing.B) {
|
||||
// Simulate cache miss by clearing and re-initializing each iteration
|
||||
poly := make([]int32, 256)
|
||||
for i := range poly {
|
||||
poly[i] = rand.Int31n(MLDSA_Q)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Clear cache forces re-initialization
|
||||
ClearNTTCaches()
|
||||
cache := GetMLDSACache()
|
||||
cache.NTT(poly)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetMLDSACache measures cache retrieval overhead
|
||||
func BenchmarkGetMLDSACache(b *testing.B) {
|
||||
// Ensure cache is initialized
|
||||
_ = GetMLDSACache()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetMLDSACache()
|
||||
}
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
//go:build cgo && gpu
|
||||
|
||||
// Package gpu provides GPU-accelerated cryptographic operations.
|
||||
package gpu
|
||||
|
||||
import "sync"
|
||||
|
||||
// Buffer pools for hot path allocations.
|
||||
// Each pool stores byte slices of a specific size to reduce GC pressure.
|
||||
//
|
||||
// Pool sizes:
|
||||
// - 32 bytes: hash outputs (SHA3-256, BLAKE3), BLS secret keys
|
||||
// - 48 bytes: BLS public keys
|
||||
// - 64 bytes: SHA3-512 outputs
|
||||
// - 96 bytes: BLS signatures
|
||||
// - 4032 bytes: ML-DSA secret keys
|
||||
var (
|
||||
pool32 = sync.Pool{
|
||||
New: func() any { return make([]byte, 32) },
|
||||
}
|
||||
|
||||
pool48 = sync.Pool{
|
||||
New: func() any { return make([]byte, 48) },
|
||||
}
|
||||
|
||||
pool64 = sync.Pool{
|
||||
New: func() any { return make([]byte, 64) },
|
||||
}
|
||||
|
||||
pool96 = sync.Pool{
|
||||
New: func() any { return make([]byte, 96) },
|
||||
}
|
||||
|
||||
pool4032 = sync.Pool{
|
||||
New: func() any { return make([]byte, 4032) },
|
||||
}
|
||||
)
|
||||
|
||||
// GetBuffer32 retrieves a 32-byte buffer from the pool.
|
||||
// The returned buffer is zeroed. Caller must call PutBuffer32 when done.
|
||||
func GetBuffer32() []byte {
|
||||
buf := pool32.Get().([]byte)
|
||||
clear(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
// PutBuffer32 returns a 32-byte buffer to the pool.
|
||||
func PutBuffer32(buf []byte) {
|
||||
if len(buf) != 32 {
|
||||
return
|
||||
}
|
||||
pool32.Put(buf)
|
||||
}
|
||||
|
||||
// GetBuffer48 retrieves a 48-byte buffer from the pool.
|
||||
func GetBuffer48() []byte {
|
||||
buf := pool48.Get().([]byte)
|
||||
clear(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
// PutBuffer48 returns a 48-byte buffer to the pool.
|
||||
func PutBuffer48(buf []byte) {
|
||||
if len(buf) != 48 {
|
||||
return
|
||||
}
|
||||
pool48.Put(buf)
|
||||
}
|
||||
|
||||
// GetBuffer64 retrieves a 64-byte buffer from the pool.
|
||||
func GetBuffer64() []byte {
|
||||
buf := pool64.Get().([]byte)
|
||||
clear(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
// PutBuffer64 returns a 64-byte buffer to the pool.
|
||||
func PutBuffer64(buf []byte) {
|
||||
if len(buf) != 64 {
|
||||
return
|
||||
}
|
||||
pool64.Put(buf)
|
||||
}
|
||||
|
||||
// GetBuffer96 retrieves a 96-byte buffer from the pool.
|
||||
func GetBuffer96() []byte {
|
||||
buf := pool96.Get().([]byte)
|
||||
clear(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
// PutBuffer96 returns a 96-byte buffer to the pool.
|
||||
func PutBuffer96(buf []byte) {
|
||||
if len(buf) != 96 {
|
||||
return
|
||||
}
|
||||
pool96.Put(buf)
|
||||
}
|
||||
|
||||
// GetBuffer4032 retrieves a 4032-byte buffer from the pool.
|
||||
func GetBuffer4032() []byte {
|
||||
buf := pool4032.Get().([]byte)
|
||||
clear(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
// PutBuffer4032 returns a 4032-byte buffer to the pool.
|
||||
func PutBuffer4032(buf []byte) {
|
||||
if len(buf) != 4032 {
|
||||
return
|
||||
}
|
||||
pool4032.Put(buf)
|
||||
}
|
||||
|
||||
// SHA3_256Into is defined in crypto.go with CGO acceleration.
|
||||
// Use it with GetBuffer32/PutBuffer32 to avoid allocations.
|
||||
|
||||
// SHA3_512Into computes SHA3-512 hash into the provided buffer.
|
||||
func SHA3_512Into(out, data []byte) {
|
||||
if len(out) < 64 {
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
clear(out[:64])
|
||||
return
|
||||
}
|
||||
hash := SHA3_512(data)
|
||||
copy(out[:64], hash)
|
||||
}
|
||||
|
||||
// BLAKE3Into computes BLAKE3 hash into the provided buffer.
|
||||
func BLAKE3Into(out, data []byte) {
|
||||
if len(out) < 32 {
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
clear(out[:32])
|
||||
return
|
||||
}
|
||||
hash := BLAKE3(data)
|
||||
copy(out[:32], hash)
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
//go:build cgo && gpu
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPool32(t *testing.T) {
|
||||
buf := GetBuffer32()
|
||||
if len(buf) != 32 {
|
||||
t.Errorf("expected 32 bytes, got %d", len(buf))
|
||||
}
|
||||
|
||||
// Verify zeroed
|
||||
for i, b := range buf {
|
||||
if b != 0 {
|
||||
t.Errorf("buffer not zeroed at index %d: got %d", i, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Modify and return
|
||||
buf[0] = 0xFF
|
||||
PutBuffer32(buf)
|
||||
|
||||
// Get again - should be zeroed
|
||||
buf2 := GetBuffer32()
|
||||
if buf2[0] != 0 {
|
||||
t.Errorf("buffer not zeroed after reuse: got %d", buf2[0])
|
||||
}
|
||||
PutBuffer32(buf2)
|
||||
}
|
||||
|
||||
func TestPool48(t *testing.T) {
|
||||
buf := GetBuffer48()
|
||||
if len(buf) != 48 {
|
||||
t.Errorf("expected 48 bytes, got %d", len(buf))
|
||||
}
|
||||
PutBuffer48(buf)
|
||||
}
|
||||
|
||||
func TestPool64(t *testing.T) {
|
||||
buf := GetBuffer64()
|
||||
if len(buf) != 64 {
|
||||
t.Errorf("expected 64 bytes, got %d", len(buf))
|
||||
}
|
||||
PutBuffer64(buf)
|
||||
}
|
||||
|
||||
func TestPool96(t *testing.T) {
|
||||
buf := GetBuffer96()
|
||||
if len(buf) != 96 {
|
||||
t.Errorf("expected 96 bytes, got %d", len(buf))
|
||||
}
|
||||
PutBuffer96(buf)
|
||||
}
|
||||
|
||||
func TestPool4032(t *testing.T) {
|
||||
buf := GetBuffer4032()
|
||||
if len(buf) != 4032 {
|
||||
t.Errorf("expected 4032 bytes, got %d", len(buf))
|
||||
}
|
||||
PutBuffer4032(buf)
|
||||
}
|
||||
|
||||
func TestPoolWrongSize(t *testing.T) {
|
||||
// Putting wrong size buffer should be silently ignored
|
||||
wrongBuf := make([]byte, 16)
|
||||
PutBuffer32(wrongBuf) // Should not panic
|
||||
|
||||
// Pool should still work
|
||||
buf := GetBuffer32()
|
||||
if len(buf) != 32 {
|
||||
t.Errorf("pool corrupted after wrong size put")
|
||||
}
|
||||
PutBuffer32(buf)
|
||||
}
|
||||
|
||||
func TestPoolConcurrent(t *testing.T) {
|
||||
const goroutines = 100
|
||||
const iterations = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
|
||||
for g := 0; g < goroutines; g++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < iterations; i++ {
|
||||
buf := GetBuffer32()
|
||||
if len(buf) != 32 {
|
||||
t.Errorf("wrong size from pool")
|
||||
return
|
||||
}
|
||||
// Simulate work
|
||||
for j := range buf {
|
||||
buf[j] = byte(j)
|
||||
}
|
||||
PutBuffer32(buf)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSHA3_256Into(t *testing.T) {
|
||||
data := []byte("test data for hashing")
|
||||
|
||||
// Using pool + Into function
|
||||
buf := GetBuffer32()
|
||||
SHA3_256Into(buf, data)
|
||||
|
||||
// Using regular function
|
||||
expected := SHA3_256(data)
|
||||
|
||||
// Should match
|
||||
for i := range expected {
|
||||
if buf[i] != expected[i] {
|
||||
t.Errorf("SHA3_256Into mismatch at %d: got %x, want %x", i, buf[i], expected[i])
|
||||
}
|
||||
}
|
||||
PutBuffer32(buf)
|
||||
}
|
||||
|
||||
func BenchmarkPoolGet32(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := GetBuffer32()
|
||||
PutBuffer32(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMakeSlice32(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := make([]byte, 32)
|
||||
_ = buf
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPoolGet96(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := GetBuffer96()
|
||||
PutBuffer96(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMakeSlice96(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := make([]byte, 96)
|
||||
_ = buf
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPoolGet4032(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := GetBuffer4032()
|
||||
PutBuffer4032(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMakeSlice4032(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := make([]byte, 4032)
|
||||
_ = buf
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark SHA3_256 with and without pooling
|
||||
func BenchmarkSHA3_256Allocating(b *testing.B) {
|
||||
data := make([]byte, 1024)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SHA3_256(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSHA3_256Pooled(b *testing.B) {
|
||||
data := make([]byte, 1024)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := GetBuffer32()
|
||||
SHA3_256Into(buf, data)
|
||||
PutBuffer32(buf)
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
//go:build cgo && gpu
|
||||
|
||||
// Package gpu provides GPU-accelerated ZK cryptographic operations.
|
||||
//
|
||||
// This package wraps github.com/luxfi/gpu for unified GPU support:
|
||||
// - Metal (Apple Silicon via MLX)
|
||||
// - CUDA (NVIDIA via MLX)
|
||||
// - CPU fallback (gnark-crypto)
|
||||
//
|
||||
// Operations:
|
||||
// - Poseidon2 hash (BN254/Fr) for Merkle trees
|
||||
// - Multi-scalar multiplication (MSM) for commitments
|
||||
// - Batch commitment/nullifier operations
|
||||
//
|
||||
// Threshold-gated routing:
|
||||
// - Below threshold: CPU (lower latency)
|
||||
// - Above threshold: GPU (higher throughput)
|
||||
//
|
||||
// Build with: go build -tags cgo
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
cgo "github.com/luxfi/gpu"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr/poseidon2"
|
||||
gnarkHash "github.com/consensys/gnark-crypto/hash"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Threshold Constants
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// ThresholdPoseidon2 is the minimum batch size for GPU Poseidon2 hashing.
|
||||
ThresholdPoseidon2 = 64
|
||||
|
||||
// ThresholdMerkle is the minimum leaf pairs for GPU Merkle layer.
|
||||
ThresholdMerkle = 128
|
||||
|
||||
// ThresholdMSM is the minimum point-scalar pairs for GPU MSM.
|
||||
ThresholdMSM = 256
|
||||
|
||||
// ThresholdCommitment is the minimum batch size for GPU commitments.
|
||||
ThresholdCommitment = 128
|
||||
|
||||
// ThresholdFRI is the minimum evaluations for GPU FRI folding.
|
||||
ThresholdFRI = 512
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Error Types
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrSizeMismatch = errors.New("input size mismatch")
|
||||
ErrNotPowerOfTwo = errors.New("size must be power of two")
|
||||
ErrGPUUnavailable = errors.New("GPU acceleration unavailable")
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Field Element Type (BN254 Fr - 256-bit)
|
||||
// =============================================================================
|
||||
|
||||
// Fr256 represents a 256-bit field element (BN254 scalar field).
|
||||
// Uses 4 x 64-bit limbs in little-endian order.
|
||||
type Fr256 = cgo.Fr256
|
||||
|
||||
// =============================================================================
|
||||
// ZK Context
|
||||
// =============================================================================
|
||||
|
||||
// ZKContext provides GPU-accelerated ZK operations with automatic routing.
|
||||
type ZKContext struct {
|
||||
mu sync.RWMutex
|
||||
gpuEnabled bool
|
||||
deviceName string
|
||||
|
||||
// Stats for monitoring
|
||||
gpuCalls int64
|
||||
cpuCalls int64
|
||||
}
|
||||
|
||||
// DefaultZKContext is the global ZK context (lazy initialized).
|
||||
var (
|
||||
defaultZKContext *ZKContext
|
||||
initOnce sync.Once
|
||||
)
|
||||
|
||||
// GetZKContext returns the global ZK context.
|
||||
func GetZKContext() *ZKContext {
|
||||
initOnce.Do(func() {
|
||||
gpuEnabled := cgo.ZKGPUAvailable()
|
||||
deviceName := cgo.ZKGetBackend()
|
||||
if !gpuEnabled {
|
||||
deviceName = "CPU (gnark-crypto)"
|
||||
}
|
||||
defaultZKContext = &ZKContext{
|
||||
gpuEnabled: gpuEnabled,
|
||||
deviceName: deviceName,
|
||||
}
|
||||
})
|
||||
return defaultZKContext
|
||||
}
|
||||
|
||||
// GPUEnabled returns true if GPU acceleration is enabled.
|
||||
func (z *ZKContext) GPUEnabled() bool {
|
||||
z.mu.RLock()
|
||||
defer z.mu.RUnlock()
|
||||
return z.gpuEnabled
|
||||
}
|
||||
|
||||
// DeviceName returns the GPU device name.
|
||||
func (z *ZKContext) DeviceName() string {
|
||||
z.mu.RLock()
|
||||
defer z.mu.RUnlock()
|
||||
return z.deviceName
|
||||
}
|
||||
|
||||
// Stats returns GPU/CPU call statistics.
|
||||
func (z *ZKContext) Stats() (gpuCalls, cpuCalls int64) {
|
||||
z.mu.RLock()
|
||||
defer z.mu.RUnlock()
|
||||
return z.gpuCalls, z.cpuCalls
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Poseidon2 Hash Operations (CPU Implementation - gnark-crypto)
|
||||
// =============================================================================
|
||||
|
||||
// poseidon2HasherPool is a pool of Poseidon2 hashers for concurrent use.
|
||||
var poseidon2HasherPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return poseidon2.NewMerkleDamgardHasher()
|
||||
},
|
||||
}
|
||||
|
||||
// poseidon2HashPairCPU computes Poseidon2(left, right) using gnark-crypto.
|
||||
func poseidon2HashPairCPU(left, right *Fr256) Fr256 {
|
||||
h := poseidon2HasherPool.Get().(gnarkHash.StateStorer)
|
||||
defer poseidon2HasherPool.Put(h)
|
||||
|
||||
h.Reset()
|
||||
|
||||
// Write left and right as bytes
|
||||
leftBytes := fr256ToBytes(left)
|
||||
rightBytes := fr256ToBytes(right)
|
||||
|
||||
_, _ = h.Write(leftBytes)
|
||||
_, _ = h.Write(rightBytes)
|
||||
|
||||
// Get the hash result
|
||||
resultBytes := h.Sum(nil)
|
||||
|
||||
// Convert back to Fr256
|
||||
var out Fr256
|
||||
_ = fr256FromBytes(&out, resultBytes[:32])
|
||||
return out
|
||||
}
|
||||
|
||||
// fr256ToBytes converts Fr256 to 32-byte big-endian slice.
|
||||
func fr256ToBytes(f *Fr256) []byte {
|
||||
buf := make([]byte, 32)
|
||||
binary.BigEndian.PutUint64(buf[0:8], f[3])
|
||||
binary.BigEndian.PutUint64(buf[8:16], f[2])
|
||||
binary.BigEndian.PutUint64(buf[16:24], f[1])
|
||||
binary.BigEndian.PutUint64(buf[24:32], f[0])
|
||||
return buf
|
||||
}
|
||||
|
||||
// fr256FromBytes sets Fr256 from 32-byte big-endian slice.
|
||||
func fr256FromBytes(f *Fr256, buf []byte) error {
|
||||
if len(buf) != 32 {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
f[3] = binary.BigEndian.Uint64(buf[0:8])
|
||||
f[2] = binary.BigEndian.Uint64(buf[8:16])
|
||||
f[1] = binary.BigEndian.Uint64(buf[16:24])
|
||||
f[0] = binary.BigEndian.Uint64(buf[24:32])
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Public API - Poseidon2
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2HashPair computes Poseidon2(left, right) with automatic routing.
|
||||
func (z *ZKContext) Poseidon2HashPair(left, right *Fr256) Fr256 {
|
||||
// Single hash always uses CPU (GPU overhead not worth it)
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
return poseidon2HashPairCPU(left, right)
|
||||
}
|
||||
|
||||
// Poseidon2BatchHashPair computes batch Poseidon2 hashes with threshold routing.
|
||||
func (z *ZKContext) Poseidon2BatchHashPair(left, right []Fr256) ([]Fr256, error) {
|
||||
n := len(left)
|
||||
if n != len(right) {
|
||||
return nil, ErrSizeMismatch
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check threshold for GPU routing
|
||||
if z.GPUEnabled() && n >= ThresholdPoseidon2 {
|
||||
z.mu.Lock()
|
||||
z.gpuCalls++
|
||||
z.mu.Unlock()
|
||||
return cgo.Poseidon2Hash(left, right)
|
||||
}
|
||||
|
||||
// CPU path using gnark-crypto
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
result := make([]Fr256, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = poseidon2HashPairCPU(&left[i], &right[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Poseidon2 Merkle Tree Operations
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2MerkleLayer computes one layer of a Merkle tree.
|
||||
func (z *ZKContext) Poseidon2MerkleLayer(nodes []Fr256) ([]Fr256, error) {
|
||||
n := len(nodes)
|
||||
if n == 0 || n%2 != 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
parentCount := n / 2
|
||||
|
||||
// Check threshold for GPU routing
|
||||
if z.GPUEnabled() && parentCount >= ThresholdMerkle {
|
||||
z.mu.Lock()
|
||||
z.gpuCalls++
|
||||
z.mu.Unlock()
|
||||
return cgo.MerkleLayer(nodes)
|
||||
}
|
||||
|
||||
// CPU path
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
parents := make([]Fr256, parentCount)
|
||||
for i := 0; i < parentCount; i++ {
|
||||
parents[i] = poseidon2HashPairCPU(&nodes[i*2], &nodes[i*2+1])
|
||||
}
|
||||
|
||||
return parents, nil
|
||||
}
|
||||
|
||||
// Poseidon2MerkleRoot computes the Merkle root from leaves.
|
||||
func (z *ZKContext) Poseidon2MerkleRoot(leaves []Fr256) (Fr256, error) {
|
||||
n := len(leaves)
|
||||
if n == 0 || (n&(n-1)) != 0 {
|
||||
return Fr256{}, ErrNotPowerOfTwo
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return leaves[0], nil
|
||||
}
|
||||
|
||||
// Use GPU if available and above threshold
|
||||
if z.GPUEnabled() && n >= ThresholdMerkle*2 {
|
||||
z.mu.Lock()
|
||||
z.gpuCalls++
|
||||
z.mu.Unlock()
|
||||
return cgo.MerkleRoot(leaves)
|
||||
}
|
||||
|
||||
// CPU path - build tree layer by layer
|
||||
current := leaves
|
||||
for len(current) > 1 {
|
||||
next, err := z.Poseidon2MerkleLayer(current)
|
||||
if err != nil {
|
||||
return Fr256{}, err
|
||||
}
|
||||
current = next
|
||||
}
|
||||
|
||||
return current[0], nil
|
||||
}
|
||||
|
||||
// Poseidon2MerkleTree builds a complete Merkle tree.
|
||||
func (z *ZKContext) Poseidon2MerkleTree(leaves []Fr256) ([]Fr256, error) {
|
||||
n := len(leaves)
|
||||
if n == 0 || (n&(n-1)) != 0 {
|
||||
return nil, ErrNotPowerOfTwo
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return []Fr256{leaves[0]}, nil
|
||||
}
|
||||
|
||||
// Use GPU if available and above threshold
|
||||
if z.GPUEnabled() && n >= ThresholdMerkle*2 {
|
||||
z.mu.Lock()
|
||||
z.gpuCalls++
|
||||
z.mu.Unlock()
|
||||
return cgo.MerkleTree(leaves)
|
||||
}
|
||||
|
||||
// CPU path - collect all internal nodes
|
||||
allNodes := make([]Fr256, 0, n-1)
|
||||
current := leaves
|
||||
|
||||
for len(current) > 1 {
|
||||
next, err := z.Poseidon2MerkleLayer(current)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allNodes = append(allNodes, next...)
|
||||
current = next
|
||||
}
|
||||
|
||||
return allNodes, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Commitment and Nullifier Operations
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2Commitment computes commitment = Poseidon2(Poseidon2(value, blinding), salt).
|
||||
func (z *ZKContext) Poseidon2Commitment(value, blinding, salt *Fr256) Fr256 {
|
||||
intermediate := poseidon2HashPairCPU(value, blinding)
|
||||
return poseidon2HashPairCPU(&intermediate, salt)
|
||||
}
|
||||
|
||||
// Poseidon2Nullifier computes nullifier = Poseidon2(Poseidon2(key, commitment), index).
|
||||
func (z *ZKContext) Poseidon2Nullifier(key, commitment, index *Fr256) Fr256 {
|
||||
intermediate := poseidon2HashPairCPU(key, commitment)
|
||||
return poseidon2HashPairCPU(&intermediate, index)
|
||||
}
|
||||
|
||||
// BatchCommitment computes batch commitments with threshold routing.
|
||||
func (z *ZKContext) BatchCommitment(values, blindings, salts []Fr256) ([]Fr256, error) {
|
||||
n := len(values)
|
||||
if n != len(blindings) || n != len(salts) {
|
||||
return nil, ErrSizeMismatch
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check threshold for GPU routing
|
||||
if z.GPUEnabled() && n >= ThresholdCommitment {
|
||||
z.mu.Lock()
|
||||
z.gpuCalls++
|
||||
z.mu.Unlock()
|
||||
return cgo.BatchCommitment(values, blindings, salts)
|
||||
}
|
||||
|
||||
// CPU path
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
result := make([]Fr256, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = z.Poseidon2Commitment(&values[i], &blindings[i], &salts[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BatchNullifier computes batch nullifiers with threshold routing.
|
||||
func (z *ZKContext) BatchNullifier(keys, commitments, indices []Fr256) ([]Fr256, error) {
|
||||
n := len(keys)
|
||||
if n != len(commitments) || n != len(indices) {
|
||||
return nil, ErrSizeMismatch
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check threshold for GPU routing
|
||||
if z.GPUEnabled() && n >= ThresholdCommitment {
|
||||
z.mu.Lock()
|
||||
z.gpuCalls++
|
||||
z.mu.Unlock()
|
||||
return cgo.BatchNullifier(keys, commitments, indices)
|
||||
}
|
||||
|
||||
// CPU path
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
result := make([]Fr256, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = z.Poseidon2Nullifier(&keys[i], &commitments[i], &indices[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Convenience Functions (use default context)
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2Hash computes Poseidon2(left, right).
|
||||
func Poseidon2Hash(left, right *Fr256) Fr256 {
|
||||
return GetZKContext().Poseidon2HashPair(left, right)
|
||||
}
|
||||
|
||||
// Poseidon2BatchHash computes batch Poseidon2 hashes.
|
||||
func Poseidon2BatchHash(left, right []Fr256) ([]Fr256, error) {
|
||||
return GetZKContext().Poseidon2BatchHashPair(left, right)
|
||||
}
|
||||
|
||||
// MerkleRoot computes the Poseidon2 Merkle root from leaves.
|
||||
func MerkleRoot(leaves []Fr256) (Fr256, error) {
|
||||
return GetZKContext().Poseidon2MerkleRoot(leaves)
|
||||
}
|
||||
|
||||
// MerkleTree builds a complete Poseidon2 Merkle tree.
|
||||
func MerkleTree(leaves []Fr256) ([]Fr256, error) {
|
||||
return GetZKContext().Poseidon2MerkleTree(leaves)
|
||||
}
|
||||
|
||||
// Commitment computes a Poseidon2 commitment.
|
||||
func Commitment(value, blinding, salt *Fr256) Fr256 {
|
||||
return GetZKContext().Poseidon2Commitment(value, blinding, salt)
|
||||
}
|
||||
|
||||
// Nullifier computes a Poseidon2 nullifier.
|
||||
func Nullifier(key, commitment, index *Fr256) Fr256 {
|
||||
return GetZKContext().Poseidon2Nullifier(key, commitment, index)
|
||||
}
|
||||
-368
@@ -1,368 +0,0 @@
|
||||
//go:build !gpu || !cgo
|
||||
|
||||
// Package gpu provides CPU-only ZK cryptographic operations when GPU support
|
||||
// is disabled or CGO is unavailable.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr/poseidon2"
|
||||
gnarkHash "github.com/consensys/gnark-crypto/hash"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Threshold Constants
|
||||
// =============================================================================
|
||||
|
||||
const (
|
||||
// ThresholdPoseidon2 is the minimum batch size for GPU Poseidon2 hashing.
|
||||
ThresholdPoseidon2 = 64
|
||||
|
||||
// ThresholdMerkle is the minimum leaf pairs for GPU Merkle layer.
|
||||
ThresholdMerkle = 128
|
||||
|
||||
// ThresholdMSM is the minimum point-scalar pairs for GPU MSM.
|
||||
ThresholdMSM = 256
|
||||
|
||||
// ThresholdCommitment is the minimum batch size for GPU commitments.
|
||||
ThresholdCommitment = 128
|
||||
|
||||
// ThresholdFRI is the minimum evaluations for GPU FRI folding.
|
||||
ThresholdFRI = 512
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Error Types
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrSizeMismatch = errors.New("input size mismatch")
|
||||
ErrNotPowerOfTwo = errors.New("size must be power of two")
|
||||
ErrGPUUnavailable = errors.New("GPU acceleration unavailable")
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Field Element Type (BN254 Fr - 256-bit)
|
||||
// =============================================================================
|
||||
|
||||
// Fr256 represents a 256-bit field element (BN254 scalar field).
|
||||
// Uses 4 x 64-bit limbs in little-endian order.
|
||||
type Fr256 [4]uint64
|
||||
|
||||
// =============================================================================
|
||||
// ZK Context
|
||||
// =============================================================================
|
||||
|
||||
// ZKContext provides CPU-only ZK operations with automatic routing.
|
||||
type ZKContext struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
gpuEnabled bool
|
||||
deviceName string
|
||||
|
||||
// Stats for monitoring
|
||||
gpuCalls int64
|
||||
cpuCalls int64
|
||||
}
|
||||
|
||||
// DefaultZKContext is the global ZK context (lazy initialized).
|
||||
var (
|
||||
defaultZKContext *ZKContext
|
||||
initOnce sync.Once
|
||||
)
|
||||
|
||||
// GetZKContext returns the global ZK context.
|
||||
func GetZKContext() *ZKContext {
|
||||
initOnce.Do(func() {
|
||||
defaultZKContext = &ZKContext{
|
||||
gpuEnabled: false,
|
||||
deviceName: "CPU (gnark-crypto)",
|
||||
}
|
||||
})
|
||||
return defaultZKContext
|
||||
}
|
||||
|
||||
// GPUEnabled returns true if GPU acceleration is enabled.
|
||||
func (z *ZKContext) GPUEnabled() bool {
|
||||
z.mu.RLock()
|
||||
defer z.mu.RUnlock()
|
||||
return z.gpuEnabled
|
||||
}
|
||||
|
||||
// DeviceName returns the GPU device name.
|
||||
func (z *ZKContext) DeviceName() string {
|
||||
z.mu.RLock()
|
||||
defer z.mu.RUnlock()
|
||||
return z.deviceName
|
||||
}
|
||||
|
||||
// Stats returns GPU/CPU call statistics.
|
||||
func (z *ZKContext) Stats() (gpuCalls, cpuCalls int64) {
|
||||
z.mu.RLock()
|
||||
defer z.mu.RUnlock()
|
||||
return z.gpuCalls, z.cpuCalls
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Poseidon2 Hash Operations (CPU Implementation - gnark-crypto)
|
||||
// =============================================================================
|
||||
|
||||
// poseidon2HasherPool is a pool of Poseidon2 hashers for concurrent use.
|
||||
var poseidon2HasherPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return poseidon2.NewMerkleDamgardHasher()
|
||||
},
|
||||
}
|
||||
|
||||
// poseidon2HashPairCPU computes Poseidon2(left, right) using gnark-crypto.
|
||||
func poseidon2HashPairCPU(left, right *Fr256) Fr256 {
|
||||
h := poseidon2HasherPool.Get().(gnarkHash.StateStorer)
|
||||
defer poseidon2HasherPool.Put(h)
|
||||
|
||||
h.Reset()
|
||||
|
||||
// Write left and right as bytes
|
||||
leftBytes := fr256ToBytes(left)
|
||||
rightBytes := fr256ToBytes(right)
|
||||
|
||||
_, _ = h.Write(leftBytes)
|
||||
_, _ = h.Write(rightBytes)
|
||||
|
||||
// Get the hash result
|
||||
resultBytes := h.Sum(nil)
|
||||
|
||||
// Convert back to Fr256
|
||||
var out Fr256
|
||||
_ = fr256FromBytes(&out, resultBytes[:32])
|
||||
return out
|
||||
}
|
||||
|
||||
// fr256ToBytes converts Fr256 to 32-byte big-endian slice.
|
||||
func fr256ToBytes(f *Fr256) []byte {
|
||||
buf := make([]byte, 32)
|
||||
binary.BigEndian.PutUint64(buf[0:8], f[3])
|
||||
binary.BigEndian.PutUint64(buf[8:16], f[2])
|
||||
binary.BigEndian.PutUint64(buf[16:24], f[1])
|
||||
binary.BigEndian.PutUint64(buf[24:32], f[0])
|
||||
return buf
|
||||
}
|
||||
|
||||
// fr256FromBytes sets Fr256 from 32-byte big-endian slice.
|
||||
func fr256FromBytes(f *Fr256, buf []byte) error {
|
||||
if len(buf) != 32 {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
f[3] = binary.BigEndian.Uint64(buf[0:8])
|
||||
f[2] = binary.BigEndian.Uint64(buf[8:16])
|
||||
f[1] = binary.BigEndian.Uint64(buf[16:24])
|
||||
f[0] = binary.BigEndian.Uint64(buf[24:32])
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Public API - Poseidon2
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2HashPair computes Poseidon2(left, right) with automatic routing.
|
||||
func (z *ZKContext) Poseidon2HashPair(left, right *Fr256) Fr256 {
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
return poseidon2HashPairCPU(left, right)
|
||||
}
|
||||
|
||||
// Poseidon2BatchHashPair computes batch Poseidon2 hashes with CPU routing.
|
||||
func (z *ZKContext) Poseidon2BatchHashPair(left, right []Fr256) ([]Fr256, error) {
|
||||
n := len(left)
|
||||
if n != len(right) {
|
||||
return nil, ErrSizeMismatch
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
result := make([]Fr256, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = poseidon2HashPairCPU(&left[i], &right[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Poseidon2 Merkle Tree Operations
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2MerkleLayer computes one layer of a Merkle tree.
|
||||
func (z *ZKContext) Poseidon2MerkleLayer(nodes []Fr256) ([]Fr256, error) {
|
||||
n := len(nodes)
|
||||
if n == 0 || n%2 != 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
parentCount := n / 2
|
||||
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
parents := make([]Fr256, parentCount)
|
||||
for i := 0; i < parentCount; i++ {
|
||||
parents[i] = poseidon2HashPairCPU(&nodes[i*2], &nodes[i*2+1])
|
||||
}
|
||||
|
||||
return parents, nil
|
||||
}
|
||||
|
||||
// Poseidon2MerkleRoot computes the Merkle root from leaves.
|
||||
func (z *ZKContext) Poseidon2MerkleRoot(leaves []Fr256) (Fr256, error) {
|
||||
n := len(leaves)
|
||||
if n == 0 || (n&(n-1)) != 0 {
|
||||
return Fr256{}, ErrNotPowerOfTwo
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return leaves[0], nil
|
||||
}
|
||||
|
||||
// CPU path - build tree layer by layer
|
||||
current := leaves
|
||||
for len(current) > 1 {
|
||||
next, err := z.Poseidon2MerkleLayer(current)
|
||||
if err != nil {
|
||||
return Fr256{}, err
|
||||
}
|
||||
current = next
|
||||
}
|
||||
|
||||
return current[0], nil
|
||||
}
|
||||
|
||||
// Poseidon2MerkleTree builds a complete Merkle tree.
|
||||
func (z *ZKContext) Poseidon2MerkleTree(leaves []Fr256) ([]Fr256, error) {
|
||||
n := len(leaves)
|
||||
if n == 0 || (n&(n-1)) != 0 {
|
||||
return nil, ErrNotPowerOfTwo
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return []Fr256{leaves[0]}, nil
|
||||
}
|
||||
|
||||
// CPU path - collect all internal nodes
|
||||
allNodes := make([]Fr256, 0, n-1)
|
||||
current := leaves
|
||||
|
||||
for len(current) > 1 {
|
||||
next, err := z.Poseidon2MerkleLayer(current)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allNodes = append(allNodes, next...)
|
||||
current = next
|
||||
}
|
||||
|
||||
return allNodes, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Commitment and Nullifier Operations
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2Commitment computes commitment = Poseidon2(Poseidon2(value, blinding), salt).
|
||||
func (z *ZKContext) Poseidon2Commitment(value, blinding, salt *Fr256) Fr256 {
|
||||
intermediate := poseidon2HashPairCPU(value, blinding)
|
||||
return poseidon2HashPairCPU(&intermediate, salt)
|
||||
}
|
||||
|
||||
// Poseidon2Nullifier computes nullifier = Poseidon2(Poseidon2(key, commitment), index).
|
||||
func (z *ZKContext) Poseidon2Nullifier(key, commitment, index *Fr256) Fr256 {
|
||||
intermediate := poseidon2HashPairCPU(key, commitment)
|
||||
return poseidon2HashPairCPU(&intermediate, index)
|
||||
}
|
||||
|
||||
// BatchCommitment computes batch commitments with CPU routing.
|
||||
func (z *ZKContext) BatchCommitment(values, blindings, salts []Fr256) ([]Fr256, error) {
|
||||
n := len(values)
|
||||
if n != len(blindings) || n != len(salts) {
|
||||
return nil, ErrSizeMismatch
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
result := make([]Fr256, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = z.Poseidon2Commitment(&values[i], &blindings[i], &salts[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BatchNullifier computes batch nullifiers with CPU routing.
|
||||
func (z *ZKContext) BatchNullifier(keys, commitments, indices []Fr256) ([]Fr256, error) {
|
||||
n := len(keys)
|
||||
if n != len(commitments) || n != len(indices) {
|
||||
return nil, ErrSizeMismatch
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
z.mu.Lock()
|
||||
z.cpuCalls++
|
||||
z.mu.Unlock()
|
||||
|
||||
result := make([]Fr256, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result[i] = z.Poseidon2Nullifier(&keys[i], &commitments[i], &indices[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Convenience Functions (use default context)
|
||||
// =============================================================================
|
||||
|
||||
// Poseidon2Hash computes Poseidon2(left, right).
|
||||
func Poseidon2Hash(left, right *Fr256) Fr256 {
|
||||
return GetZKContext().Poseidon2HashPair(left, right)
|
||||
}
|
||||
|
||||
// Poseidon2BatchHash computes batch Poseidon2 hashes.
|
||||
func Poseidon2BatchHash(left, right []Fr256) ([]Fr256, error) {
|
||||
return GetZKContext().Poseidon2BatchHashPair(left, right)
|
||||
}
|
||||
|
||||
// MerkleRoot computes the Poseidon2 Merkle root from leaves.
|
||||
func MerkleRoot(leaves []Fr256) (Fr256, error) {
|
||||
return GetZKContext().Poseidon2MerkleRoot(leaves)
|
||||
}
|
||||
|
||||
// MerkleTree builds a complete Poseidon2 Merkle tree.
|
||||
func MerkleTree(leaves []Fr256) ([]Fr256, error) {
|
||||
return GetZKContext().Poseidon2MerkleTree(leaves)
|
||||
}
|
||||
|
||||
// Commitment computes a Poseidon2 commitment.
|
||||
func Commitment(value, blinding, salt *Fr256) Fr256 {
|
||||
return GetZKContext().Poseidon2Commitment(value, blinding, salt)
|
||||
}
|
||||
|
||||
// Nullifier computes a Poseidon2 nullifier.
|
||||
func Nullifier(key, commitment, index *Fr256) Fr256 {
|
||||
return GetZKContext().Poseidon2Nullifier(key, commitment, index)
|
||||
}
|
||||
-424
@@ -1,424 +0,0 @@
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFr256Type tests that Fr256 is correctly defined.
|
||||
func TestFr256Type(t *testing.T) {
|
||||
var f Fr256
|
||||
f[0] = 0x1234567890abcdef
|
||||
f[1] = 0xfedcba0987654321
|
||||
f[2] = 0xabcdef0123456789
|
||||
f[3] = 0x9876543210fedcba
|
||||
|
||||
// Verify 4 limbs of 64 bits each
|
||||
if len(f) != 4 {
|
||||
t.Fatalf("Expected 4 limbs, got %d", len(f))
|
||||
}
|
||||
|
||||
// Test copy semantics
|
||||
f2 := f
|
||||
if f != f2 {
|
||||
t.Error("Copy failed")
|
||||
}
|
||||
|
||||
// Modify copy should not affect original
|
||||
f2[0] = 0
|
||||
if f[0] == 0 {
|
||||
t.Error("Copy modified original")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFr256ByteHelpers tests internal byte conversion helpers.
|
||||
func TestFr256ByteHelpers(t *testing.T) {
|
||||
var f Fr256
|
||||
f[0] = 0x1234567890abcdef
|
||||
f[1] = 0xfedcba0987654321
|
||||
f[2] = 0xabcdef0123456789
|
||||
f[3] = 0x9876543210fedcba
|
||||
|
||||
// Use internal helpers via roundtrip through hash
|
||||
// The byte conversion is exercised in poseidon2HashPairCPU
|
||||
buf := fr256ToBytes(&f)
|
||||
if len(buf) != 32 {
|
||||
t.Fatalf("Expected 32 bytes, got %d", len(buf))
|
||||
}
|
||||
|
||||
var f2 Fr256
|
||||
if err := fr256FromBytes(&f2, buf); err != nil {
|
||||
t.Fatalf("fr256FromBytes failed: %v", err)
|
||||
}
|
||||
|
||||
if f != f2 {
|
||||
t.Errorf("Byte roundtrip failed: got %v, want %v", f2, f)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoseidon2HashPair tests single Poseidon2 hash.
|
||||
func TestPoseidon2HashPair(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
// Create test inputs
|
||||
var left, right Fr256
|
||||
left[0] = 1
|
||||
right[0] = 2
|
||||
|
||||
// Compute hash
|
||||
result := ctx.Poseidon2HashPair(&left, &right)
|
||||
|
||||
// Result should be non-zero
|
||||
if result == (Fr256{}) {
|
||||
t.Error("Poseidon2 hash returned zero")
|
||||
}
|
||||
|
||||
// Same inputs should give same output (deterministic)
|
||||
result2 := ctx.Poseidon2HashPair(&left, &right)
|
||||
if result != result2 {
|
||||
t.Error("Poseidon2 hash not deterministic")
|
||||
}
|
||||
|
||||
// Different inputs should give different output
|
||||
left[0] = 3
|
||||
result3 := ctx.Poseidon2HashPair(&left, &right)
|
||||
if result == result3 {
|
||||
t.Error("Different inputs gave same hash")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoseidon2BatchHashPair tests batch Poseidon2 hashing.
|
||||
func TestPoseidon2BatchHashPair(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
n := 16
|
||||
left := make([]Fr256, n)
|
||||
right := make([]Fr256, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
left[i][0] = uint64(i)
|
||||
right[i][0] = uint64(i + n)
|
||||
}
|
||||
|
||||
// Compute batch hash
|
||||
results, err := ctx.Poseidon2BatchHashPair(left, right)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchHashPair failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) != n {
|
||||
t.Fatalf("Expected %d results, got %d", n, len(results))
|
||||
}
|
||||
|
||||
// Verify each result matches individual hash
|
||||
for i := 0; i < n; i++ {
|
||||
expected := ctx.Poseidon2HashPair(&left[i], &right[i])
|
||||
if results[i] != expected {
|
||||
t.Errorf("Result[%d] mismatch: got %v, want %v", i, results[i], expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoseidon2MerkleRoot tests Merkle root computation.
|
||||
func TestPoseidon2MerkleRoot(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
// Test with 4 leaves
|
||||
leaves := make([]Fr256, 4)
|
||||
for i := 0; i < 4; i++ {
|
||||
leaves[i][0] = uint64(i + 1)
|
||||
}
|
||||
|
||||
root, err := ctx.Poseidon2MerkleRoot(leaves)
|
||||
if err != nil {
|
||||
t.Fatalf("MerkleRoot failed: %v", err)
|
||||
}
|
||||
|
||||
// Root should be non-zero
|
||||
if root == (Fr256{}) {
|
||||
t.Error("Merkle root is zero")
|
||||
}
|
||||
|
||||
// Compute manually to verify
|
||||
// Level 1: hash(L0, L1), hash(L2, L3)
|
||||
h01 := ctx.Poseidon2HashPair(&leaves[0], &leaves[1])
|
||||
h23 := ctx.Poseidon2HashPair(&leaves[2], &leaves[3])
|
||||
// Level 0 (root): hash(h01, h23)
|
||||
expectedRoot := ctx.Poseidon2HashPair(&h01, &h23)
|
||||
|
||||
if root != expectedRoot {
|
||||
t.Errorf("Merkle root mismatch: got %v, want %v", root, expectedRoot)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoseidon2MerkleTree tests complete Merkle tree building.
|
||||
func TestPoseidon2MerkleTree(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
// Test with 8 leaves
|
||||
leaves := make([]Fr256, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
leaves[i][0] = uint64(i + 1)
|
||||
}
|
||||
|
||||
tree, err := ctx.Poseidon2MerkleTree(leaves)
|
||||
if err != nil {
|
||||
t.Fatalf("MerkleTree failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have 7 internal nodes (n-1 for n=8)
|
||||
expectedNodes := 7
|
||||
if len(tree) != expectedNodes {
|
||||
t.Fatalf("Expected %d nodes, got %d", expectedNodes, len(tree))
|
||||
}
|
||||
|
||||
// Last node should be the root
|
||||
root, err := ctx.Poseidon2MerkleRoot(leaves)
|
||||
if err != nil {
|
||||
t.Fatalf("MerkleRoot failed: %v", err)
|
||||
}
|
||||
|
||||
if tree[len(tree)-1] != root {
|
||||
t.Error("Last tree node should be root")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoseidon2Commitment tests commitment computation.
|
||||
func TestPoseidon2Commitment(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
var value, blinding, salt Fr256
|
||||
value[0] = 100
|
||||
blinding[0] = 0xdeadbeef
|
||||
salt[0] = 0xcafebabe
|
||||
|
||||
commitment := ctx.Poseidon2Commitment(&value, &blinding, &salt)
|
||||
|
||||
// Should be non-zero
|
||||
if commitment == (Fr256{}) {
|
||||
t.Error("Commitment is zero")
|
||||
}
|
||||
|
||||
// Same inputs should give same commitment
|
||||
commitment2 := ctx.Poseidon2Commitment(&value, &blinding, &salt)
|
||||
if commitment != commitment2 {
|
||||
t.Error("Commitment not deterministic")
|
||||
}
|
||||
|
||||
// Different blinding should give different commitment
|
||||
blinding[0] = 0xfeedface
|
||||
commitment3 := ctx.Poseidon2Commitment(&value, &blinding, &salt)
|
||||
if commitment == commitment3 {
|
||||
t.Error("Different blinding gave same commitment")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoseidon2Nullifier tests nullifier computation.
|
||||
func TestPoseidon2Nullifier(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
var key, commitment, index Fr256
|
||||
key[0] = 0x1234
|
||||
commitment[0] = 0x5678
|
||||
index[0] = 42
|
||||
|
||||
nullifier := ctx.Poseidon2Nullifier(&key, &commitment, &index)
|
||||
|
||||
// Should be non-zero
|
||||
if nullifier == (Fr256{}) {
|
||||
t.Error("Nullifier is zero")
|
||||
}
|
||||
|
||||
// Same inputs should give same nullifier
|
||||
nullifier2 := ctx.Poseidon2Nullifier(&key, &commitment, &index)
|
||||
if nullifier != nullifier2 {
|
||||
t.Error("Nullifier not deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchCommitment tests batch commitment computation.
|
||||
func TestBatchCommitment(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
n := 10
|
||||
values := make([]Fr256, n)
|
||||
blindings := make([]Fr256, n)
|
||||
salts := make([]Fr256, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
values[i][0] = uint64(i * 100)
|
||||
blindings[i][0] = uint64(i * 1000 + 1)
|
||||
salts[i][0] = uint64(i * 10000 + 2)
|
||||
}
|
||||
|
||||
commitments, err := ctx.BatchCommitment(values, blindings, salts)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchCommitment failed: %v", err)
|
||||
}
|
||||
|
||||
if len(commitments) != n {
|
||||
t.Fatalf("Expected %d commitments, got %d", n, len(commitments))
|
||||
}
|
||||
|
||||
// Verify each commitment matches individual computation
|
||||
for i := 0; i < n; i++ {
|
||||
expected := ctx.Poseidon2Commitment(&values[i], &blindings[i], &salts[i])
|
||||
if commitments[i] != expected {
|
||||
t.Errorf("Commitment[%d] mismatch", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestZKContextStats tests statistics tracking.
|
||||
func TestZKContextStats(t *testing.T) {
|
||||
ctx := GetZKContext()
|
||||
|
||||
// Get initial stats
|
||||
gpuBefore, cpuBefore := ctx.Stats()
|
||||
|
||||
// Do some operations
|
||||
var left, right Fr256
|
||||
left[0] = 1
|
||||
right[0] = 2
|
||||
_ = ctx.Poseidon2HashPair(&left, &right)
|
||||
_ = ctx.Poseidon2HashPair(&left, &right)
|
||||
|
||||
// Check stats increased
|
||||
gpuAfter, cpuAfter := ctx.Stats()
|
||||
|
||||
// CPU calls should have increased (single hashes always use CPU)
|
||||
if cpuAfter <= cpuBefore {
|
||||
t.Error("CPU calls did not increase")
|
||||
}
|
||||
|
||||
t.Logf("Stats: GPU=%d (delta=%d), CPU=%d (delta=%d)",
|
||||
gpuAfter, gpuAfter-gpuBefore, cpuAfter, cpuAfter-cpuBefore)
|
||||
}
|
||||
|
||||
// TestThresholdConstants tests threshold constant values.
|
||||
func TestThresholdConstants(t *testing.T) {
|
||||
// Verify thresholds are reasonable
|
||||
if ThresholdPoseidon2 < 1 {
|
||||
t.Error("ThresholdPoseidon2 should be >= 1")
|
||||
}
|
||||
if ThresholdMerkle < 1 {
|
||||
t.Error("ThresholdMerkle should be >= 1")
|
||||
}
|
||||
if ThresholdMSM < 1 {
|
||||
t.Error("ThresholdMSM should be >= 1")
|
||||
}
|
||||
if ThresholdCommitment < 1 {
|
||||
t.Error("ThresholdCommitment should be >= 1")
|
||||
}
|
||||
if ThresholdFRI < 1 {
|
||||
t.Error("ThresholdFRI should be >= 1")
|
||||
}
|
||||
|
||||
t.Logf("Thresholds: Poseidon2=%d, Merkle=%d, MSM=%d, Commitment=%d, FRI=%d",
|
||||
ThresholdPoseidon2, ThresholdMerkle, ThresholdMSM, ThresholdCommitment, ThresholdFRI)
|
||||
}
|
||||
|
||||
// TestConvenienceFunctions tests the package-level convenience functions.
|
||||
func TestConvenienceFunctions(t *testing.T) {
|
||||
var left, right Fr256
|
||||
left[0] = 10
|
||||
right[0] = 20
|
||||
|
||||
// Test Poseidon2Hash
|
||||
hash := Poseidon2Hash(&left, &right)
|
||||
if hash == (Fr256{}) {
|
||||
t.Error("Poseidon2Hash returned zero")
|
||||
}
|
||||
|
||||
// Test MerkleRoot
|
||||
leaves := []Fr256{left, right, left, right}
|
||||
root, err := MerkleRoot(leaves)
|
||||
if err != nil {
|
||||
t.Fatalf("MerkleRoot failed: %v", err)
|
||||
}
|
||||
if root == (Fr256{}) {
|
||||
t.Error("MerkleRoot returned zero")
|
||||
}
|
||||
|
||||
// Test Commitment
|
||||
var salt Fr256
|
||||
salt[0] = 30
|
||||
commitment := Commitment(&left, &right, &salt)
|
||||
if commitment == (Fr256{}) {
|
||||
t.Error("Commitment returned zero")
|
||||
}
|
||||
|
||||
// Test Nullifier
|
||||
nullifier := Nullifier(&left, &right, &salt)
|
||||
if nullifier == (Fr256{}) {
|
||||
t.Error("Nullifier returned zero")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPoseidon2HashPair benchmarks single Poseidon2 hash.
|
||||
func BenchmarkPoseidon2HashPair(b *testing.B) {
|
||||
ctx := GetZKContext()
|
||||
var left, right Fr256
|
||||
left[0] = 1
|
||||
right[0] = 2
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ctx.Poseidon2HashPair(&left, &right)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPoseidon2BatchHash benchmarks batch Poseidon2 hashing.
|
||||
func BenchmarkPoseidon2BatchHash(b *testing.B) {
|
||||
ctx := GetZKContext()
|
||||
sizes := []int{16, 64, 256, 1024}
|
||||
|
||||
for _, size := range sizes {
|
||||
left := make([]Fr256, size)
|
||||
right := make([]Fr256, size)
|
||||
for i := 0; i < size; i++ {
|
||||
left[i][0] = uint64(i)
|
||||
right[i][0] = uint64(i + size)
|
||||
}
|
||||
|
||||
b.Run(
|
||||
func(n int) string {
|
||||
if n >= ThresholdPoseidon2 {
|
||||
return "GPU"
|
||||
}
|
||||
return "CPU"
|
||||
}(size)+"-"+string(rune('0'+size/100)),
|
||||
func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ctx.Poseidon2BatchHashPair(left, right)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMerkleRoot benchmarks Merkle root computation.
|
||||
func BenchmarkMerkleRoot(b *testing.B) {
|
||||
ctx := GetZKContext()
|
||||
sizes := []int{8, 64, 256, 1024}
|
||||
|
||||
for _, size := range sizes {
|
||||
leaves := make([]Fr256, size)
|
||||
for i := 0; i < size; i++ {
|
||||
leaves[i][0] = uint64(i + 1)
|
||||
}
|
||||
|
||||
b.Run(
|
||||
func(n int) string {
|
||||
if n >= ThresholdMerkle*2 {
|
||||
return "GPU"
|
||||
}
|
||||
return "CPU"
|
||||
}(size)+"-"+string(rune('0'+size/100)),
|
||||
func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ctx.Poseidon2MerkleRoot(leaves)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated Blake3 hash functions via Metal.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// The C++ library handles automatic fallback to CPU when GPU is unavailable.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// C API from luxcpp/crypto
|
||||
extern void crypto_blake3(uint8_t* out, const uint8_t* in, size_t len);
|
||||
extern int crypto_batch_hash(uint8_t** outs, const uint8_t* const* ins, const size_t* lens, uint32_t count, int hash_type);
|
||||
extern int crypto_gpu_available(void);
|
||||
extern const char* crypto_get_backend(void);
|
||||
|
||||
// Metal-specific Blake3 functions
|
||||
extern int metal_blake3_hash256(const uint8_t* data, uint32_t len, uint8_t* out);
|
||||
extern int metal_blake3_hash512(const uint8_t* data, uint32_t len, uint8_t* out);
|
||||
extern int metal_blake3_xof(const uint8_t* data, uint32_t inLen, uint8_t* out, uint32_t outLen);
|
||||
extern int metal_blake3_merkle_root(const uint8_t* leaves, uint32_t numLeaves, uint8_t* out);
|
||||
extern int metal_blake3_derive_key(const uint8_t* context, uint32_t contextLen, const uint8_t* key, uint32_t keyLen, uint8_t* out);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Size256 is the standard Blake3 output size
|
||||
Size256 = 32
|
||||
// Size512 is extended Blake3 output size
|
||||
Size512 = 64
|
||||
// BatchThreshold is minimum batch size for GPU acceleration
|
||||
BatchThreshold = 64
|
||||
)
|
||||
|
||||
// Available returns true if GPU acceleration is available.
|
||||
func Available() bool {
|
||||
return C.crypto_gpu_available() != 0
|
||||
}
|
||||
|
||||
// Backend returns the active backend name ("Metal", "CUDA", or "CPU").
|
||||
func Backend() string {
|
||||
return C.GoString(C.crypto_get_backend())
|
||||
}
|
||||
|
||||
// Sum256 computes a 32-byte Blake3 hash using GPU if available.
|
||||
func Sum256(data []byte) [Size256]byte {
|
||||
var out [Size256]byte
|
||||
if len(data) == 0 {
|
||||
C.crypto_blake3((*C.uint8_t)(&out[0]), nil, 0)
|
||||
return out
|
||||
}
|
||||
C.crypto_blake3(
|
||||
(*C.uint8_t)(&out[0]),
|
||||
(*C.uint8_t)(&data[0]),
|
||||
C.size_t(len(data)),
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
// Sum512 computes a 64-byte Blake3 hash using Metal GPU.
|
||||
func Sum512(data []byte) [Size512]byte {
|
||||
var out [Size512]byte
|
||||
var dataPtr *C.uint8_t
|
||||
if len(data) > 0 {
|
||||
dataPtr = (*C.uint8_t)(&data[0])
|
||||
}
|
||||
C.metal_blake3_hash512(dataPtr, C.uint32_t(len(data)), (*C.uint8_t)(&out[0]))
|
||||
return out
|
||||
}
|
||||
|
||||
// SumXOF computes an extendable output hash of arbitrary length.
|
||||
func SumXOF(data []byte, outLen int) []byte {
|
||||
if outLen <= 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]byte, outLen)
|
||||
var dataPtr *C.uint8_t
|
||||
if len(data) > 0 {
|
||||
dataPtr = (*C.uint8_t)(&data[0])
|
||||
}
|
||||
C.metal_blake3_xof(
|
||||
dataPtr,
|
||||
C.uint32_t(len(data)),
|
||||
(*C.uint8_t)(&out[0]),
|
||||
C.uint32_t(outLen),
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
// BatchSum256 computes Blake3 hashes for multiple inputs in parallel on GPU.
|
||||
// More efficient than calling Sum256 repeatedly for batch operations.
|
||||
func BatchSum256(inputs [][]byte) [][Size256]byte {
|
||||
n := len(inputs)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare C arrays for batch processing
|
||||
outs := make([]*C.uint8_t, n)
|
||||
ins := make([]*C.uint8_t, n)
|
||||
lens := make([]C.size_t, n)
|
||||
|
||||
results := make([][Size256]byte, n)
|
||||
for i, data := range inputs {
|
||||
outs[i] = (*C.uint8_t)(&results[i][0])
|
||||
if len(data) > 0 {
|
||||
ins[i] = (*C.uint8_t)(&data[0])
|
||||
}
|
||||
lens[i] = C.size_t(len(data))
|
||||
}
|
||||
|
||||
// Call batch hash (hash_type 2 = BLAKE3)
|
||||
C.crypto_batch_hash(
|
||||
(**C.uint8_t)(unsafe.Pointer(&outs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&ins[0])),
|
||||
(*C.size_t)(&lens[0]),
|
||||
C.uint32_t(n),
|
||||
2, // BLAKE3
|
||||
)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// MerkleRoot computes the Merkle root of leaves using GPU-accelerated Blake3.
|
||||
func MerkleRoot(leaves [][]byte) [Size256]byte {
|
||||
var out [Size256]byte
|
||||
n := len(leaves)
|
||||
if n == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
// Flatten leaves to contiguous buffer
|
||||
leafData := make([]byte, n*Size256)
|
||||
for i, leaf := range leaves {
|
||||
hash := Sum256(leaf)
|
||||
copy(leafData[i*Size256:], hash[:])
|
||||
}
|
||||
|
||||
C.metal_blake3_merkle_root(
|
||||
(*C.uint8_t)(&leafData[0]),
|
||||
C.uint32_t(n),
|
||||
(*C.uint8_t)(&out[0]),
|
||||
)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeriveKey derives a key from context and key material using Blake3 KDF.
|
||||
func DeriveKey(context string, keyMaterial []byte) [Size256]byte {
|
||||
var out [Size256]byte
|
||||
contextBytes := []byte(context)
|
||||
|
||||
var contextPtr, keyPtr *C.uint8_t
|
||||
if len(contextBytes) > 0 {
|
||||
contextPtr = (*C.uint8_t)(&contextBytes[0])
|
||||
}
|
||||
if len(keyMaterial) > 0 {
|
||||
keyPtr = (*C.uint8_t)(&keyMaterial[0])
|
||||
}
|
||||
|
||||
C.metal_blake3_derive_key(
|
||||
contextPtr, C.uint32_t(len(contextBytes)),
|
||||
keyPtr, C.uint32_t(len(keyMaterial)),
|
||||
(*C.uint8_t)(&out[0]),
|
||||
)
|
||||
return out
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated Blake3 hash functions.
|
||||
// This is the stub implementation when GPU acceleration is not available.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
const (
|
||||
// Size256 is the standard Blake3 output size
|
||||
Size256 = 32
|
||||
// Size512 is extended Blake3 output size
|
||||
Size512 = 64
|
||||
// BatchThreshold is minimum batch size for GPU acceleration
|
||||
BatchThreshold = 64
|
||||
)
|
||||
|
||||
// Available returns true if GPU acceleration is available.
|
||||
func Available() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Backend returns the active backend name.
|
||||
func Backend() string {
|
||||
return "CPU"
|
||||
}
|
||||
|
||||
// Sum256 computes a 32-byte Blake3 hash.
|
||||
func Sum256(data []byte) [Size256]byte {
|
||||
return blake3.Sum256(data)
|
||||
}
|
||||
|
||||
// Sum512 computes a 64-byte Blake3 hash.
|
||||
func Sum512(data []byte) [Size512]byte {
|
||||
return blake3.Sum512(data)
|
||||
}
|
||||
|
||||
// SumXOF computes an extendable output hash of arbitrary length.
|
||||
func SumXOF(data []byte, outLen int) []byte {
|
||||
if outLen <= 0 {
|
||||
return nil
|
||||
}
|
||||
h := blake3.New(outLen, nil)
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// BatchSum256 computes multiple 32-byte hashes.
|
||||
func BatchSum256(inputs [][]byte) [][Size256]byte {
|
||||
results := make([][Size256]byte, len(inputs))
|
||||
for i, input := range inputs {
|
||||
results[i] = blake3.Sum256(input)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// MerkleRoot computes the Merkle root of leaves using Blake3.
|
||||
func MerkleRoot(leaves [][]byte) [Size256]byte {
|
||||
if len(leaves) == 0 {
|
||||
return [Size256]byte{}
|
||||
}
|
||||
if len(leaves) == 1 {
|
||||
return blake3.Sum256(leaves[0])
|
||||
}
|
||||
// Simple iterative merkle tree
|
||||
hashes := make([][Size256]byte, len(leaves))
|
||||
for i, leaf := range leaves {
|
||||
hashes[i] = blake3.Sum256(leaf)
|
||||
}
|
||||
for len(hashes) > 1 {
|
||||
next := make([][Size256]byte, (len(hashes)+1)/2)
|
||||
for i := 0; i < len(hashes); i += 2 {
|
||||
if i+1 < len(hashes) {
|
||||
combined := append(hashes[i][:], hashes[i+1][:]...)
|
||||
next[i/2] = blake3.Sum256(combined)
|
||||
} else {
|
||||
next[i/2] = hashes[i]
|
||||
}
|
||||
}
|
||||
hashes = next
|
||||
}
|
||||
return hashes[0]
|
||||
}
|
||||
|
||||
// DeriveKey derives a key using Blake3 KDF.
|
||||
func DeriveKey(context, key []byte) [Size256]byte {
|
||||
h := blake3.New(Size256, key)
|
||||
h.Write(context)
|
||||
var out [Size256]byte
|
||||
copy(out[:], h.Sum(nil))
|
||||
return out
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/google/btree"
|
||||
|
||||
hashing "github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/crypto/hash"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -149,7 +149,7 @@ type ringMutator interface {
|
||||
// hashRing is an implementation of Ring
|
||||
type hashRing struct {
|
||||
// Hashing algorithm to use when hashing keys.
|
||||
hasher hashing.Hasher
|
||||
hasher hash.Hasher
|
||||
|
||||
// Replication factor for nodes; must be greater than zero.
|
||||
virtualNodes int
|
||||
@@ -163,7 +163,7 @@ type RingConfig struct {
|
||||
// Replication factor for nodes in the ring.
|
||||
VirtualNodes int
|
||||
// Hashing implementation to use.
|
||||
Hasher hashing.Hasher
|
||||
Hasher hash.Hasher
|
||||
// Degree represents the degree of the b-tree
|
||||
Degree int
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated Poseidon2 hash functions.
|
||||
// Pure Go implementation when CGO is not available.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrNotAvailable is returned when GPU acceleration is not available.
|
||||
var ErrNotAvailable = errors.New("GPU acceleration not available")
|
||||
|
||||
// ElementSize is the size of a BN254 scalar field element in bytes (256-bit).
|
||||
const ElementSize = 32
|
||||
|
||||
// Element represents a 256-bit BN254 scalar field element.
|
||||
type Element [ElementSize]byte
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available.
|
||||
func Available() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Threshold returns the recommended batch size for GPU offload.
|
||||
func Threshold() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// HashPair computes Poseidon2 hash of two elements (2-to-1 compression).
|
||||
func HashPair(left, right Element) (Element, error) {
|
||||
return Element{}, ErrNotAvailable
|
||||
}
|
||||
|
||||
// Hash computes Poseidon2 hash of variable-length input.
|
||||
func Hash(input []Element) (Element, error) {
|
||||
return Element{}, ErrNotAvailable
|
||||
}
|
||||
|
||||
// BatchHashPair computes multiple 2-to-1 compressions in parallel.
|
||||
func BatchHashPair(pairs [][2]Element) ([]Element, error) {
|
||||
return nil, ErrNotAvailable
|
||||
}
|
||||
|
||||
// MerkleRoot computes the Merkle tree root from leaves.
|
||||
func MerkleRoot(leaves []Element) (Element, error) {
|
||||
return Element{}, ErrNotAvailable
|
||||
}
|
||||
|
||||
// MerklePath computes the Merkle proof path for a leaf.
|
||||
func MerklePath(leaves []Element, index uint32) ([]Element, error) {
|
||||
return nil, ErrNotAvailable
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated Poseidon2 hash functions via Metal.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// The C++ library handles automatic fallback to CPU when GPU is unavailable.
|
||||
// Poseidon2 is a ZK-friendly hash function designed for BN254 scalar field.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I/Users/z/work/luxcpp/crypto/include
|
||||
#cgo darwin LDFLAGS: -L/Users/z/work/luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
|
||||
#cgo linux LDFLAGS: -L/Users/z/work/luxcpp/crypto/build-local -lluxcrypto
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_poseidon2.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ElementSize is the size of a BN254 scalar field element in bytes (256-bit).
|
||||
const ElementSize = 32
|
||||
|
||||
// Element represents a 256-bit BN254 scalar field element.
|
||||
// Stored as 4 x 64-bit limbs in little-endian order.
|
||||
type Element [ElementSize]byte
|
||||
|
||||
// Threshold for GPU batch operations
|
||||
const GPUThreshold = 64
|
||||
|
||||
// context is the global Metal Poseidon2 context (initialized on first use).
|
||||
var (
|
||||
ctx *C.MetalPoseidon2Context
|
||||
ctxReady bool
|
||||
)
|
||||
|
||||
// initContext lazily initializes the Metal Poseidon2 context.
|
||||
func initContext() error {
|
||||
if ctxReady {
|
||||
return nil
|
||||
}
|
||||
ctx = C.metal_poseidon2_init()
|
||||
if ctx == nil {
|
||||
return errors.New("Metal Poseidon2 initialization failed")
|
||||
}
|
||||
ctxReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available.
|
||||
func Available() bool {
|
||||
return bool(C.metal_poseidon2_available())
|
||||
}
|
||||
|
||||
// Threshold returns the recommended batch size for GPU offload.
|
||||
func Threshold() uint32 {
|
||||
return GPUThreshold
|
||||
}
|
||||
|
||||
// HashPair computes Poseidon2 hash of two elements (2-to-1 compression).
|
||||
// This is the core operation for Merkle tree construction.
|
||||
func HashPair(left, right Element) (Element, error) {
|
||||
var out Element
|
||||
if err := initContext(); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
ret := C.metal_poseidon2_compress(
|
||||
ctx,
|
||||
C.POSEIDON2_BN254,
|
||||
unsafe.Pointer(&out[0]),
|
||||
unsafe.Pointer(&left[0]),
|
||||
unsafe.Pointer(&right[0]),
|
||||
)
|
||||
if ret != C.METAL_POSEIDON2_SUCCESS {
|
||||
return out, errors.New("Poseidon2 hash pair failed")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// BatchHashPair computes Poseidon2 hashes for multiple pairs on GPU.
|
||||
// More efficient than calling HashPair repeatedly for batch operations.
|
||||
func BatchHashPair(left, right []Element) ([]Element, error) {
|
||||
n := len(left)
|
||||
if n == 0 || n != len(right) {
|
||||
return nil, errors.New("invalid input lengths")
|
||||
}
|
||||
if err := initContext(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outputs := make([]Element, n)
|
||||
|
||||
// GPU batch processing
|
||||
ret := C.metal_poseidon2_batch_compress(
|
||||
ctx,
|
||||
C.POSEIDON2_BN254,
|
||||
unsafe.Pointer(&outputs[0]),
|
||||
unsafe.Pointer(&left[0]),
|
||||
unsafe.Pointer(&right[0]),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
if ret != C.METAL_POSEIDON2_SUCCESS {
|
||||
return nil, errors.New("Poseidon2 batch hash pair failed")
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
// MerkleLayer computes one layer of a Merkle tree from leaf pairs.
|
||||
// Input size must be even. Output size is input size / 2.
|
||||
func MerkleLayer(current []Element) ([]Element, error) {
|
||||
n := len(current)
|
||||
if n == 0 || n%2 != 0 {
|
||||
return nil, errors.New("layer size must be even")
|
||||
}
|
||||
if err := initContext(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Split into left and right halves for batch compress
|
||||
numPairs := n / 2
|
||||
lefts := make([]Element, numPairs)
|
||||
rights := make([]Element, numPairs)
|
||||
for i := 0; i < numPairs; i++ {
|
||||
lefts[i] = current[i*2]
|
||||
rights[i] = current[i*2+1]
|
||||
}
|
||||
|
||||
return BatchHashPair(lefts, rights)
|
||||
}
|
||||
|
||||
// MerkleTree builds a complete Merkle tree from leaves using GPU.
|
||||
// Returns all internal nodes (num_leaves - 1 elements) with root at index 0.
|
||||
// Number of leaves must be a power of 2.
|
||||
func MerkleTree(leaves []Element) ([]Element, error) {
|
||||
n := len(leaves)
|
||||
if n == 0 || (n&(n-1)) != 0 {
|
||||
return nil, errors.New("leaves count must be a power of 2")
|
||||
}
|
||||
if err := initContext(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Tree has n-1 internal nodes
|
||||
tree := make([]Element, n-1)
|
||||
|
||||
ret := C.metal_poseidon2_merkle_tree(
|
||||
ctx,
|
||||
C.POSEIDON2_BN254,
|
||||
unsafe.Pointer(&tree[0]),
|
||||
unsafe.Pointer(&leaves[0]),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
if ret != C.METAL_POSEIDON2_SUCCESS {
|
||||
return nil, errors.New("Poseidon2 Merkle tree failed")
|
||||
}
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// MerkleRoot computes just the Merkle root from leaves.
|
||||
func MerkleRoot(leaves []Element) (Element, error) {
|
||||
var root Element
|
||||
if err := initContext(); err != nil {
|
||||
return root, err
|
||||
}
|
||||
|
||||
ret := C.metal_poseidon2_merkle_root(
|
||||
ctx,
|
||||
C.POSEIDON2_BN254,
|
||||
unsafe.Pointer(&root[0]),
|
||||
unsafe.Pointer(&leaves[0]),
|
||||
C.uint32_t(len(leaves)),
|
||||
)
|
||||
if ret != C.METAL_POSEIDON2_SUCCESS {
|
||||
return root, errors.New("Poseidon2 Merkle root failed")
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// VerifyProof verifies a single Merkle proof.
|
||||
// Returns true if the proof is valid.
|
||||
func VerifyProof(leaf, root Element, path []Element, indices []uint32) (bool, error) {
|
||||
if len(path) != len(indices) {
|
||||
return false, errors.New("path length mismatch")
|
||||
}
|
||||
if err := initContext(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
depth := len(path)
|
||||
if depth == 0 {
|
||||
// Single node tree
|
||||
return leaf == root, nil
|
||||
}
|
||||
|
||||
ret := C.metal_poseidon2_merkle_verify(
|
||||
ctx,
|
||||
C.POSEIDON2_BN254,
|
||||
unsafe.Pointer(&root[0]),
|
||||
unsafe.Pointer(&leaf[0]),
|
||||
unsafe.Pointer(&path[0]),
|
||||
C.uint32_t(indices[0]), // leaf index
|
||||
C.uint32_t(depth),
|
||||
)
|
||||
|
||||
return ret == C.METAL_POSEIDON2_SUCCESS, nil
|
||||
}
|
||||
|
||||
// BatchVerifyProofs verifies multiple Merkle proofs in parallel on GPU.
|
||||
// Note: This implementation falls back to sequential verification as
|
||||
// the metal_poseidon2 API doesn't have a batch verify function.
|
||||
func BatchVerifyProofs(leaves, roots []Element, paths [][]Element, indices [][]uint32) ([]bool, error) {
|
||||
n := len(leaves)
|
||||
if n == 0 || n != len(roots) || n != len(paths) || n != len(indices) {
|
||||
return nil, errors.New("invalid input lengths")
|
||||
}
|
||||
|
||||
results := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
valid, err := VerifyProof(leaves[i], roots[i], paths[i], indices[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[i] = valid
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Commitment computes a Pedersen-style commitment using Poseidon2.
|
||||
// commitment = Poseidon2(value, blinding, salt)
|
||||
func Commitment(value, blinding, salt Element) (Element, error) {
|
||||
// First hash value and blinding
|
||||
temp, err := HashPair(value, blinding)
|
||||
if err != nil {
|
||||
return Element{}, err
|
||||
}
|
||||
// Then hash result with salt
|
||||
return HashPair(temp, salt)
|
||||
}
|
||||
|
||||
// BatchCommitment computes multiple commitments in parallel on GPU.
|
||||
func BatchCommitment(values, blindings, salts []Element) ([]Element, error) {
|
||||
n := len(values)
|
||||
if n == 0 || n != len(blindings) || n != len(salts) {
|
||||
return nil, errors.New("invalid input lengths")
|
||||
}
|
||||
|
||||
// First hash values and blindings
|
||||
temps, err := BatchHashPair(values, blindings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Then hash results with salts
|
||||
return BatchHashPair(temps, salts)
|
||||
}
|
||||
|
||||
// Nullifier computes a nullifier for spending a note.
|
||||
// nullifier = Poseidon2(key, commitment, index)
|
||||
func Nullifier(key, commitment, index Element) (Element, error) {
|
||||
// First hash key and commitment
|
||||
temp, err := HashPair(key, commitment)
|
||||
if err != nil {
|
||||
return Element{}, err
|
||||
}
|
||||
// Then hash result with index
|
||||
return HashPair(temp, index)
|
||||
}
|
||||
|
||||
// BatchNullifier computes multiple nullifiers in parallel on GPU.
|
||||
func BatchNullifier(keys, commitments, indices []Element) ([]Element, error) {
|
||||
n := len(keys)
|
||||
if n == 0 || n != len(commitments) || n != len(indices) {
|
||||
return nil, errors.New("invalid input lengths")
|
||||
}
|
||||
|
||||
// First hash keys and commitments
|
||||
temps, err := BatchHashPair(keys, commitments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Then hash results with indices
|
||||
return BatchHashPair(temps, indices)
|
||||
}
|
||||
|
||||
// Destroy releases the Metal Poseidon2 context.
|
||||
// Should be called when done with GPU operations.
|
||||
func Destroy() {
|
||||
if ctxReady && ctx != nil {
|
||||
C.metal_poseidon2_destroy(ctx)
|
||||
ctx = nil
|
||||
ctxReady = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2022, Lux Partners Limited All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetSHA256FromDisk(binPath string) (string, error) {
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
return "", fmt.Errorf("failed looking up plugin binary at %s: %w", binPath, err)
|
||||
}
|
||||
hasher := sha256.New()
|
||||
s, err := os.ReadFile(binPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := hasher.Write(s); err != nil {
|
||||
return "", fmt.Errorf("failed calculating the sha256 hash of the binary %s: %w", binPath, err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func SearchSHA256File(file []byte, toSearch string) (string, error) {
|
||||
lines := strings.Split(string(file), "\n")
|
||||
for _, line := range lines {
|
||||
sha256Info := strings.Fields(line)
|
||||
if len(sha256Info) == 2 {
|
||||
if sha256Info[1] == toSearch {
|
||||
return sha256Info[0], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%q not found in sha256 file", toSearch)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package consistent
|
||||
|
||||
// Hashable is an interface to be implemented by structs that need to be sharded via consistent hashing.
|
||||
type Hashable interface {
|
||||
// ConsistentHashKey is the key used to shard the blob.
|
||||
// This should be constant for a given blob.
|
||||
ConsistentHashKey() []byte
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package consistent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/google/btree"
|
||||
|
||||
"github.com/luxfi/crypto/hashing/hashing"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Ring = (*hashRing)(nil)
|
||||
_ btree.LessFunc[ringItem] = ringItem.Less
|
||||
|
||||
errEmptyRing = errors.New("ring doesn't have any members")
|
||||
)
|
||||
|
||||
// Ring is an interface for a consistent hashing ring
|
||||
// (ref: https://en.wikipedia.org/wiki/Consistent_hashing).
|
||||
//
|
||||
// Consistent hashing is a method of distributing keys across an arbitrary set
|
||||
// of destinations.
|
||||
//
|
||||
// Consider a naive approach, which uses modulo to map a key to a node to serve
|
||||
// cached requests. Let N be the number of keys and M is the amount of possible
|
||||
// hashing destinations. Here, a key would be routed to a node based on
|
||||
// h(key) % M = node assigned.
|
||||
//
|
||||
// With this approach, we can route a key to a node in O(1) time, but this
|
||||
// results in cache misses as nodes are introduced and removed, which results in
|
||||
// the keys being reshuffled across nodes, as modulo's output changes with M.
|
||||
// This approach results in O(N) keys being shuffled, which is undesirable for a
|
||||
// caching use-case.
|
||||
//
|
||||
// Consistent hashing works by hashing all keys into a circle, which can be
|
||||
// visualized as a clock. Keys are routed to a node by hashing the key, and
|
||||
// searching for the first clockwise neighbor. This requires O(N) memory to
|
||||
// maintain the state of the ring and log(N) time to route a key using
|
||||
// binary-search, but results in O(N/M) amount of keys being shuffled when a
|
||||
// node is added/removed because the addition/removal of a node results in a
|
||||
// split/merge of its counter-clockwise neighbor's hash space.
|
||||
//
|
||||
// As an example, assume we have a ring that supports hashes from 1-12.
|
||||
//
|
||||
// 12
|
||||
// 11 1
|
||||
//
|
||||
// 10 2
|
||||
//
|
||||
// 9 3
|
||||
//
|
||||
// 8 4
|
||||
//
|
||||
// 7 5
|
||||
// 6
|
||||
//
|
||||
// Add node 1 (n1). Let h(n1) = 12.
|
||||
// First, we compute the hash the node, and insert it into its corresponding
|
||||
// location on the ring.
|
||||
//
|
||||
// 12 (n1)
|
||||
// 11 1
|
||||
//
|
||||
// 10 2
|
||||
//
|
||||
// 9 3
|
||||
//
|
||||
// 8 4
|
||||
//
|
||||
// 7 5
|
||||
// 6
|
||||
//
|
||||
// Now, to see which node a key (k1) should map to, we hash the key and search
|
||||
// for its closest clockwise neighbor.
|
||||
// Let h(k1) = 3. Here, we see that since n1 is the closest neighbor, as there
|
||||
// are no other nodes in the ring.
|
||||
//
|
||||
// 12 (n1)
|
||||
// 11 1
|
||||
//
|
||||
// 10 2
|
||||
//
|
||||
// 9 3 (k1)
|
||||
//
|
||||
// 8 4
|
||||
//
|
||||
// 7 5
|
||||
// 6
|
||||
//
|
||||
// Now, let's insert another node (n2), such that h(n2) = 6.
|
||||
// Here we observe that k1 has shuffled to n2, as n2 is the closest clockwise
|
||||
// neighbor to k1.
|
||||
//
|
||||
// 12 (n1)
|
||||
// 11 1
|
||||
//
|
||||
// 10 2
|
||||
//
|
||||
// 9 3 (k1)
|
||||
//
|
||||
// 8 4
|
||||
//
|
||||
// 7 5
|
||||
// 6 (n2)
|
||||
//
|
||||
// Other optimizations can be made to help reduce blast radius of failures and
|
||||
// the variance in keys (hot shards). One such optimization is introducing
|
||||
// virtual nodes, which is to replicate a single node multiple times by salting
|
||||
// it, (e.g n1-0, n1-1...).
|
||||
//
|
||||
// Without virtualization, failures of a node cascade as each node failing
|
||||
// results in the load of the failed node being shuffled into its clockwise
|
||||
// neighbor, which can result in a sampling effect across the network.
|
||||
type Ring interface {
|
||||
RingReader
|
||||
ringMutator
|
||||
}
|
||||
|
||||
// RingReader is an interface to read values from Ring.
|
||||
type RingReader interface {
|
||||
// Get gets the closest clockwise node for a key in the ring.
|
||||
//
|
||||
// Each ring member is responsible for the hashes which fall in the range
|
||||
// between (myself, clockwise-neighbor].
|
||||
// This behavior is desirable so that we can re-use the return value of Get
|
||||
// to iterate around the ring (Ex. replication, retries, etc).
|
||||
//
|
||||
// Returns the node routed to and an error if we're unable to resolve a node
|
||||
// to map to.
|
||||
Get(Hashable) (Hashable, error)
|
||||
}
|
||||
|
||||
// ringMutator defines an interface that mutates Ring.
|
||||
type ringMutator interface {
|
||||
// Add adds a node to the ring.
|
||||
Add(Hashable)
|
||||
|
||||
// Remove removes the node from the ring.
|
||||
//
|
||||
// Returns true if the node was removed, and false if it wasn't present to
|
||||
// begin with.
|
||||
Remove(Hashable) bool
|
||||
}
|
||||
|
||||
// hashRing is an implementation of Ring
|
||||
type hashRing struct {
|
||||
// Hashing algorithm to use when hashing keys.
|
||||
hasher hashing.Hasher
|
||||
|
||||
// Replication factor for nodes; must be greater than zero.
|
||||
virtualNodes int
|
||||
|
||||
lock sync.RWMutex
|
||||
ring *btree.BTreeG[ringItem]
|
||||
}
|
||||
|
||||
// RingConfig configures settings for a Ring.
|
||||
type RingConfig struct {
|
||||
// Replication factor for nodes in the ring.
|
||||
VirtualNodes int
|
||||
// Hashing implementation to use.
|
||||
Hasher hashing.Hasher
|
||||
// Degree represents the degree of the b-tree
|
||||
Degree int
|
||||
}
|
||||
|
||||
// NewHashRing instantiates an instance of hashRing.
|
||||
func NewHashRing(config RingConfig) Ring {
|
||||
return &hashRing{
|
||||
hasher: config.Hasher,
|
||||
virtualNodes: config.VirtualNodes,
|
||||
ring: btree.NewG(config.Degree, ringItem.Less),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hashRing) Get(key Hashable) (Hashable, error) {
|
||||
h.lock.RLock()
|
||||
defer h.lock.RUnlock()
|
||||
|
||||
return h.get(key)
|
||||
}
|
||||
|
||||
func (h *hashRing) get(key Hashable) (Hashable, error) {
|
||||
// If we have no members in the ring, it's not possible to find where the
|
||||
// key belongs.
|
||||
if h.ring.Len() == 0 {
|
||||
return nil, errEmptyRing
|
||||
}
|
||||
|
||||
var (
|
||||
// Compute this key's hash
|
||||
hash = h.hasher.Hash(key.ConsistentHashKey())
|
||||
result Hashable
|
||||
)
|
||||
h.ring.AscendGreaterOrEqual(
|
||||
ringItem{
|
||||
hash: hash,
|
||||
value: key,
|
||||
},
|
||||
func(item ringItem) bool {
|
||||
if hash < item.hash {
|
||||
result = item.value
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
// If found nothing ascending the tree, we need to wrap around the ring to
|
||||
// the left-most (min) node.
|
||||
if result == nil {
|
||||
minNode, _ := h.ring.Min()
|
||||
result = minNode.value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *hashRing) Add(key Hashable) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
h.add(key)
|
||||
}
|
||||
|
||||
func (h *hashRing) add(key Hashable) {
|
||||
// Replicate the node in the ring.
|
||||
hashKey := key.ConsistentHashKey()
|
||||
for i := 0; i < h.virtualNodes; i++ {
|
||||
virtualNode := getHashKey(hashKey, i)
|
||||
virtualNodeHash := h.hasher.Hash(virtualNode)
|
||||
|
||||
// Insert it into the ring.
|
||||
h.ring.ReplaceOrInsert(ringItem{
|
||||
hash: virtualNodeHash,
|
||||
value: key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hashRing) Remove(key Hashable) bool {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
return h.remove(key)
|
||||
}
|
||||
|
||||
func (h *hashRing) remove(key Hashable) bool {
|
||||
var (
|
||||
hashKey = key.ConsistentHashKey()
|
||||
removed = false
|
||||
)
|
||||
|
||||
// We need to delete all virtual nodes created for a single node.
|
||||
for i := 0; i < h.virtualNodes; i++ {
|
||||
virtualNode := getHashKey(hashKey, i)
|
||||
virtualNodeHash := h.hasher.Hash(virtualNode)
|
||||
item := ringItem{
|
||||
hash: virtualNodeHash,
|
||||
}
|
||||
_, removed = h.ring.Delete(item)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
// getHashKey builds a key given a base key and a virtual node number.
|
||||
func getHashKey(key []byte, virtualNode int) []byte {
|
||||
return append(key, byte(virtualNode))
|
||||
}
|
||||
|
||||
// ringItem is a helper class to represent ring nodes in the b-tree.
|
||||
type ringItem struct {
|
||||
hash uint64
|
||||
value Hashable
|
||||
}
|
||||
|
||||
func (r ringItem) Less(than ringItem) bool {
|
||||
return r.hash < than.hash
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package consistent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/luxfi/crypto/hashing/hashing/hashingmock"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Hashable = (*testKey)(nil)
|
||||
|
||||
// nodes
|
||||
node1 = testKey{key: "node-1", hash: 1}
|
||||
node2 = testKey{key: "node-2", hash: 2}
|
||||
node3 = testKey{key: "node-3", hash: 3}
|
||||
)
|
||||
|
||||
// testKey is a simple wrapper around a key and its mocked hash for testing.
|
||||
type testKey struct {
|
||||
// key
|
||||
key string
|
||||
// mocked hash value of the key
|
||||
hash uint64
|
||||
}
|
||||
|
||||
func (t testKey) ConsistentHashKey() []byte {
|
||||
return []byte(t.key)
|
||||
}
|
||||
|
||||
// Tests that a key routes to its closest clockwise node.
|
||||
// Test cases are described in greater detail below; see diagrams for Ring.
|
||||
func TestGetMapsToClockwiseNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
// name of the test
|
||||
name string
|
||||
// nodes that exist in the ring
|
||||
ringNodes []testKey
|
||||
// key to try to route
|
||||
key testKey
|
||||
// expected key to be routed to
|
||||
expectedNode testKey
|
||||
}{
|
||||
{
|
||||
// If we're left of a node in the ring, we should route to it.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> foo -> node-1 -> ...
|
||||
name: "key with right node",
|
||||
ringNodes: []testKey{
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 0,
|
||||
},
|
||||
expectedNode: node1,
|
||||
},
|
||||
{
|
||||
// If we occupy the same hash as the only ring node, we should route to it.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> foo, node-1 -> ...
|
||||
name: "key with equal node",
|
||||
ringNodes: []testKey{
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 1,
|
||||
},
|
||||
expectedNode: node1,
|
||||
},
|
||||
{
|
||||
// If we're clockwise of the only node, we should wrap around and route to that node.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> foo -> ...
|
||||
name: "key wraps around to left-most node",
|
||||
ringNodes: []testKey{
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 2,
|
||||
},
|
||||
expectedNode: node1,
|
||||
},
|
||||
|
||||
{
|
||||
// If we're left of multiple nodes in the ring, we should route to the first clockwise node.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> foo -> node-1 -> node-2 -> ...
|
||||
name: "key with two right nodes",
|
||||
ringNodes: []testKey{
|
||||
node1,
|
||||
node2,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 0,
|
||||
},
|
||||
expectedNode: node1,
|
||||
},
|
||||
{
|
||||
// If we occupy the same hash as a node, we should route to the node clockwise of it.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> foo, node-1 -> node-2 -> ...
|
||||
name: "key with one equal node and one right node",
|
||||
ringNodes: []testKey{
|
||||
node2,
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 1,
|
||||
},
|
||||
expectedNode: node2,
|
||||
},
|
||||
{
|
||||
// If we're in between two nodes, we should route to the clockwise node.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> foo -> node-3 -> ...
|
||||
name: "key between two nodes",
|
||||
ringNodes: []testKey{
|
||||
node3,
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 2,
|
||||
},
|
||||
expectedNode: node3,
|
||||
},
|
||||
{
|
||||
// If we're clockwise of all ring keys, we should wrap around and route to the left-most node.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> node-2 -> foo -> ...
|
||||
name: "key with two left nodes and no right neighbors",
|
||||
ringNodes: []testKey{
|
||||
node2,
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 3,
|
||||
},
|
||||
expectedNode: node1,
|
||||
},
|
||||
{
|
||||
// If we occupy the same hash as a node, we should wrap around to the clockwise node.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> node-2, foo -> ...
|
||||
name: "key with equal neighbor and no right node wraps around to left-most node",
|
||||
ringNodes: []testKey{
|
||||
node2,
|
||||
node1,
|
||||
},
|
||||
key: testKey{
|
||||
key: "foo",
|
||||
hash: 2,
|
||||
},
|
||||
expectedNode: node1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ring, hasher := setupTest(t, 1)
|
||||
|
||||
// setup expected calls
|
||||
calls := make([]any, len(test.ringNodes)+1)
|
||||
|
||||
for i, key := range test.ringNodes {
|
||||
calls[i] = hasher.EXPECT().Hash(getHashKey(key.ConsistentHashKey(), 0)).Return(key.hash).Times(1)
|
||||
}
|
||||
|
||||
calls[len(test.ringNodes)] = hasher.EXPECT().Hash(test.key.ConsistentHashKey()).Return(test.key.hash).Times(1)
|
||||
gomock.InOrder(calls...)
|
||||
|
||||
// execute test
|
||||
for _, key := range test.ringNodes {
|
||||
ring.Add(key)
|
||||
}
|
||||
|
||||
node, err := ring.Get(test.key)
|
||||
require.NoError(err)
|
||||
require.Equal(test.expectedNode, node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that if we have an empty ring, trying to call Get results in an error, as there is no node to route to.
|
||||
func TestGetOnEmptyRingReturnsError(t *testing.T) {
|
||||
ring, _ := setupTest(t, 1)
|
||||
|
||||
foo := testKey{
|
||||
key: "foo",
|
||||
hash: 0,
|
||||
}
|
||||
_, err := ring.Get(foo)
|
||||
require.ErrorIs(t, err, errEmptyRing)
|
||||
}
|
||||
|
||||
// Tests that trying to call Remove on a node that doesn't exist should return false.
|
||||
func TestRemoveNonExistentKeyReturnsFalse(t *testing.T) {
|
||||
ring, hasher := setupTest(t, 1)
|
||||
|
||||
gomock.InOrder(
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
|
||||
)
|
||||
|
||||
// try to remove something from an empty ring.
|
||||
require.False(t, ring.Remove(node1))
|
||||
}
|
||||
|
||||
// Tests that trying to call Remove on a node that doesn't exist should return true.
|
||||
func TestRemoveExistingKeyReturnsTrue(t *testing.T) {
|
||||
ring, hasher := setupTest(t, 1)
|
||||
|
||||
gomock.InOrder(
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
|
||||
)
|
||||
|
||||
// Add a node into the ring.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> ...
|
||||
ring.Add(node1)
|
||||
|
||||
gomock.InOrder(
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
|
||||
)
|
||||
|
||||
// Try to remove it.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> (empty) -> ...
|
||||
require.True(t, ring.Remove(node1))
|
||||
}
|
||||
|
||||
// Tests that if we have a collision, the node is replaced.
|
||||
func TestAddCollisionReplacement(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ring, hasher := setupTest(t, 1)
|
||||
|
||||
foo := testKey{
|
||||
key: "foo",
|
||||
hash: 2,
|
||||
}
|
||||
|
||||
gomock.InOrder(
|
||||
// node-1 and node-2 occupy the same hash
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
|
||||
hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
|
||||
hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(uint64(1)).Times(1),
|
||||
)
|
||||
|
||||
// Ring:
|
||||
// ... -> node-1 -> ...
|
||||
ring.Add(node1)
|
||||
|
||||
// Ring:
|
||||
// ... -> node-2 -> ...
|
||||
ring.Add(node2)
|
||||
|
||||
ringMember, err := ring.Get(foo)
|
||||
require.NoError(err)
|
||||
require.Equal(node2, ringMember)
|
||||
}
|
||||
|
||||
// Tests that virtual nodes are replicated on Add.
|
||||
func TestAddVirtualNodes(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ring, hasher := setupTest(t, 3)
|
||||
|
||||
gomock.InOrder(
|
||||
// we should see 3 virtual nodes created (0, 1, 2) when we insert a node into the ring.
|
||||
|
||||
// insert node-1
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(0)).Times(1),
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 1)).Return(uint64(2)).Times(1),
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 2)).Return(uint64(4)).Times(1),
|
||||
|
||||
// insert node-2
|
||||
hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
|
||||
hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 1)).Return(uint64(3)).Times(1),
|
||||
hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 2)).Return(uint64(5)).Times(1),
|
||||
|
||||
// gets that should route to node-1
|
||||
hasher.EXPECT().Hash([]byte("foo1")).Return(uint64(1)).Times(1),
|
||||
hasher.EXPECT().Hash([]byte("foo3")).Return(uint64(3)).Times(1),
|
||||
hasher.EXPECT().Hash([]byte("foo5")).Return(uint64(5)).Times(1),
|
||||
|
||||
// gets that should route to node-2
|
||||
hasher.EXPECT().Hash([]byte("foo0")).Return(uint64(0)).Times(1),
|
||||
hasher.EXPECT().Hash([]byte("foo2")).Return(uint64(2)).Times(1),
|
||||
hasher.EXPECT().Hash([]byte("foo4")).Return(uint64(4)).Times(1),
|
||||
)
|
||||
|
||||
// Add node 1.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1-v0 -> node-1-v1 -> node-1-v2 -> ...
|
||||
ring.Add(node1)
|
||||
|
||||
// Add node 2.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1-v0 -> node-2-v0 -> node-1-v1 -> node-2-v1 -> node-1-v2 -> node-2-v2 -> ...
|
||||
ring.Add(node2)
|
||||
|
||||
// Gets that should route to node-1
|
||||
node, err := ring.Get(testKey{key: "foo1"})
|
||||
require.NoError(err)
|
||||
require.Equal(node1, node)
|
||||
node, err = ring.Get(testKey{key: "foo3"})
|
||||
require.NoError(err)
|
||||
require.Equal(node1, node)
|
||||
node, err = ring.Get(testKey{key: "foo5"})
|
||||
require.NoError(err)
|
||||
require.Equal(node1, node)
|
||||
|
||||
// Gets that should route to node-2
|
||||
node, err = ring.Get(testKey{key: "foo0"})
|
||||
require.NoError(err)
|
||||
require.Equal(node2, node)
|
||||
node, err = ring.Get(testKey{key: "foo2"})
|
||||
require.NoError(err)
|
||||
require.Equal(node2, node)
|
||||
node, err = ring.Get(testKey{key: "foo4"})
|
||||
require.NoError(err)
|
||||
require.Equal(node2, node)
|
||||
}
|
||||
|
||||
// Tests that the node routed to changes if an Add results in a key shuffle.
|
||||
func TestGetShuffleOnAdd(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ring, hasher := setupTest(t, 1)
|
||||
|
||||
foo := testKey{
|
||||
key: "foo",
|
||||
hash: 1,
|
||||
}
|
||||
|
||||
gomock.InOrder(
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(0)).Times(1),
|
||||
hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(foo.hash).Times(1),
|
||||
|
||||
hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(uint64(2)).Times(1),
|
||||
hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(foo.hash).Times(1),
|
||||
)
|
||||
|
||||
// Add node-1 into the ring
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> ...
|
||||
ring.Add(node1)
|
||||
|
||||
// node-1 is the closest clockwise node (when we wrap around), so we route to it.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> foo -> ...
|
||||
node, err := ring.Get(foo)
|
||||
require.NoError(err)
|
||||
require.Equal(node1, node)
|
||||
|
||||
// Add node-2, which results in foo being shuffled from node-1 to node-2.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> node-2 -> ...
|
||||
ring.Add(node2)
|
||||
|
||||
// Now node-2 is our closest clockwise node, so we should route to it.
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> foo -> node-2 -> ...
|
||||
node, err = ring.Get(foo)
|
||||
require.NoError(err)
|
||||
require.Equal(node2, node)
|
||||
}
|
||||
|
||||
// Tests that we can iterate around the ring.
|
||||
func TestIteration(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ring, hasher := setupTest(t, 1)
|
||||
|
||||
foo := testKey{
|
||||
key: "foo",
|
||||
hash: 0,
|
||||
}
|
||||
|
||||
gomock.InOrder(
|
||||
hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(node1.hash).Times(1),
|
||||
hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(node2.hash).Times(1),
|
||||
|
||||
hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(foo.hash).Times(1),
|
||||
hasher.EXPECT().Hash(node1.ConsistentHashKey()).Return(node1.hash).Times(1),
|
||||
)
|
||||
|
||||
// add node-1 into the ring
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> ...
|
||||
ring.Add(node1)
|
||||
|
||||
// add node-2 into the ring
|
||||
//
|
||||
// Ring:
|
||||
// ... -> node-1 -> node-2 -> ...
|
||||
ring.Add(node2)
|
||||
|
||||
// Get the neighbor of foo
|
||||
//
|
||||
// Ring:
|
||||
// ... -> foo -> node-1 -> node-2 -> ...
|
||||
node, err := ring.Get(foo)
|
||||
require.NoError(err)
|
||||
require.Equal(node1, node)
|
||||
|
||||
// iterate by re-using node-1 to get node-2
|
||||
node, err = ring.Get(node)
|
||||
require.NoError(err)
|
||||
require.Equal(node2, node)
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T, virtualNodes int) (Ring, *hashingmock.Hasher) {
|
||||
ctrl := gomock.NewController(t)
|
||||
hasher := hashingmock.NewHasher(ctrl)
|
||||
|
||||
return NewHashRing(RingConfig{
|
||||
VirtualNodes: virtualNodes,
|
||||
Hasher: hasher,
|
||||
Degree: 2,
|
||||
}), hasher
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package hashing
|
||||
|
||||
// Hasher is an interface to compute a hash value.
|
||||
type Hasher interface {
|
||||
// Hash takes a string and computes its hash value.
|
||||
// Values must be computed deterministically.
|
||||
Hash([]byte) uint64
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package hashing
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
// This file generates addresses from public keys with ripemd160. Though ripemd160 is not
|
||||
// generally recommended for use, the small size of the public key input is considered harder to
|
||||
// attack than larger payloads.
|
||||
//
|
||||
// Bitcoin similarly uses ripemd160 to generate addresses from public keys.
|
||||
//
|
||||
// Reference: https://online.tugraz.at/tug_online/voe_main2.getvolltext?pCurrPk=17675
|
||||
"golang.org/x/crypto/ripemd160" //nolint:gosec
|
||||
)
|
||||
|
||||
const (
|
||||
HashLen = sha256.Size
|
||||
AddrLen = ripemd160.Size
|
||||
)
|
||||
|
||||
var ErrInvalidHashLen = errors.New("invalid hash length")
|
||||
|
||||
// Hash256 A 256 bit long hash value.
|
||||
type Hash256 = [HashLen]byte
|
||||
|
||||
// Hash160 A 160 bit long hash value.
|
||||
type Hash160 = [ripemd160.Size]byte
|
||||
|
||||
// ComputeHash256Array computes a cryptographically strong 256 bit hash of the
|
||||
// input byte slice.
|
||||
func ComputeHash256Array(buf []byte) Hash256 {
|
||||
return sha256.Sum256(buf)
|
||||
}
|
||||
|
||||
// ComputeHash256 computes a cryptographically strong 256 bit hash of the input
|
||||
// byte slice.
|
||||
func ComputeHash256(buf []byte) []byte {
|
||||
arr := ComputeHash256Array(buf)
|
||||
return arr[:]
|
||||
}
|
||||
|
||||
// ComputeHash160Array computes a cryptographically strong 160 bit hash of the
|
||||
// input byte slice.
|
||||
func ComputeHash160Array(buf []byte) Hash160 {
|
||||
h, err := ToHash160(ComputeHash160(buf))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// ComputeHash160 computes a cryptographically strong 160 bit hash of the input
|
||||
// byte slice.
|
||||
func ComputeHash160(buf []byte) []byte {
|
||||
// See the comment on the ripemd160 import as to why the risk of use is
|
||||
// considered acceptable.
|
||||
ripe := ripemd160.New() //nolint:gosec
|
||||
_, err := io.Writer(ripe).Write(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ripe.Sum(nil)
|
||||
}
|
||||
|
||||
// Checksum creates a checksum of [length] bytes from the 256 bit hash of the
|
||||
// byte slice.
|
||||
//
|
||||
// Returns: the lower [length] bytes of the hash
|
||||
// Panics if length > 32.
|
||||
func Checksum(bytes []byte, length int) []byte {
|
||||
hash := ComputeHash256Array(bytes)
|
||||
return hash[len(hash)-length:]
|
||||
}
|
||||
|
||||
func ToHash256(bytes []byte) (Hash256, error) {
|
||||
hash := Hash256{}
|
||||
if bytesLen := len(bytes); bytesLen != HashLen {
|
||||
return hash, fmt.Errorf("%w: expected 32 bytes but got %d", ErrInvalidHashLen, bytesLen)
|
||||
}
|
||||
copy(hash[:], bytes)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func ToHash160(bytes []byte) (Hash160, error) {
|
||||
hash := Hash160{}
|
||||
if bytesLen := len(bytes); bytesLen != ripemd160.Size {
|
||||
return hash, fmt.Errorf("%w: expected 20 bytes but got %d", ErrInvalidHashLen, bytesLen)
|
||||
}
|
||||
copy(hash[:], bytes)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// PubkeyBytesToAddress converts a public key to an address using
|
||||
// Bitcoin-style SHA256+RIPEMD160 hash.
|
||||
func PubkeyBytesToAddress(key []byte) []byte {
|
||||
return ComputeHash160(ComputeHash256(key))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/luxfi/crypto/hashing/hashing (interfaces: Hasher)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package=hashingmock -destination=hashingmock/hasher.go -mock_names=Hasher=Hasher . Hasher
|
||||
//
|
||||
|
||||
// Package hashingmock is a generated GoMock package.
|
||||
package hashingmock
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// Hasher is a mock of Hasher interface.
|
||||
type Hasher struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *HasherMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// HasherMockRecorder is the mock recorder for Hasher.
|
||||
type HasherMockRecorder struct {
|
||||
mock *Hasher
|
||||
}
|
||||
|
||||
// NewHasher creates a new mock instance.
|
||||
func NewHasher(ctrl *gomock.Controller) *Hasher {
|
||||
mock := &Hasher{ctrl: ctrl}
|
||||
mock.recorder = &HasherMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *Hasher) EXPECT() *HasherMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Hash mocks base method.
|
||||
func (m *Hasher) Hash(arg0 []byte) uint64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Hash", arg0)
|
||||
ret0, _ := ret[0].(uint64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Hash indicates an expected call of Hash.
|
||||
func (mr *HasherMockRecorder) Hash(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*Hasher)(nil).Hash), arg0)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/luxfi/crypto/hashing/hashing (interfaces: Hasher)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package=hashing -destination=utils/hashing/mock_hasher.go github.com/luxfi/crypto/hashing/hashing Hasher
|
||||
//
|
||||
|
||||
// Package hashing is a generated GoMock package.
|
||||
package hashing
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/luxfi/mock/gomock"
|
||||
)
|
||||
|
||||
// MockHasher is a mock of Hasher interface.
|
||||
type MockHasher struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockHasherMockRecorder
|
||||
}
|
||||
|
||||
// MockHasherMockRecorder is the mock recorder for MockHasher.
|
||||
type MockHasherMockRecorder struct {
|
||||
mock *MockHasher
|
||||
}
|
||||
|
||||
// NewMockHasher creates a new mock instance.
|
||||
func NewMockHasher(ctrl *gomock.Controller) *MockHasher {
|
||||
mock := &MockHasher{ctrl: ctrl}
|
||||
mock.recorder = &MockHasherMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockHasher) EXPECT() *MockHasherMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Hash mocks base method.
|
||||
func (m *MockHasher) Hash(arg0 []byte) uint64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Hash", arg0)
|
||||
ret0, _ := ret[0].(uint64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Hash indicates an expected call of Hash.
|
||||
func (mr *MockHasherMockRecorder) Hash(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*MockHasher)(nil).Hash), arg0)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package hashing
|
||||
|
||||
//go:generate go run go.uber.org/mock/mockgen -package=${GOPACKAGE}mock -destination=${GOPACKAGE}mock/hasher.go -mock_names=Hasher=Hasher . Hasher
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated IPA/Verkle operations.
|
||||
// This is the pure Go fallback when CGO is not available.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/crypto/ipa/banderwagon"
|
||||
"github.com/luxfi/crypto/ipa/bandersnatch/fr"
|
||||
)
|
||||
|
||||
// Thresholds for batch operations (same as CGO version for API compatibility)
|
||||
const (
|
||||
MSMThreshold = 64
|
||||
PedersenThreshold = 32
|
||||
IPAVerifyThreshold = 16
|
||||
)
|
||||
|
||||
// Available returns false when CGO is not available.
|
||||
func Available() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MSM performs multi-scalar multiplication using pure Go.
|
||||
// result = sum_i (scalars[i] * points[i])
|
||||
func MSM(points []banderwagon.Element, scalars []fr.Element) (*banderwagon.Element, error) {
|
||||
n := len(points)
|
||||
if n == 0 || n != len(scalars) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
var result banderwagon.Element
|
||||
_, err := result.MultiExp(points, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// BatchMSM performs multiple MSM operations sequentially using pure Go.
|
||||
func BatchMSM(pointSets [][]banderwagon.Element, scalarSets [][]fr.Element) ([]*banderwagon.Element, error) {
|
||||
n := len(pointSets)
|
||||
if n == 0 || n != len(scalarSets) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
results := make([]*banderwagon.Element, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result, err := MSM(pointSets[i], scalarSets[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[i] = result
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// PedersenCommit computes a Pedersen commitment using pure Go.
|
||||
// commitment = sum_i (values[i] * basis[i])
|
||||
func PedersenCommit(basis []banderwagon.Element, values []fr.Element) (*banderwagon.Element, error) {
|
||||
return MSM(basis, values)
|
||||
}
|
||||
|
||||
// BatchPedersenCommit computes multiple Pedersen commitments sequentially.
|
||||
func BatchPedersenCommit(basis []banderwagon.Element, valueSets [][]fr.Element) ([]*banderwagon.Element, error) {
|
||||
n := len(valueSets)
|
||||
if n == 0 {
|
||||
return nil, errors.New("empty value sets")
|
||||
}
|
||||
|
||||
pointSets := make([][]banderwagon.Element, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if len(valueSets[i]) > len(basis) {
|
||||
return nil, errors.New("value set larger than basis")
|
||||
}
|
||||
pointSets[i] = basis[:len(valueSets[i])]
|
||||
}
|
||||
|
||||
return BatchMSM(pointSets, valueSets)
|
||||
}
|
||||
|
||||
// VerkleCommitNode computes a Verkle tree internal node commitment.
|
||||
// Uses width-256 Pedersen commitment optimized for Verkle trees.
|
||||
func VerkleCommitNode(children []banderwagon.Element) (*banderwagon.Element, error) {
|
||||
if len(children) != 256 {
|
||||
return nil, errors.New("Verkle node requires exactly 256 children")
|
||||
}
|
||||
|
||||
scalars := make([]fr.Element, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne()
|
||||
}
|
||||
|
||||
var result banderwagon.Element
|
||||
_, err := result.MultiExp(children, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// Destroy is a no-op in the pure Go implementation.
|
||||
func Destroy() {}
|
||||
@@ -1,344 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated IPA/Verkle operations via Metal.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// Used for Verkle witness generation and verification.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo pkg-config: lux-crypto-only
|
||||
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
|
||||
#cgo linux LDFLAGS:
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_ipa.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/crypto/ipa/banderwagon"
|
||||
"github.com/luxfi/crypto/ipa/bandersnatch/fr"
|
||||
)
|
||||
|
||||
// Thresholds for GPU batch operations
|
||||
const (
|
||||
MSMThreshold = 64 // Min points for GPU MSM
|
||||
PedersenThreshold = 32 // Min values for GPU Pedersen commit
|
||||
IPAVerifyThreshold = 16 // Min proofs for GPU IPA verify
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *C.MetalIPAContext
|
||||
ctxReady bool
|
||||
)
|
||||
|
||||
// initContext lazily initializes the Metal IPA context.
|
||||
func initContext() error {
|
||||
if ctxReady {
|
||||
return nil
|
||||
}
|
||||
ctx = C.metal_ipa_init()
|
||||
if ctx == nil {
|
||||
return errors.New("Metal IPA initialization failed")
|
||||
}
|
||||
ctxReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available for IPA.
|
||||
func Available() bool {
|
||||
return bool(C.metal_ipa_available())
|
||||
}
|
||||
|
||||
// bytesToScalar converts 32-byte big-endian to C.BanderwagonScalar (4 x uint64 limbs, little-endian).
|
||||
func bytesToScalar(b []byte) C.BanderwagonScalar {
|
||||
var s C.BanderwagonScalar
|
||||
// Convert big-endian bytes to little-endian limbs
|
||||
// b[0:8] -> limbs[3], b[8:16] -> limbs[2], b[16:24] -> limbs[1], b[24:32] -> limbs[0]
|
||||
s.limbs[0] = C.uint64_t(binary.BigEndian.Uint64(b[24:32]))
|
||||
s.limbs[1] = C.uint64_t(binary.BigEndian.Uint64(b[16:24]))
|
||||
s.limbs[2] = C.uint64_t(binary.BigEndian.Uint64(b[8:16]))
|
||||
s.limbs[3] = C.uint64_t(binary.BigEndian.Uint64(b[0:8]))
|
||||
return s
|
||||
}
|
||||
|
||||
// pointToAffine converts banderwagon.Element to C.BanderwagonAffine using serialization.
|
||||
func pointToAffine(p *banderwagon.Element) C.BanderwagonAffine {
|
||||
var affine C.BanderwagonAffine
|
||||
bytes := p.Bytes()
|
||||
// Use deserialize to get proper affine coordinates
|
||||
C.metal_ipa_point_deserialize(&affine, (*C.uint8_t)(unsafe.Pointer(&bytes[0])))
|
||||
return affine
|
||||
}
|
||||
|
||||
// affineToPoint converts C.BanderwagonAffine to banderwagon.Element using serialization.
|
||||
func affineToPoint(affine *C.BanderwagonAffine) (*banderwagon.Element, error) {
|
||||
var bytes [32]byte
|
||||
C.metal_ipa_point_serialize((*C.uint8_t)(unsafe.Pointer(&bytes[0])), affine)
|
||||
var result banderwagon.Element
|
||||
if err := result.SetBytes(bytes[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// MSM performs multi-scalar multiplication on Banderwagon points using GPU.
|
||||
// result = sum_i (scalars[i] * points[i])
|
||||
func MSM(points []banderwagon.Element, scalars []fr.Element) (*banderwagon.Element, error) {
|
||||
n := len(points)
|
||||
if n == 0 || n != len(scalars) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
// Fall back to CPU for small batches
|
||||
if n < MSMThreshold || !Available() {
|
||||
var result banderwagon.Element
|
||||
_, err := result.MultiExp(points, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
var result banderwagon.Element
|
||||
_, err := result.MultiExp(points, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// Convert scalars to C format
|
||||
cScalars := make([]C.BanderwagonScalar, n)
|
||||
for i := 0; i < n; i++ {
|
||||
bytes := scalars[i].Bytes()
|
||||
cScalars[i] = bytesToScalar(bytes[:])
|
||||
}
|
||||
|
||||
// Convert points to C format
|
||||
cPoints := make([]C.BanderwagonAffine, n)
|
||||
for i := 0; i < n; i++ {
|
||||
cPoints[i] = pointToAffine(&points[i])
|
||||
}
|
||||
|
||||
// Allocate result (API uses BanderwagonAffine for result)
|
||||
var cResult C.BanderwagonAffine
|
||||
|
||||
ret := C.metal_ipa_msm(
|
||||
ctx,
|
||||
&cResult,
|
||||
(*C.BanderwagonScalar)(unsafe.Pointer(&cScalars[0])),
|
||||
(*C.BanderwagonAffine)(unsafe.Pointer(&cPoints[0])),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_IPA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
var result banderwagon.Element
|
||||
_, err := result.MultiExp(points, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
return affineToPoint(&cResult)
|
||||
}
|
||||
|
||||
// BatchMSM performs multiple MSM operations in parallel on GPU.
|
||||
func BatchMSM(pointSets [][]banderwagon.Element, scalarSets [][]fr.Element) ([]*banderwagon.Element, error) {
|
||||
n := len(pointSets)
|
||||
if n == 0 || n != len(scalarSets) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
results := make([]*banderwagon.Element, n)
|
||||
|
||||
// For small batches, use sequential MSM
|
||||
if n < 4 || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
result, err := MSM(pointSets[i], scalarSets[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[i] = result
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Use batch GPU MSM
|
||||
if err := initContext(); err != nil {
|
||||
for i := 0; i < n; i++ {
|
||||
result, err := MSM(pointSets[i], scalarSets[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[i] = result
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Determine vector size (all must be same size for batch_msm)
|
||||
vectorSize := len(pointSets[0])
|
||||
for i := 1; i < n; i++ {
|
||||
if len(pointSets[i]) != vectorSize || len(scalarSets[i]) != vectorSize {
|
||||
// Fall back to sequential if sizes differ
|
||||
for j := 0; j < n; j++ {
|
||||
result, err := MSM(pointSets[j], scalarSets[j])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[j] = result
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten all scalars
|
||||
allScalars := make([]C.BanderwagonScalar, n*vectorSize)
|
||||
for i := 0; i < n; i++ {
|
||||
for j := 0; j < vectorSize; j++ {
|
||||
bytes := scalarSets[i][j].Bytes()
|
||||
allScalars[i*vectorSize+j] = bytesToScalar(bytes[:])
|
||||
}
|
||||
}
|
||||
|
||||
// Convert shared basis points
|
||||
cPoints := make([]C.BanderwagonAffine, vectorSize)
|
||||
for j := 0; j < vectorSize; j++ {
|
||||
cPoints[j] = pointToAffine(&pointSets[0][j])
|
||||
}
|
||||
|
||||
// Allocate results
|
||||
cResults := make([]C.BanderwagonAffine, n)
|
||||
|
||||
ret := C.metal_ipa_batch_msm(
|
||||
ctx,
|
||||
(*C.BanderwagonAffine)(unsafe.Pointer(&cResults[0])),
|
||||
(*C.BanderwagonScalar)(unsafe.Pointer(&allScalars[0])),
|
||||
(*C.BanderwagonAffine)(unsafe.Pointer(&cPoints[0])),
|
||||
C.uint32_t(n),
|
||||
C.uint32_t(vectorSize),
|
||||
)
|
||||
|
||||
if ret != C.METAL_IPA_SUCCESS {
|
||||
// Fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
result, err := MSM(pointSets[i], scalarSets[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[i] = result
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Convert results
|
||||
for i := 0; i < n; i++ {
|
||||
result, err := affineToPoint(&cResults[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[i] = result
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// PedersenCommit computes a Pedersen commitment using GPU.
|
||||
// commitment = sum_i (values[i] * basis[i])
|
||||
func PedersenCommit(basis []banderwagon.Element, values []fr.Element) (*banderwagon.Element, error) {
|
||||
return MSM(basis, values)
|
||||
}
|
||||
|
||||
// BatchPedersenCommit computes multiple Pedersen commitments in parallel.
|
||||
func BatchPedersenCommit(basis []banderwagon.Element, valueSets [][]fr.Element) ([]*banderwagon.Element, error) {
|
||||
n := len(valueSets)
|
||||
if n == 0 {
|
||||
return nil, errors.New("empty value sets")
|
||||
}
|
||||
|
||||
// All commitments use the same basis
|
||||
pointSets := make([][]banderwagon.Element, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if len(valueSets[i]) > len(basis) {
|
||||
return nil, errors.New("value set larger than basis")
|
||||
}
|
||||
pointSets[i] = basis[:len(valueSets[i])]
|
||||
}
|
||||
|
||||
return BatchMSM(pointSets, valueSets)
|
||||
}
|
||||
|
||||
// VerkleCommitNode computes a Verkle tree internal node commitment.
|
||||
// Uses width-256 Pedersen commitment optimized for Verkle trees.
|
||||
func VerkleCommitNode(children []banderwagon.Element, stem []byte) (*banderwagon.Element, error) {
|
||||
if len(children) != 256 {
|
||||
return nil, errors.New("Verkle node requires exactly 256 children")
|
||||
}
|
||||
|
||||
if !Available() {
|
||||
// Fall back to CPU
|
||||
var result banderwagon.Element
|
||||
scalars := make([]fr.Element, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne() // Identity scalar for direct sum
|
||||
}
|
||||
_, err := result.MultiExp(children, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
var result banderwagon.Element
|
||||
scalars := make([]fr.Element, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne()
|
||||
}
|
||||
_, err := result.MultiExp(children, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// Convert children to C format
|
||||
cChildren := make([]C.BanderwagonAffine, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
cChildren[i] = pointToAffine(&children[i])
|
||||
}
|
||||
|
||||
// Prepare stem (pad to 32 bytes if needed)
|
||||
var stemBytes [32]byte
|
||||
if len(stem) > 0 {
|
||||
copy(stemBytes[:], stem)
|
||||
}
|
||||
|
||||
var cResult C.BanderwagonAffine
|
||||
|
||||
ret := C.metal_verkle_commit_node(
|
||||
ctx,
|
||||
&cResult,
|
||||
(*C.BanderwagonAffine)(unsafe.Pointer(&cChildren[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&stemBytes[0])),
|
||||
)
|
||||
|
||||
if ret != C.METAL_IPA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
var result banderwagon.Element
|
||||
scalars := make([]fr.Element, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne()
|
||||
}
|
||||
_, err := result.MultiExp(children, scalars, banderwagon.MultiExpConfig{NbTasks: 4})
|
||||
return &result, err
|
||||
}
|
||||
|
||||
return affineToPoint(&cResult)
|
||||
}
|
||||
|
||||
// Destroy releases the Metal IPA context.
|
||||
func Destroy() {
|
||||
if ctxReady && ctx != nil {
|
||||
C.metal_ipa_destroy(ctx)
|
||||
ctx = nil
|
||||
ctxReady = false
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides ML-DSA operations with CPU fallback when CGO is disabled.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
const (
|
||||
BatchSignThreshold = 16
|
||||
BatchVerifyThreshold = 32
|
||||
)
|
||||
|
||||
// Available returns false when CGO is disabled.
|
||||
func Available() bool { return false }
|
||||
|
||||
// Threshold returns the batch threshold.
|
||||
func Threshold() int { return BatchVerifyThreshold }
|
||||
|
||||
// KeyGen generates a key pair using pure Go.
|
||||
func KeyGen(mode mldsa.Mode, seed []byte) (*mldsa.PrivateKey, error) {
|
||||
return mldsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
// BatchSign signs messages using pure Go (no GPU acceleration).
|
||||
func BatchSign(priv *mldsa.PrivateKey, messages [][]byte) ([][]byte, error) {
|
||||
if priv == nil {
|
||||
return nil, errors.New("nil private key")
|
||||
}
|
||||
sigs := make([][]byte, len(messages))
|
||||
for i, msg := range messages {
|
||||
sig, err := priv.Sign(nil, msg, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigs[i] = sig
|
||||
}
|
||||
return sigs, nil
|
||||
}
|
||||
|
||||
// BatchVerify verifies signatures using pure Go (no GPU acceleration).
|
||||
// Matches CGO signature: pks []*PublicKey, sigs [][]byte, msgs [][]byte
|
||||
func BatchVerify(pks []*mldsa.PublicKey, sigs [][]byte, msgs [][]byte) ([]bool, error) {
|
||||
n := len(pks)
|
||||
if n == 0 || n != len(sigs) || n != len(msgs) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
results := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].VerifySignature(msgs[i], sigs[i])
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Destroy is a no-op for pure Go.
|
||||
func Destroy() {}
|
||||
@@ -1,369 +0,0 @@
|
||||
//go:build cgo
|
||||
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Requires luxcpp/crypto C++ library for GPU acceleration.
|
||||
// Package gpu provides GPU-accelerated ML-DSA operations via Metal/CUDA.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// The C++ library handles automatic fallback to CPU when GPU is unavailable.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo pkg-config: lux-crypto-only
|
||||
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
|
||||
#cgo linux LDFLAGS:
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_mldsa.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
// Thresholds for GPU batch operations
|
||||
const (
|
||||
BatchSignThreshold = 16 // Min signatures for GPU batch sign
|
||||
BatchVerifyThreshold = 32 // Min signatures for GPU batch verify
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *C.MetalMLDSAContext
|
||||
ctxMu sync.Mutex
|
||||
ctxReady bool
|
||||
)
|
||||
|
||||
// initContext lazily initializes the Metal ML-DSA context.
|
||||
func initContext() error {
|
||||
ctxMu.Lock()
|
||||
defer ctxMu.Unlock()
|
||||
|
||||
if ctxReady {
|
||||
return nil
|
||||
}
|
||||
ctx = C.metal_mldsa_init()
|
||||
if ctx == nil {
|
||||
return errors.New("Metal ML-DSA initialization failed")
|
||||
}
|
||||
ctxReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available for ML-DSA.
|
||||
func Available() bool {
|
||||
return bool(C.metal_mldsa_available())
|
||||
}
|
||||
|
||||
// Threshold returns the minimum batch size for GPU acceleration benefit.
|
||||
func Threshold() int {
|
||||
return BatchVerifyThreshold
|
||||
}
|
||||
|
||||
// modeToC converts Go Mode to C MLDSAMode
|
||||
func modeToC(mode mldsa.Mode) C.MLDSAMode {
|
||||
switch mode {
|
||||
case mldsa.MLDSA44:
|
||||
return C.MLDSA_MODE_44
|
||||
case mldsa.MLDSA65:
|
||||
return C.MLDSA_MODE_65
|
||||
case mldsa.MLDSA87:
|
||||
return C.MLDSA_MODE_87
|
||||
default:
|
||||
return C.MLDSA_MODE_65
|
||||
}
|
||||
}
|
||||
|
||||
// getPrivKeyMode extracts mode from private key bytes
|
||||
func getPrivKeyMode(priv *mldsa.PrivateKey) mldsa.Mode {
|
||||
if priv == nil {
|
||||
return mldsa.MLDSA65
|
||||
}
|
||||
privBytes := priv.Bytes()
|
||||
switch len(privBytes) {
|
||||
case mldsa.MLDSA44PrivateKeySize:
|
||||
return mldsa.MLDSA44
|
||||
case mldsa.MLDSA65PrivateKeySize:
|
||||
return mldsa.MLDSA65
|
||||
case mldsa.MLDSA87PrivateKeySize:
|
||||
return mldsa.MLDSA87
|
||||
default:
|
||||
return mldsa.MLDSA65
|
||||
}
|
||||
}
|
||||
|
||||
// getPubKeyMode extracts mode from public key bytes
|
||||
func getPubKeyMode(pub *mldsa.PublicKey) mldsa.Mode {
|
||||
if pub == nil {
|
||||
return mldsa.MLDSA65
|
||||
}
|
||||
pubBytes := pub.Bytes()
|
||||
switch len(pubBytes) {
|
||||
case mldsa.MLDSA44PublicKeySize:
|
||||
return mldsa.MLDSA44
|
||||
case mldsa.MLDSA65PublicKeySize:
|
||||
return mldsa.MLDSA65
|
||||
case mldsa.MLDSA87PublicKeySize:
|
||||
return mldsa.MLDSA87
|
||||
default:
|
||||
return mldsa.MLDSA65
|
||||
}
|
||||
}
|
||||
|
||||
// KeyGen generates an ML-DSA key pair using GPU acceleration.
|
||||
// Falls back to CPU if GPU is unavailable.
|
||||
func KeyGen(mode mldsa.Mode, seed []byte) (*mldsa.PrivateKey, error) {
|
||||
if !Available() || len(seed) != 32 {
|
||||
// Fall back to CPU
|
||||
return mldsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
return mldsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
// Get expected sizes
|
||||
var pubSize, privSize int
|
||||
switch mode {
|
||||
case mldsa.MLDSA44:
|
||||
pubSize = mldsa.MLDSA44PublicKeySize
|
||||
privSize = mldsa.MLDSA44PrivateKeySize
|
||||
case mldsa.MLDSA65:
|
||||
pubSize = mldsa.MLDSA65PublicKeySize
|
||||
privSize = mldsa.MLDSA65PrivateKeySize
|
||||
case mldsa.MLDSA87:
|
||||
pubSize = mldsa.MLDSA87PublicKeySize
|
||||
privSize = mldsa.MLDSA87PrivateKeySize
|
||||
default:
|
||||
return nil, mldsa.ErrInvalidMode
|
||||
}
|
||||
|
||||
pubKey := make([]byte, pubSize)
|
||||
privKey := make([]byte, privSize)
|
||||
|
||||
ret := C.metal_mldsa_keygen(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(*C.uint8_t)(&pubKey[0]),
|
||||
(*C.uint8_t)(&privKey[0]),
|
||||
(*C.uint8_t)(&seed[0]),
|
||||
)
|
||||
|
||||
if ret != C.METAL_MLDSA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
return mldsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
return mldsa.PrivateKeyFromBytes(mode, privKey)
|
||||
}
|
||||
|
||||
// BatchSign signs multiple messages using GPU acceleration.
|
||||
// All messages are signed with the same private key.
|
||||
// Falls back to sequential signing if GPU is unavailable or count < threshold.
|
||||
func BatchSign(priv *mldsa.PrivateKey, messages [][]byte) ([][]byte, error) {
|
||||
n := len(messages)
|
||||
if n == 0 {
|
||||
return nil, errors.New("no messages to sign")
|
||||
}
|
||||
if priv == nil {
|
||||
return nil, errors.New("nil private key")
|
||||
}
|
||||
|
||||
// Get mode and signature size
|
||||
mode := getPrivKeyMode(priv)
|
||||
sigSize := mldsa.GetSignatureSize(mode)
|
||||
|
||||
// Allocate result slice
|
||||
signatures := make([][]byte, n)
|
||||
for i := range signatures {
|
||||
signatures[i] = make([]byte, sigSize)
|
||||
}
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchSignThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
sig, err := priv.Sign(nil, messages[i], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures[i] = sig
|
||||
}
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential on init error
|
||||
for i := 0; i < n; i++ {
|
||||
sig, err := priv.Sign(nil, messages[i], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures[i] = sig
|
||||
}
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
msgLens := make([]C.size_t, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sigPtrs[i] = (*C.uint8_t)(&signatures[i][0])
|
||||
if len(messages[i]) > 0 {
|
||||
msgPtrs[i] = (*C.uint8_t)(&messages[i][0])
|
||||
}
|
||||
msgLens[i] = C.size_t(len(messages[i]))
|
||||
}
|
||||
|
||||
privBytes := priv.Bytes()
|
||||
|
||||
ret := C.metal_mldsa_batch_sign(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(**C.uint8_t)(unsafe.Pointer(&sigPtrs[0])),
|
||||
(*C.uint8_t)(&privBytes[0]),
|
||||
(**C.uint8_t)(unsafe.Pointer(&msgPtrs[0])),
|
||||
(*C.size_t)(&msgLens[0]),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_MLDSA_SUCCESS {
|
||||
// Fall back to sequential on GPU error
|
||||
for i := 0; i < n; i++ {
|
||||
sig, err := priv.Sign(nil, messages[i], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures[i] = sig
|
||||
}
|
||||
}
|
||||
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
// BatchVerify verifies multiple ML-DSA signatures using GPU acceleration.
|
||||
// Returns a slice of booleans indicating validity of each signature.
|
||||
// Falls back to sequential verification if GPU is unavailable or count < threshold.
|
||||
func BatchVerify(pks []*mldsa.PublicKey, sigs [][]byte, msgs [][]byte) ([]bool, error) {
|
||||
n := len(pks)
|
||||
if n == 0 || n != len(sigs) || n != len(msgs) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
results := make([]bool, n)
|
||||
|
||||
// Determine mode from first public key
|
||||
if pks[0] == nil {
|
||||
return nil, errors.New("nil public key")
|
||||
}
|
||||
mode := getPubKeyMode(pks[0])
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchVerifyThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].Verify(msgs[i], sigs[i], nil)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential on init error
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].Verify(msgs[i], sigs[i], nil)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
msgLens := make([]C.size_t, n)
|
||||
intResults := make([]C.int, n)
|
||||
|
||||
// Serialize keys and signatures
|
||||
pkBytes := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
return nil, errors.New("nil public key in batch")
|
||||
}
|
||||
pkBytes[i] = pks[i].Bytes()
|
||||
|
||||
pkPtrs[i] = (*C.uint8_t)(&pkBytes[i][0])
|
||||
if len(sigs[i]) > 0 {
|
||||
sigPtrs[i] = (*C.uint8_t)(&sigs[i][0])
|
||||
}
|
||||
if len(msgs[i]) > 0 {
|
||||
msgPtrs[i] = (*C.uint8_t)(&msgs[i][0])
|
||||
}
|
||||
msgLens[i] = C.size_t(len(msgs[i]))
|
||||
}
|
||||
|
||||
ret := C.metal_mldsa_batch_verify(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(**C.uint8_t)(unsafe.Pointer(&pkPtrs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&sigPtrs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&msgPtrs[0])),
|
||||
(*C.size_t)(&msgLens[0]),
|
||||
C.uint32_t(n),
|
||||
(*C.int)(unsafe.Pointer(&intResults[0])),
|
||||
)
|
||||
|
||||
if ret < 0 {
|
||||
// GPU error, fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].Verify(msgs[i], sigs[i], nil)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Convert results
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = intResults[i] != 0
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Mode returns the current GPU mode information.
|
||||
func Mode() string {
|
||||
if Available() {
|
||||
return "Metal GPU"
|
||||
}
|
||||
return "CPU fallback"
|
||||
}
|
||||
|
||||
// Destroy releases the Metal ML-DSA context.
|
||||
// Should be called when done with GPU ML-DSA operations.
|
||||
func Destroy() {
|
||||
ctxMu.Lock()
|
||||
defer ctxMu.Unlock()
|
||||
|
||||
if ctxReady && ctx != nil {
|
||||
C.metal_mldsa_destroy(ctx)
|
||||
ctx = nil
|
||||
ctxReady = false
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides ML-KEM operations with CPU fallback when CGO is disabled.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/luxfi/crypto/mlkem"
|
||||
)
|
||||
|
||||
const (
|
||||
BatchEncapsThreshold = 16
|
||||
BatchDecapsThreshold = 16
|
||||
)
|
||||
|
||||
// Available returns false when CGO is disabled.
|
||||
func Available() bool { return false }
|
||||
|
||||
// Threshold returns the batch threshold.
|
||||
func Threshold() int { return BatchEncapsThreshold }
|
||||
|
||||
// KeyGen generates a key pair using pure Go.
|
||||
func KeyGen(mode mlkem.Mode, reader io.Reader) (*mlkem.PublicKey, *mlkem.PrivateKey, error) {
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
return mlkem.GenerateKeyPair(reader, mode)
|
||||
}
|
||||
|
||||
// BatchEncaps encapsulates shared secrets using pure Go (no GPU acceleration).
|
||||
func BatchEncaps(pks []*mlkem.PublicKey, reader io.Reader) ([][]byte, [][]byte, error) {
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
cts := make([][]byte, len(pks))
|
||||
sss := make([][]byte, len(pks))
|
||||
for i, pk := range pks {
|
||||
if pk == nil {
|
||||
return nil, nil, errors.New("nil public key")
|
||||
}
|
||||
ct, ss, err := pk.Encapsulate(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cts[i] = ct
|
||||
sss[i] = ss
|
||||
}
|
||||
return cts, sss, nil
|
||||
}
|
||||
|
||||
// BatchDecaps decapsulates shared secrets using pure Go (no GPU acceleration).
|
||||
func BatchDecaps(sk *mlkem.PrivateKey, cts [][]byte) ([][]byte, error) {
|
||||
if sk == nil {
|
||||
return nil, errors.New("nil private key")
|
||||
}
|
||||
sss := make([][]byte, len(cts))
|
||||
for i, ct := range cts {
|
||||
ss, err := sk.Decapsulate(ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sss[i] = ss
|
||||
}
|
||||
return sss, nil
|
||||
}
|
||||
|
||||
// Destroy is a no-op for pure Go.
|
||||
func Destroy() {}
|
||||
@@ -1,392 +0,0 @@
|
||||
//go:build cgo
|
||||
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Requires luxcpp/crypto C++ library for GPU acceleration.
|
||||
// Package gpu provides GPU-accelerated ML-KEM operations via Metal/CUDA.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// The C++ library handles automatic fallback to CPU when GPU is unavailable.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo pkg-config: lux-crypto-only
|
||||
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
|
||||
#cgo linux LDFLAGS:
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_mlkem.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/crypto/mlkem"
|
||||
)
|
||||
|
||||
// Thresholds for GPU batch operations
|
||||
const (
|
||||
BatchEncapsThreshold = 16 // Min encapsulations for GPU batch
|
||||
BatchDecapsThreshold = 16 // Min decapsulations for GPU batch
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *C.MetalMLKEMContext
|
||||
ctxMu sync.Mutex
|
||||
ctxReady bool
|
||||
)
|
||||
|
||||
// initContext lazily initializes the Metal ML-KEM context.
|
||||
func initContext() error {
|
||||
ctxMu.Lock()
|
||||
defer ctxMu.Unlock()
|
||||
|
||||
if ctxReady {
|
||||
return nil
|
||||
}
|
||||
ctx = C.metal_mlkem_init()
|
||||
if ctx == nil {
|
||||
return errors.New("Metal ML-KEM initialization failed")
|
||||
}
|
||||
ctxReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available for ML-KEM.
|
||||
func Available() bool {
|
||||
return bool(C.metal_mlkem_available())
|
||||
}
|
||||
|
||||
// Threshold returns the minimum batch size for GPU acceleration benefit.
|
||||
func Threshold() int {
|
||||
return BatchEncapsThreshold
|
||||
}
|
||||
|
||||
// modeToC converts Go Mode to C MLKEMMode
|
||||
func modeToC(mode mlkem.Mode) C.MLKEMMode {
|
||||
switch mode {
|
||||
case mlkem.MLKEM512:
|
||||
return C.MLKEM_MODE_512
|
||||
case mlkem.MLKEM768:
|
||||
return C.MLKEM_MODE_768
|
||||
case mlkem.MLKEM1024:
|
||||
return C.MLKEM_MODE_1024
|
||||
default:
|
||||
return C.MLKEM_MODE_768
|
||||
}
|
||||
}
|
||||
|
||||
// KeyGen generates an ML-KEM key pair using GPU acceleration.
|
||||
// Falls back to CPU if GPU is unavailable.
|
||||
func KeyGen(mode mlkem.Mode, reader io.Reader) (*mlkem.PublicKey, *mlkem.PrivateKey, error) {
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
|
||||
if !Available() {
|
||||
return mlkem.GenerateKeyPair(reader, mode)
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
return mlkem.GenerateKeyPair(reader, mode)
|
||||
}
|
||||
|
||||
// Get expected sizes
|
||||
var pubSize, privSize int
|
||||
switch mode {
|
||||
case mlkem.MLKEM512:
|
||||
pubSize = mlkem.MLKEM512PublicKeySize
|
||||
privSize = mlkem.MLKEM512PrivateKeySize
|
||||
case mlkem.MLKEM768:
|
||||
pubSize = mlkem.MLKEM768PublicKeySize
|
||||
privSize = mlkem.MLKEM768PrivateKeySize
|
||||
case mlkem.MLKEM1024:
|
||||
pubSize = mlkem.MLKEM1024PublicKeySize
|
||||
privSize = mlkem.MLKEM1024PrivateKeySize
|
||||
default:
|
||||
return nil, nil, errors.New("invalid mode")
|
||||
}
|
||||
|
||||
// Generate seed (64 bytes: d || z)
|
||||
seed := make([]byte, 64)
|
||||
if _, err := io.ReadFull(reader, seed); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pubKey := make([]byte, pubSize)
|
||||
privKey := make([]byte, privSize)
|
||||
|
||||
ret := C.metal_mlkem_keygen(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(*C.uint8_t)(&pubKey[0]),
|
||||
(*C.uint8_t)(&privKey[0]),
|
||||
(*C.uint8_t)(&seed[0]),
|
||||
)
|
||||
|
||||
if ret != C.METAL_MLKEM_SUCCESS {
|
||||
// Fall back to CPU
|
||||
return mlkem.GenerateKeyPair(reader, mode)
|
||||
}
|
||||
|
||||
pk, err := mlkem.PublicKeyFromBytes(pubKey, mode)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sk, err := mlkem.PrivateKeyFromBytes(privKey, mode)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pk, sk, nil
|
||||
}
|
||||
|
||||
// BatchEncaps performs batch encapsulation using GPU acceleration.
|
||||
// Returns ciphertexts and shared secrets for each public key.
|
||||
// Falls back to sequential encapsulation if GPU is unavailable or count < threshold.
|
||||
func BatchEncaps(pks []*mlkem.PublicKey, reader io.Reader) ([][]byte, [][]byte, error) {
|
||||
n := len(pks)
|
||||
if n == 0 {
|
||||
return nil, nil, errors.New("no public keys")
|
||||
}
|
||||
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
|
||||
// Determine mode and sizes from first public key
|
||||
if pks[0] == nil {
|
||||
return nil, nil, errors.New("nil public key")
|
||||
}
|
||||
|
||||
// Get ciphertext size based on first key
|
||||
pkBytes := pks[0].Bytes()
|
||||
var mode mlkem.Mode
|
||||
var ctSize int
|
||||
switch len(pkBytes) {
|
||||
case mlkem.MLKEM512PublicKeySize:
|
||||
mode = mlkem.MLKEM512
|
||||
ctSize = mlkem.MLKEM512CiphertextSize
|
||||
case mlkem.MLKEM768PublicKeySize:
|
||||
mode = mlkem.MLKEM768
|
||||
ctSize = mlkem.MLKEM768CiphertextSize
|
||||
case mlkem.MLKEM1024PublicKeySize:
|
||||
mode = mlkem.MLKEM1024
|
||||
ctSize = mlkem.MLKEM1024CiphertextSize
|
||||
default:
|
||||
return nil, nil, errors.New("invalid public key size")
|
||||
}
|
||||
|
||||
// Allocate results
|
||||
ciphertexts := make([][]byte, n)
|
||||
sharedSecrets := make([][]byte, n)
|
||||
for i := range ciphertexts {
|
||||
ciphertexts[i] = make([]byte, ctSize)
|
||||
sharedSecrets[i] = make([]byte, 32)
|
||||
}
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchEncapsThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
return nil, nil, errors.New("nil public key in batch")
|
||||
}
|
||||
ct, ss, err := pks[i].Encapsulate(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ciphertexts[i] = ct
|
||||
sharedSecrets[i] = ss
|
||||
}
|
||||
return ciphertexts, sharedSecrets, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
ct, ss, err := pks[i].Encapsulate(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ciphertexts[i] = ct
|
||||
sharedSecrets[i] = ss
|
||||
}
|
||||
return ciphertexts, sharedSecrets, nil
|
||||
}
|
||||
|
||||
// Generate randomness for all encapsulations
|
||||
randomness := make([]byte, n*32)
|
||||
if _, err := io.ReadFull(reader, randomness); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
ctPtrs := make([]*C.uint8_t, n)
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
pkBytesAll := make([][]byte, n)
|
||||
ssFlat := make([]byte, n*32)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
return nil, nil, errors.New("nil public key in batch")
|
||||
}
|
||||
pkBytesAll[i] = pks[i].Bytes()
|
||||
ctPtrs[i] = (*C.uint8_t)(&ciphertexts[i][0])
|
||||
pkPtrs[i] = (*C.uint8_t)(&pkBytesAll[i][0])
|
||||
}
|
||||
|
||||
ret := C.metal_mlkem_batch_encaps(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(**C.uint8_t)(unsafe.Pointer(&ctPtrs[0])),
|
||||
(*C.uint8_t)(&ssFlat[0]),
|
||||
(**C.uint8_t)(unsafe.Pointer(&pkPtrs[0])),
|
||||
(*C.uint8_t)(&randomness[0]),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_MLKEM_SUCCESS {
|
||||
// Fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
ct, ss, err := pks[i].Encapsulate(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ciphertexts[i] = ct
|
||||
sharedSecrets[i] = ss
|
||||
}
|
||||
return ciphertexts, sharedSecrets, nil
|
||||
}
|
||||
|
||||
// Extract shared secrets from flat array
|
||||
for i := 0; i < n; i++ {
|
||||
copy(sharedSecrets[i], ssFlat[i*32:(i+1)*32])
|
||||
}
|
||||
|
||||
return ciphertexts, sharedSecrets, nil
|
||||
}
|
||||
|
||||
// BatchDecaps performs batch decapsulation using GPU acceleration.
|
||||
// Returns shared secrets for each ciphertext.
|
||||
// Falls back to sequential decapsulation if GPU is unavailable or count < threshold.
|
||||
func BatchDecaps(sk *mlkem.PrivateKey, ciphertexts [][]byte) ([][]byte, error) {
|
||||
n := len(ciphertexts)
|
||||
if n == 0 {
|
||||
return nil, errors.New("no ciphertexts")
|
||||
}
|
||||
if sk == nil {
|
||||
return nil, errors.New("nil private key")
|
||||
}
|
||||
|
||||
// Allocate results
|
||||
sharedSecrets := make([][]byte, n)
|
||||
for i := range sharedSecrets {
|
||||
sharedSecrets[i] = make([]byte, 32)
|
||||
}
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchDecapsThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
ss, err := sk.Decapsulate(ciphertexts[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedSecrets[i] = ss
|
||||
}
|
||||
return sharedSecrets, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
ss, err := sk.Decapsulate(ciphertexts[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedSecrets[i] = ss
|
||||
}
|
||||
return sharedSecrets, nil
|
||||
}
|
||||
|
||||
// Get mode from private key bytes
|
||||
skBytes := sk.Bytes()
|
||||
var mode mlkem.Mode
|
||||
switch len(skBytes) {
|
||||
case mlkem.MLKEM512PrivateKeySize:
|
||||
mode = mlkem.MLKEM512
|
||||
case mlkem.MLKEM768PrivateKeySize:
|
||||
mode = mlkem.MLKEM768
|
||||
case mlkem.MLKEM1024PrivateKeySize:
|
||||
mode = mlkem.MLKEM1024
|
||||
default:
|
||||
return nil, errors.New("invalid private key size")
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
ctPtrs := make([]*C.uint8_t, n)
|
||||
ssFlat := make([]byte, n*32)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if len(ciphertexts[i]) == 0 {
|
||||
return nil, errors.New("empty ciphertext in batch")
|
||||
}
|
||||
ctPtrs[i] = (*C.uint8_t)(&ciphertexts[i][0])
|
||||
}
|
||||
|
||||
ret := C.metal_mlkem_batch_decaps(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(*C.uint8_t)(&ssFlat[0]),
|
||||
(**C.uint8_t)(unsafe.Pointer(&ctPtrs[0])),
|
||||
(*C.uint8_t)(&skBytes[0]),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_MLKEM_SUCCESS {
|
||||
// Fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
ss, err := sk.Decapsulate(ciphertexts[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedSecrets[i] = ss
|
||||
}
|
||||
return sharedSecrets, nil
|
||||
}
|
||||
|
||||
// Extract shared secrets from flat array
|
||||
for i := 0; i < n; i++ {
|
||||
copy(sharedSecrets[i], ssFlat[i*32:(i+1)*32])
|
||||
}
|
||||
|
||||
return sharedSecrets, nil
|
||||
}
|
||||
|
||||
// Mode returns the current GPU mode information.
|
||||
func Mode() string {
|
||||
if Available() {
|
||||
return "Metal GPU"
|
||||
}
|
||||
return "CPU fallback"
|
||||
}
|
||||
|
||||
// Destroy releases the Metal ML-KEM context.
|
||||
// Should be called when done with GPU ML-KEM operations.
|
||||
func Destroy() {
|
||||
ctxMu.Lock()
|
||||
defer ctxMu.Unlock()
|
||||
|
||||
if ctxReady && ctx != nil {
|
||||
C.metal_mlkem_destroy(ctx)
|
||||
ctx = nil
|
||||
ctxReady = false
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides SLH-DSA operations with CPU fallback when CGO is disabled.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/crypto/slhdsa"
|
||||
)
|
||||
|
||||
const (
|
||||
BatchSignThreshold = 8
|
||||
BatchVerifyThreshold = 16
|
||||
)
|
||||
|
||||
// Available returns false when CGO is disabled.
|
||||
func Available() bool { return false }
|
||||
|
||||
// Threshold returns the batch threshold.
|
||||
func Threshold() int { return BatchVerifyThreshold }
|
||||
|
||||
// KeyGen generates a key pair using pure Go.
|
||||
func KeyGen(mode slhdsa.Mode, seed []byte) (*slhdsa.PrivateKey, error) {
|
||||
return slhdsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
// BatchSign signs messages using pure Go (no GPU acceleration).
|
||||
func BatchSign(priv *slhdsa.PrivateKey, messages [][]byte) ([][]byte, error) {
|
||||
if priv == nil {
|
||||
return nil, errors.New("nil private key")
|
||||
}
|
||||
sigs := make([][]byte, len(messages))
|
||||
for i, msg := range messages {
|
||||
sig, err := priv.Sign(nil, msg, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigs[i] = sig
|
||||
}
|
||||
return sigs, nil
|
||||
}
|
||||
|
||||
// BatchVerify verifies signatures using pure Go (no GPU acceleration).
|
||||
// Matches CGO signature: pks []*PublicKey, sigs [][]byte, msgs [][]byte
|
||||
func BatchVerify(pks []*slhdsa.PublicKey, sigs [][]byte, msgs [][]byte) ([]bool, error) {
|
||||
n := len(pks)
|
||||
if n == 0 || n != len(sigs) || n != len(msgs) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
results := make([]bool, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].VerifySignature(msgs[i], sigs[i])
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Destroy is a no-op for pure Go.
|
||||
func Destroy() {}
|
||||
@@ -1,405 +0,0 @@
|
||||
//go:build cgo
|
||||
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Requires luxcpp/crypto C++ library for GPU acceleration.
|
||||
// Package gpu provides GPU-accelerated SLH-DSA operations via Metal/CUDA.
|
||||
// This package links to luxcpp/crypto for hardware acceleration.
|
||||
// The C++ library handles automatic fallback to CPU when GPU is unavailable.
|
||||
//
|
||||
// SLH-DSA (FIPS 205, formerly SPHINCS+) is a hash-based signature scheme.
|
||||
// GPU acceleration focuses on parallel hash tree computations.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo pkg-config: lux-crypto-only
|
||||
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
|
||||
#cgo linux LDFLAGS:
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_slhdsa.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/crypto/slhdsa"
|
||||
)
|
||||
|
||||
// Thresholds for GPU batch operations
|
||||
const (
|
||||
BatchSignThreshold = 8 // Min signatures for GPU batch sign
|
||||
BatchVerifyThreshold = 16 // Min signatures for GPU batch verify
|
||||
)
|
||||
|
||||
var (
|
||||
ctx *C.MetalSLHDSAContext
|
||||
ctxMu sync.Mutex
|
||||
ctxReady bool
|
||||
)
|
||||
|
||||
// initContext lazily initializes the Metal SLH-DSA context.
|
||||
func initContext() error {
|
||||
ctxMu.Lock()
|
||||
defer ctxMu.Unlock()
|
||||
|
||||
if ctxReady {
|
||||
return nil
|
||||
}
|
||||
ctx = C.metal_slhdsa_init()
|
||||
if ctx == nil {
|
||||
return errors.New("Metal SLH-DSA initialization failed")
|
||||
}
|
||||
ctxReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Available returns true if Metal GPU acceleration is available for SLH-DSA.
|
||||
func Available() bool {
|
||||
return bool(C.metal_slhdsa_available())
|
||||
}
|
||||
|
||||
// Threshold returns the minimum batch size for GPU acceleration benefit.
|
||||
func Threshold() int {
|
||||
return BatchVerifyThreshold
|
||||
}
|
||||
|
||||
// modeToC converts Go Mode to C SLHDSAMode
|
||||
func modeToC(mode slhdsa.Mode) C.SLHDSAMode {
|
||||
switch mode {
|
||||
case slhdsa.SHA2_128s:
|
||||
return C.SLHDSA_SHA2_128s
|
||||
case slhdsa.SHAKE_128s:
|
||||
return C.SLHDSA_SHAKE_128s
|
||||
case slhdsa.SHA2_128f:
|
||||
return C.SLHDSA_SHA2_128f
|
||||
case slhdsa.SHAKE_128f:
|
||||
return C.SLHDSA_SHAKE_128f
|
||||
case slhdsa.SHA2_192s:
|
||||
return C.SLHDSA_SHA2_192s
|
||||
case slhdsa.SHAKE_192s:
|
||||
return C.SLHDSA_SHAKE_192s
|
||||
case slhdsa.SHA2_192f:
|
||||
return C.SLHDSA_SHA2_192f
|
||||
case slhdsa.SHAKE_192f:
|
||||
return C.SLHDSA_SHAKE_192f
|
||||
case slhdsa.SHA2_256s:
|
||||
return C.SLHDSA_SHA2_256s
|
||||
case slhdsa.SHAKE_256s:
|
||||
return C.SLHDSA_SHAKE_256s
|
||||
case slhdsa.SHA2_256f:
|
||||
return C.SLHDSA_SHA2_256f
|
||||
case slhdsa.SHAKE_256f:
|
||||
return C.SLHDSA_SHAKE_256f
|
||||
default:
|
||||
return C.SLHDSA_SHA2_128s
|
||||
}
|
||||
}
|
||||
|
||||
// KeyGen generates an SLH-DSA key pair using GPU acceleration.
|
||||
// Falls back to CPU if GPU is unavailable.
|
||||
func KeyGen(mode slhdsa.Mode, seed []byte) (*slhdsa.PrivateKey, error) {
|
||||
// Determine required seed size based on security level
|
||||
var seedSize int
|
||||
switch mode {
|
||||
case slhdsa.SHA2_128s, slhdsa.SHAKE_128s, slhdsa.SHA2_128f, slhdsa.SHAKE_128f:
|
||||
seedSize = 16
|
||||
case slhdsa.SHA2_192s, slhdsa.SHAKE_192s, slhdsa.SHA2_192f, slhdsa.SHAKE_192f:
|
||||
seedSize = 24
|
||||
case slhdsa.SHA2_256s, slhdsa.SHAKE_256s, slhdsa.SHA2_256f, slhdsa.SHAKE_256f:
|
||||
seedSize = 32
|
||||
default:
|
||||
return nil, slhdsa.ErrInvalidMode
|
||||
}
|
||||
|
||||
if !Available() || len(seed) != seedSize*3 { // SLH-DSA needs 3n bytes
|
||||
// Fall back to CPU
|
||||
return slhdsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
return slhdsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
// Get expected sizes
|
||||
pubSize := slhdsa.GetPublicKeySize(mode)
|
||||
var privSize int
|
||||
switch mode {
|
||||
case slhdsa.SHA2_128s, slhdsa.SHAKE_128s, slhdsa.SHA2_128f, slhdsa.SHAKE_128f:
|
||||
privSize = 64
|
||||
case slhdsa.SHA2_192s, slhdsa.SHAKE_192s, slhdsa.SHA2_192f, slhdsa.SHAKE_192f:
|
||||
privSize = 96
|
||||
case slhdsa.SHA2_256s, slhdsa.SHAKE_256s, slhdsa.SHA2_256f, slhdsa.SHAKE_256f:
|
||||
privSize = 128
|
||||
}
|
||||
|
||||
if pubSize == 0 || privSize == 0 {
|
||||
return nil, slhdsa.ErrInvalidMode
|
||||
}
|
||||
|
||||
pubKey := make([]byte, pubSize)
|
||||
privKey := make([]byte, privSize)
|
||||
|
||||
ret := C.metal_slhdsa_keygen(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(*C.uint8_t)(&pubKey[0]),
|
||||
(*C.uint8_t)(&privKey[0]),
|
||||
(*C.uint8_t)(&seed[0]),
|
||||
)
|
||||
|
||||
if ret != C.METAL_SLHDSA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
return slhdsa.GenerateKey(nil, mode)
|
||||
}
|
||||
|
||||
return slhdsa.PrivateKeyFromBytes(mode, privKey)
|
||||
}
|
||||
|
||||
// BatchSign signs multiple messages using GPU acceleration.
|
||||
// All messages are signed with the same private key.
|
||||
// Falls back to sequential signing if GPU is unavailable or count < threshold.
|
||||
func BatchSign(priv *slhdsa.PrivateKey, messages [][]byte) ([][]byte, error) {
|
||||
n := len(messages)
|
||||
if n == 0 {
|
||||
return nil, errors.New("no messages to sign")
|
||||
}
|
||||
if priv == nil {
|
||||
return nil, errors.New("nil private key")
|
||||
}
|
||||
|
||||
// Get mode and signature size
|
||||
mode := getPrivKeyMode(priv)
|
||||
sigSize := slhdsa.GetSignatureSize(mode)
|
||||
|
||||
// Allocate result slice
|
||||
signatures := make([][]byte, n)
|
||||
for i := range signatures {
|
||||
signatures[i] = make([]byte, sigSize)
|
||||
}
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchSignThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
sig, err := priv.Sign(nil, messages[i], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures[i] = sig
|
||||
}
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential on init error
|
||||
for i := 0; i < n; i++ {
|
||||
sig, err := priv.Sign(nil, messages[i], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures[i] = sig
|
||||
}
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
msgLens := make([]C.size_t, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
sigPtrs[i] = (*C.uint8_t)(&signatures[i][0])
|
||||
if len(messages[i]) > 0 {
|
||||
msgPtrs[i] = (*C.uint8_t)(&messages[i][0])
|
||||
}
|
||||
msgLens[i] = C.size_t(len(messages[i]))
|
||||
}
|
||||
|
||||
privBytes := priv.Bytes()
|
||||
|
||||
ret := C.metal_slhdsa_batch_sign(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(**C.uint8_t)(unsafe.Pointer(&sigPtrs[0])),
|
||||
(*C.uint8_t)(&privBytes[0]),
|
||||
(**C.uint8_t)(unsafe.Pointer(&msgPtrs[0])),
|
||||
(*C.size_t)(&msgLens[0]),
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_SLHDSA_SUCCESS {
|
||||
// Fall back to sequential on GPU error
|
||||
for i := 0; i < n; i++ {
|
||||
sig, err := priv.Sign(nil, messages[i], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures[i] = sig
|
||||
}
|
||||
}
|
||||
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
// BatchVerify verifies multiple SLH-DSA signatures using GPU acceleration.
|
||||
// Returns a slice of booleans indicating validity of each signature.
|
||||
// Falls back to sequential verification if GPU is unavailable or count < threshold.
|
||||
func BatchVerify(pks []*slhdsa.PublicKey, sigs [][]byte, msgs [][]byte) ([]bool, error) {
|
||||
n := len(pks)
|
||||
if n == 0 || n != len(sigs) || n != len(msgs) {
|
||||
return nil, errors.New("mismatched input lengths")
|
||||
}
|
||||
|
||||
results := make([]bool, n)
|
||||
|
||||
// Determine mode from first public key
|
||||
if pks[0] == nil {
|
||||
return nil, errors.New("nil public key")
|
||||
}
|
||||
mode := getPubKeyMode(pks[0])
|
||||
|
||||
// Fall back to sequential for small batches or no GPU
|
||||
if n < BatchVerifyThreshold || !Available() {
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].Verify(msgs[i], sigs[i], nil)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if err := initContext(); err != nil {
|
||||
// Fall back to sequential on init error
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].Verify(msgs[i], sigs[i], nil)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Prepare C arrays
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
msgLens := make([]C.size_t, n)
|
||||
intResults := make([]C.int, n)
|
||||
|
||||
// Serialize keys and signatures
|
||||
pkBytes := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
return nil, errors.New("nil public key in batch")
|
||||
}
|
||||
pkBytes[i] = pks[i].Bytes()
|
||||
|
||||
pkPtrs[i] = (*C.uint8_t)(&pkBytes[i][0])
|
||||
if len(sigs[i]) > 0 {
|
||||
sigPtrs[i] = (*C.uint8_t)(&sigs[i][0])
|
||||
}
|
||||
if len(msgs[i]) > 0 {
|
||||
msgPtrs[i] = (*C.uint8_t)(&msgs[i][0])
|
||||
}
|
||||
msgLens[i] = C.size_t(len(msgs[i]))
|
||||
}
|
||||
|
||||
ret := C.metal_slhdsa_batch_verify(
|
||||
ctx,
|
||||
modeToC(mode),
|
||||
(**C.uint8_t)(unsafe.Pointer(&pkPtrs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&sigPtrs[0])),
|
||||
(**C.uint8_t)(unsafe.Pointer(&msgPtrs[0])),
|
||||
(*C.size_t)(&msgLens[0]),
|
||||
C.uint32_t(n),
|
||||
(*C.int)(unsafe.Pointer(&intResults[0])),
|
||||
)
|
||||
|
||||
if ret < 0 {
|
||||
// GPU error, fall back to sequential
|
||||
for i := 0; i < n; i++ {
|
||||
if pks[i] == nil {
|
||||
results[i] = false
|
||||
continue
|
||||
}
|
||||
results[i] = pks[i].Verify(msgs[i], sigs[i], nil)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Convert results
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = intResults[i] != 0
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GPUMode returns the current GPU mode information.
|
||||
func GPUMode() string {
|
||||
if Available() {
|
||||
return "Metal GPU"
|
||||
}
|
||||
return "CPU fallback"
|
||||
}
|
||||
|
||||
// Destroy releases the Metal SLH-DSA context.
|
||||
// Should be called when done with GPU SLH-DSA operations.
|
||||
func Destroy() {
|
||||
ctxMu.Lock()
|
||||
defer ctxMu.Unlock()
|
||||
|
||||
if ctxReady && ctx != nil {
|
||||
C.metal_slhdsa_destroy(ctx)
|
||||
ctx = nil
|
||||
ctxReady = false
|
||||
}
|
||||
}
|
||||
|
||||
// getPrivKeyMode extracts mode from private key bytes
|
||||
func getPrivKeyMode(priv *slhdsa.PrivateKey) slhdsa.Mode {
|
||||
if priv == nil {
|
||||
return slhdsa.SHA2_128s
|
||||
}
|
||||
// Determine mode from key size
|
||||
privBytes := priv.Bytes()
|
||||
switch len(privBytes) {
|
||||
case 64:
|
||||
return slhdsa.SHA2_128s // 128-bit security
|
||||
case 96:
|
||||
return slhdsa.SHA2_192s // 192-bit security
|
||||
case 128:
|
||||
return slhdsa.SHA2_256s // 256-bit security
|
||||
default:
|
||||
return slhdsa.SHA2_128s
|
||||
}
|
||||
}
|
||||
|
||||
// getPubKeyMode extracts mode from public key bytes
|
||||
func getPubKeyMode(pub *slhdsa.PublicKey) slhdsa.Mode {
|
||||
if pub == nil {
|
||||
return slhdsa.SHA2_128s
|
||||
}
|
||||
// Determine mode from key size
|
||||
pubBytes := pub.Bytes()
|
||||
switch len(pubBytes) {
|
||||
case 32:
|
||||
return slhdsa.SHA2_128s // 128-bit security
|
||||
case 48:
|
||||
return slhdsa.SHA2_192s // 192-bit security
|
||||
case 64:
|
||||
return slhdsa.SHA2_256s // 256-bit security
|
||||
default:
|
||||
return slhdsa.SHA2_128s
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides Verkle tree operations.
|
||||
// Pure Go implementation when CGO is not available.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/crate-crypto/go-ipa/bandersnatch/fr"
|
||||
"github.com/ethereum/go-verkle"
|
||||
)
|
||||
|
||||
// GPU acceleration thresholds (same as CGO version for API compatibility)
|
||||
const (
|
||||
MinPolyDegreeGPU = 4
|
||||
BatchCommitThreshold = 8
|
||||
)
|
||||
|
||||
// Available returns false when CGO is not available.
|
||||
func Available() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CommitToPoly performs polynomial commitment using pure Go.
|
||||
func CommitToPoly(poly []fr.Element) (*verkle.Point, error) {
|
||||
if len(poly) == 0 {
|
||||
return nil, errors.New("empty polynomial")
|
||||
}
|
||||
cfg := verkle.GetConfig()
|
||||
return cfg.CommitToPoly(poly, 0), nil
|
||||
}
|
||||
|
||||
// BatchCommitToPoly performs sequential polynomial commitments.
|
||||
func BatchCommitToPoly(polys [][]fr.Element) ([]*verkle.Point, error) {
|
||||
if len(polys) == 0 {
|
||||
return nil, errors.New("empty polynomial batch")
|
||||
}
|
||||
results := make([]*verkle.Point, len(polys))
|
||||
cfg := verkle.GetConfig()
|
||||
for i, poly := range polys {
|
||||
results[i] = cfg.CommitToPoly(poly, 0)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// VerifyProof is not available without CGO - use go-verkle directly.
|
||||
func VerifyProof(
|
||||
commitment *verkle.Point,
|
||||
proof []byte,
|
||||
point fr.Element,
|
||||
evaluation fr.Element,
|
||||
) (bool, error) {
|
||||
return false, errors.New("GPU verification not available, use go-verkle")
|
||||
}
|
||||
|
||||
// BatchVerifyProofs is not available without CGO.
|
||||
func BatchVerifyProofs(
|
||||
commitments []*verkle.Point,
|
||||
proofs [][]byte,
|
||||
points []fr.Element,
|
||||
evaluations []fr.Element,
|
||||
) ([]bool, error) {
|
||||
return nil, errors.New("GPU batch verification not available, use go-verkle")
|
||||
}
|
||||
|
||||
// Destroy is a no-op in pure Go.
|
||||
func Destroy() {}
|
||||
@@ -1,315 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated Verkle tree operations via Metal.
|
||||
// Links to luxcpp/crypto for hardware acceleration on Apple Silicon.
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I/Users/z/work/luxcpp/crypto/include
|
||||
#cgo darwin LDFLAGS: -L/Users/z/work/luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
|
||||
#cgo linux LDFLAGS: -L/Users/z/work/luxcpp/crypto/build-local -lluxcrypto
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "lux/crypto/metal_ipa.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/crate-crypto/go-ipa/bandersnatch/fr"
|
||||
"github.com/ethereum/go-verkle"
|
||||
)
|
||||
|
||||
// GPU acceleration thresholds
|
||||
const (
|
||||
// MinPolyDegreeGPU is the minimum polynomial degree for GPU acceleration
|
||||
MinPolyDegreeGPU = 4
|
||||
|
||||
// BatchCommitThreshold is minimum batch size for GPU batch commits
|
||||
BatchCommitThreshold = 8
|
||||
)
|
||||
|
||||
var (
|
||||
gpuCtx *C.MetalIPAContext
|
||||
gpuCtxOnce sync.Once
|
||||
gpuCtxErr error
|
||||
gpuCtxMu sync.Mutex
|
||||
)
|
||||
|
||||
// initGPU initializes the Metal IPA context for GPU acceleration.
|
||||
func initGPU() error {
|
||||
gpuCtxOnce.Do(func() {
|
||||
gpuCtx = C.metal_ipa_init()
|
||||
if gpuCtx == nil {
|
||||
gpuCtxErr = errors.New("failed to initialize Metal IPA context")
|
||||
}
|
||||
})
|
||||
return gpuCtxErr
|
||||
}
|
||||
|
||||
// Available returns true if GPU acceleration is available.
|
||||
func Available() bool {
|
||||
return bool(C.metal_ipa_available())
|
||||
}
|
||||
|
||||
// bytesToScalar converts 32-byte big-endian to C.BanderwagonScalar (4 x uint64 limbs, little-endian).
|
||||
func bytesToScalar(b []byte) C.BanderwagonScalar {
|
||||
var s C.BanderwagonScalar
|
||||
// Convert big-endian bytes to little-endian limbs
|
||||
s.limbs[0] = C.uint64_t(binary.BigEndian.Uint64(b[24:32]))
|
||||
s.limbs[1] = C.uint64_t(binary.BigEndian.Uint64(b[16:24]))
|
||||
s.limbs[2] = C.uint64_t(binary.BigEndian.Uint64(b[8:16]))
|
||||
s.limbs[3] = C.uint64_t(binary.BigEndian.Uint64(b[0:8]))
|
||||
return s
|
||||
}
|
||||
|
||||
// pointToAffine converts verkle.Point to C.BanderwagonAffine using serialization.
|
||||
func pointToAffine(p *verkle.Point) C.BanderwagonAffine {
|
||||
var affine C.BanderwagonAffine
|
||||
bytes := p.Bytes()
|
||||
C.metal_ipa_point_deserialize(&affine, (*C.uint8_t)(unsafe.Pointer(&bytes[0])))
|
||||
return affine
|
||||
}
|
||||
|
||||
// affineToPoint converts C.BanderwagonAffine to verkle.Point using serialization.
|
||||
func affineToPoint(affine *C.BanderwagonAffine) (*verkle.Point, error) {
|
||||
var bytes [32]byte
|
||||
C.metal_ipa_point_serialize((*C.uint8_t)(unsafe.Pointer(&bytes[0])), affine)
|
||||
point := new(verkle.Point)
|
||||
if err := point.SetBytes(bytes[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return point, nil
|
||||
}
|
||||
|
||||
// CommitToPoly performs polynomial commitment using GPU acceleration.
|
||||
// Falls back to CPU if GPU is unavailable or for small polynomials.
|
||||
func CommitToPoly(poly []fr.Element) (*verkle.Point, error) {
|
||||
n := len(poly)
|
||||
if n == 0 {
|
||||
return nil, errors.New("empty polynomial")
|
||||
}
|
||||
|
||||
// Use CPU for small polynomials
|
||||
if n < MinPolyDegreeGPU || !Available() {
|
||||
cfg := verkle.GetConfig()
|
||||
return cfg.CommitToPoly(poly, 0), nil
|
||||
}
|
||||
|
||||
if err := initGPU(); err != nil {
|
||||
cfg := verkle.GetConfig()
|
||||
return cfg.CommitToPoly(poly, 0), nil
|
||||
}
|
||||
|
||||
gpuCtxMu.Lock()
|
||||
defer gpuCtxMu.Unlock()
|
||||
|
||||
// Convert scalars to C format
|
||||
cScalars := make([]C.BanderwagonScalar, n)
|
||||
for i := 0; i < n; i++ {
|
||||
bytes := poly[i].Bytes()
|
||||
cScalars[i] = bytesToScalar(bytes[:])
|
||||
}
|
||||
|
||||
// Perform GPU commitment (no blinding for deterministic commit)
|
||||
var cResult C.BanderwagonAffine
|
||||
|
||||
ret := C.metal_ipa_pedersen_commit(
|
||||
gpuCtx,
|
||||
&cResult,
|
||||
(*C.BanderwagonScalar)(unsafe.Pointer(&cScalars[0])),
|
||||
nil, // No blinding
|
||||
C.uint32_t(n),
|
||||
)
|
||||
|
||||
if ret != C.METAL_IPA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
cfg := verkle.GetConfig()
|
||||
return cfg.CommitToPoly(poly, 0), nil
|
||||
}
|
||||
|
||||
return affineToPoint(&cResult)
|
||||
}
|
||||
|
||||
// BatchCommitToPoly performs batch polynomial commitments using GPU.
|
||||
// Much more efficient than sequential commits for multiple polynomials.
|
||||
func BatchCommitToPoly(polys [][]fr.Element) ([]*verkle.Point, error) {
|
||||
n := len(polys)
|
||||
if n == 0 {
|
||||
return nil, errors.New("empty polynomial batch")
|
||||
}
|
||||
|
||||
// Use sequential CPU for small batches
|
||||
if n < BatchCommitThreshold || !Available() {
|
||||
results := make([]*verkle.Point, n)
|
||||
cfg := verkle.GetConfig()
|
||||
for i, poly := range polys {
|
||||
results[i] = cfg.CommitToPoly(poly, 0)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if err := initGPU(); err != nil {
|
||||
results := make([]*verkle.Point, n)
|
||||
cfg := verkle.GetConfig()
|
||||
for i, poly := range polys {
|
||||
results[i] = cfg.CommitToPoly(poly, 0)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
gpuCtxMu.Lock()
|
||||
defer gpuCtxMu.Unlock()
|
||||
|
||||
// Check if all polys have same size (required for batch API)
|
||||
vectorSize := len(polys[0])
|
||||
for i := 1; i < n; i++ {
|
||||
if len(polys[i]) != vectorSize {
|
||||
// Fall back to sequential if sizes differ
|
||||
results := make([]*verkle.Point, n)
|
||||
cfg := verkle.GetConfig()
|
||||
for j, poly := range polys {
|
||||
results[j] = cfg.CommitToPoly(poly, 0)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten all scalars
|
||||
allScalars := make([]C.BanderwagonScalar, n*vectorSize)
|
||||
for i, poly := range polys {
|
||||
for j, s := range poly {
|
||||
bytes := s.Bytes()
|
||||
allScalars[i*vectorSize+j] = bytesToScalar(bytes[:])
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate results
|
||||
cResults := make([]C.BanderwagonAffine, n)
|
||||
|
||||
ret := C.metal_ipa_batch_pedersen_commit(
|
||||
gpuCtx,
|
||||
(*C.BanderwagonAffine)(unsafe.Pointer(&cResults[0])),
|
||||
(*C.BanderwagonScalar)(unsafe.Pointer(&allScalars[0])),
|
||||
nil, // No blindings
|
||||
C.uint32_t(n),
|
||||
C.uint32_t(vectorSize),
|
||||
)
|
||||
|
||||
if ret != C.METAL_IPA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
results := make([]*verkle.Point, n)
|
||||
cfg := verkle.GetConfig()
|
||||
for i, poly := range polys {
|
||||
results[i] = cfg.CommitToPoly(poly, 0)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Convert results
|
||||
results := make([]*verkle.Point, n)
|
||||
for i := 0; i < n; i++ {
|
||||
point, err := affineToPoint(&cResults[i])
|
||||
if err != nil {
|
||||
// Fall back to CPU for this one
|
||||
cfg := verkle.GetConfig()
|
||||
results[i] = cfg.CommitToPoly(polys[i], 0)
|
||||
} else {
|
||||
results[i] = point
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// VerkleCommitNode computes a Verkle tree internal node commitment.
|
||||
// Uses width-256 commitment optimized for Verkle trees.
|
||||
func VerkleCommitNode(children []*verkle.Point, stem []byte) (*verkle.Point, error) {
|
||||
if len(children) != 256 {
|
||||
return nil, errors.New("Verkle node requires exactly 256 children")
|
||||
}
|
||||
|
||||
if !Available() {
|
||||
// Fall back to CPU using verkle config
|
||||
cfg := verkle.GetConfig()
|
||||
scalars := make([]fr.Element, 256)
|
||||
points := make([]verkle.Point, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne()
|
||||
if children[i] != nil {
|
||||
points[i] = *children[i]
|
||||
}
|
||||
}
|
||||
result := cfg.CommitToPoly(scalars, 0)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if err := initGPU(); err != nil {
|
||||
cfg := verkle.GetConfig()
|
||||
scalars := make([]fr.Element, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne()
|
||||
}
|
||||
result := cfg.CommitToPoly(scalars, 0)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
gpuCtxMu.Lock()
|
||||
defer gpuCtxMu.Unlock()
|
||||
|
||||
// Convert children to C format
|
||||
cChildren := make([]C.BanderwagonAffine, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
if children[i] != nil {
|
||||
cChildren[i] = pointToAffine(children[i])
|
||||
}
|
||||
// Zero-initialized for nil children
|
||||
}
|
||||
|
||||
// Prepare stem (pad to 32 bytes if needed)
|
||||
var stemBytes [32]byte
|
||||
if len(stem) > 0 {
|
||||
copy(stemBytes[:], stem)
|
||||
}
|
||||
|
||||
var cResult C.BanderwagonAffine
|
||||
|
||||
ret := C.metal_verkle_commit_node(
|
||||
gpuCtx,
|
||||
&cResult,
|
||||
(*C.BanderwagonAffine)(unsafe.Pointer(&cChildren[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&stemBytes[0])),
|
||||
)
|
||||
|
||||
if ret != C.METAL_IPA_SUCCESS {
|
||||
// Fall back to CPU
|
||||
cfg := verkle.GetConfig()
|
||||
scalars := make([]fr.Element, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
scalars[i].SetOne()
|
||||
}
|
||||
result := cfg.CommitToPoly(scalars, 0)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return affineToPoint(&cResult)
|
||||
}
|
||||
|
||||
// Destroy releases the Metal IPA context.
|
||||
func Destroy() {
|
||||
gpuCtxMu.Lock()
|
||||
defer gpuCtxMu.Unlock()
|
||||
|
||||
if gpuCtx != nil {
|
||||
C.metal_ipa_destroy(gpuCtx)
|
||||
gpuCtx = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user