Files
chains/mpcvm/mpcvm_gpu.go
T
zeekay 3abc34922f gpu: identify a plugin before trusting it, and load only from a named place
A GPU plugin replaces a VM's state transitions wholesale. Three of the four
seams reached one by dlsym'ing launcher names out of a library nothing had
identified, and all four would fall back to a bare filename — which makes the
dynamic loader's default search path, and in bridgevm's case the working
directory of whoever started the node, the thing that decides which code
computes consensus state.

That is not theoretical. A library exporting the five bridgevm launcher names
and nothing else, compiled from twenty lines of C that return 0:

  before, LUX_GPU_PLUGIN_DIR=/tmp/rogue   AutoBackend = hip
  before, no env, cwd holds the .so       AutoBackend = hip
  after,  either                          AutoBackend = none

Two changes, neither of them new machinery.

The plugin header says a loader MUST check abi_version and vtbl_size, and
quantumvm already does, through lux_gpu_backend_init. aivm, bridgevm and mpcvm
now do the same before resolving a single launcher: a name is what was being
matched, and a name is not a contract.

Bare filenames and the CWD entry are gone from all four. What is left is
LUX_GPU_PLUGIN_DIR and LUXCPP_PREFIX — places an operator named. No new
environment variable: unset means no plugin, which is already the supported
configuration and the one every box without a GPU is in.

Verified with the real HIP plugin from lux-private/gpu-kernels: aivm, bridgevm,
mpcvm and quantumvm all build, vet and test green, aivm TestRoundTripAttestation
passes through the loaded plugin, and the rogue library is refused.
2026-08-29 19:04:26 -07:00

