mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
chains/cpp/core is what every C++ chain here shares and shares ONE of: the fixed-width names and the hashes that derive them, ZAP, and the durable byte-keyed map the state rests on. The store is the X-Chain's, moved unchanged — it is the only one in the matrix that fsyncs, replays a torn tail and rewrites its log. The names are the two chains' vocabularies reconciled: Id IS lux::consensus::Id, so a block id crosses the seam with no conversion; NodeId stays a type of its own because it shares a width with an address and the compiler should refuse that swap. The X-Chain now declares no id, no codec and no store of its own. Its suite passes unchanged, which is the claim that matters: the wire golden vectors still hold, so the codec that moved is the codec that was there. And the store is proven the only way that counts. core_durability starts a second process, has it commit, and has the kernel SIGKILL it — no destructor, no flush. A third process reopens the file and must find every committed row byte for byte, nothing written after the last commit, and the same root folded over the set. A store that only survives a clean shutdown is a cache with good manners.
157 lines
6.0 KiB
C++
157 lines
6.0 KiB
C++
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
//
|
|
// root_test.cpp — the execution root, against the cross-language KAT.
|
|
//
|
|
// A port of state/xvmroot/xvmroot_test.go, whose fixture is itself a byte-for-
|
|
// byte port of the deterministic input every one of the seven GPU backends and
|
|
// the CPU oracle hash. The expected values here are therefore not this port's
|
|
// opinion — they are what Go, the CPU oracle, and every accelerator all produce,
|
|
// and a C++ node that computed anything else could not be in a quorum with any
|
|
// of them.
|
|
|
|
#include "lux/core/check.hpp"
|
|
|
|
#include "lux/xvm/root.hpp"
|
|
|
|
using namespace lux::xvm;
|
|
using namespace lux::core::test;
|
|
|
|
namespace {
|
|
|
|
constexpr std::uint8_t kKatParentByte0 = 0xEE;
|
|
constexpr std::uint64_t kKatHeight = 100;
|
|
constexpr std::uint32_t kStatusAccepted = 1;
|
|
constexpr std::uint32_t kStatusRejected = 2;
|
|
|
|
// 8 UTXO slots; 0..3 occupied, 4..7 left zero (unoccupied, and therefore
|
|
// skipped by the fold while still consuming their slot index).
|
|
std::vector<root::UTXOLeaf> kat_utxos() {
|
|
std::vector<root::UTXOLeaf> us(8);
|
|
for (int i = 0; i < 4; ++i) {
|
|
auto& u = us[std::size_t(i)];
|
|
for (int k = 0; k < 32; ++k) {
|
|
u.utxo_id[std::size_t(k)] =
|
|
std::uint8_t((std::uint32_t(i) * 11 + std::uint32_t(k)) ^ 0x71u);
|
|
u.asset_id[std::size_t(k)] = std::uint8_t(0xA0 + (k & 0xF));
|
|
u.owner_root[std::size_t(k)] = std::uint8_t(0xC0 + (k & 0xF));
|
|
}
|
|
u.amount_lo = std::uint64_t(1000 + i);
|
|
u.amount_hi = 0;
|
|
u.locktime = 0;
|
|
u.threshold = 1;
|
|
u.status = root::kUTXOOccupied;
|
|
}
|
|
return us;
|
|
}
|
|
|
|
std::vector<root::AssetLeaf> kat_assets() {
|
|
std::vector<root::AssetLeaf> as(4); // slot 3 left zero
|
|
for (int i = 0; i < 3; ++i) {
|
|
auto& a = as[std::size_t(i)];
|
|
for (int k = 0; k < 32; ++k) {
|
|
a.asset_id[std::size_t(k)] =
|
|
std::uint8_t((std::uint32_t(i) * 7 + std::uint32_t(k)) ^ 0xC3u);
|
|
a.mint_authority_root[std::size_t(k)] = std::uint8_t(0xD0 + (k & 0xF));
|
|
}
|
|
a.total_supply_lo = std::uint64_t(1000000 + i);
|
|
a.total_supply_hi = 0;
|
|
a.freeze_flag = 1;
|
|
a.denomination = 8;
|
|
a.occupied = 1;
|
|
}
|
|
return as;
|
|
}
|
|
|
|
std::vector<root::TxLeaf> kat_txs() {
|
|
std::vector<root::TxLeaf> ts(4);
|
|
for (int i = 0; i < 4; ++i) {
|
|
auto& t = ts[std::size_t(i)];
|
|
for (int k = 0; k < 32; ++k) {
|
|
t.tx_id[std::size_t(k)] = std::uint8_t(i + k);
|
|
t.proof_digest[std::size_t(k)] = std::uint8_t(i * 5 + k);
|
|
}
|
|
t.kind = std::uint32_t(i);
|
|
switch (i % 3) {
|
|
case 0:
|
|
t.status = kStatusAccepted;
|
|
break;
|
|
case 1:
|
|
t.status = kStatusRejected;
|
|
t.reject_reason = 42;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return ts;
|
|
}
|
|
|
|
root::Digest kat_parent() {
|
|
root::Digest p{};
|
|
for (std::size_t k = 0; k < p.size(); ++k) p[k] = std::uint8_t(kKatParentByte0 + k);
|
|
return p;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
std::printf("xvm — the execution root, against the cross-language KAT\n\n");
|
|
|
|
const auto utxo = root::utxo_root(kat_utxos());
|
|
const auto asset = root::asset_root(kat_assets());
|
|
const auto tx = root::tx_root(kat_txs());
|
|
const auto exec = root::compose(kat_parent(), utxo, asset, tx, kKatHeight);
|
|
|
|
// The full execution root every backend produces.
|
|
check_eq(hex_of(exec), "4f144ef76dd14d4447ccf9c746d747c21dd4a6b0945e2180bfac489f21af2d77",
|
|
"execution_root over the KAT fixture");
|
|
|
|
// The three sub-roots, by the prefixes the KAT documents.
|
|
check_eq(hex_of(utxo).substr(0, 8), "16663d25", "utxo_root prefix");
|
|
check_eq(hex_of(asset).substr(0, 8), "12f47742", "asset_root prefix");
|
|
check_eq(hex_of(tx).substr(0, 8), "c210388d", "tx_root prefix");
|
|
|
|
// compose is the FIXED-SHAPE un-tagged keccak256, not a Merkle node. A
|
|
// regression to a tagged node hash would move these bytes.
|
|
check_eq(hex_of(root::compose(kat_parent(), root::utxo_root(kat_utxos()),
|
|
root::asset_root(kat_assets()), root::tx_root(kat_txs()),
|
|
kKatHeight)),
|
|
"4f144ef76dd14d4447ccf9c746d747c21dd4a6b0945e2180bfac489f21af2d77",
|
|
"compose is un-tagged keccak256 over parent‖utxo‖asset‖tx‖height_le");
|
|
|
|
// The empty set folds to keccak256(""), NOT to zero — an all-empty family
|
|
// must be distinguishable from an absent commitment.
|
|
const std::string empty_keccak =
|
|
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
|
|
check_eq(hex_of(root::utxo_root({})), empty_keccak, "empty utxo_root is keccak256(\"\")");
|
|
check_eq(hex_of(root::asset_root({})), empty_keccak, "empty asset_root is keccak256(\"\")");
|
|
check_eq(hex_of(root::tx_root({})), empty_keccak, "empty tx_root is keccak256(\"\")");
|
|
check_eq(hex_of(root::utxo_root(std::vector<root::UTXOLeaf>(4))), empty_keccak,
|
|
"an all-unoccupied UTXO slate also folds to the empty root");
|
|
|
|
// A single leaf is its LeafHash, with no internal node above it — the
|
|
// RFC-6962 rule that keeps a one-element tree distinct from its element.
|
|
{
|
|
root::Digest d{};
|
|
d.fill(0x11);
|
|
check(root::merkle_root({d}) == root::leaf_hash(d),
|
|
"a one-leaf tree is LeafHash(d), not NodeHash(d, d)");
|
|
check(root::merkle_root({d}) != d, "…and never the bare digest");
|
|
}
|
|
|
|
// Lone-right promotion: with three leaves the odd one moves up unchanged.
|
|
{
|
|
root::Digest a{}, b{}, c{};
|
|
a.fill(1);
|
|
b.fill(2);
|
|
c.fill(3);
|
|
const auto want = root::node_hash(
|
|
root::node_hash(root::leaf_hash(a), root::leaf_hash(b)), root::leaf_hash(c));
|
|
check(root::merkle_root({a, b, c}) == want,
|
|
"a lone right node is promoted, then paired at the next level");
|
|
}
|
|
|
|
return report("root");
|
|
}
|