Files
chains/mpcvm/mpcvm_gpu.go
T
zeekayandHanzo Dev 1c7f329fbd mpcvm: make M-Chain a real custody chain
M-Chain is the MPC/threshold-custody VM that replaces off-chain signer
clusters for bridge custody. This makes it one.

vmID — the one-way door. factory.go declared a private `thresholdvm`
literal that matched nothing else in the stack, and node/Dockerfile
installed the plugin under that name's CB58 while genesis declares
constants.MPCVMID. The two never met, so M-Chain could not have started.
VMID is now the shared constant, and TestVMID_IsCanonicalAndStable pins
the CB58 (qCURact1n41Fco...) with the list of every declaration that must
move together if it ever changes before launch.

k vs t. types.Ceremony carried Threshold/Total as bare uint16s: Validate
read Threshold as the signer count (enforcing threshold*2 > total) while
the VM read it as the polynomial degree and passed it straight to
cmp.Keygen. The same field meant K in one file and t in another, and the
two differ by one — the same silent off-by-one that produces a 4-of-5
wallet from a config saying 3-of-5. Both now carry a quorum.Policy, and t
is only ever obtained by calling Degree(). The majority floor survives as
types.HasUniqueQuorum, defined once and enforced on every key record.

State. The VM kept its custody keys in a map persisted only on a clean
Shutdown, with the key share excluded from the wire entirely, and reset
lastAccepted to a recomputed genesis block on every Initialize — so a
restart discarded the chain and came back unable to sign. State is now
persisted at the moment each fact becomes true, split into a replicated
c/ keyspace (key registry, ceremony log, blocks, height index, root) and
a node-private n/ keyspace (this validator's shares), and Initialize
resumes the accepted tip.

Blocks verify. A block carries its post-state root and every operation's
artifact. Verify re-checks a signature against the group key already in
the registry — a proposer cannot fabricate one without forging ECDSA — and
recomputes the root, so two validators that would diverge cannot both
accept. Keygen operations carry a proof of possession by the new group key
over its own registration; a participant additionally cross-checks the
declared degree against its own share, so one honest participant rejects a
mis-declared key.

Ceremonies are leaderless. The id is derived from the task
(key, digest, sorted signers), so every validator converges on the same
ceremony with no announce round and no coordinator, and the committee is
the chain's own validator set rather than an operator roster.

Address derivation was SHA-256 where every EVM chain uses Keccak-256, so
the published custody address did not belong to the group key. Funds sent
there would have been unspendable.

Deletes the duplicate signing paths and the in-memory session machinery:
one way to generate a key, one way to sign.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 14:36:22 -07:00

826 lines
28 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
#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.
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
}
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
}