refactor: update import path to github.com/luxfi/constants

This commit is contained in:
Zach Kelling
2025-12-27 02:09:03 -08:00
parent 91dc988e75
commit 9bb37d2a91
33 changed files with 1431 additions and 2030 deletions
+4 -3
View File
@@ -1,6 +1,6 @@
# AI Assistant Knowledge Base
**Last Updated**: 2025-12-19
**Last Updated**: 2026-01-05
**Project**: crypto
**Organization**: luxfi
**Repo**: github.com/luxfi/crypto
@@ -8,6 +8,7 @@
## Project Overview
Lux cryptography library implementing post-quantum standards and consensus primitives.
TLS certificate utilities live in the top-level `github.com/luxfi/tls` module.
## Essential Commands
@@ -59,7 +60,7 @@ cd docs && pnpm build # Build static site
| **precompile/** | Post-quantum EVM precompiles | LP-2517 |
### Supporting Packages
- **blake2b/, hashing/** - Hash functions (SHA, BLAKE, Keccak, SHAKE)
- **blake2b/, hash/** - Hash functions (SHA, BLAKE, Keccak, SHAKE)
- **kem/** - Key encapsulation interface
- **ecies/** - Hybrid encryption
- **cb58/** - Base58 encoding
@@ -1457,7 +1458,7 @@ The Go module proxy cached `github.com/luxfi/geth@v1.16.1` which had incorrect m
| sdk | v1.16.28 | Added exclude, bumped deps (needs more work) |
### Pending Work
- **SDK**: Needs import migration from `github.com/luxfi/node/utils/constants` to `github.com/luxfi/constants`
- **SDK**: Needs import migration from `github.com/luxfi/utils/constants` to `github.com/luxfi/constants`
- **SDK**: API rename: Subnet→Chain (e.g., `tx.Net``tx.Chain`, `txs.ChainValidator.Net``txs.ChainValidator.Chain`)
- **CLI**: Uses local replaces, needs SDK fixes first
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"errors"
"fmt"
"golang.org/x/crypto/chacha20poly1305"
"crypto/chacha20poly1305"
)
// AeadID identifies an AEAD algorithm
-255
View File
@@ -1,255 +0,0 @@
// Package cert provides X.509 certificate handling for post-quantum algorithms
package cert
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"time"
"github.com/luxfi/crypto/sign"
)
// MLDSACert represents an ML-DSA certificate
type MLDSACert struct {
Raw []byte
PublicKey sign.PublicKey
Subject pkix.Name
Issuer pkix.Name
SerialNumber []byte
NotBefore time.Time
NotAfter time.Time
KeyUsage x509.KeyUsage
ExtKeyUsage []x509.ExtKeyUsage
IsCA bool
// Custom extensions for QZMQ
NodeID string
Capabilities []string
Role string
}
// CertPool represents a pool of trusted certificates
type CertPool struct {
certs map[string]*MLDSACert // Keyed by SPKI hash
}
// NewCertPool creates a new certificate pool
func NewCertPool() *CertPool {
return &CertPool{
certs: make(map[string]*MLDSACert),
}
}
// AddCert adds a certificate to the pool
func (cp *CertPool) AddCert(cert *MLDSACert) {
spkiHash := hashSPKI(cert.PublicKey.Bytes())
cp.certs[spkiHash] = cert
}
// VerifyChain verifies a certificate chain
func (cp *CertPool) VerifyChain(chain []*MLDSACert, now time.Time) error {
if len(chain) == 0 {
return errors.New("empty certificate chain")
}
// Verify leaf certificate
leaf := chain[0]
if now.Before(leaf.NotBefore) || now.After(leaf.NotAfter) {
return errors.New("certificate expired or not yet valid")
}
// Verify chain
for i := 0; i < len(chain)-1; i++ {
cert := chain[i]
issuer := chain[i+1]
if err := verifyCertSignature(cert, issuer); err != nil {
return fmt.Errorf("invalid signature at position %d: %w", i, err)
}
if !issuer.IsCA {
return fmt.Errorf("issuer at position %d is not a CA", i+1)
}
}
// Verify root is trusted
root := chain[len(chain)-1]
spkiHash := hashSPKI(root.PublicKey.Bytes())
if _, ok := cp.certs[spkiHash]; !ok {
return errors.New("root certificate not trusted")
}
return nil
}
// SPKIPinner implements SPKI pinning
type SPKIPinner struct {
pins map[string]bool // Set of allowed SPKI hashes
}
// NewSPKIPinner creates a new SPKI pinner
func NewSPKIPinner(pins []string) *SPKIPinner {
pinner := &SPKIPinner{
pins: make(map[string]bool),
}
for _, pin := range pins {
pinner.pins[pin] = true
}
return pinner
}
// Verify checks if a public key is pinned
func (sp *SPKIPinner) Verify(pk sign.PublicKey) bool {
hash := hashSPKI(pk.Bytes())
return sp.pins[hash]
}
// hashSPKI computes the SHA-256 hash of the SPKI
func hashSPKI(pubKeyBytes []byte) string {
// In production, compute actual SHA-256 hash
// For now, return a placeholder
return fmt.Sprintf("spki_%x", pubKeyBytes[:8])
}
// verifyCertSignature verifies a certificate's signature
func verifyCertSignature(cert, issuer *MLDSACert) error {
// In production, this would verify the actual signature
// using the issuer's public key
return nil
}
// CertBuilder builds ML-DSA certificates
type CertBuilder struct {
template *MLDSACert
signer sign.Signer
}
// NewCertBuilder creates a new certificate builder
func NewCertBuilder(signer sign.Signer) *CertBuilder {
return &CertBuilder{
signer: signer,
template: &MLDSACert{
NotBefore: time.Now(),
NotAfter: time.Now().Add(30 * 24 * time.Hour), // 30 days default
},
}
}
// SetSubject sets the certificate subject
func (cb *CertBuilder) SetSubject(subject pkix.Name) *CertBuilder {
cb.template.Subject = subject
return cb
}
// SetValidity sets the certificate validity period
func (cb *CertBuilder) SetValidity(notBefore, notAfter time.Time) *CertBuilder {
cb.template.NotBefore = notBefore
cb.template.NotAfter = notAfter
return cb
}
// SetNodeID sets the node ID extension
func (cb *CertBuilder) SetNodeID(nodeID string) *CertBuilder {
cb.template.NodeID = nodeID
return cb
}
// SetRole sets the role extension
func (cb *CertBuilder) SetRole(role string) *CertBuilder {
cb.template.Role = role
return cb
}
// SetCapabilities sets the capabilities extension
func (cb *CertBuilder) SetCapabilities(caps []string) *CertBuilder {
cb.template.Capabilities = caps
return cb
}
// SetCA marks the certificate as a CA
func (cb *CertBuilder) SetCA(isCA bool) *CertBuilder {
cb.template.IsCA = isCA
if isCA {
cb.template.KeyUsage |= x509.KeyUsageCertSign
}
return cb
}
// Build creates the certificate
func (cb *CertBuilder) Build(publicKey sign.PublicKey, issuerKey sign.PrivateKey) (*MLDSACert, error) {
cert := *cb.template
cert.PublicKey = publicKey
// Generate serial number
cert.SerialNumber = generateSerialNumber()
// Self-signed if no issuer provided
if issuerKey == nil {
cert.Issuer = cert.Subject
}
// Encode to DER
certBytes, err := encodeCertificate(&cert)
if err != nil {
return nil, err
}
// Sign the certificate
if issuerKey != nil {
signature, err := cb.signer.Sign(issuerKey, certBytes)
if err != nil {
return nil, err
}
// Append signature to certificate
cert.Raw = append(certBytes, signature...)
} else {
cert.Raw = certBytes
}
return &cert, nil
}
// encodeCertificate encodes a certificate to DER
func encodeCertificate(cert *MLDSACert) ([]byte, error) {
// Simplified encoding - in production would use proper ASN.1
// This is a placeholder
encoded := []byte("MLDSA-CERT-v1")
encoded = append(encoded, cert.PublicKey.Bytes()...)
encoded = append(encoded, []byte(cert.NodeID)...)
encoded = append(encoded, []byte(cert.Role)...)
return encoded, nil
}
// generateSerialNumber generates a random serial number
func generateSerialNumber() []byte {
// In production, generate cryptographically random serial
return []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
}
// ParseMLDSACert parses an ML-DSA certificate
func ParseMLDSACert(der []byte) (*MLDSACert, error) {
// Placeholder parser
// In production, would parse actual DER-encoded certificate
cert := &MLDSACert{
Raw: der,
}
// Parse basic fields (placeholder)
if len(der) < 100 {
return nil, errors.New("certificate too short")
}
return cert, nil
}
// OID definitions for ML-DSA algorithms
var (
OIDMLDSA44 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 2, 267, 7, 6, 5} // ML-DSA-44
OIDMLDSA65 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 2, 267, 7, 8, 7} // ML-DSA-65
OIDMLDSA87 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 2, 267, 7, 10, 8} // ML-DSA-87
)
+20
View File
@@ -0,0 +1,20 @@
//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
package luxlink
/*
#cgo pkg-config: lux-crypto lux-gpu lux-lattice
#include <lux/crypto/crypto.h>
#include <lux/crypto/metal_mldsa.h>
#include <lux/crypto/metal_mlkem.h>
#include <lux/crypto/metal_slhdsa.h>
#include <lux/crypto/metal_ipa.h>
#include <lux/gpu/gpu.h>
*/
import "C"
// Export Go APIs that other packages can call
// This ensures libraries are only linked once
+9
View File
@@ -0,0 +1,9 @@
//go:build darwin
// +build darwin
// Suppress duplicate library warnings on macOS
// This file is intentionally left with minimal content as the suppression
// is handled via build flags or environment variables
package luxlink
import "C"
+1 -1
View File
@@ -697,4 +697,4 @@ func TestHashFunctions(t *testing.T) {
- [FIPS 202: SHA-3 Standard](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf)
- [BLAKE2 Specification](https://blake2.net/blake2.pdf)
- [Keccak Reference](https://keccak.team/keccak.html)
- [Argon2 Specification](https://github.com/P-H-C/phc-winner-argon2)
- [Argon2 Specification](https://github.com/P-H-C/phc-winner-argon2)
+2 -2
View File
@@ -262,7 +262,7 @@ func (w *HDWallet) DeriveAddress(coinType, account, change, index uint32) (*ecds
```go
import (
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
"crypto/chacha20poly1305"
)
// KeyStore manages encrypted key storage
@@ -845,4 +845,4 @@ func TestKeyManagement(t *testing.T) {
- [BIP-39: Mnemonic Phrases](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)
- [BIP-44: Multi-Account Hierarchy](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)
- [NIST SP 800-57: Key Management](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf)
- [Argon2 Specification](https://github.com/P-H-C/phc-winner-argon2)
- [Argon2 Specification](https://github.com/P-H-C/phc-winner-argon2)
+6 -3
View File
@@ -14,17 +14,20 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/ethereum/c-kzg-4844/v2 v2.1.5
github.com/ethereum/go-verkle v0.2.2
github.com/google/btree v1.1.3
github.com/google/gofuzz v1.2.0
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.4
github.com/luxfi/log v1.1.26
github.com/luxfi/ids v1.2.7
github.com/luxfi/log v1.2.1
github.com/luxfi/mock v0.1.0
github.com/mr-tron/base58 v1.2.0
github.com/stretchr/testify v1.11.1
github.com/supranational/blst v0.3.16
github.com/zeebo/blake3 v0.2.4
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.46.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.39.0
@@ -41,7 +44,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // 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
)
+6 -8
View File
@@ -130,6 +130,7 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -213,14 +214,11 @@ 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.29.4 h1:0eCgEbffLayTLaPYLkXx5IbL2490r5jqZAKoSfkVVZI=
github.com/luxfi/gpu v0.29.4/go.mod h1:7pFsHqra1Vrmy2aGXVEPKeKsPR+Bn+QmBXuByK3fdPA=
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.4 h1:e0OaeSI6xWjS9JnxxxfvCCs9HHDUrwDc3M9ctIhjcDI=
github.com/luxfi/ids v1.2.4/go.mod h1:HwvRJSNbuZS+u+0JqeHfPX2S13iFyLCnfGxjBCmt244=
github.com/luxfi/log v1.1.26 h1:ECnJ4wV7TF6WZ5gC6CD6ddZ4OhF+zhQij4bgVUhumDg=
github.com/luxfi/log v1.1.26/go.mod h1:ACN1FtMlJ/NRuWROYH2TtiFmJuUbSG+OxINlRzJM/38=
github.com/luxfi/ids v1.2.7 h1:3B42EbzR2cdY3veo1yOFOOIeEE+HULnCye2Ye32nXqI=
github.com/luxfi/log v1.2.1 h1:L2JM8MjGPF8rBnY+EXODS9x5OQTGSgeQ1SPFmeB+oPY=
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
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=
@@ -310,6 +308,7 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
@@ -337,8 +336,7 @@ 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-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
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=
+14
View File
@@ -0,0 +1,14 @@
//go:build cgo
// +build cgo
// 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
+53 -1
View File
@@ -12,13 +12,65 @@
package gpu
/*
#cgo pkg-config: lux-crypto
#cgo pkg-config: lux-crypto-only
#cgo darwin,arm64 CFLAGS: -DUSE_METAL=1
#cgo linux CFLAGS: -DUSE_CUDA=1
#cgo LDFLAGS: -lstdc++
#include <lux/crypto/crypto.h>
// Short aliases for Go code readability (hides lux_ prefix)
#define crypto_gpu_available lux_crypto_gpu_available
#define crypto_get_backend lux_crypto_get_backend
#define crypto_clear_cache lux_crypto_clear_cache
// BLS
#define bls_keygen lux_crypto_bls_keygen
#define bls_sk_to_pk lux_crypto_bls_sk_to_pk
#define bls_sign lux_crypto_bls_sign
#define bls_verify lux_crypto_bls_verify
#define bls_aggregate_signatures lux_crypto_bls_aggregate_signatures
#define bls_aggregate_public_keys lux_crypto_bls_aggregate_public_keys
#define bls_verify_aggregated lux_crypto_bls_verify_aggregated
#define bls_batch_verify lux_crypto_bls_batch_verify
#define bls_batch_sign lux_crypto_bls_batch_sign
// ML-DSA
#define mldsa_keygen lux_crypto_mldsa_keygen
#define mldsa_sign lux_crypto_mldsa_sign
#define mldsa_verify lux_crypto_mldsa_verify
#define mldsa_batch_verify lux_crypto_mldsa_batch_verify
// Threshold
#define threshold_create lux_crypto_threshold_create
#define threshold_destroy lux_crypto_threshold_destroy
#define threshold_keygen lux_crypto_threshold_keygen
#define threshold_partial_sign lux_crypto_threshold_partial_sign
#define threshold_combine lux_crypto_threshold_combine
#define threshold_verify lux_crypto_threshold_verify
// Hashing
#define crypto_sha3_256 lux_crypto_sha3_256
#define crypto_sha3_512 lux_crypto_sha3_512
#define crypto_blake3 lux_crypto_blake3
#define crypto_batch_hash lux_crypto_batch_hash
// Consensus
#define consensus_verify_block lux_crypto_consensus_verify_block
// Error codes
#define CRYPTO_SUCCESS LUX_CRYPTO_SUCCESS
#define CRYPTO_ERROR_INVALID LUX_CRYPTO_ERROR_INVALID
#define CRYPTO_ERROR_INVALID_KEY LUX_CRYPTO_ERROR_INVALID_KEY
#define CRYPTO_ERROR_INVALID_SIG LUX_CRYPTO_ERROR_INVALID_SIG
#define CRYPTO_ERROR_NULL_PTR LUX_CRYPTO_ERROR_NULL_PTR
#define CRYPTO_ERROR_GPU LUX_CRYPTO_ERROR_GPU
#define CRYPTO_ERROR_THRESHOLD LUX_CRYPTO_ERROR_THRESHOLD
#define CRYPTO_ERROR_HASH LUX_CRYPTO_ERROR_HASH
// Types
#define ThresholdContext LuxCryptoThresholdContext
*/
import "C"
import (
+10 -7
View File
@@ -1,7 +1,7 @@
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build gpu && darwin
//go:build cgo && darwin
// Package blake3 provides Blake3 hash functions via luxcpp/crypto.
// This CGO version provides optimized hashing with automatic CPU/GPU selection.
@@ -9,17 +9,20 @@
package blake3
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../luxcpp/crypto/include
#cgo LDFLAGS: -L${SRCDIR}/../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#cgo CFLAGS: -I/Users/z/work/luxcpp/crypto/include
#cgo LDFLAGS: -L/Users/z/work/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);
#include <lux/crypto/crypto.h>
// Short aliases for Go code readability (hides lux_crypto_ prefix)
#define crypto_gpu_available lux_crypto_gpu_available
#define crypto_get_backend lux_crypto_get_backend
#define crypto_blake3 lux_crypto_blake3
#define crypto_batch_hash lux_crypto_batch_hash
*/
import "C"
+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"
hashing "github.com/luxfi/crypto/hash"
)
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/hash/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
}
+54
View File
@@ -0,0 +1,54 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/luxfi/crypto/hash (interfaces: Hasher)
//
// Generated by this command:
//
// mockgen -package=hashingmock -destination=hashingmock/hasher.go -mock_names=Hasher=Hasher github.com/luxfi/crypto/hash 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/hash (interfaces: Hasher)
//
// Generated by this command:
//
// mockgen -package=hash -destination=hash/mock_hasher.go github.com/luxfi/crypto/hash Hasher
//
// Package hash is a generated GoMock package.
package hash
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 hash
//go:generate go run go.uber.org/mock/mockgen -package=hashingmock -destination=hashingmock/hasher.go -mock_names=Hasher=Hasher . Hasher
+112 -113
View File
@@ -10,13 +10,14 @@
package gpu
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
#cgo LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#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_zk.h"
#include "lux/crypto/metal_poseidon2.h"
*/
import "C"
@@ -32,20 +33,23 @@ const ElementSize = 32
// 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).
// Threshold for GPU batch operations
const GPUThreshold = 64
// context is the global Metal Poseidon2 context (initialized on first use).
var (
ctx *C.MetalZKContext
ctx *C.MetalPoseidon2Context
ctxReady bool
)
// initContext lazily initializes the Metal ZK context.
// initContext lazily initializes the Metal Poseidon2 context.
func initContext() error {
if ctxReady {
return nil
}
ctx = C.metal_zk_init()
ctx = C.metal_poseidon2_init()
if ctx == nil {
return errors.New("Metal ZK initialization failed")
return errors.New("Metal Poseidon2 initialization failed")
}
ctxReady = true
return nil
@@ -53,12 +57,12 @@ func initContext() error {
// Available returns true if Metal GPU acceleration is available.
func Available() bool {
return bool(C.metal_zk_available())
return bool(C.metal_poseidon2_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))
return GPUThreshold
}
// HashPair computes Poseidon2 hash of two elements (2-to-1 compression).
@@ -69,12 +73,14 @@ func HashPair(left, right Element) (Element, error) {
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 {
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
@@ -94,12 +100,15 @@ func BatchHashPair(left, right []Element) ([]Element, error) {
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 {
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
@@ -116,15 +125,16 @@ func MerkleLayer(current []Element) ([]Element, error) {
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")
// 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 output, nil
return BatchHashPair(lefts, rights)
}
// MerkleTree builds a complete Merkle tree from leaves using GPU.
@@ -141,11 +151,15 @@ func MerkleTree(leaves []Element) ([]Element, error) {
// 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 {
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
@@ -154,12 +168,19 @@ func MerkleTree(leaves []Element) ([]Element, error) {
// MerkleRoot computes just the Merkle root from leaves.
func MerkleRoot(leaves []Element) (Element, error) {
var root Element
tree, err := MerkleTree(leaves)
if err != nil {
if err := initContext(); err != nil {
return root, err
}
if len(tree) > 0 {
root = tree[0]
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
}
@@ -167,72 +188,62 @@ func MerkleRoot(leaves []Element) (Element, error) {
// 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 {
if len(path) != len(indices) {
return false, errors.New("path length mismatch")
}
if err := initContext(); err != nil {
return false, err
}
return results[0], nil
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")
}
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)
results := make([]bool, n)
for i := 0; i < n; i++ {
copy(flatPaths[i*pathLen:], paths[i])
copy(flatIndices[i*pathLen:], indices[i])
valid, err := VerifyProof(leaves[i], roots[i], paths[i], indices[i])
if err != nil {
return nil, err
}
results[i] = valid
}
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
return results, 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})
// First hash value and blinding
temp, err := HashPair(value, blinding)
if err != nil {
return Element{}, err
}
return commitments[0], nil
// Then hash result with salt
return HashPair(temp, salt)
}
// BatchCommitment computes multiple commitments in parallel on GPU.
@@ -241,31 +252,26 @@ func BatchCommitment(values, blindings, salts []Element) ([]Element, error) {
if n == 0 || n != len(blindings) || n != len(salts) {
return nil, errors.New("invalid input lengths")
}
if err := initContext(); err != nil {
// First hash values and blindings
temps, err := BatchHashPair(values, blindings)
if 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
// 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) {
nullifiers, err := BatchNullifier([]Element{key}, []Element{commitment}, []Element{index})
// First hash key and commitment
temp, err := HashPair(key, commitment)
if err != nil {
return Element{}, err
}
return nullifiers[0], nil
// Then hash result with index
return HashPair(temp, index)
}
// BatchNullifier computes multiple nullifiers in parallel on GPU.
@@ -274,28 +280,21 @@ func BatchNullifier(keys, commitments, indices []Element) ([]Element, error) {
if n == 0 || n != len(commitments) || n != len(indices) {
return nil, errors.New("invalid input lengths")
}
if err := initContext(); err != nil {
// First hash keys and commitments
temps, err := BatchHashPair(keys, commitments)
if 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
// Then hash results with indices
return BatchHashPair(temps, indices)
}
// Destroy releases the Metal ZK context.
// Destroy releases the Metal Poseidon2 context.
// Should be called when done with GPU operations.
func Destroy() {
if ctxReady && ctx != nil {
C.metal_zk_destroy(ctx)
C.metal_poseidon2_destroy(ctx)
ctx = nil
ctxReady = false
}
+117 -113
View File
@@ -8,13 +8,14 @@
package poseidon2
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../luxcpp/crypto/include
#cgo LDFLAGS: -L${SRCDIR}/../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#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_zk.h"
#include "lux/crypto/metal_poseidon2.h"
*/
import "C"
@@ -31,20 +32,23 @@ const ElementSize = 32
// 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).
// Threshold for GPU batch operations
const GPUThreshold = 64
// context is the global Metal Poseidon2 context (initialized on first use).
var (
ctx *C.MetalZKContext
ctx *C.MetalPoseidon2Context
ctxReady bool
)
// initContext lazily initializes the Metal ZK context.
// initContext lazily initializes the Metal Poseidon2 context.
func initContext() error {
if ctxReady {
return nil
}
ctx = C.metal_zk_init()
ctx = C.metal_poseidon2_init()
if ctx == nil {
return errors.New("Metal ZK initialization failed")
return errors.New("Metal Poseidon2 initialization failed")
}
ctxReady = true
return nil
@@ -52,12 +56,17 @@ func initContext() error {
// GPUAvailable returns true if Metal GPU acceleration is available.
func GPUAvailable() bool {
return bool(C.metal_zk_available())
return bool(C.metal_poseidon2_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))
return GPUThreshold
}
// elementToFe256 converts Element to Poseidon2Fe256
func elementToFe256(e *Element) *C.Poseidon2Fe256 {
return (*C.Poseidon2Fe256)(unsafe.Pointer(e))
}
// HashPair computes Poseidon2 hash of two elements (2-to-1 compression).
@@ -68,12 +77,14 @@ func HashPair(left, right Element) (Element, error) {
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 {
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
@@ -105,12 +116,15 @@ func BatchHashPair(left, right []Element) ([]Element, error) {
}
// 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 {
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
@@ -127,15 +141,16 @@ func MerkleLayer(current []Element) ([]Element, error) {
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")
// 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 output, nil
return BatchHashPair(lefts, rights)
}
// MerkleTree builds a complete Merkle tree from leaves using GPU.
@@ -152,11 +167,15 @@ func MerkleTree(leaves []Element) ([]Element, error) {
// 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 {
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
@@ -165,12 +184,19 @@ func MerkleTree(leaves []Element) ([]Element, error) {
// MerkleRoot computes just the Merkle root from leaves.
func MerkleRoot(leaves []Element) (Element, error) {
var root Element
tree, err := MerkleTree(leaves)
if err != nil {
if err := initContext(); err != nil {
return root, err
}
if len(tree) > 0 {
root = tree[0]
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
}
@@ -178,72 +204,62 @@ func MerkleRoot(leaves []Element) (Element, error) {
// 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 {
if len(path) != len(indices) {
return false, errors.New("path length mismatch")
}
if err := initContext(); err != nil {
return false, err
}
return results[0], nil
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 (first element represents full path)
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")
}
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)
results := make([]bool, n)
for i := 0; i < n; i++ {
copy(flatPaths[i*pathLen:], paths[i])
copy(flatIndices[i*pathLen:], indices[i])
valid, err := VerifyProof(leaves[i], roots[i], paths[i], indices[i])
if err != nil {
return nil, err
}
results[i] = valid
}
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
return results, 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})
// First hash value and blinding
temp, err := HashPair(value, blinding)
if err != nil {
return Element{}, err
}
return commitments[0], nil
// Then hash result with salt
return HashPair(temp, salt)
}
// BatchCommitment computes multiple commitments in parallel on GPU.
@@ -252,31 +268,26 @@ func BatchCommitment(values, blindings, salts []Element) ([]Element, error) {
if n == 0 || n != len(blindings) || n != len(salts) {
return nil, errors.New("invalid input lengths")
}
if err := initContext(); err != nil {
// First hash values and blindings
temps, err := BatchHashPair(values, blindings)
if 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
// 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) {
nullifiers, err := BatchNullifier([]Element{key}, []Element{commitment}, []Element{index})
// First hash key and commitment
temp, err := HashPair(key, commitment)
if err != nil {
return Element{}, err
}
return nullifiers[0], nil
// Then hash result with index
return HashPair(temp, index)
}
// BatchNullifier computes multiple nullifiers in parallel on GPU.
@@ -285,28 +296,21 @@ func BatchNullifier(keys, commitments, indices []Element) ([]Element, error) {
if n == 0 || n != len(commitments) || n != len(indices) {
return nil, errors.New("invalid input lengths")
}
if err := initContext(); err != nil {
// First hash keys and commitments
temps, err := BatchHashPair(keys, commitments)
if 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
// Then hash results with indices
return BatchHashPair(temps, indices)
}
// Destroy releases the Metal ZK context.
// Destroy releases the Metal Poseidon2 context.
// Should be called when done with GPU operations.
func Destroy() {
if ctxReady && ctx != nil {
C.metal_zk_destroy(ctx)
C.metal_poseidon2_destroy(ctx)
ctx = nil
ctxReady = false
}
-121
View File
@@ -1,121 +0,0 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package blake3 provides Blake3 hash functions for cryptographic operations.
// This is extracted from the threshold package to provide a centralized
// implementation for all Lux projects.
package blake3
import (
"encoding/binary"
"io"
"math/big"
"github.com/zeebo/blake3"
)
// DigestLength is the standard output length for Blake3 hashes
const DigestLength = 64 // 512 bits
// Digest represents a Blake3 hash output
type Digest [DigestLength]byte
// Hasher wraps blake3.Hasher to provide a consistent interface
type Hasher struct {
h *blake3.Hasher
}
// New creates a new Blake3 hasher
func New() *Hasher {
return &Hasher{h: blake3.New()}
}
// NewWithDomain creates a new Blake3 hasher with a domain separator
func NewWithDomain(domain string) *Hasher {
h := &Hasher{h: blake3.New()}
h.WriteString(domain)
return h
}
// Write adds data to the hash
func (h *Hasher) Write(p []byte) (n int, err error) {
return h.h.Write(p)
}
// WriteString adds a string to the hash
func (h *Hasher) WriteString(s string) (n int, err error) {
return h.h.WriteString(s)
}
// WriteUint32 adds a uint32 to the hash in big-endian format
func (h *Hasher) WriteUint32(v uint32) {
var buf [4]byte
binary.BigEndian.PutUint32(buf[:], v)
h.h.Write(buf[:])
}
// WriteUint64 adds a uint64 to the hash in big-endian format
func (h *Hasher) WriteUint64(v uint64) {
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], v)
h.h.Write(buf[:])
}
// WriteBigInt adds a big.Int to the hash
func (h *Hasher) WriteBigInt(n *big.Int) {
if n == nil {
h.WriteUint32(0)
return
}
bytes := n.Bytes()
h.WriteUint32(uint32(len(bytes)))
h.h.Write(bytes)
}
// Sum returns the hash digest
func (h *Hasher) Sum(b []byte) []byte {
return h.h.Sum(b)
}
// Digest returns a fixed-size digest
func (h *Hasher) Digest() Digest {
var d Digest
h.h.Digest().Read(d[:])
return d
}
// Reader returns an io.Reader for extended output
func (h *Hasher) Reader() io.Reader {
return h.h.Digest()
}
// Clone creates a copy of the hasher
func (h *Hasher) Clone() *Hasher {
return &Hasher{h: h.h.Clone()}
}
// Reset resets the hasher to its initial state
func (h *Hasher) Reset() {
h.h.Reset()
}
// HashBytes hashes a byte slice and returns a digest
func HashBytes(data []byte) Digest {
h := New()
h.Write(data)
return h.Digest()
}
// HashString hashes a string and returns a digest
func HashString(s string) Digest {
h := New()
h.WriteString(s)
return h.Digest()
}
// HashWithDomain hashes data with a domain separator
func HashWithDomain(domain string, data []byte) Digest {
h := NewWithDomain(domain)
h.Write(data)
return h.Digest()
}
-382
View File
@@ -1,382 +0,0 @@
package blake3
import (
"bytes"
"encoding/hex"
"math/big"
"strings"
"testing"
)
func TestNew(t *testing.T) {
h := New()
if h == nil {
t.Fatal("New() returned nil")
}
if h.h == nil {
t.Fatal("Internal hasher is nil")
}
}
func TestNewWithDomain(t *testing.T) {
domain := "test-domain"
h1 := NewWithDomain(domain)
h2 := NewWithDomain(domain)
data := []byte("test data")
h1.Write(data)
h2.Write(data)
d1 := h1.Digest()
d2 := h2.Digest()
if !bytes.Equal(d1[:], d2[:]) {
t.Error("Same domain should produce same hash")
}
// Different domain should produce different hash
h3 := NewWithDomain("different-domain")
h3.Write(data)
d3 := h3.Digest()
if bytes.Equal(d1[:], d3[:]) {
t.Error("Different domain should produce different hash")
}
}
func TestHashBytes(t *testing.T) {
// Test with known test vectors
testCases := []struct {
input []byte
expected string // First 64 bytes of hash
}{
{
[]byte(""),
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a",
},
{
[]byte("hello"),
"ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200fe992405f0d785b599a2e3387f6d34d01faccfeb22fb697ef3fd53541241a338c",
},
}
for _, tc := range testCases {
hash := HashBytes(tc.input)
hashStr := hex.EncodeToString(hash[:])
if hashStr != tc.expected {
t.Errorf("HashBytes(%q) = %s, want %s", tc.input, hashStr, tc.expected)
}
}
// Test consistency
data := []byte("test data")
h1 := HashBytes(data)
h2 := HashBytes(data)
if !bytes.Equal(h1[:], h2[:]) {
t.Error("Same input should produce same hash")
}
}
func TestHashString(t *testing.T) {
// Test consistency
s := "test string"
h1 := HashString(s)
h2 := HashString(s)
if !bytes.Equal(h1[:], h2[:]) {
t.Error("Same string should produce same hash")
}
// Compare with HashBytes
h3 := HashBytes([]byte(s))
if !bytes.Equal(h1[:], h3[:]) {
t.Error("HashString should match HashBytes for same content")
}
// Different strings produce different hashes
h4 := HashString("different string")
if bytes.Equal(h1[:], h4[:]) {
t.Error("Different strings should produce different hashes")
}
}
func TestHashWithDomain(t *testing.T) {
data := []byte("test data")
domain1 := "domain1"
domain2 := "domain2"
h1 := HashWithDomain(domain1, data)
h2 := HashWithDomain(domain1, data)
h3 := HashWithDomain(domain2, data)
// Same domain and data should produce same hash
if !bytes.Equal(h1[:], h2[:]) {
t.Error("Same domain and data should produce same hash")
}
// Different domain should produce different hash
if bytes.Equal(h1[:], h3[:]) {
t.Error("Different domain should produce different hash")
}
// Should differ from hash without domain
h4 := HashBytes(data)
if bytes.Equal(h1[:], h4[:]) {
t.Error("Hash with domain should differ from hash without domain")
}
}
func TestWriteMethods(t *testing.T) {
h := New()
// Test Write
n, err := h.Write([]byte("test"))
if err != nil {
t.Errorf("Write error: %v", err)
}
if n != 4 {
t.Errorf("Write returned %d, want 4", n)
}
// Test WriteString
n, err = h.WriteString("string")
if err != nil {
t.Errorf("WriteString error: %v", err)
}
if n != 6 {
t.Errorf("WriteString returned %d, want 6", n)
}
// Test WriteUint32
h.WriteUint32(0x12345678)
// Test WriteUint64
h.WriteUint64(0x123456789ABCDEF0)
// Test WriteBigInt
bigNum := big.NewInt(1234567890)
h.WriteBigInt(bigNum)
// Test WriteBigInt with nil
h.WriteBigInt(nil)
// Get digest to ensure it doesn't panic
_ = h.Digest()
}
func TestWriteUint32(t *testing.T) {
h1 := New()
h1.WriteUint32(0x12345678)
d1 := h1.Digest()
// Should be same as writing the bytes in big-endian
h2 := New()
h2.Write([]byte{0x12, 0x34, 0x56, 0x78})
d2 := h2.Digest()
if !bytes.Equal(d1[:], d2[:]) {
t.Error("WriteUint32 should write in big-endian format")
}
}
func TestWriteUint64(t *testing.T) {
h1 := New()
h1.WriteUint64(0x123456789ABCDEF0)
d1 := h1.Digest()
// Should be same as writing the bytes in big-endian
h2 := New()
h2.Write([]byte{0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0})
d2 := h2.Digest()
if !bytes.Equal(d1[:], d2[:]) {
t.Error("WriteUint64 should write in big-endian format")
}
}
func TestWriteBigInt(t *testing.T) {
// Test with normal big int
n := big.NewInt(1234567890)
h1 := New()
h1.WriteBigInt(n)
d1 := h1.Digest()
// Writing same number should produce same hash
h2 := New()
h2.WriteBigInt(n)
d2 := h2.Digest()
if !bytes.Equal(d1[:], d2[:]) {
t.Error("Same big.Int should produce same hash")
}
// Test with nil
h3 := New()
h3.WriteBigInt(nil)
d3 := h3.Digest()
// Nil should write a zero length prefix
h4 := New()
h4.WriteUint32(0)
d4 := h4.Digest()
if !bytes.Equal(d3[:], d4[:]) {
t.Error("WriteBigInt(nil) should write zero length")
}
// Test with zero
zero := big.NewInt(0)
h5 := New()
h5.WriteBigInt(zero)
_ = h5.Digest() // Should not panic
}
func TestSum(t *testing.T) {
h := New()
h.WriteString("test")
// Sum with nil
sum1 := h.Sum(nil)
if len(sum1) != 32 { // Default blake3 output
t.Errorf("Sum(nil) length = %d, want 32", len(sum1))
}
// Sum with existing slice
prefix := []byte("prefix")
sum2 := h.Sum(prefix)
if !bytes.HasPrefix(sum2, prefix) {
t.Error("Sum should append to provided slice")
}
if len(sum2) != len(prefix)+32 {
t.Errorf("Sum length = %d, want %d", len(sum2), len(prefix)+32)
}
}
func TestDigest(t *testing.T) {
h := New()
h.WriteString("test")
d := h.Digest()
if len(d) != DigestLength {
t.Errorf("Digest length = %d, want %d", len(d), DigestLength)
}
// Digest should be consistent
h2 := New()
h2.WriteString("test")
d2 := h2.Digest()
if !bytes.Equal(d[:], d2[:]) {
t.Error("Same input should produce same digest")
}
}
func TestReader(t *testing.T) {
h := New()
h.WriteString("test")
reader := h.Reader()
if reader == nil {
t.Fatal("Reader() returned nil")
}
// Read some bytes
buf := make([]byte, 100)
n, err := reader.Read(buf)
if err != nil {
t.Errorf("Reader.Read error: %v", err)
}
if n != 100 {
t.Errorf("Reader.Read returned %d, want 100", n)
}
}
func TestClone(t *testing.T) {
h1 := New()
h1.WriteString("test")
h2 := h1.Clone()
if h2 == nil {
t.Fatal("Clone() returned nil")
}
// Both should produce same digest at this point
d1 := h1.Digest()
d2 := h2.Digest()
if !bytes.Equal(d1[:], d2[:]) {
t.Error("Clone should produce same digest")
}
// Writing to one shouldn't affect the other
h1.WriteString("more")
d1New := h1.Digest()
d2New := h2.Digest()
if bytes.Equal(d1New[:], d2New[:]) {
t.Error("Writing to original should not affect clone")
}
}
func TestReset(t *testing.T) {
h := New()
h.WriteString("test")
d1 := h.Digest()
h.Reset()
h.WriteString("test")
d2 := h.Digest()
if !bytes.Equal(d1[:], d2[:]) {
t.Error("Reset should restore initial state")
}
// After reset, different input should produce different hash
h.Reset()
h.WriteString("different")
d3 := h.Digest()
if bytes.Equal(d1[:], d3[:]) {
t.Error("After reset, different input should produce different hash")
}
}
func TestDigestLength(t *testing.T) {
if DigestLength != 64 {
t.Errorf("DigestLength = %d, want 64", DigestLength)
}
}
func BenchmarkHashBytes(b *testing.B) {
data := make([]byte, 1024)
for i := range data {
data[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = HashBytes(data)
}
}
func BenchmarkHashString(b *testing.B) {
data := strings.Repeat("benchmark", 128)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = HashString(data)
}
}
func BenchmarkWriteBigInt(b *testing.B) {
n := new(big.Int)
n.SetString("123456789012345678901234567890123456789012345678901234567890", 10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
h := New()
h.WriteBigInt(n)
_ = h.Digest()
}
}
-11
View File
@@ -1,11 +0,0 @@
// Copyright (C) 2020-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
@@ -1,103 +0,0 @@
// Copyright (C) 2020-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))
}
-299
View File
@@ -1,299 +0,0 @@
package hashing
import (
"bytes"
"encoding/hex"
"testing"
)
func TestComputeHash256(t *testing.T) {
// Test with known vectors
testCases := []struct {
name string
input []byte
expected string
}{
{
"empty",
[]byte(""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
"abc",
[]byte("abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
},
{
"fox",
[]byte("The quick brown fox jumps over the lazy dog"),
"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test ComputeHash256
hash := ComputeHash256(tc.input)
hashStr := hex.EncodeToString(hash)
if hashStr != tc.expected {
t.Errorf("ComputeHash256(%q) = %s, want %s", tc.input, hashStr, tc.expected)
}
// Test ComputeHash256Array
hashArray := ComputeHash256Array(tc.input)
hashArrayStr := hex.EncodeToString(hashArray[:])
if hashArrayStr != tc.expected {
t.Errorf("ComputeHash256Array(%q) = %s, want %s", tc.input, hashArrayStr, tc.expected)
}
// Verify slice and array produce same result
if !bytes.Equal(hash, hashArray[:]) {
t.Error("ComputeHash256 and ComputeHash256Array should produce same result")
}
})
}
}
func TestComputeHash160(t *testing.T) {
testCases := []struct {
name string
input []byte
expected string
}{
{
"empty",
[]byte(""),
"9c1185a5c5e9fc54612808977ee8f548b2258d31",
},
{
"abc",
[]byte("abc"),
"8eb208f7e05d987a9b044a8e98c6b087f15a0bfc",
},
{
"message digest",
[]byte("message digest"),
"5d0689ef49d2fae572b881b123a85ffa21595f36",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test ComputeHash160
hash := ComputeHash160(tc.input)
hashStr := hex.EncodeToString(hash)
if hashStr != tc.expected {
t.Errorf("ComputeHash160(%q) = %s, want %s", tc.input, hashStr, tc.expected)
}
// Test ComputeHash160Array
hashArray := ComputeHash160Array(tc.input)
hashArrayStr := hex.EncodeToString(hashArray[:])
if hashArrayStr != tc.expected {
t.Errorf("ComputeHash160Array(%q) = %s, want %s", tc.input, hashArrayStr, tc.expected)
}
// Verify slice and array produce same result
if !bytes.Equal(hash, hashArray[:]) {
t.Error("ComputeHash160 and ComputeHash160Array should produce same result")
}
})
}
}
func TestChecksum(t *testing.T) {
input := []byte("test input for checksum")
// Test various checksum lengths
lengths := []int{1, 4, 8, 16, 32}
for _, length := range lengths {
checksum := Checksum(input, length)
if len(checksum) != length {
t.Errorf("Checksum length should be %d, got %d", length, len(checksum))
}
// Verify checksum is last 'length' bytes of hash
fullHash := ComputeHash256Array(input)
expected := fullHash[len(fullHash)-length:]
if !bytes.Equal(checksum, expected) {
t.Errorf("Checksum should be last %d bytes of hash", length)
}
}
// Test that same input produces same checksum
checksum1 := Checksum(input, 4)
checksum2 := Checksum(input, 4)
if !bytes.Equal(checksum1, checksum2) {
t.Error("Same input should produce same checksum")
}
// Test that different input produces different checksum
input2 := []byte("different input")
checksum3 := Checksum(input2, 4)
if bytes.Equal(checksum1, checksum3) {
t.Error("Different input should produce different checksum")
}
}
func TestToHash256(t *testing.T) {
// Test valid conversion
validBytes := make([]byte, HashLen)
for i := range validBytes {
validBytes[i] = byte(i)
}
hash, err := ToHash256(validBytes)
if err != nil {
t.Errorf("ToHash256 with valid bytes should not error: %v", err)
}
if !bytes.Equal(hash[:], validBytes) {
t.Error("ToHash256 should copy bytes correctly")
}
// Test invalid lengths
invalidLengths := []int{0, 1, 31, 33, 100}
for _, length := range invalidLengths {
invalidBytes := make([]byte, length)
_, err := ToHash256(invalidBytes)
if err == nil {
t.Errorf("ToHash256 should error with %d bytes", length)
}
if err != nil && err.Error() != ErrInvalidHashLen.Error() && !bytes.Contains([]byte(err.Error()), []byte("invalid hash length")) {
t.Errorf("Expected ErrInvalidHashLen, got: %v", err)
}
}
}
func TestToHash160(t *testing.T) {
// Test valid conversion
validBytes := make([]byte, AddrLen)
for i := range validBytes {
validBytes[i] = byte(i)
}
hash, err := ToHash160(validBytes)
if err != nil {
t.Errorf("ToHash160 with valid bytes should not error: %v", err)
}
if !bytes.Equal(hash[:], validBytes) {
t.Error("ToHash160 should copy bytes correctly")
}
// Test invalid lengths
invalidLengths := []int{0, 1, 19, 21, 100}
for _, length := range invalidLengths {
invalidBytes := make([]byte, length)
_, err := ToHash160(invalidBytes)
if err == nil {
t.Errorf("ToHash160 should error with %d bytes", length)
}
if err != nil && err.Error() != ErrInvalidHashLen.Error() && !bytes.Contains([]byte(err.Error()), []byte("invalid hash length")) {
t.Errorf("Expected ErrInvalidHashLen, got: %v", err)
}
}
}
func TestPubkeyBytesToAddress(t *testing.T) {
// Test that address generation is consistent
pubkey := []byte("test public key")
addr1 := PubkeyBytesToAddress(pubkey)
addr2 := PubkeyBytesToAddress(pubkey)
if !bytes.Equal(addr1, addr2) {
t.Error("Same pubkey should produce same address")
}
// Test that different pubkeys produce different addresses
pubkey2 := []byte("different public key")
addr3 := PubkeyBytesToAddress(pubkey2)
if bytes.Equal(addr1, addr3) {
t.Error("Different pubkeys should produce different addresses")
}
// Test that address is 20 bytes (ripemd160 size)
if len(addr1) != AddrLen {
t.Errorf("Address should be %d bytes, got %d", AddrLen, len(addr1))
}
// Test empty pubkey
emptyAddr := PubkeyBytesToAddress([]byte{})
if len(emptyAddr) != AddrLen {
t.Errorf("Empty pubkey should still produce %d byte address", AddrLen)
}
}
func TestHashConstants(t *testing.T) {
// Verify constants match expected values
if HashLen != 32 {
t.Errorf("HashLen should be 32, got %d", HashLen)
}
if AddrLen != 20 {
t.Errorf("AddrLen should be 20, got %d", AddrLen)
}
}
func BenchmarkComputeHash256(b *testing.B) {
input := make([]byte, 1024)
for i := range input {
input[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ComputeHash256(input)
}
}
func BenchmarkComputeHash256Array(b *testing.B) {
input := make([]byte, 1024)
for i := range input {
input[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ComputeHash256Array(input)
}
}
func BenchmarkComputeHash160(b *testing.B) {
input := make([]byte, 1024)
for i := range input {
input[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ComputeHash160(input)
}
}
func BenchmarkPubkeyBytesToAddress(b *testing.B) {
pubkey := make([]byte, 65) // Typical pubkey size
for i := range pubkey {
pubkey[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = PubkeyBytesToAddress(pubkey)
}
}
func BenchmarkChecksum(b *testing.B) {
input := make([]byte, 1024)
for i := range input {
input[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Checksum(input, 4)
}
}
+97 -86
View File
@@ -9,8 +9,9 @@
package gpu
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
#cgo LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#cgo pkg-config: lux-crypto-only
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
#cgo linux LDFLAGS:
#include <stdint.h>
#include <stdlib.h>
@@ -20,6 +21,7 @@ package gpu
import "C"
import (
"encoding/binary"
"errors"
"unsafe"
@@ -29,9 +31,9 @@ import (
// 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
MSMThreshold = 64 // Min points for GPU MSM
PedersenThreshold = 32 // Min values for GPU Pedersen commit
IPAVerifyThreshold = 16 // Min proofs for GPU IPA verify
)
var (
@@ -57,6 +59,38 @@ 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) {
@@ -78,33 +112,27 @@ func MSM(points []banderwagon.Element, scalars []fr.Element) (*banderwagon.Eleme
return &result, err
}
// Convert points to C format (affine coordinates)
cPoints := make([]C.BanderwagonAffine, n)
for i := 0; i < n; i++ {
bytes := points[i].Bytes()
for j := 0; j < 32; j++ {
cPoints[i].x.bytes[j] = C.uint8_t(bytes[j])
}
// Y coordinate derived from X in banderwagon (not stored)
}
// Convert scalars to C format
cScalars := make([]C.BanderwagonScalar, n)
for i := 0; i < n; i++ {
bytes := scalars[i].Bytes()
for j := 0; j < 32; j++ {
cScalars[i].bytes[j] = C.uint8_t(bytes[j])
}
cScalars[i] = bytesToScalar(bytes[:])
}
// Allocate result
var cResult C.BanderwagonExtended
// 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.BanderwagonAffine)(unsafe.Pointer(&cPoints[0])),
(*C.BanderwagonScalar)(unsafe.Pointer(&cScalars[0])),
(*C.BanderwagonAffine)(unsafe.Pointer(&cPoints[0])),
C.uint32_t(n),
)
@@ -115,18 +143,7 @@ func MSM(points []banderwagon.Element, scalars []fr.Element) (*banderwagon.Eleme
return &result, err
}
// Convert result back to Go type
var resultBytes [32]byte
for i := 0; i < 32; i++ {
resultBytes[i] = byte(cResult.x.bytes[i])
}
var result banderwagon.Element
if err := result.SetBytes(resultBytes[:]); err != nil {
return nil, err
}
return &result, nil
return affineToPoint(&cResult)
}
// BatchMSM performs multiple MSM operations in parallel on GPU.
@@ -162,42 +179,47 @@ func BatchMSM(pointSets [][]banderwagon.Element, scalarSets [][]fr.Element) ([]*
return results, nil
}
// Prepare batch data
counts := make([]C.uint32_t, n)
totalPoints := 0
for i := 0; i < n; i++ {
counts[i] = C.uint32_t(len(pointSets[i]))
totalPoints += len(pointSets[i])
}
// Flatten all points and scalars
allPoints := make([]C.BanderwagonAffine, totalPoints)
allScalars := make([]C.BanderwagonScalar, totalPoints)
idx := 0
for i := 0; i < n; i++ {
for j := 0; j < len(pointSets[i]); j++ {
bytes := pointSets[i][j].Bytes()
for k := 0; k < 32; k++ {
allPoints[idx].x.bytes[k] = C.uint8_t(bytes[k])
// 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
}
sBytes := scalarSets[i][j].Bytes()
for k := 0; k < 32; k++ {
allScalars[idx].bytes[k] = C.uint8_t(sBytes[k])
}
idx++
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.BanderwagonExtended, n)
cResults := make([]C.BanderwagonAffine, n)
ret := C.metal_ipa_batch_msm(
ctx,
(*C.BanderwagonExtended)(unsafe.Pointer(&cResults[0])),
(*C.BanderwagonAffine)(unsafe.Pointer(&allPoints[0])),
(*C.BanderwagonAffine)(unsafe.Pointer(&cResults[0])),
(*C.BanderwagonScalar)(unsafe.Pointer(&allScalars[0])),
(*C.uint32_t)(unsafe.Pointer(&counts[0])),
(*C.BanderwagonAffine)(unsafe.Pointer(&cPoints[0])),
C.uint32_t(n),
C.uint32_t(vectorSize),
)
if ret != C.METAL_IPA_SUCCESS {
@@ -214,15 +236,11 @@ func BatchMSM(pointSets [][]banderwagon.Element, scalarSets [][]fr.Element) ([]*
// Convert results
for i := 0; i < n; i++ {
var resultBytes [32]byte
for j := 0; j < 32; j++ {
resultBytes[j] = byte(cResults[i].x.bytes[j])
}
var elem banderwagon.Element
if err := elem.SetBytes(resultBytes[:]); err != nil {
result, err := affineToPoint(&cResults[i])
if err != nil {
return nil, err
}
results[i] = &elem
results[i] = result
}
return results, nil
@@ -255,7 +273,7 @@ func BatchPedersenCommit(basis []banderwagon.Element, valueSets [][]fr.Element)
// 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) {
func VerkleCommitNode(children []banderwagon.Element, stem []byte) (*banderwagon.Element, error) {
if len(children) != 256 {
return nil, errors.New("Verkle node requires exactly 256 children")
}
@@ -282,20 +300,24 @@ func VerkleCommitNode(children []banderwagon.Element) (*banderwagon.Element, err
}
// Convert children to C format
cChildren := make([]C.BanderwagonExtended, 256)
cChildren := make([]C.BanderwagonAffine, 256)
for i := 0; i < 256; i++ {
bytes := children[i].Bytes()
for j := 0; j < 32; j++ {
cChildren[i].x.bytes[j] = C.uint8_t(bytes[j])
}
cChildren[i] = pointToAffine(&children[i])
}
var cResult C.BanderwagonExtended
// 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.BanderwagonExtended)(unsafe.Pointer(&cChildren[0])),
(*C.BanderwagonAffine)(unsafe.Pointer(&cChildren[0])),
(*C.uint8_t)(unsafe.Pointer(&stemBytes[0])),
)
if ret != C.METAL_IPA_SUCCESS {
@@ -309,18 +331,7 @@ func VerkleCommitNode(children []banderwagon.Element) (*banderwagon.Element, err
return &result, err
}
// Convert result
var resultBytes [32]byte
for i := 0; i < 32; i++ {
resultBytes[i] = byte(cResult.x.bytes[i])
}
var result banderwagon.Element
if err := result.SetBytes(resultBytes[:]); err != nil {
return nil, err
}
return &result, nil
return affineToPoint(&cResult)
}
// Destroy releases the Metal IPA context.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"crypto/subtle"
"errors"
"golang.org/x/crypto/curve25519"
"crypto/curve25519"
)
// X25519Impl implements X25519 as a KEM
+3 -3
View File
@@ -10,9 +10,9 @@
package gpu
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
#cgo darwin LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#cgo linux LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto
#cgo pkg-config: lux-crypto-only
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
#cgo linux LDFLAGS:
#include <stdint.h>
#include <stdlib.h>
+3 -3
View File
@@ -10,9 +10,9 @@
package gpu
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
#cgo darwin LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#cgo linux LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto
#cgo pkg-config: lux-crypto-only
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
#cgo linux LDFLAGS:
#include <stdint.h>
#include <stdlib.h>
+3 -3
View File
@@ -13,9 +13,9 @@
package gpu
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
#cgo darwin LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#cgo linux LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto
#cgo pkg-config: lux-crypto-only
#cgo darwin LDFLAGS: -framework Metal -framework Foundation
#cgo linux LDFLAGS:
#include <stdint.h>
#include <stdlib.h>
-286
View File
@@ -1,286 +0,0 @@
// Package sign provides post-quantum signature algorithms
package sign
import (
"crypto/rand"
"crypto/subtle"
"errors"
"fmt"
)
// SigID identifies a signature algorithm
type SigID string
const (
MLDSA2 SigID = "mldsa2"
MLDSA3 SigID = "mldsa3"
SLHDSA SigID = "slhdsa"
)
// Signer interface for signature algorithms
type Signer interface {
// GenerateKeyPair generates a new signing key pair
GenerateKeyPair() (PublicKey, PrivateKey, error)
// Sign creates a signature
Sign(sk PrivateKey, message []byte) ([]byte, error)
// Verify verifies a signature
Verify(pk PublicKey, message, signature []byte) bool
// PublicKeySize returns the size of public keys
PublicKeySize() int
// PrivateKeySize returns the size of private keys
PrivateKeySize() int
// SignatureSize returns the size of signatures
SignatureSize() int
}
// PublicKey represents a signature public key
type PublicKey interface {
Bytes() []byte
Equal(PublicKey) bool
}
// PrivateKey represents a signature private key
type PrivateKey interface {
Bytes() []byte
Public() PublicKey
Equal(PrivateKey) bool
}
// MLDSA2Impl implements ML-DSA-44 (Dilithium2)
type MLDSA2Impl struct{}
// NewMLDSA2 creates a new ML-DSA-44 instance
func NewMLDSA2() Signer {
return &MLDSA2Impl{}
}
// MLDSA2PublicKey represents an ML-DSA-44 public key
type MLDSA2PublicKey struct {
data []byte
}
// MLDSA2PrivateKey represents an ML-DSA-44 private key
type MLDSA2PrivateKey struct {
data []byte
pk *MLDSA2PublicKey
}
// Constants for ML-DSA-44 (Dilithium2)
const (
mldsa2PublicKeySize = 1312
mldsa2PrivateKeySize = 2528
mldsa2SignatureSize = 2420
)
// GenerateKeyPair generates a new ML-DSA-44 key pair
func (m *MLDSA2Impl) GenerateKeyPair() (PublicKey, PrivateKey, error) {
// Placeholder for actual ML-DSA key generation
// In production, this would use liboqs or native implementation
pk := &MLDSA2PublicKey{
data: make([]byte, mldsa2PublicKeySize),
}
sk := &MLDSA2PrivateKey{
data: make([]byte, mldsa2PrivateKeySize),
pk: pk,
}
// Generate random key material (placeholder)
if _, err := rand.Read(pk.data); err != nil {
return nil, nil, err
}
if _, err := rand.Read(sk.data); err != nil {
return nil, nil, err
}
return pk, sk, nil
}
// Sign creates a signature
func (m *MLDSA2Impl) Sign(sk PrivateKey, message []byte) ([]byte, error) {
mldsaSK, ok := sk.(*MLDSA2PrivateKey)
if !ok {
return nil, errors.New("invalid private key type")
}
signature := make([]byte, mldsa2SignatureSize)
// Placeholder for actual ML-DSA signing
if _, err := rand.Read(signature); err != nil {
return nil, err
}
// In production, this would perform actual ML-DSA signing
_ = mldsaSK.data
_ = message
return signature, nil
}
// Verify verifies a signature
func (m *MLDSA2Impl) Verify(pk PublicKey, message, signature []byte) bool {
mldsaPK, ok := pk.(*MLDSA2PublicKey)
if !ok {
return false
}
if len(signature) != mldsa2SignatureSize {
return false
}
// Placeholder for actual ML-DSA verification
// In production, this would perform actual verification
_ = mldsaPK.data
_ = message
_ = signature
// Placeholder: always return true for now
return true
}
// Size methods for ML-DSA-44
func (m *MLDSA2Impl) PublicKeySize() int { return mldsa2PublicKeySize }
func (m *MLDSA2Impl) PrivateKeySize() int { return mldsa2PrivateKeySize }
func (m *MLDSA2Impl) SignatureSize() int { return mldsa2SignatureSize }
// PublicKey methods
func (pk *MLDSA2PublicKey) Bytes() []byte {
return pk.data
}
func (pk *MLDSA2PublicKey) Equal(other PublicKey) bool {
otherPK, ok := other.(*MLDSA2PublicKey)
if !ok {
return false
}
return subtle.ConstantTimeCompare(pk.data, otherPK.data) == 1
}
// PrivateKey methods
func (sk *MLDSA2PrivateKey) Bytes() []byte {
return sk.data
}
func (sk *MLDSA2PrivateKey) Public() PublicKey {
return sk.pk
}
func (sk *MLDSA2PrivateKey) Equal(other PrivateKey) bool {
otherSK, ok := other.(*MLDSA2PrivateKey)
if !ok {
return false
}
return subtle.ConstantTimeCompare(sk.data, otherSK.data) == 1
}
// MLDSA3Impl implements ML-DSA-65 (Dilithium3)
type MLDSA3Impl struct{}
// NewMLDSA3 creates a new ML-DSA-65 instance
func NewMLDSA3() Signer {
return &MLDSA3Impl{}
}
// Constants for ML-DSA-65 (Dilithium3)
const (
mldsa3PublicKeySize = 1952
mldsa3PrivateKeySize = 4000
mldsa3SignatureSize = 3293
)
// GenerateKeyPair generates a new ML-DSA-65 key pair
func (m *MLDSA3Impl) GenerateKeyPair() (PublicKey, PrivateKey, error) {
pk := &MLDSA2PublicKey{
data: make([]byte, mldsa3PublicKeySize),
}
sk := &MLDSA2PrivateKey{
data: make([]byte, mldsa3PrivateKeySize),
pk: pk,
}
if _, err := rand.Read(pk.data); err != nil {
return nil, nil, err
}
if _, err := rand.Read(sk.data); err != nil {
return nil, nil, err
}
return pk, sk, nil
}
// Sign creates a signature
func (m *MLDSA3Impl) Sign(sk PrivateKey, message []byte) ([]byte, error) {
signature := make([]byte, mldsa3SignatureSize)
if _, err := rand.Read(signature); err != nil {
return nil, err
}
return signature, nil
}
// Verify verifies a signature
func (m *MLDSA3Impl) Verify(pk PublicKey, message, signature []byte) bool {
if len(signature) != mldsa3SignatureSize {
return false
}
// Placeholder
return true
}
// Size methods for ML-DSA-65
func (m *MLDSA3Impl) PublicKeySize() int { return mldsa3PublicKeySize }
func (m *MLDSA3Impl) PrivateKeySize() int { return mldsa3PrivateKeySize }
func (m *MLDSA3Impl) SignatureSize() int { return mldsa3SignatureSize }
// GetSigner returns a Signer implementation for the given ID
func GetSigner(id SigID) (Signer, error) {
switch id {
case MLDSA2:
return NewMLDSA2(), nil
case MLDSA3:
return NewMLDSA3(), nil
case SLHDSA:
// SLH-DSA would require additional implementation
return nil, errors.New("SLH-DSA not yet implemented")
default:
return nil, fmt.Errorf("unsupported signature algorithm: %s", id)
}
}
// TranscriptSigner signs protocol transcripts
type TranscriptSigner struct {
signer Signer
sk PrivateKey
}
// NewTranscriptSigner creates a new transcript signer
func NewTranscriptSigner(signer Signer, sk PrivateKey) *TranscriptSigner {
return &TranscriptSigner{
signer: signer,
sk: sk,
}
}
// SignTranscript signs a protocol transcript
func (ts *TranscriptSigner) SignTranscript(transcript []byte) ([]byte, error) {
// Add context string to prevent cross-protocol attacks
context := []byte("QZMQ-Transcript-v1")
message := append(context, transcript...)
return ts.signer.Sign(ts.sk, message)
}
// VerifyTranscript verifies a transcript signature
func VerifyTranscript(signer Signer, pk PublicKey, transcript, signature []byte) bool {
context := []byte("QZMQ-Transcript-v1")
message := append(context, transcript...)
return signer.Verify(pk, message, signature)
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package staking
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
)
// NewCertAndKeyBytes generates a new TLS certificate and key in PEM format
func NewCertAndKeyBytes() ([]byte, []byte, error) {
// Generate RSA key pair
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
// Create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"luxd"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour * 100), // 100 years
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// Generate the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, err
}
// Encode certificate to PEM
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
// Encode private key to PEM
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, nil, err
}
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privDER,
})
return certPEM, keyPEM, nil
}
// ParseCertificate parses a DER-encoded certificate
func ParseCertificate(der []byte) (*x509.Certificate, error) {
return x509.ParseCertificate(der)
}
+116 -159
View File
@@ -8,8 +8,9 @@
package gpu
/*
#cgo CFLAGS: -I${SRCDIR}/../../../../../luxcpp/crypto/include
#cgo LDFLAGS: -L${SRCDIR}/../../../../../luxcpp/crypto/build-local -lluxcrypto -framework Metal -framework Foundation
#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>
@@ -18,6 +19,7 @@ package gpu
import "C"
import (
"encoding/binary"
"errors"
"sync"
"unsafe"
@@ -36,10 +38,10 @@ const (
)
var (
gpuCtx *C.MetalIPAContext
gpuCtxOnce sync.Once
gpuCtxErr error
gpuCtxMu sync.Mutex
gpuCtx *C.MetalIPAContext
gpuCtxOnce sync.Once
gpuCtxErr error
gpuCtxMu sync.Mutex
)
// initGPU initializes the Metal IPA context for GPU acceleration.
@@ -58,6 +60,36 @@ 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) {
@@ -84,18 +116,17 @@ func CommitToPoly(poly []fr.Element) (*verkle.Point, error) {
cScalars := make([]C.BanderwagonScalar, n)
for i := 0; i < n; i++ {
bytes := poly[i].Bytes()
for j := 0; j < 32; j++ {
cScalars[i].bytes[j] = C.uint8_t(bytes[j])
}
cScalars[i] = bytesToScalar(bytes[:])
}
// Perform GPU commitment (uses precomputed Lagrange basis)
var cResult C.BanderwagonExtended
// 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),
)
@@ -105,20 +136,7 @@ func CommitToPoly(poly []fr.Element) (*verkle.Point, error) {
return cfg.CommitToPoly(poly, 0), nil
}
// Convert result to verkle.Point
var resultBytes [32]byte
for i := 0; i < 32; i++ {
resultBytes[i] = byte(cResult.x.bytes[i])
}
point := new(verkle.Point)
if err := point.SetBytes(resultBytes[:]); err != nil {
// Fall back to CPU on conversion error
cfg := verkle.GetConfig()
return cfg.CommitToPoly(poly, 0), nil
}
return point, nil
return affineToPoint(&cResult)
}
// BatchCommitToPoly performs batch polynomial commitments using GPU.
@@ -151,36 +169,39 @@ func BatchCommitToPoly(polys [][]fr.Element) ([]*verkle.Point, error) {
gpuCtxMu.Lock()
defer gpuCtxMu.Unlock()
// Prepare batch data
counts := make([]C.uint32_t, n)
totalScalars := 0
for i, poly := range polys {
counts[i] = C.uint32_t(len(poly))
totalScalars += len(poly)
// 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, totalScalars)
idx := 0
for _, poly := range polys {
for _, s := range poly {
allScalars := make([]C.BanderwagonScalar, n*vectorSize)
for i, poly := range polys {
for j, s := range poly {
bytes := s.Bytes()
for j := 0; j < 32; j++ {
allScalars[idx].bytes[j] = C.uint8_t(bytes[j])
}
idx++
allScalars[i*vectorSize+j] = bytesToScalar(bytes[:])
}
}
// Allocate results
cResults := make([]C.BanderwagonExtended, n)
cResults := make([]C.BanderwagonAffine, n)
ret := C.metal_ipa_batch_pedersen_commit(
gpuCtx,
(*C.BanderwagonExtended)(unsafe.Pointer(&cResults[0])),
(*C.BanderwagonAffine)(unsafe.Pointer(&cResults[0])),
(*C.BanderwagonScalar)(unsafe.Pointer(&allScalars[0])),
(*C.uint32_t)(unsafe.Pointer(&counts[0])),
nil, // No blindings
C.uint32_t(n),
C.uint32_t(vectorSize),
)
if ret != C.METAL_IPA_SUCCESS {
@@ -196,13 +217,8 @@ func BatchCommitToPoly(polys [][]fr.Element) ([]*verkle.Point, error) {
// Convert results
results := make([]*verkle.Point, n)
for i := 0; i < n; i++ {
var resultBytes [32]byte
for j := 0; j < 32; j++ {
resultBytes[j] = byte(cResults[i].x.bytes[j])
}
point := new(verkle.Point)
if err := point.SetBytes(resultBytes[:]); err != nil {
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)
@@ -214,136 +230,77 @@ func BatchCommitToPoly(polys [][]fr.Element) ([]*verkle.Point, error) {
return results, nil
}
// VerifyProof verifies an IPA proof using GPU acceleration.
func VerifyProof(
commitment *verkle.Point,
proof []byte,
point fr.Element,
evaluation fr.Element,
) (bool, error) {
// 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() {
return false, errors.New("GPU not available, use CPU verification")
// 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 {
return false, err
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 commitment
var cCommit C.BanderwagonExtended
commitBytes := commitment.Bytes()
for i := 0; i < 32; i++ {
cCommit.x.bytes[i] = C.uint8_t(commitBytes[i])
// 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
}
// Convert point and evaluation
var cPoint, cEval C.BanderwagonScalar
pointBytes := point.Bytes()
evalBytes := evaluation.Bytes()
for i := 0; i < 32; i++ {
cPoint.bytes[i] = C.uint8_t(pointBytes[i])
cEval.bytes[i] = C.uint8_t(evalBytes[i])
// Prepare stem (pad to 32 bytes if needed)
var stemBytes [32]byte
if len(stem) > 0 {
copy(stemBytes[:], stem)
}
// Prepare proof
if len(proof) == 0 {
return false, errors.New("empty proof")
}
cProof := (*C.uint8_t)(unsafe.Pointer(&proof[0]))
var cResult C.BanderwagonAffine
ret := C.metal_ipa_verify(
ret := C.metal_verkle_commit_node(
gpuCtx,
&cCommit,
cProof,
C.uint32_t(len(proof)),
&cPoint,
&cEval,
)
return ret == C.METAL_IPA_SUCCESS, nil
}
// BatchVerifyProofs verifies multiple IPA proofs in parallel on GPU.
func BatchVerifyProofs(
commitments []*verkle.Point,
proofs [][]byte,
points []fr.Element,
evaluations []fr.Element,
) ([]bool, error) {
n := len(commitments)
if n == 0 || n != len(proofs) || n != len(points) || n != len(evaluations) {
return nil, errors.New("mismatched input lengths")
}
if !Available() {
return nil, errors.New("GPU not available")
}
if err := initGPU(); err != nil {
return nil, err
}
gpuCtxMu.Lock()
defer gpuCtxMu.Unlock()
// Convert commitments
cCommits := make([]C.BanderwagonExtended, n)
for i := 0; i < n; i++ {
bytes := commitments[i].Bytes()
for j := 0; j < 32; j++ {
cCommits[i].x.bytes[j] = C.uint8_t(bytes[j])
}
}
// Convert points and evaluations
cPoints := make([]C.BanderwagonScalar, n)
cEvals := make([]C.BanderwagonScalar, n)
for i := 0; i < n; i++ {
pBytes := points[i].Bytes()
eBytes := evaluations[i].Bytes()
for j := 0; j < 32; j++ {
cPoints[i].bytes[j] = C.uint8_t(pBytes[j])
cEvals[i].bytes[j] = C.uint8_t(eBytes[j])
}
}
// Prepare proofs
proofPtrs := make([]*C.uint8_t, n)
proofLens := make([]C.uint32_t, n)
for i := 0; i < n; i++ {
if len(proofs[i]) > 0 {
proofPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&proofs[i][0]))
}
proofLens[i] = C.uint32_t(len(proofs[i]))
}
// Allocate results
cResults := make([]C.int, n)
ret := C.metal_ipa_batch_verify(
gpuCtx,
(*C.BanderwagonExtended)(unsafe.Pointer(&cCommits[0])),
(**C.uint8_t)(unsafe.Pointer(&proofPtrs[0])),
(*C.uint32_t)(unsafe.Pointer(&proofLens[0])),
(*C.BanderwagonScalar)(unsafe.Pointer(&cPoints[0])),
(*C.BanderwagonScalar)(unsafe.Pointer(&cEvals[0])),
C.uint32_t(n),
(*C.int)(unsafe.Pointer(&cResults[0])),
&cResult,
(*C.BanderwagonAffine)(unsafe.Pointer(&cChildren[0])),
(*C.uint8_t)(unsafe.Pointer(&stemBytes[0])),
)
if ret != C.METAL_IPA_SUCCESS {
return nil, errors.New("batch verification failed")
// 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
}
results := make([]bool, n)
for i := 0; i < n; i++ {
results[i] = cResults[i] != 0
}
return results, nil
return affineToPoint(&cResult)
}
// Destroy releases the Metal IPA context.