mirror of
https://github.com/lux-cpp/node.git
synced 2026-10-30 14:48:15 +00:00
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]>
228 lines
10 KiB
C++
228 lines
10 KiB
C++
// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
//
|
|
// wire_vector_test.cpp — the bytes node puts on the wire, written down by hand
|
|
// from the SPEC and compared against what the real send path actually emits.
|
|
//
|
|
// A round-trip test proves the encoder agrees with itself. Swap two same-width
|
|
// fields, or move the length to little-endian on both sides, and it still passes
|
|
// while every Go peer reads a different message. The Go reference pins its own
|
|
// formats this way (consensus engine/chain/cert_wire_vector_test.go); this is the
|
|
// same discipline for the two things node owns end to end:
|
|
//
|
|
// the greeting's frame [4-byte BE length][that many bytes] (LP-10602)
|
|
// a vote frame [4-byte BE length][1-byte msg_type][payload]
|
|
// payload = three ZAP length-framed fields:
|
|
// [4-byte BE 32][block_id 32]
|
|
// [4-byte BE 48][voter pk 48]
|
|
// [4-byte BE 96][signature 96]
|
|
// 188 payload bytes, 193 on the wire, msg_type 0x11.
|
|
//
|
|
// Every byte below is a literal, and the bytes it is compared against come off a
|
|
// real socket written by the real transport — not from a re-encode. That layout
|
|
// is `github.com/luxfi/api/zap` verbatim (HeaderSize 5, big-endian length), which
|
|
// is what makes a C++ frame parse on a Go node.
|
|
|
|
#include "lux/node/host.hpp"
|
|
#include "lux/node/mesh_vote_transport.hpp"
|
|
#include "lux/node/statement.hpp"
|
|
#include "names.hpp"
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <map>
|
|
#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 lux::consensus;
|
|
|
|
namespace {
|
|
|
|
int g_fail = 0;
|
|
void check(bool ok, const std::string& what) {
|
|
if (!ok) { std::printf(" ASSERT FAILED: %s\n", what.c_str()); ++g_fail; }
|
|
}
|
|
|
|
std::string hex(const std::vector<std::uint8_t>& b, std::size_t upto) {
|
|
static const char* d = "0123456789abcdef";
|
|
std::string s;
|
|
for (std::size_t i = 0; i < b.size() && i < upto; ++i) {
|
|
s += d[b[i] >> 4];
|
|
s += d[b[i] & 0xF];
|
|
}
|
|
return s;
|
|
}
|
|
|
|
void append(std::vector<std::uint8_t>& v, std::initializer_list<std::uint8_t> bytes) {
|
|
v.insert(v.end(), bytes);
|
|
}
|
|
void repeat(std::vector<std::uint8_t>& v, std::uint8_t b, std::size_t n) {
|
|
v.insert(v.end(), n, b);
|
|
}
|
|
|
|
// Read exactly n bytes, or as many as arrive before the socket goes quiet.
|
|
std::vector<std::uint8_t> read_n(int fd, std::size_t n) {
|
|
std::vector<std::uint8_t> out;
|
|
out.reserve(n);
|
|
std::uint8_t tmp[512];
|
|
while (out.size() < n) {
|
|
const ssize_t r = ::recv(fd, tmp, std::min(sizeof tmp, n - out.size()), 0);
|
|
if (r <= 0) break;
|
|
out.insert(out.end(), tmp, tmp + r);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
std::printf("================ node — WIRE VECTOR (the bytes, from the spec) ================\n");
|
|
std::printf("what a Go peer reads off this socket, pinned byte for byte\n\n");
|
|
|
|
// ── the framing constants node depends on, as Go records them ────────────
|
|
// github.com/luxfi/api/zap: header_size 5, max_message_size 16777216,
|
|
// response_flag 0x80, error_flag 0x40, type_mask 0x3F. A service id must stay
|
|
// under 0x40 or both ends read it as a flag and dispatch it somewhere else.
|
|
{
|
|
check(lux::zap::HeaderSize == 5, "ZAP header is 5 bytes (4 BE length + 1 type)");
|
|
check(lux::zap::MaxMessageSize == 16u * 1024u * 1024u, "ZAP ceiling is 16 MiB");
|
|
check(lux::zap::MsgResponseFlag == 0x80 && lux::zap::MsgErrorFlag == 0x40 &&
|
|
lux::zap::MsgTypeMask == 0x3F, "the flag bits and the type mask are Go's");
|
|
check(lux::consensus::zap::kVoteMsgType < 0x40,
|
|
"the vote type fits the low six bits, so the flags OR in cleanly");
|
|
check(kMaxVoteFrame <= lux::zap::MaxMessageSize,
|
|
"the vote link's cap sits under the ZAP ceiling");
|
|
}
|
|
|
|
// ── the vote frame, as the real transport writes it ───────────────────────
|
|
{
|
|
int fds[2];
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { std::puts("socketpair"); return 2; }
|
|
|
|
MeshVoteTransport tx([](const SignedVote&) {});
|
|
tx.add_peer(fds[0], 1);
|
|
|
|
SignedVote v;
|
|
v.block_id.fill(0x11);
|
|
v.voter.fill(0x22);
|
|
v.sig.fill(0x33);
|
|
tx.broadcast(v);
|
|
|
|
// The golden frame, assembled from literal bytes.
|
|
std::vector<std::uint8_t> want;
|
|
append(want, {0x00, 0x00, 0x00, 0xBC}); // length 188, big-endian
|
|
append(want, {0x11}); // msg_type = kVoteMsgType
|
|
append(want, {0x00, 0x00, 0x00, 0x20}); // field length 32
|
|
repeat(want, 0x11, 32); // block_id
|
|
append(want, {0x00, 0x00, 0x00, 0x30}); // field length 48
|
|
repeat(want, 0x22, 48); // voter public key (compressed G1)
|
|
append(want, {0x00, 0x00, 0x00, 0x60}); // field length 96
|
|
repeat(want, 0x33, 96); // signature (compressed G2)
|
|
check(want.size() == 193, "the documented frame is 193 bytes on the wire");
|
|
|
|
const std::vector<std::uint8_t> got = read_n(fds[1], want.size());
|
|
check(got == want, "the frame the transport wrote matches the documented layout");
|
|
if (got != want) std::printf(" got %s\n want %s\n", hex(got, 24).c_str(), hex(want, 24).c_str());
|
|
check(got.size() == 193, "and is exactly 193 bytes, no padding, no trailer");
|
|
std::printf(" frame: len=0x000000BC type=0x11 payload=188 total=%zu\n %s...\n",
|
|
got.size(), hex(got, 20).c_str());
|
|
::close(fds[1]);
|
|
}
|
|
|
|
// ── the statement frame: what a validator signs, as the engine sends it ───
|
|
// lux-rs/node's vote frame, byte for byte: msg type 62 and three ZAP
|
|
// length-prefixed fields — the 226-byte message, the 20-byte node id, the
|
|
// 96-byte signature. 354 bytes of payload, 359 on the wire.
|
|
{
|
|
int fds[2];
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { std::puts("socketpair"); return 2; }
|
|
MeshVoteTransport tx([](const SignedVote&) {});
|
|
tx.add_peer(fds[0], 1);
|
|
|
|
Statement st;
|
|
st.message.fill(0x44);
|
|
st.node.fill(0x55);
|
|
st.sig.fill(0x66);
|
|
tx.gossip(kStatementMsgType, encode(st));
|
|
|
|
std::vector<std::uint8_t> want;
|
|
append(want, {0x00, 0x00, 0x01, 0x62}); // length 354, big-endian
|
|
append(want, {0x3E}); // msg_type = 62
|
|
append(want, {0x00, 0x00, 0x00, 0xE2}); // field length 226
|
|
repeat(want, 0x44, 226); // the signed message
|
|
append(want, {0x00, 0x00, 0x00, 0x14}); // field length 20
|
|
repeat(want, 0x55, 20); // the signer's node id
|
|
append(want, {0x00, 0x00, 0x00, 0x60}); // field length 96
|
|
repeat(want, 0x66, 96); // the signature
|
|
const std::vector<std::uint8_t> got = read_n(fds[1], want.size());
|
|
check(want.size() == 359 && got == want, "a statement is 359 bytes on the wire, laid out as Rust's vote");
|
|
if (got != want) std::printf(" got %s\n want %s\n", hex(got, 24).c_str(), hex(want, 24).c_str());
|
|
std::printf(" statement: len=0x00000162 type=0x3E payload=354 total=%zu\n", got.size());
|
|
::close(fds[1]);
|
|
}
|
|
|
|
// ── the peer handshake, as the real host dials it ─────────────────────────
|
|
{
|
|
// Stand in for validator 7's listener; a host at index 5 dials it.
|
|
const int srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
sockaddr_in a{};
|
|
a.sin_family = AF_INET;
|
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
a.sin_port = 0;
|
|
::bind(srv, reinterpret_cast<sockaddr*>(&a), sizeof a);
|
|
::listen(srv, 4);
|
|
socklen_t len = sizeof a;
|
|
::getsockname(srv, reinterpret_cast<sockaddr*>(&a), &len);
|
|
const std::uint16_t port = ntohs(a.sin_port);
|
|
|
|
HostConfig cfg;
|
|
cfg.index = 5;
|
|
cfg.port = 0;
|
|
const test::Names names(1);
|
|
cfg.link = names.link(0);
|
|
Host host(std::move(cfg));
|
|
host.listen_bind();
|
|
|
|
std::thread dial([&] {
|
|
std::map<std::uint32_t, PeerAddr> peers{{7, {"127.0.0.1", port}}};
|
|
host.connect_mesh(peers, 2000);
|
|
});
|
|
|
|
// The dialer no longer announces an index. It greets: a 4-byte
|
|
// big-endian length and that many bytes, and the bytes are a signed
|
|
// INIT (LP-10602 — pq_vector_test holds their contents to a handshake
|
|
// Go produced). What is pinned HERE is the framing: the length is big
|
|
// endian, it counts the body and nothing else, and there is no tag byte
|
|
// in front of it — the one difference from the vote frame below.
|
|
const int conn = ::accept(srv, nullptr, nullptr);
|
|
const std::vector<std::uint8_t> head = read_n(conn, 4);
|
|
const std::uint32_t announced =
|
|
(std::uint32_t(head[0]) << 24) | (std::uint32_t(head[1]) << 16) |
|
|
(std::uint32_t(head[2]) << 8) | std::uint32_t(head[3]);
|
|
const std::vector<std::uint8_t> body = read_n(conn, announced);
|
|
check(body.size() == announced, "the greeting's length counts the body, big-endian");
|
|
// 1 + 1 + 32 + 1 + 20 + (4+1952) + (4+1184) + (4+3309) = 6512.
|
|
check(announced == 6512, "and an ML-KEM-768 INIT under ML-DSA-65 is 6512 bytes of it");
|
|
check(body[0] == 0x01 && body[1] == 0x01,
|
|
"which begins with the version and the profile, not a message tag");
|
|
std::printf(" greeting: %s... %u bytes\n", hex(head, 4).c_str(), announced);
|
|
|
|
dial.join();
|
|
::close(conn);
|
|
::close(srv);
|
|
}
|
|
|
|
std::printf("--------------------------------------------------------------------------------\n");
|
|
if (g_fail) { std::printf("==== node WIRE VECTOR: FAIL (%d) ====\n", g_fail); return 1; }
|
|
std::printf("==== node WIRE VECTOR: PASS — the wire is what the spec says it is ====\n");
|
|
return 0;
|
|
}
|