mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
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.
467 lines
20 KiB
C++
467 lines
20 KiB
C++
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
//
|
|
// warp_test.cpp — a message another chain signed, and the proof enough of it did.
|
|
//
|
|
// Ported from Go vms/platformvm/warp: unsigned_message_test.go, message_test.go,
|
|
// signature_test.go and validator_test.go. The verification case is not a
|
|
// self-check: the message, the three validators, the bit vector and the
|
|
// aggregated signature all came out of the Go reference, so a port that
|
|
// aggregated differently, indexed the bits differently, or used the wrong domain
|
|
// tag fails here.
|
|
|
|
#include "golden.hpp"
|
|
#include "harness.hpp"
|
|
#include "lux/platformvm/warp.hpp"
|
|
#include "lux/platformvm/warpmsg.hpp"
|
|
|
|
#include <string>
|
|
|
|
using namespace lux::platformvm;
|
|
|
|
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;
|
|
}
|
|
NodeId node_of(std::uint8_t b) {
|
|
NodeId v{};
|
|
for (std::size_t i = 0; i < kNodeIdLen; ++i) v.b[i] = static_cast<std::uint8_t>(b + i);
|
|
return v;
|
|
}
|
|
|
|
std::vector<std::uint8_t> unhex(const char* s) {
|
|
auto nib = [](char c) -> int {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
return -1;
|
|
};
|
|
std::vector<std::uint8_t> out;
|
|
for (std::size_t i = 0; s[i] != 0 && s[i + 1] != 0; i += 2)
|
|
out.push_back(static_cast<std::uint8_t>(nib(s[i]) * 16 + nib(s[i + 1])));
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::uint8_t> payload() {
|
|
const std::string p = "hello-from-the-other-chain";
|
|
return {p.begin(), p.end()};
|
|
}
|
|
|
|
// The three validators the Go reference built, in the canonical order a bit
|
|
// vector indexes: by uncompressed public key, ascending. Equal weights, so a
|
|
// two-of-three signature is exactly two-thirds.
|
|
std::map<NodeId, std::pair<std::vector<std::uint8_t>, std::uint64_t>> reference_set(
|
|
std::uint64_t w0 = 100, std::uint64_t w1 = 100, std::uint64_t w2 = 100) {
|
|
return {
|
|
{node_of(1), {unhex(pvmgold::warp_vdr0_pk), w0}},
|
|
{node_of(2), {unhex(pvmgold::warp_vdr1_pk), w1}},
|
|
{node_of(3), {unhex(pvmgold::warp_vdr2_pk), w2}},
|
|
};
|
|
}
|
|
|
|
warp::BitSetSignature reference_signature(std::vector<std::uint8_t> signers = {0x03}) {
|
|
warp::BitSetSignature s;
|
|
s.signers = std::move(signers);
|
|
const auto raw = unhex(pvmgold::warp_agg_sig);
|
|
std::copy(raw.begin(), raw.end(), s.signature.begin());
|
|
return s;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Go: warp.NewUnsignedMessage — bytes and id.
|
|
TEST(UnsignedMessageWire) {
|
|
auto m = warp::UnsignedMessage::build(96369, id_of(0xC0), payload());
|
|
REQUIRE_OK(m);
|
|
REQUIRE_EQ(std::string(pvmgold::warp_unsigned), hex(m.value().bytes));
|
|
REQUIRE_EQ(std::string(pvmgold::warp_unsigned_id), hex(m.value().id));
|
|
|
|
auto back = warp::UnsignedMessage::parse(m.value().bytes);
|
|
REQUIRE_OK(back);
|
|
REQUIRE_EQ_NUM(96369, back.value().network_id);
|
|
REQUIRE_EQ(id_of(0xC0), back.value().source_chain_id);
|
|
REQUIRE_EQ(payload(), back.value().payload);
|
|
REQUIRE_EQ(m.value().id, back.value().id);
|
|
}
|
|
|
|
// Go: warp.NewMessage — the container plus its signature.
|
|
TEST(MessageWire) {
|
|
auto u = warp::UnsignedMessage::build(96369, id_of(0xC0), payload());
|
|
REQUIRE_OK(u);
|
|
auto m = warp::Message::build(u.value(), reference_signature());
|
|
REQUIRE_OK(m);
|
|
REQUIRE_EQ(std::string(pvmgold::warp_message), hex(m.value().bytes));
|
|
|
|
auto back = warp::Message::parse(m.value().bytes);
|
|
REQUIRE_OK(back);
|
|
REQUIRE_EQ(u.value().bytes, back.value().unsigned_message.bytes);
|
|
REQUIRE_EQ(u.value().id, back.value().unsigned_message.id);
|
|
REQUIRE(back.value().signature == reference_signature());
|
|
}
|
|
|
|
// The bit vector is a big-endian big integer, and one with a leading zero byte
|
|
// denotes the same set with different bytes — which would be two messages with
|
|
// one meaning, under two ids.
|
|
TEST(TheBitVectorIsCanonical) {
|
|
REQUIRE_EQ_NUM(2, reference_signature({0x03}).num_signers().value());
|
|
REQUIRE_EQ_NUM(1, reference_signature({0x04}).num_signers().value());
|
|
REQUIRE_EQ_NUM(0, reference_signature({}).num_signers().value());
|
|
REQUIRE_EQ_NUM(9, reference_signature({0xff, 0x01}).num_signers().value());
|
|
REQUIRE_ERR(reference_signature({0x00, 0x03}).num_signers(), Err::InvalidBitSet);
|
|
}
|
|
|
|
// Go: warp.FlattenValidatorSet. Two nodes sharing a key are ONE signer with the
|
|
// sum of their weights — one aggregate cannot tell them apart, so the set must
|
|
// not either — and a keyless validator's stake still counts toward the total.
|
|
TEST(FlattenTheValidatorSet) {
|
|
auto set = warp::flatten(reference_set());
|
|
REQUIRE_OK(set);
|
|
REQUIRE_EQ_NUM(3, set.value().validators.size());
|
|
REQUIRE_U64(300u, set.value().total_weight);
|
|
|
|
// Ordered by the uncompressed key, which is what the bit vector indexes.
|
|
auto shared = reference_set();
|
|
shared[node_of(4)] = {unhex(pvmgold::warp_vdr0_pk), 50};
|
|
auto merged = warp::flatten(shared);
|
|
REQUIRE_OK(merged);
|
|
REQUIRE_EQ_NUM(3, merged.value().validators.size());
|
|
REQUIRE_U64(350u, merged.value().total_weight);
|
|
REQUIRE_U64(150u, merged.value().validators[0].weight);
|
|
REQUIRE_EQ_NUM(2, merged.value().validators[0].node_ids.size());
|
|
|
|
// A validator with no key cannot sign, and its stake still counts.
|
|
auto keyless = reference_set();
|
|
keyless[node_of(5)] = {{}, 400};
|
|
auto with_keyless = warp::flatten(keyless);
|
|
REQUIRE_OK(with_keyless);
|
|
REQUIRE_EQ_NUM(3, with_keyless.value().validators.size());
|
|
REQUIRE_U64(700u, with_keyless.value().total_weight);
|
|
}
|
|
|
|
// Go: warp.FilterValidators. A signature cannot name a validator that is not
|
|
// in the set.
|
|
TEST(FilterSigners) {
|
|
auto set = warp::flatten(reference_set());
|
|
REQUIRE_OK(set);
|
|
const auto& vdrs = set.value().validators;
|
|
|
|
auto two = warp::filter(std::vector<std::uint8_t>{0x03}, vdrs);
|
|
REQUIRE_OK(two);
|
|
REQUIRE_EQ_NUM(2, two.value().size());
|
|
REQUIRE(two.value()[0].public_key == vdrs[0].public_key);
|
|
REQUIRE(two.value()[1].public_key == vdrs[1].public_key);
|
|
|
|
auto third = warp::filter(std::vector<std::uint8_t>{0x04}, vdrs);
|
|
REQUIRE_OK(third);
|
|
REQUIRE_EQ_NUM(1, third.value().size());
|
|
REQUIRE(third.value()[0].public_key == vdrs[2].public_key);
|
|
|
|
// Bit 3 names a fourth validator, and there are three.
|
|
REQUIRE_ERR(warp::filter(std::vector<std::uint8_t>{0x08}, vdrs), Err::UnknownValidator);
|
|
REQUIRE_ERR(warp::filter(std::vector<std::uint8_t>{0x00, 0x03}, vdrs), Err::InvalidBitSet);
|
|
}
|
|
|
|
// Go: warp.VerifyWeight. Computed without dividing, so nothing rounds in the
|
|
// attacker's favour.
|
|
TEST(VerifyWeight) {
|
|
REQUIRE_OK(warp::verify_weight(200, 300, 2, 3)); // exactly two-thirds
|
|
REQUIRE_OK(warp::verify_weight(300, 300, 2, 3));
|
|
REQUIRE_ERR(warp::verify_weight(199, 300, 2, 3), Err::InsufficientWeight);
|
|
REQUIRE_OK(warp::verify_weight(0, 0, 2, 3)); // an empty set is trivially met
|
|
|
|
// Nothing overflows on the way: the comparison is at full width.
|
|
REQUIRE_OK(warp::verify_weight(UINT64_MAX, UINT64_MAX, 1, 1));
|
|
REQUIRE_ERR(warp::verify_weight(UINT64_MAX - 1, UINT64_MAX, 1, 1), Err::InsufficientWeight);
|
|
}
|
|
|
|
// The one that matters: the Go reference's own aggregate over the Go
|
|
// reference's own message verifies here, and every way of breaking it fails.
|
|
TEST(VerifyTheReferenceSignature) {
|
|
auto u = warp::UnsignedMessage::build(96369, id_of(0xC0), payload());
|
|
REQUIRE_OK(u);
|
|
auto set = warp::flatten(reference_set());
|
|
REQUIRE_OK(set);
|
|
const auto sig = reference_signature();
|
|
|
|
// Two of three equal weights is exactly two-thirds.
|
|
REQUIRE_OK(warp::verify(sig, u.value(), 96369, set.value(), 2, 3));
|
|
|
|
// Three-quarters is more than two of three.
|
|
REQUIRE_ERR(warp::verify(sig, u.value(), 96369, set.value(), 3, 4), Err::InsufficientWeight);
|
|
|
|
// The message must have been signed for THIS network.
|
|
REQUIRE_ERR(warp::verify(sig, u.value(), 96370, set.value(), 2, 3), Err::WrongNetworkID);
|
|
|
|
// Claiming a signer who did not sign breaks the aggregate.
|
|
auto over_claimed = sig;
|
|
over_claimed.signers = {0x07};
|
|
REQUIRE_ERR(warp::verify(over_claimed, u.value(), 96370, set.value(), 2, 3), Err::WrongNetworkID);
|
|
REQUIRE_ERR(warp::verify(over_claimed, u.value(), 96369, set.value(), 2, 3),
|
|
Err::InvalidWarpSignature);
|
|
|
|
// Claiming the wrong two is the same failure.
|
|
auto wrong_pair = sig;
|
|
wrong_pair.signers = {0x06};
|
|
REQUIRE_ERR(warp::verify(wrong_pair, u.value(), 96369, set.value(), 2, 3), Err::InvalidWarpSignature);
|
|
|
|
// A different message is not this message.
|
|
auto other = warp::UnsignedMessage::build(96369, id_of(0xC0), std::vector<std::uint8_t>{1, 2, 3});
|
|
REQUIRE_OK(other);
|
|
REQUIRE_ERR(warp::verify(sig, other.value(), 96369, set.value(), 2, 3), Err::InvalidWarpSignature);
|
|
|
|
// And a flipped bit in the signature itself — whether it stops the
|
|
// signature decompressing or merely stops the pairing, the answer is the
|
|
// same refusal.
|
|
auto tampered = sig;
|
|
tampered.signature[0] ^= 0x01;
|
|
REQUIRE_ERR(warp::verify(tampered, u.value(), 96369, set.value(), 2, 3), Err::InvalidWarpSignature);
|
|
auto tampered_tail = sig;
|
|
tampered_tail.signature[95] ^= 0x01;
|
|
REQUIRE_ERR(warp::verify(tampered_tail, u.value(), 96369, set.value(), 2, 3),
|
|
Err::InvalidWarpSignature);
|
|
}
|
|
|
|
// A keyless validator's weight raises the bar without being able to help clear
|
|
// it. That is what stops a chain from cheapening its own quorum.
|
|
TEST(KeylessWeightRaisesTheBar) {
|
|
auto u = warp::UnsignedMessage::build(96369, id_of(0xC0), payload());
|
|
REQUIRE_OK(u);
|
|
auto padded = reference_set();
|
|
padded[node_of(9)] = {{}, 1'000};
|
|
auto set = warp::flatten(padded);
|
|
REQUIRE_OK(set);
|
|
REQUIRE_U64(1'300u, set.value().total_weight);
|
|
|
|
// The same two signers now hold 200 of 1300, which is not two-thirds.
|
|
REQUIRE_ERR(warp::verify(reference_signature(), u.value(), 96369, set.value(), 2, 3),
|
|
Err::InsufficientWeight);
|
|
}
|
|
|
|
// A signature scheme this port does not implement parses to a refusal, not to a
|
|
// signature that verifies trivially.
|
|
TEST(AnUnimplementedSchemeIsRefused) {
|
|
auto u = warp::UnsignedMessage::build(96369, id_of(0xC0), payload());
|
|
REQUIRE_OK(u);
|
|
auto m = warp::Message::build(u.value(), reference_signature());
|
|
REQUIRE_OK(m);
|
|
|
|
// The signature buffer is the second byte field of the message object; its
|
|
// own first byte is the scheme.
|
|
auto b = m.value().bytes;
|
|
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);
|
|
const std::int64_t slot = static_cast<std::int64_t>(root) + 8; // signature bytes @8
|
|
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 sig_buf = slot + rel;
|
|
// Inside that buffer, the root object's first byte is the scheme kind.
|
|
const std::uint32_t sig_root =
|
|
static_cast<std::uint32_t>(b[sig_buf + 8]) | (static_cast<std::uint32_t>(b[sig_buf + 9]) << 8) |
|
|
(static_cast<std::uint32_t>(b[sig_buf + 10]) << 16) |
|
|
(static_cast<std::uint32_t>(b[sig_buf + 11]) << 24);
|
|
b[sig_buf + sig_root] = 0x01; // the post-quantum scheme
|
|
REQUIRE_ERR(warp::Message::parse(b), Err::UnknownWarpSignature);
|
|
}
|
|
|
|
// ── the payload layer: what a warp message SAYS
|
|
//
|
|
// Ported from Go warp/payload (payload_test, addressed_call_test, hash_test)
|
|
// and warp/message (register_l1_validator_test, l1_validator_registration_test,
|
|
// l1_validator_weight_test, chain_to_l1_conversion_test).
|
|
|
|
namespace {
|
|
|
|
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;
|
|
}
|
|
|
|
signer::PublicKeyBytes reference_key() {
|
|
signer::PublicKeyBytes k{};
|
|
for (std::size_t i = 0; i < k.size(); ++i) k[i] = static_cast<std::uint8_t>(0x20 + i);
|
|
return k;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// The envelope says WHO sent this and from what address; the message inside
|
|
// says what.
|
|
TEST(TheEnvelopeWire) {
|
|
auto h = warpmsg::Hash::build(id_of(0xD0));
|
|
REQUIRE_OK(h);
|
|
REQUIRE_EQ(std::string(pvmgold::payload_hash), hex(h.value().bytes));
|
|
|
|
const std::string src = "source-addr", call = "the-call";
|
|
auto a = warpmsg::AddressedCall::build(
|
|
std::vector<std::uint8_t>(src.begin(), src.end()),
|
|
std::vector<std::uint8_t>(call.begin(), call.end()));
|
|
REQUIRE_OK(a);
|
|
REQUIRE_EQ(std::string(pvmgold::payload_addressed_call), hex(a.value().bytes));
|
|
|
|
auto back = warpmsg::parse_envelope(a.value().bytes);
|
|
REQUIRE_OK(back);
|
|
const auto* parsed = std::get_if<warpmsg::AddressedCall>(&back.value());
|
|
REQUIRE(parsed != nullptr);
|
|
REQUIRE_EQ(std::vector<std::uint8_t>(src.begin(), src.end()), parsed->source_address);
|
|
REQUIRE_EQ(std::vector<std::uint8_t>(call.begin(), call.end()), parsed->payload);
|
|
|
|
auto back_hash = warpmsg::parse_envelope(h.value().bytes);
|
|
REQUIRE_OK(back_hash);
|
|
REQUIRE(std::get_if<warpmsg::Hash>(&back_hash.value()) != nullptr);
|
|
|
|
// A kind that names nothing is refused rather than guessed at.
|
|
auto junk = h.value().bytes;
|
|
const std::uint32_t root = static_cast<std::uint32_t>(junk[8]) |
|
|
(static_cast<std::uint32_t>(junk[9]) << 8) |
|
|
(static_cast<std::uint32_t>(junk[10]) << 16) |
|
|
(static_cast<std::uint32_t>(junk[11]) << 24);
|
|
junk[root] = 0x7f;
|
|
REQUIRE_ERR(warpmsg::parse_envelope(junk), Err::WrongPayloadType);
|
|
}
|
|
|
|
// A validator's NAME on the P-chain is the hash of the message that registered
|
|
// it. Two registrations differing anywhere are two validators, and a replay of
|
|
// one cannot become the other.
|
|
TEST(RegisterL1ValidatorWire) {
|
|
auto m = warpmsg::RegisterL1Validator::build(
|
|
id_of(0xD1), node_of(0xD2), reference_key(), 1717171717,
|
|
warpmsg::PChainOwner{1, {short_of(0x30)}},
|
|
warpmsg::PChainOwner{2, {short_of(0x40), short_of(0x50)}}, 9000);
|
|
REQUIRE_OK(m);
|
|
REQUIRE_EQ(std::string(pvmgold::msg_register_l1_validator), hex(m.value().bytes));
|
|
REQUIRE_EQ(std::string(pvmgold::msg_register_l1_validator_id), hex(m.value().validation_id()));
|
|
REQUIRE_OK(m.value().verify());
|
|
|
|
auto back = warpmsg::parse_message(m.value().bytes);
|
|
REQUIRE_OK(back);
|
|
const auto* parsed = std::get_if<warpmsg::RegisterL1Validator>(&back.value());
|
|
REQUIRE(parsed != nullptr);
|
|
REQUIRE_EQ(id_of(0xD1), parsed->chain_id);
|
|
REQUIRE_U64(1717171717u, parsed->expiry);
|
|
REQUIRE_U64(9000u, parsed->weight);
|
|
REQUIRE(parsed->bls_public_key == reference_key());
|
|
REQUIRE_EQ(warpmsg::PChainOwner({1, {short_of(0x30)}}), parsed->remaining_balance_owner);
|
|
REQUIRE_EQ(warpmsg::PChainOwner({2, {short_of(0x40), short_of(0x50)}}), parsed->disable_owner);
|
|
// The name follows the bytes, so it survives the round trip.
|
|
REQUIRE_EQ(m.value().validation_id(), parsed->validation_id());
|
|
|
|
// One changed field is a different validator.
|
|
auto other = warpmsg::RegisterL1Validator::build(
|
|
id_of(0xD1), node_of(0xD2), reference_key(), 1717171717,
|
|
warpmsg::PChainOwner{1, {short_of(0x30)}},
|
|
warpmsg::PChainOwner{2, {short_of(0x40), short_of(0x50)}}, 9001);
|
|
REQUIRE_OK(other);
|
|
REQUIRE(!(other.value().validation_id() == m.value().validation_id()));
|
|
}
|
|
|
|
// Go: RegisterL1Validator.Verify — the four refusals.
|
|
TEST(RegisterL1ValidatorVerify) {
|
|
auto build = [&](const Id& chain, const NodeId& node, std::uint64_t weight,
|
|
const warpmsg::PChainOwner& rem, const warpmsg::PChainOwner& dis) {
|
|
return warpmsg::RegisterL1Validator::build(chain, node, reference_key(), 1, rem, dis, weight);
|
|
};
|
|
const warpmsg::PChainOwner good{1, {short_of(0x30)}};
|
|
|
|
// The primary network is not an L1 and does not register validators this way.
|
|
auto primary = build(kPrimaryNetworkId, node_of(1), 1, good, good);
|
|
REQUIRE_OK(primary);
|
|
REQUIRE_ERR(primary.value().verify(), Err::InvalidChainID);
|
|
|
|
auto weightless = build(id_of(0xD1), node_of(1), 0, good, good);
|
|
REQUIRE_OK(weightless);
|
|
REQUIRE_ERR(weightless.value().verify(), Err::InvalidWeight);
|
|
|
|
NodeId empty{};
|
|
auto anonymous = build(id_of(0xD1), empty, 1, good, good);
|
|
REQUIRE_OK(anonymous);
|
|
REQUIRE_ERR(anonymous.value().verify(), Err::InvalidNodeID);
|
|
|
|
// An owner nobody can satisfy would make the balance unrecoverable.
|
|
const warpmsg::PChainOwner unspendable{1, {}};
|
|
auto bad_owner = build(id_of(0xD1), node_of(1), 1, unspendable, good);
|
|
REQUIRE_OK(bad_owner);
|
|
REQUIRE_ERR(bad_owner.value().verify(), Err::InvalidOwner);
|
|
|
|
auto ok_msg = build(id_of(0xD1), node_of(1), 1, good, good);
|
|
REQUIRE_OK(ok_msg);
|
|
REQUIRE_OK(ok_msg.value().verify());
|
|
}
|
|
|
|
// Registration and weight: the two things an L1 tells the P-chain about a
|
|
// validator after it exists.
|
|
TEST(RegistrationAndWeightWire) {
|
|
auto reg = warpmsg::L1ValidatorRegistration::build(id_of(0xD3), true);
|
|
REQUIRE_OK(reg);
|
|
REQUIRE_EQ(std::string(pvmgold::msg_l1_validator_registration), hex(reg.value().bytes));
|
|
|
|
auto back = warpmsg::parse_message(reg.value().bytes);
|
|
REQUIRE_OK(back);
|
|
const auto* p = std::get_if<warpmsg::L1ValidatorRegistration>(&back.value());
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE_EQ(id_of(0xD3), p->validation_id);
|
|
REQUIRE(p->registered);
|
|
|
|
// False here is permanent: this id is not and can never become a validator,
|
|
// which is what makes an expiry final rather than a retry.
|
|
auto never = warpmsg::L1ValidatorRegistration::build(id_of(0xD3), false);
|
|
REQUIRE_OK(never);
|
|
auto never_back = warpmsg::parse_message(never.value().bytes);
|
|
REQUIRE_OK(never_back);
|
|
REQUIRE(!std::get_if<warpmsg::L1ValidatorRegistration>(&never_back.value())->registered);
|
|
|
|
auto vw = warpmsg::L1ValidatorWeight::build(id_of(0xD4), 7, 12345);
|
|
REQUIRE_OK(vw);
|
|
REQUIRE_EQ(std::string(pvmgold::msg_l1_validator_weight), hex(vw.value().bytes));
|
|
auto vw_back = warpmsg::parse_message(vw.value().bytes);
|
|
REQUIRE_OK(vw_back);
|
|
const auto* w = std::get_if<warpmsg::L1ValidatorWeight>(&vw_back.value());
|
|
REQUIRE(w != nullptr);
|
|
REQUIRE_EQ(id_of(0xD4), w->validation_id);
|
|
// The nonce is what stops an old weight being replayed over a newer one.
|
|
REQUIRE_U64(7u, w->nonce);
|
|
REQUIRE_U64(12345u, w->weight);
|
|
}
|
|
|
|
// The conversion: what a network became when it went sovereign. Its id is the
|
|
// hash of its own canonical encoding, so the id and the bytes cannot diverge.
|
|
TEST(ConversionWire) {
|
|
warpmsg::ConversionData d;
|
|
d.chain_id = id_of(0xD5);
|
|
d.manager_chain_id = id_of(0xD6);
|
|
const std::string mgr = "manager";
|
|
d.manager_address.assign(mgr.begin(), mgr.end());
|
|
const auto n1 = node_of(0xD7), n2 = node_of(0xD8);
|
|
d.validators = {
|
|
{std::vector<std::uint8_t>(n1.b.begin(), n1.b.end()), reference_key(), 11},
|
|
{std::vector<std::uint8_t>(n2.b.begin(), n2.b.end()), reference_key(), 22},
|
|
};
|
|
|
|
auto encoded = d.encode();
|
|
REQUIRE_OK(encoded);
|
|
REQUIRE_EQ(std::string(pvmgold::conversion_data), hex(encoded.value()));
|
|
|
|
auto cid = d.conversion_id();
|
|
REQUIRE_OK(cid);
|
|
REQUIRE_EQ(std::string(pvmgold::conversion_id), hex(cid.value()));
|
|
|
|
auto m = warpmsg::ChainToL1Conversion::build(cid.value());
|
|
REQUIRE_OK(m);
|
|
REQUIRE_EQ(std::string(pvmgold::msg_chain_to_l1_conversion), hex(m.value().bytes));
|
|
|
|
auto back = warpmsg::parse_message(m.value().bytes);
|
|
REQUIRE_OK(back);
|
|
const auto* p = std::get_if<warpmsg::ChainToL1Conversion>(&back.value());
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE_EQ(cid.value(), p->id);
|
|
|
|
// One changed validator weight is a different conversion.
|
|
auto d2 = d;
|
|
d2.validators[1].weight = 23;
|
|
REQUIRE(!(d2.conversion_id().value() == cid.value()));
|
|
}
|