mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
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:
@@ -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.
|
||||||
@@ -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(¤t, 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(¤t))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefault overrides the active backend.
|
||||||
|
//
|
||||||
|
// Use the empty string or "auto" via Parse to revert to Auto behavior.
|
||||||
|
func SetDefault(b Backend) {
|
||||||
|
atomic.StoreUint32(¤t, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ require (
|
|||||||
github.com/google/gofuzz v1.2.0
|
github.com/google/gofuzz v1.2.0
|
||||||
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7
|
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7
|
||||||
github.com/leanovate/gopter v0.2.11
|
github.com/leanovate/gopter v0.2.11
|
||||||
|
github.com/luxfi/accel v1.0.7
|
||||||
github.com/luxfi/age v1.4.0
|
github.com/luxfi/age v1.4.0
|
||||||
github.com/luxfi/cache v1.1.0
|
github.com/luxfi/cache v1.1.0
|
||||||
github.com/luxfi/ids v1.2.9
|
github.com/luxfi/ids v1.2.9
|
||||||
|
|||||||
@@ -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/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 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
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/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.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
|||||||
+31
-55
@@ -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.
|
// See the file LICENSE for licensing terms.
|
||||||
|
|
||||||
// Package gpu provides GPU-accelerated cryptographic operations.
|
// Package gpu exposes the runtime status of the GPU acceleration layer used
|
||||||
// When GPU hardware is not available, all operations return errors.
|
// 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
|
package gpu
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"github.com/luxfi/accel"
|
||||||
// HashType represents the type of hash function to use.
|
"github.com/luxfi/crypto/internal/gpuhost"
|
||||||
type HashType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
HashTypeSHA3_256 HashType = iota
|
|
||||||
HashTypeSHA256
|
|
||||||
HashTypeBlake3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GPUAvailable returns whether GPU acceleration is available.
|
// Available returns true when an accel session was successfully created
|
||||||
func GPUAvailable() bool {
|
// and at least one backend is reporting available devices.
|
||||||
return false
|
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.
|
// Devices returns the list of devices visible to the accel layer.
|
||||||
func GetBackend() string {
|
func Devices() []accel.DeviceInfo {
|
||||||
return "none"
|
if !Available() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return accel.Devices()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SHA3_256 returns zeroed output when GPU is not available.
|
// Version returns the underlying accel library version string.
|
||||||
// Callers must check GPUAvailable() before relying on this output.
|
func Version() string { return accel.GetVersion() }
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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) }
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user