mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Merge feature/quantumvm-cgo-bridge: quantumvm GPU plugin bridge
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quantumvm
|
||||
|
||||
// Q-Chain GPU substrate — host-side ABI v14 plugin bridge for batched
|
||||
// post-quantum signature verification + signing.
|
||||
//
|
||||
// The kernels live in ~/work/lux-private/gpu-kernels under
|
||||
// ops/crypto/{mldsa,slhdsa}/<backend>/ and are shipped inside the
|
||||
// libluxgpu_backend_<x>.{so,dylib} plugin shared libraries. The Go side
|
||||
// dlopens whichever plugin is on disk at process start, populates the
|
||||
// ABI v14 backend descriptor via the `lux_gpu_backend_init` entry point
|
||||
// (see lux-private/gpu-kernels/include/lux/gpu/backend_plugin.h), and
|
||||
// invokes the op_mldsa_verify_batch / op_mldsa_sign_batch /
|
||||
// op_slhdsa_verify_batch slots from the resulting vtbl.
|
||||
//
|
||||
// Decomplecting from cevm/cevm_cgo.go: cevm links its kernel bundle via
|
||||
// pkg-config (lux-cevm.pc → -levm -levm-gpu …) and reaches the C ABI
|
||||
// through extern "C" gpu_* helpers. Q-Chain takes the opposite tack —
|
||||
// no pkg-config, no static link, no header-bound C ABI helpers. The
|
||||
// plugin is OPTIONAL by design: when no libluxgpu_backend_*.{so,dylib}
|
||||
// is on the dlopen search path, AutoBackend() returns BackendNone and
|
||||
// every GPUBackend method returns ErrGPUNotAvailable so the existing
|
||||
// CPU verify path stays unchanged.
|
||||
//
|
||||
// Pattern (mirrors evm/cevm + evm/backend_cgo.go split):
|
||||
//
|
||||
// backend.go — shared types: Backend enum, GPUBackend
|
||||
// interface, ErrGPUNotAvailable, active
|
||||
// backend atomic + AutoBackend(). Build-
|
||||
// tag-free; the runtime probe lives in
|
||||
// the cgo/nocgo files below.
|
||||
// quantumvm_gpu.go cgo — dlopen + dlsym of lux_gpu_backend_init,
|
||||
// vtbl slot invocations.
|
||||
// quantumvm_gpu_nocgo.go !cgo — stub returning ErrGPUNotAvailable.
|
||||
//
|
||||
// This file is build-tag-free so vm.go can reference Backend, GPUBackend,
|
||||
// AutoBackend(), and ErrGPUNotAvailable regardless of CGO_ENABLED. The
|
||||
// implementation behind ActiveGPUBackend() switches based on the build
|
||||
// tag (real plugin handle vs. stub returning ErrGPUNotAvailable).
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Backend identifies which GPU plugin satisfied the runtime dlopen probe.
|
||||
//
|
||||
// Probe order is fixed by the spec: cuda → hip → metal → vulkan → webgpu.
|
||||
// The first plugin that resolves the `lux_gpu_backend_init` entry point and
|
||||
// reports a v14 vtbl with the three Q-Chain slots (op_mldsa_verify_batch,
|
||||
// op_mldsa_sign_batch, op_slhdsa_verify_batch) wins; remaining probes are
|
||||
// skipped. Each plugin is fully self-contained — there is no fallback chain
|
||||
// once one is chosen (this keeps backend selection deterministic across
|
||||
// reboots, matching consensus-safety expectations).
|
||||
type Backend uint8
|
||||
|
||||
const (
|
||||
// BackendNone means no GPU plugin is loaded — every GPUBackend method
|
||||
// returns ErrGPUNotAvailable. This is the value reported by
|
||||
// AutoBackend() under !cgo, and under cgo when no
|
||||
// libluxgpu_backend_*.{so,dylib} was findable.
|
||||
BackendNone Backend = 0
|
||||
// BackendCUDA selects libluxgpu_backend_cuda.so (NVIDIA, Linux/Windows).
|
||||
BackendCUDA Backend = 1
|
||||
// BackendHIP selects libluxgpu_backend_hip.so (AMD ROCm, Linux/Windows).
|
||||
BackendHIP Backend = 2
|
||||
// BackendMetal selects libluxgpu_backend_metal.dylib (Apple, darwin).
|
||||
BackendMetal Backend = 3
|
||||
// BackendVulkan selects libluxgpu_backend_vulkan.{so,dylib} (portable).
|
||||
BackendVulkan Backend = 4
|
||||
// BackendWebGPU selects libluxgpu_backend_webgpu.{so,dylib} (portable).
|
||||
BackendWebGPU Backend = 5
|
||||
)
|
||||
|
||||
// String returns the canonical lowercase name of the backend. The same
|
||||
// name appears in the plugin DSO filename (libluxgpu_backend_<name>.*)
|
||||
// and in the lux_gpu_backend_desc.backend_name field reported by the
|
||||
// loaded plugin.
|
||||
func (b Backend) String() string {
|
||||
switch b {
|
||||
case BackendNone:
|
||||
return "none"
|
||||
case BackendCUDA:
|
||||
return "cuda"
|
||||
case BackendHIP:
|
||||
return "hip"
|
||||
case BackendMetal:
|
||||
return "metal"
|
||||
case BackendVulkan:
|
||||
return "vulkan"
|
||||
case BackendWebGPU:
|
||||
return "webgpu"
|
||||
default:
|
||||
return fmt.Sprintf("unknown(%d)", uint8(b))
|
||||
}
|
||||
}
|
||||
|
||||
// ErrGPUNotAvailable is returned by every GPUBackend method when no plugin
|
||||
// is loaded — either because the binary was built without CGo, no
|
||||
// libluxgpu_backend_*.{so,dylib} was findable on the dlopen search path,
|
||||
// the loaded plugin reported a non-v14 ABI version, or its vtbl was
|
||||
// missing one of the three Q-Chain slots.
|
||||
//
|
||||
// The error is sentinel-comparable via errors.Is so callers can route
|
||||
// the CPU verify path cleanly:
|
||||
//
|
||||
// if errors.Is(err, quantumvm.ErrGPUNotAvailable) {
|
||||
// return cpuVerify(pk, msg, sig)
|
||||
// }
|
||||
var ErrGPUNotAvailable = errors.New("quantumvm: GPU backend not available")
|
||||
|
||||
// MLDSAMode selects which ML-DSA security parameter set the batch
|
||||
// targets. The ABI v14 vtbl currently exposes only ML-DSA-65 (mode 3,
|
||||
// FIPS 204 Dilithium3) — modes 44 and 87 will land at their own vtbl
|
||||
// slots in a future ABI bump (see backend_plugin.h v14 block). Callers
|
||||
// pass the mode through so the wrapper can validate batch byte widths
|
||||
// at the boundary, even though only MLDSA65 round-trips through the
|
||||
// plugin today.
|
||||
type MLDSAMode uint8
|
||||
|
||||
const (
|
||||
// MLDSAMode44 is FIPS 204 Dilithium2. Not yet wired through the v14
|
||||
// vtbl — passing this to a batch method returns ErrGPUNotAvailable
|
||||
// (caller falls through to the CPU path).
|
||||
MLDSAMode44 MLDSAMode = 1
|
||||
// MLDSAMode65 is FIPS 204 Dilithium3 — the canonical Q-Chain mode.
|
||||
// pk=1952 B, sig=3309 B (max), sk=4032 B.
|
||||
MLDSAMode65 MLDSAMode = 2
|
||||
// MLDSAMode87 is FIPS 204 Dilithium5. Not yet wired through the v14
|
||||
// vtbl — passing this returns ErrGPUNotAvailable.
|
||||
MLDSAMode87 MLDSAMode = 3
|
||||
)
|
||||
|
||||
// ML-DSA-65 byte sizes per FIPS 204 §4. Pinned constants — the plugin
|
||||
// rejects mismatched sk_stride at the boundary, so we surface them up
|
||||
// front to keep the call sites unambiguous.
|
||||
const (
|
||||
MLDSA65PublicKeySize = 1952
|
||||
MLDSA65SecretKeySize = 4032
|
||||
MLDSA65SignatureSize = 3309
|
||||
)
|
||||
|
||||
// SLHDSAVariant enumerates the 12 FIPS 205 SLH-DSA parameter sets. Each
|
||||
// variant has its own (pk, sig) byte width — surfaced via PublicKeySize()
|
||||
// and SignatureSize() so callers can build the batch arrays correctly.
|
||||
//
|
||||
// At ABI v14 only the SHAKE-128f and SHAKE-192f verify slots are wired
|
||||
// (op_slhdsa_verify_batch and op_slhdsa_verify_batch_shake192f). The
|
||||
// remaining 10 variants land at their own vtbl slots in future ABI
|
||||
// bumps; passing them today returns ErrGPUNotAvailable.
|
||||
type SLHDSAVariant uint8
|
||||
|
||||
const (
|
||||
SLHDSAShake128f SLHDSAVariant = iota
|
||||
SLHDSAShake128s
|
||||
SLHDSAShake192f
|
||||
SLHDSAShake192s
|
||||
SLHDSAShake256f
|
||||
SLHDSAShake256s
|
||||
SLHDSASha2128f
|
||||
SLHDSASha2128s
|
||||
SLHDSASha2192f
|
||||
SLHDSASha2192s
|
||||
SLHDSASha2256f
|
||||
SLHDSASha2256s
|
||||
)
|
||||
|
||||
// String returns the FIPS 205 canonical name of the variant.
|
||||
func (v SLHDSAVariant) String() string {
|
||||
switch v {
|
||||
case SLHDSAShake128f:
|
||||
return "shake-128f"
|
||||
case SLHDSAShake128s:
|
||||
return "shake-128s"
|
||||
case SLHDSAShake192f:
|
||||
return "shake-192f"
|
||||
case SLHDSAShake192s:
|
||||
return "shake-192s"
|
||||
case SLHDSAShake256f:
|
||||
return "shake-256f"
|
||||
case SLHDSAShake256s:
|
||||
return "shake-256s"
|
||||
case SLHDSASha2128f:
|
||||
return "sha2-128f"
|
||||
case SLHDSASha2128s:
|
||||
return "sha2-128s"
|
||||
case SLHDSASha2192f:
|
||||
return "sha2-192f"
|
||||
case SLHDSASha2192s:
|
||||
return "sha2-192s"
|
||||
case SLHDSASha2256f:
|
||||
return "sha2-256f"
|
||||
case SLHDSASha2256s:
|
||||
return "sha2-256s"
|
||||
default:
|
||||
return fmt.Sprintf("slhdsa(%d)", uint8(v))
|
||||
}
|
||||
}
|
||||
|
||||
// PublicKeySize returns the FIPS 205 byte width of an SLH-DSA public key
|
||||
// for this variant. {128,192,256}{f,s} all share the same width per
|
||||
// security level (32 / 48 / 64 bytes).
|
||||
func (v SLHDSAVariant) PublicKeySize() int {
|
||||
switch v {
|
||||
case SLHDSAShake128f, SLHDSAShake128s, SLHDSASha2128f, SLHDSASha2128s:
|
||||
return 32
|
||||
case SLHDSAShake192f, SLHDSAShake192s, SLHDSASha2192f, SLHDSASha2192s:
|
||||
return 48
|
||||
case SLHDSAShake256f, SLHDSAShake256s, SLHDSASha2256f, SLHDSASha2256s:
|
||||
return 64
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureSize returns the FIPS 205 byte width of an SLH-DSA signature
|
||||
// for this variant. The 12 widths are fixed by parameter set; see the
|
||||
// FIPS 205 standard §11 width table.
|
||||
func (v SLHDSAVariant) SignatureSize() int {
|
||||
switch v {
|
||||
case SLHDSAShake128f, SLHDSASha2128f:
|
||||
return 17088
|
||||
case SLHDSAShake128s, SLHDSASha2128s:
|
||||
return 7856
|
||||
case SLHDSAShake192f, SLHDSASha2192f:
|
||||
return 35664
|
||||
case SLHDSAShake192s, SLHDSASha2192s:
|
||||
return 16224
|
||||
case SLHDSAShake256f, SLHDSASha2256f:
|
||||
return 49856
|
||||
case SLHDSAShake256s, SLHDSASha2256s:
|
||||
return 29792
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// GPUBackend is the narrow Q-Chain surface that vm.go's batch verify /
|
||||
// sign paths can opt into. Three 1:1 mappings to the ABI v14 vtbl slots,
|
||||
// plus Backend() / Close() lifecycle.
|
||||
//
|
||||
// Buffer ownership: callers own every slice / struct passed in. The vtbl
|
||||
// slot does H2D / D2H internally. On return every output slice has been
|
||||
// overwritten with the kernel's result; the caller can read immediately,
|
||||
// no further sync needed.
|
||||
//
|
||||
// Composition: the three batch methods are orthogonal — a "round" that
|
||||
// verifies a mixed batch of ML-DSA + SLH-DSA stamps composes them in
|
||||
// the round applier. We do NOT try to express a "verify all PQ sigs"
|
||||
// supercall here; that would couple the round-level policy to the
|
||||
// substrate, and the substrate intentionally only exposes primitives.
|
||||
type GPUBackend interface {
|
||||
// MLDSAVerifyBatch verifies a batch of ML-DSA signatures. mode pins
|
||||
// the FIPS 204 parameter set; only MLDSAMode65 is wired through the
|
||||
// v14 vtbl (modes 44/87 return ErrGPUNotAvailable). pubkeys[i],
|
||||
// messages[i], and signatures[i] supply the per-element inputs;
|
||||
// msgLens[i] is the byte length of messages[i] (NULL slice means
|
||||
// every message is exactly msgWidthHint bytes — see the
|
||||
// op_mldsa_verify_batch comment in backend_plugin.h for the
|
||||
// uniform-batch convenience contract). results[i] is set to true
|
||||
// iff verification succeeds.
|
||||
//
|
||||
// The slices must all have the same length; mismatched lengths are
|
||||
// caught at the boundary and returned as an error (no UB into C).
|
||||
MLDSAVerifyBatch(
|
||||
mode MLDSAMode,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error
|
||||
|
||||
// MLDSASignBatch signs a batch of messages with packed ML-DSA secret
|
||||
// keys. mode pins the FIPS 204 parameter set; only MLDSAMode65 is
|
||||
// wired through the v14 vtbl. skeys is the contiguous pool of
|
||||
// per-element packed secret keys (each MLDSA65SecretKeySize = 4032
|
||||
// bytes); the wrapper feeds the pool through cudaMemcpy2D directly
|
||||
// in one shot. msgs is the contiguous pool of per-element message
|
||||
// payloads (msgLens[i] bytes from offset sum(msgLens[0..i-1])).
|
||||
// sigsOut receives count × MLDSA65SignatureSize bytes; sigLensOut
|
||||
// returns the per-element actual signature length (3309 on accept,
|
||||
// 0 on kappa-cap reject — see backend_plugin.h v14 block).
|
||||
MLDSASignBatch(
|
||||
mode MLDSAMode,
|
||||
skeys []byte,
|
||||
msgs []byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
count int,
|
||||
sigsOut []byte,
|
||||
sigLensOut []uint32,
|
||||
) error
|
||||
|
||||
// SLHDSAVerifyBatch verifies a batch of SLH-DSA signatures. variant
|
||||
// pins the FIPS 205 parameter set; only SHAKE-128f and SHAKE-192f
|
||||
// are wired through the v14 vtbl (the other 10 variants return
|
||||
// ErrGPUNotAvailable). pubkeys[i], messages[i], and signatures[i]
|
||||
// supply the per-element inputs at the FIPS 205 widths reported by
|
||||
// variant.PublicKeySize() / variant.SignatureSize(); msgLens[i] is
|
||||
// the byte length of messages[i] (any value in [0, INT32_MAX-2]).
|
||||
// results[i] is set to true iff verification succeeds.
|
||||
SLHDSAVerifyBatch(
|
||||
variant SLHDSAVariant,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error
|
||||
|
||||
// Backend reports which plugin is currently loaded.
|
||||
Backend() Backend
|
||||
|
||||
// Close releases the dlopen handle and the LuxBackendContext. Safe
|
||||
// on a nil receiver and idempotent.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// activeBackend is set by the init() in quantumvm_gpu.go (cgo) or
|
||||
// quantumvm_gpu_nocgo.go (!cgo). Read via AutoBackend(). Stored as an
|
||||
// atomic uint32 so concurrent reads from vm.go's batch paths don't race
|
||||
// with the init() store.
|
||||
var activeBackend atomic.Uint32
|
||||
|
||||
// AutoBackend returns the GPU plugin chosen by the dlopen probe at process
|
||||
// start. BackendNone means no plugin is loaded — callers should route to
|
||||
// the CPU verify path. The probe runs exactly once at init time; this
|
||||
// getter is one atomic load and safe to call from any goroutine.
|
||||
func AutoBackend() Backend {
|
||||
return Backend(uint8(activeBackend.Load()))
|
||||
}
|
||||
|
||||
// setActiveBackend records the probe result. Called by init() in the
|
||||
// cgo / nocgo files. Not exported — there is one and only one way to
|
||||
// load the plugin (the init probe at process start).
|
||||
func setActiveBackend(b Backend) {
|
||||
activeBackend.Store(uint32(uint8(b)))
|
||||
}
|
||||
@@ -0,0 +1,925 @@
|
||||
//go:build cgo
|
||||
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package quantumvm GPU backend — runtime-loaded plugin bridge via the
|
||||
// ABI v14 vtbl entry point.
|
||||
//
|
||||
// Unlike cevm/cevm_cgo.go (which links libevm + libevm-gpu via pkg-config
|
||||
// and reaches the C ABI through extern "C" gpu_* helpers), Q-Chain
|
||||
// resolves its GPU substrate at PROCESS START via dlopen / dlsym against
|
||||
// the lux-gpu-kernels plugin DSO and reaches the per-op kernel through
|
||||
// function pointers inside a vtbl populated by the plugin's
|
||||
// `lux_gpu_backend_init` entry point. This keeps the chains module
|
||||
// compilable without lux-private/gpu-kernels present in the build tree —
|
||||
// the plugin is fully optional. When no libluxgpu_backend_*.{so,dylib}
|
||||
// is findable on the dlopen search path, AutoBackend() returns
|
||||
// BackendNone, every GPUBackend method returns ErrGPUNotAvailable, and
|
||||
// the existing CPU verify path stays unchanged.
|
||||
//
|
||||
// Probe order (init()): cuda → hip → metal → vulkan → webgpu. First
|
||||
// plugin that resolves the entry point AND reports a v14 ABI matching
|
||||
// the header we compiled against AND exposes the three Q-Chain slots
|
||||
// (op_mldsa_verify_batch, op_mldsa_sign_batch,
|
||||
// op_slhdsa_verify_batch[_shake192f]) wins; remaining probes are
|
||||
// skipped. The cookie checks (abi_version + vtbl_size) defend against
|
||||
// a plugin that lies about its ABI version while shipping a truncated
|
||||
// vtable (see backend_plugin.h §"Backend Descriptor" comment).
|
||||
//
|
||||
// Search path: the dlopen library name is the bare basename
|
||||
// (libluxgpu_backend_<x>.{so,dylib}); the loader's standard
|
||||
// LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / rpath resolution finds it.
|
||||
// LUX_GPU_PLUGIN_DIR overrides — if set, every probe joins it to the
|
||||
// basename before calling dlopen.
|
||||
package quantumvm
|
||||
|
||||
/*
|
||||
#cgo darwin LDFLAGS: -ldl
|
||||
#cgo linux LDFLAGS: -ldl
|
||||
|
||||
// Workspace-relative path to the ABI v14 plugin header. From
|
||||
// ${SRCDIR} = ~/work/lux/chains/quantumvm we walk three levels up
|
||||
// (chains → lux → work) and one down (lux-private/gpu-kernels/include)
|
||||
// to reach the canonical header. External consumers building from
|
||||
// $GOMODCACHE need lux-private/gpu-kernels checked out at the standard
|
||||
// sibling location; this matches the convention LLM.md establishes
|
||||
// for every other workspace-coupled component (LUXCPP_DIR etc.).
|
||||
//
|
||||
// Header-only inclusion: we DO NOT link against any luxcpp / lux-gpu
|
||||
// library. The plugin DSO is dlopen'd at runtime; the only compile-
|
||||
// time dependency is the C struct layout in backend_plugin.h.
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../../lux-private/gpu-kernels/include
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lux/gpu/backend_plugin.h"
|
||||
|
||||
// ABI v14 vtbl entry-point thunk. dlsym returns void*; this struct lets
|
||||
// Go pass the function pointer back through the bridge without losing
|
||||
// type information at the cgo boundary.
|
||||
typedef bool (*qvm_backend_init_fn)(lux_gpu_backend_desc* out);
|
||||
|
||||
// qvm_backend_init invokes the plugin's `lux_gpu_backend_init` symbol
|
||||
// and returns 0 on success, non-zero (1..5) on a specific cookie /
|
||||
// slot failure. The cookies (abi_version + vtbl_size) detect plugins
|
||||
// built against a different header — fail fast at process start
|
||||
// instead of UB at the first vtbl slot call (a stale plugin's
|
||||
// op_mldsa_verify_batch at the wrong offset would be a junk function
|
||||
// pointer dispatch otherwise).
|
||||
//
|
||||
// ret == 0 — desc populated AND all three Q-Chain slots non-NULL.
|
||||
// ret == 1 — init function returned false (no runtime available on
|
||||
// this host: missing driver, ICD, etc.). Caller treats
|
||||
// as SKIP, not FAIL.
|
||||
// ret == 2 — abi_version mismatch (plugin is from a different ABI
|
||||
// than the Go bindings were compiled against).
|
||||
// ret == 3 — vtbl_size mismatch (plugin shipped a truncated vtbl).
|
||||
// ret == 4 — vtbl pointer is NULL (malformed descriptor).
|
||||
// ret == 5 — vtbl is missing one of the three Q-Chain slots.
|
||||
static int qvm_backend_init(void* sym, lux_gpu_backend_desc* desc) {
|
||||
if (!sym || !desc) return 4;
|
||||
memset(desc, 0, sizeof(*desc));
|
||||
qvm_backend_init_fn fn = (qvm_backend_init_fn)sym;
|
||||
if (!fn(desc)) return 1;
|
||||
if (desc->abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 2;
|
||||
if (desc->vtbl_size != sizeof(lux_gpu_backend_vtbl)) return 3;
|
||||
if (!desc->vtbl) return 4;
|
||||
if (!desc->vtbl->create_context || !desc->vtbl->destroy_context) return 5;
|
||||
// ML-DSA verify is the canonical Q-Chain slot. ML-DSA sign and
|
||||
// SLH-DSA verify may be nullptr on backends that haven't wired
|
||||
// their orchestrator yet — those methods then return
|
||||
// LUX_BACKEND_ERROR_NOT_SUPPORTED at call time, which the Go
|
||||
// wrapper translates to ErrGPUNotAvailable. The plugin is still
|
||||
// viable if at least op_mldsa_verify_batch is wired (ML-DSA
|
||||
// verify is the load-bearing Q-Chain op).
|
||||
if (!desc->vtbl->op_mldsa_verify_batch) return 5;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lifecycle helpers — exported so the Go side can open / close a
|
||||
// LuxBackendContext per probe-result backend exactly once. device_index
|
||||
// = 0 is the canonical "first available device" sentinel — backends
|
||||
// like Metal that don't expose multi-GPU selection assume 0 and would
|
||||
// throw on -1 (NSRangeException on Apple Silicon, observed).
|
||||
static LuxBackendContext* qvm_create_context(const lux_gpu_backend_vtbl* vtbl) {
|
||||
return vtbl->create_context(0);
|
||||
}
|
||||
static void qvm_destroy_context(const lux_gpu_backend_vtbl* vtbl, LuxBackendContext* ctx) {
|
||||
if (ctx) vtbl->destroy_context(ctx);
|
||||
}
|
||||
|
||||
// Per-slot wrappers. The trampolines unpack Go's flat parameter slices
|
||||
// and dispatch through the vtbl function pointer. Each returns the raw
|
||||
// LuxBackendError code — the Go side maps NOT_SUPPORTED (3) →
|
||||
// ErrGPUNotAvailable and other non-zero codes → fmt.Errorf("rc=%d").
|
||||
|
||||
// op_mldsa_verify_batch — see backend_plugin.h v14 block. msg_lens is
|
||||
// optional (NULL → use msg_width_hint for every element).
|
||||
static int qvm_op_mldsa_verify(
|
||||
const lux_gpu_backend_vtbl* vtbl,
|
||||
LuxBackendContext* ctx,
|
||||
const uint8_t* const* pubkeys,
|
||||
const uint8_t* const* messages,
|
||||
const size_t* msg_lens,
|
||||
uint32_t msg_width_hint,
|
||||
const uint8_t* const* signatures,
|
||||
bool* results,
|
||||
size_t count) {
|
||||
if (!vtbl->op_mldsa_verify_batch) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
||||
return (int)vtbl->op_mldsa_verify_batch(
|
||||
ctx, pubkeys, messages, msg_lens,
|
||||
msg_width_hint, signatures, results, count);
|
||||
}
|
||||
|
||||
// op_mldsa_sign_batch — see backend_plugin.h v14 block. sig_lens_out is
|
||||
// optional (NULL is fine — caller falls back to per-element verify if
|
||||
// it needs accept / reject feedback).
|
||||
static int qvm_op_mldsa_sign(
|
||||
const lux_gpu_backend_vtbl* vtbl,
|
||||
LuxBackendContext* ctx,
|
||||
const uint8_t* skeys,
|
||||
size_t sk_stride,
|
||||
const uint8_t* msgs,
|
||||
const size_t* msg_lens,
|
||||
uint32_t msg_width_hint,
|
||||
size_t count,
|
||||
uint8_t* sigs_out,
|
||||
uint32_t* sig_lens_out) {
|
||||
if (!vtbl->op_mldsa_sign_batch) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
||||
return (int)vtbl->op_mldsa_sign_batch(
|
||||
ctx, skeys, sk_stride, msgs, msg_lens,
|
||||
msg_width_hint, count, sigs_out, sig_lens_out);
|
||||
}
|
||||
|
||||
// op_slhdsa_verify_batch — SHAKE-128f. See backend_plugin.h ABI v13.
|
||||
static int qvm_op_slhdsa_verify_128f(
|
||||
const lux_gpu_backend_vtbl* vtbl,
|
||||
LuxBackendContext* ctx,
|
||||
const uint8_t* const* pubkeys,
|
||||
const uint8_t* const* messages,
|
||||
const size_t* msg_lens,
|
||||
const uint8_t* const* signatures,
|
||||
bool* results,
|
||||
size_t count) {
|
||||
if (!vtbl->op_slhdsa_verify_batch) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
||||
return (int)vtbl->op_slhdsa_verify_batch(
|
||||
ctx, pubkeys, messages, msg_lens, signatures, results, count);
|
||||
}
|
||||
|
||||
// op_slhdsa_verify_batch_shake192f — SHAKE-192f. See backend_plugin.h
|
||||
// ABI v13.
|
||||
static int qvm_op_slhdsa_verify_192f(
|
||||
const lux_gpu_backend_vtbl* vtbl,
|
||||
LuxBackendContext* ctx,
|
||||
const uint8_t* const* pubkeys,
|
||||
const uint8_t* const* messages,
|
||||
const size_t* msg_lens,
|
||||
const uint8_t* const* signatures,
|
||||
bool* results,
|
||||
size_t count) {
|
||||
if (!vtbl->op_slhdsa_verify_batch_shake192f) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
||||
return (int)vtbl->op_slhdsa_verify_batch_shake192f(
|
||||
ctx, pubkeys, messages, msg_lens, signatures, results, count);
|
||||
}
|
||||
|
||||
// dlopen / dlsym wrappers — kept here so backend.go stays pure Go.
|
||||
static void* qvm_dlopen(const char* path) {
|
||||
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||
}
|
||||
static void* qvm_dlsym(void* handle, const char* sym) {
|
||||
return dlsym(handle, sym);
|
||||
}
|
||||
static const char* qvm_dlerror(void) {
|
||||
return dlerror();
|
||||
}
|
||||
static void qvm_dlclose(void* handle) {
|
||||
if (handle) dlclose(handle);
|
||||
}
|
||||
|
||||
// Allocates a lux_gpu_backend_desc the Go side keeps for the lifetime
|
||||
// of the GPUBackend. Heap-allocated so the Go object's address can be
|
||||
// shared across cgo calls without escape-analysis surprises.
|
||||
static lux_gpu_backend_desc* qvm_alloc_desc(void) {
|
||||
lux_gpu_backend_desc* d = (lux_gpu_backend_desc*)calloc(1, sizeof(*d));
|
||||
return d;
|
||||
}
|
||||
static void qvm_free_desc(lux_gpu_backend_desc* d) {
|
||||
free(d);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// pluginEntrySymbol is the canonical ABI v14 entry point exported by every
|
||||
// libluxgpu_backend_<x>.{so,dylib} plugin. Resolving this symbol yields a
|
||||
// function pointer that populates a lux_gpu_backend_desc with abi_version,
|
||||
// vtbl_size cookies, and the vtbl pointer the rest of this file walks.
|
||||
const pluginEntrySymbol = "lux_gpu_backend_init"
|
||||
|
||||
// pluginEnv overrides the dlopen search path. When set, every probe joins
|
||||
// the env value to the bare basename before calling dlopen — useful for
|
||||
// in-tree development against a freshly-built plugin under
|
||||
// ~/work/lux-private/gpu-kernels/build/ without installing it.
|
||||
const pluginEnv = "LUX_GPU_PLUGIN_DIR"
|
||||
|
||||
// probeOrder is the dlopen probe sequence fixed by the spec:
|
||||
// cuda → hip → metal → vulkan → webgpu. First plugin that satisfies the
|
||||
// cookie checks AND exposes the load-bearing op_mldsa_verify_batch slot
|
||||
// wins; the remaining probes are skipped. Order is preserved at struct
|
||||
// definition (not encoded as a map) so the iteration order is the same
|
||||
// on every platform.
|
||||
var probeOrder = []struct {
|
||||
kind Backend
|
||||
name string // basename without lib prefix or .so/.dylib suffix
|
||||
}{
|
||||
{BackendCUDA, "luxgpu_backend_cuda"},
|
||||
{BackendHIP, "luxgpu_backend_hip"},
|
||||
{BackendMetal, "luxgpu_backend_metal"},
|
||||
{BackendVulkan, "luxgpu_backend_vulkan"},
|
||||
{BackendWebGPU, "luxgpu_backend_webgpu"},
|
||||
}
|
||||
|
||||
// pluginBasenames returns the platform-specific DSO basenames to attempt
|
||||
// for a given backend. On darwin we try both .dylib (native) and .so
|
||||
// (cross-tooling fallback). On linux / other we try .so first. The
|
||||
// order within each entry's slice matches the lookup order used by
|
||||
// dlopen — first hit wins.
|
||||
func pluginBasenames(name string) []string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return []string{
|
||||
"lib" + name + ".dylib",
|
||||
"lib" + name + ".so",
|
||||
}
|
||||
case "windows":
|
||||
return []string{
|
||||
name + ".dll",
|
||||
}
|
||||
default:
|
||||
return []string{
|
||||
"lib" + name + ".so",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// gpuBackend — the cgo implementation of the GPUBackend interface.
|
||||
//
|
||||
// One struct per loaded plugin; pinned at package level by init()'s probe.
|
||||
// All vtbl invocations route through C trampolines (qvm_op_*) defined in
|
||||
// the cgo preamble above. mu serialises Close() against concurrent op
|
||||
// dispatch — every vtbl call increments / decrements an in-flight counter
|
||||
// so Close() can wait for outstanding calls before destroying the context.
|
||||
// =============================================================================
|
||||
|
||||
type gpuBackend struct {
|
||||
mu sync.Mutex
|
||||
kind Backend
|
||||
handle unsafe.Pointer // dlopen result (void*)
|
||||
desc *C.lux_gpu_backend_desc // heap-allocated descriptor
|
||||
ctx *C.LuxBackendContext // backend-managed opaque context
|
||||
path string
|
||||
}
|
||||
|
||||
// init runs the dlopen probe exactly once at process start. The probe
|
||||
// walks probeOrder, attempts each candidate basename, and stops at the
|
||||
// first plugin that:
|
||||
//
|
||||
// 1. Successfully dlopens.
|
||||
// 2. Exposes the lux_gpu_backend_init symbol.
|
||||
// 3. Reports abi_version == LUX_GPU_BACKEND_ABI_VERSION (14).
|
||||
// 4. Reports vtbl_size == sizeof(lux_gpu_backend_vtbl) on our header.
|
||||
// 5. Has op_mldsa_verify_batch wired (the load-bearing Q-Chain slot).
|
||||
// 6. Successfully opens a LuxBackendContext via vtbl->create_context.
|
||||
//
|
||||
// On success the package-level activeBackend atomic is set to the
|
||||
// matching Backend enum value. Failure at any stage is silent — we move
|
||||
// to the next candidate. If no candidate succeeds, activeBackend stays
|
||||
// BackendNone and ActiveGPUBackend() returns a noGPUBackend stub.
|
||||
func init() {
|
||||
if b, ok := tryProbe(); ok {
|
||||
setActiveBackend(b.kind)
|
||||
setActiveGPU(b)
|
||||
return
|
||||
}
|
||||
setActiveBackend(BackendNone)
|
||||
setActiveGPU(nil)
|
||||
}
|
||||
|
||||
// tryProbe runs the probeOrder dlopen loop. Returns the loaded gpuBackend
|
||||
// on success, or (nil, false) if no candidate satisfied all six checks.
|
||||
// Errors are deliberately not surfaced — this is a probe, not a hard
|
||||
// requirement. Callers see BackendNone and route to the CPU path.
|
||||
func tryProbe() (*gpuBackend, bool) {
|
||||
dir := os.Getenv(pluginEnv)
|
||||
for _, p := range probeOrder {
|
||||
for _, bn := range pluginBasenames(p.name) {
|
||||
path := bn
|
||||
if dir != "" {
|
||||
path = filepath.Join(dir, bn)
|
||||
}
|
||||
b, err := openPlugin(p.kind, path)
|
||||
if err == nil {
|
||||
return b, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// openPlugin attempts to dlopen `path`, resolve lux_gpu_backend_init,
|
||||
// run the cookie checks, and create a backend context. Returns the
|
||||
// loaded gpuBackend on success, or (nil, err) with the offending stage
|
||||
// documented in the error. The dlopen handle is closed on any failure
|
||||
// past the dlopen point — we never leak a half-bound plugin.
|
||||
func openPlugin(kind Backend, path string) (*gpuBackend, error) {
|
||||
cpath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cpath))
|
||||
|
||||
// Clear pending dlerror so a stale message from a prior failed call
|
||||
// doesn't get mis-attributed to this dlopen.
|
||||
C.qvm_dlerror()
|
||||
|
||||
handle := C.qvm_dlopen(cpath)
|
||||
if handle == nil {
|
||||
return nil, fmt.Errorf("quantumvm: dlopen(%s): %s",
|
||||
path, C.GoString(C.qvm_dlerror()))
|
||||
}
|
||||
|
||||
csym := C.CString(pluginEntrySymbol)
|
||||
defer C.free(unsafe.Pointer(csym))
|
||||
C.qvm_dlerror()
|
||||
sym := C.qvm_dlsym(handle, csym)
|
||||
if sym == nil {
|
||||
C.qvm_dlclose(handle)
|
||||
return nil, fmt.Errorf("quantumvm: dlsym(%s, %s): %s",
|
||||
path, pluginEntrySymbol, C.GoString(C.qvm_dlerror()))
|
||||
}
|
||||
|
||||
desc := C.qvm_alloc_desc()
|
||||
if desc == nil {
|
||||
C.qvm_dlclose(handle)
|
||||
return nil, fmt.Errorf("quantumvm: calloc(lux_gpu_backend_desc) failed")
|
||||
}
|
||||
if rc := C.qvm_backend_init(sym, desc); rc != 0 {
|
||||
C.qvm_free_desc(desc)
|
||||
C.qvm_dlclose(handle)
|
||||
return nil, fmt.Errorf(
|
||||
"quantumvm: %s plugin init failed (rc=%d, path=%s)",
|
||||
kind, int(rc), path)
|
||||
}
|
||||
|
||||
ctx := C.qvm_create_context(desc.vtbl)
|
||||
if ctx == nil {
|
||||
C.qvm_free_desc(desc)
|
||||
C.qvm_dlclose(handle)
|
||||
return nil, fmt.Errorf(
|
||||
"quantumvm: %s create_context returned NULL (no runtime on this host?)",
|
||||
kind)
|
||||
}
|
||||
|
||||
return &gpuBackend{
|
||||
kind: kind,
|
||||
handle: handle,
|
||||
desc: desc,
|
||||
ctx: ctx,
|
||||
path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Active-backend handle plumbing.
|
||||
//
|
||||
// activeGPU stores the *gpuBackend resolved by init()'s probe (or nil
|
||||
// when no plugin loaded). Read via ActiveGPUBackend() — which returns
|
||||
// the GPUBackend interface (gpuBackend implements it). The pointer is
|
||||
// pinned at init time and never mutated thereafter; we still serialise
|
||||
// access through a Mutex for safety against a future caller that might
|
||||
// add an explicit re-probe path.
|
||||
// =============================================================================
|
||||
|
||||
var (
|
||||
activeGPUMu sync.RWMutex
|
||||
activeGPU *gpuBackend
|
||||
)
|
||||
|
||||
func setActiveGPU(b *gpuBackend) {
|
||||
activeGPUMu.Lock()
|
||||
activeGPU = b
|
||||
activeGPUMu.Unlock()
|
||||
}
|
||||
|
||||
// ActiveGPUBackend returns the package-level GPUBackend handle. Under
|
||||
// cgo this is the *gpuBackend chosen by init()'s probe, or a sentinel
|
||||
// noGPUBackend stub when no plugin loaded. The returned handle's
|
||||
// methods are safe to call from any goroutine — every vtbl call
|
||||
// holds the backend mutex.
|
||||
func ActiveGPUBackend() GPUBackend {
|
||||
activeGPUMu.RLock()
|
||||
b := activeGPU
|
||||
activeGPUMu.RUnlock()
|
||||
if b == nil {
|
||||
return noGPUBackend{}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// noGPUBackend is the cgo build's stub for the "no plugin loaded" case.
|
||||
// Distinct from the !cgo noGPUBackend so the !cgo file can ship without
|
||||
// any cgo machinery; same method semantics.
|
||||
type noGPUBackend struct{}
|
||||
|
||||
func (noGPUBackend) Backend() Backend { return BackendNone }
|
||||
|
||||
func (noGPUBackend) Close() error { return nil }
|
||||
|
||||
func (noGPUBackend) MLDSAVerifyBatch(
|
||||
mode MLDSAMode,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error {
|
||||
_ = mode
|
||||
_ = messages
|
||||
_ = msgLens
|
||||
_ = msgWidthHint
|
||||
_ = signatures
|
||||
_ = results
|
||||
if len(pubkeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
func (noGPUBackend) MLDSASignBatch(
|
||||
mode MLDSAMode,
|
||||
skeys []byte,
|
||||
msgs []byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
count int,
|
||||
sigsOut []byte,
|
||||
sigLensOut []uint32,
|
||||
) error {
|
||||
_ = mode
|
||||
_ = skeys
|
||||
_ = msgs
|
||||
_ = msgLens
|
||||
_ = msgWidthHint
|
||||
_ = sigsOut
|
||||
_ = sigLensOut
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
func (noGPUBackend) SLHDSAVerifyBatch(
|
||||
variant SLHDSAVariant,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error {
|
||||
_ = variant
|
||||
_ = messages
|
||||
_ = msgLens
|
||||
_ = signatures
|
||||
_ = results
|
||||
if len(pubkeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// gpuBackend method implementations.
|
||||
// =============================================================================
|
||||
|
||||
func (b *gpuBackend) Backend() Backend {
|
||||
if b == nil {
|
||||
return BackendNone
|
||||
}
|
||||
return b.kind
|
||||
}
|
||||
|
||||
// Close destroys the LuxBackendContext, dlcloses the plugin, and frees
|
||||
// the descriptor. Safe on a nil receiver and idempotent — a second call
|
||||
// is a no-op once the handle has been cleared.
|
||||
func (b *gpuBackend) Close() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.handle == nil {
|
||||
return nil
|
||||
}
|
||||
if b.ctx != nil {
|
||||
C.qvm_destroy_context(b.desc.vtbl, b.ctx)
|
||||
b.ctx = nil
|
||||
}
|
||||
if b.desc != nil {
|
||||
C.qvm_free_desc(b.desc)
|
||||
b.desc = nil
|
||||
}
|
||||
C.qvm_dlclose(b.handle)
|
||||
b.handle = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// MLDSAVerifyBatch dispatches op_mldsa_verify_batch from the loaded
|
||||
// vtbl. See the GPUBackend interface doc + backend_plugin.h v14 block
|
||||
// for the contract.
|
||||
func (b *gpuBackend) MLDSAVerifyBatch(
|
||||
mode MLDSAMode,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error {
|
||||
if mode != MLDSAMode65 {
|
||||
// v14 vtbl only exposes ML-DSA-65 at op_mldsa_verify_batch.
|
||||
// Modes 44 / 87 will land at sibling slots in a future ABI
|
||||
// bump (see backend_plugin.h v14 block).
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
n := len(pubkeys)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(messages) != n || len(signatures) != n || len(results) != n {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSAVerifyBatch: length mismatch (pubkeys=%d messages=%d signatures=%d results=%d)",
|
||||
n, len(messages), len(signatures), len(results))
|
||||
}
|
||||
if msgLens != nil && len(msgLens) != n {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSAVerifyBatch: msgLens length %d != batch %d",
|
||||
len(msgLens), n)
|
||||
}
|
||||
if b == nil || b.ctx == nil || b.desc == nil {
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Pin every Go-side byte slice for the duration of the C call.
|
||||
// runtime.Pinner guarantees the GC won't move (or collect) the
|
||||
// backing arrays while the kernel reads them.
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
var lensBuf []C.size_t
|
||||
for i := 0; i < n; i++ {
|
||||
if len(pubkeys[i]) != MLDSA65PublicKeySize {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSAVerifyBatch[%d]: pubkey len %d != %d",
|
||||
i, len(pubkeys[i]), MLDSA65PublicKeySize)
|
||||
}
|
||||
if len(signatures[i]) > MLDSA65SignatureSize {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSAVerifyBatch[%d]: signature len %d > max %d",
|
||||
i, len(signatures[i]), MLDSA65SignatureSize)
|
||||
}
|
||||
pinner.Pin(&pubkeys[i][0])
|
||||
pkPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&pubkeys[i][0]))
|
||||
// Allow zero-length message (legal — empty-message verify is
|
||||
// a defined SHAKE256 absorb-zero operation per FIPS 204).
|
||||
if len(messages[i]) > 0 {
|
||||
pinner.Pin(&messages[i][0])
|
||||
msgPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&messages[i][0]))
|
||||
}
|
||||
pinner.Pin(&signatures[i][0])
|
||||
sigPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&signatures[i][0]))
|
||||
}
|
||||
if msgLens != nil {
|
||||
lensBuf = make([]C.size_t, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if msgLens[i] < 0 {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSAVerifyBatch[%d]: negative msgLen %d",
|
||||
i, msgLens[i])
|
||||
}
|
||||
lensBuf[i] = C.size_t(msgLens[i])
|
||||
}
|
||||
}
|
||||
|
||||
// bool layout in cgo: C.bool is one byte. Stage results in a
|
||||
// scratch buffer; copy back to the caller's []bool after the call.
|
||||
cresults := make([]C.bool, n)
|
||||
pkPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&pkPtrs[0]))
|
||||
msgPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&msgPtrs[0]))
|
||||
sigPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&sigPtrs[0]))
|
||||
var lensPtr *C.size_t
|
||||
if lensBuf != nil {
|
||||
lensPtr = (*C.size_t)(unsafe.Pointer(&lensBuf[0]))
|
||||
}
|
||||
|
||||
rc := C.qvm_op_mldsa_verify(
|
||||
b.desc.vtbl,
|
||||
b.ctx,
|
||||
pkPtrsPtr,
|
||||
msgPtrsPtr,
|
||||
lensPtr,
|
||||
C.uint32_t(msgWidthHint),
|
||||
sigPtrsPtr,
|
||||
(*C.bool)(unsafe.Pointer(&cresults[0])),
|
||||
C.size_t(n),
|
||||
)
|
||||
runtime.KeepAlive(pkPtrs)
|
||||
runtime.KeepAlive(msgPtrs)
|
||||
runtime.KeepAlive(sigPtrs)
|
||||
runtime.KeepAlive(lensBuf)
|
||||
runtime.KeepAlive(cresults)
|
||||
|
||||
if rc != 0 {
|
||||
return mapBackendError(int(rc), "op_mldsa_verify_batch")
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = bool(cresults[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MLDSASignBatch dispatches op_mldsa_sign_batch from the loaded vtbl.
|
||||
// See the GPUBackend interface doc + backend_plugin.h v14 block.
|
||||
func (b *gpuBackend) MLDSASignBatch(
|
||||
mode MLDSAMode,
|
||||
skeys []byte,
|
||||
msgs []byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
count int,
|
||||
sigsOut []byte,
|
||||
sigLensOut []uint32,
|
||||
) error {
|
||||
if mode != MLDSAMode65 {
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
const skStride = MLDSA65SecretKeySize
|
||||
const sigStride = MLDSA65SignatureSize
|
||||
if len(skeys) != count*skStride {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSASignBatch: skeys len %d != count*%d (%d)",
|
||||
len(skeys), skStride, count*skStride)
|
||||
}
|
||||
if len(sigsOut) != count*sigStride {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSASignBatch: sigsOut len %d != count*%d (%d)",
|
||||
len(sigsOut), sigStride, count*sigStride)
|
||||
}
|
||||
if msgLens != nil && len(msgLens) != count {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSASignBatch: msgLens length %d != count %d",
|
||||
len(msgLens), count)
|
||||
}
|
||||
if sigLensOut != nil && len(sigLensOut) != count {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSASignBatch: sigLensOut length %d != count %d",
|
||||
len(sigLensOut), count)
|
||||
}
|
||||
if b == nil || b.ctx == nil || b.desc == nil {
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
// Validate the msgs pool is large enough to cover the per-element
|
||||
// lengths. NULL msgLens uses msgWidthHint uniformly — same check
|
||||
// against count * msgWidthHint.
|
||||
var total int
|
||||
if msgLens != nil {
|
||||
for i := 0; i < count; i++ {
|
||||
if msgLens[i] < 0 {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSASignBatch[%d]: negative msgLen %d",
|
||||
i, msgLens[i])
|
||||
}
|
||||
total += msgLens[i]
|
||||
}
|
||||
} else {
|
||||
total = int(msgWidthHint) * count
|
||||
}
|
||||
if len(msgs) < total {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: MLDSASignBatch: msgs pool len %d < total %d",
|
||||
len(msgs), total)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
pinner.Pin(&skeys[0])
|
||||
if total > 0 {
|
||||
pinner.Pin(&msgs[0])
|
||||
}
|
||||
pinner.Pin(&sigsOut[0])
|
||||
var lensBuf []C.size_t
|
||||
var lensPtr *C.size_t
|
||||
if msgLens != nil {
|
||||
lensBuf = make([]C.size_t, count)
|
||||
for i := 0; i < count; i++ {
|
||||
lensBuf[i] = C.size_t(msgLens[i])
|
||||
}
|
||||
lensPtr = (*C.size_t)(unsafe.Pointer(&lensBuf[0]))
|
||||
}
|
||||
var sigLensPtr *C.uint32_t
|
||||
if sigLensOut != nil {
|
||||
sigLensPtr = (*C.uint32_t)(unsafe.Pointer(&sigLensOut[0]))
|
||||
}
|
||||
var msgsPtr *C.uint8_t
|
||||
if total > 0 {
|
||||
msgsPtr = (*C.uint8_t)(unsafe.Pointer(&msgs[0]))
|
||||
}
|
||||
|
||||
rc := C.qvm_op_mldsa_sign(
|
||||
b.desc.vtbl,
|
||||
b.ctx,
|
||||
(*C.uint8_t)(unsafe.Pointer(&skeys[0])),
|
||||
C.size_t(skStride),
|
||||
msgsPtr,
|
||||
lensPtr,
|
||||
C.uint32_t(msgWidthHint),
|
||||
C.size_t(count),
|
||||
(*C.uint8_t)(unsafe.Pointer(&sigsOut[0])),
|
||||
sigLensPtr,
|
||||
)
|
||||
runtime.KeepAlive(lensBuf)
|
||||
|
||||
if rc != 0 {
|
||||
return mapBackendError(int(rc), "op_mldsa_sign_batch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SLHDSAVerifyBatch dispatches the SHAKE-128f or SHAKE-192f variant of
|
||||
// op_slhdsa_verify_batch from the loaded vtbl. The 10 other FIPS 205
|
||||
// variants are not exposed at v14 — they return ErrGPUNotAvailable.
|
||||
func (b *gpuBackend) SLHDSAVerifyBatch(
|
||||
variant SLHDSAVariant,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error {
|
||||
is128f := variant == SLHDSAShake128f
|
||||
is192f := variant == SLHDSAShake192f
|
||||
if !is128f && !is192f {
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
n := len(pubkeys)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(messages) != n || len(signatures) != n || len(results) != n {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: SLHDSAVerifyBatch: length mismatch (pubkeys=%d messages=%d signatures=%d results=%d)",
|
||||
n, len(messages), len(signatures), len(results))
|
||||
}
|
||||
if msgLens != nil && len(msgLens) != n {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: SLHDSAVerifyBatch: msgLens length %d != batch %d",
|
||||
len(msgLens), n)
|
||||
}
|
||||
pkSize := variant.PublicKeySize()
|
||||
sigSize := variant.SignatureSize()
|
||||
if b == nil || b.ctx == nil || b.desc == nil {
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
pkPtrs := make([]*C.uint8_t, n)
|
||||
msgPtrs := make([]*C.uint8_t, n)
|
||||
sigPtrs := make([]*C.uint8_t, n)
|
||||
lensBuf := make([]C.size_t, n)
|
||||
for i := 0; i < n; i++ {
|
||||
if len(pubkeys[i]) != pkSize {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: SLHDSAVerifyBatch[%d]: pubkey len %d != %d",
|
||||
i, len(pubkeys[i]), pkSize)
|
||||
}
|
||||
if len(signatures[i]) != sigSize {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: SLHDSAVerifyBatch[%d]: signature len %d != %d",
|
||||
i, len(signatures[i]), sigSize)
|
||||
}
|
||||
pinner.Pin(&pubkeys[i][0])
|
||||
pkPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&pubkeys[i][0]))
|
||||
if len(messages[i]) > 0 {
|
||||
pinner.Pin(&messages[i][0])
|
||||
msgPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&messages[i][0]))
|
||||
}
|
||||
pinner.Pin(&signatures[i][0])
|
||||
sigPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&signatures[i][0]))
|
||||
// SLH-DSA has no msg_width_hint surface — msg_lens is
|
||||
// mandatory at the vtbl boundary. Fill lensBuf from either
|
||||
// msgLens or the message slice length.
|
||||
if msgLens != nil {
|
||||
if msgLens[i] < 0 {
|
||||
return fmt.Errorf(
|
||||
"quantumvm: SLHDSAVerifyBatch[%d]: negative msgLen %d",
|
||||
i, msgLens[i])
|
||||
}
|
||||
lensBuf[i] = C.size_t(msgLens[i])
|
||||
} else {
|
||||
lensBuf[i] = C.size_t(len(messages[i]))
|
||||
}
|
||||
}
|
||||
cresults := make([]C.bool, n)
|
||||
pkPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&pkPtrs[0]))
|
||||
msgPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&msgPtrs[0]))
|
||||
sigPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&sigPtrs[0]))
|
||||
lensPtr := (*C.size_t)(unsafe.Pointer(&lensBuf[0]))
|
||||
|
||||
var rc C.int
|
||||
if is128f {
|
||||
rc = C.qvm_op_slhdsa_verify_128f(
|
||||
b.desc.vtbl,
|
||||
b.ctx,
|
||||
pkPtrsPtr,
|
||||
msgPtrsPtr,
|
||||
lensPtr,
|
||||
sigPtrsPtr,
|
||||
(*C.bool)(unsafe.Pointer(&cresults[0])),
|
||||
C.size_t(n),
|
||||
)
|
||||
} else {
|
||||
rc = C.qvm_op_slhdsa_verify_192f(
|
||||
b.desc.vtbl,
|
||||
b.ctx,
|
||||
pkPtrsPtr,
|
||||
msgPtrsPtr,
|
||||
lensPtr,
|
||||
sigPtrsPtr,
|
||||
(*C.bool)(unsafe.Pointer(&cresults[0])),
|
||||
C.size_t(n),
|
||||
)
|
||||
}
|
||||
runtime.KeepAlive(pkPtrs)
|
||||
runtime.KeepAlive(msgPtrs)
|
||||
runtime.KeepAlive(sigPtrs)
|
||||
runtime.KeepAlive(lensBuf)
|
||||
runtime.KeepAlive(cresults)
|
||||
|
||||
if rc != 0 {
|
||||
return mapBackendError(int(rc), "op_slhdsa_verify_batch")
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
results[i] = bool(cresults[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapBackendError translates a LuxBackendError code to a Go error.
|
||||
// NOT_SUPPORTED (3) maps to the package sentinel so callers can route
|
||||
// to the CPU oracle via errors.Is. Other non-zero codes return a
|
||||
// formatted error including the numeric code for diagnostics.
|
||||
func mapBackendError(rc int, op string) error {
|
||||
switch rc {
|
||||
case 0:
|
||||
return nil
|
||||
case int(C.LUX_BACKEND_ERROR_NOT_SUPPORTED):
|
||||
return ErrGPUNotAvailable
|
||||
case int(C.LUX_BACKEND_ERROR_INVALID_ARGUMENT):
|
||||
return fmt.Errorf("quantumvm: %s: invalid argument (rc=1)", op)
|
||||
case int(C.LUX_BACKEND_ERROR_OUT_OF_MEMORY):
|
||||
return fmt.Errorf("quantumvm: %s: out of memory (rc=2)", op)
|
||||
case int(C.LUX_BACKEND_ERROR_DEVICE_LOST):
|
||||
return fmt.Errorf("quantumvm: %s: device lost (rc=4)", op)
|
||||
case int(C.LUX_BACKEND_ERROR_INTERNAL):
|
||||
return fmt.Errorf("quantumvm: %s: internal error (rc=5)", op)
|
||||
default:
|
||||
return fmt.Errorf("quantumvm: %s: rc=%d", op, rc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//go:build !cgo
|
||||
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quantumvm
|
||||
|
||||
// !cgo build: no plugin DSO is loadable without cgo (we cannot call
|
||||
// dlopen / dlsym from pure Go), so every GPUBackend method returns
|
||||
// ErrGPUNotAvailable and AutoBackend() reports BackendNone. The cgo
|
||||
// build's quantumvm_gpu.go does the runtime probe; this file keeps the
|
||||
// package surface identical regardless of CGO_ENABLED.
|
||||
|
||||
func init() {
|
||||
setActiveBackend(BackendNone)
|
||||
}
|
||||
|
||||
// noGPUBackend is the !cgo implementation of the GPUBackend interface —
|
||||
// every method returns ErrGPUNotAvailable so callers route to the CPU
|
||||
// verify path cleanly.
|
||||
type noGPUBackend struct{}
|
||||
|
||||
// ActiveGPUBackend returns the package-level GPUBackend handle. Under
|
||||
// !cgo this is always a stub returning ErrGPUNotAvailable on every
|
||||
// method call.
|
||||
func ActiveGPUBackend() GPUBackend {
|
||||
return noGPUBackend{}
|
||||
}
|
||||
|
||||
func (noGPUBackend) Backend() Backend { return BackendNone }
|
||||
|
||||
func (noGPUBackend) Close() error { return nil }
|
||||
|
||||
func (noGPUBackend) MLDSAVerifyBatch(
|
||||
mode MLDSAMode,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error {
|
||||
_ = mode
|
||||
_ = messages
|
||||
_ = msgLens
|
||||
_ = msgWidthHint
|
||||
_ = signatures
|
||||
_ = results
|
||||
// Empty batch is a legal no-op — match the cgo implementation's
|
||||
// short-circuit so callers don't have to special-case "no plugin
|
||||
// loaded AND empty batch" at the call site.
|
||||
if len(pubkeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
func (noGPUBackend) MLDSASignBatch(
|
||||
mode MLDSAMode,
|
||||
skeys []byte,
|
||||
msgs []byte,
|
||||
msgLens []int,
|
||||
msgWidthHint uint32,
|
||||
count int,
|
||||
sigsOut []byte,
|
||||
sigLensOut []uint32,
|
||||
) error {
|
||||
_ = mode
|
||||
_ = skeys
|
||||
_ = msgs
|
||||
_ = msgLens
|
||||
_ = msgWidthHint
|
||||
_ = sigsOut
|
||||
_ = sigLensOut
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
|
||||
func (noGPUBackend) SLHDSAVerifyBatch(
|
||||
variant SLHDSAVariant,
|
||||
pubkeys [][]byte,
|
||||
messages [][]byte,
|
||||
msgLens []int,
|
||||
signatures [][]byte,
|
||||
results []bool,
|
||||
) error {
|
||||
_ = variant
|
||||
_ = messages
|
||||
_ = msgLens
|
||||
_ = signatures
|
||||
_ = results
|
||||
if len(pubkeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ErrGPUNotAvailable
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//go:build cgo
|
||||
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quantumvm
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
// TestAutoBackend_Probes documents what the init() probe found at process
|
||||
// start. Skipped when no plugin loaded — this is the on-CI / clean-build
|
||||
// signal that the binary stays portable. When a plugin IS loaded, we log
|
||||
// its name so flakes can be tied back to a specific backend.
|
||||
func TestAutoBackend_Probes(t *testing.T) {
|
||||
b := AutoBackend()
|
||||
if b == BackendNone {
|
||||
t.Skip("no libluxgpu_backend_*.{so,dylib} loadable on the dlopen path — Q-Chain CPU path remains the only option")
|
||||
}
|
||||
t.Logf("AutoBackend() = %s", b)
|
||||
}
|
||||
|
||||
// TestActiveGPUBackend_NoBackendReturnsErr exercises the noGPUBackend
|
||||
// stub returned when no plugin was loaded. Every method must return
|
||||
// ErrGPUNotAvailable so callers can route to the CPU verify path via
|
||||
// errors.Is.
|
||||
func TestActiveGPUBackend_NoBackendReturnsErr(t *testing.T) {
|
||||
if AutoBackend() != BackendNone {
|
||||
t.Skip("plugin loaded — noGPUBackend stub not exercised on this host")
|
||||
}
|
||||
g := ActiveGPUBackend()
|
||||
if g.Backend() != BackendNone {
|
||||
t.Fatalf("Backend() = %s, want none", g.Backend())
|
||||
}
|
||||
err := g.MLDSAVerifyBatch(MLDSAMode65, nil, nil, nil, 0, nil, nil)
|
||||
if err != nil {
|
||||
// Empty batch is a legal no-op even when no plugin loaded;
|
||||
// length 0 short-circuits before the noGPUBackend stub runs.
|
||||
t.Fatalf("MLDSAVerifyBatch(empty) = %v, want nil", err)
|
||||
}
|
||||
err = g.MLDSAVerifyBatch(MLDSAMode65,
|
||||
[][]byte{make([]byte, MLDSA65PublicKeySize)},
|
||||
[][]byte{nil},
|
||||
nil, 0,
|
||||
[][]byte{make([]byte, MLDSA65SignatureSize)},
|
||||
make([]bool, 1))
|
||||
if !errors.Is(err, ErrGPUNotAvailable) {
|
||||
t.Fatalf("MLDSAVerifyBatch on noGPUBackend = %v, want ErrGPUNotAvailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMLDSAVerifyBatch_RoundTrip — open a backend context, sign one
|
||||
// message via the in-process circl ML-DSA-65 implementation, then run
|
||||
// op_mldsa_verify_batch on the (pk, msg, sig) triple and assert
|
||||
// results[0] == true.
|
||||
//
|
||||
// The plugin's wrapper consumes FIPS 204 wire-format (pk=1952 B,
|
||||
// sig=3309 B max) which is exactly what circl's MarshalBinary() /
|
||||
// SignTo() emit, so the round-trip is byte-compatible — no PQClean
|
||||
// detour needed in the test.
|
||||
//
|
||||
// Skip conditions:
|
||||
// * No plugin loaded -> AutoBackend() == BackendNone
|
||||
// * Plugin loaded but op_mldsa_verify_batch is a -> rc=NOT_SUPPORTED
|
||||
// CPU oracle returning NOT_SUPPORTED (e.g. some
|
||||
// WebGPU phase-2 builds) — surfaces as ErrGPUNotAvailable.
|
||||
//
|
||||
// Either skip is a CLEAN exit, not a FAIL. The property under test is
|
||||
// "ABI v14 wire-format round-trips through the loaded plugin", which
|
||||
// is only meaningful when the plugin actually runs the verify path.
|
||||
func TestMLDSAVerifyBatch_RoundTrip(t *testing.T) {
|
||||
if AutoBackend() == BackendNone {
|
||||
t.Skip("no GPU plugin loaded — round-trip test requires the dlopen probe to have succeeded at init()")
|
||||
}
|
||||
g := ActiveGPUBackend()
|
||||
t.Logf("backend = %s", g.Backend())
|
||||
|
||||
// Generate one ML-DSA-65 keypair + signature via circl. The test
|
||||
// is single-shot so we don't bother batching beyond N=1; the v14
|
||||
// vtbl wrapper has the same shape for every batch size.
|
||||
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
if err != nil {
|
||||
t.Fatalf("mldsa.GenerateKey: %v", err)
|
||||
}
|
||||
pkBytes := priv.PublicKey.Bytes()
|
||||
if len(pkBytes) != MLDSA65PublicKeySize {
|
||||
t.Fatalf("pubkey size = %d, want %d", len(pkBytes), MLDSA65PublicKeySize)
|
||||
}
|
||||
|
||||
// 64-byte message — matches what the Q-Chain quantum stamp path
|
||||
// hands the verifier (sha512 digest of the round digest + nonce +
|
||||
// timestamp). Using a deterministic byte pattern keeps the test
|
||||
// reproducible across reruns.
|
||||
msg := make([]byte, 64)
|
||||
for i := range msg {
|
||||
msg[i] = byte(i)
|
||||
}
|
||||
// Sign with ctx=nil to match the orchestrator's ctx_len=0 wrap.
|
||||
sig, err := priv.Sign(rand.Reader, msg, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("priv.Sign: %v", err)
|
||||
}
|
||||
if got, max := len(sig), MLDSA65SignatureSize; got > max {
|
||||
t.Fatalf("sig size = %d > max %d", got, max)
|
||||
}
|
||||
// Pad the signature out to the fixed 3309-byte width the orchestrator
|
||||
// reads. Padding bytes after the real sig length are ignored by the
|
||||
// FIPS 204 verify path (sig parsing terminates at the c̃ + z + h
|
||||
// structural decode).
|
||||
sigPadded := make([]byte, MLDSA65SignatureSize)
|
||||
copy(sigPadded, sig)
|
||||
|
||||
results := make([]bool, 1)
|
||||
err = g.MLDSAVerifyBatch(
|
||||
MLDSAMode65,
|
||||
[][]byte{pkBytes},
|
||||
[][]byte{msg},
|
||||
[]int{len(msg)},
|
||||
/*msgWidthHint=*/ 0,
|
||||
[][]byte{sigPadded},
|
||||
results,
|
||||
)
|
||||
if errors.Is(err, ErrGPUNotAvailable) {
|
||||
t.Skipf("plugin op_mldsa_verify_batch returned NOT_SUPPORTED (backend=%s) — surface contract honoured, round-trip not testable", g.Backend())
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("MLDSAVerifyBatch: %v", err)
|
||||
}
|
||||
if !results[0] {
|
||||
t.Fatalf("honest verify returned false (backend=%s) — wire-format incompatibility", g.Backend())
|
||||
}
|
||||
t.Logf("backend=%s op_mldsa_verify_batch honest verify -> true", g.Backend())
|
||||
}
|
||||
|
||||
// TestMLDSAVerifyBatch_LengthMismatch covers the length-validation
|
||||
// branch at the boundary. The wrapper must reject batches where the
|
||||
// per-element slices disagree on cardinality before any C call.
|
||||
func TestMLDSAVerifyBatch_LengthMismatch(t *testing.T) {
|
||||
if AutoBackend() == BackendNone {
|
||||
t.Skip("no GPU plugin loaded")
|
||||
}
|
||||
g := ActiveGPUBackend()
|
||||
err := g.MLDSAVerifyBatch(
|
||||
MLDSAMode65,
|
||||
make([][]byte, 2),
|
||||
make([][]byte, 1),
|
||||
nil, 0,
|
||||
make([][]byte, 2),
|
||||
make([]bool, 2),
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected length-mismatch error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMLDSAVerifyBatch_NonMLDSA65Skips exercises the mode gate. Modes
|
||||
// 44 / 87 aren't wired at v14 — passing them must surface as
|
||||
// ErrGPUNotAvailable so callers fall through cleanly.
|
||||
func TestMLDSAVerifyBatch_NonMLDSA65Skips(t *testing.T) {
|
||||
g := ActiveGPUBackend()
|
||||
for _, mode := range []MLDSAMode{MLDSAMode44, MLDSAMode87} {
|
||||
err := g.MLDSAVerifyBatch(mode,
|
||||
[][]byte{make([]byte, 1)},
|
||||
[][]byte{nil}, nil, 0,
|
||||
[][]byte{make([]byte, 1)},
|
||||
make([]bool, 1))
|
||||
if !errors.Is(err, ErrGPUNotAvailable) {
|
||||
t.Fatalf("mode %d: got %v, want ErrGPUNotAvailable", mode, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user