mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
cpp/one-way-state-ids moved four things out of the two chains that each had their own: the fixed-width names and the one hash that derives them, the mempool, the durable store, and the codec. Three of those merge as written. The fourth cannot, because the branch and the trunk answer the same question differently, and only one of the two answers is allowed here. The branch's answer was a hand-written codec at lux/core/zap.hpp — one copy instead of four, which was the right direction from where it started. The trunk's answer arrived after it: the wire comes out of the generator, and the runtime it calls is the published zap package every chain here already links. A hand-written copy under core would be the fifth one, and the one nothing regenerates. So core links zap::zap and the store writes its rows with the same reader a chain parses a block with. The two files that still named the private header — the store itself and the P-chain's persistence — read the published one now; parse returns what it found rather than filling in an out parameter, and a list says size() where the copy said len(). What the ported tests caught, and a differential could not. The P-chain asks whether a CreateChainTx names a VM and whether a TransformChainTx names an asset, and it asked by calling .empty() on the id. main's Id was a struct whose .empty() looked at the bytes. The node's Id is a std::array, whose empty() is a compile-time false — so both refusals silently stopped firing, and no vector in the corpus has an empty VM id to notice. They compare against kEmptyId now, which is how the branch already wrote every other one. This is the whole argument for porting a reference's tests rather than only its code. The rest of the seam follows from the same move: `id_from_hash(sha256(x))` is `sha256(x)` when the hash already returns a name, the P-chain's private sha256 is gone because core carries the hashes a name is made of, and the X-chain's pool keeps drop reasons in the gossip layer that owns them rather than in the structural pool, which knows nothing about a chain and so cannot have a reason. Verified: C++ P-chain 23/23 and X-chain 19/19, both including the durability proof that has the kernel SIGKILL the writer and demands the same root back.
313 lines
12 KiB
C++
313 lines
12 KiB
C++
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
//
|
|
// block_test.cpp — the block wire and its id, against the Go reference.
|
|
//
|
|
// Ported from Go vms/platformvm/block: blockwire_test.go (the four kinds round
|
|
// trip), block_determinism_test.go (one block, one id), dispatch_test.go (the
|
|
// visitor reaches the right arm) and parse.go's refusals. Every expected buffer
|
|
// and every expected id came out of the Go package itself.
|
|
|
|
#include "golden.hpp"
|
|
#include "harness.hpp"
|
|
#include "lux/platformvm/block.hpp"
|
|
|
|
#include <string>
|
|
|
|
using namespace lux::platformvm;
|
|
namespace blk = lux::platformvm::block;
|
|
|
|
namespace {
|
|
|
|
Id id_of(std::uint8_t b) {
|
|
Id v{};
|
|
for (std::size_t i = 0; i < kIdLen; ++i) v[i] = static_cast<std::uint8_t>(b + i);
|
|
return v;
|
|
}
|
|
ShortId short_of(std::uint8_t b) {
|
|
ShortId v{};
|
|
for (std::size_t i = 0; i < kShortIdLen; ++i) v[i] = static_cast<std::uint8_t>(b + i);
|
|
return v;
|
|
}
|
|
|
|
// The same envelope goldgen used, so the txs inside these blocks are the very
|
|
// buffers the transaction suite already pinned.
|
|
BaseTx base_fixture() {
|
|
const Id asset = id_of(0x10);
|
|
BaseTx b;
|
|
b.network_id = 96369;
|
|
b.blockchain_id = id_of(0x20);
|
|
b.outs = {
|
|
TransferableOutput{asset, 0,
|
|
TransferOutput{1'000'000, OutputOwners{7, 2, {short_of(0x30), short_of(0x40), short_of(0x50)}}}},
|
|
TransferableOutput{asset, 999, TransferOutput{42, OutputOwners{0, 1, {short_of(0x30)}}}},
|
|
};
|
|
b.ins = {TransferableInput{UtxoId{id_of(0x60), 1}, asset, 999, TransferInput{1'000'042, {0, 1}}}};
|
|
const std::string memo = "native-zap";
|
|
b.memo.assign(memo.begin(), memo.end());
|
|
return b;
|
|
}
|
|
|
|
txs::Tx sign_free(std::shared_ptr<txs::UnsignedTx> u) {
|
|
txs::Tx t;
|
|
t.unsigned_tx = std::move(u);
|
|
(void)t.initialize();
|
|
return t;
|
|
}
|
|
|
|
txs::Tx base_tx_fixture() {
|
|
auto u = txs::BaseTxUnsigned::create(base_fixture());
|
|
return sign_free(u.value());
|
|
}
|
|
|
|
txs::Tx import_tx_fixture() {
|
|
std::vector<TransferableInput> imported = {
|
|
TransferableInput{UtxoId{id_of(0x80), 3}, id_of(0x10), 0, TransferInput{500, {0}}}};
|
|
auto u = txs::ImportTx::create(base_fixture(), id_of(0x70), imported);
|
|
return sign_free(u.value());
|
|
}
|
|
|
|
txs::Tx reward_tx_fixture() { return sign_free(txs::RewardValidatorTx::create(id_of(0x7b))); }
|
|
|
|
constexpr std::uint64_t kTs = 1'600'000'000;
|
|
|
|
} // namespace
|
|
|
|
// Go: block.NewAbortBlock — bytes and id.
|
|
TEST(AbortBlockWire) {
|
|
auto b = blk::AbortBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(std::string(pvmgold::abort_block), hex(b.value()->bytes()));
|
|
REQUIRE_EQ(std::string(pvmgold::abort_block_id), hex(b.value()->id()));
|
|
REQUIRE_EQ(id_of(0x20), b.value()->parent());
|
|
REQUIRE_U64(7u, b.value()->height());
|
|
REQUIRE_U64(kTs, b.value()->timestamp());
|
|
REQUIRE(b.value()->decision_txs().empty());
|
|
}
|
|
|
|
// Go: block.NewCommitBlock. Abort and commit differ in ONE byte, and therefore
|
|
// in their whole id — which is the point: the two outcomes of one proposal must
|
|
// never be confusable.
|
|
TEST(CommitBlockWire) {
|
|
auto b = blk::CommitBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(std::string(pvmgold::commit_block), hex(b.value()->bytes()));
|
|
REQUIRE_EQ(std::string(pvmgold::commit_block_id), hex(b.value()->id()));
|
|
|
|
auto a = blk::AbortBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(a);
|
|
REQUIRE(!(a.value()->id() == b.value()->id()));
|
|
}
|
|
|
|
// Go: block.NewStandardBlock with two decision txs.
|
|
TEST(StandardBlockWire) {
|
|
const std::vector<txs::Tx> decisions = {base_tx_fixture(), import_tx_fixture()};
|
|
auto b = blk::StandardBlock::create(kTs, id_of(0x20), 8, decisions);
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(std::string(pvmgold::standard_block), hex(b.value()->bytes()));
|
|
REQUIRE_EQ(std::string(pvmgold::standard_block_id), hex(b.value()->id()));
|
|
|
|
const auto back = b.value()->decision_txs();
|
|
REQUIRE_EQ_NUM(2, back.size());
|
|
REQUIRE_EQ(hex(decisions[0].bytes), hex(back[0].bytes));
|
|
REQUIRE_EQ(hex(decisions[1].bytes), hex(back[1].bytes));
|
|
REQUIRE_EQ(decisions[0].tx_id, back[0].tx_id);
|
|
REQUIRE_EQ(decisions[1].tx_id, back[1].tx_id);
|
|
}
|
|
|
|
TEST(StandardBlockEmptyWire) {
|
|
auto b = blk::StandardBlock::create(kTs, id_of(0x20), 9, {});
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(std::string(pvmgold::standard_block_empty), hex(b.value()->bytes()));
|
|
REQUIRE(b.value()->decision_txs().empty());
|
|
}
|
|
|
|
// Go: block.NewProposalBlock — the proposal tx plus a tail of decision txs.
|
|
TEST(ProposalBlockWire) {
|
|
const std::vector<txs::Tx> decisions = {base_tx_fixture()};
|
|
const auto proposal = reward_tx_fixture();
|
|
auto b = blk::ProposalBlock::create(kTs, id_of(0x20), 10, proposal, decisions);
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(std::string(pvmgold::proposal_block), hex(b.value()->bytes()));
|
|
REQUIRE_EQ(std::string(pvmgold::proposal_block_id), hex(b.value()->id()));
|
|
|
|
auto tx = b.value()->tx();
|
|
REQUIRE_OK(tx);
|
|
REQUIRE_EQ(proposal.tx_id, tx.value().tx_id);
|
|
REQUIRE(tx.value().unsigned_tx->kind() == txs::Kind::RewardValidator);
|
|
|
|
const auto back = b.value()->decision_txs();
|
|
REQUIRE_EQ_NUM(1, back.size());
|
|
REQUIRE_EQ(decisions[0].tx_id, back[0].tx_id);
|
|
}
|
|
|
|
TEST(ProposalBlockNoDecisionsWire) {
|
|
auto b = blk::ProposalBlock::create(kTs, id_of(0x20), 11, reward_tx_fixture(), {});
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(std::string(pvmgold::proposal_block_no_decisions), hex(b.value()->bytes()));
|
|
REQUIRE(b.value()->decision_txs().empty());
|
|
REQUIRE_OK(b.value()->tx());
|
|
}
|
|
|
|
// Go: block.Parse — the dispatch returns the concrete type, and the id survives
|
|
// the round trip because the bytes were never re-encoded.
|
|
TEST(ParseRoundTripsEveryKind) {
|
|
const std::vector<txs::Tx> decisions = {base_tx_fixture(), import_tx_fixture()};
|
|
|
|
auto a = blk::AbortBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(a);
|
|
auto pa = blk::parse(a.value()->bytes());
|
|
REQUIRE_OK(pa);
|
|
REQUIRE(pa.value()->kind() == blk::Kind::Abort);
|
|
REQUIRE_EQ(a.value()->id(), pa.value()->id());
|
|
|
|
auto c = blk::CommitBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(c);
|
|
auto pc = blk::parse(c.value()->bytes());
|
|
REQUIRE_OK(pc);
|
|
REQUIRE(pc.value()->kind() == blk::Kind::Commit);
|
|
REQUIRE_EQ(c.value()->id(), pc.value()->id());
|
|
|
|
auto s = blk::StandardBlock::create(kTs, id_of(0x20), 8, decisions);
|
|
REQUIRE_OK(s);
|
|
auto ps = blk::parse(s.value()->bytes());
|
|
REQUIRE_OK(ps);
|
|
REQUIRE(ps.value()->kind() == blk::Kind::Standard);
|
|
REQUIRE_EQ(s.value()->id(), ps.value()->id());
|
|
REQUIRE_EQ_NUM(2, ps.value()->decision_txs().size());
|
|
|
|
auto p = blk::ProposalBlock::create(kTs, id_of(0x20), 10, reward_tx_fixture(), decisions);
|
|
REQUIRE_OK(p);
|
|
auto pp = blk::parse(p.value()->bytes());
|
|
REQUIRE_OK(pp);
|
|
REQUIRE(pp.value()->kind() == blk::Kind::Proposal);
|
|
REQUIRE_EQ(p.value()->id(), pp.value()->id());
|
|
REQUIRE_EQ_NUM(2, pp.value()->decision_txs().size());
|
|
}
|
|
|
|
// Go: block_determinism_test — the same fields build the same bytes and the
|
|
// same id, every time.
|
|
TEST(BuildIsDeterministic) {
|
|
const std::vector<txs::Tx> decisions = {base_tx_fixture(), import_tx_fixture()};
|
|
auto a = blk::StandardBlock::create(kTs, id_of(0x20), 8, decisions);
|
|
auto b = blk::StandardBlock::create(kTs, id_of(0x20), 8, decisions);
|
|
REQUIRE_OK(a);
|
|
REQUIRE_OK(b);
|
|
REQUIRE_EQ(hex(a.value()->bytes()), hex(b.value()->bytes()));
|
|
REQUIRE_EQ(a.value()->id(), b.value()->id());
|
|
}
|
|
|
|
// Go: block.ErrExtraSpace — a trailing byte changes the id while wrapping the
|
|
// same message, so it is refused rather than silently accepted.
|
|
TEST(ParseRejectsTrailingBytes) {
|
|
auto a = blk::AbortBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(a);
|
|
std::vector<std::uint8_t> b(a.value()->bytes().begin(), a.value()->bytes().end());
|
|
b.push_back(0);
|
|
REQUIRE_ERR(blk::parse(b), Err::BlockExtraSpace);
|
|
}
|
|
|
|
// Go: block.Parse default arm — slot 0 and any unnamed slot are refused.
|
|
TEST(ParseRejectsUnknownKind) {
|
|
auto a = blk::AbortBlock::create(kTs, id_of(0x20), 7);
|
|
REQUIRE_OK(a);
|
|
std::vector<std::uint8_t> b(a.value()->bytes().begin(), a.value()->bytes().end());
|
|
const std::uint32_t root = static_cast<std::uint32_t>(b[8]) | (static_cast<std::uint32_t>(b[9]) << 8) |
|
|
(static_cast<std::uint32_t>(b[10]) << 16) |
|
|
(static_cast<std::uint32_t>(b[11]) << 24);
|
|
b[root] = 0;
|
|
REQUIRE_ERR(blk::parse(b), Err::UnknownBlockKind);
|
|
b[root] = 9;
|
|
REQUIRE_ERR(blk::parse(b), Err::UnknownBlockKind);
|
|
}
|
|
|
|
// Go: block.errNoProposalTx — a proposal block without a proposal is refused
|
|
// where it enters, not where it is dereferenced.
|
|
TEST(ProposalBlockWithoutProposalIsRefused) {
|
|
// Built: a standard block's buffer relabelled as a proposal leaves the
|
|
// proposal slot outside the object, so it reads back empty.
|
|
auto s = blk::StandardBlock::create(kTs, id_of(0x20), 8, {base_tx_fixture()});
|
|
REQUIRE_OK(s);
|
|
std::vector<std::uint8_t> b(s.value()->bytes().begin(), s.value()->bytes().end());
|
|
const std::uint32_t root = static_cast<std::uint32_t>(b[8]) | (static_cast<std::uint32_t>(b[9]) << 8) |
|
|
(static_cast<std::uint32_t>(b[10]) << 16) |
|
|
(static_cast<std::uint32_t>(b[11]) << 24);
|
|
b[root] = static_cast<std::uint8_t>(blk::Kind::Proposal);
|
|
REQUIRE_ERR(blk::parse(b), Err::NoProposalTx);
|
|
|
|
// And a block that carries no proposal tx cannot be built at all.
|
|
txs::Tx empty;
|
|
REQUIRE_ERR(blk::ProposalBlock::create(kTs, id_of(0x20), 10, empty, {}), Err::NoProposalTx);
|
|
}
|
|
|
|
// Go: block.readTxList bounds check — a length that overruns the blob is a
|
|
// refusal, never a read past the end.
|
|
TEST(ParseRejectsTxLengthOverrunningBlob) {
|
|
auto s = blk::StandardBlock::create(kTs, id_of(0x20), 8, {base_tx_fixture()});
|
|
REQUIRE_OK(s);
|
|
std::vector<std::uint8_t> b(s.value()->bytes().begin(), s.value()->bytes().end());
|
|
const std::int64_t root = static_cast<std::int64_t>(
|
|
static_cast<std::uint32_t>(b[8]) | (static_cast<std::uint32_t>(b[9]) << 8) |
|
|
(static_cast<std::uint32_t>(b[10]) << 16) | (static_cast<std::uint32_t>(b[11]) << 24));
|
|
// The length list is a backward pointer from the object's tx_lengths slot.
|
|
const std::int64_t slot = root + wire::kStandardTxLengthsOff;
|
|
const std::int32_t rel = static_cast<std::int32_t>(
|
|
static_cast<std::uint32_t>(b[slot]) | (static_cast<std::uint32_t>(b[slot + 1]) << 8) |
|
|
(static_cast<std::uint32_t>(b[slot + 2]) << 16) | (static_cast<std::uint32_t>(b[slot + 3]) << 24));
|
|
const std::int64_t list = slot + rel;
|
|
b[list] = 0xff; // first tx length = huge
|
|
b[list + 1] = 0xff;
|
|
REQUIRE_ERR(blk::parse(b), Err::TxOverrunsBlob);
|
|
}
|
|
|
|
// Go: block/dispatch_test — the visitor reaches exactly one arm per kind.
|
|
namespace {
|
|
struct CountingVisitor final : blk::Visitor {
|
|
int aborts = 0, commits = 0, proposals = 0, standards = 0;
|
|
Status abort_block(const blk::AbortBlock&) override {
|
|
++aborts;
|
|
return ok();
|
|
}
|
|
Status commit_block(const blk::CommitBlock&) override {
|
|
++commits;
|
|
return ok();
|
|
}
|
|
Status proposal_block(const blk::ProposalBlock&) override {
|
|
++proposals;
|
|
return ok();
|
|
}
|
|
Status standard_block(const blk::StandardBlock&) override {
|
|
++standards;
|
|
return ok();
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST(VisitorDispatch) {
|
|
CountingVisitor v;
|
|
auto a = blk::AbortBlock::create(kTs, id_of(0x20), 7);
|
|
auto c = blk::CommitBlock::create(kTs, id_of(0x20), 7);
|
|
auto s = blk::StandardBlock::create(kTs, id_of(0x20), 8, {base_tx_fixture()});
|
|
auto p = blk::ProposalBlock::create(kTs, id_of(0x20), 10, reward_tx_fixture(), {});
|
|
REQUIRE_OK(a);
|
|
REQUIRE_OK(c);
|
|
REQUIRE_OK(s);
|
|
REQUIRE_OK(p);
|
|
REQUIRE_OK(a.value()->visit(v));
|
|
REQUIRE_OK(c.value()->visit(v));
|
|
REQUIRE_OK(s.value()->visit(v));
|
|
REQUIRE_OK(p.value()->visit(v));
|
|
REQUIRE_EQ_NUM(1, v.aborts);
|
|
REQUIRE_EQ_NUM(1, v.commits);
|
|
REQUIRE_EQ_NUM(1, v.standards);
|
|
REQUIRE_EQ_NUM(1, v.proposals);
|
|
|
|
// And through a parsed block, where the concrete type came off the wire.
|
|
CountingVisitor w;
|
|
auto pp = blk::parse(p.value()->bytes());
|
|
REQUIRE_OK(pp);
|
|
REQUIRE_OK(pp.value()->visit(w));
|
|
REQUIRE_EQ_NUM(1, w.proposals);
|
|
REQUIRE_EQ_NUM(0, w.standards);
|
|
}
|