Closes Red CRITICAL-2 (red-c-new-1). The luxcpp/accel layer's M-1
tri-state propagation (5a02e55b) was effectively defeated by Go
consumers that swallowed every error into a silent CPU fallback. The
consensus-split-prevention claim was hollow: a malformed input the
GPU rejected with INVALID_ARGUMENT would silently CPU-pass via
cloudflare/circl, which accepts arbitrary-length messages per FIPS
204. Two nodes running the same input could produce different
verdicts depending on whether they ran GPU or CPU → consensus split.
Fix: ErrInvalidArgument from accel propagates as a hard error;
ErrNotSupported, ErrOutOfMemory, ErrKernelFailed, ErrNoBackends
remain silent-fallback (they're recoverable: GPU path unavailable,
not contract violation). Three call sites updated:
* crypto/mldsa/gpu.go::batchVerifyGPU — returns (false, err) with
ErrInvalidArgument so batch.go::BatchVerify panics (no error
return, same shape as existing length-mismatch + mixed-mode
panics in the same function).
* crypto/mldsa/gpu.go::batchSignGPU — same shape; batch.go::
BatchSign already returns error so it surfaces directly.
* crypto/pq/mldsa/gpu/gpu_cgo.go::{signBatch,verifyBatch} — same
classifier; signBatch returns the error to BatchSign caller,
verifyBatch returns (nil, err) to BatchVerify caller.
Skip-list in batchVerifyGPU: when ErrInvalidArgument bubbles from
MLDSAVerifyBatch and the mode is ML-DSA-65, do NOT fall back to the
legacy DilithiumVerifyBatch path — that path runs the SAME C ABI
under a different dispatch slot and would reject again. Only mask
the legacy retry when the FIRST error was something other than
ErrInvalidArgument (NotSupported is the canonical case: a substrate
that doesn't publish the new MLDSAVerifyBatch slot but does publish
the legacy Dilithium3 kernel).
New test: pq/mldsa/gpu/invalid_argument_propagation_test.go locks
the policy in as a unit test. It verifies:
1. accel.ErrInvalidArgument exists.
2. errors.Is(...) detects it through fmt.Errorf %w wraps.
3. Other accel sentinels do not alias it.
4. The classifier ("hard" vs "recover") matches the dispatch code
for nil, ErrInvalidArgument (raw + wrapped), ErrNotSupported,
ErrOutOfMemory, ErrKernelFailed, ErrNoBackends, and an unrelated
error.
This is the propagation policy test the prompt asked for. A
real-input fault-injection test would need a > 2 GB synthetic
message (Go can't construct one in CI memory budgets) or a session
mock harness (overengineering for a single check); the policy test
locks the error-routing logic at the exact `errors.Is` boundary
where the contract lives.
luxgo crypto module version stays at current; no breaking-API change.
Adds Provenance() on slhdsa/mldsa packages so reviewers can ask the
running binary "which dispatch tier is live right now?" instead of
trusting a static claim. Honest by construction: a release build
without the SLH-DSA / ML-DSA backend plugin reports TierAccelCPUFallback
or TierGoroutineParallelCPU, never TierGPUSubstrate.
The strong-symbol observation is recorded by batchVerifyGPU /
batchSignGPU after a successful C ABI dispatch; subsequent
GetProvenance() calls report TierGPUSubstrate. The cache only goes
0 -> 1 so transient tensor allocation failures cannot regress
provenance to "no plugin".
mldsa/batch.go: 3-tier dispatch (GPU -> goroutine-parallel CPU ->
serial CPU), mirroring slhdsa. concurrentBatchThreshold=8 tuned for
the FIPS 204 verify cost.
ML-DSA-44 and ML-DSA-87 batch tests pin KAT replay + CPU/GPU
equivalence + tamper rejection across the dispatch tiers. Previously
only ML-DSA-65 had this coverage.
Removed dead pq/slhdsa/gpu/gpu.go (replaced by the live dispatcher
in slhdsa/gpu.go).
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.