mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
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.
155 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|