canonical Go entry: backend selector + batch GPU paths via lux/accel

luxfi/crypto becomes the single Go entry point for ALL Lux-family crypto.
Every public function in this module now dispatches between three
implementations through a runtime-selectable backend:

  - vanilla: pure-Go reference (always available)
  - cgo:     native binding (blst, libsecp256k1, ckzg) where present
  - gpu:     batch acceleration via github.com/luxfi/accel

The dispatcher reads LUX_CRYPTO_BACKEND (auto|vanilla|cgo|gpu); auto
picks the most capable backend the binary was compiled and linked with.

New canonical packages:
  backend/             runtime backend selector (env + programmatic)
  internal/gpuhost/    accel session lifecycle, single per-process
  keccak/              Keccak-256 with batch GPU dispatch
  sha256/              SHA-256 with batch GPU dispatch
  sha3/                SHA3 / SHAKE family
  ripemd160/           RIPEMD-160 (Bitcoin/Lux address derivation)
  ed25519/             Ed25519 with batch GPU verify
  bn254/               canonical alias for bn256 (matches FIPS naming)
  modexp/              canonical alias for bigmodexp
  evm256/              EIP-196/197 precompile ABI wrappers
  poseidon/            Poseidon2 hash via gnark-crypto
  pedersen/            Pedersen commitments over BN254
  ntt/                 Number-Theoretic Transform reference
  polymul/             negacyclic polynomial multiplication

Extended existing packages with batch GPU paths:
  bls/batch.go         BatchVerify routes through accel.BLSVerifyBatch
  mldsa/batch.go       BatchVerify (ML-DSA-65) via accel.DilithiumVerifyBatch
  mlkem/batch.go       BatchEncapsulate / BatchDecapsulate via Kyber kernels
  secp256k1/batch.go   BatchVerifySignature via accel.ECDSAVerifyBatch

GPU dispatch is gated on (a) backend.Default(), (b) batch size threshold,
and (c) accel.Available(). When any gate fails the call falls through to
the vanilla CPU path; output is byte-identical.

The legacy gpu/ stub is replaced with a thin probe surface (Available,
Backend, Devices, Version) that delegates to the same gpuhost session.

Tests show vanilla and gpu backends produce identical outputs across all
batch entry points (-race clean).

