Files
consensus/test/registration_test.cpp
zeekayandHanzo Dev cf1bfcdad3 measure what the tests reach, and reach the rest
The gate had no coverage measurement at all, so "the tests are thorough" was an
opinion. -DCONSENSUS_COVERAGE=ON now instruments consensus's own targets with
clang's source-based coverage and adds `coverage` (the report) and
`coverage-show` (the per-line HTML). Regions, not lines: a line carrying two
clauses reports both, and a gate whose clauses ARE the rule cannot be measured
by a tool that calls a half-tested predicate covered. The same instrumentation
cargo llvm-cov reports the Rust crate with, so the two numbers mean one thing.

Scoped exactly like CONSENSUS_SANITIZE — blst and bls_signature stay clean — and
the two now read ONE list of consensus's own targets, so a target instrumented
in one build cannot be silently missed in the other.

Measured: 83.28% of regions, 90.52% of functions. The tests then close the gap
to 99.08% / 100%, and the shape of what was missing is the point: the whole
refusal table of verify_cert, every answer record_vote can give, the registry
read as a second wall behind admit(), the BLS decode failures, and the entire
lux_quasar_* C ABI — a published contract at 45%.

Every refusal is now checked against a certificate one edit from one the same
engine accepts, so a clause that stopped refusing cannot hide behind another.
This is the C++ reading of Go's ErrQC* table and Rust's Cert rejects; the three
are meant to be read side by side.

The remaining 8 regions are defensive arms behind walls that already hold: a
pairing failure over points both callers decoded, aggregation over signatures
each already verified, a canonical round trip blst refuses on the way in, an
uncompress after pop_verify decoded the same bytes, and two enum defaults
reachable only by casting a garbage value in.

Tests only — src/ and include/ are untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 22:00:21 -07:00

702 lines
36 KiB
C++

// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// registration_test.cpp — the admission door, against the rule Go writes in
// validator/registration.go.
//
// The properties under test are the ones a door can get wrong, and the first one
// is the reason the door exists at all:
//
// 0. THE HAZARD IS REAL, AND IT IS NOW UNBUILDABLE. Two node ids resolving to
// ONE key clear a two-signer floor on ONE holder's signature, and the
// certificate verifier is RIGHT to accept it: it counts what the set it was
// handed says, and that set says two nodes signed. Nothing downstream can
// catch it. So the catch is upstream and it is doubled — admit() refuses to
// build the set, and Registry refuses to seat it however it was built.
// 1. POSSESSION IS REQUIRED. No proof, a wrong-width proof, a proof lifted from
// another node, and the IETF pubkey-only proof are each refused.
// 2. ONE KEY, ONE NODE. Both registrations are individually SOUND — the holder
// of a key can mint a genuine node-bound proof for any identity it likes —
// so possession cannot refuse this, and uniqueness must.
// 3. ONE NODE, ONE KEY. The other axis, and neither implies the other.
// 4. ZERO WEIGHT IS REFUSED: a signer that counts toward the signer floor and
// not toward the stake floor puts the two floors out of step.
// 5. THE SET IS WHOLE. One bad registration fails the call; a refused call
// returns no partial set, because a set that lost a signer has a total
// weight that no longer describes it.
// 6. THE VERDICT IS A FUNCTION OF THE SET, not of the caller's vector order.
// 7. THE ADMITTED SET IS THE ONE THE GATE RUNS ON: it constructs the engine and
// seats the certificate registry, closing the loop the hazard opened.
//
// Every proof used here is minted under the PROOF domain and then handed to
// bls::pop_verify — the function pop_conformance_test pins byte-for-byte against
// the Go corpus — before it is used. A minting bug therefore cannot quietly turn
// a "both proofs are genuine" case into a weaker one.
#include "lux/consensus/bls.hpp"
#include "lux/consensus/cert.hpp"
#include "lux/consensus/quorum_cert_engine.hpp"
#include "lux/consensus/registration.hpp"
#include "bls_signature.hpp" // the eth2 ..._RO_POP_ surface: the pubkey-only proof
#include <blst.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <string>
#include <vector>
using namespace lux::consensus;
namespace {
int g_pass = 0, g_fail = 0;
void check(bool ok, const std::string& what) {
if (ok) { ++g_pass; std::printf(" ok %s\n", what.c_str()); return; }
++g_fail;
std::printf(" FAIL %s\n", what.c_str());
}
[[noreturn]] void die(const char* what) {
std::printf("registration_test: %s\n", what);
std::exit(2);
}
struct Key {
std::array<std::uint8_t, 32> sk{};
PubKey pk{};
};
Key make_key(std::uint8_t tag) {
std::array<std::uint8_t, 32> seed{};
seed[0] = tag;
for (int i = 1; i < 32; ++i) seed[i] = std::uint8_t(0x5A ^ (tag + i));
Key k;
if (bls::keygen(seed.data(), k.sk.data()) != 0) die("keygen");
if (bls::sk_to_pk(k.sk.data(), k.pk.data()) != 0) die("sk_to_pk");
return k;
}
Node make_node(std::uint8_t tag) {
Node n{};
n.fill(tag);
return n;
}
std::vector<std::uint8_t> bytes(const PubKey& k) { return {k.begin(), k.end()}; }
// The proof a registrant makes for its own pair: a signature by sk over the
// 68-byte preimage node ‖ key, under the PROOF domain — never the vote's, which
// is the whole point of there being two tags.
std::vector<std::uint8_t> mint(const Key& k, const Node& node) {
std::uint8_t msg[bls::kNodeLen + 48];
std::memcpy(msg, node.data(), bls::kNodeLen);
std::memcpy(msg + bls::kNodeLen, k.pk.data(), 48);
blst_scalar s;
blst_scalar_from_bendian(&s, k.sk.data());
blst_p2 hash;
blst_hash_to_g2(&hash, msg, sizeof(msg),
reinterpret_cast<const std::uint8_t*>(bls::kPopDST), bls::kPopDSTLen,
/*aug=*/nullptr, /*aug_len=*/0);
blst_p2 sig;
blst_sign_pk_in_g1(&sig, &hash, &s);
std::vector<std::uint8_t> proof(96);
blst_p2_compress(proof.data(), &sig);
// The proof is only worth anything to this test if the CONFORMANT verifier
// accepts it. Assert that here, once, so every "genuine proof" below is
// genuine by the corpus's measure and not by this file's.
if (bls::pop_verify(node.data(), k.pk.data(), proof.data()) != bls::Pop::Ok)
die("minted a proof the conformant verifier refuses");
return proof;
}
Registration reg(const Key& k, const Node& node, std::uint64_t weight) {
return Registration{node, bytes(k.pk), mint(k, node), weight};
}
const char* pop_name(bls::Pop p) {
switch (p) {
case bls::Pop::Ok: return "ok";
case bls::Pop::Key: return "key";
case bls::Pop::Proof: return "proof";
case bls::Pop::Possession: return "possession";
}
return "unknown";
}
// One refusal case, named, so a divergence says which clause moved.
void refuses(const std::vector<Registration>& rs, Admission::Why want, const std::string& what) {
CanonicalSet set;
const Admission a = admit(rs, set);
const bool right = (a.why == want);
check(right && !a, what + " → " + admission_name(want) +
(right ? "" : std::string(" (got ") + admission_name(a.why) + ")"));
check(set.validators.empty() && set.total_weight == 0,
" …and a refused call returns no partial set");
}
// A key resolver that answers for SEVERAL node ids with ONE holder's key: the
// set the door exists to keep out of the world. It is a Keys, not a Registry,
// precisely because a Registry can no longer be one — and the hazard has to stay
// demonstrable, or the reason for the lock stops being visible.
struct OneHolder : Keys {
OneHolder(const PubKey& k, std::vector<Node> ns) : key(k), names(std::move(ns)) {}
PubKey key{};
std::vector<Node> names;
[[nodiscard]] bool verify(const Node& node,
const std::uint8_t* message, std::size_t message_len,
const std::uint8_t* signature,
std::size_t signature_len) const override {
if (std::find(names.begin(), names.end(), node) == names.end()) return false;
if (signature_len != 96) return false;
return bls::verify(key.data(), message, message_len, signature) == 0;
}
};
// Can a registry be seated by hand? The question is asked of a TEMPLATE
// parameter on purpose: an access failure during substitution is a false
// concept, where the same expression written against Registry directly is a hard
// error — which is the truth being asserted, but not in a form a test can hold.
template <class R>
concept Seatable = requires(R& r, const Node& n, const PubKey& k) { r.insert(n, k); };
// The same seat, left open — so the detector is known to DISCRIMINATE and the
// assertion about Registry is not a question that always answers no.
struct OpenSeat {
bool insert(const Node&, const PubKey&) { return true; }
};
static_assert(Seatable<OpenSeat>, "the detector must see a seat that is reachable");
VotePosition make_pos(std::uint8_t tag) {
VotePosition p{};
p.block_id.fill(tag);
p.canonical_id.fill(std::uint8_t(tag ^ 0xFF));
p.height = 7;
p.round = 2;
return p;
}
} // namespace
int main() {
std::printf("================================================================================\n");
std::printf("REGISTRATION — the admission door\n");
std::printf("================================================================================\n");
// ── 0. The hazard the door exists to close ───────────────────────────────
// One holder, two identities — shown to be genuinely dangerous, and then
// shown to be unreachable by every route into a live Registry.
{
std::printf("\n[0] the hazard: two node ids, one key, one signature, a two-signer floor\n");
const Key holder = make_key(0x91);
const Node one = make_node(0x10), two = make_node(0x20);
const VotePosition pos = make_pos(0x33);
const std::vector<std::uint8_t> msg = canonical_vote_message(pos, true);
Signature sig{};
if (bls::sign(holder.sk.data(), msg.data(), msg.size(), sig.data()) != 0) die("sign");
Cert c;
c.position = pos;
c.threshold = 2;
c.votes = {Vote{one, true, {sig.begin(), sig.end()}},
Vote{two, true, {sig.begin(), sig.end()}}};
// THE HAZARD, on a set that says what a hand-built registry used to be
// able to say. The verifier is not at fault here and this is not a
// finding against it: it resolved both names, checked a real signature
// under each, and counted two. The lie is in the set.
OneHolder hazard{holder.pk, {one, two}};
check(c.verify(hazard) == Refusal::None,
"a set answering for two node ids with one key clears a two-signer floor "
"on ONE signature");
// ROUTE 1 — the seat itself. There is no hand to build that registry
// with: seating is private and CanonicalSet::install is its only friend,
// so the call below is not a refusal at run time, it is not a program.
static_assert(!Seatable<Registry>,
"Registry::insert must be private — install() is the one seating route");
check(Seatable<OpenSeat> && !Seatable<Registry>,
"no caller can seat a Registry by hand — insert is private to install()");
// ROUTE 2 — a canonical set written down by hand. CanonicalSet is a plain
// aggregate, so this compiles and admit() never saw it; the seat holds
// the uniqueness rule anyway, and holds it WHOLE.
CanonicalSet forged;
forged.validators = {CanonicalValidator{one, holder.pk, 1},
CanonicalValidator{two, holder.pk, 1}};
forged.total_weight = 2;
Registry keys;
check(!forged.install(keys), "a forged set — two node ids, one key — seats nothing");
check(keys.size() == 0, "…and the registry it was offered is still empty");
// The other axis, forged the same way: one node id under two keys is two
// signer indices and two shares of the weight under one identity.
const Key second = make_key(0x92);
CanonicalSet twins;
twins.validators = {CanonicalValidator{one, holder.pk, 1},
CanonicalValidator{one, second.pk, 1}};
twins.total_weight = 2;
Registry twin_keys;
check(!twins.install(twin_keys), "a forged set — one node id, two keys — seats nothing");
check(twin_keys.size() == 0, "…and that registry is still empty too");
// ROUTE 3 — the door. Both proofs are genuine, because the holder can
// prove possession for any identity it likes, and the set is refused
// anyway: uniqueness is a property of the set, not of a pair.
CanonicalSet set;
const Admission a = admit({reg(holder, one, 1), reg(holder, two, 1)}, set);
check(a.why == Admission::Why::DuplicateKey, "the door refuses to build that set");
check(a.node == two && a.holder == one,
"…naming the node refused and the node already holding the key");
}
// ── 1. The happy path, and the shape it guarantees ───────────────────────
{
std::printf("\n[1] a proven set is admitted, in canonical order\n");
const Key a = make_key(1), b = make_key(2), c = make_key(3);
CanonicalSet set;
const Admission ok = admit({reg(a, make_node(0x01), 100),
reg(b, make_node(0x02), 200),
reg(c, make_node(0x03), 300)},
set);
check(bool(ok) && ok.why == Admission::Why::Ok, "three proven registrations are admitted");
check(set.validators.size() == 3, "three validators seated");
check(set.total_weight == 600, "total weight is the sum of the admitted weights");
bool ascending = true, distinct_nodes = true;
for (std::size_t i = 1; i < set.validators.size(); ++i) {
if (!(set.validators[i - 1].key < set.validators[i].key)) ascending = false;
if (set.validators[i - 1].node == set.validators[i].node) distinct_nodes = false;
}
check(ascending, "the set is strictly ascending by compressed key — the canonical order");
check(distinct_nodes, "every validator carries exactly one node id — the merge is gone");
// The key the set carries is the canonical spelling of the point, and it
// is the same 48 bytes the proof was made over.
bool keys_match = false;
for (const CanonicalValidator& v : set.validators)
if (v.node == make_node(0x02)) keys_match = (v.key == b.pk);
check(keys_match, "the seated key is the registrant's own key, re-serialized from the point");
}
// ── 2. Possession is required ────────────────────────────────────────────
{
std::printf("\n[2] possession: the proof must bind THIS node to THIS key\n");
const Key k = make_key(6);
const Node home = make_node(0x30), elsewhere = make_node(0x31);
Registration lifted = reg(k, elsewhere, 1);
lifted.proof = mint(k, home); // same key, different node, the first node's proof
refuses({lifted}, Admission::Why::Possession, "a proof lifted from another node");
// The IETF pubkey-only proof: a statement about a key, so it travels with
// the key. It proves someone holds the secret and nothing about WHO.
Registration pubkey_only = reg(k, make_node(0x32), 1);
std::vector<std::uint8_t> eth2(96);
if (cevm::crypto::bls::sign(k.sk.data(), k.pk.data(), k.pk.size(), eth2.data()) != 0)
die("eth2 sign");
pubkey_only.proof = eth2;
refuses({pubkey_only}, Admission::Why::Possession, "the pubkey-only proof");
Registration none = reg(k, make_node(0x33), 1);
none.proof.clear();
refuses({none}, Admission::Why::Possession, "no proof at all");
Registration garbage = reg(k, make_node(0x34), 1);
garbage.proof.assign(96, 0xAB);
refuses({garbage}, Admission::Why::Possession, "96 bytes of garbage");
Registration short_proof = reg(k, make_node(0x35), 1);
short_proof.proof.pop_back();
refuses({short_proof}, Admission::Why::Possession, "a 95-byte proof");
Registration short_key = reg(k, make_node(0x36), 1);
short_key.key.pop_back();
refuses({short_key}, Admission::Why::Possession, "a 47-byte key");
Registration no_key = reg(k, make_node(0x37), 1);
no_key.key.clear();
refuses({no_key}, Admission::Why::NoKey, "no key at all");
Registration identity = reg(k, make_node(0x38), 1);
identity.key.assign(48, 0x00);
identity.key[0] = 0xC0; // compressed G1 infinity
refuses({identity}, Admission::Why::Possession, "the identity key");
// The class the proof was refused under travels with the verdict, so an
// operator is told which leg failed and not merely that one did.
CanonicalSet set;
const Admission a = admit({lifted}, set);
check(a.possession == bls::Pop::Possession,
std::string("a decodable but unbinding proof is refused as possession, not encoding "
"(got ") + pop_name(a.possession) + ")");
const Admission b = admit({short_key}, set);
check(b.possession == bls::Pop::Key, "a wrong-width key is refused as an encoding fault");
}
// ── 3. One key, one node ─────────────────────────────────────────────────
{
std::printf("\n[3] one key, one node — both proofs genuine, the set still refused\n");
const Key k = make_key(5);
const Node one = make_node(0x40), two = make_node(0x41);
// Individually sound: this is what makes possession alone insufficient.
check(bls::pop_verify(one.data(), k.pk.data(), mint(k, one).data()) == bls::Pop::Ok &&
bls::pop_verify(two.data(), k.pk.data(), mint(k, two).data()) == bls::Pop::Ok,
"each registration proves possession on its own");
refuses({reg(k, one, 1), reg(k, two, 1)}, Admission::Why::DuplicateKey,
"one key under two nodes");
// The exact same registration twice is BOTH duplicates at once. Which
// clause answers is fixed by the order the rule is written in — key
// before node — and it is fixed the same way in Go, so the two
// implementations refuse the same bytes with the same word.
refuses({reg(k, one, 1), reg(k, one, 1)}, Admission::Why::DuplicateKey,
"one registration presented twice: the key clause answers first");
}
// ── 4. One node, one key ─────────────────────────────────────────────────
{
std::printf("\n[4] one node, one key — the other axis\n");
const Node node = make_node(0x50);
const Key a = make_key(40), b = make_key(41);
check(bls::pop_verify(node.data(), a.pk.data(), mint(a, node).data()) == bls::Pop::Ok &&
bls::pop_verify(node.data(), b.pk.data(), mint(b, node).data()) == bls::Pop::Ok,
"each registration proves possession on its own");
refuses({reg(a, node, 100), reg(b, node, 100)}, Admission::Why::DuplicateNode,
"one node under two keys");
}
// ── 5. Weight ────────────────────────────────────────────────────────────
{
std::printf("\n[5] weight: no phantom signers, no total that wrapped\n");
const Key k = make_key(9), j = make_key(10);
refuses({reg(k, make_node(0x60), 0)}, Admission::Why::ZeroWeight, "a zero-weight validator");
refuses({reg(k, make_node(0x61), 5), reg(j, make_node(0x62), 0)},
Admission::Why::ZeroWeight, "a zero-weight validator beside a good one");
constexpr std::uint64_t kMax = ~std::uint64_t(0);
refuses({reg(k, make_node(0x63), kMax), reg(j, make_node(0x64), 1)},
Admission::Why::WeightOverflow, "a total weight that would wrap");
CanonicalSet set;
const Admission ok = admit({reg(k, make_node(0x65), kMax - 1), reg(j, make_node(0x66), 1)}, set);
check(bool(ok) && set.total_weight == kMax, "…and the largest total that fits is admitted");
}
// ── 5b. The empty set ────────────────────────────────────────────────────
// Go admits it — Register(nil) is an empty set and no error — so this does
// too, and the parity is the point: a set with nobody in it is not the door's
// refusal to make. It dies one layer down, where a quorum of nobody is a
// configuration error, and that is where it has to die, because the door
// cannot know whether an empty batch is the whole set or one page of it.
{
std::printf("\n[5b] an empty set is admitted, and the gate is where a quorum of nobody dies\n");
CanonicalSet set;
const Admission ok = admit({}, set);
check(bool(ok) && set.validators.empty() && set.total_weight == 0,
"no registrations admit to an empty set, as in Go");
bool refused = false;
try {
QuorumCertEngine empty(set.weights());
(void)empty;
} catch (const std::invalid_argument&) {
refused = true;
}
check(refused, "…and the gate refuses to be constructed over it");
}
// ── 6. What is, and is not, a function of the set ────────────────────────
// The stable sort is TOTAL when the node ids are distinct, and only then. Two
// registrations sharing a node id it cannot separate, so their input order
// survives into the walk — which is exactly Go's behaviour (SortStableFunc),
// and the reason the header's claim is scoped rather than absolute.
{
std::printf("\n[6] the set decides the verdict; the order can decide the wording\n");
const Key k = make_key(15);
const Node one = make_node(0x70), two = make_node(0x71);
const Registration a = reg(k, one, 1), b = reg(k, two, 1);
CanonicalSet s1, s2;
const Admission forward = admit({a, b}, s1);
const Admission backward = admit({b, a}, s2);
check(forward.why == backward.why && forward.node == backward.node &&
forward.holder == backward.holder,
"distinct node ids: forward and backward refuse the same registration "
"for the same reason");
check(forward.node == two && forward.holder == one,
"…and it is the higher node id that is refused, on both");
// A REPEATED NODE ID — the one input the sort cannot order. Both entries
// claim node 0x72 and both are faulty, at DIFFERENT clauses: one stakes
// nothing, the other carries a proof minted for another node. Every
// per-registration clause answers for its own entry, so whichever is
// walked first is the one that answers, and the sort will not choose.
{
const Key h = make_key(16), g = make_key(17);
const Node twin = make_node(0x72), elsewhere = make_node(0x73);
const Registration weightless = reg(h, twin, 0);
Registration unproven = reg(g, twin, 1);
unproven.proof = mint(g, elsewhere); // genuine, for another node
CanonicalSet t1, t2;
const Admission first = admit({weightless, unproven}, t1);
const Admission second = admit({unproven, weightless}, t2);
// THE PROPERTY THAT HOLDS, and the only one consensus needs: the
// decision is the set's, and a refusal carries no set either way.
check(!first && !second, "a repeated node id is refused whichever order it arrives in");
check(t1.validators.empty() && t1.total_weight == 0 &&
t2.validators.empty() && t2.total_weight == 0,
"…and neither refusal leaves a partial set behind");
// THE PROPERTY THAT DOES NOT, pinned so a "fix" that made the sort
// total would fail here rather than diverge from Go in silence: both
// refusals name node 0x72, and which fault they name follows the
// order the caller happened to write.
check(first.node == twin && second.node == twin,
"…both name the node they share");
check(first.why == Admission::Why::ZeroWeight &&
second.why == Admission::Why::Possession,
"…while WHICH clause answers follows the order, exactly as it does in Go");
}
// A good set admits to the same bytes in the same order whatever order it
// arrived in: the canonical order is a property of the set.
const Key p = make_key(21), q = make_key(22), r = make_key(23);
CanonicalSet u, v;
(void)admit({reg(p, make_node(0x80), 1), reg(q, make_node(0x81), 2), reg(r, make_node(0x82), 3)}, u);
(void)admit({reg(r, make_node(0x82), 3), reg(p, make_node(0x80), 1), reg(q, make_node(0x81), 2)}, v);
bool same = u.validators.size() == v.validators.size() && u.total_weight == v.total_weight;
for (std::size_t i = 0; same && i < u.validators.size(); ++i)
same = u.validators[i].key == v.validators[i].key &&
u.validators[i].node == v.validators[i].node &&
u.validators[i].weight == v.validators[i].weight;
check(same, "two orders of one good set admit to the same set, in the same order");
}
// ── 7. The admitted set is the one the gate runs on ──────────────────────
{
std::printf("\n[7] the door feeds the gate and the certificate registry\n");
const std::array<Key, 4> ks = {make_key(70), make_key(71), make_key(72), make_key(73)};
std::vector<Registration> rs;
for (std::uint8_t i = 0; i < 4; ++i) rs.push_back(reg(ks[i], make_node(std::uint8_t(0xA0 + i)), 100));
CanonicalSet set;
const Admission ok = admit(rs, set);
check(bool(ok), "four proven validators are admitted");
// The gate, constructed over nothing but the admitted set.
QuorumCertEngine engine(set.weights());
check(engine.validator_count() == 4 && engine.total_stake() == set.total_weight,
"the gate is constructed over the admitted set and agrees on the total");
const VotePosition pos = make_pos(0x44);
check(engine.submit(pos), "a block is pending");
for (const Key& k : ks) {
const std::vector<std::uint8_t> msg = canonical_vote_message(pos, true);
Signature s{};
if (bls::sign(k.sk.data(), msg.data(), msg.size(), s.data()) != 0) die("sign");
check(engine.record_vote(pos.block_id, k.pk, s) == VoteResult::Recorded, " a vote joins");
}
check(engine.is_final(pos.block_id, Tier::Quasar), "the admitted set finalizes at Quasar");
// …and the certificate registry, seated from the same set: four DISTINCT
// holders behind a four-signer floor, which is what the door bought.
Registry keys;
check(set.install(keys), "the admitted set seats the certificate registry");
check(keys.size() == 4, "four distinct node ids, four distinct keys");
// …and it is the ONLY set in there. Seating over a live registry would
// leave a retired validator's node resolvable, so it is refused.
check(!set.install(keys), "a second set refuses to seat over a live registry");
CanonicalSet other;
other.validators = {CanonicalValidator{make_node(0xF1), ks[0].pk, 1}};
other.total_weight = 1;
Registry stale;
check(other.install(stale), "a registry seated from some other set");
check(!set.install(stale), "…and an admitted set will not be added to it");
// A SEAT THAT FAILS MIDWAY LEAVES NOTHING. The refusal is total for the
// same reason admission is: a registry holding a PREFIX of a set resolves
// some of its nodes and none of the rest, which is a validator set nobody
// chose and whose total the floors were never taken of.
CanonicalSet doomed;
doomed.validators = {CanonicalValidator{make_node(0xE1), ks[0].pk, 1},
CanonicalValidator{make_node(0xE2), ks[1].pk, 1},
CanonicalValidator{make_node(0xE3), ks[0].pk, 1}};
doomed.total_weight = 3;
Registry partial;
check(!doomed.install(partial), "a set that repeats a key on its third seat is refused");
check(partial.size() == 0, "…and leaves the registry EMPTY, not holding the two good seats");
Cert c;
c.position = pos;
c.threshold = 4;
const std::vector<std::uint8_t> msg = canonical_vote_message(pos, true);
for (const CanonicalValidator& v : set.validators) {
const Key* owner = nullptr;
for (const Key& k : ks)
if (k.pk == v.key) owner = &k;
if (owner == nullptr) die("an admitted key belongs to no test validator");
Signature s{};
if (bls::sign(owner->sk.data(), msg.data(), msg.size(), s.data()) != 0) die("sign");
c.votes.push_back(Vote{v.node, true, {s.begin(), s.end()}});
}
std::sort(c.votes.begin(), c.votes.end(),
[](const Vote& x, const Vote& y) { return x.node < y.node; });
check(c.verify(keys) == Refusal::None,
"a certificate from the admitted set verifies against it");
}
// ── 8. The registry is the SECOND wall, and it does not trust the first ──
// admit() builds a canonical set; install() seats it. CanonicalSet is a plain
// aggregate, so a set that never went through admit() can be handed to
// install() by anything that can name the type — a caller reading one off a
// wire, a future door with a bug in it. The registry therefore decodes every
// key itself rather than believing the set that carries it, and refuses the
// whole seating if any one of them is not a key. Seated whole or not at all.
std::printf("\n[8] a set that did not come through the door is still decoded key by key\n");
{
const Key good = make_key(0x71);
const Node n0 = make_node(0x01), n1 = make_node(0x02);
const auto seats = [&](const PubKey& second, const std::string& what, bool want) {
CanonicalSet forged;
forged.validators.push_back(CanonicalValidator{n0, good.pk, 10});
forged.validators.push_back(CanonicalValidator{n1, second, 10});
forged.total_weight = 20;
Registry keys;
const bool ok = forged.install(keys);
check(ok == want, what);
// Whole or not at all: a refused install leaves NOTHING seated, so the
// first validator cannot be resolvable while the second is not.
check(keys.size() == (want ? 2u : 0u),
want ? " …and both seats are taken" : " …and no partial registry is left behind");
};
PubKey not_a_point{};
not_a_point.fill(0xAB);
PubKey identity{};
identity[0] = 0xC0; // the compressed identity: it verifies ANY message
PubKey off_subgroup{};
{
// On the curve, outside the prime-order subgroup — past the decode
// clause and into the subgroup one, which is a separate refusal.
bool found = false;
for (std::uint32_t x = 1; x < 4096 && !found; ++x) {
off_subgroup.fill(0);
off_subgroup[47] = std::uint8_t(x);
off_subgroup[46] = std::uint8_t(x >> 8);
off_subgroup[0] |= 0x80;
blst_p1_affine a;
found = blst_p1_uncompress(&a, off_subgroup.data()) == BLST_SUCCESS &&
!blst_p1_affine_in_g1(&a);
}
if (!found) die("no off-subgroup G1 candidate found");
}
seats(make_key(0x72).pk, "a set of two real keys seats", true);
seats(not_a_point, "a key that is not a curve point seats nothing", false);
seats(identity, "the identity key seats nothing — it would verify any message", false);
seats(off_subgroup, "a curve point outside the prime-order subgroup seats nothing", false);
seats(good.pk, "and one key under two node ids seats nothing", false);
// A registry that already holds a set is not this set: seating is a
// rotation, not an accumulation.
{
CanonicalSet a;
a.validators.push_back(CanonicalValidator{n0, good.pk, 10});
a.total_weight = 10;
Registry keys;
check(a.install(keys) && keys.size() == 1, "a fresh registry takes a set");
CanonicalSet b;
b.validators.push_back(CanonicalValidator{n1, make_key(0x73).pk, 10});
b.total_weight = 10;
check(!b.install(keys), "a registry that already holds a set refuses a second");
check(keys.size() == 1, " …and keeps the one it had");
}
}
// ── 9. A seated registry refuses a signature that was never a point ──────
// The certificate verifier asks the registry, and the registry asks blst. A
// vote carrying 96 bytes that do not decode is a refusal at that leg — never
// a pairing, and never a skip that would leave the vote uncounted but the
// certificate still valid.
std::printf("\n[9] a vote whose signature is not a G2 point is refused, not skipped\n");
{
const Key k = make_key(0x81);
const Node n = make_node(0x11);
CanonicalSet set;
set.validators.push_back(CanonicalValidator{n, k.pk, 10});
set.total_weight = 10;
Registry keys;
if (!set.install(keys)) die("install");
const VotePosition pos = make_pos(0x55);
Cert c;
c.position = pos;
c.threshold = 1;
const std::vector<std::uint8_t> msg = canonical_vote_message(pos);
Signature s{};
if (bls::sign(k.sk.data(), msg.data(), msg.size(), s.data()) != 0) die("sign");
c.votes.push_back(Vote{n, true, {s.begin(), s.end()}});
check(c.verify(keys) == Refusal::None, "the genuine vote verifies against the registry");
Cert bad = c;
bad.votes[0].signature.assign(96, 0xCD);
check(bad.verify(keys) == Refusal::Signature,
"96 bytes that are not a G2 point are a signature refusal");
Cert unknown = c;
unknown.votes[0].node = make_node(0x99);
check(unknown.verify(keys) == Refusal::Signature,
"a voter the registry cannot resolve is refused, not skipped");
}
// ── 10. Every verdict this door and this verifier can give has a name ────
// The names are not decoration: conformance_test compares them against the Go
// corpus, so a table that fell out of step with its enum would report a
// refusal Go never made and call the two implementations equal.
std::printf("\n[10] the verdict names are the ones the corpus is compared against\n");
{
check(std::string(admission_name(Admission::Why::Ok)) == "ok" &&
std::string(admission_name(Admission::Why::NoKey)) == "no_key" &&
std::string(admission_name(Admission::Why::ZeroWeight)) == "zero_weight" &&
std::string(admission_name(Admission::Why::Possession)) == "possession" &&
std::string(admission_name(Admission::Why::DuplicateKey)) == "duplicate_key" &&
std::string(admission_name(Admission::Why::DuplicateNode)) == "duplicate_node" &&
std::string(admission_name(Admission::Why::WeightOverflow)) == "weight_overflow",
"every admission verdict has its own name");
check(std::string(refusal_name(Refusal::None)) == "ok" &&
std::string(refusal_name(Refusal::Version)) == "version" &&
std::string(refusal_name(Refusal::Role)) == "role" &&
std::string(refusal_name(Refusal::Tier)) == "tier" &&
std::string(refusal_name(Refusal::ThresholdZero)) == "threshold_zero" &&
std::string(refusal_name(Refusal::NoVotes)) == "no_votes" &&
std::string(refusal_name(Refusal::Order)) == "order" &&
std::string(refusal_name(Refusal::NotAccept)) == "not_accept" &&
std::string(refusal_name(Refusal::Signature)) == "signature" &&
std::string(refusal_name(Refusal::BelowThreshold)) == "below_threshold" &&
std::string(refusal_name(Refusal::Wire)) == "wire",
"and every certificate refusal has its own name");
}
std::printf("\n--------------------------------------------------------------------------------\n");
std::printf("checks: %d passed, %d failed\n", g_pass, g_fail);
if (g_fail) { std::printf("==== REGISTRATION: FAIL ====\n"); return 1; }
std::printf("==== REGISTRATION: PASS — possession required, one key one node, one node one key ====\n");
return 0;
}