854 lines
29 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo
package mpcvm
// GPU bridge — dlopen + dlsym substrate for the MPCVM state-machine kernels.
//
// This file binds Go to the host launchers exported by the per-backend GPU
// plugins in the lux GPU plugin backends/<X>/. Unlike the cevm bridge
// which links statically through pkg-config (libevm + libevm-gpu), the
// mpcvm bridge is a runtime overlay: zero compile-time dependency on
// the plugin, no pkg-config, no link-time resolution. Either the dlopen
// succeeds at init() and GPUBackend is populated, or it doesn't and
// callers fall back to the CPU reference (the Go state machine in
// protocol/, factory.go, executor.go — unchanged).
//
// Symbols expected per backend (host launchers in
// the lux GPU plugin backends/<X>/src/mpcvm_launchers.{cpp,mm} and
// the kernel TUs in the lux GPU plugin ops/mpcvm/<X>/):
//
// lux_<X>_mpcvm_ceremony_apply (begin/cancel + contribution dedup)
// lux_<X>_mpcvm_ceremony_sweep (DKG finalize -> key share assignment)
// lux_<X>_mpcvm_compute_leaves (parallel keccak per slot)
// lux_<X>_mpcvm_compose_root (serial fold + epoch advance)
//
// where <X> ∈ {cuda, hip, metal, vulkan, webgpu}. The Go bridge exposes
// the four MPC state-machine ops requested by the substrate contract:
//
// GPUBackend.CeremonyApply → ceremony_apply (ceremony ops only)
// GPUBackend.KeyShareApply → ceremony_sweep (DKG finalize path)
// GPUBackend.ContributionApply → ceremony_apply (contribution ops only)
// GPUBackend.MPCTransition → compute_leaves + compose_root pair
//
// The same ceremony_apply launcher is used twice with disjoint ops because
// the MPC state machine separates "ceremony admin" (begin/cancel) from
// "round contribution" (per-party payload) — both flow through the same
// dedup-and-write GPU kernel by design (see ops/mpcvm/cuda/mpcvm_ceremony.cu).
//
// Corona = R-LWE threshold (the PQ variant per project memory). Pulsar =
// threshold ML-DSA. Magnetar = threshold SLH-DSA (not built yet). When the
// caller invokes CeremonyApply with a CeremonyOp whose kind is kKindCoronaDkg
// or kKindCoronaSign, the GPU dispatches the Corona path; the kernel
// dispatch table is in ops/mpcvm/<X>/mpcvm_corona.cu (or .metal, etc.).
/*
#cgo CFLAGS: -Wno-unused-parameter
#cgo darwin LDFLAGS: -ldl
#cgo linux LDFLAGS: -ldl
#cgo CFLAGS: -I${SRCDIR}/../internal/luxgpu/include
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// =============================================================================
// Function-pointer typedefs — match the extern "C" launcher signatures in
// the lux GPU plugin backends/<X>/src/mpcvm_launchers.{cpp,mm}.
//
// All pointers are HOST pointers. The launcher wraps them into the backend's
// native buffer type (MTLBuffer, VkBuffer, cudaMalloc, etc.) and uploads /
// downloads via memcpy on UMA backends or H2D/D2H on discrete backends.
//
// Return code: 0 = OK; non-zero = launcher-defined error (1 invalid arg,
// 2 no device, 3 pipeline build, 4 dispatch, 5 readback, 6 submit).
// =============================================================================
typedef int (*lux_mpcvm_ceremony_apply_fn)(
const void* desc,
const void* ceremony_ops,
const void* contribution_ops,
void* ceremonies,
void* contributions,
void* ceremony_applied_out,
void* contribution_applied_out,
uint32_t ceremony_count,
uint32_t contribution_count,
uint64_t next_contribution_id_in,
void* stream);
typedef int (*lux_mpcvm_ceremony_sweep_fn)(
const void* desc,
void* ceremonies,
void* key_shares,
void* contributions,
void* round_advance_out,
void* finalized_out,
void* failed_out,
uint32_t ceremony_count,
uint32_t key_share_count,
uint32_t contribution_count,
uint64_t next_share_id_in,
void* stream);
typedef int (*lux_mpcvm_compute_leaves_fn)(
const void* ceremonies,
const void* shares,
const void* contributions,
void* ceremony_leaf_hashes,
void* share_leaf_hashes,
void* contribution_leaf_hashes,
void* active_count_out,
void* finalized_count_out,
void* failed_count_out,
void* share_count_out,
void* ceremony_used_mask,
void* share_used_mask,
void* contribution_used_mask,
uint32_t ceremony_count,
uint32_t share_count,
uint32_t contribution_count,
void* stream);
typedef int (*lux_mpcvm_compose_root_fn)(
const void* desc,
const void* ceremony_leaf_hashes,
const void* share_leaf_hashes,
const void* contribution_leaf_hashes,
const void* ceremony_used_mask,
const void* share_used_mask,
const void* contribution_used_mask,
const void* active_count_in,
const void* finalized_count_in,
const void* failed_count_in,
const void* share_count_in,
void* state,
void* result,
uint32_t ceremony_count,
uint32_t share_count,
uint32_t contribution_count,
void* stream);
// Thin call shims — Go can't call a C function pointer directly.
static int call_ceremony_apply(
void* fn, const void* desc, const void* cops, const void* nops,
void* cer, void* con, void* capp, void* napp,
uint32_t cc, uint32_t nc, uint64_t next_cont, void* stream)
{
return ((lux_mpcvm_ceremony_apply_fn)fn)(
desc, cops, nops, cer, con, capp, napp, cc, nc, next_cont, stream);
}
static int call_ceremony_sweep(
void* fn, const void* desc,
void* cer, void* ks, void* con,
void* ra, void* fin, void* fai,
uint32_t cc, uint32_t kc, uint32_t nc, uint64_t next_share, void* stream)
{
return ((lux_mpcvm_ceremony_sweep_fn)fn)(
desc, cer, ks, con, ra, fin, fai, cc, kc, nc, next_share, stream);
}
static int call_compute_leaves(
void* fn,
const void* cer, const void* ks, const void* con,
void* cl, void* sl, void* nl,
void* ac, void* fc, void* fac, void* sc,
void* cm, void* sm, void* nm,
uint32_t cc, uint32_t kc, uint32_t nc, void* stream)
{
return ((lux_mpcvm_compute_leaves_fn)fn)(
cer, ks, con, cl, sl, nl, ac, fc, fac, sc, cm, sm, nm,
cc, kc, nc, stream);
}
static int call_compose_root(
void* fn,
const void* desc,
const void* cl, const void* sl, const void* nl,
const void* cm, const void* sm, const void* nm,
const void* ac, const void* fc, const void* fac, const void* sc,
void* state, void* result,
uint32_t cc, uint32_t kc, uint32_t nc, void* stream)
{
return ((lux_mpcvm_compose_root_fn)fn)(
desc, cl, sl, nl, cm, sm, nm, ac, fc, fac, sc, state, result,
cc, kc, nc, stream);
}
// dlsym helper — returns NULL when symbol is missing. Callers check.
static void* lux_dlsym(void* handle, const char* name) {
if (handle == NULL) return NULL;
dlerror();
return dlsym(handle, name);
}
// dlopen wrapper — try multiple candidate filenames in order. Returns NULL
// if every candidate fails. The dlopen mode is RTLD_NOW|RTLD_LOCAL: NOW so
// any missing symbol fails fast at load time, LOCAL so the plugin's
// internal symbols don't pollute the global namespace.
#include "lux/gpu/backend_plugin.h"
// mpcvm_plugin_trusted resolves lux_gpu_backend_init in an already-dlopen'd
// library, runs it, and checks the two cookies the plugin header requires a
// loader to check: abi_version must be the one this build was compiled
// against, and vtbl_size must be our sizeof. Without it, dlsym'ing a launcher
// by name trusts a library nothing has identified — the name is the only thing
// being matched, and a name is not a contract. quantumvm already does this.
//
// 0 trusted | 1 no init symbol | 2 init false | 3 abi | 4 vtbl_size
static int mpcvm_plugin_trusted(void* h) {
void* sym = dlsym(h, LUX_GPU_BACKEND_INIT_SYMBOL);
if (sym == NULL) return 1;
lux_gpu_backend_desc desc;
memset(&desc, 0, sizeof desc);
if (!((lux_gpu_backend_init_fn)sym)(&desc)) return 2;
if (desc.abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 3;
if (desc.vtbl_size != (uint32_t)sizeof(lux_gpu_backend_vtbl)) return 4;
return 0;
}
static void* lux_dlopen_first(const char* a, const char* b, const char* c) {
void* h = NULL;
if (a && *a) { h = dlopen(a, RTLD_NOW|RTLD_LOCAL); if (h) return h; }
if (b && *b) { h = dlopen(b, RTLD_NOW|RTLD_LOCAL); if (h) return h; }
if (c && *c) { h = dlopen(c, RTLD_NOW|RTLD_LOCAL); if (h) return h; }
return NULL;
}
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"sync"
"unsafe"
)
// ErrGPUNotAvailable is returned by GPUBackend methods when no plugin was
// resolved at init() time. Callers check this to fall back to the CPU
// reference (the protocol/ + executor.go state machine, which is unchanged
// by this bridge).
var ErrGPUNotAvailable = errors.New("mpcvm: GPU backend not available (no plugin dlopened)")
// GPUBackendKind is the resolved plugin family. Matches the dlopen probe
// order in backend.go: cuda → hip → metal → vulkan → webgpu.
type GPUBackendKind uint8
const (
GPUBackendNone GPUBackendKind = 0
GPUBackendCUDA GPUBackendKind = 1
GPUBackendHIP GPUBackendKind = 2
GPUBackendMetal GPUBackendKind = 3
GPUBackendVulkan GPUBackendKind = 4
GPUBackendWebGPU GPUBackendKind = 5
)
// String returns the launcher prefix used in symbol resolution.
func (k GPUBackendKind) String() string {
switch k {
case GPUBackendCUDA:
return "cuda"
case GPUBackendHIP:
return "hip"
case GPUBackendMetal:
return "metal"
case GPUBackendVulkan:
return "vulkan"
case GPUBackendWebGPU:
return "webgpu"
default:
return "none"
}
}
// =============================================================================
// Wire structs — byte-equivalent to ops/mpcvm/cuda/mpcvm_kernels_common.cuh.
//
// The struct layout MUST match the device-side __align__(16) declarations.
// Go's natural field alignment delivers the exact same packing on amd64 and
// arm64 (verified by the unsafe.Sizeof asserts in init()).
//
// Sizes (post __align__(16) tail padding):
//
// Ceremony = 128
// KeyShare = 368
// Contribution = 432
// MPCVMState = 160
// MPCVMRoundDescriptor = 96
// CeremonyOp = 96
// ContributionOp = 416
// MPCVMTransitionResult = 176
// =============================================================================
// GPUCeremony is the on-GPU ceremony state. 128 bytes, __align__(16). The
// GPU prefix distinguishes the wire mirror from the domain-level Ceremony
// types in protocols.go / runtime/. ONLY the GPU bridge ever touches these.
type GPUCeremony struct {
CeremonyID uint64
StartedAtNs uint64
DeadlineNs uint64
ParticipantsBitmap uint64
Kind uint32
Round uint32
Threshold uint32
TotalParticipants uint32
Status uint32
ContributionCount uint32
Subject [32]byte
CeremonySeed [32]byte
_ [8]byte // tail pad to 128
}
// GPUKeyShare is the on-GPU key share record. 368 bytes, __align__(16).
type GPUKeyShare struct {
ShareID uint64
CeremonyID uint64
HolderAddr uint64
Scheme uint32
HolderIndex uint32
ShareDataLen uint32
Occupied uint32
ShareData [320]byte
Pad0 uint64
}
// GPUContribution is the on-GPU contribution record. 432 bytes, __align__(16).
type GPUContribution struct {
ContributionID uint64
CeremonyID uint64
HolderAddr uint64
Round uint32
HolderIndex uint32
PayloadLen uint32
Status uint32
Payload [384]byte
Pad0 uint64
}
// GPUMPCVMState is the on-GPU substrate state. 160 bytes, __align__(16).
type GPUMPCVMState struct {
CurrentEpoch uint64
NowNs uint64
ActiveCeremonyCount uint32
FinalizedCeremonyCount uint32
FailedCeremonyCount uint32
KeyShareCount uint32
CeremonyRoot [32]byte
KeyShareRoot [32]byte
ContributionRoot [32]byte
MPCVMStateRoot [32]byte
}
// GPUMPCVMRoundDescriptor describes one round's input envelope. 96 bytes.
type GPUMPCVMRoundDescriptor struct {
ChainID uint64
Round uint64
TimestampNs uint64
Epoch uint64
Mode uint32
CeremonyOpCount uint32
ContributionOpCount uint32
ClosingFlag uint32
Pad0 uint32
Pad1 uint32
Pad2 uint64
ParentStateRoot [32]byte
}
// GPUCeremonyOp is one inbound ceremony op (begin/cancel). 96 bytes.
type GPUCeremonyOp struct {
CeremonyID uint64
DeadlineNs uint64
Kind uint32
CeremonyKind uint32
Threshold uint32
TotalParticipants uint32
Subject [32]byte
CeremonySeed [32]byte
}
// GPUContributionOp is one inbound contribution payload. 416 bytes.
type GPUContributionOp struct {
CeremonyID uint64
HolderAddr uint64
Round uint32
HolderIndex uint32
PayloadLen uint32
Pad0 uint32
Payload [384]byte
}
// GPUMPCVMTransitionResult is the transition envelope written by
// compose_root. 176 bytes.
type GPUMPCVMTransitionResult struct {
Status uint32
CeremonyApplyCount uint32
ContributionApplyCount uint32
FinalizedThisRound uint32
FailedThisRound uint32
ActiveCeremonyCount uint32
KeyShareCount uint32
RoundAdvanceCount uint32
Epoch uint64
NowNs uint64
CeremonyRoot [32]byte
KeyShareRoot [32]byte
ContributionRoot [32]byte
MPCVMStateRoot [32]byte
}
// =============================================================================
// Layout asserts — pin Go-side sizeof() to the device-side __align__(16)
// values. Any drift between the host structs and the device structs would
// produce silently-wrong results (the kernel reads bytes at fixed offsets),
// so any mismatch must fail at init() time. These are documented again in
// the .cuh peer file ops/mpcvm/cuda/mpcvm_kernels_common.cuh.
// =============================================================================
const (
sizeCeremony = 128
sizeKeyShare = 368
sizeContribution = 432
sizeMPCVMState = 160
sizeMPCVMRoundDescriptor = 96
sizeCeremonyOp = 96
sizeContributionOp = 416
sizeMPCVMTransitionResult = 176
)
func assertSizes() {
type pair struct {
name string
want int
got uintptr
}
checks := []pair{
{"GPUCeremony", sizeCeremony, unsafe.Sizeof(GPUCeremony{})},
{"GPUKeyShare", sizeKeyShare, unsafe.Sizeof(GPUKeyShare{})},
{"GPUContribution", sizeContribution, unsafe.Sizeof(GPUContribution{})},
{"GPUMPCVMState", sizeMPCVMState, unsafe.Sizeof(GPUMPCVMState{})},
{"GPUMPCVMRoundDescriptor", sizeMPCVMRoundDescriptor, unsafe.Sizeof(GPUMPCVMRoundDescriptor{})},
{"GPUCeremonyOp", sizeCeremonyOp, unsafe.Sizeof(GPUCeremonyOp{})},
{"GPUContributionOp", sizeContributionOp, unsafe.Sizeof(GPUContributionOp{})},
{"GPUMPCVMTransitionResult", sizeMPCVMTransitionResult, unsafe.Sizeof(GPUMPCVMTransitionResult{})},
}
for _, c := range checks {
if int(c.got) != c.want {
panic(fmt.Sprintf(
"mpcvm: GPU layout mismatch — %s sizeof=%d want=%d. "+
"Rebuild gpu-kernels (ops/mpcvm/cuda/mpcvm_kernels_common.cuh) "+
"or update the Go struct.", c.name, c.got, c.want))
}
}
}
// =============================================================================
// GPUBackend — dlopen'd plugin handle + resolved function pointers.
//
// Constructed exactly once at init() (backend.go). Concurrent reads are safe
// (the symbol table is immutable after init). Concurrent calls into the same
// launcher are safe because the launchers themselves are stateless modulo
// the singleton MTL/cuda/vulkan context which holds its own mutex.
// =============================================================================
// GPUBackend is the resolved plugin substrate. Zero value = not available.
type GPUBackend struct {
Kind GPUBackendKind
Path string // dlopen'd library path, for diagnostics
handle unsafe.Pointer // dlopen handle; nil = no plugin
// Resolved symbols. Nil = symbol missing in plugin (launcher disabled).
fnCeremonyApply unsafe.Pointer
fnCeremonySweep unsafe.Pointer
fnComputeLeaves unsafe.Pointer
fnComposeRoot unsafe.Pointer
}
var (
gpuBackend *GPUBackend
gpuBackendOnce sync.Once
)
// Backend returns the resolved GPU plugin. nil means no plugin was loaded.
func Backend() *GPUBackend {
return gpuBackend
}
// IsAvailable reports whether the bridge has a usable plugin with at least
// the ceremony_apply and ceremony_sweep launchers resolved. compute_leaves
// and compose_root are required for MPCTransition but are checked
// per-method to allow partial GPU coverage when a plugin ships fewer
// symbols (e.g. an early Vulkan port).
func (g *GPUBackend) IsAvailable() bool {
if g == nil {
return false
}
return g.handle != nil &&
g.fnCeremonyApply != nil &&
g.fnCeremonySweep != nil
}
// =============================================================================
// MPC state-machine surface — four methods covering the substrate's apply /
// transition contract. Each one is a thin shim over the corresponding
// host launcher symbol; the device-side semantics live in
// ops/mpcvm/<X>/mpcvm_*.{cu,metal,wgsl,comp}.
//
// The same ceremony_apply launcher is used twice — CeremonyApply runs it
// with contribution_ops empty, ContributionApply runs it with ceremony_ops
// empty. This mirrors the orthogonal state-machine view (one kernel, two
// op streams) while keeping the C ABI surface tight.
// =============================================================================
// CeremonyApply applies ceremony begin/cancel ops to the ceremony table.
// Contribution slots are not touched (callers passing nil contribution_ops
// get the lean ceremony-admin dispatch).
//
// ceremonies is the open-addressed ceremony hash table (must be a power-of-2
// length on the device-side; the Go caller owns the buffer). The kernel
// mutates it in place; the round descriptor's CeremonyOpCount tells the
// kernel how many ops to consume from ceremonyOps.
//
// next_contribution_id_in is the substrate's monotonically increasing
// contribution-id counter at the start of the round; the kernel doesn't
// advance it on the ceremony-only path but the parameter is part of the
// shared launcher signature.
func (g *GPUBackend) CeremonyApply(
desc *GPUMPCVMRoundDescriptor,
ceremonyOps []GPUCeremonyOp,
ceremonies []GPUCeremony,
) (applied uint32, err error) {
if g == nil || g.handle == nil || g.fnCeremonyApply == nil {
return 0, ErrGPUNotAvailable
}
if desc == nil || len(ceremonies) == 0 {
return 0, fmt.Errorf("mpcvm.CeremonyApply: nil desc or empty ceremonies")
}
var (
ceremonyApplied uint32
contributionApplied uint32
emptyContribOps [1]GPUContributionOp
emptyContributions [1]GPUContribution
)
descPtr := unsafe.Pointer(desc)
cerOpsPtr := unsafe.Pointer(&emptyContribOps[0])
if len(ceremonyOps) > 0 {
cerOpsPtr = unsafe.Pointer(&ceremonyOps[0])
}
cerPtr := unsafe.Pointer(&ceremonies[0])
conPtr := unsafe.Pointer(&emptyContributions[0])
rc := C.call_ceremony_apply(
g.fnCeremonyApply,
descPtr,
cerOpsPtr,
unsafe.Pointer(&emptyContribOps[0]), // contribution_ops: empty stream
cerPtr,
conPtr, // contributions: 1-slot placeholder (count=1)
unsafe.Pointer(&ceremonyApplied),
unsafe.Pointer(&contributionApplied),
C.uint32_t(len(ceremonies)),
C.uint32_t(1),
C.uint64_t(0),
nil,
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ceremonyOps)
runtime.KeepAlive(ceremonies)
runtime.KeepAlive(emptyContribOps)
runtime.KeepAlive(emptyContributions)
if rc != 0 {
return 0, fmt.Errorf("mpcvm.CeremonyApply: launcher rc=%d", int(rc))
}
return ceremonyApplied, nil
}
// KeyShareApply runs the per-slot fan-out sweep that advances ceremonies,
// finalizes keygens (assigning canonical share_ids), and times out expired
// ceremonies. Backed by lux_<X>_mpcvm_ceremony_sweep.
//
// On DKG finalize, fresh KeyShare slots are written into keyShares with the
// share_data_len matching the scheme (Frost=65, CGGMP21=65, Corona=256).
// next_share_id_in seeds the prefix-sum scheme that gives every finalized
// share a deterministic share_id.
func (g *GPUBackend) KeyShareApply(
desc *GPUMPCVMRoundDescriptor,
ceremonies []GPUCeremony,
keyShares []GPUKeyShare,
contributions []GPUContribution,
nextShareID uint64,
) (roundAdvance, finalized, failed uint32, err error) {
if g == nil || g.handle == nil || g.fnCeremonySweep == nil {
return 0, 0, 0, ErrGPUNotAvailable
}
if desc == nil || len(ceremonies) == 0 ||
len(keyShares) == 0 || len(contributions) == 0 {
return 0, 0, 0, fmt.Errorf("mpcvm.KeyShareApply: nil desc or empty arena")
}
rc := C.call_ceremony_sweep(
g.fnCeremonySweep,
unsafe.Pointer(desc),
unsafe.Pointer(&ceremonies[0]),
unsafe.Pointer(&keyShares[0]),
unsafe.Pointer(&contributions[0]),
unsafe.Pointer(&roundAdvance),
unsafe.Pointer(&finalized),
unsafe.Pointer(&failed),
C.uint32_t(len(ceremonies)),
C.uint32_t(len(keyShares)),
C.uint32_t(len(contributions)),
C.uint64_t(nextShareID),
nil,
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ceremonies)
runtime.KeepAlive(keyShares)
runtime.KeepAlive(contributions)
if rc != 0 {
return 0, 0, 0, fmt.Errorf("mpcvm.KeyShareApply: launcher rc=%d", int(rc))
}
return roundAdvance, finalized, failed, nil
}
// ContributionApply applies contribution payloads to the contribution table.
// Ceremony slots are not touched (callers passing nil ceremonyOps get the
// lean contribution-only dispatch). Uses the same ceremony_apply kernel
// because the dedup-and-write path is the same on the device side; only
// the op stream differs.
func (g *GPUBackend) ContributionApply(
desc *GPUMPCVMRoundDescriptor,
contributionOps []GPUContributionOp,
ceremonies []GPUCeremony,
contributions []GPUContribution,
nextContributionID uint64,
) (applied uint32, err error) {
if g == nil || g.handle == nil || g.fnCeremonyApply == nil {
return 0, ErrGPUNotAvailable
}
if desc == nil || len(ceremonies) == 0 || len(contributions) == 0 {
return 0, fmt.Errorf("mpcvm.ContributionApply: nil desc or empty arena")
}
var (
ceremonyApplied uint32
contributionApplied uint32
emptyCeremonyOps [1]GPUCeremonyOp
)
conOpsPtr := unsafe.Pointer(&emptyCeremonyOps[0]) // bound buffer placeholder
conActualOpsPtr := unsafe.Pointer(&emptyCeremonyOps[0])
if len(contributionOps) > 0 {
conActualOpsPtr = unsafe.Pointer(&contributionOps[0])
}
rc := C.call_ceremony_apply(
g.fnCeremonyApply,
unsafe.Pointer(desc),
conOpsPtr, // ceremony_ops: empty stream
conActualOpsPtr,
unsafe.Pointer(&ceremonies[0]),
unsafe.Pointer(&contributions[0]),
unsafe.Pointer(&ceremonyApplied),
unsafe.Pointer(&contributionApplied),
C.uint32_t(len(ceremonies)),
C.uint32_t(len(contributions)),
C.uint64_t(nextContributionID),
nil,
)
runtime.KeepAlive(desc)
runtime.KeepAlive(emptyCeremonyOps)
runtime.KeepAlive(contributionOps)
runtime.KeepAlive(ceremonies)
runtime.KeepAlive(contributions)
if rc != 0 {
return 0, fmt.Errorf("mpcvm.ContributionApply: launcher rc=%d", int(rc))
}
return contributionApplied, nil
}
// MPCTransition runs the per-leaf keccak pass and the canonical-order fold
// pass, producing the round's MPCVMTransitionResult and advancing the
// substrate state. This is the composition of compute_leaves and
// compose_root — one substrate state transition per call.
//
// Returns the result envelope written by compose_root. The substrate state
// in `state` is also updated in place (cur_epoch, now_ns, counts, all four
// roots).
func (g *GPUBackend) MPCTransition(
desc *GPUMPCVMRoundDescriptor,
ceremonies []GPUCeremony,
keyShares []GPUKeyShare,
contributions []GPUContribution,
state *GPUMPCVMState,
) (*GPUMPCVMTransitionResult, error) {
if g == nil || g.handle == nil {
return nil, ErrGPUNotAvailable
}
if g.fnComputeLeaves == nil || g.fnComposeRoot == nil {
return nil, fmt.Errorf("mpcvm.MPCTransition: %w (compute_leaves or compose_root missing)",
ErrGPUNotAvailable)
}
if desc == nil || state == nil ||
len(ceremonies) == 0 || len(keyShares) == 0 || len(contributions) == 0 {
return nil, fmt.Errorf("mpcvm.MPCTransition: nil desc/state or empty arena")
}
// Scratch buffers consumed by compute_leaves and feeding compose_root.
// All Go-allocated, all live for the duration of both calls — pinned by
// being on the local stack (escape-analysis-promoted to heap if needed).
ceremonyLeaves := make([]byte, len(ceremonies)*32)
shareLeaves := make([]byte, len(keyShares)*32)
contributionLeaves := make([]byte, len(contributions)*32)
ceremonyMask := make([]byte, len(ceremonies))
shareMask := make([]byte, len(keyShares))
contributionMask := make([]byte, len(contributions))
var activeCount, finalizedCount, failedCount, shareCount uint32
var result GPUMPCVMTransitionResult
rc := C.call_compute_leaves(
g.fnComputeLeaves,
unsafe.Pointer(&ceremonies[0]),
unsafe.Pointer(&keyShares[0]),
unsafe.Pointer(&contributions[0]),
unsafe.Pointer(&ceremonyLeaves[0]),
unsafe.Pointer(&shareLeaves[0]),
unsafe.Pointer(&contributionLeaves[0]),
unsafe.Pointer(&activeCount),
unsafe.Pointer(&finalizedCount),
unsafe.Pointer(&failedCount),
unsafe.Pointer(&shareCount),
unsafe.Pointer(&ceremonyMask[0]),
unsafe.Pointer(&shareMask[0]),
unsafe.Pointer(&contributionMask[0]),
C.uint32_t(len(ceremonies)),
C.uint32_t(len(keyShares)),
C.uint32_t(len(contributions)),
nil,
)
if rc != 0 {
return nil, fmt.Errorf("mpcvm.MPCTransition compute_leaves: launcher rc=%d", int(rc))
}
rc = C.call_compose_root(
g.fnComposeRoot,
unsafe.Pointer(desc),
unsafe.Pointer(&ceremonyLeaves[0]),
unsafe.Pointer(&shareLeaves[0]),
unsafe.Pointer(&contributionLeaves[0]),
unsafe.Pointer(&ceremonyMask[0]),
unsafe.Pointer(&shareMask[0]),
unsafe.Pointer(&contributionMask[0]),
unsafe.Pointer(&activeCount),
unsafe.Pointer(&finalizedCount),
unsafe.Pointer(&failedCount),
unsafe.Pointer(&shareCount),
unsafe.Pointer(state),
unsafe.Pointer(&result),
C.uint32_t(len(ceremonies)),
C.uint32_t(len(keyShares)),
C.uint32_t(len(contributions)),
nil,
)
runtime.KeepAlive(desc)
runtime.KeepAlive(ceremonies)
runtime.KeepAlive(keyShares)
runtime.KeepAlive(contributions)
runtime.KeepAlive(state)
runtime.KeepAlive(ceremonyLeaves)
runtime.KeepAlive(shareLeaves)
runtime.KeepAlive(contributionLeaves)
runtime.KeepAlive(ceremonyMask)
runtime.KeepAlive(shareMask)
runtime.KeepAlive(contributionMask)
if rc != 0 {
return nil, fmt.Errorf("mpcvm.MPCTransition compose_root: launcher rc=%d", int(rc))
}
return &result, nil
}
// =============================================================================
// Plugin loader — dlopen one candidate path, dlsym the four symbol pairs,
// fail soft if the plugin is missing or any required symbol is absent.
// Soft-fail returns nil; callers then see Backend() == nil and route to CPU.
// =============================================================================
// tryLoadPlugin attempts to dlopen the candidate filenames in order and
// resolve the four expected symbols using the given backend prefix. Returns
// non-nil only when dlopen succeeded AND ceremony_apply + ceremony_sweep
// both resolved (the minimum useful surface). compute_leaves and
// compose_root are optional at this stage — MPCTransition will surface
// ErrGPUNotAvailable when either is missing.
func tryLoadPlugin(kind GPUBackendKind, candidates ...string) *GPUBackend {
prefix := kind.String()
if prefix == "none" {
return nil
}
var (
ca *C.char
cb *C.char
cc *C.char
)
if len(candidates) > 0 && candidates[0] != "" {
ca = C.CString(candidates[0])
defer C.free(unsafe.Pointer(ca))
}
if len(candidates) > 1 && candidates[1] != "" {
cb = C.CString(candidates[1])
defer C.free(unsafe.Pointer(cb))
}
if len(candidates) > 2 && candidates[2] != "" {
cc = C.CString(candidates[2])
defer C.free(unsafe.Pointer(cc))
}
h := C.lux_dlopen_first(ca, cb, cc)
if h == nil {
return nil
}
// Identify the library before trusting any symbol in it.
if rc := C.mpcvm_plugin_trusted(h); rc != 0 {
return nil
}
resolve := func(name string) unsafe.Pointer {
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
return unsafe.Pointer(C.lux_dlsym(h, cname))
}
b := &GPUBackend{
Kind: kind,
handle: unsafe.Pointer(h),
fnCeremonyApply: resolve(fmt.Sprintf("lux_%s_mpcvm_ceremony_apply", prefix)),
fnCeremonySweep: resolve(fmt.Sprintf("lux_%s_mpcvm_ceremony_sweep", prefix)),
fnComputeLeaves: resolve(fmt.Sprintf("lux_%s_mpcvm_compute_leaves", prefix)),
fnComposeRoot: resolve(fmt.Sprintf("lux_%s_mpcvm_compose_root", prefix)),
}
if b.fnCeremonyApply == nil || b.fnCeremonySweep == nil {
// Required symbols missing — close the handle and return nil so the
// probe falls through to the next backend.
_ = C.dlclose(h)
return nil
}
// Pick a resolved path string from the first non-empty candidate that
// dlopen could have used. We don't get the resolved name back from
// dlopen on every platform, so this is best-effort diagnostic.
for _, c := range candidates {
if c != "" {
b.Path = c
break
}
}
return b
}