Files
chains/aivm/aivm_gpu_test.go
zeekay eb02967a9a aivm: one staging layer, one commit, one chain
A-Chain kept the tip only in memory, staged every pending block's writes
in one shared versiondb, and accepted a block by committing that layer
before storing the block. So a restart came back at genesis, accepting A
published B's intent, rejecting B discarded A's, and a failed store left
the state durable and the block gone.

Verify and BuildBlock now decide against an overlay over committed state
and throw it away, so a block that is proposed or checked moves nothing.
Accept stages the transition, the block, its height entry and the tip
through one view and commits them once. Custody moves into engine state,
so balances commit with the stake records that depend on them instead of
living beside them in memory.

Verify also gains the lineage, time and size checks it never had: the
parent must be the tip or a block in flight above it, the height must
follow, the timestamp must sit between the parent's and the local clock
plus a skew allowance, and the encoding must be one a peer will accept.
Accept re-checks the tip under the lock that commits, because it moves
between the two calls.

A block is now named under its chain id, which never travels, so the same
bytes name different blocks on different chains. Nested payloads must
account for their blob exactly, id lists must be whole ids, and an
element the encoder cannot write is reported rather than dropped. A
merkle proof's index must be spent by the fold, so an out-of-range index
no longer verifies on its low bits.

Also: the running flag is read under the lock that writes it, a vertex no
longer moves the block chain's head, and eligibleSet says what its quorum
is a quorum of.

The ten probe files that proved all this are converted to regression
tests, not deleted.
2026-08-28 15:03:32 -07:00

95 lines
3.7 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build cgo
package aivm
import (
"testing"
"unsafe"
)
// TestLayoutSizes pins the Go struct sizes against the on-device layout in
// ops/aivm/cuda/aivm_kernels_common.cuh. A failure here means Go would
// write garbage at the cgo boundary — every kernel reads these structs via
// reinterpret_cast.
//
// The init() in aivm_gpu.go already panics on drift, but a Test in test
// mode catches it via `go test` instead of a process-start crash and
// reports which struct drifted.
func TestLayoutSizes(t *testing.T) {
cases := []struct {
name string
got uintptr
want uintptr
}{
{"Attestation", unsafe.Sizeof(Attestation{}), 144},
{"ModelRegistryEntry", unsafe.Sizeof(ModelRegistryEntry{}), 160},
{"AuditAnchor", unsafe.Sizeof(AuditAnchor{}), 128},
{"AIVMEpochState", unsafe.Sizeof(AIVMEpochState{}), 160},
{"AIVMRoundDescriptor", unsafe.Sizeof(AIVMRoundDescriptor{}), 96},
{"AttestationOp", unsafe.Sizeof(AttestationOp{}), 144},
{"ModelOp", unsafe.Sizeof(ModelOp{}), 160},
{"AnchorOp", unsafe.Sizeof(AnchorOp{}), 128},
{"AIVMTransitionResult", unsafe.Sizeof(AIVMTransitionResult{}), 192},
{"InferenceWeights", unsafe.Sizeof(InferenceWeights{}), 672},
{"InferenceOp", unsafe.Sizeof(InferenceOp{}), 144},
{"InferenceResult", unsafe.Sizeof(InferenceResult{}), 112},
{"ProofVerifyOp", unsafe.Sizeof(ProofVerifyOp{}), 240},
{"ProofVerifyResult", unsafe.Sizeof(ProofVerifyResult{}), 48},
}
for _, c := range cases {
if c.got != c.want {
t.Errorf("sizeof(%s) = %d, want %d (layout drift vs ops/aivm/cuda/aivm_kernels_common.cuh)",
c.name, c.got, c.want)
}
}
}
// The two round-trip tests that used to live here probed the host for a vendor
// plugin DSO and SKIPPED when none was found — which was every machine without
// CUDA, so they never ran and never could have failed. What they were reaching
// for, the dlopen/dlsym/launcher round trip, is now driven in gpu_plugin_test.go
// against a mirror plugin compiled at test time, so it runs everywhere. What
// they additionally asserted — that a real kernel leaves a zero-op table
// untouched — is kernel arithmetic, which no mirror can stand in for and which
// belongs with the kernel.
// TestNoPluginFallback ensures the package degrades cleanly when no plugin
// is reachable. We deliberately probe a path that can't exist and assert
// openGPUBackend returns an error (not a panic, not a zero-handle
// success). The active backend stays whatever init() resolved.
func TestNoPluginFallback(t *testing.T) {
_, err := openGPUBackend(AvailableMetal, "/nonexistent/aivm-no-such-plugin.dylib")
if err == nil {
t.Fatalf("openGPUBackend on missing plugin: expected error, got nil")
}
}
// TestModeAPI pins the public SetBackend / ActiveMode surface used by the
// chain bootstrap to opt the AIVM transition into GPU mode.
func TestModeAPI(t *testing.T) {
// Snapshot + restore the package-level mode so we don't leak state to
// other tests that may run in the same binary.
prev := ActiveMode()
defer SetBackend(prev)
SetBackend(CPUAIVM)
if got := ActiveMode(); got != CPUAIVM {
t.Errorf("ActiveMode after SetBackend(CPUAIVM) = %s, want %s", got, CPUAIVM)
}
if got := EffectiveBackendKind(); got != AvailableNone {
t.Errorf("EffectiveBackendKind under CPUAIVM = %s, want none", got)
}
SetBackend(AutoAIVM)
if got := ActiveMode(); got != AutoAIVM {
t.Errorf("ActiveMode after SetBackend(AutoAIVM) = %s, want %s", got, AutoAIVM)
}
// EffectiveBackendKind under AutoAIVM is whatever the dlopen probe
// resolved at init(). We don't pin a specific value — the test only
// asserts the API call doesn't panic and returns a valid BackendKind.
_ = EffectiveBackendKind().String()
}