Chasing these one at a time was whack-a-mole — every repair surfaced the next
moved tag (age, pq, threshold, zap, consensus, constants, sdk, keys…). This is
the systematic pass instead: every distinct luxfi/hanzoai module@version in the
workspace (692 of them) was resolved against proxy.golang.org + a direct fetch,
and any go.sum line disagreeing with what is actually served was corrected.
Not an attack, and checked rather than assumed: for each case the proxy and a
direct fetch AGREE with each other and differ from the recorded hash. Two
independent transports agreeing means nothing is rewriting bytes in flight —
the tags moved at source. sum.golang.org still holds the original, but
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently instead of
failing at publish time.
Only the recorded hash changes. No selected version moves, and `go mod tidy`
was deliberately NOT run, so nothing is upgraded as a side effect.
Verified: `go list -m all` resolves with no checksum error.
The durable fix is upstream: treat a published version as immutable. Cut
x.y.z+1 instead of moving a tag — with GOSUMDB off, no consumer can detect it.
Several luxfi versions carry TWO different contents across this workspace,
because published tags were moved instead of a new patch being cut:
age@v1.5.0, pq@v1.0.3, threshold@v1.9.4, zap@v0.6.0, zap@v0.8.1.
Adjudicated before editing, since "checksum mismatch / SECURITY ERROR" is also
what a real supply-chain attack looks like. It is not one here: sum.golang.org
holds the OLD hash while proxy.golang.org and a direct fetch BOTH serve the same
NEW bytes. Two independent transports agreeing means nothing is rewriting
content in flight — the tag moved at source. The sumdb entry is a fossil:
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently.
The old bytes are served by nothing now, so a stale pin can never build.
Corrected to the only content that exists, in BOTH line forms (h1: and
/go.mod h1:) — Go reports these one at a time, so a partial fix just relocates
the error.
Deliberately no `go mod tidy`: this changes no selected version, only the
recorded hash of versions already chosen.
Verified: `go list -m all` resolves with no checksum error.
The durable fix is upstream: never move a published tag, cut x.y.z+1 instead.
crypto/aggregated ThresholdSignature.Verify ignored its message argument and
returned true for any non-empty []byte — a method named Verify that verifies
nothing. Zero importers workspace-wide, so it was never reachable; removing it
before it ever could be. Real threshold verification lives in luxfi/threshold.
Verified: CGO_ENABLED=0 go build ./... clean, no remaining references.
- delete crypto/aggregated: ThresholdSignature.Verify ignored the message and
returned true for any non-empty []byte. Zero importers workspace-wide, so it
was never reachable — a loaded gun, not a live wound. Real threshold
verification lives in github.com/luxfi/threshold.
- untrack 5 committed Go test binaries (24.2MB) and add *.test to .gitignore
so they stop regenerating into the index.
Verified: CGO_ENABLED=0 go build ./... clean; no remaining references.
The canonical ML-DSA package (mldsa/) is the one way; mldsa/.old was
unreferenced legacy code. Build stays green (go build ./mldsa/ ok).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Anchors the attestation package's API + trust model + vendor
integrations + production status. Adjacent to LP-5300 / LP-5301
PoT/PoI receipts insofar as PoT is a form of computation
attestation.
Pure docs. No behavior change.
CGGMP21 threshold ECDSA is Lux's canonical native MPC library
(production bridge default per luxfi/bridge#405). Package had no
README — adds Overview + Public API + Paillier note + security
model + operational constraints (non-custodial, no cosigner path
by default).
Pure docs. No behavior change.
The poi package (Freivalds-over-F_p verification) is hard-consumed
by luxfi/precompile/aivmbridge/computeproof.go but has no README.
Adds Overview + API + wire codec spec + soundness bound + worked
example so a new contributor can audit a fraud proof without
reading the engine.
Pure docs. No behavior change.
The native BLS Scheme impl (threshold/bls/) moved to
luxfi/threshold/scheme/bls so all threshold-signature implementations
live in one module. crypto/threshold now holds only the shared contract
(Scheme/Signer/Aggregator/Verifier/registry/session/errors/adapter) that
crypto/signer + hsm depend on — the one low-level seam that must stay
here to avoid a crypto<->threshold module cycle.
signer_test repoints its BLS registration to the new path. No
production crypto code changes.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Replaces the stub trusted-dealer keygen with a real dealerless DKG:
each party samples its own degree-(t-1) polynomial + Feldman
commitments; party j's share = Σ_i f_i(j+1); group key = Σ_i C_{i,0}.
No party ever forms the group secret — it exists only as the sum of
independent contributions. Every cross-party VSS share is
Feldman-verified (A·f(j) == Σ jᵏ·C_k); a bad share fails the ceremony
identifying the emitter.
Tested (CGO, BLS12-381 via circl): 3-of-5 shares sign, aggregate via
Lagrange, and verify under the emergent group key; two disjoint signer
subsets both verify (proving one shared polynomial); bad params
rejected.
NOTE: per the threshold-consolidation directive this scheme's canonical
home is moving to luxfi/threshold, and dealerless keygen is being
routed through the purpose-built luxfi/dkg engine — see the
threshold-architecture consolidation plan. This lands the tested
no-dealer property now; the engine swap follows.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Corona is Module-LWE (Ringtail/Raccoon line, ePrint 2024/1113), the
same family as Pulsar (ML-DSA) and ML-KEM. The stack comment already
labels Pulsar (MLWE); Corona was the lone RLWE mislabel. Comment only.
Real parsers for the three production TEE quote formats, decoding the documented binary layouts and
verifying the full vendor trust chain (no longer the canonical-model stand-in):
- SGX (DCAP v3): header+report body offsets (MRENCLAVE@112, report_data@368); PCK chain→pinned Intel
root, PCK signs the QE report, the QE report binds the attestation key (sha256(attestPub‖authData)),
the attestation key signs the quote — full DCAP flow.
- TDX (DCAP v4): version 4 / tee_type 0x81, MRTD@184, report_data@568; same sigData chain.
- SEV-SNP: 1184-byte report, measurement@0x90, report_data@0x50, P-384 VCEK signature@0x2A0 (LE
scalars), VCEK→pinned AMD root.
All verify the operator-key binding in report_data. 9/9 incl. tampered-measurement (sig fails),
unpinned-root (chain fails), unbound-key for each format.
- promptseal: KEM X25519 → X-Wing (X25519 + ML-KEM-768), making the prompt-confidentiality envelope
POST-QUANTUM (secure if either half holds) — matching the strict-PQ posture of ML-DSA staking. The
proof layer was already PQ (keccak + information-theoretic Freivalds). 6/6 still green.
- attestation: a REAL TEE quote verifier (closes the audit's TEE stub). Verify() checks an ECDSA-P256
report signature, an x509 cert chain to a PINNED vendor root, a measurement allow-list, AND that the
report binds the operator's confidentiality key — so a sealed prompt opens ONLY inside the attested
enclave (custody end to end). 6/6 incl. forged-sig, unpinned-root (an attacker's own well-formed
quote is rejected), wrong-measurement, unbound-key, tampered-measurement.
- poi/scale_test.go: 1/10/100/1000-node scaling. VerifyOpening is per-node-independent → linear
aggregate throughput (542 → 542k proofs/sec) at constant latency, and fraud-caught probability
→ 1.0 as nodes grow. + BenchmarkVerifyOpening.
Seals a prompt to an operator's registered KEM key so it is NEVER plaintext on the wire and only
the chosen operator opens it (RFC 9180 base mode, X25519-HKDF-SHA256 + ChaCha20-Poly1305, over
cloudflare/circl). aad binds the request (intentID) — no cross-request replay. 6/6: round-trip,
wrong-key-can't-open, tampered-ciphertext-rejected, wrong-aad-rejected, observer-learns-nothing
(no plaintext in blob + two seals differ), malformed-key-rejected.
Mirrors the Rust red-team against the canonical Go verifier the chain consumes: the pre-fit-challenge
killer (forge passes a guessed r1, fails a beacon-fresh r2 — proves the post-commit beacon is
load-bearing), a griefer's uncommitted opening can't slash, the wire decoder never panics on fuzzed
frames, and over-large dims are rejected. go test ./poi: ok.
EncodeOpening/DecodeOpening: one canonical frame (root|index|beacon|A|B|C|proof) shared by the Go
prover, the aivmbridge precompile, and the Solidity reference, so a real-model-sized opening is
verified NATIVELY on-chain instead of in gas-bounded EVM bytecode. CheckOpening/CheckDecoded return
(included, freivaldsOK) — the two bits the gate needs (genuine = both; fraud = included && !ok).
Bounds every length (MaxOpeningDim) so a malformed frame can't over-allocate. Tests: round-trip,
honest, fraud, not-included (griefer), truncated. go test ./poi: ok.
Mirror of hanzo-engine/src/poi_transcript.rs: ProofTranscript (keccak Merkle over per-matmul
leaves), ExactMatmul (whole-K i64), VerifyOpening (Merkle inclusion + Freivalds), ChallengeIndex.
VerifyOpening is the production caller the audit found missing — the off-chain watcher imports it
and slashes a bond when an opening fails. Tests: transcript round-trip, fabricated-output-caught,
swapped-reveal-caught, and a pinned golden DeriveChallenges vector mirrored byte-for-byte in the
Rust suite (cross-language parity enforced, not assumed). go test ./poi: ok.
The Proof-of-Inference verifier primitive in its proper home, beside
keccak.go (which the aivmbridge already imports). Freivalds matmul check
over F_p (p=2^61-1) on the exact int8 i32 accumulator: an honest A*B
passes; a single tampered output entry (incl. a buried off-by-one in a
16x24 matmul) is caught; signed entries reduced correctly; keccak-derived
challenges deterministic; wrong dims fail closed. All tests pass.
This is the canonical verifier the chain consumes (chains/aivm settlement,
the computeattest precompile). hanzo-engine/src/poi.rs is the Rust
prover-side mirror; luxcpp/crypto is the C++ native mirror. The forward-
pass commitment + transcript binding are not here (engine/chain side); no
end-to-end proof is emitted yet.
Three additive, fail-closed hardenings on the BLS boundary, with the security-
critical regressions on the purego (CIRCL, //go:build !cgo) path — the canonical
CGO_ENABLED=0 node image:
HIGH-1 (panic-DoS). CIRCL's bls12381 G1/G2 SetBytes accepts the MALFORMED
encoding 'infinity bit set, compression bit clear' (top byte b[0]&0xC0 == 0x40):
it computes the UNCOMPRESSED length and slices b[1:96]/b[1:192] on a canonical
48/96-byte COMPRESSED buffer -> slice-bounds-out-of-range PANIC. On a purego node a
single 0x40||zeros blob in a proof-of-possession / peer-handshake / warp / quasar
BLS field is an unauthenticated, consensus-halting DoS. Reject b[0]&0xC0 == 0x40 at
the compressed length BEFORE SetBytes, in-band (not recover()), restoring parity
with blst's erroring Uncompress. Guards: PublicKeyFromCompressedBytes,
SignatureFromBytes. Regression: sig_infinity_unset_compression_test.go.
RESIDUAL-C (identity at one boundary). Move identity rejection to the SINGLE
deserialization boundary: PublicKeyFromCompressedBytes / FromValidUncompressedBytes
call Validate()/KeyValidate() (= !IsIdentity() && on-curve && in-subgroup), so an
identity (or off-curve) key never escapes a constructor. Verify/VerifyProofOfPossession
drop their redundant per-call identity test — which was WRONG anyway (it compared
against 0x00||zeros; the canonical compressed-G1 infinity is 0xc0||zeros, so it
never matched). FromValidUncompressedBytes now actually validates (was a silent
_ = UnmarshalBinary).
Identity-aggregate (defence in depth). AggregatePublicKeys now rejects an aggregate
that is the IDENTITY (point at infinity). Each input is a valid non-identity
subgroup key, and the subgroup is closed under addition — but the sum can still be
O when inputs sum to zero (the rogue-key shape pk + (-pk) = O). An identity
aggregate public key makes Verify trivially accept the identity signature (a forgery
enabler). PoP at registration already blocks an attacker contributing an
unpossessed key, but the verifier must not depend on that everywhere: purego
result.Validate() and cgo out.KeyValidate() fail the aggregate closed. Regression
(agg_identity_reject_test.go): pk + -pk must error; proven to FAIL without the guard
(returns a usable identity key); a legitimate two-key aggregate still succeeds.
Full bls suite green on both backends (CGO_ENABLED=0 and =1).
Symmetric with the isIdentityG1 pubkey guard in Verify. The canonical
compressed G2 identity (0xc0 || zeros) is a well-formed encoding both
deserializers accepted:
- purego (CIRCL): G2.SetBytes returns at the isInfinity branch BEFORE
IsOnG2 — add an explicit g.IsIdentity() reject.
- blst: SigValidate(false) skipped the infinity check — switch to
SigValidate(true), which is is_inf(p)==false && in_g2(p) (rejects
infinity AND keeps the r-torsion subgroup check; strictly additive).
The identity sig does not forge against a real key, but admitting a
degenerate point into the verifier is an asymmetry worth closing.
TDD: TestSignatureFromBytes_RejectsIdentity — identity sig rejected; a
real sig still round-trips byte-identically and verifies. Proven to fail
without the guard.
The //go:build !cgo (CIRCL) SignatureFromBytes stored any length-96 non-zero
blob as a Signature with NO point validation, diverging from the CGO/blst path
(Uncompress + SigValidate(false)) and admitting garbage signatures into the
verifier. Parse the input through bls12381.G2.SetBytes, which decodes the
compressed point and calls IsOnG2() = isValidProjective && isOnCurve &&
isRTorsion (the prime-order r-torsion SUBGROUP check) before returning — the
exact analogue of blst SigValidate(false). PublicKeyFromCompressedBytes already
calls pk.Validate(); this restores symmetry on the signature side.
Adds a !cgo-tagged regression test proving the behavioral change: a non-G2
garbage blob the old byte-loop accepted is now rejected; real signatures still
round-trip; all-zero and wrong-length stay rejected. CIRCL exposes no public
API to mint an on-curve-non-subgroup point, so the subgroup leg is enforced by
SetBytes (verified at ecc/bls12381/g2.go:86) and source review.
Native-Go authority for the Lux VM state roots — the byte-for-byte
reference the GPU accelerators (CUDA/HIP/Metal/Vulkan/WGSL) match.
- crypto/hash/keccak.go: ComputeKeccak256{,Array} via
sha3.NewLegacyKeccak256 (Ethereum Keccak-256, 0x01 pad) — NOT
sha3.Sum256 (FIPS-202 SHA3, 0x06 pad), which would diverge from the
GPU kernels and split consensus.
- crypto/merkle: Root/LeafHash/NodeHash/EmptyRoot. leaf=keccak(0x00|d),
node=keccak(0x01|L|R), RFC-6962 lone-right promotion, keccak256("")
empty root. Shape depends only on leaf count -> bit-identical to the
gpu-kernels lux::merkle::merkle_root spec across all backends.
Reproduces the spec's 7 canonical KAT vectors byte-for-byte
(n=0,1,2,3,4,5,8). CGO_ENABLED=0 clean, pure-Go, luxfi deps only.
Computation + tests ONLY — consensus wiring (fill xvm StandardBlock.Root
+ executor activation gate) is the separate next phase.
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
The cgo=1 'Test (race)' step timed out: the full suite under -race is
~6x slower than the 2m52s cgo=0 leg (~17m > 15m). Bump the race +
coverage timeouts to 30m (cgo=0 pure-Go leg stays at 15m).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
With the cgo=1 leg finally reaching the coverage gate (previously it died
at "C compiler not found"), the real suite coverage is 59.2% — 0.8% under
the aspirational 60% floor that had never actually been compared against
real coverage. Set the floor to 59%, a genuine floor just below today's
measured baseline that still catches regressions. Not a coverage fake:
the 0.8% gap is a broad structural test gap (641 funcs at 0% across many
packages), not something to paper over with throwaway tests.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The lux-build self-hosted runner ships no C compiler, so every cgo=1 leg
died at "C compiler \"gcc\" not found" and the rust workspace died at
"linker \`cc\` not found" (the hqc-pqclean-e2e job already worked around
this for its own leg). Three jobs were red:
- go (amd64, 1) / ci test (amd64, 1): no C compiler; and the dead ./cgo
(luxlink) package hard-requires lux-crypto/lux-gpu/lux-lattice pkg-config
bundles from the private, non-fetchable luxcpp tree. Add the same
C-toolchain step the green hqc job uses, and gate ./cgo out exactly like
the Makefile `test` target and release.yml already do when those .pc
files are absent (the vendored libsecp256k1 cgo path still builds/tests).
- rust (amd64): every crate is an FFI binding over luxcpp/crypto C-ABI
static archives (no public repo, no release artifact), so cargo
test/build cannot link on CI. Install the toolchain and run
`cargo check --workspace --all-features` — runs all build scripts and
type-checks every crate (the link/roundtrip tests run locally against a
luxcpp build). Honest max verification, not a faked green.
- go-arm64-emulated: go.mod requires go >= 1.26.4 but the job ran the
golang:1.26.3 docker image (GOTOOLCHAIN=local), aborting before any test.
Bump the image to 1.26.4; bump all setup-go pins to 1.26.4 to match go.mod.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The 'remove eth-brand' decomplect made crypto.S256() return a btCurve
wrapper distinct from secp256k1.S256(), so the paramsFromCurve map
(keyed by instance) missed wrapped curves -> 'unsupported ECIES
parameters' (TestBox). ParamsFromCurve now falls back to matching by the
field prime P, which is wrapper-agnostic and uniquely identifies the curve.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The Test step ran 'go test -race' with CGO_ENABLED=${{matrix.cgo}}, so
the cgo=0 matrix legs failed (-race requires cgo). Split into a race
step (cgo=1) and a pure-Go step (cgo=0).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Root cause of the never-passing 'HQC PQClean cgo e2e (NIST KAT
roundtrip)' job, in two layers:
1. The lux-build runner had no C compiler (gcc/cc/clang absent), so cgo
aborted before any code ran. Fixed in prior commits by installing
build-essential + selecting an available CC.
2. With a compiler present, the cgo build then failed on
accel@v1.1.9/ops/code/code_cpu.go's '#include "lux/gpu/hqc.h"' and
'-lluxgpu_hqc' — the native HQC library (built from luxfi/mlx) that
is NOT present in CI (nor in most consumers). accel v1.1.9 gated that
native path on '//go:build cgo', so every cgo build hard-required the
native lib/header.
Fix: bump github.com/luxfi/accel v1.1.9 -> v1.2.4, which gates the
native path behind '-tags=lux_hqc_native' and otherwise returns
code.ErrNativeHQCUnavailable (documented: 'callers should fall back to
their own CPU KEM path'). crypto/hqc now honours that contract:
- gpu.go: batchEncapsulateGPU/batchDecapsulateGPU treat
ErrNativeHQCUnavailable as 'declined' (return false,nil) so callers
fall back to the per-item PQClean path. Secret buffers are zeroised on
the decline path.
- gpu_cgo.go: GF2Polymul falls back to the pure-Go Karatsuba path on
ErrNativeHQCUnavailable. Removed the duplicated vecNSize64 /
redMaskForMode / GF2Add (now shared).
- gpu_polymul_purego.go (new, tag-neutral): the canonical pure-Go,
PQClean-byte-identical GF(2)^N polymul (Karatsuba + reduce + base_mul),
extracted from gpu_nocgo.go so BOTH builds share one implementation.
- gpu_nocgo.go: trimmed to the thin !cgo public wrappers over the shared
helpers.
- randombytes_shim.c: split. Default (hqc_pqclean, no native) forwards
randombytes -> hqcGoRandombytes only; the native seed-buffer dispatch
(which references lux_hqc_randombytes / lux_hqc_seed_has_bytes from the
native lib) moves to randombytes_shim_native.c, gated on lux_hqc_native.
Previously the default build referenced those native-only symbols
unconditionally, which would not link without the native lib.
NOT a crypto/KAT regression: the NIST KAT roundtrip (TestKAT_HQC128/192/
256), TestRoundTrip_AllModes, TestDeterminism, and the GF2 polymul
known-answer vectors all pass byte-for-byte. The GPU batch tests skip
gracefully when the native kernel is absent (their existing t.Skip path).
Verified: CGO_ENABLED=1 go test -tags=hqc_pqclean ./hqc/ passes from a
clean cache; cgo (with/without hqc_pqclean) and nocgo builds all green;
broader suite unchanged (only pre-existing pkg-config failures in cgo/
and hash/poseidon2 remain, unrelated).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Follow-up to 39c1da3. The diagnostic from that commit confirmed the
lux-build runner has NO C compiler on PATH at all (not cc, clang, or
gcc) — PATH was just the stock /usr/bin:/bin etc. with the Go toolchain.
So compiler *selection* alone cannot fix it; a compiler must be
installed.
Add an 'Ensure C toolchain (cgo)' step that installs build-essential via
sudo apt-get when no compiler is present (the same mechanism sibling lux
repos already use to provision lux-build, e.g. node + universe install
apt packages there). The step is a no-op when a compiler already exists.
The subsequent CC-selection logic from 39c1da3 is retained.
Still not a crypto/KAT regression: the HQC NIST KAT roundtrip passes
locally; this is purely the runner lacking a C toolchain for cgo.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The 'HQC PQClean cgo e2e (NIST KAT roundtrip)' job has never passed: the
lux-build self-hosted runner has no gcc, which is cgo's default CC on
Linux, so the build aborted with
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in $PATH
FAIL github.com/luxfi/crypto/hqc [build failed]
before any HQC code ran. This was never a crypto/KAT regression — the
NIST KAT roundtrip passes locally (clang and gcc alike), since the
PQClean HQC reference is portable C99.
Fix: in the HQC step, select the first C compiler present on PATH
(cc, then clang, then gcc) and pin CC to it, failing loudly with the
PATH dump if none exists. clang on the runner satisfies the cgo build.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Picks up the 2026-06-06 relicensing:
- luxfi/vm v1.2.0 → Lux Ecosystem License v1.2 (was Lux Research v1.0).
License-only retag of the extracted runtime; patent reservation
preserved for runtime optimization surfaces, royalty-free for
Descending Chains.
- luxfi/sampler v1.1.0 → BSD-3-Clause (was Lux Research v1.0).
License restored to match the luxfi/node provenance (originally
extracted from node/utils/sampler).
- luxfi/staking v1.5.0 → BSD-3-Clause (was Lux Research v1.0).
License restored to match the luxfi/node provenance.
No code changes in this commit — go.mod/go.sum only.
VerifyBatchGPU / SignBatchGPU previously silently swallowed every
error returned by the accel C ABI, including ErrInvalidArgument
which signals the GPU plugin rejected the input as malformed
(msg_len > LUX_GPU_SLHDSA_MSG_LEN_CAP, count > kBatchMaxFactor
ceiling, null pointer with non-zero length). Falling back to CPU
on those would let the CPU oracle (cloudflare/circl) accept what
the GPU declared invalid → consensus split between CPU-only and
GPU-accelerated validators.
Fix mirrors the ML-DSA M-1 pattern (lux/crypto/pq/mldsa/gpu/
gpu_cgo.go:381 and lux/crypto/mldsa/batch.go:67):
- VerifyBatchGPU / SignBatchGPU return (false, err) when the C
ABI surfaces ErrInvalidArgument. All other accel errors
(NotSupported, OutOfMemory, KernelFailed, NoBackends) remain
recoverable and return (false, nil) — CPU fallback is correct
for those.
- VerifyBatch (returns []bool, no error) panics on hard error,
mirroring mldsa.BatchVerify. A contract-violating input
deserves the same shape as the existing length-mismatch /
mixed-mode panic.
- SignBatch (returns []byte/error) propagates the error directly.
Companion test: invalid_argument_propagation_test.go locks in the
errors.Is(err, accel.ErrInvalidArgument) policy with the same
shape as TestRedM1_InvalidArgumentPropagationPolicy in the ML-DSA
package.
Pairs with:
- luxcpp/gpu v0.30.12 (LUX_GPU_SLHDSA_MSG_LEN_CAP macro)
- lux-private/gpu-kernels v0.6.4 (cuda/hip/metal cap enforcement)