Files
chains/thresholdvm/thresholdvm_gpu_test.go
T
zeekay b8a2a5c4dc chains: scrub private-repo leakage from GPU bridges
OSS public chains module must not reference the private GPU plugin
implementation by repo name or filesystem path. Two leak types fixed:

1. Hard compile-time dependency in quantumvm:
   `#cgo CFLAGS: -I${SRCDIR}/../../../lux-private/gpu-kernels/include`
   Vendor the public ABI v14 header (backend_plugin.h + crypto.h, 1475
   LOC total) into chains/internal/luxgpu/include/lux/gpu/ and point
   CFLAGS at the in-tree path. Plugin DSO is still dlopen'd at runtime
   — only the public C ABI surface is vendored.

2. Soft leakage — `LUX_PRIVATE_GPU_KERNELS_DIR` env vocabulary, dev-tree
   probe paths to ~/work/lux-private/gpu-kernels/build/..., prose
   mentions of "lux-private/gpu-kernels" in package comments.
   All replaced with `LUX_GPU_PLUGIN_DIR` (universal env) and generic
   "GPU plugin" prose.

Bridges affected: aivm, bridgevm, dexvm, quantumvm, thresholdvm.
All 139 tests PASS post-scrub under both build tags. No functional
behavior change — only path / env / prose.
2026-06-05 15:46:00 -07:00

