mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Mirrors the chains/aivm/aivm_gpu.go + chains/evm/cevm pattern: the GPU
substrate for the P-Chain validator/stake/slashing/epoch transitions is
resolved at PROCESS START via dlopen/dlsym against the lux-gpu-kernels
plugin DSOs. Bridge is FALSE-by-default — the chain continues to drive
consensus through the existing pure-Go path until an operator opts in
by setting LUX_PLATFORMVM_GPU=1. Any launcher error from the GPU falls
back to the Go path WITHOUT panic.
Three new files (plus one round-trip test) decomplect cleanly:
- platformvm_gpu.go (build tag cgo) — exposes GPUBackend with four
methods (ValidatorSetApply, StakeTransition, SlashingTransition,
EpochTransition) that dlsym the host launchers
lux_<backend>_platformvm_<op>. Layout-drift init() asserts every
struct (PVMValidatorSlot=176, PVMStakeRecord=64, PVMSlashEvidence=80,
PVMEpochState=160, PVMRoundDescriptor=96, PVMValidatorOp=176,
PVMStakeOp=64, PVMTransitionResult=192) against the on-device layout
at ops/platformvm/cuda/platformvm_kernels_common.cuh + the
authoritative platformvm_gpu_layout.hpp.
- platformvm_gpu_nocgo.go (build tag !cgo) — keeps the public surface
identical (same struct names, method signatures, constants); every
method returns ErrGPUNotAvailable.
- backend.go — pure-Go probe driver. Walks the canonical plugin order
(cuda → hip → metal → vulkan → webgpu) at process start via init(),
pins the first successful open as ActiveGPUBackend(). AutoBackend()
is the explicit re-entry point. Search paths cover the operator
override (LUX_PLATFORMVM_GPU_DIR), shared (LUX_GPU_PLUGIN_DIR), dev
tree (~/work/lux-private/gpu-kernels/build/*_backend), prefix install
(/usr/local/lib/lux-gpu), system (/usr/lib/lux-gpu), and the empty
DYLD/LD_LIBRARY_PATH fallback. GPUEnabled() reads the operator
opt-in env knob — decomplected from the probe itself so operators
can inspect a loaded-but-inert backend before flipping the gate.
- platformvm_gpu_test.go — three subtests:
1. AutoBackend round-trip: dlopen plugin, dlsym four launchers, call
ValidatorSetApply on a 1-validator (kVOpAdd) fixture, assert
applied=1 and slot Status = Active|PendingAdd. SKIPs cleanly when
no plugin is on disk (the production default).
2. Nil-handle contract: zero-value *GPUBackend returns
ErrGPUNotAvailable from every method without panic.
3. Env knob: GPUEnabled() recognises 1/true/yes/TRUE as opt-in;
anything else (including unset) stays at the false-by-default.
Verified round-trip PASSes against metal, vulkan, and webgpu plugins
on M1 Max (laptop). CUDA / HIP are the linux NVIDIA/AMD paths and
were verified in the spark.local GB10 Blackwell sm_120 micro-bench
session that produced the cited cells (validator_set_apply ~5.5ms,
stake_transition ~3.0ms, slashing_transition ~0.06ms, epoch transition
~7.0ms — gpu-kernels HEAD 4e166de).
Does NOT modify vm.go / block.go / state.go core paths; this is the
bridge surface only. The existing transition functions can opt into
the GPU path later behind the LUX_PLATFORMVM_GPU=1 gate.
All builds succeed: go build, go build -tags cgo, CGO_ENABLED=0 go
build. All existing tests in vms/platformvm pass under both modes —
no regressions.
728 lines
22 KiB
Go
728 lines
22 KiB
Go
//go:build cgo
|
||
|
||
// Package platformvm GPU backend — runtime-loaded plugin bridge.
|
||
//
|
||
// Mirrors the chains/aivm/aivm_gpu.go + chains/evm/cevm pattern: the GPU
|
||
// substrate for the P-Chain validator/stake/slashing/epoch transitions is
|
||
// resolved at PROCESS START via dlopen/dlsym against the lux-gpu-kernels
|
||
// plugin DSOs. This keeps the node module compilable without
|
||
// lux-private/gpu-kernels present in the build tree — the plugin is fully
|
||
// optional and the chain runs the existing pure-Go path otherwise.
|
||
//
|
||
// Lookup order (handled by backend.go):
|
||
//
|
||
// libluxgpu_backend_cuda.so (Linux x86_64 + NVIDIA)
|
||
// libluxgpu_backend_hip.so (Linux x86_64 + AMD ROCm)
|
||
// libluxgpu_backend_metal.dylib (macOS Apple Silicon / Intel)
|
||
// libluxgpu_backend_vulkan.so/.dylib (any Vulkan ICD)
|
||
// libluxgpu_backend_webgpu.so/.dylib (Dawn / wgpu-native)
|
||
//
|
||
// Each plugin exports four extern "C" host launchers per backend:
|
||
//
|
||
// lux_<backend>_platformvm_validator_set_apply
|
||
// lux_<backend>_platformvm_stake_transition
|
||
// lux_<backend>_platformvm_slashing_transition
|
||
// lux_<backend>_platformvm_epoch_transition
|
||
//
|
||
// Struct layout matches ops/platformvm/cuda/platformvm_kernels_common.cuh
|
||
// and platformvm_gpu_layout.hpp byte-for-byte (asserted by init()).
|
||
// Pointer ABI is HOST pointers — the launcher does H2D-upload / dispatch /
|
||
// D2H-download internally.
|
||
//
|
||
// FALSE-by-default: even when cgo is on and a plugin loads, the chain
|
||
// runs the existing pure-Go path until an operator explicitly sets
|
||
// `LUX_PLATFORMVM_GPU=1`. Any launcher error → fall back to Go path
|
||
// WITHOUT panic and emit a warn log.
|
||
package platformvm
|
||
|
||
/*
|
||
#cgo darwin LDFLAGS: -ldl
|
||
#cgo linux LDFLAGS: -ldl
|
||
|
||
#include <dlfcn.h>
|
||
#include <stdint.h>
|
||
#include <stdlib.h>
|
||
|
||
// Four host-launcher trampolines — invoked by Go via cgo. The function
|
||
// pointer is opaque (void*); the cgo bridge casts it to the expected
|
||
// C signature and forwards arguments.
|
||
//
|
||
// All four launcher prototypes are identical across backends (cuda / hip /
|
||
// metal / vulkan / webgpu). Argument pointers are HOST pointers; the
|
||
// last `void*` is a "stream" handle that is always nullptr on the host-
|
||
// pointer ABI backends (metal / vulkan / webgpu) and the cuda/hip stream
|
||
// slot from the kernel author's POV.
|
||
|
||
typedef int (*pvm_validator_set_fn)(
|
||
const void* desc,
|
||
const void* ops,
|
||
void* validators,
|
||
void* applied_out,
|
||
uint32_t validator_count,
|
||
void* stream);
|
||
|
||
typedef int (*pvm_stake_fn)(
|
||
const void* desc,
|
||
const void* ops,
|
||
void* validators,
|
||
void* stake,
|
||
void* applied_out,
|
||
uint32_t validator_count,
|
||
uint32_t stake_count,
|
||
void* stream);
|
||
|
||
typedef int (*pvm_slashing_fn)(
|
||
const void* desc,
|
||
const void* evidence,
|
||
void* validators,
|
||
void* slashing,
|
||
void* applied_out,
|
||
void* total_lo_out,
|
||
void* total_hi_out,
|
||
uint32_t validator_count,
|
||
uint32_t slashing_count,
|
||
void* stream);
|
||
|
||
typedef int (*pvm_epoch_fn)(
|
||
const void* desc,
|
||
void* validators,
|
||
void* stake,
|
||
void* slashing,
|
||
void* epoch,
|
||
void* result,
|
||
uint32_t validator_count,
|
||
uint32_t stake_count,
|
||
uint32_t slashing_count,
|
||
void* leaf_scratch,
|
||
void* stream);
|
||
|
||
static int call_pvm_validator_set(void* fn,
|
||
const void* desc, const void* ops,
|
||
void* validators, void* applied_out,
|
||
uint32_t validator_count) {
|
||
return ((pvm_validator_set_fn)fn)(desc, ops, validators, applied_out,
|
||
validator_count, NULL);
|
||
}
|
||
static int call_pvm_stake(void* fn,
|
||
const void* desc, const void* ops,
|
||
void* validators, void* stake, void* applied_out,
|
||
uint32_t validator_count, uint32_t stake_count) {
|
||
return ((pvm_stake_fn)fn)(desc, ops, validators, stake, applied_out,
|
||
validator_count, stake_count, NULL);
|
||
}
|
||
static int call_pvm_slashing(void* fn,
|
||
const void* desc, const void* evidence,
|
||
void* validators, void* slashing, void* applied_out,
|
||
void* total_lo_out, void* total_hi_out,
|
||
uint32_t validator_count, uint32_t slashing_count) {
|
||
return ((pvm_slashing_fn)fn)(desc, evidence, validators, slashing,
|
||
applied_out, total_lo_out, total_hi_out,
|
||
validator_count, slashing_count, NULL);
|
||
}
|
||
static int call_pvm_epoch(void* fn,
|
||
const void* desc, void* validators, void* stake,
|
||
void* slashing, void* epoch, void* result,
|
||
uint32_t validator_count, uint32_t stake_count,
|
||
uint32_t slashing_count, void* leaf_scratch) {
|
||
return ((pvm_epoch_fn)fn)(desc, validators, stake, slashing, epoch, result,
|
||
validator_count, stake_count, slashing_count,
|
||
leaf_scratch, NULL);
|
||
}
|
||
|
||
// dlopen / dlsym wrappers — kept here so backend.go can stay pure Go.
|
||
static void* lux_pvm_dlopen(const char* path) {
|
||
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||
}
|
||
static void* lux_pvm_dlsym(void* handle, const char* sym) {
|
||
return dlsym(handle, sym);
|
||
}
|
||
static const char* lux_pvm_dlerror() {
|
||
return dlerror();
|
||
}
|
||
static void lux_pvm_dlclose(void* handle) {
|
||
dlclose(handle);
|
||
}
|
||
*/
|
||
import "C"
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"runtime"
|
||
"sync"
|
||
"unsafe"
|
||
)
|
||
|
||
// ErrGPUNotAvailable is returned by every GPUBackend method when no plugin
|
||
// was loadable at init time. Callers fall through to the existing Go path.
|
||
var ErrGPUNotAvailable = errors.New("platformvm: no GPU plugin available")
|
||
|
||
// GPUBackendKind identifies which lux-gpu-kernels plugin satisfied the
|
||
// runtime dlopen probe. AvailableNone is the sentinel "fall through to Go".
|
||
type GPUBackendKind uint8
|
||
|
||
const (
|
||
GPUNone GPUBackendKind = 0
|
||
GPUCUDA GPUBackendKind = 1
|
||
GPUHIP GPUBackendKind = 2
|
||
GPUMetal GPUBackendKind = 3
|
||
GPUVulkan GPUBackendKind = 4
|
||
GPUWebGPU GPUBackendKind = 5
|
||
)
|
||
|
||
// String returns the human-readable name for the backend kind.
|
||
func (k GPUBackendKind) String() string {
|
||
switch k {
|
||
case GPUNone:
|
||
return "none"
|
||
case GPUCUDA:
|
||
return "cuda"
|
||
case GPUHIP:
|
||
return "hip"
|
||
case GPUMetal:
|
||
return "metal"
|
||
case GPUVulkan:
|
||
return "vulkan"
|
||
case GPUWebGPU:
|
||
return "webgpu"
|
||
default:
|
||
return fmt.Sprintf("backend(%d)", uint8(k))
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// Layout-drift guards — match ops/platformvm/cuda/platformvm_kernels_common.cuh
|
||
// + platformvm_gpu_layout.hpp byte-for-byte.
|
||
//
|
||
// The struct bytes Go hands to C MUST match the on-disk layout file at
|
||
// ~/work/lux-private/gpu-kernels/ops/platformvm/op.yaml — every kernel reads
|
||
// them via reinterpret_cast. A silent layout shift produces consensus-divergent
|
||
// state roots. init() refuses to load if any size drifts.
|
||
// =============================================================================
|
||
|
||
// PVMValidatorSlot mirrors platformvm::gpu::ValidatorSlot (176 bytes).
|
||
type PVMValidatorSlot struct {
|
||
ValidatorID uint64
|
||
Weight uint64
|
||
BLSPubkey [48]byte
|
||
CoronaPubkey [32]byte // Corona / Corona commitment digest
|
||
MLDSAPubkey [32]byte
|
||
MLDSAGroth16Root [32]byte
|
||
Status uint32
|
||
JailUntilEpoch uint32
|
||
Occupied uint32
|
||
_pad0 uint32
|
||
}
|
||
|
||
// PVMStakeRecord mirrors platformvm::gpu::StakeRecord (64 bytes).
|
||
type PVMStakeRecord struct {
|
||
DelegatorID uint64
|
||
ValidatorID uint64
|
||
Amount uint64
|
||
LockUntilEpoch uint64
|
||
RewardAccumulator uint64
|
||
CommissionBPS uint32
|
||
Status uint32
|
||
EpochBonded uint32
|
||
EpochUnbonded uint32
|
||
_pad0 uint64
|
||
}
|
||
|
||
// PVMSlashEvidence mirrors platformvm::gpu::SlashEvidence (80 bytes).
|
||
type PVMSlashEvidence struct {
|
||
ValidatorID uint64
|
||
Height uint64
|
||
SlashAmount uint64
|
||
Kind uint32
|
||
Epoch uint32
|
||
JailForEpochs uint32
|
||
_pad0 uint32
|
||
EvidenceDigest [32]byte
|
||
_pad1 uint64
|
||
}
|
||
|
||
// PVMEpochState mirrors platformvm::gpu::EpochState (160 bytes).
|
||
type PVMEpochState struct {
|
||
CurrentEpoch uint64
|
||
NextEpochHeight uint64
|
||
TotalActiveStake uint64
|
||
ActiveValidatorCount uint32
|
||
PendingDropCount uint32
|
||
ValidatorSetRoot [32]byte
|
||
StakeRoot [32]byte
|
||
SlashingRoot [32]byte
|
||
EpochRoot [32]byte
|
||
}
|
||
|
||
// PVMRoundDescriptor mirrors platformvm::gpu::PVMRoundDescriptor (96 bytes).
|
||
type PVMRoundDescriptor struct {
|
||
ChainID uint64
|
||
Round uint64
|
||
TimestampNS uint64
|
||
Epoch uint64
|
||
Mode uint32
|
||
ValidatorOpCount uint32
|
||
StakeOpCount uint32
|
||
SlashEvidenceCount uint32
|
||
ClosingFlag uint32
|
||
_pad0 uint32
|
||
_pad1 uint64
|
||
ParentEpochRoot [32]byte
|
||
}
|
||
|
||
// PVMValidatorOp mirrors platformvm::gpu::ValidatorOp (176 bytes).
|
||
type PVMValidatorOp struct {
|
||
ValidatorID uint64
|
||
Weight uint64
|
||
BLSPubkey [48]byte
|
||
CoronaPubkey [32]byte
|
||
MLDSAPubkey [32]byte
|
||
MLDSAGroth16Root [32]byte
|
||
Kind uint32
|
||
JailUntilEpoch uint32
|
||
Epoch uint32
|
||
_pad0 uint32
|
||
}
|
||
|
||
// PVMStakeOp mirrors platformvm::gpu::StakeOp (64 bytes).
|
||
type PVMStakeOp struct {
|
||
DelegatorID uint64
|
||
ValidatorID uint64
|
||
Amount uint64
|
||
LockUntilEpoch uint64
|
||
SourceValidatorID uint64
|
||
Kind uint32
|
||
CommissionBPS uint32
|
||
Epoch uint32
|
||
_pad0 uint32
|
||
_pad1 uint64
|
||
}
|
||
|
||
// PVMTransitionResult mirrors platformvm::gpu::PVMTransitionResult (192 bytes).
|
||
type PVMTransitionResult struct {
|
||
Status uint32
|
||
ValidatorApplyCount uint32
|
||
StakeApplyCount uint32
|
||
SlashApplyCount uint32
|
||
ActiveValidatorCount uint32
|
||
PendingDropCount uint32
|
||
JailedCount uint32
|
||
TombstonedCount uint32
|
||
TotalActiveStake uint64
|
||
TotalSlashed uint64
|
||
TotalRewards uint64
|
||
Epoch uint64
|
||
ValidatorSetRoot [32]byte
|
||
StakeRoot [32]byte
|
||
SlashingRoot [32]byte
|
||
EpochRoot [32]byte
|
||
}
|
||
|
||
// PlatformVM op-kind constants — must match
|
||
// ops/platformvm/cuda/platformvm_kernels_common.cuh.
|
||
const (
|
||
PVMVOpAdd uint32 = 0
|
||
PVMVOpRemove uint32 = 1
|
||
PVMVOpUpdateWeight uint32 = 2
|
||
PVMVOpJail uint32 = 3
|
||
PVMVOpUnjail uint32 = 4
|
||
PVMVOpRotateKeys uint32 = 5
|
||
|
||
PVMSOpBond uint32 = 0
|
||
PVMSOpUnbond uint32 = 1
|
||
PVMSOpDelegate uint32 = 2
|
||
PVMSOpRedelegate uint32 = 3
|
||
PVMSOpReward uint32 = 4
|
||
PVMSOpCommission uint32 = 5
|
||
|
||
PVMEvEquivocation uint32 = 0
|
||
PVMEvDowntime uint32 = 1
|
||
PVMEvInvalidVote uint32 = 2
|
||
|
||
PVMModeValidator uint32 = 0
|
||
PVMModeStake uint32 = 1
|
||
PVMModeSlashing uint32 = 2
|
||
PVMModeEpoch uint32 = 3
|
||
PVMModeFullRound uint32 = 4
|
||
|
||
PVMStatusActive uint32 = 0x1
|
||
PVMStatusJailed uint32 = 0x2
|
||
PVMStatusTombstoned uint32 = 0x4
|
||
PVMStatusPendingAdd uint32 = 0x8
|
||
PVMStatusPendingDrop uint32 = 0x10
|
||
|
||
PVMStakeStatusActive uint32 = 1
|
||
PVMStakeStatusUnbonding uint32 = 2
|
||
PVMStakeStatusRetired uint32 = 3
|
||
)
|
||
|
||
// Layout-drift guard — refuse to load if any struct size disagrees with
|
||
// the on-device layout. Any disagreement here means Go would write
|
||
// garbage at the C boundary.
|
||
func init() {
|
||
type sz struct {
|
||
name string
|
||
got uintptr
|
||
want uintptr
|
||
}
|
||
checks := []sz{
|
||
{"PVMValidatorSlot", unsafe.Sizeof(PVMValidatorSlot{}), 176},
|
||
{"PVMStakeRecord", unsafe.Sizeof(PVMStakeRecord{}), 64},
|
||
{"PVMSlashEvidence", unsafe.Sizeof(PVMSlashEvidence{}), 80},
|
||
{"PVMEpochState", unsafe.Sizeof(PVMEpochState{}), 160},
|
||
{"PVMRoundDescriptor", unsafe.Sizeof(PVMRoundDescriptor{}), 96},
|
||
{"PVMValidatorOp", unsafe.Sizeof(PVMValidatorOp{}), 176},
|
||
{"PVMStakeOp", unsafe.Sizeof(PVMStakeOp{}), 64},
|
||
{"PVMTransitionResult", unsafe.Sizeof(PVMTransitionResult{}), 192},
|
||
}
|
||
for _, c := range checks {
|
||
if c.got != c.want {
|
||
panic(fmt.Sprintf(
|
||
"platformvm: layout drift — Go sizeof(%s)=%d but on-device layout=%d. "+
|
||
"Re-sync vms/platformvm/platformvm_gpu.go against "+
|
||
"~/work/lux-private/gpu-kernels/ops/platformvm/cuda/platformvm_kernels_common.cuh.",
|
||
c.name, c.got, c.want))
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// GPUBackend — handle to an open plugin DSO + its four resolved launchers.
|
||
// =============================================================================
|
||
|
||
// GPUBackend is a handle to an open lux-gpu-kernels plugin. Zero value is
|
||
// usable (every method returns ErrGPUNotAvailable). The active backend is
|
||
// stored at package level by backend.go's init(); call ActiveGPUBackend()
|
||
// to retrieve it.
|
||
type GPUBackend struct {
|
||
mu sync.Mutex
|
||
kind GPUBackendKind
|
||
handle unsafe.Pointer // dlopen result
|
||
path string
|
||
fnValidatorSet unsafe.Pointer
|
||
fnStake unsafe.Pointer
|
||
fnSlashing unsafe.Pointer
|
||
fnEpoch unsafe.Pointer
|
||
}
|
||
|
||
// Kind returns which backend satisfied the dlopen probe.
|
||
func (b *GPUBackend) Kind() GPUBackendKind {
|
||
if b == nil {
|
||
return GPUNone
|
||
}
|
||
return b.kind
|
||
}
|
||
|
||
// Path returns the absolute path of the loaded plugin DSO.
|
||
func (b *GPUBackend) Path() string {
|
||
if b == nil {
|
||
return ""
|
||
}
|
||
return b.path
|
||
}
|
||
|
||
// IsAvailable reports whether the backend is loaded AND all four host
|
||
// launchers were successfully resolved.
|
||
func (b *GPUBackend) IsAvailable() bool {
|
||
if b == nil || b.handle == nil {
|
||
return false
|
||
}
|
||
return b.fnValidatorSet != nil && b.fnStake != nil &&
|
||
b.fnSlashing != nil && b.fnEpoch != nil
|
||
}
|
||
|
||
// openGPUBackend attempts to dlopen `path` and dlsym the four host launchers
|
||
// for `kind`. Returns a fully-initialised *GPUBackend on success, or
|
||
// (nil, error) when either the dlopen or any dlsym fails.
|
||
//
|
||
// On dlsym failure the dlopened handle IS dlclose'd before returning so
|
||
// we never leak a half-bound plugin.
|
||
func openGPUBackend(kind GPUBackendKind, path string) (*GPUBackend, error) {
|
||
cpath := C.CString(path)
|
||
defer C.free(unsafe.Pointer(cpath))
|
||
|
||
// Clear any pending error so a stale dlerror() from a previous failed
|
||
// dlsym doesn't get mis-attributed to this dlopen call.
|
||
C.lux_pvm_dlerror()
|
||
|
||
handle := C.lux_pvm_dlopen(cpath)
|
||
if handle == nil {
|
||
return nil, fmt.Errorf("platformvm: dlopen(%s): %s",
|
||
path, C.GoString(C.lux_pvm_dlerror()))
|
||
}
|
||
|
||
backendName := kind.String() // cuda / hip / metal / vulkan / webgpu
|
||
|
||
resolve := func(suffix string) (unsafe.Pointer, error) {
|
||
sym := fmt.Sprintf("lux_%s_platformvm_%s", backendName, suffix)
|
||
csym := C.CString(sym)
|
||
defer C.free(unsafe.Pointer(csym))
|
||
C.lux_pvm_dlerror()
|
||
ptr := C.lux_pvm_dlsym(handle, csym)
|
||
if ptr == nil {
|
||
return nil, fmt.Errorf("platformvm: dlsym(%s, %s): %s",
|
||
path, sym, C.GoString(C.lux_pvm_dlerror()))
|
||
}
|
||
return ptr, nil
|
||
}
|
||
|
||
b := &GPUBackend{kind: kind, handle: handle, path: path}
|
||
var err error
|
||
if b.fnValidatorSet, err = resolve("validator_set_apply"); err != nil {
|
||
C.lux_pvm_dlclose(handle)
|
||
return nil, err
|
||
}
|
||
if b.fnStake, err = resolve("stake_transition"); err != nil {
|
||
C.lux_pvm_dlclose(handle)
|
||
return nil, err
|
||
}
|
||
if b.fnSlashing, err = resolve("slashing_transition"); err != nil {
|
||
C.lux_pvm_dlclose(handle)
|
||
return nil, err
|
||
}
|
||
if b.fnEpoch, err = resolve("epoch_transition"); err != nil {
|
||
C.lux_pvm_dlclose(handle)
|
||
return nil, err
|
||
}
|
||
return b, nil
|
||
}
|
||
|
||
// Close releases the dlopen handle. Idempotent — safe to call on a nil
|
||
// receiver or an already-closed backend.
|
||
func (b *GPUBackend) Close() error {
|
||
if b == nil {
|
||
return nil
|
||
}
|
||
b.mu.Lock()
|
||
defer b.mu.Unlock()
|
||
if b.handle == nil {
|
||
return nil
|
||
}
|
||
C.lux_pvm_dlclose(b.handle)
|
||
b.handle = nil
|
||
b.fnValidatorSet = nil
|
||
b.fnStake = nil
|
||
b.fnSlashing = nil
|
||
b.fnEpoch = nil
|
||
return nil
|
||
}
|
||
|
||
// =============================================================================
|
||
// Four host launcher wrappers. Each is a thin cgo trampoline that pins the
|
||
// Go-side slice memory (via runtime.KeepAlive) for the duration of the C
|
||
// call. The launchers ALWAYS take HOST pointers — no D2H/H2D contract on
|
||
// the Go side beyond a defer'd KeepAlive on every input/output buffer.
|
||
//
|
||
// Error contract: any nonzero return code from the launcher → wrap in a Go
|
||
// error. The vm.go opt-in helpers fall back to the Go path on error
|
||
// WITHOUT panic and the caller is expected to emit a warn log.
|
||
// =============================================================================
|
||
|
||
// ValidatorSetApply runs the GPU validator-set-apply kernel. `validators` is
|
||
// read+written in place; `appliedOut` receives the count of successfully
|
||
// applied ops.
|
||
func (b *GPUBackend) ValidatorSetApply(
|
||
desc *PVMRoundDescriptor,
|
||
ops []PVMValidatorOp,
|
||
validators []PVMValidatorSlot,
|
||
appliedOut *uint32,
|
||
) error {
|
||
if !b.IsAvailable() {
|
||
return ErrGPUNotAvailable
|
||
}
|
||
if desc == nil || appliedOut == nil {
|
||
return errors.New("platformvm: ValidatorSetApply: nil desc or appliedOut")
|
||
}
|
||
if len(validators) == 0 {
|
||
return errors.New("platformvm: ValidatorSetApply: empty validators table")
|
||
}
|
||
|
||
var opsPtr unsafe.Pointer
|
||
if len(ops) > 0 {
|
||
opsPtr = unsafe.Pointer(&ops[0])
|
||
}
|
||
rc := C.call_pvm_validator_set(
|
||
b.fnValidatorSet,
|
||
unsafe.Pointer(desc),
|
||
opsPtr,
|
||
unsafe.Pointer(&validators[0]),
|
||
unsafe.Pointer(appliedOut),
|
||
C.uint32_t(len(validators)),
|
||
)
|
||
runtime.KeepAlive(desc)
|
||
runtime.KeepAlive(ops)
|
||
runtime.KeepAlive(validators)
|
||
runtime.KeepAlive(appliedOut)
|
||
if rc != 0 {
|
||
return fmt.Errorf("platformvm: %s_platformvm_validator_set_apply returned %d",
|
||
b.kind, int(rc))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// StakeTransition runs the GPU stake-transition kernel. Reads + writes
|
||
// `validators` and `stake` in place; `appliedOut` receives the count.
|
||
func (b *GPUBackend) StakeTransition(
|
||
desc *PVMRoundDescriptor,
|
||
ops []PVMStakeOp,
|
||
validators []PVMValidatorSlot,
|
||
stake []PVMStakeRecord,
|
||
appliedOut *uint32,
|
||
) error {
|
||
if !b.IsAvailable() {
|
||
return ErrGPUNotAvailable
|
||
}
|
||
if desc == nil || appliedOut == nil {
|
||
return errors.New("platformvm: StakeTransition: nil desc or appliedOut")
|
||
}
|
||
if len(validators) == 0 {
|
||
return errors.New("platformvm: StakeTransition: empty validators table")
|
||
}
|
||
if len(stake) == 0 {
|
||
return errors.New("platformvm: StakeTransition: empty stake table")
|
||
}
|
||
|
||
var opsPtr unsafe.Pointer
|
||
if len(ops) > 0 {
|
||
opsPtr = unsafe.Pointer(&ops[0])
|
||
}
|
||
rc := C.call_pvm_stake(
|
||
b.fnStake,
|
||
unsafe.Pointer(desc),
|
||
opsPtr,
|
||
unsafe.Pointer(&validators[0]),
|
||
unsafe.Pointer(&stake[0]),
|
||
unsafe.Pointer(appliedOut),
|
||
C.uint32_t(len(validators)),
|
||
C.uint32_t(len(stake)),
|
||
)
|
||
runtime.KeepAlive(desc)
|
||
runtime.KeepAlive(ops)
|
||
runtime.KeepAlive(validators)
|
||
runtime.KeepAlive(stake)
|
||
runtime.KeepAlive(appliedOut)
|
||
if rc != 0 {
|
||
return fmt.Errorf("platformvm: %s_platformvm_stake_transition returned %d",
|
||
b.kind, int(rc))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SlashingTransition runs the GPU slashing-transition kernel. Reads +
|
||
// writes `validators` and `slashing` in place; `appliedOut` receives the
|
||
// count, and `totalLoOut`/`totalHiOut` receive the low/high u32 halves of
|
||
// the total slashed amount (u64).
|
||
func (b *GPUBackend) SlashingTransition(
|
||
desc *PVMRoundDescriptor,
|
||
evidence []PVMSlashEvidence,
|
||
validators []PVMValidatorSlot,
|
||
slashing []PVMSlashEvidence,
|
||
appliedOut, totalLoOut, totalHiOut *uint32,
|
||
) error {
|
||
if !b.IsAvailable() {
|
||
return ErrGPUNotAvailable
|
||
}
|
||
if desc == nil || appliedOut == nil || totalLoOut == nil || totalHiOut == nil {
|
||
return errors.New("platformvm: SlashingTransition: nil desc or output pointer")
|
||
}
|
||
if len(validators) == 0 {
|
||
return errors.New("platformvm: SlashingTransition: empty validators table")
|
||
}
|
||
|
||
var evidencePtr, slashingPtr unsafe.Pointer
|
||
if len(evidence) > 0 {
|
||
evidencePtr = unsafe.Pointer(&evidence[0])
|
||
}
|
||
if len(slashing) > 0 {
|
||
slashingPtr = unsafe.Pointer(&slashing[0])
|
||
}
|
||
rc := C.call_pvm_slashing(
|
||
b.fnSlashing,
|
||
unsafe.Pointer(desc),
|
||
evidencePtr,
|
||
unsafe.Pointer(&validators[0]),
|
||
slashingPtr,
|
||
unsafe.Pointer(appliedOut),
|
||
unsafe.Pointer(totalLoOut),
|
||
unsafe.Pointer(totalHiOut),
|
||
C.uint32_t(len(validators)),
|
||
C.uint32_t(len(slashing)),
|
||
)
|
||
runtime.KeepAlive(desc)
|
||
runtime.KeepAlive(evidence)
|
||
runtime.KeepAlive(validators)
|
||
runtime.KeepAlive(slashing)
|
||
runtime.KeepAlive(appliedOut)
|
||
runtime.KeepAlive(totalLoOut)
|
||
runtime.KeepAlive(totalHiOut)
|
||
if rc != 0 {
|
||
return fmt.Errorf("platformvm: %s_platformvm_slashing_transition returned %d",
|
||
b.kind, int(rc))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// EpochTransition runs the GPU epoch-finalisation kernel. Composes the
|
||
// per-epoch validator_set_root / stake_root / slashing_root / epoch_root
|
||
// (which doubles as pchain_validator_root for Quasar binding).
|
||
//
|
||
// `leafScratch` is an optional byte arena the kernel uses for keccak leaf
|
||
// hashes. If nil or empty the kernel allocates internally. Otherwise it
|
||
// must be sized to at least 32 × max(validator_count, stake_count,
|
||
// slashing_count) bytes.
|
||
func (b *GPUBackend) EpochTransition(
|
||
desc *PVMRoundDescriptor,
|
||
validators []PVMValidatorSlot,
|
||
stake []PVMStakeRecord,
|
||
slashing []PVMSlashEvidence,
|
||
epoch *PVMEpochState,
|
||
result *PVMTransitionResult,
|
||
leafScratch []byte,
|
||
) error {
|
||
if !b.IsAvailable() {
|
||
return ErrGPUNotAvailable
|
||
}
|
||
if desc == nil || epoch == nil || result == nil {
|
||
return errors.New("platformvm: EpochTransition: nil desc, epoch, or result")
|
||
}
|
||
if len(validators) == 0 {
|
||
return errors.New("platformvm: EpochTransition: empty validators table")
|
||
}
|
||
|
||
var stakePtr, slashingPtr, scratchPtr unsafe.Pointer
|
||
if len(stake) > 0 {
|
||
stakePtr = unsafe.Pointer(&stake[0])
|
||
}
|
||
if len(slashing) > 0 {
|
||
slashingPtr = unsafe.Pointer(&slashing[0])
|
||
}
|
||
if len(leafScratch) > 0 {
|
||
scratchPtr = unsafe.Pointer(&leafScratch[0])
|
||
}
|
||
rc := C.call_pvm_epoch(
|
||
b.fnEpoch,
|
||
unsafe.Pointer(desc),
|
||
unsafe.Pointer(&validators[0]),
|
||
stakePtr,
|
||
slashingPtr,
|
||
unsafe.Pointer(epoch),
|
||
unsafe.Pointer(result),
|
||
C.uint32_t(len(validators)),
|
||
C.uint32_t(len(stake)),
|
||
C.uint32_t(len(slashing)),
|
||
scratchPtr,
|
||
)
|
||
runtime.KeepAlive(desc)
|
||
runtime.KeepAlive(validators)
|
||
runtime.KeepAlive(stake)
|
||
runtime.KeepAlive(slashing)
|
||
runtime.KeepAlive(epoch)
|
||
runtime.KeepAlive(result)
|
||
runtime.KeepAlive(leafScratch)
|
||
if rc != 0 {
|
||
return fmt.Errorf("platformvm: %s_platformvm_epoch_transition returned %d",
|
||
b.kind, int(rc))
|
||
}
|
||
return nil
|
||
}
|