Files
consensus/test/pop_conformance_test.cpp
zeekayandHanzo Dev 5ac599106a pop: take the over-long key from the corpus
The frozen corpus grew the 49-byte key it was missing, so the width is now
refused from both sides here too: a decoder that guarded only the short side
would read the first 48 bytes of a longer buffer and check a proof against a
PREFIX of what the registrant published, which is a second spelling of an
already-seated key.

Fourteen cases, from the one source all three implementations read.

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

128 lines
4.7 KiB
C++

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// pop_conformance_test.cpp — C++ conforms to the Go oracle, case for case.
//
// vectors/pop.json is the frozen node-bound proof-of-possession corpus from
// luxfi/conformance, generated by the Go validator/pop and self-checked against
// its verifier. This holds bls::pop_verify to it: the same accept/reject, and on
// a reject the same error class, for every case — so a certificate whose keys
// verify in Go verify here, and a proof Go refuses is refused here for the same
// reason. Rust proves the same against the same bytes; three languages, one wire.
#include "lux/consensus/bls.hpp"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
namespace {
using lux::consensus::bls::Pop;
[[noreturn]] void die(const std::string& m) {
std::fprintf(stderr, "pop_conformance: %s\n", m.c_str());
std::exit(1);
}
std::string slurp(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) die("cannot open " + path);
std::stringstream ss;
ss << in.rdbuf();
return ss.str();
}
// Grab the string value of `key` within one case object `obj`.
std::string field(const std::string& obj, const std::string& key) {
const std::string k = "\"" + key + "\": \"";
const auto p = obj.find(k);
if (p == std::string::npos) return {};
const auto start = p + k.size();
return obj.substr(start, obj.find('"', start) - start);
}
std::vector<std::uint8_t> unhex(const std::string& h) {
std::vector<std::uint8_t> out;
out.reserve(h.size() / 2);
for (std::size_t i = 0; i + 1 < h.size(); i += 2)
out.push_back(static_cast<std::uint8_t>(std::strtoul(h.substr(i, 2).c_str(), nullptr, 16)));
return out;
}
// The Go oracle's three error strings, so a mismatch names the diverging clause.
const char* classOf(Pop p) {
switch (p) {
case Pop::Ok: return "";
case Pop::Key: return "pop: public key is not a canonical compressed BLS12-381 G1 point";
case Pop::Proof: return "pop: proof is not a canonical compressed BLS12-381 G2 point";
case Pop::Possession: return "pop: proof does not bind this node to this key";
}
return "?";
}
} // namespace
int main() {
const std::string raw = slurp(std::string(CONSENSUS_VECTORS) + "/pop.json");
int checked = 0;
std::size_t pos = 0;
const std::string marker = "\"name\":";
while ((pos = raw.find(marker, pos)) != std::string::npos) {
const std::size_t end = raw.find(marker, pos + marker.size());
const std::string obj = raw.substr(pos, (end == std::string::npos ? raw.size() : end) - pos);
pos += marker.size();
const std::string name = field(obj, "name");
const auto node = unhex(field(obj, "node"));
const auto key = unhex(field(obj, "key"));
const auto proof = unhex(field(obj, "proof"));
const bool accepted = obj.find("\"accepted\": true") != std::string::npos;
if (node.size() != 20) die("case '" + name + "': node is not 20 bytes");
// Pad key/proof to their fixed widths so a short/long case reaches the
// decoder (which is what rejects it), never a bounds fault here.
std::uint8_t k[48] = {0}, s[96] = {0};
std::memcpy(k, key.data(), key.size() < 48 ? key.size() : 48);
std::memcpy(s, proof.data(), proof.size() < 96 ? proof.size() : 96);
// A wrong-length key or proof is a bad encoding by definition; only a
// correct-length input is handed to the verifier, exactly as Go's length
// guard does before decoding.
const bool key_ok_len = key.size() == 48;
const bool proof_ok_len = proof.size() == 96;
Pop got;
if (!key_ok_len) {
got = Pop::Key;
} else if (!proof_ok_len) {
got = Pop::Proof;
} else {
got = lux::consensus::bls::pop_verify(node.data(), k, s);
}
const bool got_ok = (got == Pop::Ok);
if (got_ok != accepted) {
die("case '" + name + "': C++ accepted=" + (got_ok ? "true" : "false") +
" but Go said accepted=" + (accepted ? "true" : "false"));
}
if (!accepted) {
const std::string want = field(obj, "error");
if (want != classOf(got))
die("case '" + name + "': error class diverged: got '" + classOf(got) +
"' want '" + want + "'");
}
++checked;
}
if (checked != 14) die("expected 14 frozen cases, checked " + std::to_string(checked));
std::printf("pop_conformance: %d cases match the Go oracle\n", checked);
return 0;
}