chore: sync with node requirements

This commit is contained in:
Zach Kelling
2025-12-27 04:19:14 -08:00
parent f4d92f5ef9
commit af49b0a2b6
59 changed files with 1034 additions and 8458 deletions
+13 -13
View File
@@ -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)
---
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
View File
-330
View File
@@ -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
}
}
-55
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4 -6
View File
@@ -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
)
+3 -10
View File
@@ -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=
-14
View File
@@ -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
-24
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-829
View File
@@ -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")
}
-284
View File
@@ -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")
}
-239
View File
@@ -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
-235
View File
@@ -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
}
-445
View File
@@ -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
View File
@@ -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)
}
-194
View File
@@ -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)
}
}
-442
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
})
}
}
-177
View File
@@ -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
}
-97
View File
@@ -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
}
+3 -3
View File
@@ -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
}
-56
View File
@@ -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
}
-301
View File
@@ -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
}
}
+41
View File
@@ -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)
}
+11
View File
@@ -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
}
+282
View File
@@ -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
}
+447
View File
@@ -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
}
+11
View File
@@ -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
}
+103
View File
@@ -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))
}
+54
View File
@@ -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)
}
+53
View File
@@ -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)
}
+6
View File
@@ -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
View File
@@ -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() {}
-344
View File
@@ -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
}
}
View File
-63
View File
@@ -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() {}
-369
View File
@@ -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
}
}
-71
View File
@@ -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() {}
-392
View File
@@ -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
}
}
-63
View File
@@ -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() {}
-405
View File
@@ -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
}
}
View File
View File
-71
View File
@@ -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() {}
-315
View File
@@ -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
}
}