mirror of
https://github.com/lux-cpp/consensus.git
synced 2026-10-30 18:14:55 +00:00
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]>
128 lines
4.7 KiB
C++
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;
|
|
}
|