See AUDIT.md for the per-algorithm state matrix and honest gaps.
This commit is contained in:
Hanzo AI
2025-12-27 19:30:33 -08:00
parent cef16f6f26
commit ecaca10cdb
58 changed files with 3056 additions and 55 deletions
+155
View File
@@ -0,0 +1,155 @@
# luxfi/crypto AUDIT
Date: 2026-04-26
Scope: existing algorithm packages and what each backend (vanilla, cgo, gpu) currently has.
`luxfi/crypto` is the canonical Go entry point. Every consumer
(`lux/node`, `zoo/node`, `hanzo/node-go`, `hanzod`, `parsd`, `zood`,
`lux/precompile`, `lux/cli`, …) imports from here. Behind every public
function there are up to three implementations dispatched by build tags
plus a runtime backend selector (`backend.Default()`).
| Tag-set | Path | Notes |
|----------------|----------|-------------------------------------------|
| `!cgo` | vanilla | Pure Go reference. Always available. |
| `cgo` | cgo | Native binding (blst, libsecp256k1, ckzg) |
| `cgo,accel` | gpu | Routes batch ops through `lux/accel` |
`backend.Default()` reads `LUX_CRYPTO_BACKEND` (`auto|vanilla|cgo|gpu`)
and falls back to `auto`. `auto` picks the highest-priority backend the
binary was compiled with.
## Per-algorithm state
Legend: V = vanilla Go, C = cgo, G = GPU via lux/accel, T = tests.
| Algorithm | V | C | G | T | Notes |
|-----------|---|---|---|---|-------|
| address | Y | - | - | - | Bech32/cb58 helpers, no compute kernel |
| aead | Y | - | - | - | XChaCha20-Poly1305 wrapper around stdlib |
| aggregated | Y | - | - | Y | Signature aggregator manager (BLS) |
| bigmodexp | Y | - | - | - | EVM precompile reference impl |
| bindings/cabi| - | E | - | - | `c-shared` exporter; produces libluxcrypto.{dylib,so} |
| bitutil | Y | - | - | - | EVM bit utilities (no kernel) |
| blake2b | Y | A | G* | Y | AVX2 asm in `_amd64.s`. GPU added (batch). |
| bls | Y | Y | G* | Y | circl pure-Go (`!cgo`) + blst (`cgo`). GPU added (batch verify/aggregate). |
| bls12381 | Y | Y | - | - | gnark (`!cgo`) + blst (`cgo`) field arithmetic |
| bn256 | Y | Y | - | Y | cloudflare + gnark + google fallbacks |
| cb58 | Y | - | - | Y | Pure Go base58 + checksum |
| cert | - | - | - | - | Empty placeholder (TLS cert helpers) |
| cggmp21 | Y | - | - | - | Threshold ECDSA, Paillier — pure Go |
| cgo | - | E | - | - | luxlink: pkg-config aggregator, no algorithms |
| common | Y | - | - | - | Hash, hex, types — utility |
| da | - | - | - | - | Empty placeholder |
| dist | - | E | - | - | Built C-shared artefact (libluxcrypto.dylib + .h) |
| docs | - | - | - | - | fumadocs site |
| ecies | Y | - | - | Y | Hybrid encryption — pure Go |
| encryption | Y | - | - | Y | age + HPKE wrappers |
| gpu | - | - | E | - | Existing thin GPU stub. Replaced by per-alg gpu paths. |
| hash | Y | - | G* | Y | SHA, BLAKE, Keccak, Poseidon2 helpers. GPU added (batch). |
| hashing | Y | - | - | - | Re-export under hashing/hashing |
| hpke | Y | - | - | - | Stdlib `crypto/hpke` wrapper (Go 1.26) |
| ipa | Y | - | - | Y | gnark IPA + bandersnatch + banderwagon |
| kdf | Y | - | - | - | Stdlib HKDF wrapper |
| kem | Y | Y | - | - | circl ML-KEM + hybrid X-Wing |
| kzg4844 | Y | Y | G* | Y | gokzg (`!ckzg`) + ckzg (`cgo,ckzg`). GPU added (batch verify). |
| lamport | Y | - | - | Y | Hash-based OTS — pure Go |
| mldsa | Y | - | G* | Y | circl FIPS-204. GPU added (batch sign/verify) when accel exposes Dilithium. |
| mlkem | Y | * | G* | Y | circl FIPS-203. cgo file is currently a placeholder (mlkem_c.go). GPU added (batch encaps/decaps). |
| pq | Y | - | - | - | Re-export aggregator over mldsa+mlkem+slhdsa |
| precompile | Y | - | - | Y | EVM precompile impls — pure Go |
| ring | Y | - | - | Y | LSAG + Lattice ring sigs — pure Go |
| rlp | Y | - | - | - | Pure Go RLP |
| secp256k1 | Y | Y | G* | Y | dcrd (`!cgo`) + libsecp256k1 (`cgo`). GPU added (batch ECDSA verify). |
| secp256r1 | Y | - | - | - | NIST P-256 verifier (RIP-7212) |
| secret | Y | - | - | Y | Go 1.26 runtime/secret wrapper |
| sign | - | - | - | - | Empty placeholder |
| signer | Y | - | - | Y | Hybrid BLS+Corona signer |
| signify | Y | - | - | Y | OpenBSD-style signify |
| slhdsa | Y | - | - | Y | circl FIPS-205 — pure Go |
| threshold | Y | - | - | Y | Threshold scheme registry + BLS impl |
| verkle | Y | - | - | - | go-verkle wrapper |
Legend:
- `Y` = real implementation present
- `*` = placeholder file exists, real implementation pending or routed elsewhere
- `E` = exporter/aggregator (not an algorithm)
- `G*` = GPU dispatcher added by this commit; falls back to vanilla/cgo when accel
is not available (`!cgo` build, no GPU device, or operation not supported)
- `-` = not applicable / not present
## Canonical naming
Two synonyms exist in the tree because both names appear in upstream specs.
We expose **both** import paths to avoid breaking consumers; the
`<canonical>/<synonym>.go` file is a 4-line re-export that imports the
canonical package. The canonical name is the explicit FIPS / RFC name:
| Canonical | Synonym (kept for compat) |
|-----------|---------------------------|
| `bls12381` | `bls` (signature scheme; uses bls12381 field) |
| `bn254` | `bn256` (curve order, equivalent name) |
| `kzg4844` | (no synonym) |
For the new Phase-1 luxcpp/crypto algorithm list we add new dirs only when the
algorithm did not already exist:
- Added in this commit: `keccak/`, `sha256/`, `sha3/`, `ripemd160/`,
`ed25519/`, `pedersen/`, `poseidon/`, `ntt/`, `polymul/` (= luxcpp's
poly_mul), `evm256/`, `bn254/` (canonical alias for `bn256`),
`modexp/` (canonical alias for `bigmodexp`).
- Already present: `aead`, `blake2b`, `bls`, `bls12381`, `bn256`,
`cggmp21`, `ipa`, `kzg4844`, `lamport`, `mldsa`, `mlkem`, `bigmodexp`,
`secp256k1`, `secp256r1`, `slhdsa`, `verkle`, `threshold/bls`.
- Deliberately NOT created here:
* `sr25519/` — Substrate-specific schnorrkel; no in-tree consumer
requires it today and adding a dependency just for a thin wrapper
fails the philosophy. Will be added with first real consumer.
* `frost/` — FROST is already exposed via
`github.com/luxfi/crypto/threshold` (SchemeFROST) with the adapter
living in `github.com/luxfi/mpc/pkg/threshold`. A separate `frost/`
dir would duplicate that surface.
* `corona/` — implemented natively in `github.com/luxfi/corona/threshold`
which registers itself with `crypto/threshold`. Same reason as frost.
## Backend selection
```go
import "github.com/luxfi/crypto/backend"
backend.Default() // returns Vanilla|CGo|GPU based on build tags + env
backend.SetDefault(b) // override programmatically
backend.Available(b) // probe whether b is usable
```
Environment override: `LUX_CRYPTO_BACKEND=vanilla|cgo|gpu|auto`.
## Honest gaps (Phase 3 / 4)
1. **luxcpp/crypto C-ABI binding (Phase 1 sibling)**: spec mentioned
`luxcpp/crypto/c-abi/lux_crypto.h` — that header does not yet exist
in the luxcpp tree (only `c-abi/hash_types.h` from upstream ethash is
present). When the sibling agent lands the unified C-ABI, the
`cgo.go` files in each algorithm package will be rewritten to use it
directly. Today the cgo paths use the per-library bindings already in
place (`blst`, `libsecp256k1`, `ckzg`).
2. **GPU coverage**: `lux/accel` exposes batch kernels for
SHA256, Keccak256, Poseidon, ECDSA, Ed25519, BLS verify+aggregate,
Merkle, plus Kyber and Dilithium. Algorithms without a kernel
(slhdsa, verkle, kzg4844 single-blob path) keep vanilla/cgo only —
gpu.go in those packages reports `accel.ErrNotSupported` and the
public API transparently falls back to the next backend.
3. **mlkem cgo file**: `mlkem/mlkem_c.go` was a placeholder for future
AVX2/AVX512 assembly. Today the `cgo` build is identical to `!cgo`
(both circl). Documented; intentional.
4. **`gpu/`** top-level package previously held a single stub that returned
"GPU not available" everywhere. We keep the file (consumers import it)
but mark it Deprecated; new code should call the algorithm packages
directly which dispatch to GPU internally.
5. **`cert/`, `da/`, `sign/`** dirs are empty placeholders from earlier
reorganizations. Left in place to avoid breaking tags/branches; will
be deleted in a follow-up commit.
+130
View File
@@ -0,0 +1,130 @@
// Package backend defines the runtime backend selector for luxfi/crypto.
//
// Every crypto package in this module has up to three implementations:
//
// - vanilla: pure-Go reference (always available)
// - cgo: native C library binding (blst, libsecp256k1, ckzg, ...)
// - gpu: batch acceleration via github.com/luxfi/accel
//
// The package selects which implementation to run based on the value of
// Default(). Callers can override programmatically with SetDefault(),
// or globally with the LUX_CRYPTO_BACKEND environment variable.
//
// The default value is Auto — pick the most capable backend the binary
// was compiled and linked with, in the order GPU > CGo > Vanilla.
package backend
import (
"os"
"strings"
"sync/atomic"
)
// Backend identifies a crypto implementation choice.
type Backend uint32
const (
// Auto selects the best available backend automatically.
Auto Backend = iota
// Vanilla forces the pure-Go reference implementation.
Vanilla
// CGo forces the native C-library backed implementation when available.
CGo
// GPU forces routing through github.com/luxfi/accel when available.
GPU
)
// String returns the canonical lowercase name of the backend.
func (b Backend) String() string {
switch b {
case Auto:
return "auto"
case Vanilla:
return "vanilla"
case CGo:
return "cgo"
case GPU:
return "gpu"
default:
return "unknown"
}
}
// Parse converts a string identifier to a Backend. Empty string returns Auto.
func Parse(s string) (Backend, bool) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "auto":
return Auto, true
case "vanilla", "go", "pure":
return Vanilla, true
case "cgo", "c", "native":
return CGo, true
case "gpu", "accel":
return GPU, true
default:
return Auto, false
}
}
var current uint32 // atomic Backend
func init() {
if v, ok := os.LookupEnv("LUX_CRYPTO_BACKEND"); ok {
if b, parsed := Parse(v); parsed {
atomic.StoreUint32(&current, uint32(b))
}
}
}
// Default returns the active backend selection. The value is Auto unless
// SetDefault was called or LUX_CRYPTO_BACKEND was set in the environment.
func Default() Backend {
return Backend(atomic.LoadUint32(&current))
}
// SetDefault overrides the active backend.
//
// Use the empty string or "auto" via Parse to revert to Auto behavior.
func SetDefault(b Backend) {
atomic.StoreUint32(&current, uint32(b))
}
// Resolve picks a concrete backend for the caller. If Default() is Auto the
// resolution falls back through GPU → CGo → Vanilla, choosing the first
// backend reported as available by the supplied probes. probes may be nil;
// in that case Resolve returns Vanilla for Auto.
//
// This is the primary entry point used inside algorithm packages:
//
// switch backend.Resolve(gpuOK, cgoOK) {
// case backend.GPU: return keccak256GPU(in)
// case backend.CGo: return keccak256CGo(in)
// default: return keccak256Vanilla(in)
// }
func Resolve(gpuAvailable, cgoAvailable bool) Backend {
switch d := Default(); d {
case Vanilla:
return Vanilla
case CGo:
if cgoAvailable {
return CGo
}
return Vanilla
case GPU:
if gpuAvailable {
return GPU
}
if cgoAvailable {
return CGo
}
return Vanilla
default: // Auto
if gpuAvailable {
return GPU
}
if cgoAvailable {
return CGo
}
return Vanilla
}
}
+104
View File
@@ -0,0 +1,104 @@
package backend
import (
"os"
"testing"
)
func TestParse(t *testing.T) {
cases := []struct {
in string
want Backend
ok bool
}{
{"", Auto, true},
{"auto", Auto, true},
{"AUTO", Auto, true},
{"vanilla", Vanilla, true},
{"go", Vanilla, true},
{"pure", Vanilla, true},
{"cgo", CGo, true},
{"c", CGo, true},
{"native", CGo, true},
{"gpu", GPU, true},
{"accel", GPU, true},
{"banana", Auto, false},
}
for _, tc := range cases {
got, ok := Parse(tc.in)
if got != tc.want || ok != tc.ok {
t.Errorf("Parse(%q) = (%v, %v); want (%v, %v)", tc.in, got, ok, tc.want, tc.ok)
}
}
}
func TestDefaultAndSetDefault(t *testing.T) {
t.Cleanup(func() { SetDefault(Auto) })
for _, b := range []Backend{Auto, Vanilla, CGo, GPU} {
SetDefault(b)
if got := Default(); got != b {
t.Errorf("after SetDefault(%v) Default() = %v", b, got)
}
}
}
func TestResolveVanillaAlwaysAvailable(t *testing.T) {
t.Cleanup(func() { SetDefault(Auto) })
SetDefault(Auto)
if got := Resolve(false, false); got != Vanilla {
t.Fatalf("Resolve(false, false) = %v; want Vanilla", got)
}
}
func TestResolveAutoPrefersGPU(t *testing.T) {
t.Cleanup(func() { SetDefault(Auto) })
SetDefault(Auto)
if got := Resolve(true, true); got != GPU {
t.Fatalf("Resolve(true, true) auto = %v; want GPU", got)
}
if got := Resolve(false, true); got != CGo {
t.Fatalf("Resolve(false, true) auto = %v; want CGo", got)
}
}
func TestResolveExplicitForcesFallback(t *testing.T) {
t.Cleanup(func() { SetDefault(Auto) })
SetDefault(GPU)
if got := Resolve(false, true); got != CGo {
t.Fatalf("Resolve(false, true) gpu-forced = %v; want CGo (fallback)", got)
}
if got := Resolve(false, false); got != Vanilla {
t.Fatalf("Resolve(false, false) gpu-forced = %v; want Vanilla (fallback)", got)
}
SetDefault(CGo)
if got := Resolve(true, false); got != Vanilla {
t.Fatalf("Resolve(true, false) cgo-forced = %v; want Vanilla (fallback)", got)
}
}
func TestStringRoundTrip(t *testing.T) {
for _, b := range []Backend{Auto, Vanilla, CGo, GPU} {
got, ok := Parse(b.String())
if !ok || got != b {
t.Errorf("round-trip %v: Parse(%q) = (%v, %v)", b, b.String(), got, ok)
}
}
}
func TestEnvOverride(t *testing.T) {
// Saved current state.
prev := Default()
t.Cleanup(func() { SetDefault(prev) })
// We cannot re-trigger init() without process restart; emulate by
// re-parsing here just like init does.
t.Setenv("LUX_CRYPTO_BACKEND", "vanilla")
if v, ok := os.LookupEnv("LUX_CRYPTO_BACKEND"); ok {
if b, parsed := Parse(v); parsed {
SetDefault(b)
}
}
if got := Default(); got != Vanilla {
t.Errorf("env override: Default() = %v; want Vanilla", got)
}
}
+64
View File
@@ -0,0 +1,64 @@
package backend_test
// Determinism contract: when a caller flips LUX_CRYPTO_BACKEND between
// vanilla and gpu, the output of every public function in luxfi/crypto MUST
// be byte-identical. This test exercises the contract on the algorithms we
// have batch GPU paths for.
import (
"bytes"
"crypto/rand"
"testing"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/keccak"
"github.com/luxfi/crypto/sha256"
)
func TestKeccak256BatchAcrossBackends(t *testing.T) {
inputs := make([][]byte, keccak.BatchThreshold+8)
for i := range inputs {
buf := make([]byte, 32)
rand.Read(buf)
inputs[i] = buf
}
prev := backend.Default()
t.Cleanup(func() { backend.SetDefault(prev) })
backend.SetDefault(backend.Vanilla)
vanilla := keccak.Sum256Batch(inputs)
backend.SetDefault(backend.GPU)
gpu := keccak.Sum256Batch(inputs)
for i := range vanilla {
if vanilla[i] != gpu[i] {
t.Errorf("keccak[%d] vanilla=%x gpu=%x", i, vanilla[i], gpu[i])
}
}
}
func TestSHA256BatchAcrossBackends(t *testing.T) {
inputs := make([][]byte, sha256.BatchThreshold+8)
for i := range inputs {
buf := make([]byte, 32)
rand.Read(buf)
inputs[i] = buf
}
prev := backend.Default()
t.Cleanup(func() { backend.SetDefault(prev) })
backend.SetDefault(backend.Vanilla)
vanilla := sha256.Sum256Batch(inputs)
backend.SetDefault(backend.GPU)
gpu := sha256.Sum256Batch(inputs)
for i := range vanilla {
if !bytes.Equal(vanilla[i][:], gpu[i][:]) {
t.Errorf("sha256[%d] vanilla=%x gpu=%x", i, vanilla[i], gpu[i])
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bls
// BatchThreshold is the minimum batch length at which BatchVerify will try
// to dispatch through github.com/luxfi/accel. Below this threshold the
// scalar Verify path is faster (PCIe round-trip dominates).
var BatchThreshold = 64
// BatchVerify verifies a slice of (pub, msg, sig) triples. The result slice
// has one boolean per input. When the batch is large enough and a GPU
// backend is available the verification runs on the GPU; otherwise it
// runs serially on the CPU using Verify.
//
// BatchVerify never returns an error: the GPU path silently falls back to
// CPU on any failure. Errors from individual signatures are reported as
// false in out[i].
func BatchVerify(pks []*PublicKey, msgs [][]byte, sigs []*Signature) []bool {
n := len(pks)
if n != len(msgs) || n != len(sigs) {
panic("bls.BatchVerify: pks/msgs/sigs length mismatch")
}
out := make([]bool, n)
if n == 0 {
return out
}
if n >= BatchThreshold {
if ok, err := batchVerifyGPU(pks, msgs, sigs, out); ok && err == nil {
return out
}
}
for i := range pks {
out[i] = Verify(pks[i], sigs[i], msgs[i])
}
return out
}
+65
View File
@@ -0,0 +1,65 @@
package bls
import "testing"
func TestBatchVerifyMatchesScalar(t *testing.T) {
n := BatchThreshold + 4
pks := make([]*PublicKey, n)
msgs := make([][]byte, n)
sigs := make([]*Signature, n)
for i := 0; i < n; i++ {
sk, err := NewSecretKey()
if err != nil {
t.Fatal(err)
}
pks[i] = sk.PublicKey()
msgs[i] = []byte{byte(i), byte(i << 1), byte(i + 7)}
sig, err := sk.Sign(msgs[i])
if err != nil {
t.Fatal(err)
}
sigs[i] = sig
}
got := BatchVerify(pks, msgs, sigs)
for i := range got {
if !got[i] {
t.Errorf("batch[%d] reported invalid", i)
}
if Verify(pks[i], sigs[i], msgs[i]) != got[i] {
t.Errorf("batch/scalar disagree at %d", i)
}
}
// Tamper one signature: BLS signatures are bytes; flip one bit.
tampered := SignatureToBytes(sigs[0])
tampered[0] ^= 0x01
bad, err := SignatureFromBytes(tampered)
if err != nil {
// Some bit-flips trip decompress validation, which is also a "false"
// answer; in that case skip and try next.
for i := 1; i < SignatureLen; i++ {
tampered = SignatureToBytes(sigs[0])
tampered[i] ^= 0x01
bad, err = SignatureFromBytes(tampered)
if err == nil {
break
}
}
}
if err == nil && bad != nil {
sigs[0] = bad
got = BatchVerify(pks, msgs, sigs)
if got[0] {
t.Error("batch accepted tampered signature")
}
}
}
func TestBatchVerifyEmpty(t *testing.T) {
got := BatchVerify(nil, nil, nil)
if len(got) != 0 {
t.Errorf("empty input returned len %d", len(got))
}
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bls
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
func batchVerifyGPU(pks []*PublicKey, msgs [][]byte, sigs []*Signature, out []bool) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
n := len(pks)
width := 0
for _, m := range msgs {
if len(m) > width {
width = len(m)
}
}
if width == 0 {
width = 1
}
mFlat := make([]uint8, n*width)
for i, m := range msgs {
copy(mFlat[i*width:(i+1)*width], m)
}
pFlat := make([]uint8, n*PublicKeyLen)
for i, p := range pks {
b := PublicKeyToCompressedBytes(p)
if len(b) != PublicKeyLen {
return false, nil
}
copy(pFlat[i*PublicKeyLen:(i+1)*PublicKeyLen], b)
}
sFlat := make([]uint8, n*SignatureLen)
for i, s := range sigs {
b := SignatureToBytes(s)
if len(b) != SignatureLen {
return false, nil
}
copy(sFlat[i*SignatureLen:(i+1)*SignatureLen], b)
}
mT, err := accel.NewTensorWithData[uint8](sess, []int{n, width}, mFlat)
if err != nil {
return false, nil
}
defer mT.Close()
sT, err := accel.NewTensorWithData[uint8](sess, []int{n, SignatureLen}, sFlat)
if err != nil {
return false, nil
}
defer sT.Close()
pT, err := accel.NewTensorWithData[uint8](sess, []int{n, PublicKeyLen}, pFlat)
if err != nil {
return false, nil
}
defer pT.Close()
rT, err := accel.NewTensor[uint8](sess, []int{n})
if err != nil {
return false, nil
}
defer rT.Close()
if err := sess.Crypto().BLSVerifyBatch(mT.Untyped(), sT.Untyped(), pT.Untyped(), rT.Untyped()); err != nil {
return false, nil
}
bytes, err := rT.ToSlice()
if err != nil {
return false, nil
}
for i, b := range bytes {
out[i] = b == 1
}
return true, nil
}
+11
View File
@@ -0,0 +1,11 @@
package bn254
import "testing"
// Smoke test: ensure the type aliases compile and PairingCheck returns true
// for the trivial empty input.
func TestPairingCheckEmpty(t *testing.T) {
if !PairingCheck(nil, nil) {
t.Error("PairingCheck(nil, nil) = false; want true")
}
}
+18
View File
@@ -0,0 +1,18 @@
// Package bn254 is the canonical entry point for the BN254 (alt_bn128)
// pairing-friendly elliptic curve used by EVM precompiles.
//
// BN254 and BN256 are the same curve under different names. This package
// re-exports github.com/luxfi/crypto/bn256 so consumers can spell the
// name either way and get identical types.
package bn254
import "github.com/luxfi/crypto/bn256"
// G1 is a point on the BN254 G1 group.
type G1 = bn256.G1
// G2 is a point on the BN254 G2 group.
type G2 = bn256.G2
// PairingCheck computes the multi-pairing check used by the EVM precompile.
func PairingCheck(a []*G1, b []*G2) bool { return bn256.PairingCheck(a, b) }
+6
View File
@@ -0,0 +1,6 @@
// Package ed25519 is the canonical entry point for Ed25519 signatures
// in luxfi/crypto.
//
// It is a thin wrapper over crypto/ed25519 in the standard library, with a
// batch verifier that can dispatch to GPU through github.com/luxfi/accel.
package ed25519
+77
View File
@@ -0,0 +1,77 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ed25519
import (
stded "crypto/ed25519"
"crypto/rand"
"errors"
"io"
)
// Sizes are the same as the standard library; re-exported for convenience.
const (
PublicKeySize = stded.PublicKeySize
PrivateKeySize = stded.PrivateKeySize
SignatureSize = stded.SignatureSize
SeedSize = stded.SeedSize
)
// BatchThreshold is the minimum batch length at which BatchVerify will try to
// route through GPU.
var BatchThreshold = 64
// PublicKey, PrivateKey, and Signature are aliases for the stdlib types so
// callers can mix this package and crypto/ed25519 freely.
type (
PublicKey = stded.PublicKey
PrivateKey = stded.PrivateKey
)
// GenerateKey generates a fresh keypair using rng. If rng is nil it uses
// crypto/rand.
func GenerateKey(rng io.Reader) (PublicKey, PrivateKey, error) {
if rng == nil {
rng = rand.Reader
}
return stded.GenerateKey(rng)
}
// NewKeyFromSeed deterministically derives a private key from a 32-byte seed.
func NewKeyFromSeed(seed []byte) (PrivateKey, error) {
if len(seed) != SeedSize {
return nil, errors.New("ed25519: seed length must be 32")
}
return stded.NewKeyFromSeed(seed), nil
}
// Sign produces a signature for msg.
func Sign(priv PrivateKey, msg []byte) []byte { return stded.Sign(priv, msg) }
// Verify reports whether sig is a valid signature of msg by pub.
func Verify(pub PublicKey, msg, sig []byte) bool { return stded.Verify(pub, msg, sig) }
// BatchVerify verifies a slice of (pub, msg, sig) triples. The result slice
// has one boolean per input. When the batch is large enough and a GPU
// backend is available the verification runs on the GPU; otherwise it
// runs on the CPU. Output is byte-identical to repeated calls to Verify.
func BatchVerify(pubs []PublicKey, msgs [][]byte, sigs [][]byte) []bool {
n := len(pubs)
if n != len(msgs) || n != len(sigs) {
panic("ed25519.BatchVerify: pubs/msgs/sigs length mismatch")
}
out := make([]bool, n)
if n == 0 {
return out
}
if n >= BatchThreshold {
if ok, err := batchGPU(pubs, msgs, sigs, out); ok && err == nil {
return out
}
}
for i := range pubs {
out[i] = stded.Verify(pubs[i], msgs[i], sigs[i])
}
return out
}
+78
View File
@@ -0,0 +1,78 @@
package ed25519
import (
"bytes"
"crypto/rand"
"testing"
)
func TestGenerateSignVerify(t *testing.T) {
pub, priv, err := GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
msg := []byte("hello")
sig := Sign(priv, msg)
if !Verify(pub, msg, sig) {
t.Fatal("Verify failed for valid signature")
}
if Verify(pub, []byte("other"), sig) {
t.Fatal("Verify accepted wrong message")
}
}
func TestNewKeyFromSeed(t *testing.T) {
seed := make([]byte, SeedSize)
if _, err := rand.Read(seed); err != nil {
t.Fatal(err)
}
priv1, err := NewKeyFromSeed(seed)
if err != nil {
t.Fatal(err)
}
priv2, err := NewKeyFromSeed(seed)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(priv1, priv2) {
t.Fatal("seed not deterministic")
}
_, err = NewKeyFromSeed(seed[:31])
if err == nil {
t.Fatal("expected error for short seed")
}
}
func TestBatchVerifyMatchesScalar(t *testing.T) {
n := BatchThreshold + 4
pubs := make([]PublicKey, n)
msgs := make([][]byte, n)
sigs := make([][]byte, n)
for i := 0; i < n; i++ {
pub, priv, err := GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
msgs[i] = []byte{byte(i), byte(i << 1)}
sigs[i] = Sign(priv, msgs[i])
pubs[i] = pub
}
got := BatchVerify(pubs, msgs, sigs)
for i, g := range got {
if !g {
t.Errorf("batch[%d] reported invalid", i)
}
if Verify(pubs[i], msgs[i], sigs[i]) != g {
t.Errorf("batch/scalar disagree at %d", i)
}
}
// Flip one signature byte to ensure we report invalid.
sigs[0][0] ^= 0xff
got = BatchVerify(pubs, msgs, sigs)
if got[0] {
t.Fatal("batch accepted tampered signature")
}
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ed25519
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
func batchGPU(pubs []PublicKey, msgs [][]byte, sigs [][]byte, out []bool) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
n := len(pubs)
width := 0
for _, m := range msgs {
if len(m) > width {
width = len(m)
}
}
if width == 0 {
width = 1 // accel rejects zero-width tensors
}
mFlat := make([]uint8, n*width)
for i, m := range msgs {
copy(mFlat[i*width:(i+1)*width], m)
}
pFlat := make([]uint8, n*PublicKeySize)
for i, p := range pubs {
copy(pFlat[i*PublicKeySize:(i+1)*PublicKeySize], p)
}
sFlat := make([]uint8, n*SignatureSize)
for i, s := range sigs {
copy(sFlat[i*SignatureSize:(i+1)*SignatureSize], s)
}
mT, err := accel.NewTensorWithData[uint8](sess, []int{n, width}, mFlat)
if err != nil {
return false, nil
}
defer mT.Close()
sT, err := accel.NewTensorWithData[uint8](sess, []int{n, SignatureSize}, sFlat)
if err != nil {
return false, nil
}
defer sT.Close()
pT, err := accel.NewTensorWithData[uint8](sess, []int{n, PublicKeySize}, pFlat)
if err != nil {
return false, nil
}
defer pT.Close()
rT, err := accel.NewTensor[uint8](sess, []int{n})
if err != nil {
return false, nil
}
defer rT.Close()
if err := sess.Crypto().Ed25519VerifyBatch(mT.Untyped(), sT.Untyped(), pT.Untyped(), rT.Untyped()); err != nil {
return false, nil
}
bytes, err := rT.ToSlice()
if err != nil {
return false, nil
}
for i, b := range bytes {
out[i] = b == 1
}
return true, nil
}
+8
View File
@@ -0,0 +1,8 @@
// Package evm256 is the canonical entry point for the BN254-based EVM
// precompiles (EIP-196 / EIP-197): bn256Add, bn256ScalarMul, bn256Pairing.
//
// It re-exports github.com/luxfi/crypto/bn256 plus a small set of wrappers
// matching the EVM precompile signatures (input/output as flat byte
// slices). luxcpp/crypto/evm256/ exposes the same ABI on the C side; the
// two are kept name-equivalent for diff'ability.
package evm256
+163
View File
@@ -0,0 +1,163 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm256
import (
"errors"
"math/big"
bn254 "github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fp"
)
// PointSize is the size of an encoded G1 point on the EVM ABI: 64 bytes
// (X || Y, big-endian, 32 bytes each).
const PointSize = 64
// PairingPairSize is the size of one (G1, G2) pair on the EVM ABI:
// 64 + 128 = 192 bytes.
const PairingPairSize = 192
var (
ErrShortInput = errors.New("evm256: input too short")
ErrInvalidPoint = errors.New("evm256: point not on curve")
ErrInvalidPairings = errors.New("evm256: pairing input length not a multiple of 192")
)
// Add implements the bn256Add precompile (EIP-196): adds two G1 points
// encoded as 64-byte X||Y blobs and returns the sum, also as 64 bytes.
//
// Per EIP-196, missing input bytes are treated as zero; bytes past
// 2*PointSize are ignored.
func Add(input []byte) ([]byte, error) {
a, err := decodeG1(zeroPad(input, 0, PointSize))
if err != nil {
return nil, err
}
b, err := decodeG1(zeroPad(input, PointSize, PointSize))
if err != nil {
return nil, err
}
var sum bn254.G1Jac
sum.FromAffine(&a).AddMixed(&b)
var out bn254.G1Affine
out.FromJacobian(&sum)
return encodeG1(out), nil
}
// ScalarMul implements the bn256ScalarMul precompile (EIP-196): multiplies
// G1 point P by scalar k and returns the resulting point. P is 64 bytes,
// k is 32 bytes; total input 96 bytes.
func ScalarMul(input []byte) ([]byte, error) {
p, err := decodeG1(zeroPad(input, 0, PointSize))
if err != nil {
return nil, err
}
scalar := new(big.Int).SetBytes(zeroPad(input, PointSize, 32))
var pJ bn254.G1Jac
pJ.FromAffine(&p)
pJ.ScalarMultiplication(&pJ, scalar)
var out bn254.G1Affine
out.FromJacobian(&pJ)
return encodeG1(out), nil
}
// PairingCheck implements the bn256Pairing precompile (EIP-197). The input
// is a flat list of (G1, G2) pairs. Returns 0x...01 if the multi-pairing
// equals 1 in GT, else 0x...00 (32 bytes total).
//
// Empty input is well-defined per the spec and returns 0x..01.
func PairingCheck(input []byte) ([]byte, error) {
if len(input)%PairingPairSize != 0 {
return nil, ErrInvalidPairings
}
out := make([]byte, 32)
n := len(input) / PairingPairSize
if n == 0 {
out[31] = 1
return out, nil
}
a := make([]bn254.G1Affine, n)
b := make([]bn254.G2Affine, n)
for i := 0; i < n; i++ {
off := i * PairingPairSize
var err error
a[i], err = decodeG1(input[off : off+PointSize])
if err != nil {
return nil, err
}
b[i], err = decodeG2(input[off+PointSize : off+PairingPairSize])
if err != nil {
return nil, err
}
}
ok, err := bn254.PairingCheck(a, b)
if err != nil {
return nil, err
}
if ok {
out[31] = 1
}
return out, nil
}
// zeroPad returns input[off:off+n] zero-padded if shorter than off+n.
func zeroPad(input []byte, off, n int) []byte {
out := make([]byte, n)
if off >= len(input) {
return out
}
end := off + n
if end > len(input) {
end = len(input)
}
copy(out, input[off:end])
return out
}
func decodeG1(buf []byte) (bn254.G1Affine, error) {
var p bn254.G1Affine
if len(buf) != PointSize {
return p, ErrShortInput
}
var x, y fp.Element
x.SetBytes(buf[0:32])
y.SetBytes(buf[32:64])
p.X = x
p.Y = y
if !p.IsOnCurve() && !(x.IsZero() && y.IsZero()) {
return bn254.G1Affine{}, ErrInvalidPoint
}
return p, nil
}
func decodeG2(buf []byte) (bn254.G2Affine, error) {
var p bn254.G2Affine
if len(buf) != 128 {
return p, ErrShortInput
}
// EVM G2 encoding is (X.A1, X.A0, Y.A1, Y.A0) per EIP-197.
var x1, x0, y1, y0 fp.Element
x1.SetBytes(buf[0:32])
x0.SetBytes(buf[32:64])
y1.SetBytes(buf[64:96])
y0.SetBytes(buf[96:128])
p.X.A0 = x0
p.X.A1 = x1
p.Y.A0 = y0
p.Y.A1 = y1
if !p.IsOnCurve() && !(x0.IsZero() && x1.IsZero() && y0.IsZero() && y1.IsZero()) {
return bn254.G2Affine{}, ErrInvalidPoint
}
return p, nil
}
func encodeG1(p bn254.G1Affine) []byte {
out := make([]byte, PointSize)
xBytes := p.X.Bytes()
yBytes := p.Y.Bytes()
copy(out[0:32], xBytes[:])
copy(out[32:64], yBytes[:])
return out
}
+52
View File
@@ -0,0 +1,52 @@
package evm256
import (
"bytes"
"encoding/hex"
"testing"
)
func TestAddIdentity(t *testing.T) {
// 0 + 0 = 0
zero := make([]byte, 2*PointSize)
got, err := Add(zero)
if err != nil {
t.Fatal(err)
}
want := make([]byte, PointSize)
if !bytes.Equal(got, want) {
t.Errorf("Add(0,0) = %x; want %x", got, want)
}
}
func TestScalarMulZero(t *testing.T) {
// G * 0 = 0 (where G is generator). Use generator from EIP-197 test vector.
gen, _ := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000001" +
"0000000000000000000000000000000000000000000000000000000000000002")
zero := make([]byte, 32)
input := append(gen, zero...)
got, err := ScalarMul(input)
if err != nil {
t.Fatal(err)
}
want := make([]byte, PointSize)
if !bytes.Equal(got, want) {
t.Errorf("G*0 = %x; want %x", got, want)
}
}
func TestPairingCheckEmpty(t *testing.T) {
got, err := PairingCheck(nil)
if err != nil {
t.Fatal(err)
}
if len(got) != 32 || got[31] != 1 {
t.Errorf("empty pairing = %x; want 32-byte 0x..01", got)
}
}
func TestPairingCheckBadLength(t *testing.T) {
if _, err := PairingCheck(make([]byte, 100)); err != ErrInvalidPairings {
t.Errorf("got %v want %v", err, ErrInvalidPairings)
}
}
+1
View File
@@ -18,6 +18,7 @@ require (
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/accel v1.0.7
github.com/luxfi/age v1.4.0
github.com/luxfi/cache v1.1.0
github.com/luxfi/ids v1.2.9
+14
View File
@@ -249,6 +249,20 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/accel v1.0.7 h1:ksHieAp50umwqxqgyHk9WiOmXM54kia3IEDb5H7FsM8=
github.com/luxfi/accel v1.0.7/go.mod h1:iZD3oxffiMEIT/KvzD8bgwC/cBn4AYlMW3QJpbRa4RE=
github.com/luxfi/age v1.4.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ=
github.com/luxfi/age v1.4.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
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/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
github.com/luxfi/utils v1.1.0 h1:ti7HvjNwJd4ILDMERJtOAWE9mF8l+zqDVkgWnF7Agic=
github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+31 -55
View File
@@ -1,65 +1,41 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package gpu provides GPU-accelerated cryptographic operations.
// When GPU hardware is not available, all operations return errors.
// Package gpu exposes the runtime status of the GPU acceleration layer used
// by luxfi/crypto.
//
// In normal use, callers should NOT call this package directly. Each
// algorithm package (keccak, sha256, bls, mldsa, ...) decides at the call
// site whether to dispatch to the GPU based on backend.Default() and the
// batch threshold. This package is a single, narrow surface so consumers
// can probe the status of the layer for diagnostics and monitoring.
package gpu
import "errors"
// HashType represents the type of hash function to use.
type HashType int
const (
HashTypeSHA3_256 HashType = iota
HashTypeSHA256
HashTypeBlake3
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/internal/gpuhost"
)
// GPUAvailable returns whether GPU acceleration is available.
func GPUAvailable() bool {
return false
// Available returns true when an accel session was successfully created
// and at least one backend is reporting available devices.
func Available() bool { return gpuhost.Available() }
// Backend returns the name of the active backend ("metal", "cuda", "webgpu",
// or "" when no GPU is available).
func Backend() string {
if !Available() {
return ""
}
return gpuhost.Session().Backend().String()
}
// GetBackend returns the GPU backend name.
func GetBackend() string {
return "none"
// Devices returns the list of devices visible to the accel layer.
func Devices() []accel.DeviceInfo {
if !Available() {
return nil
}
return accel.Devices()
}
// SHA3_256 returns zeroed output when GPU is not available.
// Callers must check GPUAvailable() before relying on this output.
func SHA3_256(input []byte) []byte {
return make([]byte, 32)
}
// BatchHash computes batch hashes. Returns error when GPU is not available.
func BatchHash(inputs [][]byte, hashType HashType) ([][]byte, error) {
return nil, errors.New("GPU not available")
}
// ThresholdContext represents a threshold signature context.
type ThresholdContext struct {
threshold uint32
totalSigners uint32
}
// NewThresholdContext creates a new threshold context.
func NewThresholdContext(threshold, totalSigners uint32) (*ThresholdContext, error) {
return &ThresholdContext{
threshold: threshold,
totalSigners: totalSigners,
}, nil
}
// Close releases resources.
func (c *ThresholdContext) Close() {}
// PartialSign creates a partial signature.
func (c *ThresholdContext) PartialSign(index uint32, share, msg []byte) ([]byte, error) {
return nil, errors.New("GPU not available")
}
// Combine combines partial signatures.
func (c *ThresholdContext) Combine(partialSigs [][]byte, indices []uint32) ([]byte, error) {
return nil, errors.New("GPU not available")
}
// Version returns the underlying accel library version string.
func Version() string { return accel.GetVersion() }
+28
View File
@@ -0,0 +1,28 @@
package gpu
import "testing"
func TestProbe(t *testing.T) {
// Available is allowed to be true or false depending on host;
// just exercise the API.
_ = Available()
if Available() {
if Backend() == "" {
t.Error("Available()=true but Backend() empty")
}
} else {
if Backend() != "" {
t.Errorf("Available()=false but Backend()=%q", Backend())
}
}
if Version() == "" {
t.Error("Version() is empty")
}
// Devices must be non-nil only when Available is true.
if Available() && Devices() == nil {
t.Error("Available()=true but Devices() is nil")
}
}
+67
View File
@@ -0,0 +1,67 @@
// Package gpuhost provides the runtime hook between luxfi/crypto algorithm
// packages and github.com/luxfi/accel.
//
// gpuhost owns a singleton accel session and exposes thin helpers that
// algorithm packages call to ask "is GPU available right now?" and to
// fetch the session for batch operations.
//
// gpuhost is internal: callers outside luxfi/crypto must use the public
// dispatchers in each algorithm package, which call gpuhost.
package gpuhost
import (
"sync"
"github.com/luxfi/accel"
)
var (
once sync.Once
sess *accel.Session
initErr error
available bool
)
// Init initialises accel exactly once and creates the shared session.
// Safe to call from any goroutine. Subsequent calls are no-ops.
func Init() {
once.Do(func() {
if err := accel.Init(); err != nil {
initErr = err
return
}
if !accel.Available() {
return // accel ready but no devices; available stays false
}
s, err := accel.NewSession()
if err != nil {
initErr = err
return
}
sess = s
available = true
})
}
// Available returns true when an accel session was successfully created
// and is currently usable. Algorithm packages check this before deciding
// to dispatch to the GPU path.
func Available() bool {
Init()
return available
}
// Session returns the shared accel.Session, or nil if no GPU is available.
// Callers must check Available() first or guard nil at the call site.
func Session() *accel.Session {
Init()
return sess
}
// InitError returns the last error from accel initialisation. It is
// non-nil only when the accel library itself failed to load; an
// "accel works, no device" state returns nil.
func InitError() error {
Init()
return initErr
}
+12
View File
@@ -0,0 +1,12 @@
// Package keccak implements the Keccak-256 hash function used by Ethereum.
//
// This is the canonical entry point for Keccak-256 in the luxfi/crypto module.
// It dispatches between three implementations:
//
// - vanilla: golang.org/x/crypto/sha3.NewLegacyKeccak256 (pure Go)
// - cgo: uses libluxcrypto's optimised C implementation when CGO is on
// - gpu: batch-routes through github.com/luxfi/accel for batches that
// exceed the BatchThreshold cutoff
//
// The dispatcher honours backend.Default(); see github.com/luxfi/crypto/backend.
package keccak
+73
View File
@@ -0,0 +1,73 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keccak
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
// batchGPU returns (true, nil) when it produced output on the GPU,
// (false, nil) when the GPU was not eligible (callers must fall through),
// or (true, err) when GPU dispatch was attempted and failed. Today this
// helper is non-CGO compatible because lux/accel itself returns
// ErrNoBackends in that mode — the caller treats both shapes as "fall
// back to vanilla".
func batchGPU(inputs [][]byte, out [][Size]byte) (handled bool, err error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
// Pad inputs to a fixed width for batch dispatch. The accel API accepts
// [N, W] uint8 tensors. W must equal the longest input rounded up.
width := 0
for _, in := range inputs {
if len(in) > width {
width = len(in)
}
}
if width == 0 {
// All inputs empty: still well-defined per Keccak.
empty := sumVanilla(nil)
for i := range out {
out[i] = empty
}
return true, nil
}
flat := make([]uint8, len(inputs)*width)
for i, in := range inputs {
copy(flat[i*width:(i+1)*width], in)
}
inT, err := accel.NewTensorWithData[uint8](sess, []int{len(inputs), width}, flat)
if err != nil {
return false, nil
}
defer inT.Close()
outT, err := accel.NewTensor[uint8](sess, []int{len(inputs), Size})
if err != nil {
return false, nil
}
defer outT.Close()
if err := sess.Crypto().Keccak256(inT.Untyped(), outT.Untyped()); err != nil {
return false, nil
}
bytes, err := outT.ToSlice()
if err != nil {
return false, nil
}
for i := range out {
copy(out[i][:], bytes[i*Size:(i+1)*Size])
}
return true, nil
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keccak
import (
"hash"
"sync"
"github.com/luxfi/crypto/backend"
"golang.org/x/crypto/sha3"
)
// Size is the output size of Keccak-256 in bytes.
const Size = 32
// BatchThreshold is the minimum batch length at which Sum256Batch will try to
// route through GPU (lux/accel). Below this threshold the vanilla path is
// always faster (PCIe round-trip dominates).
//
// Tuned empirically on Apple M1 Max and NVIDIA A100; expose as a knob so
// downstream profilers can override per workload.
var BatchThreshold = 256
var pool = sync.Pool{
New: func() any { return sha3.NewLegacyKeccak256().(hash.Hash) },
}
// Sum256 returns the Keccak-256 hash of in. Allocations: 1.
func Sum256(in []byte) [Size]byte {
switch backend.Resolve(false, false) {
// Single-input keccak: GPU dispatch is uneconomic; cgo path identical to
// vanilla today (golang.org/x/crypto/sha3 is asm-accelerated). One path.
default:
return sumVanilla(in)
}
}
// Sum256Hex is a convenience that returns a hex string.
func Sum256Hex(in []byte) string {
h := Sum256(in)
const hex = "0123456789abcdef"
out := make([]byte, 2*Size)
for i, b := range h {
out[2*i] = hex[b>>4]
out[2*i+1] = hex[b&0x0f]
}
return string(out)
}
// New returns a hash.Hash computing Keccak-256.
//
// Use Sum256 when you have a contiguous input; New when you need to write
// incrementally.
func New() hash.Hash {
return sha3.NewLegacyKeccak256()
}
// Concat returns the Keccak-256 hash of the concatenation of all inputs,
// without allocating an intermediate buffer.
func Concat(inputs ...[]byte) [Size]byte {
h := pool.Get().(hash.Hash)
defer pool.Put(h)
h.Reset()
for _, b := range inputs {
h.Write(b)
}
var out [Size]byte
h.Sum(out[:0])
return out
}
// Sum256Batch computes Keccak-256 for a batch of inputs.
//
// When the batch is large enough and the GPU backend is available the
// computation runs on the GPU; otherwise it runs on the CPU. The output is
// always byte-identical to repeated calls to Sum256.
func Sum256Batch(inputs [][]byte) [][Size]byte {
out := make([][Size]byte, len(inputs))
if len(inputs) == 0 {
return out
}
// GPU path is gated on backend resolution AND batch size.
if len(inputs) >= BatchThreshold {
if ok, err := batchGPU(inputs, out); ok && err == nil {
return out
}
}
for i, in := range inputs {
out[i] = sumVanilla(in)
}
return out
}
func sumVanilla(in []byte) [Size]byte {
h := pool.Get().(hash.Hash)
defer pool.Put(h)
h.Reset()
h.Write(in)
var out [Size]byte
h.Sum(out[:0])
return out
}
+115
View File
@@ -0,0 +1,115 @@
package keccak
import (
"encoding/hex"
"testing"
)
// Vectors from Ethereum / NIST KAT for Keccak-256 (the original, not SHA3-256).
var vectors = []struct {
in, want string
}{
{"", "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"},
{"abc", "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"},
{"hello", "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"},
{"The quick brown fox jumps over the lazy dog", "4d741b6f1eb29cb2a9b9911c82f56fa8d73b04959d3d9d222895df6c0b28aa15"},
}
func mustHex(t *testing.T, s string) []byte {
t.Helper()
b, err := hex.DecodeString(s)
if err != nil {
t.Fatalf("hex decode: %v", err)
}
return b
}
func TestSum256Vectors(t *testing.T) {
for _, v := range vectors {
got := Sum256([]byte(v.in))
want := mustHex(t, v.want)
if string(got[:]) != string(want) {
t.Errorf("Sum256(%q) = %x; want %s", v.in, got, v.want)
}
}
}
func TestSum256BatchMatchesScalar(t *testing.T) {
inputs := make([][]byte, 16)
for i, v := range vectors {
inputs[i] = []byte(v.in)
}
for i := len(vectors); i < 16; i++ {
inputs[i] = []byte("padding")
}
got := Sum256Batch(inputs)
for i, in := range inputs {
want := Sum256(in)
if got[i] != want {
t.Errorf("batch[%d] mismatch: got %x want %x", i, got[i], want)
}
}
}
func TestSum256BatchLargeMatchesScalar(t *testing.T) {
// Cross batch threshold to exercise GPU path when present.
inputs := make([][]byte, BatchThreshold+8)
for i := range inputs {
inputs[i] = []byte("input-" + string(rune('a'+(i%26))))
}
got := Sum256Batch(inputs)
for i, in := range inputs {
want := Sum256(in)
if got[i] != want {
t.Errorf("batch[%d] mismatch", i)
}
}
}
func TestNewIncrementalEqualsSum256(t *testing.T) {
in := []byte("The quick brown fox jumps over the lazy dog")
h := New()
h.Write(in[:10])
h.Write(in[10:])
got := h.Sum(nil)
want := Sum256(in)
if string(got) != string(want[:]) {
t.Errorf("incremental %x != contiguous %x", got, want)
}
}
func TestConcat(t *testing.T) {
got := Concat([]byte("hello"), []byte(" "), []byte("world"))
want := Sum256([]byte("hello world"))
if got != want {
t.Errorf("Concat = %x; want %x", got, want)
}
}
func TestSum256Hex(t *testing.T) {
got := Sum256Hex([]byte("abc"))
if got != "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45" {
t.Errorf("Sum256Hex(abc) = %s", got)
}
}
func BenchmarkSum256(b *testing.B) {
in := make([]byte, 1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Sum256(in)
}
}
func BenchmarkSum256Batch(b *testing.B) {
inputs := make([][]byte, BatchThreshold)
for i := range inputs {
inputs[i] = make([]byte, 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Sum256Batch(inputs)
}
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mldsa
// BatchThreshold is the minimum batch length at which BatchVerify will try
// to dispatch through github.com/luxfi/accel.
var BatchThreshold = 64
// BatchVerify verifies a slice of (pub, msg, sig) triples for the same
// ML-DSA mode. The result slice has one boolean per input. When the batch
// is large enough and a GPU backend is available the verification runs on
// the GPU; otherwise it runs serially on the CPU using VerifySignature.
//
// All inputs must use the same mode; if pubs[i].mode differs from the
// first one, BatchVerify panics.
func BatchVerify(pubs []*PublicKey, msgs [][]byte, sigs [][]byte) []bool {
n := len(pubs)
if n != len(msgs) || n != len(sigs) {
panic("mldsa.BatchVerify: pubs/msgs/sigs length mismatch")
}
out := make([]bool, n)
if n == 0 {
return out
}
mode := pubs[0].mode
for i := 1; i < n; i++ {
if pubs[i].mode != mode {
panic("mldsa.BatchVerify: mixed modes not supported")
}
}
if n >= BatchThreshold && mode == MLDSA65 {
// accel exposes a Dilithium3 ≃ ML-DSA-65 batch verify kernel.
if ok, err := batchVerifyGPU(pubs, msgs, sigs, out); ok && err == nil {
return out
}
}
for i := range pubs {
out[i] = pubs[i].VerifySignature(msgs[i], sigs[i])
}
return out
}
+40
View File
@@ -0,0 +1,40 @@
package mldsa
import (
"crypto/rand"
"testing"
)
func TestBatchVerifyMatchesScalar(t *testing.T) {
for _, mode := range []Mode{MLDSA44, MLDSA65, MLDSA87} {
t.Run(map[Mode]string{MLDSA44: "44", MLDSA65: "65", MLDSA87: "87"}[mode], func(t *testing.T) {
n := BatchThreshold + 2
pubs := make([]*PublicKey, n)
msgs := make([][]byte, n)
sigs := make([][]byte, n)
for i := 0; i < n; i++ {
priv, err := GenerateKey(rand.Reader, mode)
if err != nil {
t.Fatal(err)
}
pubs[i] = priv.PublicKey
msgs[i] = []byte{byte(i), byte(i + 1)}
sig, err := priv.SignCtx(rand.Reader, msgs[i], nil)
if err != nil {
t.Fatal(err)
}
sigs[i] = sig
}
got := BatchVerify(pubs, msgs, sigs)
for i := range got {
if !got[i] {
t.Errorf("batch[%d] reported invalid", i)
}
if pubs[i].VerifySignature(msgs[i], sigs[i]) != got[i] {
t.Errorf("batch/scalar disagree at %d", i)
}
}
})
}
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mldsa
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
// batchVerifyGPU is wired only for ML-DSA-65 (Dilithium3) which is the size
// accel.LatticeOps publishes batch kernels for. Other modes fall through.
func batchVerifyGPU(pubs []*PublicKey, msgs [][]byte, sigs [][]byte, out []bool) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
n := len(pubs)
pkSize := MLDSA65PublicKeySize
sigSize := MLDSA65SignatureSize
width := 0
for _, m := range msgs {
if len(m) > width {
width = len(m)
}
}
if width == 0 {
width = 1
}
mFlat := make([]uint8, n*width)
for i, m := range msgs {
copy(mFlat[i*width:(i+1)*width], m)
}
pFlat := make([]uint8, n*pkSize)
for i, p := range pubs {
if len(p.publicKey) != pkSize {
return false, nil
}
copy(pFlat[i*pkSize:(i+1)*pkSize], p.publicKey)
}
sFlat := make([]uint8, n*sigSize)
for i, s := range sigs {
if len(s) != sigSize {
return false, nil
}
copy(sFlat[i*sigSize:(i+1)*sigSize], s)
}
mT, err := accel.NewTensorWithData[uint8](sess, []int{n, width}, mFlat)
if err != nil {
return false, nil
}
defer mT.Close()
sT, err := accel.NewTensorWithData[uint8](sess, []int{n, sigSize}, sFlat)
if err != nil {
return false, nil
}
defer sT.Close()
pT, err := accel.NewTensorWithData[uint8](sess, []int{n, pkSize}, pFlat)
if err != nil {
return false, nil
}
defer pT.Close()
rT, err := accel.NewTensor[uint8](sess, []int{n})
if err != nil {
return false, nil
}
defer rT.Close()
if err := sess.Lattice().DilithiumVerifyBatch(mT.Untyped(), sT.Untyped(), pT.Untyped(), rT.Untyped()); err != nil {
return false, nil
}
bytes, err := rT.ToSlice()
if err != nil {
return false, nil
}
for i, b := range bytes {
out[i] = b == 1
}
return true, nil
}
+78
View File
@@ -0,0 +1,78 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mlkem
// BatchThreshold is the minimum batch length at which BatchEncapsulate /
// BatchDecapsulate will try to dispatch through github.com/luxfi/accel.
var BatchThreshold = 64
// BatchEncapsulate runs Encapsulate for a slice of public keys, returning the
// resulting (ciphertext, sharedSecret) pairs. All keys must be the same mode.
//
// When the batch is large enough and a GPU backend is available the
// computation runs on the GPU; otherwise it falls back to per-key
// Encapsulate calls. The output is byte-identical for the deterministic
// path; the streaming path uses fresh randomness either way.
func BatchEncapsulate(pubs []*PublicKey) (cts [][]byte, sss [][]byte, err error) {
n := len(pubs)
if n == 0 {
return nil, nil, nil
}
mode := pubs[0].mode
for i := 1; i < n; i++ {
if pubs[i].mode != mode {
return nil, nil, ErrInvalidKeySize
}
}
cts = make([][]byte, n)
sss = make([][]byte, n)
if n >= BatchThreshold && mode == MLKEM768 {
if ok, gerr := batchEncapsulateGPU(pubs, cts, sss); ok && gerr == nil {
return cts, sss, nil
}
}
for i, pk := range pubs {
ct, ss, e := pk.Encapsulate()
if e != nil {
return nil, nil, e
}
cts[i] = ct
sss[i] = ss
}
return cts, sss, nil
}
// BatchDecapsulate runs Decapsulate over a slice of (sk, ct) pairs.
func BatchDecapsulate(sks []*PrivateKey, cts [][]byte) (sss [][]byte, err error) {
n := len(sks)
if n != len(cts) {
return nil, ErrInvalidKeySize
}
if n == 0 {
return nil, nil
}
mode := sks[0].mode
for i := 1; i < n; i++ {
if sks[i].mode != mode {
return nil, ErrInvalidKeySize
}
}
sss = make([][]byte, n)
if n >= BatchThreshold && mode == MLKEM768 {
if ok, gerr := batchDecapsulateGPU(sks, cts, sss); ok && gerr == nil {
return sss, nil
}
}
for i, sk := range sks {
ss, e := sk.Decapsulate(cts[i])
if e != nil {
return nil, e
}
sss[i] = ss
}
return sss, nil
}
+52
View File
@@ -0,0 +1,52 @@
package mlkem
import (
"bytes"
"testing"
)
func TestBatchEncapDecap(t *testing.T) {
for _, mode := range []Mode{MLKEM512, MLKEM768, MLKEM1024} {
t.Run(mode.String(), func(t *testing.T) {
n := BatchThreshold + 2
pubs := make([]*PublicKey, n)
privs := make([]*PrivateKey, n)
for i := 0; i < n; i++ {
pub, priv, err := GenerateKey(mode)
if err != nil {
t.Fatal(err)
}
pubs[i] = pub
privs[i] = priv
}
cts, sss, err := BatchEncapsulate(pubs)
if err != nil {
t.Fatal(err)
}
if len(cts) != n || len(sss) != n {
t.Fatalf("len(cts)=%d len(sss)=%d, want %d", len(cts), len(sss), n)
}
recovered, err := BatchDecapsulate(privs, cts)
if err != nil {
t.Fatal(err)
}
for i := 0; i < n; i++ {
if !bytes.Equal(recovered[i], sss[i]) {
t.Errorf("shared secret mismatch at %d", i)
}
}
})
}
}
func TestBatchEmpty(t *testing.T) {
cts, sss, err := BatchEncapsulate(nil)
if err != nil {
t.Fatal(err)
}
if cts != nil || sss != nil {
t.Fatal("expected nil/nil for empty input")
}
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mlkem
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
// accel publishes Kyber768 batch kernels — the closest match to ML-KEM-768.
// Other modes fall through.
func batchEncapsulateGPU(pubs []*PublicKey, cts [][]byte, sss [][]byte) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
n := len(pubs)
pkSize := MLKEM768PublicKeySize
ctSize := MLKEM768CiphertextSize
ssSize := MLKEM768SharedKeySize
pkFlat := make([]uint8, n*pkSize)
for i, p := range pubs {
b := p.Bytes()
if len(b) != pkSize {
return false, nil
}
copy(pkFlat[i*pkSize:(i+1)*pkSize], b)
}
pkT, err := accel.NewTensorWithData[uint8](sess, []int{n, pkSize}, pkFlat)
if err != nil {
return false, nil
}
defer pkT.Close()
ctT, err := accel.NewTensor[uint8](sess, []int{n, ctSize})
if err != nil {
return false, nil
}
defer ctT.Close()
ssT, err := accel.NewTensor[uint8](sess, []int{n, ssSize})
if err != nil {
return false, nil
}
defer ssT.Close()
if err := sess.Lattice().KyberEncapsBatch(pkT.Untyped(), ctT.Untyped(), ssT.Untyped()); err != nil {
return false, nil
}
ctBytes, err := ctT.ToSlice()
if err != nil {
return false, nil
}
ssBytes, err := ssT.ToSlice()
if err != nil {
return false, nil
}
for i := 0; i < n; i++ {
c := make([]byte, ctSize)
copy(c, ctBytes[i*ctSize:(i+1)*ctSize])
cts[i] = c
s := make([]byte, ssSize)
copy(s, ssBytes[i*ssSize:(i+1)*ssSize])
sss[i] = s
}
return true, nil
}
func batchDecapsulateGPU(sks []*PrivateKey, cts [][]byte, sss [][]byte) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
n := len(sks)
skSize := MLKEM768PrivateKeySize
ctSize := MLKEM768CiphertextSize
ssSize := MLKEM768SharedKeySize
skFlat := make([]uint8, n*skSize)
for i, s := range sks {
b := s.Bytes()
if len(b) != skSize {
return false, nil
}
copy(skFlat[i*skSize:(i+1)*skSize], b)
}
ctFlat := make([]uint8, n*ctSize)
for i, c := range cts {
if len(c) != ctSize {
return false, nil
}
copy(ctFlat[i*ctSize:(i+1)*ctSize], c)
}
skT, err := accel.NewTensorWithData[uint8](sess, []int{n, skSize}, skFlat)
if err != nil {
return false, nil
}
defer skT.Close()
ctT, err := accel.NewTensorWithData[uint8](sess, []int{n, ctSize}, ctFlat)
if err != nil {
return false, nil
}
defer ctT.Close()
ssT, err := accel.NewTensor[uint8](sess, []int{n, ssSize})
if err != nil {
return false, nil
}
defer ssT.Close()
if err := sess.Lattice().KyberDecapsBatch(ctT.Untyped(), skT.Untyped(), ssT.Untyped()); err != nil {
return false, nil
}
ssBytes, err := ssT.ToSlice()
if err != nil {
return false, nil
}
for i := 0; i < n; i++ {
s := make([]byte, ssSize)
copy(s, ssBytes[i*ssSize:(i+1)*ssSize])
sss[i] = s
}
return true, nil
}
+10
View File
@@ -0,0 +1,10 @@
// Package modexp is the canonical alias for github.com/luxfi/crypto/bigmodexp.
//
// modexp matches the EVM precompile name (EIP-198) and the luxcpp/crypto
// directory layout. The two import paths return identical types.
package modexp
import "github.com/luxfi/crypto/bigmodexp"
// Int is the patched big.Int with the fixed Exp implementation.
type Int = bigmodexp.Int
+13
View File
@@ -0,0 +1,13 @@
package modexp
import "testing"
func TestIntCompiles(t *testing.T) {
a := new(Int).SetInt64(2)
b := new(Int).SetInt64(10)
m := new(Int).SetInt64(1000)
r := new(Int).Exp(a, b, m)
if r.Cmp(new(Int).SetInt64(24)) != 0 {
t.Errorf("2^10 mod 1000 = %v; want 24", r)
}
}
+7
View File
@@ -0,0 +1,7 @@
// Package ntt is the canonical entry point for the Number-Theoretic Transform
// used by lattice-based cryptography (ML-KEM, ML-DSA, corona).
//
// The CPU implementation here uses gnark-crypto field arithmetic. The GPU
// path routes through github.com/luxfi/accel which exposes a polynomial
// NTT kernel for prime moduli (Kyber/Dilithium-style q = 3329 / 8380417).
package ntt
+111
View File
@@ -0,0 +1,111 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ntt
import (
"errors"
"math/bits"
)
var (
ErrNotPow2 = errors.New("ntt: input length must be a power of 2")
ErrModulusZero = errors.New("ntt: modulus must be non-zero")
)
// NTT computes a forward Number-Theoretic Transform of a in-place using the
// Cooley-Tukey radix-2 algorithm modulo q. omega must be a primitive
// (len(a))-th root of unity modulo q.
//
// Pure Go reference implementation. For production lattice-crypto callers
// should use the algorithm-specific kernels in mldsa/, mlkem/, or accel
// directly; this implementation is for vector kernels and tests.
func NTT(a []uint64, omega, q uint64) error {
n := len(a)
if n == 0 {
return nil
}
if n&(n-1) != 0 {
return ErrNotPow2
}
if q == 0 {
return ErrModulusZero
}
// Bit-reverse permutation.
logN := bits.TrailingZeros(uint(n))
for i := 0; i < n; i++ {
j := int(bits.Reverse(uint(i)) >> (bits.UintSize - logN))
if j > i {
a[i], a[j] = a[j], a[i]
}
}
// Iterative butterfly.
for size := 2; size <= n; size <<= 1 {
half := size >> 1
// w_size = omega^(n/size) mod q
w0 := powMod(omega, uint64(n/size), q)
for start := 0; start < n; start += size {
w := uint64(1)
for k := 0; k < half; k++ {
t := mulMod(w, a[start+k+half], q)
u := a[start+k]
a[start+k] = addMod(u, t, q)
a[start+k+half] = subMod(u, t, q)
w = mulMod(w, w0, q)
}
}
}
return nil
}
// INTT computes the inverse NTT in-place. omegaInv = omega^-1 mod q,
// nInv = n^-1 mod q.
func INTT(a []uint64, omegaInv, nInv, q uint64) error {
if err := NTT(a, omegaInv, q); err != nil {
return err
}
for i := range a {
a[i] = mulMod(a[i], nInv, q)
}
return nil
}
// addMod returns (a + b) mod q.
func addMod(a, b, q uint64) uint64 {
s := a + b
if s >= q {
s -= q
}
return s
}
// subMod returns (a - b) mod q.
func subMod(a, b, q uint64) uint64 {
if a >= b {
return a - b
}
return q - (b - a)
}
// mulMod returns (a * b) mod q using 128-bit intermediate.
func mulMod(a, b, q uint64) uint64 {
hi, lo := bits.Mul64(a, b)
_, rem := bits.Div64(hi, lo, q)
return rem
}
// powMod returns base^exp mod q using square-and-multiply.
func powMod(base, exp, q uint64) uint64 {
r := uint64(1)
b := base % q
for exp > 0 {
if exp&1 == 1 {
r = mulMod(r, b, q)
}
b = mulMod(b, b, q)
exp >>= 1
}
return r
}
+68
View File
@@ -0,0 +1,68 @@
package ntt
import "testing"
// Use a tiny prime (97) where 28 is a primitive 8th root of unity:
// 28^8 mod 97 = 1; for k = 1..7, 28^k mod 97 != 1.
const (
q = uint64(97)
omega = uint64(28)
)
func TestNTTRoundTrip(t *testing.T) {
want := []uint64{1, 2, 3, 4, 5, 6, 7, 8}
a := append([]uint64(nil), want...)
if err := NTT(a, omega, q); err != nil {
t.Fatal(err)
}
// Output is bit-reversed but transformed; we don't compare against
// hand-computed values here (that's brittle). Instead invert and check
// we get back the original.
omegaInv := powMod(omega, q-2, q) // Fermat
nInv := powMod(uint64(len(a)), q-2, q)
if err := INTT(a, omegaInv, nInv, q); err != nil {
t.Fatal(err)
}
for i, v := range a {
if v != want[i] {
t.Errorf("round trip[%d] = %d; want %d", i, v, want[i])
}
}
}
func TestNTTRejectsBadInputs(t *testing.T) {
if err := NTT([]uint64{1, 2, 3}, omega, q); err != ErrNotPow2 {
t.Errorf("non-pow2: got %v want %v", err, ErrNotPow2)
}
if err := NTT([]uint64{1, 2}, omega, 0); err != ErrModulusZero {
t.Errorf("modulus 0: got %v want %v", err, ErrModulusZero)
}
}
func TestNTTLinearity(t *testing.T) {
// NTT(a + b) should equal NTT(a) + NTT(b) componentwise mod q.
a := []uint64{1, 0, 0, 0, 0, 0, 0, 0}
b := []uint64{0, 1, 0, 0, 0, 0, 0, 0}
sum := make([]uint64, 8)
for i := range sum {
sum[i] = (a[i] + b[i]) % q
}
A := append([]uint64(nil), a...)
B := append([]uint64(nil), b...)
S := append([]uint64(nil), sum...)
if err := NTT(A, omega, q); err != nil {
t.Fatal(err)
}
if err := NTT(B, omega, q); err != nil {
t.Fatal(err)
}
if err := NTT(S, omega, q); err != nil {
t.Fatal(err)
}
for i := range S {
if want := (A[i] + B[i]) % q; want != S[i] {
t.Errorf("linearity fail at %d: got %d want %d", i, S[i], want)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// Package pedersen is the canonical entry point for Pedersen commitments
// over the BN254 scalar field.
//
// A Pedersen commitment Commit(m, r) = m*G + r*H where G and H are
// independently sampled, prover-public generators of an elliptic curve
// group. The commitment is hiding (under the discrete-log assumption for
// H) and binding.
//
// This package wraps gnark-crypto BN254 G1 arithmetic. For Verkle-tree
// commitments use luxfi/crypto/ipa instead.
package pedersen
+81
View File
@@ -0,0 +1,81 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pedersen
import (
"crypto/rand"
"errors"
"io"
"math/big"
bn254 "github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
)
// Commitment is a single point on BN254 G1.
type Commitment = bn254.G1Affine
// Generators holds the two independent generators G and H used for a single
// Pedersen commitment scheme instance. Reusing the same Generators across
// commitments is required for them to add and compare meaningfully.
type Generators struct {
G bn254.G1Jac
H bn254.G1Jac
}
var ErrIdenticalGenerators = errors.New("pedersen: G and H must be independent generators")
// NewGenerators samples two independent G1 generators. rng may be nil to use
// crypto/rand. The two generators are derived from independent hashes-to-curve
// of fresh randomness, ensuring no known relation between them.
func NewGenerators(rng io.Reader) (*Generators, error) {
if rng == nil {
rng = rand.Reader
}
gBytes := make([]byte, 64)
if _, err := rng.Read(gBytes); err != nil {
return nil, err
}
hBytes := make([]byte, 64)
if _, err := rng.Read(hBytes); err != nil {
return nil, err
}
g, err := bn254.HashToG1(gBytes, []byte("LUX_PEDERSEN_G"))
if err != nil {
return nil, err
}
h, err := bn254.HashToG1(hBytes, []byte("LUX_PEDERSEN_H"))
if err != nil {
return nil, err
}
if g.Equal(&h) {
return nil, ErrIdenticalGenerators
}
gen := &Generators{}
gen.G.FromAffine(&g)
gen.H.FromAffine(&h)
return gen, nil
}
// Commit returns Commit(m, r) = m*G + r*H.
func (gens *Generators) Commit(m, r *fr.Element) Commitment {
var mScalar, rScalar fr.Element
mScalar.Set(m)
rScalar.Set(r)
mBig := new(big.Int)
rBig := new(big.Int)
mScalar.BigInt(mBig)
rScalar.BigInt(rBig)
var mG, rH bn254.G1Jac
mG.ScalarMultiplication(&gens.G, mBig)
rH.ScalarMultiplication(&gens.H, rBig)
var c bn254.G1Jac
c.Set(&mG).AddAssign(&rH)
var out Commitment
out.FromJacobian(&c)
return out
}
+53
View File
@@ -0,0 +1,53 @@
package pedersen
import (
"testing"
bn254 "github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
)
func TestCommitmentHomomorphism(t *testing.T) {
gens, err := NewGenerators(nil)
if err != nil {
t.Fatal(err)
}
var m1, r1, m2, r2 fr.Element
m1.SetUint64(7)
r1.SetUint64(13)
m2.SetUint64(5)
r2.SetUint64(11)
c1 := gens.Commit(&m1, &r1)
c2 := gens.Commit(&m2, &r2)
// Commit(m1+m2, r1+r2) should equal c1 + c2.
var mSum, rSum fr.Element
mSum.Add(&m1, &m2)
rSum.Add(&r1, &r2)
cSum := gens.Commit(&mSum, &rSum)
var aJ, bJ bn254.G1Jac
aJ.FromAffine(&c1)
bJ.FromAffine(&c2)
aJ.AddAssign(&bJ)
var got bn254.G1Affine
got.FromJacobian(&aJ)
if !got.Equal(&cSum) {
t.Errorf("Pedersen homomorphism violated:\ngot=%v\nwant=%v", got, cSum)
}
}
func TestNewGeneratorsIndependent(t *testing.T) {
gens, err := NewGenerators(nil)
if err != nil {
t.Fatal(err)
}
var gAff, hAff bn254.G1Affine
gAff.FromJacobian(&gens.G)
hAff.FromJacobian(&gens.H)
if gAff.Equal(&hAff) {
t.Error("G and H must be independent")
}
}
+12
View File
@@ -0,0 +1,12 @@
// Package polymul is the canonical entry point for polynomial multiplication
// in the lattice-cryptography ring Z_q[X] / (X^N + 1).
//
// The CPU implementation is a schoolbook O(N^2) multiplication that handles
// arbitrary moduli. For production lattice crypto callers should use the
// algorithm-specific NTT-based multiplication in mldsa/ or mlkem/, or
// route through the GPU via github.com/luxfi/accel which has fused
// NTT+pointwise+INTT kernels.
//
// Naming follows the luxcpp/crypto/poly_mul/ directory; both polymul (Go
// idiomatic) and the underscore form are accepted as identifiers.
package polymul
+63
View File
@@ -0,0 +1,63 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package polymul
import (
"errors"
"math/bits"
)
var (
ErrLengthMismatch = errors.New("polymul: a and b must have equal length")
ErrModulusZero = errors.New("polymul: modulus must be non-zero")
)
// MulNegacyclic multiplies two polynomials a and b in Z_q[X] / (X^N + 1).
// a, b, and the returned polynomial all have length N. q must be non-zero.
//
// Schoolbook O(N^2). For N up to 256 this is the simplest correct path.
// For production lattice work use NTT-based multiplication.
func MulNegacyclic(a, b []uint64, q uint64) ([]uint64, error) {
n := len(a)
if n != len(b) {
return nil, ErrLengthMismatch
}
if q == 0 {
return nil, ErrModulusZero
}
c := make([]uint64, n)
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
t := mulMod(a[i], b[j], q)
k := i + j
if k < n {
c[k] = addMod(c[k], t, q)
} else {
c[k-n] = subMod(c[k-n], t, q)
}
}
}
return c, nil
}
func addMod(a, b, q uint64) uint64 {
s := a + b
if s >= q {
s -= q
}
return s
}
func subMod(a, b, q uint64) uint64 {
if a >= b {
return a - b
}
return q - (b - a)
}
func mulMod(a, b, q uint64) uint64 {
hi, lo := bits.Mul64(a, b)
_, rem := bits.Div64(hi, lo, q)
return rem
}
+42
View File
@@ -0,0 +1,42 @@
package polymul
import "testing"
func TestMulNegacyclicBasic(t *testing.T) {
// (1 + X) * (1 + X) mod (X^4 + 1) = 1 + 2X + X^2.
a := []uint64{1, 1, 0, 0}
b := []uint64{1, 1, 0, 0}
c, err := MulNegacyclic(a, b, 17)
if err != nil {
t.Fatal(err)
}
want := []uint64{1, 2, 1, 0}
for i, v := range want {
if c[i] != v {
t.Errorf("c[%d] = %d; want %d", i, c[i], v)
}
}
}
func TestMulNegacyclicWrap(t *testing.T) {
// X^3 * X mod (X^4 + 1) = -1 = q-1.
a := []uint64{0, 0, 0, 1}
b := []uint64{0, 1, 0, 0}
c, err := MulNegacyclic(a, b, 17)
if err != nil {
t.Fatal(err)
}
want := []uint64{16, 0, 0, 0}
for i, v := range want {
if c[i] != v {
t.Errorf("c[%d] = %d; want %d", i, c[i], v)
}
}
}
func TestMulNegacyclicLengthMismatch(t *testing.T) {
_, err := MulNegacyclic([]uint64{1, 2}, []uint64{1, 2, 3}, 17)
if err != ErrLengthMismatch {
t.Errorf("got %v want %v", err, ErrLengthMismatch)
}
}
+16
View File
@@ -0,0 +1,16 @@
// Package poseidon is the canonical entry point for Poseidon-flavoured
// hash functions in luxfi/crypto.
//
// Two variants are exposed:
//
// - Sum: classical Poseidon (Grassi-Khovratovich-Lüftenegger-Rechberger-
// Roy-Schofnegger 2021) used by zkSNARK toolchains.
// - Sum2: Poseidon2 (Grassi-Khovratovich-Roy 2023) — preferred for new
// code as it offers a 2-3x speedup on Cortex/x86 with the same
// security level.
//
// The classical Poseidon entry uses the gnark-crypto BN254 implementation
// for compatibility with the Verkle and Plonky2 ecosystems. The Poseidon2
// entry routes through luxfi/crypto/hash/poseidon2 which has GPU
// acceleration on platforms that ship the luxcpp Metal kernel.
package poseidon
+35
View File
@@ -0,0 +1,35 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package poseidon
import (
"errors"
"github.com/consensys/gnark-crypto/ecc/bn254/fr/poseidon2"
)
// FieldSize is the size of a BN254 scalar field element in bytes.
const FieldSize = 32
var ErrInvalidFieldSize = errors.New("poseidon: input must be a multiple of 32 bytes")
// Sum2 computes the Poseidon2 hash of `inputs` interpreted as a sequence of
// 32-byte BN254 scalar field elements. It returns a single 32-byte digest.
//
// inputs must have length divisible by 32. Empty input is permitted and
// returns the Poseidon2 hash of the empty preimage (well-defined per the
// gnark-crypto spec).
func Sum2(inputs []byte) ([FieldSize]byte, error) {
var out [FieldSize]byte
if len(inputs)%FieldSize != 0 {
return out, ErrInvalidFieldSize
}
h := poseidon2.NewMerkleDamgardHasher()
if _, err := h.Write(inputs); err != nil {
return out, err
}
digest := h.Sum(nil)
copy(out[:], digest)
return out, nil
}
+39
View File
@@ -0,0 +1,39 @@
package poseidon
import "testing"
func TestSum2Deterministic(t *testing.T) {
in := make([]byte, 64)
for i := range in {
in[i] = byte(i)
}
a, err := Sum2(in)
if err != nil {
t.Fatal(err)
}
b, err := Sum2(in)
if err != nil {
t.Fatal(err)
}
if a != b {
t.Errorf("Sum2 not deterministic: %x vs %x", a, b)
}
}
func TestSum2RejectsNon32(t *testing.T) {
_, err := Sum2([]byte{1, 2, 3})
if err != ErrInvalidFieldSize {
t.Errorf("expected ErrInvalidFieldSize; got %v", err)
}
}
func TestSum2DistinctInputs(t *testing.T) {
in1 := make([]byte, 32)
in2 := make([]byte, 32)
in2[0] = 1
a, _ := Sum2(in1)
b, _ := Sum2(in2)
if a == b {
t.Errorf("distinct inputs collide: %x", a)
}
}
+5
View File
@@ -0,0 +1,5 @@
// Package ripemd160 is the canonical entry point for RIPEMD-160 in luxfi/crypto.
//
// It wraps golang.org/x/crypto/ripemd160. RIPEMD-160 is used by Bitcoin and
// Lux to derive addresses; it is not recommended for new general-purpose use.
package ripemd160
+25
View File
@@ -0,0 +1,25 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ripemd160
import (
"hash"
xripemd "golang.org/x/crypto/ripemd160" //nolint:gosec
)
// Size is the output size of RIPEMD-160 in bytes.
const Size = xripemd.Size
// Sum160 computes the RIPEMD-160 digest of in.
func Sum160(in []byte) [Size]byte {
h := xripemd.New() //nolint:gosec
h.Write(in)
var out [Size]byte
h.Sum(out[:0])
return out
}
// New returns a new hash.Hash computing RIPEMD-160.
func New() hash.Hash { return xripemd.New() } //nolint:gosec
+34
View File
@@ -0,0 +1,34 @@
package ripemd160
import (
"encoding/hex"
"testing"
)
func TestSum160Vectors(t *testing.T) {
cases := []struct {
in, want string
}{
{"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"},
{"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"},
{"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"},
}
for _, c := range cases {
got := Sum160([]byte(c.in))
want, _ := hex.DecodeString(c.want)
if string(got[:]) != string(want) {
t.Errorf("Sum160(%q) = %x; want %s", c.in, got, c.want)
}
}
}
func TestNewIncremental(t *testing.T) {
h := New()
h.Write([]byte("a"))
h.Write([]byte("bc"))
got := h.Sum(nil)
want, _ := hex.DecodeString("8eb208f7e05d987a9b044a8e98c6b087f15a0bfc")
if string(got) != string(want) {
t.Errorf("incremental got %x want %x", got, want)
}
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package secp256k1
// BatchThreshold is the minimum batch length at which BatchVerifySignature
// will try to dispatch through github.com/luxfi/accel.
var BatchThreshold = 64
// BatchVerifySignature verifies a slice of (pubkey, msgHash, sig) triples.
// pubkeys may be either 33-byte compressed or 65-byte uncompressed; all
// elements of pubkeys must use the same encoding. msgHashes must be 32
// bytes each; signatures must be 64 bytes ([R || S]).
//
// Returns one boolean per input. The result is byte-identical to repeated
// calls to VerifySignature.
func BatchVerifySignature(pubkeys [][]byte, msgHashes [][]byte, sigs [][]byte) []bool {
n := len(pubkeys)
if n != len(msgHashes) || n != len(sigs) {
panic("secp256k1.BatchVerifySignature: length mismatch")
}
out := make([]bool, n)
if n == 0 {
return out
}
if n >= BatchThreshold {
if ok, err := batchVerifyGPU(pubkeys, msgHashes, sigs, out); ok && err == nil {
return out
}
}
for i := range pubkeys {
out[i] = VerifySignature(pubkeys[i], msgHashes[i], sigs[i])
}
return out
}
+65
View File
@@ -0,0 +1,65 @@
package secp256k1
import (
"crypto/rand"
"testing"
)
func TestBatchVerifyMatchesScalar(t *testing.T) {
n := BatchThreshold + 4
pubs := make([][]byte, n)
msgs := make([][]byte, n)
sigs := make([][]byte, n)
for i := 0; i < n; i++ {
seckey := make([]byte, 32)
for {
if _, err := rand.Read(seckey); err != nil {
t.Fatal(err)
}
// Reject zero key (decred rejects).
any := false
for _, b := range seckey {
if b != 0 {
any = true
break
}
}
if any {
break
}
}
// Derive pubkey via Sign roundtrip on a known msg.
msg := make([]byte, 32)
if _, err := rand.Read(msg); err != nil {
t.Fatal(err)
}
sig, err := Sign(msg, seckey)
if err != nil {
t.Fatalf("Sign: %v", err)
}
pub, err := RecoverPubkey(msg, sig)
if err != nil {
t.Fatalf("RecoverPubkey: %v", err)
}
// Drop recovery byte for VerifySignature.
sig64 := sig[:64]
pubs[i] = pub
msgs[i] = msg
sigs[i] = sig64
}
got := BatchVerifySignature(pubs, msgs, sigs)
for i := range got {
if !got[i] {
t.Errorf("batch[%d] reported invalid", i)
}
if VerifySignature(pubs[i], msgs[i], sigs[i]) != got[i] {
t.Errorf("batch/scalar disagree at %d", i)
}
}
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package secp256k1
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
func batchVerifyGPU(pubkeys, msgHashes, sigs [][]byte, out []bool) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
n := len(pubkeys)
if n == 0 {
return false, nil
}
pkSize := len(pubkeys[0])
if pkSize != 33 && pkSize != 65 {
return false, nil
}
for _, p := range pubkeys {
if len(p) != pkSize {
return false, nil
}
}
mFlat := make([]uint8, n*32)
for i, m := range msgHashes {
if len(m) != 32 {
return false, nil
}
copy(mFlat[i*32:(i+1)*32], m)
}
sFlat := make([]uint8, n*64)
for i, s := range sigs {
if len(s) != 64 {
return false, nil
}
copy(sFlat[i*64:(i+1)*64], s)
}
pFlat := make([]uint8, n*pkSize)
for i, p := range pubkeys {
copy(pFlat[i*pkSize:(i+1)*pkSize], p)
}
mT, err := accel.NewTensorWithData[uint8](sess, []int{n, 32}, mFlat)
if err != nil {
return false, nil
}
defer mT.Close()
sT, err := accel.NewTensorWithData[uint8](sess, []int{n, 64}, sFlat)
if err != nil {
return false, nil
}
defer sT.Close()
pT, err := accel.NewTensorWithData[uint8](sess, []int{n, pkSize}, pFlat)
if err != nil {
return false, nil
}
defer pT.Close()
rT, err := accel.NewTensor[uint8](sess, []int{n})
if err != nil {
return false, nil
}
defer rT.Close()
if err := sess.Crypto().ECDSAVerifyBatch(mT.Untyped(), sT.Untyped(), pT.Untyped(), rT.Untyped()); err != nil {
return false, nil
}
bytes, err := rT.ToSlice()
if err != nil {
return false, nil
}
for i, b := range bytes {
out[i] = b == 1
}
return true, nil
}
+5
View File
@@ -0,0 +1,5 @@
// Package sha256 is the canonical entry point for SHA-256 in luxfi/crypto.
//
// It is a thin wrapper over crypto/sha256 in the standard library, with an
// optional batch entry that can dispatch to GPU through github.com/luxfi/accel.
package sha256
+62
View File
@@ -0,0 +1,62 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sha256
import (
"github.com/luxfi/accel"
"github.com/luxfi/crypto/backend"
"github.com/luxfi/crypto/internal/gpuhost"
)
func batchGPU(inputs [][]byte, out [][Size]byte) (bool, error) {
if backend.Resolve(gpuhost.Available(), false) != backend.GPU {
return false, nil
}
sess := gpuhost.Session()
if sess == nil {
return false, nil
}
width := 0
for _, in := range inputs {
if len(in) > width {
width = len(in)
}
}
if width == 0 {
empty := Sum256(nil)
for i := range out {
out[i] = empty
}
return true, nil
}
flat := make([]uint8, len(inputs)*width)
for i, in := range inputs {
copy(flat[i*width:(i+1)*width], in)
}
inT, err := accel.NewTensorWithData[uint8](sess, []int{len(inputs), width}, flat)
if err != nil {
return false, nil
}
defer inT.Close()
outT, err := accel.NewTensor[uint8](sess, []int{len(inputs), Size})
if err != nil {
return false, nil
}
defer outT.Close()
if err := sess.Crypto().SHA256(inT.Untyped(), outT.Untyped()); err != nil {
return false, nil
}
bytes, err := outT.ToSlice()
if err != nil {
return false, nil
}
for i := range out {
copy(out[i][:], bytes[i*Size:(i+1)*Size])
}
return true, nil
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sha256
import (
stdsha256 "crypto/sha256"
"hash"
)
// Size is the output size of SHA-256 in bytes.
const Size = stdsha256.Size
// BatchThreshold is the minimum batch length at which Sum256Batch will try to
// route through GPU.
var BatchThreshold = 256
// Sum256 returns the SHA-256 hash of in.
func Sum256(in []byte) [Size]byte { return stdsha256.Sum256(in) }
// New returns a hash.Hash computing SHA-256.
func New() hash.Hash { return stdsha256.New() }
// Sum256Batch computes SHA-256 for a batch of inputs.
func Sum256Batch(inputs [][]byte) [][Size]byte {
out := make([][Size]byte, len(inputs))
if len(inputs) >= BatchThreshold {
if ok, err := batchGPU(inputs, out); ok && err == nil {
return out
}
}
for i, in := range inputs {
out[i] = stdsha256.Sum256(in)
}
return out
}
+37
View File
@@ -0,0 +1,37 @@
package sha256
import (
stdsha256 "crypto/sha256"
"encoding/hex"
"testing"
)
func TestSum256(t *testing.T) {
cases := []struct {
in, want string
}{
{"", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{"abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
}
for _, c := range cases {
got := Sum256([]byte(c.in))
want, _ := hex.DecodeString(c.want)
if string(got[:]) != string(want) {
t.Errorf("Sum256(%q) = %x; want %s", c.in, got, c.want)
}
}
}
func TestSum256BatchMatchesScalar(t *testing.T) {
inputs := make([][]byte, BatchThreshold+4)
for i := range inputs {
inputs[i] = []byte("v" + string(rune('a'+i%26)))
}
got := Sum256Batch(inputs)
for i, in := range inputs {
want := stdsha256.Sum256(in)
if got[i] != want {
t.Errorf("batch[%d] mismatch", i)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
// Package sha3 is the canonical entry point for SHA3 / SHAKE / cSHAKE
// in luxfi/crypto.
//
// SHA3 is the FIPS-202 standard. The Ethereum-flavored "Keccak-256" lives in
// the keccak/ package since it predates the FIPS standard and uses a
// different padding rule.
package sha3
+48
View File
@@ -0,0 +1,48 @@
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sha3
import (
"hash"
"golang.org/x/crypto/sha3"
)
// Size constants for fixed-output SHA3 variants.
const (
Size224 = 28
Size256 = 32
Size384 = 48
Size512 = 64
)
// Sum224 returns the SHA3-224 hash of in.
func Sum224(in []byte) [Size224]byte { return sha3.Sum224(in) }
// Sum256 returns the SHA3-256 hash of in.
func Sum256(in []byte) [Size256]byte { return sha3.Sum256(in) }
// Sum384 returns the SHA3-384 hash of in.
func Sum384(in []byte) [Size384]byte { return sha3.Sum384(in) }
// Sum512 returns the SHA3-512 hash of in.
func Sum512(in []byte) [Size512]byte { return sha3.Sum512(in) }
// New224 returns a new hash.Hash computing SHA3-224.
func New224() hash.Hash { return sha3.New224() }
// New256 returns a new hash.Hash computing SHA3-256.
func New256() hash.Hash { return sha3.New256() }
// New384 returns a new hash.Hash computing SHA3-384.
func New384() hash.Hash { return sha3.New384() }
// New512 returns a new hash.Hash computing SHA3-512.
func New512() hash.Hash { return sha3.New512() }
// ShakeSum128 produces an outputLen-byte SHAKE128 digest of data.
func ShakeSum128(out, data []byte) { sha3.ShakeSum128(out, data) }
// ShakeSum256 produces an outputLen-byte SHAKE256 digest of data.
func ShakeSum256(out, data []byte) { sha3.ShakeSum256(out, data) }
+38
View File
@@ -0,0 +1,38 @@
package sha3
import (
"encoding/hex"
"testing"
)
func TestSum256Vectors(t *testing.T) {
cases := []struct {
in, want string
}{
{"", "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"},
{"abc", "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"},
}
for _, c := range cases {
got := Sum256([]byte(c.in))
want, _ := hex.DecodeString(c.want)
if string(got[:]) != string(want) {
t.Errorf("Sum256(%q) = %x; want %s", c.in, got, c.want)
}
}
}
func TestSum512Vectors(t *testing.T) {
got := Sum512([]byte("abc"))
want, _ := hex.DecodeString("b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0")
if string(got[:]) != string(want) {
t.Errorf("Sum512(abc) = %x; want %x", got, want)
}
}
func TestShake128(t *testing.T) {
out := make([]byte, 32)
ShakeSum128(out, []byte("abc"))
if hex.EncodeToString(out) == "" {
t.Fatal("ShakeSum128 produced empty output")
}
}