feat: add GPU stub files for CGO-less builds

Add stub implementations for GPU packages that return
false for Available() when GPU libraries are not present.
This allows building with CGO_ENABLED=0 or CGO_ENABLED=1
without requiring Metal/CUDA libraries.

- bls/gpu/gpu_stub.go
- hash/blake3/gpu/gpu_stub.go
- hash/poseidon2/gpu/gpu_stub.go
- pqcrypto/*/gpu/gpu_stub.go (mldsa, mlkem, slhdsa)
This commit is contained in:
Zach Kelling
2025-12-26 21:22:38 -08:00
parent 7b0ce8ca27
commit 46efbf33ed
14 changed files with 3005 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo && darwin
// +build cgo,darwin
// Package blake3 provides Blake3 hash functions via luxcpp/crypto.
// This CGO version provides optimized hashing with automatic CPU/GPU selection.
// For GPU-only operations, use the blake3/gpu subpackage.
package blake3
/*
#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);
*/
import "C"
import (
"encoding/binary"
"unsafe"
)
const (
// Size256 is the standard Blake3 output size
Size256 = 32
// Size512 is extended Blake3 output size
Size512 = 64
// gpuThreshold is minimum batch size for GPU acceleration
gpuThreshold = 64
)
var (
// useGPU indicates if GPU acceleration is available
useGPU = C.crypto_gpu_available() != 0
)
// GPUAvailable returns true if GPU acceleration is available.
func GPUAvailable() bool {
return useGPU
}
// 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.
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 XOF mode.
// Implemented as XOF by hashing with counter blocks.
func Sum512(data []byte) [Size512]byte {
var out [Size512]byte
xof := sumXOF(data, Size512)
copy(out[:], xof)
return out
}
// SumXOF computes an extendable output hash of arbitrary length.
func SumXOF(data []byte, outLen int) []byte {
if outLen <= 0 {
return nil
}
return sumXOF(data, outLen)
}
// sumXOF implements XOF by chaining hash outputs with counter.
func sumXOF(data []byte, outLen int) []byte {
if outLen <= Size256 {
hash := Sum256(data)
return hash[:outLen]
}
out := make([]byte, outLen)
// First block: hash the data directly
first := Sum256(data)
copy(out[:Size256], first[:])
// Subsequent blocks: hash(data || counter)
remaining := outLen - Size256
counter := uint64(1)
offset := Size256
for remaining > 0 {
// Create input: data || counter (little-endian)
counterBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(counterBytes, counter)
input := append(data, counterBytes...)
block := Sum256(input)
toCopy := Size256
if remaining < Size256 {
toCopy = remaining
}
copy(out[offset:offset+toCopy], block[:toCopy])
offset += toCopy
remaining -= toCopy
counter++
}
return out
}
// BatchSum256 computes Blake3 hashes for multiple inputs in parallel.
// More efficient than calling Sum256 repeatedly for batch operations.
func BatchSum256(inputs [][]byte) [][Size256]byte {
n := len(inputs)
if n == 0 {
return nil
}
// For small batches, use sequential processing
if n < gpuThreshold || !useGPU {
results := make([][Size256]byte, n)
for i, data := range inputs {
results[i] = Sum256(data)
}
return results
}
// 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 Blake3.
// This is a software implementation; for GPU-accelerated version, use blake3/gpu.
func MerkleRoot(leaves [][]byte) [Size256]byte {
var out [Size256]byte
n := len(leaves)
if n == 0 {
return out
}
// Hash each leaf first
hashes := make([][Size256]byte, n)
for i, leaf := range leaves {
hashes[i] = Sum256(leaf)
}
// Build tree bottom-up
for len(hashes) > 1 {
nextLevel := make([][Size256]byte, (len(hashes)+1)/2)
for i := 0; i < len(hashes); i += 2 {
if i+1 < len(hashes) {
// Hash pair
combined := make([]byte, Size256*2)
copy(combined[:Size256], hashes[i][:])
copy(combined[Size256:], hashes[i+1][:])
nextLevel[i/2] = Sum256(combined)
} else {
// Odd node, promote to next level
nextLevel[i/2] = hashes[i]
}
}
hashes = nextLevel
}
if len(hashes) > 0 {
out = hashes[0]
}
return out
}
// DeriveKey derives a key from context and key material using Blake3 KDF.
func DeriveKey(context string, keyMaterial []byte) [Size256]byte {
// Combine context and key material with domain separation
input := append([]byte(context), 0x00)
input = append(input, keyMaterial...)
return Sum256(input)
}
+177
View File
@@ -0,0 +1,177 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build luxgpu && darwin
// 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
@@ -0,0 +1,97 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build !luxgpu
// 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
}
+302
View File
@@ -0,0 +1,302 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build luxgpu && darwin
// 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${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_zk.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
// context is the global Metal ZK context (initialized on first use).
var (
ctx *C.MetalZKContext
ctxReady bool
)
// initContext lazily initializes the Metal ZK context.
func initContext() error {
if ctxReady {
return nil
}
ctx = C.metal_zk_init()
if ctx == nil {
return errors.New("Metal ZK initialization failed")
}
ctxReady = true
return nil
}
// Available returns true if Metal GPU acceleration is available.
func Available() bool {
return bool(C.metal_zk_available())
}
// Threshold returns the recommended batch size for GPU offload.
func Threshold() uint32 {
return uint32(C.metal_zk_get_threshold(C.METAL_ZK_OP_POSEIDON2_HASH))
}
// 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
}
leftFr := (*C.Fr256)(unsafe.Pointer(&left[0]))
rightFr := (*C.Fr256)(unsafe.Pointer(&right[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&out[0]))
ret := C.metal_zk_poseidon2_hash_pair(ctx, outFr, leftFr, rightFr, 1)
if ret != C.METAL_ZK_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
leftFr := (*C.Fr256)(unsafe.Pointer(&left[0]))
rightFr := (*C.Fr256)(unsafe.Pointer(&right[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&outputs[0]))
ret := C.metal_zk_poseidon2_hash_pair(ctx, outFr, leftFr, rightFr, C.uint32_t(n))
if ret != C.METAL_ZK_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
}
output := make([]Element, n/2)
currentFr := (*C.Fr256)(unsafe.Pointer(&current[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&output[0]))
ret := C.metal_zk_poseidon2_merkle_layer(ctx, outFr, currentFr, C.uint32_t(n))
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 Merkle layer failed")
}
return output, nil
}
// 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)
leavesFr := (*C.Fr256)(unsafe.Pointer(&leaves[0]))
treeFr := (*C.Fr256)(unsafe.Pointer(&tree[0]))
ret := C.metal_zk_poseidon2_merkle_tree(ctx, treeFr, leavesFr, C.uint32_t(n))
if ret != C.METAL_ZK_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
tree, err := MerkleTree(leaves)
if err != nil {
return root, err
}
if len(tree) > 0 {
root = tree[0]
}
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) {
results, err := BatchVerifyProofs([]Element{leaf}, []Element{root}, [][]Element{path}, [][]uint32{indices})
if err != nil {
return false, err
}
return results[0], nil
}
// BatchVerifyProofs verifies multiple Merkle proofs in parallel on GPU.
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")
}
if n > 0 && len(paths[0]) != len(indices[0]) {
return nil, errors.New("path length mismatch")
}
if err := initContext(); err != nil {
return nil, err
}
pathLen := len(paths[0])
results := make([]C.int, n)
// Flatten paths and indices
flatPaths := make([]Element, n*pathLen)
flatIndices := make([]uint32, n*pathLen)
for i := 0; i < n; i++ {
copy(flatPaths[i*pathLen:], paths[i])
copy(flatIndices[i*pathLen:], indices[i])
}
leavesFr := (*C.Fr256)(unsafe.Pointer(&leaves[0]))
rootsFr := (*C.Fr256)(unsafe.Pointer(&roots[0]))
pathsFr := (*C.Fr256)(unsafe.Pointer(&flatPaths[0]))
indicesPtr := (*C.uint32_t)(unsafe.Pointer(&flatIndices[0]))
resultsPtr := (*C.int)(unsafe.Pointer(&results[0]))
ret := C.metal_zk_poseidon2_verify_proofs(
ctx,
resultsPtr,
leavesFr,
pathsFr,
indicesPtr,
rootsFr,
C.uint32_t(n),
C.uint32_t(pathLen),
)
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 batch verify proofs failed")
}
boolResults := make([]bool, n)
for i := 0; i < n; i++ {
boolResults[i] = results[i] != 0
}
return boolResults, nil
}
// Commitment computes a Pedersen-style commitment using Poseidon2.
// commitment = Poseidon2(value, blinding, salt)
func Commitment(value, blinding, salt Element) (Element, error) {
commitments, err := BatchCommitment([]Element{value}, []Element{blinding}, []Element{salt})
if err != nil {
return Element{}, err
}
return commitments[0], nil
}
// 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")
}
if err := initContext(); err != nil {
return nil, err
}
outputs := make([]Element, n)
valuesFr := (*C.Fr256)(unsafe.Pointer(&values[0]))
blindingsFr := (*C.Fr256)(unsafe.Pointer(&blindings[0]))
saltsFr := (*C.Fr256)(unsafe.Pointer(&salts[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&outputs[0]))
ret := C.metal_zk_batch_commitment(ctx, outFr, valuesFr, blindingsFr, saltsFr, C.uint32_t(n))
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 batch commitment failed")
}
return outputs, nil
}
// Nullifier computes a nullifier for spending a note.
// nullifier = Poseidon2(key, commitment, index)
func Nullifier(key, commitment, index Element) (Element, error) {
nullifiers, err := BatchNullifier([]Element{key}, []Element{commitment}, []Element{index})
if err != nil {
return Element{}, err
}
return nullifiers[0], nil
}
// 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")
}
if err := initContext(); err != nil {
return nil, err
}
outputs := make([]Element, n)
keysFr := (*C.Fr256)(unsafe.Pointer(&keys[0]))
commitmentsFr := (*C.Fr256)(unsafe.Pointer(&commitments[0]))
indicesFr := (*C.Fr256)(unsafe.Pointer(&indices[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&outputs[0]))
ret := C.metal_zk_batch_nullifier(ctx, outFr, keysFr, commitmentsFr, indicesFr, C.uint32_t(n))
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 batch nullifier failed")
}
return outputs, nil
}
// Destroy releases the Metal ZK context.
// Should be called when done with GPU operations.
func Destroy() {
if ctxReady && ctx != nil {
C.metal_zk_destroy(ctx)
ctx = nil
ctxReady = false
}
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build !luxgpu
// Package gpu provides GPU-accelerated Poseidon2 hash functions.
// This is the stub implementation when GPU acceleration 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
}
+381
View File
@@ -0,0 +1,381 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo && darwin
// Package poseidon2 provides GPU-accelerated Poseidon2 hash functions via luxcpp/crypto.
// Poseidon2 is a ZK-friendly hash function designed for BN254 scalar field.
package poseidon2
/*
#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_zk.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
// context is the global Metal ZK context (initialized on first use).
var (
ctx *C.MetalZKContext
ctxReady bool
)
// initContext lazily initializes the Metal ZK context.
func initContext() error {
if ctxReady {
return nil
}
ctx = C.metal_zk_init()
if ctx == nil {
return errors.New("Metal ZK initialization failed")
}
ctxReady = true
return nil
}
// GPUAvailable returns true if Metal GPU acceleration is available.
func GPUAvailable() bool {
return bool(C.metal_zk_available())
}
// Threshold returns the recommended batch size for GPU offload.
func Threshold() uint32 {
return uint32(C.metal_zk_get_threshold(C.METAL_ZK_OP_POSEIDON2_HASH))
}
// 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
}
leftFr := (*C.Fr256)(unsafe.Pointer(&left[0]))
rightFr := (*C.Fr256)(unsafe.Pointer(&right[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&out[0]))
ret := C.metal_zk_poseidon2_hash_pair(ctx, outFr, leftFr, rightFr, 1)
if ret != C.METAL_ZK_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)
// Below threshold, process sequentially
if uint32(n) < Threshold() || !GPUAvailable() {
for i := 0; i < n; i++ {
out, err := HashPair(left[i], right[i])
if err != nil {
return nil, err
}
outputs[i] = out
}
return outputs, nil
}
// GPU batch processing
leftFr := (*C.Fr256)(unsafe.Pointer(&left[0]))
rightFr := (*C.Fr256)(unsafe.Pointer(&right[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&outputs[0]))
ret := C.metal_zk_poseidon2_hash_pair(ctx, outFr, leftFr, rightFr, C.uint32_t(n))
if ret != C.METAL_ZK_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
}
output := make([]Element, n/2)
currentFr := (*C.Fr256)(unsafe.Pointer(&current[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&output[0]))
ret := C.metal_zk_poseidon2_merkle_layer(ctx, outFr, currentFr, C.uint32_t(n))
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 Merkle layer failed")
}
return output, nil
}
// 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)
leavesFr := (*C.Fr256)(unsafe.Pointer(&leaves[0]))
treeFr := (*C.Fr256)(unsafe.Pointer(&tree[0]))
ret := C.metal_zk_poseidon2_merkle_tree(ctx, treeFr, leavesFr, C.uint32_t(n))
if ret != C.METAL_ZK_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
tree, err := MerkleTree(leaves)
if err != nil {
return root, err
}
if len(tree) > 0 {
root = tree[0]
}
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) {
results, err := BatchVerifyProofs([]Element{leaf}, []Element{root}, [][]Element{path}, [][]uint32{indices})
if err != nil {
return false, err
}
return results[0], nil
}
// BatchVerifyProofs verifies multiple Merkle proofs in parallel on GPU.
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")
}
if n > 0 && len(paths[0]) != len(indices[0]) {
return nil, errors.New("path length mismatch")
}
if err := initContext(); err != nil {
return nil, err
}
pathLen := len(paths[0])
results := make([]C.int, n)
// Flatten paths and indices
flatPaths := make([]Element, n*pathLen)
flatIndices := make([]uint32, n*pathLen)
for i := 0; i < n; i++ {
copy(flatPaths[i*pathLen:], paths[i])
copy(flatIndices[i*pathLen:], indices[i])
}
leavesFr := (*C.Fr256)(unsafe.Pointer(&leaves[0]))
rootsFr := (*C.Fr256)(unsafe.Pointer(&roots[0]))
pathsFr := (*C.Fr256)(unsafe.Pointer(&flatPaths[0]))
indicesPtr := (*C.uint32_t)(unsafe.Pointer(&flatIndices[0]))
resultsPtr := (*C.int)(unsafe.Pointer(&results[0]))
ret := C.metal_zk_poseidon2_verify_proofs(
ctx,
resultsPtr,
leavesFr,
pathsFr,
indicesPtr,
rootsFr,
C.uint32_t(n),
C.uint32_t(pathLen),
)
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 batch verify proofs failed")
}
boolResults := make([]bool, n)
for i := 0; i < n; i++ {
boolResults[i] = results[i] != 0
}
return boolResults, nil
}
// Commitment computes a Pedersen-style commitment using Poseidon2.
// commitment = Poseidon2(value, blinding, salt)
func Commitment(value, blinding, salt Element) (Element, error) {
commitments, err := BatchCommitment([]Element{value}, []Element{blinding}, []Element{salt})
if err != nil {
return Element{}, err
}
return commitments[0], nil
}
// 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")
}
if err := initContext(); err != nil {
return nil, err
}
outputs := make([]Element, n)
valuesFr := (*C.Fr256)(unsafe.Pointer(&values[0]))
blindingsFr := (*C.Fr256)(unsafe.Pointer(&blindings[0]))
saltsFr := (*C.Fr256)(unsafe.Pointer(&salts[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&outputs[0]))
ret := C.metal_zk_batch_commitment(ctx, outFr, valuesFr, blindingsFr, saltsFr, C.uint32_t(n))
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 batch commitment failed")
}
return outputs, nil
}
// Nullifier computes a nullifier for spending a note.
// nullifier = Poseidon2(key, commitment, index)
func Nullifier(key, commitment, index Element) (Element, error) {
nullifiers, err := BatchNullifier([]Element{key}, []Element{commitment}, []Element{index})
if err != nil {
return Element{}, err
}
return nullifiers[0], nil
}
// 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")
}
if err := initContext(); err != nil {
return nil, err
}
outputs := make([]Element, n)
keysFr := (*C.Fr256)(unsafe.Pointer(&keys[0]))
commitmentsFr := (*C.Fr256)(unsafe.Pointer(&commitments[0]))
indicesFr := (*C.Fr256)(unsafe.Pointer(&indices[0]))
outFr := (*C.Fr256)(unsafe.Pointer(&outputs[0]))
ret := C.metal_zk_batch_nullifier(ctx, outFr, keysFr, commitmentsFr, indicesFr, C.uint32_t(n))
if ret != C.METAL_ZK_SUCCESS {
return nil, errors.New("Poseidon2 batch nullifier failed")
}
return outputs, nil
}
// Destroy releases the Metal ZK context.
// Should be called when done with GPU operations.
func Destroy() {
if ctxReady && ctx != nil {
C.metal_zk_destroy(ctx)
ctx = nil
ctxReady = false
}
}
// Hash computes Poseidon2 hash of multiple elements (1-16) using Merkle-Damgard construction.
// This mirrors the gnark-crypto Poseidon2 Merkle-Damgard hasher behavior.
// For 2 elements, it uses HashPair directly. For more, it chains hashes.
func Hash(elements []Element) (Element, error) {
n := len(elements)
if n == 0 {
return Element{}, errors.New("empty input")
}
if n > 16 {
return Element{}, errors.New("too many elements: maximum 16")
}
// Single element: hash with zero element
if n == 1 {
return HashPair(elements[0], Element{})
}
// Two elements: direct hash pair
if n == 2 {
return HashPair(elements[0], elements[1])
}
// More elements: Merkle-Damgard construction
// H(e0, e1) -> state
// H(state, e2) -> state
// ... continue until all elements consumed
state, err := HashPair(elements[0], elements[1])
if err != nil {
return Element{}, err
}
for i := 2; i < n; i++ {
state, err = HashPair(state, elements[i])
if err != nil {
return Element{}, err
}
}
return state, nil
}
// HashFromBytes computes Poseidon2 hash from raw bytes.
// Input must be a multiple of 32 bytes (1-16 field elements).
// This is the main entry point for precompile use.
func HashFromBytes(input []byte) ([]byte, error) {
if len(input) == 0 || len(input)%ElementSize != 0 {
return nil, errors.New("invalid input length: must be multiple of 32 bytes")
}
numElements := len(input) / ElementSize
if numElements > 16 {
return nil, errors.New("too many inputs: maximum 16 field elements")
}
// Parse into Elements
elements := make([]Element, numElements)
for i := 0; i < numElements; i++ {
copy(elements[i][:], input[i*ElementSize:(i+1)*ElementSize])
}
result, err := Hash(elements)
if err != nil {
return nil, err
}
return result[:], nil
}