Files
zeekay 2cddf94e1c The P-chain speaks the node's names, and the node's one hash
platformvm had its own thirty-two-byte name wrapped in a struct, its own
twenty-byte address, and — under both of them — its own complete SHA-256.
Two implementations of the hash a transaction is NAMED by is the sharpest
version of this problem: they agree until the day they do not, and the day
they do not is a fork.

So ids.hpp now declares nothing. It adopts core's names and core's hash,
and keeps only the one thing that is a P-chain fact rather than a naming
fact: the primary network is the zero id. sha256.hpp is deleted; its FIPS
180-4 vectors moved to core, where the hash now lives, and sha256_test
became ids_test, which checks what is left to check here — that a UTXO's
name is the reference's ids.ID.Prefix.

Six identical local hex() helpers in the test files are gone too.

ONE HAZARD, NAMED, BECAUSE IT IS SILENT. FixedId::empty() meant "all
zero"; std::array::empty() means "has no elements", which is false for
thirty-two bytes and compiles without a word. Five checks were of the
first kind and would have become dead: the two empty-asset gates, the
empty-VM-id gate, and the reward transaction's own id. They now read
`== kEmptyId`, which cannot rot into a tautology. Three of the five were
caught by the suite; the reward-id one was not, and nothing tests it.

208 differential vectors still agree across Go, Rust and C++, so the names
that changed shape did not change value.
2026-09-05 15:04:34 -07:00

163 lines
5.9 KiB
C++

// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// components_test.cpp — the spending model, ported from
// github.com/luxfi/utxo (transferables_test, secp256k1fx output/input tests)
// and the platformvm's own stakeable lock tests.
//
// The fx wire envelope cases assert against bytes produced by the Go
// reference: that envelope is the SORT KEY for outputs, so getting it wrong
// would make this implementation disagree with the network about which
// transactions are well-formed, silently and only for some address lists.
#include "golden.hpp"
#include "harness.hpp"
#include "lux/platformvm/components.hpp"
#include <string>
using namespace lux::platformvm;
namespace {
ShortId short_id(std::uint8_t b) {
ShortId s{};
for (std::size_t i = 0; i < kShortIdLen; ++i) s[i] = static_cast<std::uint8_t>(b + i);
return s;
}
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;
}
} // namespace
// Go: secp256k1fx TestOutputOwnersVerify — the four refusals, in order.
TEST(OutputOwnersVerify) {
OutputOwners ok_owners{0, 1, {short_id(0x10)}};
REQUIRE_OK(ok_owners.verify());
OutputOwners unspendable{0, 2, {short_id(0x10)}};
REQUIRE_ERR(unspendable.verify(), Err::OutputUnspendable);
OutputOwners unoptimized{0, 0, {short_id(0x10)}};
REQUIRE_ERR(unoptimized.verify(), Err::OutputUnoptimized);
OutputOwners unsorted{0, 1, {short_id(0x20), short_id(0x10)}};
REQUIRE_ERR(unsorted.verify(), Err::AddrsNotSortedUnique);
OutputOwners duplicated{0, 1, {short_id(0x10), short_id(0x10)}};
REQUIRE_ERR(duplicated.verify(), Err::AddrsNotSortedUnique);
// Threshold 0 with no addresses is the canonical "burned" owner and is fine.
OutputOwners empty{0, 0, {}};
REQUIRE_OK(empty.verify());
}
// Go: secp256k1fx TestTransferOutputVerify
TEST(TransferOutputVerify) {
TransferOutput zero_value{0, {0, 1, {short_id(0x10)}}};
REQUIRE_ERR(zero_value.verify(), Err::NoValueOutput);
TransferOutput good{1, {0, 1, {short_id(0x10)}}};
REQUIRE_OK(good.verify());
}
// Go: secp256k1fx TestTransferInputVerify / TestInputVerify
TEST(TransferInputVerify) {
TransferInput zero_value{0, {0}};
REQUIRE_ERR(zero_value.verify(), Err::NoValueInput);
TransferInput unsorted{1, {1, 0}};
REQUIRE_ERR(unsorted.verify(), Err::InputIndicesNotSortedUnique);
TransferInput duplicated{1, {0, 0}};
REQUIRE_ERR(duplicated.verify(), Err::InputIndicesNotSortedUnique);
TransferInput good{1, {0, 1}};
REQUIRE_OK(good.verify());
const auto c = good.cost();
REQUIRE(c.has_value());
REQUIRE_U64(2000, *c);
}
// Go: utxo TransferableOutput.Verify — an empty asset id is not a valid asset.
TEST(TransferableOutputVerify) {
TransferableOutput no_asset{Id{}, 0, TransferOutput{1, {0, 1, {short_id(0x10)}}}};
REQUIRE_ERR(no_asset.verify(), Err::EmptyAssetID);
TransferableOutput good{id_of(0x10), 0, TransferOutput{1, {0, 1, {short_id(0x10)}}}};
REQUIRE_OK(good.verify());
}
// Go: utxo BaseTx.Verify — the three metadata refusals.
TEST(BaseTxVerify) {
const Runtime rt{96369, id_of(0x20), id_of(0x10)};
BaseTx wrong_network{1, id_of(0x20), {}, {}, {}};
REQUIRE_ERR(wrong_network.verify(rt), Err::WrongNetworkID);
BaseTx wrong_chain{96369, id_of(0x21), {}, {}, {}};
REQUIRE_ERR(wrong_chain.verify(rt), Err::WrongChainID);
BaseTx big_memo{96369, id_of(0x20), {}, {}, std::vector<std::uint8_t>(kMaxMemoSize + 1, 0)};
REQUIRE_ERR(big_memo.verify(rt), Err::MemoTooLarge);
BaseTx good{96369, id_of(0x20), {}, {}, std::vector<std::uint8_t>(kMaxMemoSize, 0)};
REQUIRE_OK(good.verify(rt));
}
// The output sort key: the inner fx wire envelope, byte-for-byte the Go
// reference's secp256k1fx.TransferOutput.Bytes / stakeable.LockOut.Bytes.
TEST(OutputWireEnvelopeMatchesGo) {
TransferableOutput plain{id_of(0x10), 0,
TransferOutput{1000000, {7, 2, {short_id(0x30), short_id(0x40), short_id(0x50)}}}};
REQUIRE(hex(plain.wire_bytes()) == pvmgold::secp_transfer_output);
TransferableOutput locked{id_of(0x10), 999, TransferOutput{42, {0, 1, {short_id(0x30)}}}};
REQUIRE(hex(locked.wire_bytes()) == pvmgold::locked_output);
TransferableOutput no_addrs{id_of(0x10), 0, TransferOutput{5, {0, 0, {}}}};
REQUIRE(hex(no_addrs.wire_bytes()) == pvmgold::secp_transfer_output_no_addrs);
}
// Ordering is consensus: outputs by (asset, envelope bytes), non-strict.
TEST(OutputOrdering) {
TransferableOutput a{id_of(0x10), 0, TransferOutput{1, {0, 1, {short_id(0x10)}}}};
TransferableOutput b{id_of(0x20), 0, TransferOutput{1, {0, 1, {short_id(0x10)}}}};
REQUIRE(outputs_sorted({a, b}));
REQUIRE(!outputs_sorted({b, a}));
// Equal outputs are a legitimate transaction, so the order is not strict.
REQUIRE(outputs_sorted({a, a}));
std::vector<TransferableOutput> v{b, a};
sort_outputs(v);
REQUIRE(v[0].asset == a.asset);
REQUIRE(v[1].asset == b.asset);
}
// Inputs are STRICTLY ordered and unique: spending one UTXO twice in a
// transaction is a double spend, not a spelling.
TEST(InputOrdering) {
TransferableInput a{{id_of(0x10), 0}, id_of(0x10), 0, {1, {0}}};
TransferableInput b{{id_of(0x10), 1}, id_of(0x10), 0, {1, {0}}};
TransferableInput c{{id_of(0x20), 0}, id_of(0x10), 0, {1, {0}}};
REQUIRE(inputs_sorted_unique({a, b, c}));
REQUIRE(!inputs_sorted_unique({b, a}));
REQUIRE(!inputs_sorted_unique({a, a}));
std::vector<TransferableInput> v{c, b, a};
sort_inputs(v);
REQUIRE(v[0].utxo == a.utxo);
REQUIRE(v[1].utxo == b.utxo);
REQUIRE(v[2].utxo == c.utxo);
}
// Post-Durango, the memo field must be empty.
TEST(MemoFieldLength) {
REQUIRE_OK(verify_memo_field_length({}));
const std::uint8_t one[1] = {0};
REQUIRE_ERR(verify_memo_field_length({one, 1}), Err::MemoTooLarge);
}