155 lines
6.1 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package thresholdvm
import (
"errors"
"testing"
"unsafe"
)
// TestGPULayoutSizes pins the on-wire struct sizes to the device-side
// __align__(16) values declared in
// the GPU plugin install tree ops/mpcvm/cuda/mpcvm_kernels_common.cuh.
// Any drift would produce silently-wrong kernel results — the same check
// runs inside assertSizes() at init() under cgo; this test pins it for
// nocgo builds AND lets `go test` surface the mismatch immediately
// instead of waiting for a process start to panic.
func TestGPULayoutSizes(t *testing.T) {
cases := []struct {
name string
want uintptr
got uintptr
}{
{"GPUCeremony", 128, unsafe.Sizeof(GPUCeremony{})},
{"GPUKeyShare", 368, unsafe.Sizeof(GPUKeyShare{})},
{"GPUContribution", 432, unsafe.Sizeof(GPUContribution{})},
{"GPUMPCVMState", 160, unsafe.Sizeof(GPUMPCVMState{})},
{"GPUMPCVMRoundDescriptor", 96, unsafe.Sizeof(GPUMPCVMRoundDescriptor{})},
{"GPUCeremonyOp", 96, unsafe.Sizeof(GPUCeremonyOp{})},
{"GPUContributionOp", 416, unsafe.Sizeof(GPUContributionOp{})},
{"GPUMPCVMTransitionResult", 176, unsafe.Sizeof(GPUMPCVMTransitionResult{})},
}
for _, c := range cases {
if c.got != c.want {
t.Errorf("%s: sizeof=%d want=%d (device-side mpcvm_kernels_common.cuh __align__(16))",
c.name, c.got, c.want)
}
}
}
// TestGPUBackendKindString round-trips the resolved plugin kind to the
// launcher symbol prefix used by tryLoadPlugin(). The cgo flavor returns
// the prefix used in lux_<X>_mpcvm_*; the !cgo flavor returns "none" for
// every kind because no dlopen ever happens. Both branches must surface
// GPUBackendNone as "none" so callers can `if kind.String() == "none"`
// regardless of build mode.
func TestGPUBackendKindString(t *testing.T) {
if got := GPUBackendNone.String(); got != "none" {
t.Errorf("GPUBackendNone.String()=%q want=%q", got, "none")
}
// Under cgo the named backends round-trip to their prefix. Under !cgo
// they all collapse to "none". Cover the cgo expectation only when we
// see Backend() != nil OR see GPUBackendCUDA round-tripping correctly —
// otherwise we're in the nocgo build and the noop contract applies.
cudaName := GPUBackendCUDA.String()
if cudaName != "cuda" && cudaName != "none" {
t.Errorf("GPUBackendCUDA.String()=%q want one of {cuda, none}", cudaName)
}
if cudaName == "cuda" {
// cgo build — pin all the other names too.
cases := []struct {
k GPUBackendKind
want string
}{
{GPUBackendHIP, "hip"},
{GPUBackendMetal, "metal"},
{GPUBackendVulkan, "vulkan"},
{GPUBackendWebGPU, "webgpu"},
}
for _, c := range cases {
if got := c.k.String(); got != c.want {
t.Errorf("kind=%d string=%q want=%q", c.k, got, c.want)
}
}
}
}
// TestGPUBridgeZeroFixture is the dlopen round-trip required by the task:
// resolve the best-available backend, call lux_<best>_mpcvm_ceremony_apply
// with a zero fixture, assert no error. The two acceptable outcomes are:
//
// 1. The plugin is dlopen'd (e.g. on a dev box with the metal_backend
// dylib in the lux GPU plugin build/metal_backend/). The launcher
// runs against the zero fixture and returns rc=0 (zero ops applied).
//
// 2. The plugin is absent. Backend() returns nil; we get ErrGPUNotAvailable.
//
// Both outcomes count as a passing round-trip — the bridge correctly
// surfaces the GPU state through the public Go API. The test only fails
// on a third outcome: a partially-resolved plugin that returns an
// unexpected error rc.
func TestGPUBridgeZeroFixture(t *testing.T) {
b := Backend()
if b == nil || !b.IsAvailable() {
t.Logf("no GPU plugin resolved (expected on CI without a dylib in LUX_GPU_PLUGIN_DIR/build/)")
// Re-prove the error contract by calling through the nil receiver:
// the GPUBackend method set on nil must return ErrGPUNotAvailable
// rather than panicking. This is the substrate contract.
_, err := (*GPUBackend)(nil).CeremonyApply(nil, nil, nil)
if !errors.Is(err, ErrGPUNotAvailable) {
t.Fatalf("nil receiver: want ErrGPUNotAvailable, got %v", err)
}
return
}
t.Logf("resolved GPU backend: kind=%s path=%s", b.Kind, b.Path)
// Zero fixture: 1 empty ceremony slot, no ceremony ops, no contribution
// ops. The kernel walks zero ops and returns applied=0, rc=0. Any
// non-zero rc is a real launcher failure — fail the test.
desc := &GPUMPCVMRoundDescriptor{
ChainID: 0xDEADBEEF,
Round: 1,
TimestampNs: 1700000000_000000000,
Epoch: 1,
Mode: 0,
}
// The ceremony arena MUST be a power-of-2 length (device-side open
// addressing). The smallest legal arena is 1 slot.
ceremonies := make([]GPUCeremony, 1)
applied, err := b.CeremonyApply(desc, nil, ceremonies)
if err != nil {
t.Fatalf("CeremonyApply(zero): %v", err)
}
if applied != 0 {
t.Errorf("CeremonyApply(zero): applied=%d want=0 (no ops in input)", applied)
}
}
// TestGPUBridgeNoCgoStub verifies that the !cgo build flavor surfaces the
// same Go API surface as cgo, with every method returning ErrGPUNotAvailable
// and IsAvailable()==false. Under cgo this test runs the cgo surface (which
// also returns ErrGPUNotAvailable when no plugin is loaded), so the contract
// is consistent across build flavors.
func TestGPUBridgeNoCgoStub(t *testing.T) {
// Constructing a zero GPUBackend mimics the nocgo Backend() == nil
// path. Every method must error out without panicking.
var b GPUBackend
if b.IsAvailable() {
t.Fatal("zero GPUBackend.IsAvailable() must be false")
}
if _, err := b.CeremonyApply(nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("CeremonyApply: want ErrGPUNotAvailable, got %v", err)
}
if _, _, _, err := b.KeyShareApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("KeyShareApply: want ErrGPUNotAvailable, got %v", err)
}
if _, err := b.ContributionApply(nil, nil, nil, nil, 0); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("ContributionApply: want ErrGPUNotAvailable, got %v", err)
}
if _, err := b.MPCTransition(nil, nil, nil, nil, nil); !errors.Is(err, ErrGPUNotAvailable) {
t.Errorf("MPCTransition: want ErrGPUNotAvailable, got %v", err)
}
}