mirror of
https://github.com/lux-cpp/consensus.git
synced 2026-10-30 18:14:55 +00:00
QuorumCertEngine took an alpha and used it as the Quasar distinct-voter floor. That made the number of independent parties export finality reports a value the operator picks, and at alpha = 1 it reports one: a validator holding two thirds of the stake could mint an export-grade certificate on a single signature. A security parameter chosen by the party it constrains is not a floor. signer_floor(Quasar) is now two_thirds_count(n) = floor(2n/3)+1 — the same supermajority the stake floor asks for, read in seats, derived from two_thirds_stake_floor over unit weights so the two cannot drift. Go's config.TwoThirdsCount and Rust's two_thirds_count are the same number, and all three now decide the whale case the same way. Quasar was alpha's only use, so the constructor parameter is gone and Party no longer carries one. Both tiers' floors come from the validator set and nothing else. alpha survives only as the wave's per-round sampling ratio in alpha_threshold(k, alpha) — a different quantity that shares a letter. equal_stake_supermajority is renamed two_thirds_count for the same reason the floor exists: the value is the two-thirds count of n at every stake distribution, and "equal stake" named one of its two uses rather than the value. The conformance harness used to build the engine at alpha = 1, reasoning that the smallest legal value could never bind, so C++ would ask exactly Go's question. It was right about Go and wrong about what a corpus is for: Go's export rung had no count floor, C++ had whichever one the deployment configured, and a harness at alpha = 1 could not see the difference — so the corpus reported agreement between two implementations that decided the whale case differently on every real deployment. It now builds the engine from the set and checks the floor the engine derives against the one Go recorded. decision.json's signer_floor is the RUNG's, so a Quasar row no longer reports Nova's, and carries the two new whale-at-export rows. The skewed row in toy_stub_killer isolates the stake gate with four light voters, not three: three no longer clears the count floor on five seats, so the row would have stopped isolating anything. Co-authored-by: Hanzo Dev <[email protected]>
126 lines
5.4 KiB
C++
126 lines
5.4 KiB
C++
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||
//
|
||
// engine_integration_test.cpp — proves the two consensus layers compose into
|
||
// one engine: the LIVENESS layer (photon sampling + wave FPC confidence) decides
|
||
// WHICH block the network converges on; the SAFETY layer (QuorumCertEngine, real
|
||
// BLS + >2/3-stake quorum cert) PROVES it. The contract:
|
||
//
|
||
// wave reaches β-confidence ACCEPT ⇒ the gate certifies the block with a real
|
||
// aggregate signature that independently re-verifies.
|
||
// wave does NOT decide ⇒ no certification is even attempted.
|
||
//
|
||
// This is the cert-carrying finalization path a node drives: poll until decided,
|
||
// then collect the signed votes into a verifiable certificate.
|
||
|
||
#include "lux/consensus/photon.hpp"
|
||
#include "lux/consensus/wave.hpp"
|
||
#include "lux/consensus/quorum_cert_engine.hpp"
|
||
#include "lux/consensus/bls.hpp"
|
||
|
||
#include <array>
|
||
#include <cstdio>
|
||
#include <cstdint>
|
||
#include <cstdlib>
|
||
#include <vector>
|
||
|
||
using namespace lux::consensus;
|
||
|
||
namespace {
|
||
int g_fail = 0;
|
||
void check(bool ok, const char* what) {
|
||
if (!ok) { std::printf(" ASSERT FAILED: %s\n", what); ++g_fail; }
|
||
}
|
||
|
||
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(0xA5 ^ (tag + i));
|
||
Key k;
|
||
if (bls::keygen(seed.data(), k.sk.data()) != 0) { std::puts("keygen failed"); std::exit(2); }
|
||
if (bls::sk_to_pk(k.sk.data(), k.pk.data()) != 0) { std::puts("sk_to_pk failed"); std::exit(2); }
|
||
return k;
|
||
}
|
||
Signature sign_vote(const Key& key, const VotePosition& pos) {
|
||
const std::vector<std::uint8_t> msg = canonical_vote_message(pos);
|
||
Signature sig{};
|
||
if (bls::sign(key.sk.data(), msg.data(), msg.size(), sig.data()) != 0) { std::puts("sign failed"); std::exit(2); }
|
||
return sig;
|
||
}
|
||
} // namespace
|
||
|
||
int main() {
|
||
std::printf("========== consensus — engine integration (liveness ⇒ safety) ==========\n");
|
||
std::printf("wave decides the block; the BLS quorum-cert gate proves it\n\n");
|
||
|
||
// Validator set: 5 validators, stake 20 each (total 100), α = 4.
|
||
std::vector<Key> keys;
|
||
for (std::uint8_t i = 0; i < 5; ++i) keys.push_back(make_key(std::uint8_t(0x30 + i)));
|
||
std::vector<Validator> set;
|
||
for (const auto& k : keys) set.push_back({k.pk, 20});
|
||
|
||
QuorumCertEngine gate(set);
|
||
Wave wave(WaveConfig{/*k=*/5, /*threshold=*/4, /*beta=*/4});
|
||
|
||
// ── [1] Happy path: wave reaches β ACCEPT, then the gate certifies. ──────
|
||
{
|
||
VotePosition A{};
|
||
A.block_id.fill(0x41);
|
||
A.height = 100;
|
||
A.round = 7;
|
||
const auto & item = A.block_id;
|
||
|
||
// Liveness: 4 virtuous polls (all 5 sampled peers vote yes) → decide Accept.
|
||
Decision d = Decision::Undecided;
|
||
for (int r = 0; r < 4; ++r) d = wave.record_round(item, /*yes=*/5, /*total=*/5);
|
||
check(d == Decision::Accept, "wave decided ACCEPT after β rounds");
|
||
|
||
// Safety: ONLY because wave decided Accept do we certify. Collect α signed
|
||
// votes carrying >2/3 stake into a real, re-verifying quorum certificate.
|
||
check(d == Decision::Accept && gate.submit(A), "gate.submit after wave Accept");
|
||
for (int i = 0; i < 4; ++i)
|
||
check(gate.record_vote(A.block_id, keys[i].pk, sign_vote(keys[i], A)) == VoteResult::Recorded,
|
||
"signed ACCEPT vote recorded");
|
||
check(gate.is_final(A.block_id), "gate: block is final (≥α distinct, >2/3 stake)");
|
||
auto cert = gate.assemble_cert(A.block_id);
|
||
check(cert.has_value(), "aggregate quorum cert assembled");
|
||
check(cert && gate.verify_cert(*cert), "the cert independently RE-VERIFIES");
|
||
std::printf("[1/2] wave ACCEPT ⇒ gate emits a verifiable quorum cert => %s\n",
|
||
g_fail == 0 ? "PASS" : "FAIL");
|
||
}
|
||
|
||
// ── [2] Liveness gates the attempt: an undecided poll ⇒ no certification. ─
|
||
{
|
||
VotePosition B{};
|
||
B.block_id.fill(0x42);
|
||
B.height = 101;
|
||
B.round = 7;
|
||
const auto & item = B.block_id;
|
||
|
||
// 3 yes-rounds then a split (inconclusive) → confidence resets, undecided.
|
||
wave.record_round(item, 5, 5);
|
||
wave.record_round(item, 5, 5);
|
||
wave.record_round(item, 5, 5);
|
||
Decision d = wave.record_round(item, 2, 5); // inconclusive → reset
|
||
check(d == Decision::Undecided, "wave NOT decided after an inconclusive poll");
|
||
|
||
// The node does not certify an undecided block. Even if it (wrongly) tried,
|
||
// the gate would still need real votes — but the contract is: no decision,
|
||
// no certification attempt.
|
||
const bool would_certify = (d == Decision::Accept);
|
||
check(!would_certify, "no certification attempted for an undecided block");
|
||
check(!gate.is_final(B.block_id), "gate has no finality for B (never submitted/voted)");
|
||
std::printf("[2/2] undecided wave ⇒ no cert (liveness gates the attempt) => %s\n",
|
||
g_fail == 0 ? "PASS" : "FAIL");
|
||
}
|
||
|
||
std::printf("-------------------------------------------------------------------------\n");
|
||
if (g_fail) { std::printf("==== ENGINE INTEGRATION: FAIL (%d) ====\n", g_fail); return 1; }
|
||
std::printf("==== ENGINE INTEGRATION: 2/2 PASS — liveness decides, safety proves ====\n");
|
||
return 0;
|
||
}
|