Files
node/test/live_test.cpp
zeekayandHanzo Dev 375c83bf8c A height is decided on Go's rule, and an imported tip is anchored
The proposer was height mod n with no fallback, so one seat down halted the
chain at its turn, and a node that read an export could never build on it.

- windower: Go's proposervm windower (lux-rs/node src/proposer.rs), held to
  windower_test.go's vectors; a proposal is ML-DSA-signed by its slot's
  proposer and checked against it.
- engine: lux-rs/node's step rule, clause for clause — one proposer a window
  from a clock counted off the tip's time, one signature a height through a
  durable journal, the lowest live block that can still reach alpha after a
  settle window, no building beside a live block or f+1 votes, sibling caps,
  one settle deadline a height, the retry, and catch-up on certificates.
- the anchor: a gated validator signs one statement at its imported tip (T, H)
  under its own tag, through the same journal; alpha of them are the
  certificate that lifts the gate, and T+1 is built on it.
- the mesh binds an address, resolves names, accepts on its own thread and is
  swept again, so a seat that restarts is met again.
- tests: windower, statement, engine and live (five seats anchor Zoo
  mainnet's tip and build on it; one down; one that imported another tip; one
  that restarts and catches up).

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 16:13:14 -07:00

422 lines
17 KiB
C++

// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// live_test.cpp — a network of validators that imported a chain, run to see it
// continue: the anchor, the proposer windows, one seat down, a seat that
// imported something else, and a seat that stopped and came back.
//
// Every validator here is the daemon's own composition — keys a Signer made
// and published, a committee read from those lines, a Host on a real loopback
// socket greeting its peers with the post-quantum handshake, an Engine with its
// journal and ledger on disk — driven on its own thread. Only the chain is a
// stand-in (fake_chain.hpp), because what is under test is the engine and not
// the EVM; scripts/live.sh runs the same scenarios as processes over cevm and a
// real export.
//
// The history each validator imports is Zoo mainnet's, as a tip: height 799,
// 0xd6f92941…f72b8, the tip `zood --import-chain-data zoo-mainnet-200200.rlp`
// reaches.
#include "fake_chain.hpp"
#include "lux/node/engine.hpp"
#include "lux/node/host.hpp"
#include "lux/node/journal.hpp"
#include "lux/node/signer.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using namespace lux::node;
using namespace std::chrono_literals;
namespace {
int g_fail = 0;
void check(bool ok, const std::string& what) {
std::printf(" %s %s\n", ok ? "ok " : "FAIL", what.c_str());
std::fflush(stdout);
if (!ok) ++g_fail;
}
Id from_hex(const char* h) {
Id id{};
for (std::size_t i = 0; i < 32; ++i) {
unsigned v = 0;
std::sscanf(h + 2 + 2 * i, "%2x", &v);
id[i] = std::uint8_t(v);
}
return id;
}
std::string hex8(const Id& id) {
char b[9];
std::snprintf(b, sizeof b, "%02x%02x%02x%02x", id[0], id[1], id[2], id[3]);
return b;
}
// Zoo mainnet's imported tip.
const Id kZooTip = from_hex("0xd6f92941bb2ac91dfd2443f32b0d93f2730e6cc81ddb2edd3f35720a9e1f72b8");
constexpr std::uint64_t kZooHeight = 799;
constexpr std::uint64_t kZooTime = 1747000000; // a time long before the test runs
constexpr auto kWindow = 400ms;
// A port nothing listens on: bound and let go.
std::uint16_t free_port() {
const int fd = ::socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
::bind(fd, reinterpret_cast<sockaddr*>(&a), sizeof a);
socklen_t len = sizeof a;
::getsockname(fd, reinterpret_cast<sockaddr*>(&a), &len);
::close(fd);
return ntohs(a.sin_port);
}
struct Seat {
std::uint32_t index = 0;
std::filesystem::path dir;
std::unique_ptr<Signer> signer;
std::shared_ptr<test::Store> store;
std::uint16_t port = 0;
std::unique_ptr<Host> host;
std::unique_ptr<Engine> engine;
std::mutex guard;
std::thread driver;
std::atomic<bool> stop{false};
std::atomic<bool> running{false};
std::mutex mu; // guards what the driver records
std::map<std::uint64_t, Id> decided;
std::map<std::uint64_t, std::uint64_t> slots;
std::optional<std::pair<std::uint64_t, Id>> anchored;
std::vector<std::string> log;
};
class Cluster {
public:
Cluster(const std::string& name, std::uint32_t n) : chain_{} {
chain_.fill(0x20);
chain_[0] = 0x0c;
root_ = std::filesystem::temp_directory_path() / ("node-live-" + std::to_string(::getpid()) + "-" + name);
std::filesystem::remove_all(root_);
std::string text;
for (std::uint32_t i = 0; i < n; ++i) {
auto s = std::make_unique<Seat>();
s->index = i;
s->dir = root_ / ("v" + std::to_string(i));
s->signer = std::make_unique<Signer>(Signer::open(s->dir));
s->port = free_port();
text += s->signer->publish(chain_) + "\n";
seats_.push_back(std::move(s));
}
committee_ = Committee::read(text, chain_);
for (std::uint32_t i = 0; i < n; ++i) peers_[i] = PeerAddr{"127.0.0.1", seats_[i]->port};
}
~Cluster() {
for (auto& s : seats_) stop(s->index);
std::filesystem::remove_all(root_);
}
Seat& seat(std::uint32_t i) { return *seats_[i]; }
const Committee& committee() const { return committee_; }
const Id& chain() const { return chain_; }
// Give seat `i` its imported history.
void import(std::uint32_t i, const Id& tip) {
seats_[i]->store = test::Store::imported(tip, kZooHeight, kZooTime);
close_gate(seats_[i]->dir); // what the daemon does before the chain reads an export
}
// Bind every listed seat, then let each meet the others on its own thread
// and drive its engine until stopped.
void start(const std::vector<std::uint32_t>& which) {
for (auto i : which) {
Seat& s = *seats_[i];
HostConfig cfg;
cfg.index = i;
cfg.port = s.port;
cfg.link.me = s.signer->identity();
cfg.link.chain = chain_;
cfg.link.seat = [this](const Node& who) -> std::optional<std::uint32_t> {
const auto at = committee_.seat(who);
if (!at) return std::nullopt;
return std::uint32_t(*at);
};
s.host = std::make_unique<Host>(std::move(cfg));
s.host->listen_bind();
EngineConfig ec;
ec.committee = committee_;
ec.self = s.signer->node();
ec.sk = s.signer->secret();
ec.identity = s.signer->identity();
ec.chain = chain_;
ec.binding = Binding::Transport;
ec.window = kWindow;
ec.data = s.dir;
s.engine = std::make_unique<Engine>(std::make_unique<test::FakeChain>(s.store), *s.host, s.guard,
std::move(ec));
}
for (auto i : which) {
Seat& s = *seats_[i];
s.stop = false;
s.running = true;
s.driver = std::thread([this, &s] { drive(s); });
}
}
void stop(std::uint32_t i) {
Seat& s = *seats_[i];
if (!s.running) return;
s.stop = true;
if (s.driver.joinable()) s.driver.join();
s.engine.reset();
s.host.reset();
s.running = false;
}
// Wait until `ok` holds, polling, or `limit` passes.
bool until(const std::function<bool()>& ok, std::chrono::milliseconds limit) {
const auto end = std::chrono::steady_clock::now() + limit;
while (std::chrono::steady_clock::now() < end) {
if (ok()) return true;
std::this_thread::sleep_for(50ms);
}
return ok();
}
bool reached(std::uint32_t i, std::uint64_t h) {
const std::lock_guard<std::mutex> lock(seats_[i]->store->mu);
return seats_[i]->store->height >= h;
}
// The accepted id at every height in [from, to], the same on every listed
// seat — read off each seat's own chain.
bool agree(const std::vector<std::uint32_t>& which, std::uint64_t from, std::uint64_t to) {
for (std::uint64_t h = from; h <= to; ++h) {
std::optional<Id> first;
for (auto i : which) {
const std::lock_guard<std::mutex> lock(seats_[i]->store->mu);
const auto it = seats_[i]->store->at.find(h);
if (it == seats_[i]->store->at.end()) return false;
if (!first) first = it->second;
else if (*first != it->second) return false;
}
}
return true;
}
void dump(std::uint32_t i, std::size_t last = 12) {
Seat& s = *seats_[i];
const std::lock_guard<std::mutex> lock(s.mu);
const std::size_t from = s.log.size() > last ? s.log.size() - last : 0;
for (std::size_t k = from; k < s.log.size(); ++k) std::printf(" [%u] %s\n", i, s.log[k].c_str());
}
private:
void drive(Seat& s) {
const std::map<std::uint32_t, PeerAddr> others = [&] {
auto m = peers_;
m.erase(s.index);
return m;
}();
s.host->connect_mesh(others, 1500);
auto met = std::chrono::steady_clock::now();
while (!s.stop) {
if (std::chrono::steady_clock::now() - met >= kWindow * 2) {
s.host->sweep(others);
met = std::chrono::steady_clock::now();
}
const Step pass = s.engine->step();
if (!pass.events.empty()) {
const std::lock_guard<std::mutex> lock(s.mu);
for (const auto& e : pass.events) {
switch (e.what) {
case Event::What::Decided:
s.decided[e.height] = e.block;
s.slots[e.height] = e.slot.value_or(0);
s.log.push_back("decided " + std::to_string(e.height) + " " + hex8(e.block) +
" slot " + std::to_string(e.slot.value_or(0)) + " signers " +
std::to_string(e.signers));
break;
case Event::What::Anchored:
s.anchored = std::make_pair(e.height, e.block);
s.log.push_back("anchored " + std::to_string(e.height) + " " + hex8(e.block));
break;
default:
s.log.push_back(e.text);
}
}
}
if (pass.quiet && pass.events.empty()) std::this_thread::sleep_for(1ms);
}
}
Id chain_;
std::filesystem::path root_;
std::vector<std::unique_ptr<Seat>> seats_;
Committee committee_;
std::map<std::uint32_t, PeerAddr> peers_;
};
std::vector<std::uint32_t> all(std::uint32_t n) {
std::vector<std::uint32_t> v;
for (std::uint32_t i = 0; i < n; ++i) v.push_back(i);
return v;
}
} // namespace
int main() {
std::printf("====== node — a validator network continues an imported chain ======\n");
std::printf("five seats import Zoo mainnet to height 799 and build 800, 801, ...\n\n");
// [1] Five seats import the same export, anchor it, and build on it.
{
std::printf("[1] five seats, one export\n");
Cluster c("five", 5);
for (auto i : all(5)) c.import(i, kZooTip);
c.start(all(5));
const bool built = c.until([&] {
for (auto i : all(5)) if (!c.reached(i, 806)) return false;
return true;
}, 60s);
check(built, "[1] every seat reached height 806");
bool anchored = true;
for (auto i : all(5)) {
const std::lock_guard<std::mutex> lock(c.seat(i).mu);
anchored = anchored && c.seat(i).anchored && c.seat(i).anchored->first == kZooHeight &&
c.seat(i).anchored->second == kZooTip;
}
check(anchored, "[1] every seat anchored height 799 at Zoo mainnet's tip");
check(c.agree(all(5), 800, 806), "[1] and all five accepted the same block at 800..806");
for (auto i : all(5)) check(!gated(c.seat(i).dir), "[1] seat " + std::to_string(i) + "'s gate is lifted");
if (!built) for (auto i : all(5)) c.dump(i);
}
// [2] One seat down: four of five certify, and the heights whose first
// window belongs to the missing seat are built in the next one. Then the
// seat comes back, lifts its gate on the anchor its peers hold, and
// catches up.
{
std::printf("[2] five seats, one of them down\n");
Cluster c("down", 5);
const std::uint32_t down = 3;
for (auto i : all(5)) c.import(i, kZooTip);
std::vector<std::uint32_t> up{0, 1, 2, 4};
// The heights where the missing seat holds the first window.
std::vector<std::pair<Node, std::uint64_t>> w;
for (const auto& m : c.committee().members()) w.emplace_back(m.node, m.weight);
const Windower windower(c.chain(), w);
std::vector<std::uint64_t> its;
for (std::uint64_t h = 800; h < 1000 && its.size() < 2; ++h)
if (windower.expected(h, 0) == c.committee().members()[down].node) its.push_back(h);
const std::uint64_t target = its.back() + 1;
std::printf(" the missing seat leads slot 0 at heights %llu and %llu; running to %llu\n",
static_cast<unsigned long long>(its[0]), static_cast<unsigned long long>(its[1]),
static_cast<unsigned long long>(target));
c.start(up);
const bool built = c.until([&] {
for (auto i : up) if (!c.reached(i, target)) return false;
return true;
}, 90s);
check(built, "[2] four seats carried the chain to " + std::to_string(target));
check(c.agree(up, 800, target), "[2] and agree at every height");
{
const std::lock_guard<std::mutex> lock(c.seat(0).mu);
bool later = true;
for (auto h : its) later = later && c.seat(0).slots.count(h) && c.seat(0).slots[h] >= 1;
check(later, "[2] a height whose first window was the missing seat's was built in a later one");
}
if (!built) for (auto i : up) c.dump(i);
c.start({down});
const bool back = c.until([&] { return c.reached(down, target + 2) && c.agree({0, down}, 800, target + 2); },
60s);
check(back, "[2] the seat that was down came back, anchored, caught up, and went on with the others");
check(!gated(c.seat(down).dir), "[2] its gate is lifted");
if (!back) c.dump(down, 30);
}
// [3] A seat that imported a different tip cannot join the anchor, and
// cannot stop the four that agree.
{
std::printf("[3] one seat imported another tip\n");
Cluster c("stranger", 5);
const std::uint32_t odd = 4;
Id other = kZooTip;
other[31] ^= 0x01;
for (auto i : all(5)) c.import(i, i == odd ? other : kZooTip);
c.start(all(5));
std::vector<std::uint32_t> good{0, 1, 2, 3};
const bool built = c.until([&] {
for (auto i : good) if (!c.reached(i, 805)) return false;
return true;
}, 90s);
check(built, "[3] the four that imported the same tip built to 805");
check(c.agree(good, 800, 805), "[3] and agree");
{
const std::lock_guard<std::mutex> lock(c.seat(odd).mu);
check(!c.seat(odd).anchored && c.seat(odd).decided.empty(), "[3] the odd seat anchored nothing and decided nothing");
}
check(!c.reached(odd, 800) && gated(c.seat(odd).dir), "[3] its chain stayed at its own tip, gated");
c.stop(odd);
auto j = Journal::open(c.seat(odd).dir / "votes");
const auto anchor = j.standing(Slot{c.chain(), kZooHeight, 0});
const auto claim = anchor ? read(*anchor) : std::nullopt;
check(j.size() == 1 && claim && claim->kind == Kind::Anchor && claim->block == other,
"[3] its journal holds one statement: its anchor on its own tip, and no vote");
if (!built) for (auto i : all(5)) c.dump(i);
}
// [4] A seat stops and comes back: its journal holds it to what it signed,
// its peers meet it again, and it takes the blocks it missed on their
// certificates.
{
std::printf("[4] a seat restarts\n");
Cluster c("restart", 5);
for (auto i : all(5)) c.import(i, kZooTip);
c.start(all(5));
const bool first = c.until([&] { return c.reached(2, 803); }, 60s);
check(first, "[4] seat 2 reached 803");
c.stop(2);
std::vector<std::uint32_t> rest{0, 1, 3, 4};
const bool on = c.until([&] {
for (auto i : rest) if (!c.reached(i, 808)) return false;
return true;
}, 60s);
check(on, "[4] the other four went on to 808 without it");
c.start({2});
const bool back = c.until([&] { return c.reached(2, 811) && c.agree(all(5), 800, 811); }, 60s);
check(back, "[4] seat 2 caught up on certificates and went on to 811 with the others");
if (!back) c.dump(2, 30);
c.stop(2);
// One statement a height, before and after the restart.
auto j = Journal::open(c.seat(2).dir / "votes");
check(j.size() >= 1 && j.standing(Slot{c.chain(), kZooHeight, 0}).has_value(),
"[4] its journal reopens whole, anchor included");
}
std::printf("--------------------------------------------------------------------\n");
if (g_fail) { std::printf("==== node LIVE: FAIL (%d) ====\n", g_fail); return 1; }
std::printf("==== node LIVE: PASS — an imported chain continues, and one seat cannot stop it ====\n");
return 0;
}