Files
pvm/test/pvm_layout_test.cpp
zeekay b0a5ca07b7 v0.52 — PVM GPU layout
Establish PVM (Platform-VM) GPU substrate scaffold for LP-137
GPU-native compliance:

  - pvm_gpu_layout.hpp     ValidatorSlot, StakeRecord/Op, SlashEvidence,
                            EpochState, PVMRoundDescriptor, ValidatorOp,
                            PVMTransitionResult — all 16-byte aligned with
                            explicit pad to fixed arena strides for
                            byte-stable host/device ABI
  - pvm_cpu_reference.cpp  Deterministic CPU oracle implementing four
                            canonical kernels: ValidatorSetApply,
                            StakeTransition, SlashingTransition,
                            EpochTransition (root composition)
  - pvm_layout_test        12 layout + reference-determinism checks; all
                            pass with byte-identical roots across runs

This closes the layout half of the P-Chain GPU-native gap. v0.53 ships
the matching CUDA + Metal transition kernels and the cross-backend
determinism harness.
2026-04-26 14:37:43 -07:00

332 lines
13 KiB
C++

// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
/// @file pvm_layout_test.cpp
/// PVM v0.52 — layout invariants for cross-backend determinism.
///
/// Validates struct sizes, alignment, member offsets, and basic CPU-reference
/// determinism on a small canonical workload. Anything that drifts in the
/// host header without a parallel update in the GPU kernels would break the
/// CPU/Metal/CUDA equivalence — these checks are the first line of defense.
#include "lux/pvm/pvm_gpu_layout.hpp"
#include "lux/pvm/pvm_cpu_reference.hpp"
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace pvm::gpu;
namespace {
int g_passed = 0;
int g_failed = 0;
#define EXPECT(name, cond) \
do { \
if (!(cond)) { \
std::printf(" FAIL[%s]: %s\n", (name), #cond); \
std::fflush(stdout); \
++g_failed; \
return; \
} \
} while (0)
#define PASS(name) \
do { \
std::printf(" ok : %s\n", (name)); \
std::fflush(stdout); \
++g_passed; \
} while (0)
void test_validator_slot_layout()
{
// 8 (id) + 8 (weight) + 48 + 32 + 32 + 32 (keys) + 4 + 4 + 4 + 4 = 176
EXPECT("ValidatorSlot.size", sizeof(ValidatorSlot) == 176);
EXPECT("ValidatorSlot.align", alignof(ValidatorSlot) == 16);
EXPECT("ValidatorSlot.id.off", offsetof(ValidatorSlot, validator_id) == 0);
EXPECT("ValidatorSlot.weight.off", offsetof(ValidatorSlot, weight) == 8);
EXPECT("ValidatorSlot.bls.off", offsetof(ValidatorSlot, bls_pubkey) == 16);
EXPECT("ValidatorSlot.ringtail.off", offsetof(ValidatorSlot, ringtail_pubkey) == 64);
EXPECT("ValidatorSlot.mldsa.off", offsetof(ValidatorSlot, mldsa_pubkey) == 96);
EXPECT("ValidatorSlot.groth16.off", offsetof(ValidatorSlot, mldsa_groth16_root) == 128);
EXPECT("ValidatorSlot.status.off", offsetof(ValidatorSlot, status) == 160);
EXPECT("ValidatorSlot.jail.off", offsetof(ValidatorSlot, jail_until_epoch) == 164);
EXPECT("ValidatorSlot.occupied.off", offsetof(ValidatorSlot, occupied) == 168);
PASS("ValidatorSlot layout");
}
void test_stake_record_layout()
{
// 8 * 5 + 4 * 4 + 8 (pad) = 64
EXPECT("StakeRecord.size", sizeof(StakeRecord) == 64);
EXPECT("StakeRecord.align", alignof(StakeRecord) == 16);
EXPECT("StakeRecord.deleg.off", offsetof(StakeRecord, delegator_id) == 0);
EXPECT("StakeRecord.val.off", offsetof(StakeRecord, validator_id) == 8);
EXPECT("StakeRecord.amount.off", offsetof(StakeRecord, amount) == 16);
EXPECT("StakeRecord.lock.off", offsetof(StakeRecord, lock_until_epoch) == 24);
EXPECT("StakeRecord.reward.off", offsetof(StakeRecord, reward_accumulator) == 32);
EXPECT("StakeRecord.commission.off", offsetof(StakeRecord, commission_bps) == 40);
EXPECT("StakeRecord.status.off", offsetof(StakeRecord, status) == 44);
EXPECT("StakeRecord.epoch_b.off", offsetof(StakeRecord, epoch_bonded) == 48);
EXPECT("StakeRecord.epoch_u.off", offsetof(StakeRecord, epoch_unbonded) == 52);
PASS("StakeRecord layout");
}
void test_stake_op_layout()
{
// 8 * 5 + 4 * 4 + 8 (pad) = 64
EXPECT("StakeOp.size", sizeof(StakeOp) == 64);
EXPECT("StakeOp.align", alignof(StakeOp) == 16);
EXPECT("StakeOp.kind.off", offsetof(StakeOp, kind) == 40);
PASS("StakeOp layout");
}
void test_slash_evidence_layout()
{
// 8*3 + 4*4 + 32 + 8 (pad) = 80
EXPECT("SlashEvidence.size", sizeof(SlashEvidence) == 80);
EXPECT("SlashEvidence.align", alignof(SlashEvidence) == 16);
EXPECT("SlashEvidence.id.off", offsetof(SlashEvidence, validator_id) == 0);
EXPECT("SlashEvidence.height.off", offsetof(SlashEvidence, height) == 8);
EXPECT("SlashEvidence.amount.off", offsetof(SlashEvidence, slash_amount) == 16);
EXPECT("SlashEvidence.kind.off", offsetof(SlashEvidence, kind) == 24);
EXPECT("SlashEvidence.epoch.off", offsetof(SlashEvidence, epoch) == 28);
EXPECT("SlashEvidence.jail.off", offsetof(SlashEvidence, jail_for_epochs) == 32);
EXPECT("SlashEvidence.digest.off", offsetof(SlashEvidence, evidence_digest) == 40);
PASS("SlashEvidence layout");
}
void test_epoch_state_layout()
{
// 8*3 + 4*2 + 32*4 = 24 + 8 + 128 = 160
EXPECT("EpochState.size", sizeof(EpochState) == 160);
EXPECT("EpochState.align", alignof(EpochState) == 16);
EXPECT("EpochState.curr.off", offsetof(EpochState, current_epoch) == 0);
EXPECT("EpochState.next.off", offsetof(EpochState, next_epoch_height) == 8);
EXPECT("EpochState.total.off", offsetof(EpochState, total_active_stake) == 16);
EXPECT("EpochState.actc.off", offsetof(EpochState, active_validator_count) == 24);
EXPECT("EpochState.pdrp.off", offsetof(EpochState, pending_drop_count) == 28);
EXPECT("EpochState.vroot.off", offsetof(EpochState, validator_set_root) == 32);
EXPECT("EpochState.sroot.off", offsetof(EpochState, stake_root) == 64);
EXPECT("EpochState.slroot.off",offsetof(EpochState, slashing_root) == 96);
EXPECT("EpochState.eroot.off", offsetof(EpochState, epoch_root) == 128);
PASS("EpochState layout");
}
void test_validator_op_layout()
{
// 8 + 8 + 48 + 32*3 + 4*4 = 16 + 48 + 96 + 16 = 176
EXPECT("ValidatorOp.size", sizeof(ValidatorOp) == 176);
EXPECT("ValidatorOp.align", alignof(ValidatorOp) == 16);
EXPECT("ValidatorOp.bls.off", offsetof(ValidatorOp, bls_pubkey) == 16);
EXPECT("ValidatorOp.kind.off", offsetof(ValidatorOp, kind) == 160);
PASS("ValidatorOp layout");
}
void test_round_descriptor_layout()
{
// 8*4 + 4*6 + 8 + 32 = 32 + 24 + 8 + 32 = 96
EXPECT("PVMRoundDescriptor.size", sizeof(PVMRoundDescriptor) == 96);
EXPECT("PVMRoundDescriptor.align", alignof(PVMRoundDescriptor) == 16);
EXPECT("Desc.mode.off", offsetof(PVMRoundDescriptor, mode) == 32);
EXPECT("Desc.parent.off",offsetof(PVMRoundDescriptor, parent_epoch_root) == 64);
PASS("PVMRoundDescriptor layout");
}
void test_transition_result_layout()
{
// 4*8 + 8*4 + 32*4 = 32 + 32 + 128 = 192
EXPECT("PVMTransitionResult.size", sizeof(PVMTransitionResult) == 192);
EXPECT("PVMTransitionResult.align", alignof(PVMTransitionResult) == 16);
EXPECT("Result.epoch_root.off", offsetof(PVMTransitionResult, epoch_root) == 160);
PASS("PVMTransitionResult layout");
}
PVMRoundDescriptor make_desc(uint64_t round, uint32_t mode = 4)
{
PVMRoundDescriptor d{};
d.chain_id = 1u;
d.round = round;
d.timestamp_ns = 1700000000000000000ULL;
d.epoch = 0u;
d.mode = mode; // FullRound default
d.closing_flag = 1u; // close epoch by default
return d;
}
ValidatorOp make_validator_add(uint64_t id, uint64_t weight, uint8_t fill = 0xAB)
{
ValidatorOp op{};
op.validator_id = id;
op.weight = weight;
op.kind = static_cast<uint32_t>(ValidatorOpKind::Add);
for (auto& b : op.bls_pubkey) b = fill;
for (auto& b : op.ringtail_pubkey) b = fill ^ 1u;
for (auto& b : op.mldsa_pubkey) b = fill ^ 2u;
for (auto& b : op.mldsa_groth16_root) b = fill ^ 3u;
return op;
}
StakeOp make_bond(uint64_t deleg, uint64_t val, uint64_t amount)
{
StakeOp op{};
op.delegator_id = deleg;
op.validator_id = val;
op.amount = amount;
op.kind = static_cast<uint32_t>(StakeOpKind::Bond);
return op;
}
void test_cpu_reference_determinism()
{
// Build a small canonical input.
std::vector<ValidatorOp> v_ops;
for (uint64_t i = 1; i <= 8; ++i)
v_ops.push_back(make_validator_add(i, 1000u * i));
std::vector<StakeOp> s_ops;
for (uint64_t d = 100; d < 116; ++d)
s_ops.push_back(make_bond(d, (d % 8u) + 1u, 500u));
std::vector<SlashEvidence> ev_ops;
SlashEvidence ev{};
ev.validator_id = 5u;
ev.height = 12345u;
ev.kind = static_cast<uint32_t>(SlashEvidenceKind::Equivocation);
ev.epoch = 0u;
for (auto& b : ev.evidence_digest) b = 0xCC;
ev_ops.push_back(ev);
auto desc = make_desc(1u, /*mode=*/4u);
// Run the reference twice and require byte-identical results.
auto state1 = ref::PVMReferenceState::empty();
auto state2 = ref::PVMReferenceState::empty();
auto r1 = ref::run_reference(state1, desc, v_ops, s_ops, ev_ops);
auto r2 = ref::run_reference(state2, desc, v_ops, s_ops, ev_ops);
EXPECT("ref.det.status", r1.status == 1u);
EXPECT("ref.det.vapply", r1.validator_apply_count == 8u);
EXPECT("ref.det.sapply", r1.stake_apply_count == 16u);
EXPECT("ref.det.slapply", r1.slash_apply_count == 1u);
EXPECT("ref.det.vroot", std::memcmp(r1.validator_set_root, r2.validator_set_root, 32) == 0);
EXPECT("ref.det.sroot", std::memcmp(r1.stake_root, r2.stake_root, 32) == 0);
EXPECT("ref.det.slroot", std::memcmp(r1.slashing_root, r2.slashing_root, 32) == 0);
EXPECT("ref.det.eroot", std::memcmp(r1.epoch_root, r2.epoch_root, 32) == 0);
EXPECT("ref.det.epoch", r1.epoch == 1u);
// epoch_root must be non-zero (some validator + stake activity).
bool any_nonzero = false;
for (auto b : r1.epoch_root) if (b != 0) { any_nonzero = true; break; }
EXPECT("ref.det.eroot.nz", any_nonzero);
// Tombstoned validator (5) must NOT count as active.
EXPECT("ref.det.active", r1.active_validator_count == 7u);
EXPECT("ref.det.tomb", r1.tombstoned_count == 1u);
PASS("CPU reference determinism");
}
void test_cpu_reference_jail_excludes()
{
std::vector<ValidatorOp> v_ops;
v_ops.push_back(make_validator_add(1u, 1000u));
v_ops.push_back(make_validator_add(2u, 2000u));
// Jail validator 1 until epoch 10.
ValidatorOp jail{};
jail.validator_id = 1u;
jail.kind = static_cast<uint32_t>(ValidatorOpKind::Jail);
jail.jail_until_epoch = 10u;
v_ops.push_back(jail);
auto desc = make_desc(1u, 4u);
auto state = ref::PVMReferenceState::empty();
auto r = ref::run_reference(state, desc, v_ops, {}, {});
EXPECT("jail.active", r.active_validator_count == 1u); // only validator 2
EXPECT("jail.jailed", r.jailed_count == 1u);
PASS("Jailed validator excluded");
}
void test_cpu_reference_redelegate_atomic()
{
std::vector<ValidatorOp> v_ops;
v_ops.push_back(make_validator_add(1u, 0u));
v_ops.push_back(make_validator_add(2u, 0u));
std::vector<StakeOp> s_ops;
// Bond 500 to v1
s_ops.push_back(make_bond(100u, 1u, 500u));
// Redelegate 300 from v1 -> v2
StakeOp rd{};
rd.delegator_id = 100u;
rd.validator_id = 2u; // dest
rd.source_validator_id = 1u; // src
rd.amount = 300u;
rd.kind = static_cast<uint32_t>(StakeOpKind::Redelegate);
s_ops.push_back(rd);
auto desc = make_desc(1u, 4u);
auto state = ref::PVMReferenceState::empty();
auto r = ref::run_reference(state, desc, v_ops, s_ops, {});
// After: v1 weight = 200, v2 weight = 300, total = 500
EXPECT("rd.active", r.active_validator_count == 2u);
EXPECT("rd.total", r.total_active_stake == 500u);
EXPECT("rd.applied",r.stake_apply_count == 2u);
PASS("Redelegate atomic");
}
void test_cpu_reference_empty_round()
{
auto desc = make_desc(1u, 4u);
auto state = ref::PVMReferenceState::empty();
auto r = ref::run_reference(state, desc, {}, {}, {});
EXPECT("empty.status", r.status == 1u);
EXPECT("empty.epoch", r.epoch == 1u);
// epoch_root for empty epoch: still deterministic, but composed over zero arenas.
auto state2 = ref::PVMReferenceState::empty();
auto r2 = ref::run_reference(state2, desc, {}, {}, {});
EXPECT("empty.eroot.det", std::memcmp(r.epoch_root, r2.epoch_root, 32) == 0);
bool any_nonzero = false;
for (auto b : r.epoch_root) if (b != 0) { any_nonzero = true; break; }
EXPECT("empty.eroot.nz", any_nonzero);
PASS("Empty round deterministic");
}
} // namespace
int main(int /*argc*/, char** /*argv*/)
{
setvbuf(stdout, nullptr, _IOLBF, 0);
std::printf("[pvm_layout_test] starting\n");
test_validator_slot_layout();
test_stake_record_layout();
test_stake_op_layout();
test_slash_evidence_layout();
test_epoch_state_layout();
test_validator_op_layout();
test_round_descriptor_layout();
test_transition_result_layout();
test_cpu_reference_determinism();
test_cpu_reference_jail_excludes();
test_cpu_reference_redelegate_atomic();
test_cpu_reference_empty_round();
std::printf("[pvm_layout_test] passed=%d failed=%d\n", g_passed, g_failed);
return g_failed == 0 ? 0 : 1;
}