// Hiercoin — Fractal Social Hierarchy UTXO with UBI by demurrage. // C++ port of the reference Go node (hiercoin.go); consensus-, wire- // and API-compatible. A complete node in one file: a civil-registry // Merkle tree, a UTXO set as a binary Merkle sum trie where all value // decays 20 %/year (the decay IS the basic income) and every UTXO // pays rent for its place in the trie (spendable = value − rent_owed; // at ≤ 0 the UTXO expires and the validator prunes it, collecting the // remainder), and slot-based consensus with elected validators: one // block per 60 s slot, the slot's validator drawn by // `rand mod committed` from an election trie of hash-onion vote // commitments, rebuilt each YEAR-long period from vote tokens claimed // against tree contribution. Plus wire format, an append-only block // log (re-verified on startup, truncatable at reorgs), a JSON API // with mempool and slot production, and a plain HTTP network layer: // static peers, block push + poll sync, spec fork choice, and // finality at the previous period boundary bounding every reorg. // // Dependencies: OpenSSL libcrypto (SHA-256, Ed25519) + POSIX. // // g++ -std=c++20 -O2 -pthread hiercoin.cpp -lcrypto -o hiercoin // // ./hiercoin keygen // ./hiercoin init -dir data // ./hiercoin run -dir data -listen 127.0.0.1:8080 [-peers http://host:8081,...] // ./hiercoin join -dir data2 -peer http://host:8080 // ./hiercoin sign -seed @data/validator.seed -msg // ./hiercoin replay -dir data (verify the log and exit) // ./hiercoin selftest (deterministic internal tests) // // API: GET /api/status /api/node/{key} /api/balance/{key} /api/block/{seq} /api/mempool // POST /api/tx/prepare (unsigned tx -> bytes to sign) // POST /api/tx (signed tx -> mempool) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // =============================================================== util using u8 = uint8_t; using u32 = uint32_t; using u64 = uint64_t; using u128 = unsigned __int128; using i64 = int64_t; using Bytes = std::vector; using Hash32 = std::array; using PubKey = std::array; using Sig = std::array; static const Hash32 zero32{}; static const PubKey ZeroKey{}; static const Sig ZeroSig{}; // Err is the single error/exception type. The Go node distinguishes // returned errors from panics (which kill the process); here both are // exceptions, caught at transaction/block/decode boundaries and turned // into rejections. On every input the Go node survives, the outcomes // are identical; on inputs that would panic Go, this node rejects // instead of crashing — strictly safer, and no divergence can arise // among surviving nodes. struct Err : std::runtime_error { explicit Err(const std::string& m) : std::runtime_error(m) {} }; static std::string hexEncode(const u8* p, size_t n) { static const char* d = "0123456789abcdef"; std::string s(2 * n, '0'); for (size_t i = 0; i < n; i++) { s[2 * i] = d[p[i] >> 4]; s[2 * i + 1] = d[p[i] & 15]; } return s; } static std::string hex(const Bytes& b) { return hexEncode(b.data(), b.size()); } template static std::string hex(const std::array& a) { return hexEncode(a.data(), N); } static std::string hexShort(const Hash32& h) { return hexEncode(h.data(), 8); } static std::string keyShort(const PubKey& k) { return hexEncode(k.data(), 4); } static int hexVal(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } // hexDecode decodes s; throws on bad characters or odd length. static Bytes hexDecode(const std::string& s) { if (s.size() % 2) throw Err("hex: odd length"); Bytes out(s.size() / 2); for (size_t i = 0; i < out.size(); i++) { int a = hexVal(s[2 * i]), b = hexVal(s[2 * i + 1]); if (a < 0 || b < 0) throw Err("hex: bad character"); out[i] = u8(a << 4 | b); } return out; } // hexN mirrors the Go helper: exactly n bytes or error. static Bytes hexN(const std::string& s, size_t n) { Bytes b = hexDecode(s); if (b.size() != n) throw Err("want " + std::to_string(n) + " bytes, got " + std::to_string(b.size())); return b; } template static std::array toArr(const Bytes& b) { std::array a{}; if (b.size() != N) throw Err("bad length"); memcpy(a.data(), b.data(), N); return a; } struct ArrHash { template size_t operator()(const std::array& a) const { u64 x; memcpy(&x, a.data(), 8); // keys/hashes are uniform return size_t(x); } }; // u128 decimal formatting / parsing (JSON amounts are decimal strings). static std::string u128str(u128 x) { if (x == 0) return "0"; char buf[40]; int i = 40; while (x > 0) { buf[--i] = char('0' + int(x % 10)); x /= 10; } return std::string(buf + i, buf + 40); } // i-flavoured print for the (rare) signed spendable values. static std::string spendStr(u128 value, u128 rent) { if (value >= rent) return u128str(value - rent); return "-" + u128str(rent - value); } static const u128 U128_MAX = ~(u128)0; // parseAmount mirrors Go's pAmount: base-10 big.Int, non-negative, // ≤ 128 bits. big.Int.SetString accepts an optional leading sign. static u128 parseAmount(const std::string& s) { size_t i = 0; if (i < s.size() && s[i] == '+') i++; if (i >= s.size()) throw Err("bad amount"); u128 v = 0; for (; i < s.size(); i++) { if (s[i] < '0' || s[i] > '9') throw Err("bad amount"); u8 d = u8(s[i] - '0'); if (v > (U128_MAX - d) / 10) throw Err("bad amount"); // >128 bits v = v * 10 + d; } return v; } static u64 parseU64(const std::string& s) { if (s.empty()) throw Err("bad number"); u64 v = 0; for (char c : s) { if (c < '0' || c > '9') throw Err("bad number"); u64 d = u64(c - '0'); if (v > (UINT64_MAX - d) / 10) throw Err("number overflow"); v = v * 10 + d; } return v; } // =============================================================== enc // Buf is a tiny canonical encoder: fixed-width big-endian fields, // u32 length prefixes for lists. All hashes and signatures in the // system are computed over encodings produced by this type, so the // byte layout here IS the wire/consensus format (identical to the Go // node's `buf`). struct Buf { Bytes b; void u8b(u8 x) { b.push_back(x); } void u32b(u32 x) { b.push_back(u8(x >> 24)); b.push_back(u8(x >> 16)); b.push_back(u8(x >> 8)); b.push_back(u8(x)); } void u64b(u64 x) { for (int i = 7; i >= 0; i--) b.push_back(u8(x >> (8 * i))); } // u128b writes exactly 16 big-endian bytes (amounts are unsigned // 128-bit by definition; the type enforces the range the Go node // panics on). void u128b(u128 x) { for (int i = 15; i >= 0; i--) b.push_back(u8(x >> (8 * i))); } void bytes(const u8* p, size_t n) { b.insert(b.end(), p, p + n); } void bytes(const Bytes& p) { bytes(p.data(), p.size()); } template void bytes(const std::array& a) { bytes(a.data(), N); } void boolb(bool v) { u8b(v ? 1 : 0); } }; // H is SHA-256 over the given bytes. static Hash32 H(const u8* p, size_t n) { Hash32 o{}; unsigned int len = 32; EVP_Digest(p, n, o.data(), &len, EVP_sha256(), nullptr); return o; } static Hash32 H(const Bytes& b) { return H(b.data(), b.size()); } // Two-part convenience (hash onions: H(candidate || layer)). static Hash32 H2(const PubKey& a, const Hash32& b) { u8 buf[64]; memcpy(buf, a.data(), 32); memcpy(buf + 32, b.data(), 32); return H(buf, 64); } // ============================================================ amount // Fixed-point scale: 1 TOKEN = 10^16 base units. Decay powers are // always ≤ SCALE and therefore fit in a u64; full amounts are u128. static const u64 Scale = 10'000'000'000'000'000ULL; static const u64 TOKEN = 10'000'000'000'000'000ULL; // Per-second decay factor at scale 10^16: the largest integer where // power(decay, YEAR) < 0.8 × SCALE. 20% per year. Fixed by the spec. static const u64 DecayPerSecond = 9'999'999'929'290'076ULL; // RentPerSecond is the trie rent: 1000 base units per second per UTXO. static const u64 RentPerSecond = 1000; // rentDenom = SCALE − decay, the per-second fixed-point loss. static const u64 rentDenom = Scale - DecayPerSecond; static const u64 SecondsPerYear = 31'557'600ULL; // Julian year // Consensus timing, fixed by the spec (see the Go node for the full // divisibility rationale: slot | period | NormPeriod). static const u64 SlotSeconds = 60; static const u64 PeriodSeconds = SecondsPerYear; // MaxMix caps a vote token's total hop budget (see spec / Go node). static const u32 MaxMix = 10; // norm_time sits on multiples of 4 × YEAR from Unix 0. static const u64 NormPeriod = 4 * SecondsPerYear; static u64 normTimeFor(u64 t) { return t - t % NormPeriod; } // --- wide helpers: 128×64 → 192-bit multiply, 192 ÷ 64 divide. // These implement THE rounding rule of the system — floor at every // step — with results bit-identical to Go's math/big Quo on // non-negative operands. static void mul128x64(u128 a, u64 b, u64 out[3]) { u64 a0 = u64(a), a1 = u64(a >> 64); u128 p0 = u128(a0) * b; u128 p1 = u128(a1) * b + (p0 >> 64); out[0] = u64(p0); out[1] = u64(p1); out[2] = u64(p1 >> 64); } static void div192by64(const u64 in[3], u64 d, u64 q[3]) { u128 rem = 0; for (int i = 2; i >= 0; i--) { u128 cur = (rem << 64) | in[i]; q[i] = u64(cur / d); rem = cur % d; } } // mulScale computes floor(a*b / Scale) for b ≤ Scale (every call site // passes a decay power). The quotient is then ≤ a, so it fits u128. static u128 mulScale(u128 a, u64 b) { u64 p[3], q[3]; mul128x64(a, b, p); div192by64(p, Scale, q); if (q[2] != 0) throw Err("amount out of u128 range"); return (u128(q[1]) << 64) | q[0]; } static std::mutex powMu; static std::unordered_map powCache; // DecayPow returns decay^dt at scale 10^16, computed by binary // exponentiation with floor rounding at every step. Deterministic and // bit-reproducible; all values ≤ Scale, hence u64. static u64 DecayPow(u64 dt) { { std::lock_guard g(powMu); auto it = powCache.find(dt); if (it != powCache.end()) return it->second; } u64 res = Scale, base = DecayPerSecond; for (u64 e = dt; e > 0; e >>= 1) { if (e & 1) res = u64(u128(res) * base / Scale); if (e > 1) base = u64(u128(base) * base / Scale); } { std::lock_guard g(powMu); powCache[dt] = res; } return res; } // Normalize converts a real amount at time t to its normalized value // at reference time norm: floor(amount * Scale / decay^(t-norm)). // Values that exceed 128 bits are an overflow: the Go node either // errors at the UTXO insert or panics at the next u128 encode; here // the throw surfaces at the same transaction/block boundary. static u128 Normalize(u128 amount, u64 t, u64 norm) { if (t < norm) throw Err("Normalize: time before norm_time"); u64 p = DecayPow(t - norm); u64 m[3], q[3]; mul128x64(amount, Scale, m); div192by64(m, p, q); if (q[2] != 0) throw Err("normalized amount out of range"); return (u128(q[1]) << 64) | q[0]; } // ValueAt converts a normalized value back to its real value at T: // floor(norm * decay^(T-normTime) / Scale). static u128 ValueAt(u128 normVal, u64 T, u64 normTime) { if (T < normTime) throw Err("ValueAt: time before norm_time"); return mulScale(normVal, DecayPow(T - normTime)); } // RentOwed: rent × (SCALE − decay^dt) / (SCALE − decay), floor. // RentOwed(0) = 0, RentOwed(1) = rent; bounded by rent×SCALE/denom. static u128 RentOwed(u64 dt) { if (dt == 0) return 0; u128 n = u128(Scale - DecayPow(dt)) * RentPerSecond; return n / rentDenom; } static u128 tokens(u64 n) { return u128(n) * TOKEN; } // ClaimableAt: contribution × TOKEN × (1 − decay^(T − last_ubi)). static u128 ClaimableAt(u64 own, u64 lastUBI, u64 T) { if (T <= lastUBI || own == 0) return 0; u128 ct = tokens(own); return ct - mulScale(ct, DecayPow(T - lastUBI)); } // checked u128 add — Go's big.Int grows silently and the overflow is // caught at the next 128-bit encode or BitLen check; a throw here // lands at the same boundary. static u128 addChecked(u128 a, u128 b, const char* what) { u128 s = a + b; if (s < a) throw Err(std::string(what) + " overflow"); return s; } // ============================================================== keys struct EvpFree { void operator()(EVP_PKEY* p) const { EVP_PKEY_free(p); } void operator()(EVP_MD_CTX* c) const { EVP_MD_CTX_free(c); } }; // A private key is carried as its 32-byte seed (same as Go's // ed25519.PrivateKey.Seed()); OpenSSL's raw Ed25519 private key IS // the seed. using Seed = std::array; static PubKey PubFromSeed(const Seed& seed) { std::unique_ptr k( EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, seed.data(), 32)); if (!k) throw Err("ed25519: bad seed"); PubKey pub{}; size_t len = 32; if (EVP_PKEY_get_raw_public_key(k.get(), pub.data(), &len) != 1 || len != 32) throw Err("ed25519: pubkey derivation failed"); return pub; } static Seed GenSeed() { Seed s{}; if (RAND_bytes(s.data(), 32) != 1) throw Err("rand failed"); return s; } static Sig SignMsg(const Seed& seed, const u8* msg, size_t n) { std::unique_ptr k( EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, seed.data(), 32)); if (!k) throw Err("ed25519: bad seed"); std::unique_ptr ctx(EVP_MD_CTX_new()); Sig sig{}; size_t sl = 64; if (EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, k.get()) != 1 || EVP_DigestSign(ctx.get(), sig.data(), &sl, msg, n) != 1 || sl != 64) throw Err("ed25519: sign failed"); return sig; } static Sig SignMsg(const Seed& seed, const Bytes& m) { return SignMsg(seed, m.data(), m.size()); } static Sig SignMsg(const Seed& seed, const Hash32& h) { return SignMsg(seed, h.data(), 32); } static bool VerifySig(const PubKey& pub, const u8* msg, size_t n, const Sig& sig) { std::unique_ptr k( EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub.data(), 32)); if (!k) return false; std::unique_ptr ctx(EVP_MD_CTX_new()); if (EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, k.get()) != 1) return false; return EVP_DigestVerify(ctx.get(), sig.data(), 64, msg, n) == 1; } static bool VerifySig(const PubKey& pub, const Hash32& h, const Sig& sig) { return VerifySig(pub, h.data(), 32, sig); } static bool VerifySig(const PubKey& pub, const Bytes& m, const Sig& sig) { return VerifySig(pub, m.data(), m.size(), sig); } // ================================================================ tx // Opcodes. Every signature in the system covers an encoding that // begins with an opcode ("all signatures include an opcode"). static const u8 OpClaim = 0x01; static const u8 OpTransfer = 0x02; static const u8 OpAdd = 0x03; static const u8 OpRemove = 0x04; static const u8 OpMove = 0x05; static const u8 OpLeave = 0x06; static const u8 OpRekey = 0x07; static const u8 OpPrune = 0x08; static const u8 OpVote = 0x09; // election mix/commit static const u8 OpVoteClaim = 0x0A; // mint this period's vote tokens static const u8 OpHeader = 0xF0; // Output as it appears inside a transaction: consensus stamps `time` // at processing, so transactions only carry (amount, owner). struct Output { u128 amount = 0; PubKey owner{}; }; static void encodeOutputs(Buf& w, const std::vector& outs) { w.u32b(u32(outs.size())); for (const auto& o : outs) { w.u128b(o.amount); w.bytes(o.owner); } } struct Outpoint { Hash32 tx{}; u32 index = 0; bool operator==(const Outpoint& o) const { return tx == o.tx && index == o.index; } bool operator<(const Outpoint& o) const { int c = memcmp(tx.data(), o.tx.data(), 32); if (c != 0) return c < 0; return index < o.index; } }; struct OutpointHash { size_t operator()(const Outpoint& o) const { u64 x; memcpy(&x, o.tx.data(), 8); return size_t(x ^ (u64(o.index) * 0x9E3779B97F4A7C15ULL)); } }; // Tx is anything that can be included in a block. ID excludes all // signatures (anti-malleability); for single-signer transactions it // doubles as the signing hash. struct Tx { virtual ~Tx() = default; virtual u8 opc() const = 0; virtual Bytes body() const = 0; Hash32 id() const { return H(body()); } Hash32 sigHash() const { return id(); } }; using TxPtr = std::shared_ptr; // ---------------------------------------------------------------- Claim // Claim mints accrued UBI for a tree node with contribution > 0. struct Claim : Tx { PubKey key{}; u128 amount = 0; u64 nonce = 0; Sig sig{}; u8 opc() const override { return OpClaim; } Bytes body() const override { Buf w; w.u8b(OpClaim); w.bytes(key); w.u128b(amount); w.u64b(nonce); return w.b; } }; // -------------------------------------------------------------- Transfer // Transfer spends UTXOs and creates new ones. One signature per input; // inputs may have different owners. struct Transfer : Tx { std::vector inputs; std::vector outputs; std::vector sigs; // one per input, over sigHash u8 opc() const override { return OpTransfer; } Bytes body() const override { Buf w; w.u8b(OpTransfer); w.u32b(u32(inputs.size())); for (const auto& in : inputs) { w.bytes(in.tx); w.u32b(in.index); } encodeOutputs(w, outputs); return w.b; } }; // ----------------------------------------------------------------- Prune // Prune removes expired UTXOs (spendable(T) ≤ 0 at block time). No // signatures: validity is objective. struct Prune : Tx { std::vector inputs; u8 opc() const override { return OpPrune; } Bytes body() const override { Buf w; w.u8b(OpPrune); w.u32b(u32(inputs.size())); for (const auto& in : inputs) { w.bytes(in.tx); w.u32b(in.index); } return w.b; } }; // ------------------------------------------------------------------- Add // NodeTemplate describes a subtree being added; see the Go node for // the field semantics (consensus supersedes last_ubi/last_vote and // recomputes tree_ubi on import). struct NodeTemplate { PubKey key{}; bool leaf = false; u64 nonce = 0, lastUBI = 0, lastVote = 0, treeCount = 0; u128 treeUBI = 0; std::vector children; int countNodes() const { int n = 1; for (const auto& c : children) n += c.countNodes(); return n; } Hash32 hash() const; // defined after nodeHash }; // Add attaches a child subtree under a parent. struct Add : Tx { PubKey parent{}, childKey{}; Hash32 hashv{}; u64 nonce = 0; // parent's nonce u64 deadline = 0; // block time after which consent expires Sig consent{}; // by childKey over ConsentMsg Sig sig{}; // by parent over sigHash NodeTemplate tmpl; // transport of the subtree data; bound via hashv u8 opc() const override { return OpAdd; } Bytes body() const override { Buf w; w.u8b(OpAdd); w.bytes(parent); w.bytes(childKey); w.bytes(hashv); w.u64b(nonce); w.u64b(deadline); return w.b; } }; // ConsentMsg: child signs hash + deadline + parent pubkey. static Bytes ConsentMsg(const Hash32& hash, u64 deadline, const PubKey& parent) { Buf w; w.bytes(hash); w.u64b(deadline); w.bytes(parent); return w.b; } // ---------------------------------------------------------------- Remove // Remove deletes a child and its entire subtree; accrued UBI for every // person in the subtree is auto-minted, in pre-order. struct Remove : Tx { PubKey parent{}, child{}; u64 nonce = 0; // parent's nonce Sig sig{}; u8 opc() const override { return OpRemove; } Bytes body() const override { Buf w; w.u8b(OpRemove); w.bytes(parent); w.bytes(child); w.u64b(nonce); return w.b; } }; // ----------------------------------------------------------------- Rekey struct Rekey : Tx { PubKey oldKey{}, newKey{}; u64 nonce = 0; Sig sig{}; // by oldKey u8 opc() const override { return OpRekey; } Bytes body() const override { Buf w; w.u8b(OpRekey); w.bytes(oldKey); w.bytes(newKey); w.u64b(nonce); return w.b; } }; // ------------------------------------------------------------------ Move // Move transfers a child (and its subtree) to a new parent; state is // fully preserved. struct Move : Tx { PubKey child{}, newParent{}; u64 nonce = 0; // new parent's nonce u64 deadline = 0; Sig consent{}; // by child over MoveConsentMsg Sig sig{}; // by newParent u8 opc() const override { return OpMove; } Bytes body() const override { Buf w; w.u8b(OpMove); w.bytes(child); w.bytes(newParent); w.u64b(nonce); w.u64b(deadline); return w.b; } }; // MoveConsentMsg: child signs deadline + new_parent. static Bytes MoveConsentMsg(u64 deadline, const PubKey& newParent) { Buf w; w.u64b(deadline); w.bytes(newParent); return w.b; } // ----------------------------------------------------------------- Leave // Leave: like Remove but initiated by the child. struct Leave : Tx { PubKey child{}; u64 nonce = 0; // child's nonce Sig sig{}; u8 opc() const override { return OpLeave; } Bytes body() const override { Buf w; w.u8b(OpLeave); w.bytes(child); w.u64b(nonce); return w.b; } }; // ============================================================== tree // PNode is a node in the people tree; Own is the node's own person // contribution, OwnUBI = Normalize(Own × TOKEN, LastUBI, norm_time). struct PNode { PubKey key{}; bool leaf = false; u64 nonce = 0, lastUBI = 0, lastVote = 0, own = 0; u128 ownUBI = 0; std::vector children; PNode* parent = nullptr; u64 treeCount = 0; u128 treeUBI = 0; Hash32 hashv{}; void recompute(); }; // nodeHash is the spec's H(Node): SHA-256(key || leaf || children || // nonce || last_ubi || last_vote || tree_count || tree_ubi). static Hash32 nodeHash(const PubKey& key, const std::vector& childHashes, bool leaf, u64 nonce, u64 lastUBI, u64 lastVote, u64 treeCount, u128 treeUBI) { Buf w; w.bytes(key); w.boolb(leaf); for (const auto& h : childHashes) w.bytes(h); w.u64b(nonce); w.u64b(lastUBI); w.u64b(lastVote); w.u64b(treeCount); w.u128b(treeUBI); return H(w.b); } Hash32 NodeTemplate::hash() const { std::vector ch; ch.reserve(children.size()); for (const auto& c : children) ch.push_back(c.hash()); return nodeHash(key, ch, leaf, nonce, lastUBI, lastVote, treeCount, treeUBI); } void PNode::recompute() { u64 tc = own; u128 tu = ownUBI; std::vector ch; ch.reserve(children.size()); for (const auto* c : children) { tc += c->treeCount; tu = addChecked(tu, c->treeUBI, "tree_ubi"); ch.push_back(c->hashv); } treeCount = tc; treeUBI = tu; hashv = nodeHash(key, ch, leaf, nonce, lastUBI, lastVote, treeCount, treeUBI); } // RemovedPerson: person entry collected while removing a subtree, // used to auto-mint accrued UBI. struct RemovedPerson { PubKey key{}; u64 own = 0, lastUBI = 0; }; static const int maxTemplateNodes = 4096; // PeopleTree is the population register. struct PeopleTree { PNode* root = nullptr; std::unordered_map byKey; PeopleTree() = default; PeopleTree(const PeopleTree&) = delete; PeopleTree& operator=(const PeopleTree&) = delete; ~PeopleTree() { freeSub(root); } static void freeSub(PNode* n) { if (!n) return; for (auto* c : n->children) freeSub(c); delete n; } // NewPeopleTree: single root node whose UBI accrual starts at t0. static std::unique_ptr make(const PubKey& rootKey, u64 own, u64 t0, u64 norm) { auto t = std::make_unique(); auto* r = new PNode(); r->key = rootKey; r->own = own; r->lastUBI = t0; r->lastVote = t0; // same entry-stamp rule as Add: first vote next period r->ownUBI = Normalize(tokens(own), t0, norm); r->recompute(); t->root = r; t->byKey[rootKey] = r; return t; } PNode* get(const PubKey& k) const { auto it = byKey.find(k); return it == byKey.end() ? nullptr : it->second; } Hash32 rootHash() const { return root->hashv; } u64 population() const { return root->treeCount; } // Bubble recomputes aggregates and hashes from n up to the root. void bubble(PNode* n) { for (; n; n = n->parent) n->recompute(); } // UnclaimedAt: tree_count × TOKEN − tree_ubi × decay^(T − norm), // clamped at 0. u128 unclaimedAt(u64 T, u64 norm) const { u128 total = tokens(root->treeCount); u128 v = ValueAt(root->treeUBI, T, norm); return total > v ? total - v : 0; } // checkTemplate: key uniqueness (globally and within the // template), leaf consistency, tree_count consistency, size cap. void checkTemplate(const NodeTemplate& tpl) const { if (tpl.countNodes() > maxTemplateNodes) throw Err("template too large"); std::unordered_set seen; std::function walk = [&](const NodeTemplate& n) { if (seen.count(n.key)) throw Err("duplicate key in template: " + keyShort(n.key)); if (byKey.count(n.key)) throw Err("key already in tree: " + keyShort(n.key)); seen.insert(n.key); if (n.leaf && !n.children.empty()) throw Err("leaf node with children"); u64 sum = 0; for (const auto& c : n.children) { walk(c); u64 s2 = sum + c.treeCount; if (s2 < sum) throw Err("tree_count overflow at " + keyShort(n.key)); sum = s2; } if (sum > n.treeCount) throw Err("tree_count " + std::to_string(n.treeCount) + " below children sum " + std::to_string(sum) + " at " + keyShort(n.key)); }; walk(tpl); } // doAdd validates and attaches a subtree under parentKey; last_ubi // and last_vote are set to blockTime for every node (per spec) and // tree_ubi is recomputed. The caller increments the parent's nonce // BEFORE calling (a single bubble covers everything). void doAdd(const PubKey& parentKey, const NodeTemplate& tpl, u64 blockTime, u64 norm) { PNode* p = get(parentKey); if (!p) throw Err("parent not in tree"); if (p->leaf) throw Err("parent is a leaf"); checkTemplate(tpl); std::function build = [&](const NodeTemplate& tp, PNode* parent) -> PNode* { u64 sum = 0; for (const auto& c : tp.children) sum += c.treeCount; u64 own = tp.treeCount - sum; // ≥ 0, ensured by checkTemplate auto* n = new PNode(); n->key = tp.key; n->leaf = tp.leaf; n->nonce = tp.nonce; n->own = own; n->lastUBI = blockTime; n->lastVote = blockTime; n->ownUBI = Normalize(tokens(own), blockTime, norm); n->parent = parent; for (const auto& c : tp.children) n->children.push_back(build(c, n)); n->recompute(); byKey[n->key] = n; return n; }; PNode* child = build(tpl, p); p->children.push_back(child); bubble(p); } // doRemove detaches childKey's subtree from parentKey and returns // all persons (own > 0) in pre-order. The caller increments the // parent's nonce before calling. std::vector doRemove(const PubKey& parentKey, const PubKey& childKey) { PNode* p = get(parentKey); if (!p) throw Err("parent not in tree"); PNode* c = get(childKey); if (!c) throw Err("child not in tree"); if (c->parent != p) throw Err("not a child of parent"); std::vector persons; std::function walk = [&](PNode* n) { if (n->own > 0) persons.push_back({n->key, n->own, n->lastUBI}); byKey.erase(n->key); for (auto* ch : n->children) walk(ch); }; walk(c); auto& pc = p->children; pc.erase(std::find(pc.begin(), pc.end(), c)); c->parent = nullptr; freeSub(c); bubble(p); return persons; } // doRekey changes a node's key; the node's nonce is incremented // here (the caller verifies the signature). void doRekey(const PubKey& oldK, const PubKey& newK) { PNode* n = get(oldK); if (!n) throw Err("node not in tree"); if (byKey.count(newK)) throw Err("new key already in tree"); byKey.erase(oldK); n->key = newK; n->nonce++; byKey[newK] = n; bubble(n); } // doMove detaches childKey from its current parent and attaches it // under newParentKey; all state is preserved. void doMove(const PubKey& childKey, const PubKey& newParentKey) { PNode* c = get(childKey); if (!c) throw Err("child not in tree"); if (!c->parent) throw Err("cannot move the root"); PNode* np = get(newParentKey); if (!np) throw Err("new parent not in tree"); if (np->leaf) throw Err("new parent is a leaf"); for (PNode* p = np; p; p = p->parent) if (p == c) throw Err("new parent is inside child's subtree"); PNode* old = c->parent; auto& oc = old->children; oc.erase(std::find(oc.begin(), oc.end(), c)); c->parent = np; np->children.push_back(c); bubble(old); bubble(np); } std::unique_ptr clone() const { auto t = std::make_unique(); std::function cp = [&](const PNode* n, PNode* parent) -> PNode* { auto* c = new PNode(*n); // copies scalars incl. hash c->children.clear(); c->parent = parent; for (const auto* ch : n->children) c->children.push_back(cp(ch, c)); t->byKey[c->key] = c; return c; }; t->root = cp(root, nullptr); return t; } }; // ============================================================== utxo // Entry is an unspent output as stored in the set; Norm is the amount // normalized to the current norm_time and is what the trie sums. struct Entry { Outpoint op{}; u128 amount = 0; u128 norm = 0; u64 time = 0; // creation (block) time PubKey owner{}; // Value: gross value at T = amount × decay^(T − time) / SCALE. u128 value(u64 T) const { if (T < time) throw Err("Entry.Value: time before creation"); return mulScale(amount, DecayPow(T - time)); } // Rent: accumulated rent owed at T. u128 rent(u64 T) const { return RentOwed(T - time); } // Spendable may be ≤ 0 — that is expiry; callers compare the two // sides (value vs rent) since amounts here are unsigned. bool expired(u64 T) const { return value(T) <= rent(T); } }; // The UTXO commitment is a binary Merkle sum trie keyed by // H(tx_hash || index); internal nodes sum their children. Canonical // for a given key set. struct tnode { bool leaf = false; Hash32 key{}; // leaf only Entry* e = nullptr; // leaf only (owned by the UTXOSet map) tnode* l = nullptr; tnode* r = nullptr; u128 sum = 0; Hash32 h{}; }; static int bitAt(const Hash32& k, int d) { return (k[d >> 3] >> (7 - (d & 7))) & 1; } static void tfix(tnode* n) { if (n->leaf) { n->sum = n->e->norm; Buf w; w.u8b(0x00); w.bytes(n->key); w.u128b(n->e->amount); w.u64b(n->e->time); w.bytes(n->e->owner); n->h = H(w.b); return; } n->sum = 0; Hash32 lh = zero32, rh = zero32; if (n->l) { n->sum = addChecked(n->sum, n->l->sum, "utxo sum"); lh = n->l->h; } if (n->r) { n->sum = addChecked(n->sum, n->r->sum, "utxo sum"); rh = n->r->h; } Buf w; w.u8b(0x01); w.bytes(lh); w.bytes(rh); w.u128b(n->sum); n->h = H(w.b); } static tnode* tLeaf(const Hash32& key, Entry* e) { auto* n = new tnode(); n->leaf = true; n->key = key; n->e = e; tfix(n); return n; } // splitLeaves builds the internal chain from depth d down to the // first bit where the two keys diverge. static tnode* splitLeaves(tnode* a, tnode* b, int d) { auto* in = new tnode(); int ba = bitAt(a->key, d), bb = bitAt(b->key, d); if (ba == bb) { tnode* c = splitLeaves(a, b, d + 1); if (ba == 0) in->l = c; else in->r = c; } else { if (ba == 0) { in->l = a; in->r = b; } else { in->l = b; in->r = a; } } tfix(in); return in; } static tnode* tInsertRec(tnode* n, int d, tnode* lf) { if (!n) return lf; if (n->leaf) { if (n->key == lf->key) throw Err("duplicate utxo key"); return splitLeaves(n, lf, d); } if (bitAt(lf->key, d) == 0) n->l = tInsertRec(n->l, d + 1, lf); else n->r = tInsertRec(n->r, d + 1, lf); tfix(n); return n; } // tDeleteRec removes key and collapses now-redundant internals. // Returns {new subtree root, removed leaf (caller frees)}. static std::pair tDeleteRec(tnode* n, int d, const Hash32& key) { if (!n) return {nullptr, nullptr}; if (n->leaf) { if (n->key == key) return {nullptr, n}; return {n, nullptr}; } tnode* rem = nullptr; if (bitAt(key, d) == 0) std::tie(n->l, rem) = tDeleteRec(n->l, d + 1, key); else std::tie(n->r, rem) = tDeleteRec(n->r, d + 1, key); if (!rem) return {n, nullptr}; if (!n->l && !n->r) { delete n; return {nullptr, rem}; } if (!n->l && n->r->leaf) { tnode* up = n->r; delete n; return {up, rem}; } if (!n->r && n->l->leaf) { tnode* up = n->l; delete n; return {up, rem}; } tfix(n); return {n, rem}; } static void tFree(tnode* n) { if (!n) return; tFree(n->l); tFree(n->r); delete n; } static Hash32 opKey(const Outpoint& o) { Buf w; w.bytes(o.tx); w.u32b(o.index); return H(w.b); } // UTXOSet combines the Merkle sum trie (commitment) with a direct map // (O(1) validation lookups). Both are kept in sync. struct UTXOSet { tnode* root = nullptr; std::unordered_map entries; UTXOSet() = default; UTXOSet(const UTXOSet&) = delete; UTXOSet& operator=(const UTXOSet&) = delete; ~UTXOSet() { tFree(root); for (auto& kv : entries) delete kv.second; } void insert(const Outpoint& o, u128 amount, u128 norm, u64 time, const PubKey& owner) { if (entries.count(o)) throw Err("outpoint already exists"); // (amount/norm range checks are enforced by the u128 type and // by Normalize; mirrors the Go BitLen checks.) auto* e = new Entry{o, amount, norm, time, owner}; tnode* lf = tLeaf(opKey(o), e); try { root = tInsertRec(root, 0, lf); } catch (...) { delete lf; delete e; throw; } entries[o] = e; } Entry* get(const Outpoint& o) const { auto it = entries.find(o); return it == entries.end() ? nullptr : it->second; } // spend removes the entry from trie + map. The Entry object is // freed: all call sites read the entry's fields before spending. void spend(const Outpoint& o) { auto it = entries.find(o); if (it == entries.end()) throw Err("output missing or already spent"); auto [nr, rem] = tDeleteRec(root, 0, opKey(o)); if (!rem) throw Err("trie desync"); delete rem; root = nr; delete it->second; entries.erase(it); } u128 sum() const { return root ? root->sum : 0; } Hash32 rootHash() const { return root ? root->h : zero32; } size_t len() const { return entries.size(); } template void forEach(F f) const { for (const auto& kv : entries) f(kv.second); } std::unique_ptr clone() const { auto c = std::make_unique(); for (const auto& kv : entries) { const Entry* e = kv.second; c->insert(e->op, e->amount, e->norm, e->time, e->owner); } return c; } }; // ========================================================== vote trie // VoteEntry is one entry in an election trie, keyed like a UTXO by the // (tx, index) that created it. Uncommitted: {amount, owner, mixed}. // Committed: {commit, owner?}, amount implicitly 1, locked. struct VoteEntry { Outpoint op{}; bool committed = false; u64 amount = 0; // uncommitted only (committed is implicitly 1) PubKey owner{}; // uncommitted: required; committed: fee-share hint bool hasOwner = false; // committed only u32 mixed = 0; // uncommitted entries only: 1..MaxMix Hash32 commit{}; }; // vnode mirrors tnode with a committed-entry count as the aggregate. struct vnode { bool leaf = false; Hash32 key{}; VoteEntry* e = nullptr; vnode* l = nullptr; vnode* r = nullptr; u64 count = 0; Hash32 h{}; }; static void vfix(vnode* n) { if (n->leaf) { Buf w; if (n->e->committed) { n->count = 1; w.u8b(0x02); w.bytes(n->key); w.bytes(n->e->commit); w.boolb(n->e->hasOwner); w.bytes(n->e->owner); } else { n->count = 0; w.u8b(0x03); w.bytes(n->key); w.u64b(n->e->amount); w.bytes(n->e->owner); w.u32b(n->e->mixed); } n->h = H(w.b); return; } n->count = 0; Hash32 lh = zero32, rh = zero32; if (n->l) { n->count += n->l->count; lh = n->l->h; } if (n->r) { n->count += n->r->count; rh = n->r->h; } Buf w; w.u8b(0x04); w.bytes(lh); w.bytes(rh); w.u64b(n->count); n->h = H(w.b); } static vnode* vLeaf(const Hash32& key, VoteEntry* e) { auto* n = new vnode(); n->leaf = true; n->key = key; n->e = e; vfix(n); return n; } static vnode* vSplit(vnode* a, vnode* b, int d) { auto* in = new vnode(); int ba = bitAt(a->key, d), bb = bitAt(b->key, d); if (ba == bb) { vnode* c = vSplit(a, b, d + 1); if (ba == 0) in->l = c; else in->r = c; } else { if (ba == 0) { in->l = a; in->r = b; } else { in->l = b; in->r = a; } } vfix(in); return in; } static vnode* vInsertRec(vnode* n, int d, vnode* lf) { if (!n) return lf; if (n->leaf) { if (n->key == lf->key) throw Err("duplicate vote entry"); return vSplit(n, lf, d); } if (bitAt(lf->key, d) == 0) n->l = vInsertRec(n->l, d + 1, lf); else n->r = vInsertRec(n->r, d + 1, lf); vfix(n); return n; } static std::pair vDeleteRec(vnode* n, int d, const Hash32& key) { if (!n) return {nullptr, nullptr}; if (n->leaf) { if (n->key == key) return {nullptr, n}; return {n, nullptr}; } vnode* rem = nullptr; if (bitAt(key, d) == 0) std::tie(n->l, rem) = vDeleteRec(n->l, d + 1, key); else std::tie(n->r, rem) = vDeleteRec(n->r, d + 1, key); if (!rem) return {n, nullptr}; if (!n->l && !n->r) { delete n; return {nullptr, rem}; } if (!n->l && n->r->leaf) { vnode* up = n->r; delete n; return {up, rem}; } if (!n->r && n->l->leaf) { vnode* up = n->l; delete n; return {up, rem}; } vfix(n); return {n, rem}; } static void vFree(vnode* n) { if (!n) return; vFree(n->l); vFree(n->r); delete n; } // VoteTrie: Merkle count trie + direct map, kept in sync. struct VoteTrie { vnode* root = nullptr; std::unordered_map entries; VoteTrie() = default; VoteTrie(const VoteTrie&) = delete; VoteTrie& operator=(const VoteTrie&) = delete; ~VoteTrie() { vFree(root); for (auto& kv : entries) delete kv.second; } void insert(const VoteEntry& ent) { if (entries.count(ent.op)) throw Err("duplicate vote entry"); auto* e = new VoteEntry(ent); vnode* lf = vLeaf(opKey(e->op), e); try { root = vInsertRec(root, 0, lf); } catch (...) { delete lf; delete e; throw; } entries[e->op] = e; } VoteEntry* get(const Outpoint& o) const { auto it = entries.find(o); return it == entries.end() ? nullptr : it->second; } // spend removes and returns (a copy of) the entry. VoteEntry spend(const Outpoint& o) { auto it = entries.find(o); if (it == entries.end()) throw Err("vote entry missing"); auto [nr, rem] = vDeleteRec(root, 0, opKey(o)); if (!rem) throw Err("vote trie desync"); // cannot happen delete rem; root = nr; VoteEntry e = *it->second; delete it->second; entries.erase(it); return e; } // updateCommit replaces a committed entry's commit with the // revealed layer (delete + reinsert; count unchanged). void updateCommit(const Outpoint& o, const Hash32& commit) { VoteEntry e = spend(o); e.commit = commit; insert(e); } u64 committedCount() const { return root ? root->count : 0; } size_t len() const { return entries.size(); } Hash32 rootHash() const { return root ? root->h : zero32; } // select walks the cumulative counts to the committed entry at // position pos (0-based). VoteEntry* select(u64 pos) const { vnode* n = root; while (n && !n->leaf) { if (n->l && pos < n->l->count) { n = n->l; } else { if (n->l) pos -= n->l->count; n = n->r; } } if (!n || !n->e->committed) return nullptr; return n->e; } // selectRand is the spec's `rand mod total_committed`. VoteEntry* selectRand(const Hash32& r) const { u64 n = committedCount(); if (n == 0) return nullptr; u128 pos = 0; for (u8 b : r) pos = ((pos << 8) | b) % n; return select(u64(pos)); } template void forEach(F f) const { for (const auto& kv : entries) f(kv.second); } std::unique_ptr clone() const { auto c = std::make_unique(); for (const auto& kv : entries) c->insert(*kv.second); return c; } }; // ========================================================== election // slotOf/slotTime index the absolute Unix-0-anchored 60 s grid; header // seq counts slots since genesis instead (gaps where slots skipped). static u64 slotOf(u64 t) { return t / SlotSeconds; } static u64 slotTime(u64 s) { return s * SlotSeconds; } // periodStart is the start of the election period containing t. static u64 periodStart(u64 t) { return t - t % PeriodSeconds; } // phaseOpen: the first half of the period; at the midpoint // next_election locks. static bool phaseOpen(u64 t) { return t % PeriodSeconds < PeriodSeconds / 2; } // hashU64 is the spec's H(seq) for skipped-slot rand mixing. static Hash32 hashU64(u64 x) { Buf w; w.u64b(x); return H(w.b); } static Hash32 xor32(const Hash32& a, const Hash32& b) { Hash32 o; for (int i = 0; i < 32; i++) o[i] = a[i] ^ b[i]; return o; } // selRand accumulates the selection rand for a block at seq `to`, // folding in H(seq) of every skipped slot strictly between. static Hash32 selRand(Hash32 r, u64 prevSeq, u64 to) { for (u64 s = prevSeq + 1; s < to; s++) r = xor32(r, hashU64(s)); return r; } // --------------------------------------------------------- hash onion // OnionCommit: o_0 = seed, o_i = H(candidate || o_{i-1}), // commit = o_depth. static Hash32 OnionCommit(const PubKey& candidate, const Hash32& seed, u64 depth) { Hash32 o = seed; for (u64 i = 0; i < depth; i++) o = H2(candidate, o); return o; } // Onion is the producer side: it can find the layer below any commit // on its chain. Checkpoints every onionStride layers bound the work // per reveal. static const u64 onionStride = 4096; struct Onion { PubKey candidate{}; Hash32 seed{}; u64 depth = 0; std::vector cps; // cps[j] = layer at position j*onionStride u64 pos = 0; bool posValid = false; Onion(const PubKey& cand, const Hash32& sd, u64 dep) : candidate(cand), seed(sd), depth(dep) { Hash32 l = seed; cps.push_back(l); for (u64 i = 1; i <= depth; i++) { l = H2(candidate, l); if (i % onionStride == 0) cps.push_back(l); } } Hash32 layerAt(u64 i) const { u64 j = i / onionStride; Hash32 l = cps[j]; for (u64 p = j * onionStride; p < i; p++) l = H2(candidate, l); return l; } Hash32 commit() const { return layerAt(depth); } // reveal returns the layer directly below `current`, or false if // `current` is not on this onion or the onion is exhausted. std::pair reveal(const Hash32& current) { if (!posValid || layerAt(pos) != current) { posValid = false; Hash32 l = seed; for (u64 i = 0;; i++) { if (l == current) { pos = i; posValid = true; break; } if (i == depth) break; l = H2(candidate, l); } if (!posValid) return {zero32, false}; } if (pos == 0) return {zero32, false}; // exhausted Hash32 r = layerAt(pos - 1); pos--; // the commit becomes r once the block applies return {r, true}; } }; // ------------------------------------------------------------ Vote tx // VoteClaim mints this period's vote tokens for one node. One claim // per node and period, gated by last_vote. Open phase only. struct VoteClaim : Tx { PubKey key{}; u64 nonce = 0; Sig sig{}; u8 opc() const override { return OpVoteClaim; } Bytes body() const override { Buf w; w.u8b(OpVoteClaim); w.bytes(key); w.u64b(nonce); return w.b; } }; // VoteOutput mirrors VoteEntry minus the outpoint. struct VoteOutput { bool committed = false; u64 amount = 0; // uncommitted only; committed is 1 PubKey owner{}; bool hasOwner = false; // committed only u32 mixed = 0; // declared by every output: 1..MaxMix Hash32 commit{}; }; // Vote mixes and commits existing uncommitted entries in // next_election; Σ inputs = Σ outputs. Open phase only. struct Vote : Tx { std::vector inputs; std::vector outputs; std::vector sigs; // one per input, by the entry's owner u8 opc() const override { return OpVote; } Bytes body() const override { Buf w; w.u8b(OpVote); w.u32b(u32(inputs.size())); for (const auto& in : inputs) { w.bytes(in.tx); w.u32b(in.index); } w.u32b(u32(outputs.size())); for (const auto& o : outputs) { w.boolb(o.committed); if (o.committed) { w.bytes(o.commit); w.boolb(o.hasOwner); w.bytes(o.owner); w.u32b(o.mixed); } else { w.u64b(o.amount); w.bytes(o.owner); w.u32b(o.mixed); } } return w.b; } }; // ============================================================= state static const size_t maxOutputs = 1024; static const size_t maxInputs = 1024; // FeeOutpoint identifies the synthetic per-block fee output. static Outpoint FeeOutpoint(u64 seq) { Buf w; w.bytes((const u8*)"fee", 3); w.u64b(seq); return Outpoint{H(w.b), 0}; } // GenesisVoteOutpoint identifies the committed votes genesis seeds the // election tries with (index 0 → election_trie, 1 → next_election). static Outpoint GenesisVoteOutpoint(u32 index) { return Outpoint{H((const u8*)"genesis vote", 12), index}; } // State is the full chain state between blocks. struct State { u64 normTime = 0; // reference time for normalized values u64 time = 0; // time of the last applied block u64 seq = 0; // slots since genesis of the last applied block u64 genesis = 0; // genesis block time (on the Unix 60 s grid) Hash32 lastHash{}; Hash32 rnd{}; // last header's rand; seeds the next selection std::unique_ptr tree; std::unique_ptr utxo; std::unique_ptr election; // active: selects one validator per slot std::unique_ptr nextElection; // being built; activates at the boundary u64 seqAt(u64 T) const { return (T - genesis) / SlotSeconds; } std::shared_ptr clone() const { auto c = std::make_shared(); c->normTime = normTime; c->time = time; c->seq = seq; c->genesis = genesis; c->lastHash = lastHash; c->rnd = rnd; c->tree = tree->clone(); c->utxo = utxo->clone(); c->election = election->clone(); c->nextElection = nextElection->clone(); return c; } // ------------------------------------------------------- helpers // checkOutputs: positive amounts, sane count; returns the sum. static u128 checkOutputs(const std::vector& outs, bool allowEmpty) { if (outs.size() > maxOutputs) throw Err("too many outputs"); if (outs.empty() && !allowEmpty) throw Err("no outputs"); u128 sum = 0; for (const auto& o : outs) { if (o.amount == 0) throw Err("invalid output amount"); u128 s = sum + o.amount; if (s < sum) throw Err("output sum overflow"); sum = s; } return sum; } // mintOutputs inserts outs as (txid, i) at block time T. void mintOutputs(const Hash32& txid, const std::vector& outs, u64 T) { for (size_t i = 0; i < outs.size(); i++) { u128 norm = Normalize(outs[i].amount, T, normTime); utxo->insert(Outpoint{txid, u32(i)}, outs[i].amount, norm, T, outs[i].owner); } } // --------------------------------------------------------- Claim // applyClaim returns the fee: claimable(T) minus the claimed // amount; last_ubi advances to T regardless. u128 applyClaim(const Claim& c, u64 T) { PNode* n = tree->get(c.key); if (!n) throw Err("claim: key not in tree"); if (n->own == 0) throw Err("claim: node has no person contribution"); if (c.nonce != n->nonce) throw Err("claim: bad nonce (have " + std::to_string(c.nonce) + " want " + std::to_string(n->nonce) + ")"); if (!VerifySig(c.key, c.sigHash(), c.sig)) throw Err("claim: bad signature"); u128 want = ClaimableAt(n->own, n->lastUBI, T); if (want == 0) throw Err("claim: nothing claimable"); if (c.amount == 0) throw Err("claim: bad amount"); if (c.amount > want) throw Err("claim: amount " + u128str(c.amount) + " exceeds claimable " + u128str(want)); mintOutputs(c.id(), {Output{c.amount, c.key}}, T); n->lastUBI = T; n->ownUBI = Normalize(tokens(n->own), T, normTime); n->nonce++; tree->bubble(n); return want - c.amount; } // ------------------------------------------------------ Transfer // applyTransfer returns everything the validator collects: // Σ input gross value − Σ outputs. Validation per spec: // Σ outputs ≤ Σ input spendable at block time. u128 applyTransfer(const Transfer& t, u64 T) { if (t.inputs.empty() || t.inputs.size() > maxInputs) throw Err("transfer: bad input count"); if (t.sigs.size() != t.inputs.size()) throw Err("transfer: need one signature per input"); std::unordered_set seen; std::vector ents; ents.reserve(t.inputs.size()); for (const auto& op : t.inputs) { if (!seen.insert(op).second) throw Err("transfer: duplicate input"); const Entry* e = utxo->get(op); if (!e) throw Err("transfer: input missing or spent"); ents.push_back(e); } Hash32 sh = t.sigHash(); for (size_t i = 0; i < ents.size(); i++) if (!VerifySig(ents[i]->owner, sh, t.sigs[i])) throw Err("transfer: bad signature for input " + std::to_string(i)); u128 outSum = checkOutputs(t.outputs, true); u128 inValue = 0; // Σ gross values u128 inRent = 0; // Σ rent_owed (spendable = value − rent, signed) for (const Entry* e : ents) { inValue = addChecked(inValue, e->value(T), "transfer input"); inRent = addChecked(inRent, e->rent(T), "transfer rent"); } // outputs ≤ Σ spendable ⇔ outSum + ΣRent ≤ ΣValue (signed-safe). bool ok = inValue >= inRent && outSum <= inValue - inRent; if (!ok) throw Err("transfer: outputs " + u128str(outSum) + " exceed spendable " + spendStr(inValue, inRent)); for (const auto& op : t.inputs) utxo->spend(op); mintOutputs(t.id(), t.outputs, T); // fee + collected rent = gross − outputs (≥ 0 by the check). return inValue - outSum; } // --------------------------------------------------------- Prune // applyPrune removes UTXOs with spendable(T) ≤ 0 and returns their // remaining gross value. u128 applyPrune(const Prune& p, u64 T) { if (p.inputs.empty() || p.inputs.size() > maxInputs) throw Err("prune: bad input count"); std::unordered_set seen; u128 collected = 0; for (const auto& op : p.inputs) { if (!seen.insert(op).second) throw Err("prune: duplicate input"); const Entry* e = utxo->get(op); if (!e) throw Err("prune: output missing or spent"); if (!e->expired(T)) throw Err("prune: output not expired (spendable " + spendStr(e->value(T), e->rent(T)) + ")"); collected = addChecked(collected, e->value(T), "prune"); } for (const auto& op : p.inputs) utxo->spend(op); return collected; } // ----------------------------------------------------------- Add void applyAdd(const Add& a, u64 T) { PNode* p = tree->get(a.parent); if (!p) throw Err("add: parent not in tree"); if (a.nonce != p->nonce) throw Err("add: bad nonce (have " + std::to_string(a.nonce) + " want " + std::to_string(p->nonce) + ")"); if (T > a.deadline) throw Err("add: consent expired"); if (!VerifySig(a.parent, a.sigHash(), a.sig)) throw Err("add: bad parent signature"); if (a.tmpl.key != a.childKey) throw Err("add: template root key mismatch"); if (a.tmpl.hash() != a.hashv) throw Err("add: template hash mismatch"); if (!VerifySig(a.childKey, ConsentMsg(a.hashv, a.deadline, a.parent), a.consent)) throw Err("add: bad child consent"); p->nonce++; try { tree->doAdd(a.parent, a.tmpl, T, normTime); } catch (const Err& e) { throw Err(std::string("add: ") + e.what()); } } // -------------------------------------------------------- Remove void applyRemove(const Remove& r, u64 T) { PNode* p = tree->get(r.parent); if (!p) throw Err("remove: parent not in tree"); if (r.nonce != p->nonce) throw Err("remove: bad nonce (have " + std::to_string(r.nonce) + " want " + std::to_string(p->nonce) + ")"); if (!VerifySig(r.parent, r.sigHash(), r.sig)) throw Err("remove: bad signature"); p->nonce++; std::vector persons; try { persons = tree->doRemove(r.parent, r.child); } catch (const Err& e) { throw Err(std::string("remove: ") + e.what()); } // Auto-mint accrued UBI to each removed person, in pre-order; // zero-claimable persons are skipped. Hash32 txid = r.id(); u32 idx = 0; for (const auto& pr : persons) { u128 amt = ClaimableAt(pr.own, pr.lastUBI, T); if (amt == 0) continue; u128 norm = Normalize(amt, T, normTime); utxo->insert(Outpoint{txid, idx}, amt, norm, T, pr.key); idx++; } } // --------------------------------------------------------- Rekey void applyRekey(const Rekey& r, u64) { PNode* n = tree->get(r.oldKey); if (!n) throw Err("rekey: node not in tree"); if (r.nonce != n->nonce) throw Err("rekey: bad nonce (have " + std::to_string(r.nonce) + " want " + std::to_string(n->nonce) + ")"); if (!VerifySig(r.oldKey, r.sigHash(), r.sig)) throw Err("rekey: bad signature"); tree->doRekey(r.oldKey, r.newKey); } // ---------------------------------------------------------- Move void applyMove(const Move& m, u64 T) { PNode* c = tree->get(m.child); if (!c) throw Err("move: child not in tree"); if (!c->parent) throw Err("move: cannot move the root"); PNode* np = tree->get(m.newParent); if (!np) throw Err("move: new parent not in tree"); if (m.nonce != np->nonce) throw Err("move: bad nonce (have " + std::to_string(m.nonce) + " want " + std::to_string(np->nonce) + ")"); if (T > m.deadline) throw Err("move: consent expired"); if (!VerifySig(m.newParent, m.sigHash(), m.sig)) throw Err("move: bad new parent signature"); if (!VerifySig(m.child, MoveConsentMsg(m.deadline, m.newParent), m.consent)) throw Err("move: bad child consent"); np->nonce++; tree->doMove(m.child, m.newParent); } // --------------------------------------------------------- Leave void applyLeave(const Leave& l, u64 T) { PNode* c = tree->get(l.child); if (!c) throw Err("leave: child not in tree"); if (!c->parent) throw Err("leave: root cannot leave"); if (l.nonce != c->nonce) throw Err("leave: bad nonce (have " + std::to_string(l.nonce) + " want " + std::to_string(c->nonce) + ")"); if (!VerifySig(l.child, l.sigHash(), l.sig)) throw Err("leave: bad signature"); PNode* parent = c->parent; std::vector persons; try { persons = tree->doRemove(parent->key, l.child); } catch (const Err& e) { throw Err(std::string("leave: ") + e.what()); } Hash32 txid = l.id(); u32 idx = 0; for (const auto& pr : persons) { u128 amt = ClaimableAt(pr.own, pr.lastUBI, T); if (amt == 0) continue; u128 norm = Normalize(amt, T, normTime); utxo->insert(Outpoint{txid, idx}, amt, norm, T, pr.key); idx++; } } // ----------------------------------------------------- VoteClaim void applyVoteClaim(const VoteClaim& c, u64 T) { if (!phaseOpen(T)) throw Err("vote claim: next_election is locked (second half of period)"); PNode* n = tree->get(c.key); if (!n) throw Err("vote claim: key not in tree"); if (n->own == 0) throw Err("vote claim: node has no person contribution"); if (c.nonce != n->nonce) throw Err("vote claim: bad nonce (have " + std::to_string(c.nonce) + " want " + std::to_string(n->nonce) + ")"); if (n->lastVote >= periodStart(T)) throw Err("vote claim: already claimed this period"); if (!VerifySig(c.key, c.sigHash(), c.sig)) throw Err("vote claim: bad signature"); VoteEntry e; e.op = Outpoint{c.id(), 0}; e.amount = n->own; e.owner = c.key; e.mixed = 0; nextElection->insert(e); n->lastVote = T; n->nonce++; tree->bubble(n); } // ---------------------------------------------------------- Vote void applyVote(const Vote& v, u64 T) { if (!phaseOpen(T)) throw Err("vote: next_election is locked (second half of period)"); if (v.inputs.empty() || v.inputs.size() > maxInputs) throw Err("vote: bad input count"); if (v.outputs.empty() || v.outputs.size() > maxOutputs) throw Err("vote: bad output count"); if (v.sigs.size() != v.inputs.size()) throw Err("vote: need one signature per input"); Hash32 sh = v.sigHash(); // Inputs: uncommitted, in next_election, signed by owner. u128 inSum = 0; u64 inMix = 0; std::unordered_set seenOp; for (size_t i = 0; i < v.inputs.size(); i++) { const auto& op = v.inputs[i]; if (!seenOp.insert(op).second) throw Err("vote: duplicate input"); const VoteEntry* e = nextElection->get(op); if (!e) throw Err("vote: input missing or spent"); if (e->committed) throw Err("vote: input is committed (locked)"); if (!VerifySig(e->owner, sh, v.sigs[i])) throw Err("vote: bad signature for input " + std::to_string(i)); inSum += e->amount; inMix += e->mixed; } u128 outSum = 0; u64 outMix = 0; for (const auto& o : v.outputs) { if (o.mixed < 1 || o.mixed > MaxMix) throw Err("vote: output mixed " + std::to_string(o.mixed) + " outside 1.." + std::to_string(MaxMix)); outMix += o.mixed; if (o.committed) { outSum += 1; // committed amount is 1 } else { if (o.amount == 0) throw Err("vote: zero-amount output"); outSum += o.amount; } } if (inSum != outSum) throw Err("vote: inputs " + u128str(inSum) + " != outputs " + u128str(outSum)); // Spec: sum(output mixed) ≥ sum(input mixed) + count(outputs). if (outMix < inMix + u64(v.outputs.size())) throw Err("vote: mixed budget " + std::to_string(outMix) + " < " + std::to_string(inMix + u64(v.outputs.size())) + " required"); // Apply. for (const auto& op : v.inputs) nextElection->spend(op); Hash32 txid = v.id(); for (size_t i = 0; i < v.outputs.size(); i++) { const auto& o = v.outputs[i]; VoteEntry e; e.op = Outpoint{txid, u32(i)}; e.committed = o.committed; e.owner = o.owner; if (o.committed) { e.amount = 1; e.hasOwner = o.hasOwner; e.commit = o.commit; } else { e.amount = o.amount; e.mixed = o.mixed; } nextElection->insert(e); } } // applyTxs applies transactions in order and returns total fees. u128 applyTxs(const std::vector& txs, u64 T) { u128 fees = 0; for (size_t i = 0; i < txs.size(); i++) { try { Tx* tx = txs[i].get(); switch (tx->opc()) { case OpClaim: fees = addChecked(fees, applyClaim(*static_cast(tx), T), "fees"); break; case OpTransfer: fees = addChecked(fees, applyTransfer(*static_cast(tx), T), "fees"); break; case OpPrune: fees = addChecked(fees, applyPrune(*static_cast(tx), T), "fees"); break; case OpAdd: applyAdd(*static_cast(tx), T); break; case OpRemove: applyRemove(*static_cast(tx), T); break; case OpRekey: applyRekey(*static_cast(tx), T); break; case OpMove: applyMove(*static_cast(tx), T); break; case OpLeave: applyLeave(*static_cast(tx), T); break; case OpVote: applyVote(*static_cast(tx), T); break; case OpVoteClaim: applyVoteClaim(*static_cast(tx), T); break; default: throw Err("unknown tx type"); } } catch (const std::exception& e) { throw Err("tx " + std::to_string(i) + ": " + e.what()); } } return fees; } // SupplyAt: real UTXO supply at T from the trie root. u128 supplyAt(u64 T) const { return ValueAt(utxo->sum(), T, normTime); } // UnclaimedAt: unclaimed UBI at T from the tree root. u128 unclaimedAt(u64 T) const { return tree->unclaimedAt(T, normTime); } }; using StatePtr = std::shared_ptr; // ============================================================= block // Header per spec; seq starts at 0 at genesis and increments by 1 per // slot — skipped slots leave gaps; time = genesis_time + seq × slot. struct Header { u64 seq = 0; u64 time = 0; Hash32 peopleTree{}; Hash32 utxoTrie{}; Hash32 electionTrie{}; Hash32 nextElection{}; Hash32 prev{}; PubKey validator{}; Hash32 rnd{}; // selection rand XOR revealed onion layer Sig sig{}; Bytes encode(bool withSig) const { Buf w; w.u8b(OpHeader); w.u64b(seq); w.u64b(time); w.bytes(peopleTree); w.bytes(utxoTrie); w.bytes(electionTrie); w.bytes(nextElection); w.bytes(prev); w.bytes(validator); w.bytes(rnd); if (withSig) w.bytes(sig); return w.b; } // SigHash: what the validator signs (opcode included, sig excluded). Hash32 sigHash() const { return H(encode(false)); } // Hash identifies the block (signature included). Hash32 hash() const { return H(encode(true)); } }; // Block: header + the transactions that produce its state; the block // commits to the *resulting* state, verification re-executes. struct Block { Header header; std::vector txs; }; using BlockPtr = std::shared_ptr; // advancePeriods applies every period boundary crossed in (from, to]: // next_election becomes the active trie, a fresh one opens — // unconditionally (voting is a liveness requirement). static void advancePeriods(State& ns, u64 from, u64 to) { for (u64 b = periodStart(from) + PeriodSeconds; b <= to && b >= PeriodSeconds; b += PeriodSeconds) { ns.election = std::move(ns.nextElection); ns.nextElection = std::make_unique(); } } // preSelect runs the transition parts that precede the reveal: // slot-grid checks, period activation, skipped-slot rand folding, and // the positional selection of the slot's committed vote entry. // Returns {new state, selection rand, selected entry}. static std::tuple preSelect(const State& prev, u64 T) { if (T % SlotSeconds != 0) throw Err("block time not on the slot grid"); if (T <= prev.time) throw Err("block time not after previous block"); StatePtr ns = prev.clone(); advancePeriods(*ns, prev.time, T); Hash32 r = selRand(ns->rnd, prev.seq, prev.seqAt(T)); VoteEntry* entry = ns->election->selectRand(r); if (!entry) throw Err("no committed votes in election trie"); return {ns, r, entry}; } // finish: verify the reveal against the selected commit, walk the // onion one layer down in the trie, chain the rand, apply the txs, // mint the fee output to the slot's validator. static void finish(State& ns, const std::vector& txs, u64 T, const PubKey& validator, const Hash32& r, const Hash32& reveal, VoteEntry* entry) { if (H2(validator, reveal) != entry->commit) throw Err("reveal does not match the selected commit"); ns.election->updateCommit(entry->op, reveal); ns.rnd = xor32(r, reveal); u128 fees = ns.applyTxs(txs, T); if (fees > 0) { u128 norm = Normalize(fees, T, ns.normTime); try { ns.utxo->insert(FeeOutpoint(ns.seqAt(T)), fees, norm, T, validator); } catch (const Err& e) { throw Err(std::string("fee mint: ") + e.what()); } } } // BuildBlock executes txs on top of prev at slot time T and produces // a signed block plus the resulting state. Fails when no held onion // matches the selected commit (someone else's slot) — the caller // treats that as a skipped slot. static std::pair BuildBlock(const State& prev, const std::vector& txs, u64 T, const Seed& valPriv, std::vector>& onions) { PubKey pub = PubFromSeed(valPriv); auto [ns, r, entry] = preSelect(prev, T); Hash32 reveal{}; bool ok = false; for (auto& o : onions) { auto [rv, hit] = o->reveal(entry->commit); if (hit) { reveal = rv; ok = true; break; } } if (!ok) throw Err("build: slot " + std::to_string(slotOf(T)) + " not ours (or onion exhausted)"); finish(*ns, txs, T, pub, r, reveal, entry); auto b = std::make_shared(); Header& h = b->header; h.seq = prev.seqAt(T); h.time = T; h.peopleTree = ns->tree->rootHash(); h.utxoTrie = ns->utxo->rootHash(); h.electionTrie = ns->election->rootHash(); h.nextElection = ns->nextElection->rootHash(); h.prev = prev.lastHash; h.validator = pub; h.rnd = ns->rnd; h.sig = SignMsg(valPriv, h.sigHash()); b->txs = txs; ns->seq = h.seq; ns->time = T; ns->lastHash = h.hash(); return {b, ns}; } // VerifyBlock checks b against prev and, on success, returns the new // state. reveal = selection_rand XOR header.rand; verification // re-executes the transactions and requires exact root matches. static StatePtr VerifyBlock(const State& prev, const Block& b) { const Header& h = b.header; if (h.seq != prev.seqAt(h.time)) throw Err("verify: seq " + std::to_string(h.seq) + " is not slots-since-genesis " + std::to_string(prev.seqAt(h.time))); if (h.prev != prev.lastHash) throw Err("verify: prev hash mismatch"); StatePtr ns; Hash32 r{}; VoteEntry* entry = nullptr; try { std::tie(ns, r, entry) = preSelect(prev, h.time); Hash32 reveal = xor32(r, h.rnd); finish(*ns, b.txs, h.time, h.validator, r, reveal, entry); } catch (const Err& e) { throw Err(std::string("verify: ") + e.what()); } if (!VerifySig(h.validator, h.sigHash(), h.sig)) throw Err("verify: bad validator signature"); if (ns->tree->rootHash() != h.peopleTree) throw Err("verify: people_tree root mismatch"); if (ns->utxo->rootHash() != h.utxoTrie) throw Err("verify: utxo_trie root mismatch"); if (ns->election->rootHash() != h.electionTrie) throw Err("verify: election_trie root mismatch"); if (ns->nextElection->rootHash() != h.nextElection) throw Err("verify: next_election root mismatch"); ns->seq = h.seq; ns->time = h.time; ns->lastHash = h.hash(); return ns; } // ============================================================= chain // Chain: current state, block history, and the finality snapshot the // fork choice pivots on. Blocks before the previous election period // boundary are final. struct Chain { StatePtr state; std::vector blocks; StatePtr finalState; // state after blocks[finalIdx]; never reorged int finalIdx = 0; Hash32 tipHash() const { return state->lastHash; } // horizon: the period boundary BEFORE the one the tip sits in. u64 horizon() const { u64 p = periodStart(state->time); if (p < PeriodSeconds) return 0; return p - PeriodSeconds; } // advanceFinality folds newly-final blocks into the snapshot. void advanceFinality() { u64 h = horizon(); while (finalIdx + 1 < int(blocks.size()) && blocks[finalIdx + 1]->header.time < h) { StatePtr ns; try { ns = VerifyBlock(*finalState, *blocks[finalIdx + 1]); } catch (const std::exception& e) { // cannot happen: block was verified on append fprintf(stderr, "finality replay diverged: %s\n", e.what()); abort(); } finalState = ns; finalIdx++; } } // stateAt: the state after blocks[i] (i ≥ finalIdx), replaying // from the finality snapshot when needed. StatePtr stateAt(int i) const { if (i == int(blocks.size()) - 1) return state; StatePtr st = finalState->clone(); for (int j = finalIdx + 1; j <= i; j++) st = VerifyBlock(*st, *blocks[j]); return st; } // Produce builds the next block, self-verifies it, and advances. BlockPtr produce(const std::vector& txs, u64 T, const Seed& valPriv, std::vector>& onions) { auto [b, ns] = BuildBlock(*state, txs, T, valPriv, onions); StatePtr vs; try { vs = VerifyBlock(*state, *b); } catch (const Err& e) { throw Err(std::string("self-verify failed: ") + e.what()); } if (vs->lastHash != ns->lastHash) throw Err("self-verify: state divergence"); state = ns; blocks.push_back(b); advanceFinality(); return b; } // TryAdopt evaluates a competing branch (see the Go node for the // fork-choice discussion: maximizing block count == minimizing // skips with a shared genesis). Returns the fork block's index. int tryAdopt(const std::vector& branch) { if (branch.empty()) throw Err("adopt: empty branch"); int forkIdx = -1; for (int i = int(blocks.size()) - 1; i >= finalIdx; i--) { if (blocks[i]->header.hash() == branch[0]->header.prev) { forkIdx = i; break; } } if (forkIdx < 0) throw Err("adopt: fork point unknown or below the finality horizon"); if (forkIdx + 1 + int(branch.size()) <= int(blocks.size())) throw Err("adopt: branch has " + std::to_string(branch.size()) + " blocks from the fork, ours has " + std::to_string(blocks.size() - forkIdx - 1) + " — not strictly better"); StatePtr st = stateAt(forkIdx)->clone(); for (const auto& b : branch) { try { st = VerifyBlock(*st, *b); } catch (const Err& e) { throw Err(std::string("adopt: ") + e.what()); } } blocks.resize(forkIdx + 1); blocks.insert(blocks.end(), branch.begin(), branch.end()); state = st; advanceFinality(); return forkIdx; } }; // NewChain creates genesis at t0 (aligned down to the slot grid): a // people tree with a single root person, an empty UTXO set, and both // election tries seeded with one committed vote each. static std::unique_ptr NewChain(const PubKey& rootKey, u64 t0, const Seed& valPriv, const Hash32& commit0, const Hash32& commit1) { PubKey valKey = PubFromSeed(valPriv); t0 -= t0 % SlotSeconds; u64 norm = normTimeFor(t0); auto st = std::make_shared(); st->normTime = norm; st->time = t0; st->seq = 0; st->genesis = t0; st->tree = PeopleTree::make(rootKey, 1, t0, norm); st->utxo = std::make_unique(); st->election = std::make_unique(); st->nextElection = std::make_unique(); { VoteEntry e; e.op = GenesisVoteOutpoint(0); e.committed = true; e.commit = commit0; st->election->insert(e); } { VoteEntry e; e.op = GenesisVoteOutpoint(1); e.committed = true; e.commit = commit1; st->nextElection->insert(e); } auto gb = std::make_shared(); Header& h = gb->header; h.seq = 0; h.time = t0; h.peopleTree = st->tree->rootHash(); h.utxoTrie = st->utxo->rootHash(); h.electionTrie = st->election->rootHash(); h.nextElection = st->nextElection->rootHash(); h.validator = valKey; h.sig = SignMsg(valPriv, h.sigHash()); st->lastHash = h.hash(); auto ch = std::make_unique(); ch->state = st; ch->blocks.push_back(gb); ch->finalState = st->clone(); ch->finalIdx = 0; return ch; } // ============================================================== wire // Rdr is the decoding counterpart of Buf: it never throws mid-parse, // it accumulates the first error and returns zero values after it. struct Rdr { const u8* p = nullptr; size_t n = 0; std::string err; Rdr(const u8* data, size_t len) : p(data), n(len) {} explicit Rdr(const Bytes& b) : p(b.data()), n(b.size()) {} const u8* need(size_t k) { static const u8 zeros[64] = {}; if (!err.empty()) return zeros; if (n < k) { err = "wire: truncated"; return zeros; } const u8* q = p; p += k; n -= k; return q; } u8 u8v() { return need(1)[0]; } u32 u32v() { const u8* q = need(4); return u32(q[0]) << 24 | u32(q[1]) << 16 | u32(q[2]) << 8 | u32(q[3]); } u64 u64v() { const u8* q = need(8); u64 x = 0; for (int i = 0; i < 8; i++) x = x << 8 | q[i]; return x; } u128 u128v() { const u8* q = need(16); u128 x = 0; for (int i = 0; i < 16; i++) x = x << 8 | q[i]; return x; } bool boolv() { return u8v() != 0; } Hash32 h32() { Hash32 h{}; memcpy(h.data(), need(32), 32); return h; } PubKey key() { return h32(); } Sig sig() { Sig s{}; memcpy(s.data(), need(64), 64); return s; } void done() { if (err.empty() && n > 0) throw Err("wire: trailing bytes"); if (!err.empty()) throw Err(err); } }; // ------------------------------------------------------------- outputs static std::vector decodeOutputs(Rdr& r) { u32 n = r.u32v(); if (n > maxOutputs) { r.err = "wire: too many outputs"; return {}; } std::vector outs; outs.reserve(n); for (u32 i = 0; i < n && r.err.empty(); i++) outs.push_back(Output{r.u128v(), r.key()}); return outs; } // ------------------------------------------------------------ template static void encodeTemplate(Buf& w, const NodeTemplate& t) { w.bytes(t.key); w.boolb(t.leaf); w.u64b(t.nonce); w.u64b(t.lastUBI); w.u64b(t.lastVote); w.u64b(t.treeCount); w.u128b(t.treeUBI); w.u32b(u32(t.children.size())); for (const auto& c : t.children) encodeTemplate(w, c); } static NodeTemplate decodeTemplate(Rdr& r, int& budget) { budget--; if (budget < 0) { r.err = "wire: template too large"; return {}; } NodeTemplate t; t.key = r.key(); t.leaf = r.boolv(); t.nonce = r.u64v(); t.lastUBI = r.u64v(); t.lastVote = r.u64v(); t.treeCount = r.u64v(); t.treeUBI = r.u128v(); u32 n = r.u32v(); if (n > u32(maxTemplateNodes)) { r.err = "wire: template too large"; return t; } for (u32 i = 0; i < n && r.err.empty(); i++) t.children.push_back(decodeTemplate(r, budget)); return t; } // ------------------------------------------------------------------ tx // EncodeTx serializes a transaction: hashed body first, then the // signatures, then (Add) the template — identical to the Go wire. static Bytes EncodeTx(const Tx& t) { Buf w; w.bytes(t.body()); switch (t.opc()) { case OpClaim: w.bytes(static_cast(t).sig); break; case OpTransfer: for (const auto& s : static_cast(t).sigs) w.bytes(s); break; case OpPrune: break; // no signatures: validity is objective case OpAdd: { const auto& a = static_cast(t); w.bytes(a.consent); w.bytes(a.sig); encodeTemplate(w, a.tmpl); break; } case OpRemove: w.bytes(static_cast(t).sig); break; case OpRekey: w.bytes(static_cast(t).sig); break; case OpMove: { const auto& m = static_cast(t); w.bytes(m.consent); w.bytes(m.sig); break; } case OpLeave: w.bytes(static_cast(t).sig); break; case OpVote: for (const auto& s : static_cast(t).sigs) w.bytes(s); break; case OpVoteClaim: w.bytes(static_cast(t).sig); break; default: throw Err("EncodeTx: unknown tx type"); } return w.b; } static TxPtr decodeTxInner(Rdr& r) { u8 op = r.u8v(); switch (op) { case OpClaim: { auto t = std::make_shared(); t->key = r.key(); t->amount = r.u128v(); t->nonce = r.u64v(); t->sig = r.sig(); return t; } case OpTransfer: { u32 n = r.u32v(); if (n > maxInputs) { r.err = "wire: too many inputs"; return nullptr; } auto t = std::make_shared(); for (u32 i = 0; i < n && r.err.empty(); i++) t->inputs.push_back(Outpoint{r.h32(), r.u32v()}); t->outputs = decodeOutputs(r); for (u32 i = 0; i < n && r.err.empty(); i++) t->sigs.push_back(r.sig()); return t; } case OpPrune: { u32 n = r.u32v(); if (n > maxInputs) { r.err = "wire: too many inputs"; return nullptr; } auto t = std::make_shared(); for (u32 i = 0; i < n && r.err.empty(); i++) t->inputs.push_back(Outpoint{r.h32(), r.u32v()}); return t; } case OpAdd: { auto t = std::make_shared(); t->parent = r.key(); t->childKey = r.key(); t->hashv = r.h32(); t->nonce = r.u64v(); t->deadline = r.u64v(); t->consent = r.sig(); t->sig = r.sig(); int budget = maxTemplateNodes; t->tmpl = decodeTemplate(r, budget); return t; } case OpRemove: { auto t = std::make_shared(); t->parent = r.key(); t->child = r.key(); t->nonce = r.u64v(); t->sig = r.sig(); return t; } case OpRekey: { auto t = std::make_shared(); t->oldKey = r.key(); t->newKey = r.key(); t->nonce = r.u64v(); t->sig = r.sig(); return t; } case OpMove: { auto t = std::make_shared(); t->child = r.key(); t->newParent = r.key(); t->nonce = r.u64v(); t->deadline = r.u64v(); t->consent = r.sig(); t->sig = r.sig(); return t; } case OpLeave: { auto t = std::make_shared(); t->child = r.key(); t->nonce = r.u64v(); t->sig = r.sig(); return t; } case OpVoteClaim: { auto t = std::make_shared(); t->key = r.key(); t->nonce = r.u64v(); t->sig = r.sig(); return t; } case OpVote: { auto t = std::make_shared(); u32 ni = r.u32v(); if (ni > maxInputs) { r.err = "wire: too many inputs"; return nullptr; } for (u32 i = 0; i < ni && r.err.empty(); i++) t->inputs.push_back(Outpoint{r.h32(), r.u32v()}); u32 no = r.u32v(); if (no > maxOutputs) { r.err = "wire: too many outputs"; return nullptr; } for (u32 i = 0; i < no && r.err.empty(); i++) { VoteOutput o; o.committed = r.boolv(); if (o.committed) { o.commit = r.h32(); o.hasOwner = r.boolv(); o.owner = r.key(); o.mixed = r.u32v(); o.amount = 1; } else { o.amount = r.u64v(); o.owner = r.key(); o.mixed = r.u32v(); } t->outputs.push_back(o); } for (u32 i = 0; i < ni && r.err.empty(); i++) t->sigs.push_back(r.sig()); return t; } default: { char b[48]; snprintf(b, sizeof b, "wire: unknown opcode 0x%02x", op); r.err = b; return nullptr; } } } // DecodeTx parses exactly one transaction. static TxPtr DecodeTx(const u8* data, size_t len) { Rdr r(data, len); TxPtr t = decodeTxInner(r); r.done(); return t; } // --------------------------------------------------------------- block // EncodeBlock: header (with sig) + u32 tx count + per tx u32 len + bytes. static Bytes EncodeBlock(const Block& b) { Buf w; w.bytes(b.header.encode(true)); w.u32b(u32(b.txs.size())); for (const auto& t : b.txs) { Bytes tb = EncodeTx(*t); w.u32b(u32(tb.size())); w.bytes(tb); } return w.b; } static Header decodeHeader(Rdr& r) { u8 op = r.u8v(); if (op != OpHeader && r.err.empty()) { char b[48]; snprintf(b, sizeof b, "wire: bad header opcode 0x%02x", op); r.err = b; } Header h; h.seq = r.u64v(); h.time = r.u64v(); h.peopleTree = r.h32(); h.utxoTrie = r.h32(); h.electionTrie = r.h32(); h.nextElection = r.h32(); h.prev = r.h32(); h.validator = r.key(); h.rnd = r.h32(); h.sig = r.sig(); return h; } static const size_t maxTxBytes = 1 << 22; // 4 MiB per tx, sanity cap static BlockPtr DecodeBlock(const u8* data, size_t len) { Rdr r(data, len); auto blk = std::make_shared(); blk->header = decodeHeader(r); u32 n = r.u32v(); for (u32 i = 0; i < n && r.err.empty(); i++) { u32 l = r.u32v(); if (l > maxTxBytes) throw Err("wire: tx too large"); const u8* tb = r.need(l); if (!r.err.empty()) break; try { blk->txs.push_back(DecodeTx(tb, l)); } catch (const Err& e) { throw Err("wire: tx " + std::to_string(i) + ": " + e.what()); } } r.done(); return blk; } static BlockPtr DecodeBlock(const Bytes& b) { return DecodeBlock(b.data(), b.size()); } // ============================================================= store // The chain persists as an append-only block log; state is fully // derived (every block re-verified on startup). // // Layout: // record 0: u32 len | rootKey(32) | t0(8) | commit0(32) | commit1(32) | genesis header // record N: u32 len | EncodeBlock(block N) struct Store { int fd = -1; std::vector offsets; // start offset of record i i64 end = 0; // end of the last record Bytes genesisRec; ~Store() { if (fd >= 0) close(fd); } static void writeRec(int fd, const Bytes& rec) { u8 l[4] = {u8(rec.size() >> 24), u8(rec.size() >> 16), u8(rec.size() >> 8), u8(rec.size())}; if (write(fd, l, 4) != 4) throw Err("store: write failed"); ssize_t off = 0; while (off < ssize_t(rec.size())) { ssize_t w = write(fd, rec.data() + off, rec.size() - off); if (w <= 0) throw Err("store: write failed"); off += w; } } void append(const Block& b) { Bytes rec = EncodeBlock(b); writeRec(fd, rec); offsets.push_back(end); end += i64(4 + rec.size()); if (fsync(fd) != 0) throw Err("store: fsync failed"); } // reorg truncates the log to its first `keep` records and appends // the adopted branch. keep counts records including genesis. void reorg(int keep, const std::vector& branch) { if (keep < 1 || keep > int(offsets.size())) throw Err("store: bad reorg keep count"); i64 cut = end; if (keep < int(offsets.size())) cut = offsets[keep]; if (ftruncate(fd, cut) != 0) throw Err("store: truncate failed"); if (lseek(fd, cut, SEEK_SET) < 0) throw Err("store: seek failed"); offsets.resize(keep); end = cut; for (const auto& b : branch) { Bytes rec = EncodeBlock(*b); writeRec(fd, rec); offsets.push_back(end); end += i64(4 + rec.size()); } if (fsync(fd) != 0) throw Err("store: fsync failed"); } }; // genesisState rebuilds and validates the genesis state from the // stored parameters + header (the header is checked, not trusted). static StatePtr genesisState(const PubKey& rootKey, u64 t0, const Hash32& commit0, const Hash32& commit1, const Header& h) { u64 norm = normTimeFor(t0); auto st = std::make_shared(); st->normTime = norm; st->time = t0; st->seq = 0; st->genesis = t0; st->tree = PeopleTree::make(rootKey, 1, t0, norm); st->utxo = std::make_unique(); st->election = std::make_unique(); st->nextElection = std::make_unique(); { VoteEntry e; e.op = GenesisVoteOutpoint(0); e.committed = true; e.commit = commit0; st->election->insert(e); } { VoteEntry e; e.op = GenesisVoteOutpoint(1); e.committed = true; e.commit = commit1; st->nextElection->insert(e); } if (h.seq != 0 || h.time != t0 || h.prev != zero32) throw Err("genesis: bad seq/time/prev"); if (t0 % SlotSeconds != 0) throw Err("genesis: t0 not on the slot grid"); if (h.rnd != zero32) throw Err("genesis: rand must be zero"); if (h.peopleTree != st->tree->rootHash() || h.utxoTrie != st->utxo->rootHash() || h.electionTrie != st->election->rootHash() || h.nextElection != st->nextElection->rootHash()) throw Err("genesis: root mismatch"); if (!VerifySig(h.validator, h.sigHash(), h.sig)) throw Err("genesis: bad validator signature"); st->lastHash = h.hash(); return st; } // CreateStore writes a fresh log for the given genesis. static std::unique_ptr CreateStore(const std::string& path, const PubKey& rootKey, u64 t0, const Hash32& commit0, const Hash32& commit1, const Header& gen) { int fd = open(path.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0644); if (fd < 0) throw Err("store: " + path + ": " + strerror(errno)); auto s = std::make_unique(); s->fd = fd; Buf w; w.bytes(rootKey); w.u64b(t0); w.bytes(commit0); w.bytes(commit1); w.bytes(gen.encode(true)); Store::writeRec(fd, w.b); if (fsync(fd) != 0) throw Err("store: fsync failed"); s->offsets = {0}; s->end = i64(4 + w.b.size()); s->genesisRec = w.b; return s; } static Bytes readFileAll(const std::string& path) { int fd = open(path.c_str(), O_RDONLY); if (fd < 0) throw Err(path + ": " + strerror(errno)); Bytes out; u8 buf[1 << 16]; ssize_t n; while ((n = read(fd, buf, sizeof buf)) > 0) out.insert(out.end(), buf, buf + n); close(fd); if (n < 0) throw Err(path + ": read failed"); return out; } // OpenStore reads the log, replays and verifies every block, and // returns the store (positioned for appends) plus the resulting chain. static std::pair, std::unique_ptr> OpenStore(const std::string& path) { Bytes data = readFileAll(path); size_t off = 0; std::vector offsets; auto next = [&](Bytes& rec) -> bool { if (off == data.size()) return false; if (data.size() - off < 4) throw Err("store: truncated length"); u32 l = u32(data[off]) << 24 | u32(data[off + 1]) << 16 | u32(data[off + 2]) << 8 | u32(data[off + 3]); if (data.size() - off - 4 < l) throw Err("store: truncated record"); rec.assign(data.begin() + off + 4, data.begin() + off + 4 + l); offsets.push_back(i64(off)); off += 4 + l; return true; }; Bytes rec; if (!next(rec)) throw Err("store: genesis record: missing"); Bytes genRec = rec; Rdr r(rec); PubKey rootKey = r.key(); u64 t0 = r.u64v(); Hash32 commit0 = r.h32(); Hash32 commit1 = r.h32(); Header gh = decodeHeader(r); try { r.done(); } catch (const Err& e) { throw Err(std::string("store: genesis record: ") + e.what()); } StatePtr st = genesisState(rootKey, t0, commit0, commit1, gh); auto ch = std::make_unique(); ch->state = st; auto gb = std::make_shared(); gb->header = gh; ch->blocks.push_back(gb); ch->finalState = st->clone(); ch->finalIdx = 0; while (next(rec)) { BlockPtr blk; try { blk = DecodeBlock(rec); } catch (const Err& e) { throw Err("store: record after slot " + std::to_string(ch->state->seq) + ": " + e.what()); } StatePtr ns; try { ns = VerifyBlock(*ch->state, *blk); } catch (const Err& e) { throw Err("store: block " + std::to_string(blk->header.seq) + ": " + e.what()); } ch->state = ns; ch->blocks.push_back(blk); } ch->advanceFinality(); int fd = open(path.c_str(), O_RDWR | O_APPEND); if (fd < 0) throw Err("store: " + path + ": " + strerror(errno)); auto s = std::make_unique(); s->fd = fd; s->offsets = std::move(offsets); s->end = i64(off); s->genesisRec = genRec; return {std::move(s), std::move(ch)}; } // ============================================================== json // A small ordered JSON value: enough for this node's API. Numbers keep // their raw literal so u64 values round-trip exactly. struct Json { enum Kind { Null, BoolK, NumK, StrK, ArrK, ObjK } kind = Null; bool b = false; std::string num; // raw literal std::string str; std::vector arr; std::vector> obj; static Json mkNull() { return {}; } static Json mkBool(bool v) { Json j; j.kind = BoolK; j.b = v; return j; } static Json mkNum(u64 v) { Json j; j.kind = NumK; j.num = std::to_string(v); return j; } static Json mkStr(std::string s) { Json j; j.kind = StrK; j.str = std::move(s); return j; } static Json mkArr() { Json j; j.kind = ArrK; return j; } static Json mkObj() { Json j; j.kind = ObjK; return j; } Json& set(const std::string& k, Json v) { obj.emplace_back(k, std::move(v)); return *this; } Json& add(Json v) { arr.push_back(std::move(v)); return *this; } const Json* get(const std::string& k) const { if (kind != ObjK) return nullptr; for (const auto& kv : obj) if (kv.first == k) return &kv.second; return nullptr; } // typed accessors with Go-style zero defaults on absence std::string s(const std::string& k) const { const Json* j = get(k); return j && j->kind == StrK ? j->str : ""; } u64 u(const std::string& k) const { const Json* j = get(k); if (!j || j->kind != NumK) return 0; return parseU64(j->num); } bool flag(const std::string& k) const { const Json* j = get(k); return j && j->kind == BoolK && j->b; } const std::vector& a(const std::string& k) const { static const std::vector empty; const Json* j = get(k); return j && j->kind == ArrK ? j->arr : empty; } static void escapeTo(std::string& o, const std::string& s) { for (char c : s) { switch (c) { case '"': o += "\\\""; break; case '\\': o += "\\\\"; break; case '\n': o += "\\n"; break; case '\r': o += "\\r"; break; case '\t': o += "\\t"; break; default: if (u8(c) < 0x20) { char b[8]; snprintf(b, sizeof b, "\\u%04x", c); o += b; } else { o += c; } } } } void dumpTo(std::string& o, int indent, int depth) const { std::string pad(size_t(indent) * (depth + 1), ' '); std::string pad0(size_t(indent) * depth, ' '); switch (kind) { case Null: o += "null"; break; case BoolK: o += b ? "true" : "false"; break; case NumK: o += num; break; case StrK: o += '"'; escapeTo(o, str); o += '"'; break; case ArrK: if (arr.empty()) { o += "[]"; break; } o += "[\n"; for (size_t i = 0; i < arr.size(); i++) { o += pad; arr[i].dumpTo(o, indent, depth + 1); if (i + 1 < arr.size()) o += ','; o += '\n'; } o += pad0 + "]"; break; case ObjK: if (obj.empty()) { o += "{}"; break; } o += "{\n"; for (size_t i = 0; i < obj.size(); i++) { o += pad + '"'; escapeTo(o, obj[i].first); o += "\": "; obj[i].second.dumpTo(o, indent, depth + 1); if (i + 1 < obj.size()) o += ','; o += '\n'; } o += pad0 + "}"; break; } } std::string dump() const { std::string o; dumpTo(o, 2, 0); o += '\n'; return o; } }; // --- parser struct JParser { const char* p; const char* e; int depth = 0; explicit JParser(const std::string& s) : p(s.data()), e(s.data() + s.size()) {} void ws() { while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; } [[noreturn]] void fail(const char* m) { throw Err(std::string("json: ") + m); } Json parse() { ws(); Json v = value(); ws(); if (p != e) fail("trailing data"); return v; } Json value() { if (++depth > 128) fail("too deep"); ws(); if (p >= e) fail("unexpected end"); Json v; switch (*p) { case '{': v = objV(); break; case '[': v = arrV(); break; case '"': v = Json::mkStr(strV()); break; case 't': lit("true"); v = Json::mkBool(true); break; case 'f': lit("false"); v = Json::mkBool(false); break; case 'n': lit("null"); v = Json::mkNull(); break; default: v = numV(); } depth--; return v; } void lit(const char* s) { size_t n = strlen(s); if (size_t(e - p) < n || memcmp(p, s, n) != 0) fail("bad literal"); p += n; } Json objV() { Json v = Json::mkObj(); p++; // { ws(); if (p < e && *p == '}') { p++; return v; } while (true) { ws(); if (p >= e || *p != '"') fail("expected key"); std::string k = strV(); ws(); if (p >= e || *p != ':') fail("expected :"); p++; v.obj.emplace_back(std::move(k), value()); ws(); if (p < e && *p == ',') { p++; continue; } if (p < e && *p == '}') { p++; return v; } fail("expected , or }"); } } Json arrV() { Json v = Json::mkArr(); p++; // [ ws(); if (p < e && *p == ']') { p++; return v; } while (true) { v.arr.push_back(value()); ws(); if (p < e && *p == ',') { p++; continue; } if (p < e && *p == ']') { p++; return v; } fail("expected , or ]"); } } std::string strV() { p++; // " std::string s; while (p < e && *p != '"') { char c = *p++; if (c != '\\') { s += c; continue; } if (p >= e) fail("bad escape"); char x = *p++; switch (x) { case '"': s += '"'; break; case '\\': s += '\\'; break; case '/': s += '/'; break; case 'b': s += '\b'; break; case 'f': s += '\f'; break; case 'n': s += '\n'; break; case 'r': s += '\r'; break; case 't': s += '\t'; break; case 'u': { if (e - p < 4) fail("bad \\u"); u32 cp = 0; for (int i = 0; i < 4; i++) { int h = hexVal(*p++); if (h < 0) fail("bad \\u"); cp = cp << 4 | u32(h); } // surrogate pair if (cp >= 0xD800 && cp <= 0xDBFF && e - p >= 6 && p[0] == '\\' && p[1] == 'u') { u32 lo = 0; const char* q = p + 2; bool ok = true; for (int i = 0; i < 4; i++) { int h = hexVal(q[i]); if (h < 0) { ok = false; break; } lo = lo << 4 | u32(h); } if (ok && lo >= 0xDC00 && lo <= 0xDFFF) { cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); p += 6; } } // encode UTF-8 if (cp < 0x80) s += char(cp); else if (cp < 0x800) { s += char(0xC0 | cp >> 6); s += char(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { s += char(0xE0 | cp >> 12); s += char(0x80 | ((cp >> 6) & 0x3F)); s += char(0x80 | (cp & 0x3F)); } else { s += char(0xF0 | cp >> 18); s += char(0x80 | ((cp >> 12) & 0x3F)); s += char(0x80 | ((cp >> 6) & 0x3F)); s += char(0x80 | (cp & 0x3F)); } break; } default: fail("bad escape"); } } if (p >= e) fail("unterminated string"); p++; // " return s; } Json numV() { const char* s = p; if (p < e && *p == '-') p++; while (p < e && ((*p >= '0' && *p <= '9') || *p == '.' || *p == 'e' || *p == 'E' || *p == '+' || *p == '-')) p++; if (p == s) fail("bad number"); Json v; v.kind = Json::NumK; v.num.assign(s, p); return v; } }; static Json jsonParse(const std::string& s) { return JParser(s).parse(); } // ============================================================== http // A deliberately plain HTTP/1.1 layer over POSIX sockets: enough for // the JSON API, the block push, and peer sync — the same surface the // Go node exposes with net/http. struct HttpReq { std::string method, path, query; Bytes body; }; struct HttpResp { int code = 200; std::string ctype = "application/json"; Bytes body; HttpResp() = default; HttpResp(int c, std::string ct, std::string b) : code(c), ctype(std::move(ct)) { body.assign(b.begin(), b.end()); } HttpResp(int c, std::string ct, Bytes b) : code(c), ctype(std::move(ct)), body(std::move(b)) {} }; static const size_t maxHttpHead = 64 << 10; static const size_t maxHttpBody = 64 << 20; static void setTimeouts(int fd, int sec) { timeval tv{sec, 0}; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); } static bool writeAll(int fd, const u8* p, size_t n) { while (n > 0) { ssize_t w = send(fd, p, n, MSG_NOSIGNAL); if (w <= 0) return false; p += w; n -= size_t(w); } return true; } static bool writeAll(int fd, const std::string& s) { return writeAll(fd, (const u8*)s.data(), s.size()); } static std::string lower(std::string s) { for (auto& c : s) c = char(tolower(u8(c))); return s; } // readUntilHeaders reads into buf until \r\n\r\n; returns header end // offset or npos. static size_t readHead(int fd, std::string& buf) { char tmp[4096]; while (buf.size() < maxHttpHead) { size_t hit = buf.find("\r\n\r\n"); if (hit != std::string::npos) return hit + 4; ssize_t n = recv(fd, tmp, sizeof tmp, 0); if (n <= 0) return std::string::npos; buf.append(tmp, size_t(n)); } return std::string::npos; } static bool readBodyN(int fd, std::string& buf, size_t already, size_t want, Bytes& out) { out.assign(buf.begin() + already, buf.end()); char tmp[8192]; while (out.size() < want) { ssize_t n = recv(fd, tmp, std::min(sizeof tmp, want - out.size()), 0); if (n <= 0) return false; out.insert(out.end(), tmp, tmp + n); } return out.size() == want; } // ------------------------------------------------------------ server struct HttpServer { int lfd = -1; std::function handler; void listenOn(const std::string& addr) { std::string host = "0.0.0.0", port = "8080"; size_t c = addr.rfind(':'); if (c != std::string::npos) { host = addr.substr(0, c); port = addr.substr(c + 1); } if (host.empty()) host = "0.0.0.0"; addrinfo hints{}, *res = nullptr; hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; if (getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || !res) throw Err("listen: cannot resolve " + addr); lfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (lfd < 0) throw Err("listen: socket failed"); int one = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); if (bind(lfd, res->ai_addr, res->ai_addrlen) != 0) { freeaddrinfo(res); throw Err("listen: bind " + addr + ": " + strerror(errno)); } freeaddrinfo(res); if (listen(lfd, 64) != 0) throw Err("listen failed"); } [[noreturn]] void serve() { while (true) { int cfd = accept(lfd, nullptr, nullptr); if (cfd < 0) continue; std::thread([this, cfd] { handleConn(cfd); }).detach(); } } void handleConn(int fd) { setTimeouts(fd, 30); std::string buf; size_t hend = readHead(fd, buf); if (hend == std::string::npos) { close(fd); return; } // request line size_t eol = buf.find("\r\n"); std::string line = buf.substr(0, eol); HttpReq req; { size_t sp1 = line.find(' '); size_t sp2 = line.rfind(' '); if (sp1 == std::string::npos || sp2 == sp1) { close(fd); return; } req.method = line.substr(0, sp1); std::string target = line.substr(sp1 + 1, sp2 - sp1 - 1); size_t q = target.find('?'); if (q == std::string::npos) { req.path = target; } else { req.path = target.substr(0, q); req.query = target.substr(q + 1); } } // headers size_t clen = 0; { size_t pos = eol + 2; while (pos < hend - 2) { size_t nl = buf.find("\r\n", pos); std::string h = buf.substr(pos, nl - pos); pos = nl + 2; size_t col = h.find(':'); if (col == std::string::npos) continue; std::string name = lower(h.substr(0, col)); std::string val = h.substr(col + 1); while (!val.empty() && val.front() == ' ') val.erase(val.begin()); if (name == "content-length") { try { clen = parseU64(val); } catch (...) { close(fd); return; } } } } if (clen > maxHttpBody) { close(fd); return; } if (clen > 0 && !readBodyN(fd, buf, hend, clen, req.body)) { close(fd); return; } HttpResp resp; if (req.method == "OPTIONS") { resp = HttpResp(200, "text/plain", std::string()); } else { try { resp = handler(req); } catch (const std::exception& e) { Json j = Json::mkObj(); j.set("error", Json::mkStr(e.what())); resp = HttpResp(500, "application/json", j.dump()); } } const char* stat = resp.code == 200 ? "OK" : resp.code == 400 ? "Bad Request" : resp.code == 404 ? "Not Found" : resp.code == 409 ? "Conflict" : resp.code == 422 ? "Unprocessable Entity" : "Internal Server Error"; std::string head = "HTTP/1.1 " + std::to_string(resp.code) + " " + stat + "\r\nAccess-Control-Allow-Origin: *" "\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS" "\r\nAccess-Control-Allow-Headers: Content-Type" "\r\nContent-Type: " + resp.ctype + "\r\nContent-Length: " + std::to_string(resp.body.size()) + "\r\nConnection: close\r\n\r\n"; writeAll(fd, head) && writeAll(fd, resp.body.data(), resp.body.size()); close(fd); } }; // ------------------------------------------------------------ client // parseURL: http://host[:port]/path — the only scheme peers use. static bool parseURL(const std::string& url, std::string& host, std::string& port, std::string& path) { const std::string pre = "http://"; if (url.rfind(pre, 0) != 0) return false; size_t hs = pre.size(); size_t slash = url.find('/', hs); std::string hostport = slash == std::string::npos ? url.substr(hs) : url.substr(hs, slash - hs); path = slash == std::string::npos ? "/" : url.substr(slash); size_t col = hostport.rfind(':'); if (col == std::string::npos) { host = hostport; port = "80"; } else { host = hostport.substr(0, col); port = hostport.substr(col + 1); } return !host.empty(); } static int dialTimeout(const std::string& host, const std::string& port, int sec) { addrinfo hints{}, *res = nullptr; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if (getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || !res) return -1; int fd = -1; for (addrinfo* ai = res; ai; ai = ai->ai_next) { fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (fd < 0) continue; int fl = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, fl | O_NONBLOCK); int rc = connect(fd, ai->ai_addr, ai->ai_addrlen); if (rc != 0 && errno == EINPROGRESS) { pollfd pfd{fd, POLLOUT, 0}; if (poll(&pfd, 1, sec * 1000) == 1) { int soerr = 0; socklen_t sl = sizeof soerr; getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl); rc = soerr == 0 ? 0 : -1; } else { rc = -1; } } if (rc == 0) { fcntl(fd, F_SETFL, fl); break; } close(fd); fd = -1; } freeaddrinfo(res); return fd; } // httpRequest performs one request with a 5 s timeout (mirroring the // Go peer client). Handles Content-Length, chunked, and read-to-EOF // bodies. Returns {status, body} or nullopt on transport failure. static std::optional> httpRequest(const std::string& method, const std::string& url, const Bytes& body, const std::string& ctype) { std::string host, port, path; if (!parseURL(url, host, port, path)) return std::nullopt; int fd = dialTimeout(host, port, 5); if (fd < 0) return std::nullopt; setTimeouts(fd, 5); std::string req = method + " " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\nAccept-Encoding: identity\r\n"; if (!body.empty() || method == "POST") req += "Content-Type: " + ctype + "\r\nContent-Length: " + std::to_string(body.size()) + "\r\n"; req += "\r\n"; if (!writeAll(fd, req) || !writeAll(fd, body.data(), body.size())) { close(fd); return std::nullopt; } std::string buf; size_t hend = readHead(fd, buf); if (hend == std::string::npos) { close(fd); return std::nullopt; } size_t eol = buf.find("\r\n"); std::string status = buf.substr(0, eol); int code = 0; { size_t sp = status.find(' '); if (sp == std::string::npos) { close(fd); return std::nullopt; } code = atoi(status.c_str() + sp + 1); } bool chunked = false; bool haveLen = false; size_t clen = 0; { size_t pos = eol + 2; while (pos < hend - 2) { size_t nl = buf.find("\r\n", pos); std::string h = buf.substr(pos, nl - pos); pos = nl + 2; size_t col = h.find(':'); if (col == std::string::npos) continue; std::string name = lower(h.substr(0, col)); std::string val = h.substr(col + 1); while (!val.empty() && val.front() == ' ') val.erase(val.begin()); if (name == "content-length") { haveLen = true; clen = size_t(strtoull(val.c_str(), nullptr, 10)); } else if (name == "transfer-encoding" && lower(val).find("chunked") != std::string::npos) { chunked = true; } } } Bytes out; if (chunked) { // decode chunked from buf tail + socket std::string rest = buf.substr(hend); auto more = [&](size_t want) -> bool { char tmp[8192]; while (rest.size() < want) { ssize_t n = recv(fd, tmp, sizeof tmp, 0); if (n <= 0) return false; rest.append(tmp, size_t(n)); } return true; }; size_t pos = 0; while (true) { size_t nl; while ((nl = rest.find("\r\n", pos)) == std::string::npos) { if (!more(rest.size() + 1)) { close(fd); return std::nullopt; } } size_t sz = strtoull(rest.c_str() + pos, nullptr, 16); pos = nl + 2; if (sz == 0) break; while (rest.size() < pos + sz + 2) if (!more(pos + sz + 2)) { close(fd); return std::nullopt; } out.insert(out.end(), rest.begin() + pos, rest.begin() + pos + sz); pos += sz + 2; if (out.size() > maxHttpBody) { close(fd); return std::nullopt; } } } else if (haveLen) { if (clen > maxHttpBody || !readBodyN(fd, buf, hend, clen, out)) { close(fd); return std::nullopt; } } else { out.assign(buf.begin() + hend, buf.end()); char tmp[8192]; ssize_t n; while ((n = recv(fd, tmp, sizeof tmp, 0)) > 0) { out.insert(out.end(), tmp, tmp + n); if (out.size() > maxHttpBody) break; } } close(fd); return std::make_pair(code, std::move(out)); } static std::optional> httpGet(const std::string& url) { return httpRequest("GET", url, {}, "text/plain"); } static std::optional> httpPost(const std::string& url, const Bytes& body, const std::string& ctype) { return httpRequest("POST", url, body, ctype); } // ============================================================ server // -------------------------------------------------------- JSON <-> tx static PubKey pKey(const std::string& s) { return toArr<32>(hexN(s, 32)); } static Hash32 p32(const std::string& s) { return toArr<32>(hexN(s, 32)); } static Sig pSig(const std::string& s) { if (s.empty()) return ZeroSig; // allowed unsigned (for /tx/prepare) return toArr<64>(hexN(s, 64)); } static std::vector pOutputs(const std::vector& js) { std::vector outs; for (const auto& o : js) { Output out; out.amount = parseAmount(o.s("amount")); out.owner = pKey(o.s("owner")); outs.push_back(out); } return outs; } static std::vector pInputs(const std::vector& js) { std::vector ins; for (const auto& in : js) ins.push_back(Outpoint{p32(in.s("tx")), u32(in.u("index"))}); return ins; } static NodeTemplate pTemplate(const Json& j) { NodeTemplate t; t.key = pKey(j.s("key")); t.leaf = j.flag("leaf"); t.nonce = j.u("nonce"); t.lastUBI = j.u("last_ubi"); t.lastVote = j.u("last_vote"); t.treeCount = j.u("tree_count"); std::string tu = j.s("tree_ubi"); if (!tu.empty()) { try { t.treeUBI = parseAmount(tu); } catch (const Err& e) { throw Err(std::string("tree_ubi: ") + e.what()); } } for (const auto& c : j.a("children")) t.children.push_back(pTemplate(c)); return t; } // toTx builds a transaction from JSON. For Add, the committed hash is // derived from the template. static TxPtr toTx(const Json& j) { std::string type = j.s("type"); if (type == "claim") { auto t = std::make_shared(); t->key = pKey(j.s("key")); t->amount = parseAmount(j.s("amount")); t->nonce = j.u("nonce"); t->sig = pSig(j.s("sig")); return t; } if (type == "vote_claim") { auto t = std::make_shared(); t->key = pKey(j.s("key")); t->nonce = j.u("nonce"); t->sig = pSig(j.s("sig")); return t; } if (type == "transfer") { auto t = std::make_shared(); t->inputs = pInputs(j.a("inputs")); t->outputs = pOutputs(j.a("outputs")); for (const auto& s : j.a("sigs")) t->sigs.push_back(pSig(s.str)); return t; } if (type == "prune") { auto t = std::make_shared(); t->inputs = pInputs(j.a("inputs")); return t; } if (type == "vote") { auto t = std::make_shared(); t->inputs = pInputs(j.a("inputs")); for (const auto& o : j.a("vote_outputs")) { VoteOutput vo; vo.committed = o.flag("committed"); if (vo.committed) { vo.commit = p32(o.s("commit")); vo.amount = 1; vo.mixed = u32(o.u("mixed")); std::string ow = o.s("owner"); if (!ow.empty()) { vo.owner = pKey(ow); vo.hasOwner = true; } } else { vo.owner = pKey(o.s("owner")); vo.amount = o.u("amount"); vo.mixed = u32(o.u("mixed")); } t->outputs.push_back(vo); } for (const auto& s : j.a("sigs")) t->sigs.push_back(pSig(s.str)); return t; } if (type == "add") { const Json* tj = j.get("template"); if (!tj || tj->kind != Json::ObjK) throw Err("add: template required"); auto t = std::make_shared(); t->parent = pKey(j.s("parent")); t->childKey = pKey(j.s("child_key")); t->tmpl = pTemplate(*tj); t->hashv = t->tmpl.hash(); t->nonce = j.u("nonce"); t->deadline = j.u("deadline"); t->consent = pSig(j.s("consent")); t->sig = pSig(j.s("sig")); return t; } if (type == "remove") { auto t = std::make_shared(); t->parent = pKey(j.s("parent")); t->child = pKey(j.s("child")); t->nonce = j.u("nonce"); t->sig = pSig(j.s("sig")); return t; } if (type == "rekey") { auto t = std::make_shared(); t->oldKey = pKey(j.s("old")); t->newKey = pKey(j.s("new")); t->nonce = j.u("nonce"); t->sig = pSig(j.s("sig")); return t; } if (type == "move") { auto t = std::make_shared(); t->child = pKey(j.s("child")); t->newParent = pKey(j.s("new_parent")); t->nonce = j.u("nonce"); t->deadline = j.u("deadline"); t->consent = pSig(j.s("consent")); t->sig = pSig(j.s("sig")); return t; } if (type == "leave") { auto t = std::make_shared(); t->child = pKey(j.s("child")); t->nonce = j.u("nonce"); t->sig = pSig(j.s("sig")); return t; } throw Err("unknown tx type \"" + type + "\""); } static Json jTemplate(const NodeTemplate& t) { Json j = Json::mkObj(); j.set("key", Json::mkStr(hex(t.key))); j.set("leaf", Json::mkBool(t.leaf)); if (t.nonce) j.set("nonce", Json::mkNum(t.nonce)); if (t.lastUBI) j.set("last_ubi", Json::mkNum(t.lastUBI)); if (t.lastVote) j.set("last_vote", Json::mkNum(t.lastVote)); j.set("tree_count", Json::mkNum(t.treeCount)); j.set("tree_ubi", Json::mkStr(u128str(t.treeUBI))); if (!t.children.empty()) { Json ch = Json::mkArr(); for (const auto& c : t.children) ch.add(jTemplate(c)); j.set("children", std::move(ch)); } return j; } // fromTx renders a transaction as the API JSON (used by /block, // /mempool and peer tx forwarding — parseable by Go and C++ alike). static Json fromTx(const Tx& t) { Json j = Json::mkObj(); auto sigsOf = [&](const std::vector& ss) { Json a = Json::mkArr(); for (const auto& s : ss) a.add(Json::mkStr(hex(s))); return a; }; auto insOf = [&](const std::vector& ins) { Json a = Json::mkArr(); for (const auto& in : ins) { Json o = Json::mkObj(); o.set("tx", Json::mkStr(hex(in.tx))); o.set("index", Json::mkNum(in.index)); a.add(std::move(o)); } return a; }; auto outsOf = [&](const std::vector& outs) { Json a = Json::mkArr(); for (const auto& o : outs) { Json jo = Json::mkObj(); jo.set("amount", Json::mkStr(u128str(o.amount))); jo.set("owner", Json::mkStr(hex(o.owner))); a.add(std::move(jo)); } return a; }; switch (t.opc()) { case OpClaim: { const auto& v = static_cast(t); j.set("type", Json::mkStr("claim")); j.set("key", Json::mkStr(hex(v.key))); j.set("nonce", Json::mkNum(v.nonce)); j.set("amount", Json::mkStr(u128str(v.amount))); j.set("sig", Json::mkStr(hex(v.sig))); break; } case OpVoteClaim: { const auto& v = static_cast(t); j.set("type", Json::mkStr("vote_claim")); j.set("key", Json::mkStr(hex(v.key))); j.set("nonce", Json::mkNum(v.nonce)); j.set("sig", Json::mkStr(hex(v.sig))); break; } case OpTransfer: { const auto& v = static_cast(t); j.set("type", Json::mkStr("transfer")); j.set("nonce", Json::mkNum(0)); j.set("outputs", outsOf(v.outputs)); j.set("inputs", insOf(v.inputs)); j.set("sigs", sigsOf(v.sigs)); break; } case OpPrune: { const auto& v = static_cast(t); j.set("type", Json::mkStr("prune")); j.set("nonce", Json::mkNum(0)); j.set("inputs", insOf(v.inputs)); break; } case OpVote: { const auto& v = static_cast(t); j.set("type", Json::mkStr("vote")); j.set("nonce", Json::mkNum(0)); j.set("inputs", insOf(v.inputs)); Json vo = Json::mkArr(); for (const auto& o : v.outputs) { Json e = Json::mkObj(); e.set("committed", Json::mkBool(o.committed)); if (o.committed) { e.set("commit", Json::mkStr(hex(o.commit))); e.set("mixed", Json::mkNum(o.mixed)); if (o.hasOwner) e.set("owner", Json::mkStr(hex(o.owner))); } else { e.set("amount", Json::mkNum(o.amount)); e.set("owner", Json::mkStr(hex(o.owner))); e.set("mixed", Json::mkNum(o.mixed)); } vo.add(std::move(e)); } j.set("vote_outputs", std::move(vo)); j.set("sigs", sigsOf(v.sigs)); break; } case OpAdd: { const auto& v = static_cast(t); j.set("type", Json::mkStr("add")); j.set("parent", Json::mkStr(hex(v.parent))); j.set("child_key", Json::mkStr(hex(v.childKey))); j.set("nonce", Json::mkNum(v.nonce)); j.set("deadline", Json::mkNum(v.deadline)); j.set("template", jTemplate(v.tmpl)); j.set("consent", Json::mkStr(hex(v.consent))); j.set("sig", Json::mkStr(hex(v.sig))); break; } case OpRemove: { const auto& v = static_cast(t); j.set("type", Json::mkStr("remove")); j.set("parent", Json::mkStr(hex(v.parent))); j.set("child", Json::mkStr(hex(v.child))); j.set("nonce", Json::mkNum(v.nonce)); j.set("sig", Json::mkStr(hex(v.sig))); break; } case OpRekey: { const auto& v = static_cast(t); j.set("type", Json::mkStr("rekey")); j.set("old", Json::mkStr(hex(v.oldKey))); j.set("new", Json::mkStr(hex(v.newKey))); j.set("nonce", Json::mkNum(v.nonce)); j.set("sig", Json::mkStr(hex(v.sig))); break; } case OpMove: { const auto& v = static_cast(t); j.set("type", Json::mkStr("move")); j.set("child", Json::mkStr(hex(v.child))); j.set("new_parent", Json::mkStr(hex(v.newParent))); j.set("nonce", Json::mkNum(v.nonce)); j.set("deadline", Json::mkNum(v.deadline)); j.set("consent", Json::mkStr(hex(v.consent))); j.set("sig", Json::mkStr(hex(v.sig))); break; } case OpLeave: { const auto& v = static_cast(t); j.set("type", Json::mkStr("leave")); j.set("child", Json::mkStr(hex(v.child))); j.set("nonce", Json::mkNum(v.nonce)); j.set("sig", Json::mkStr(hex(v.sig))); break; } default: break; } return j; } static std::string txTypeName(const Tx& t) { switch (t.opc()) { case OpClaim: return "claim"; case OpTransfer: return "transfer"; case OpPrune: return "prune"; case OpAdd: return "add"; case OpRemove: return "remove"; case OpRekey: return "rekey"; case OpMove: return "move"; case OpLeave: return "leave"; case OpVote: return "vote"; case OpVoteClaim: return "vote_claim"; } return "?"; } // ------------------------------------------------------------ logging static std::mutex logMu; static void logf(const char* fmt, ...) { char msg[1024]; va_list ap; va_start(ap, fmt); vsnprintf(msg, sizeof msg, fmt, ap); va_end(ap); time_t now = ::time(nullptr); tm tmv{}; localtime_r(&now, &tmv); std::lock_guard g(logMu); fprintf(stderr, "%02d:%02d:%02d %s\n", tmv.tm_hour, tmv.tm_min, tmv.tm_sec, msg); } // ------------------------------------------------------------- Server // Server: JSON API + mempool + block production loop + the network // layer. One mutex guards chain, store and mempool; all network I/O // happens outside the lock. struct Server { std::mutex mu; std::unique_ptr chain; std::unique_ptr store; std::vector mempool; Seed valPriv{}; std::vector> onions; u64 lastSlot = 0; // last slot we attempted, produced or not std::vector peers; std::unordered_set seen; // tx ids accepted this session // nowT is the block clock: wall time, never before the chain tip. u64 nowT() { u64 t = u64(::time(nullptr)); if (t < chain->state->time) t = chain->state->time; return t; } // ---------------------------------------------- production loop [[noreturn]] void produceLoop() { while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::lock_guard g(mu); u64 slot = slotOf(nowT()); if (slot > lastSlot && slotTime(slot) > chain->state->time) { lastSlot = slot; produceLocked(slotTime(slot)); } } } // expiredPrune scans for UTXOs with spendable(T) ≤ 0 and builds a // Prune collecting them (or nullptr). Inputs sorted for a // deterministic tx. std::shared_ptr expiredPrune(u64 T) { std::vector ops; chain->state->utxo->forEach([&](const Entry* e) { if (ops.size() < maxInputs && e->expired(T)) ops.push_back(e->op); }); if (ops.empty()) return nullptr; std::sort(ops.begin(), ops.end()); auto p = std::make_shared(); p->inputs = std::move(ops); return p; } void produceLocked(u64 T) { std::shared_ptr prune = expiredPrune(T); // Select the valid subset in submission order: each candidate // is tried on a scratch clone so a failing tx cannot poison // state. The validator's own prune goes first. StatePtr scratch = chain->state->clone(); std::vector keep; if (prune) { try { scratch->applyTxs({prune}, T); keep.push_back(prune); } catch (const std::exception& e) { logf("drop prune: %s", e.what()); // cannot happen by construction } } for (const auto& tx : mempool) { StatePtr trial = scratch->clone(); try { trial->applyTxs({tx}, T); } catch (const std::exception& e) { logf("drop tx %s: %s", hex(tx->id()).c_str(), e.what()); continue; } scratch = trial; keep.push_back(tx); } mempool.clear(); BlockPtr b; try { b = chain->produce(keep, T, valPriv, onions); } catch (const std::exception& e) { // Not our slot, onion exhausted, or a real failure: the // slot passes unfilled — a skip. logf("seq %llu skipped: %s", (unsigned long long)chain->state->seqAt(T), e.what()); if (!keep.empty()) { keep.insert(keep.end(), mempool.begin(), mempool.end()); mempool = std::move(keep); // retry next slot } return; } try { store->append(*b); } catch (const std::exception& e) { logf("store append failed: %s", e.what()); // cannot continue safely abort(); } logf("block %llu @ %llu: %zu tx, people %s utxo %s", (unsigned long long)b->header.seq, (unsigned long long)b->header.time, b->txs.size(), hexEncode(b->header.peopleTree.data(), 6).c_str(), hexEncode(b->header.utxoTrie.data(), 6).c_str()); Bytes body = EncodeBlock(*b); std::vector ps = peers; std::thread([body = std::move(body), ps] { for (const auto& p : ps) httpPost(p + "/api/block", body, "application/octet-stream"); }).detach(); } // -------------------------------------------------------- network // forwardTx relays a freshly accepted transaction to every peer. void forwardTx(const std::string& j) { Bytes body(j.begin(), j.end()); std::vector ps = peers; std::thread([body = std::move(body), ps] { for (const auto& p : ps) httpPost(p + "/api/tx", body, "application/json"); }).detach(); } // ingest is the write path for blocks from the network. Blocks // from the future (beyond one slot of clock drift) are refused at // this layer only — consensus itself stays clock-free. void ingest(const std::vector& blocks) { if (blocks.empty()) return; std::lock_guard g(mu); if (blocks.back()->header.time > nowT() + SlotSeconds) throw Err("ingest: block from the future"); int forkIdx = chain->tryAdopt(blocks); try { store->reorg(forkIdx + 1, blocks); } catch (const std::exception& e) { logf("store reorg failed: %s", e.what()); // cannot continue safely abort(); } logf("adopted %zu block(s) from peer, tip seq %llu @ %llu", blocks.size(), (unsigned long long)chain->state->seq, (unsigned long long)chain->state->time); } // readRecords parses a stream of length-prefixed block records. static std::vector readRecords(const Bytes& data) { std::vector out; size_t off = 0; while (off < data.size()) { if (data.size() - off < 4) throw Err("sync: truncated length"); u32 l = u32(data[off]) << 24 | u32(data[off + 1]) << 16 | u32(data[off + 2]) << 8 | u32(data[off + 3]); off += 4; if (data.size() - off < l) throw Err("sync: truncated record"); out.push_back(DecodeBlock(data.data() + off, l)); off += l; } return out; } // fetchChain pulls blocks with seq > from, following the server's // batching until the stream dries up. std::vector fetchChain(const std::string& peer, u64 from) { std::vector out; while (true) { auto resp = httpGet(peer + "/api/chain?from=" + std::to_string(from)); if (!resp) throw Err("sync: fetch failed"); std::vector blocks = readRecords(resp->second); if (blocks.empty()) return out; out.insert(out.end(), blocks.begin(), blocks.end()); from = blocks.back()->header.seq; } } // syncPeer compares tips with one peer and, when the peer's chain // is better, fetches and adopts it. Returns true if progress was // made. bool syncPeer(const std::string& peer) { auto resp = httpGet(peer + "/api/status"); if (!resp) throw Err("status fetch failed"); Json st = jsonParse(std::string(resp->second.begin(), resp->second.end())); u64 stBlocks = st.u("blocks"); std::string stTip = st.s("tip_hash"); std::string stGen = st.s("genesis_hash"); Hash32 ourGenesis; u64 ourBlocks, fromSeq, finalSeq; Hash32 ourTip; { std::lock_guard g(mu); ourGenesis = chain->blocks[0]->header.hash(); ourBlocks = chain->blocks.size(); ourTip = chain->tipHash(); fromSeq = chain->state->seq; finalSeq = chain->blocks[chain->finalIdx]->header.seq; } if (stGen != hex(ourGenesis)) throw Err("sync: peer has a different genesis"); if (stBlocks <= ourBlocks || stTip == hex(ourTip)) return false; // nothing better there // Fast path: everything above our tip; if the first block // doesn't extend us we diverged → refetch the reorg window. std::vector blocks = fetchChain(peer, fromSeq); if (blocks.empty()) return false; if (blocks[0]->header.prev != ourTip) { blocks = fetchChain(peer, finalSeq); std::unordered_set have; { std::lock_guard g(mu); for (size_t i = size_t(chain->finalIdx); i < chain->blocks.size(); i++) have.insert(chain->blocks[i]->header.hash()); } size_t cut = 0; while (cut < blocks.size() && have.count(blocks[cut]->header.hash())) cut++; blocks.erase(blocks.begin(), blocks.begin() + cut); if (blocks.empty()) return false; } ingest(blocks); return true; } // pollLoop keeps us in sync with every peer. [[noreturn]] void pollLoop() { while (true) { std::this_thread::sleep_for(std::chrono::seconds(2)); for (const auto& p : peers) { while (true) { bool more = false; try { more = syncPeer(p); } catch (const std::exception& e) { logf("sync %s: %s", p.c_str(), e.what()); break; } if (!more) break; } } } } // ------------------------------------------------------- handlers static HttpResp jresp(int code, const Json& j) { return HttpResp(code, "application/json", j.dump()); } static HttpResp jerr(int code, const std::string& msg) { Json j = Json::mkObj(); j.set("error", Json::mkStr(msg)); return jresp(code, j); } HttpResp handleStatus() { std::lock_guard g(mu); const State& st = *chain->state; u64 T = nowT(); u128 supply = st.supplyAt(T); u128 uncl = st.unclaimedAt(T); u128 sum = supply + uncl; u128 target = u128(st.tree->population()) * TOKEN; Json j = Json::mkObj(); j.set("seq", Json::mkNum(st.seq)); j.set("blocks", Json::mkNum(chain->blocks.size())); j.set("time", Json::mkNum(st.time)); j.set("now", Json::mkNum(T)); j.set("norm_time", Json::mkNum(st.normTime)); j.set("rand", Json::mkStr(hex(st.rnd))); j.set("tip_hash", Json::mkStr(hex(chain->tipHash()))); j.set("genesis_hash", Json::mkStr(hex(chain->blocks[0]->header.hash()))); j.set("period_start", Json::mkNum(periodStart(st.time))); j.set("phase", Json::mkStr(phaseOpen(st.time) ? "open" : "locked")); j.set("committed", Json::mkNum(st.election->committedCount())); j.set("committed_next", Json::mkNum(st.nextElection->committedCount())); j.set("election_root", Json::mkStr(hex(st.election->rootHash()))); j.set("people_root", Json::mkStr(hex(st.tree->rootHash()))); j.set("utxo_root", Json::mkStr(hex(st.utxo->rootHash()))); j.set("population", Json::mkNum(st.tree->population())); j.set("utxo_count", Json::mkNum(st.utxo->len())); j.set("supply", Json::mkStr(u128str(supply))); j.set("unclaimed", Json::mkStr(u128str(uncl))); j.set("sum", Json::mkStr(u128str(sum))); j.set("target", Json::mkStr(u128str(target))); j.set("mempool", Json::mkNum(mempool.size())); j.set("token", Json::mkStr(u128str(TOKEN))); j.set("rent_per_second", Json::mkStr(u128str(RentPerSecond))); j.set("slot_seconds", Json::mkNum(SlotSeconds)); j.set("period_seconds", Json::mkNum(PeriodSeconds)); j.set("max_mix", Json::mkNum(MaxMix)); return jresp(200, j); } HttpResp handleNode(const std::string& keyHex) { PubKey k; try { k = pKey(keyHex); } catch (const std::exception& e) { return jerr(400, e.what()); } std::lock_guard g(mu); PNode* n = chain->state->tree->get(k); if (!n) return jerr(404, "key not in tree"); u64 T = nowT(); Json j = Json::mkObj(); j.set("key", Json::mkStr(keyHex)); j.set("leaf", Json::mkBool(n->leaf)); j.set("own", Json::mkNum(n->own)); j.set("nonce", Json::mkNum(n->nonce)); j.set("last_ubi", Json::mkNum(n->lastUBI)); j.set("last_vote", Json::mkNum(n->lastVote)); j.set("tree_count", Json::mkNum(n->treeCount)); j.set("claimable_now", Json::mkStr(u128str(ClaimableAt(n->own, n->lastUBI, T)))); j.set("now", Json::mkNum(T)); j.set("parent", Json::mkStr(n->parent ? hex(n->parent->key) : "")); Json ch = Json::mkArr(); for (const auto* c : n->children) ch.add(Json::mkStr(hex(c->key))); j.set("children", std::move(ch)); return jresp(200, j); } HttpResp handleBalance(const std::string& keyHex) { PubKey k; try { k = pKey(keyHex); } catch (const std::exception& e) { return jerr(400, e.what()); } std::lock_guard g(mu); u64 T = nowT(); struct Ju { std::string tx; u32 index; u128 value, rentOwed; std::string spendable; bool expired; u64 created; }; u128 total = 0, spendTotal = 0; std::vector us; chain->state->utxo->forEach([&](const Entry* e) { if (e->owner != k) return; u128 v = e->value(T), rent = e->rent(T); total += v; if (v > rent) spendTotal += v - rent; us.push_back(Ju{hex(e->op.tx), e->op.index, v, rent, spendStr(v, rent), v <= rent, e->time}); }); std::sort(us.begin(), us.end(), [](const Ju& a, const Ju& b) { if (a.tx != b.tx) return a.tx < b.tx; return a.index < b.index; }); Json arr = Json::mkArr(); for (const auto& u : us) { Json e = Json::mkObj(); e.set("tx", Json::mkStr(u.tx)); e.set("index", Json::mkNum(u.index)); e.set("value_now", Json::mkStr(u128str(u.value))); e.set("rent_owed", Json::mkStr(u128str(u.rentOwed))); e.set("spendable", Json::mkStr(u.spendable)); if (u.expired) e.set("expired", Json::mkBool(true)); e.set("created", Json::mkNum(u.created)); arr.add(std::move(e)); } Json j = Json::mkObj(); j.set("now", Json::mkNum(T)); j.set("total", Json::mkStr(u128str(total))); j.set("spendable", Json::mkStr(u128str(spendTotal))); j.set("utxos", std::move(arr)); return jresp(200, j); } HttpResp handleBlock(const std::string& seqStr) { u64 seq; try { seq = parseU64(seqStr); } catch (const std::exception& e) { return jerr(400, e.what()); } std::lock_guard g(mu); const auto& bs = chain->blocks; auto it = std::lower_bound(bs.begin(), bs.end(), seq, [](const BlockPtr& b, u64 s) { return b->header.seq < s; }); if (it == bs.end() || (*it)->header.seq != seq) return jerr(404, "no block at that slot (skipped, or beyond the tip)"); const Block& b = **it; const Header& h = b.header; Json j = Json::mkObj(); j.set("seq", Json::mkNum(h.seq)); j.set("time", Json::mkNum(h.time)); j.set("people_tree", Json::mkStr(hex(h.peopleTree))); j.set("utxo_trie", Json::mkStr(hex(h.utxoTrie))); j.set("prev", Json::mkStr(hex(h.prev))); j.set("validator", Json::mkStr(hex(h.validator))); j.set("election_trie", Json::mkStr(hex(h.electionTrie))); j.set("rand", Json::mkStr(hex(h.rnd))); j.set("sig", Json::mkStr(hex(h.sig))); j.set("hash", Json::mkStr(hex(h.hash()))); Json txs = Json::mkArr(); for (const auto& t : b.txs) txs.add(fromTx(*t)); j.set("txs", std::move(txs)); return jresp(200, j); } HttpResp handleMempool() { std::lock_guard g(mu); Json arr = Json::mkArr(); for (const auto& t : mempool) { Json e = Json::mkObj(); e.set("id", Json::mkStr(hex(t->id()))); e.set("type", Json::mkStr(txTypeName(*t))); arr.add(std::move(e)); } return jresp(200, arr); } // handleTxPrepare: submit an UNSIGNED tx, get back the exact bytes // to sign (hex). Nothing is queued. HttpResp handleTxPrepare(const Bytes& body) { TxPtr t; try { Json j = jsonParse(std::string(body.begin(), body.end())); t = toTx(j); } catch (const std::exception& e) { return jerr(400, e.what()); } Hash32 id = t->id(); Json resp = Json::mkObj(); resp.set("id", Json::mkStr(hex(id))); resp.set("sign_message", Json::mkStr(hex(id))); // SigHash == ID std::string note = "sign with ed25519 over sign_message bytes; put hex signature in 'sig' (or 'sigs', one per input) and POST /tx"; if (t->opc() == OpVote) note = "sign with ed25519 over sign_message bytes; one sig per input in 'sigs'; POST /tx"; resp.set("note", Json::mkStr(note)); if (t->opc() == OpAdd) { const auto& a = static_cast(*t); resp.set("consent_message", Json::mkStr(hex(ConsentMsg(a.hashv, a.deadline, a.parent)))); resp.set("template_hash", Json::mkStr(hex(a.hashv))); } if (t->opc() == OpMove) { const auto& m = static_cast(*t); resp.set("consent_message", Json::mkStr(hex(MoveConsentMsg(m.deadline, m.newParent)))); } return jresp(200, resp); } HttpResp handleTxSubmit(const Bytes& body) { TxPtr t; try { Json j = jsonParse(std::string(body.begin(), body.end())); t = toTx(j); } catch (const std::exception& e) { return jerr(400, e.what()); } Hash32 id = t->id(); std::string forwarded; { std::lock_guard g(mu); if (seen.count(id)) { Json j = Json::mkObj(); j.set("id", Json::mkStr(hex(id))); j.set("status", Json::mkStr("known")); return jresp(200, j); } // Dry-run against confirmed state + current mempool, at // the earliest possible inclusion time. u64 T = nowT(); StatePtr trial = chain->state->clone(); for (const auto& p : mempool) { try { trial->applyTxs({p}, T); } catch (...) { // best effort; conflicts re-checked at production } } try { trial->applyTxs({t}, T); } catch (const std::exception& e) { return jerr(422, e.what()); } mempool.push_back(t); seen.insert(id); if (!peers.empty()) forwarded = fromTx(*t).dump(); } // Relay so the tx reaches whichever validator wins a slot. if (!forwarded.empty()) forwardTx(forwarded); Json j = Json::mkObj(); j.set("id", Json::mkStr(hex(id))); j.set("status", Json::mkStr("queued")); return jresp(200, j); } // handleGenesis serves the verbatim genesis record. HttpResp handleGenesis() { Buf w; w.u32b(u32(store->genesisRec.size())); w.bytes(store->genesisRec); return HttpResp(200, "application/octet-stream", w.b); } // handleChain streams length-prefixed block records with // seq > from, capped per request. HttpResp handleChain(const std::string& query) { u64 from = 0; if (query.rfind("from=", 0) == 0) { try { from = parseU64(query.substr(5)); } catch (const std::exception& e) { return jerr(400, e.what()); } } Buf w; { std::lock_guard g(mu); const auto& bs = chain->blocks; auto it = std::upper_bound(bs.begin(), bs.end(), from, [](u64 s, const BlockPtr& b) { return s < b->header.seq; }); size_t count = 0, total = 0; for (; it != bs.end() && count < 2048 && total < (4u << 20); ++it, ++count) { Bytes rec = EncodeBlock(**it); w.u32b(u32(rec.size())); w.bytes(rec); total += 4 + rec.size(); } } return HttpResp(200, "application/octet-stream", w.b); } // handleBlockPush accepts one pushed block (wire format). A block // that doesn't fit right now is a 409; the poll loop resolves any // real divergence. HttpResp handleBlockPush(const Bytes& body) { if (body.size() > maxTxBytes) return jerr(400, "block too large"); BlockPtr b; try { b = DecodeBlock(body); } catch (const std::exception& e) { return jerr(400, e.what()); } try { ingest({b}); } catch (const std::exception& e) { return jerr(409, e.what()); } Json j = Json::mkObj(); j.set("status", Json::mkStr("accepted")); return jresp(200, j); } HttpResp route(const HttpReq& r) { auto pathArg = [&](const char* prefix) -> std::optional { size_t n = strlen(prefix); if (r.path.size() > n && r.path.compare(0, n, prefix) == 0 && r.path.find('/', n) == std::string::npos) return r.path.substr(n); return std::nullopt; }; if (r.method == "GET") { if (r.path == "/api/status") return handleStatus(); if (auto k = pathArg("/api/node/")) return handleNode(*k); if (auto k = pathArg("/api/balance/")) return handleBalance(*k); if (auto s = pathArg("/api/block/")) return handleBlock(*s); if (r.path == "/api/mempool") return handleMempool(); if (r.path == "/api/genesis") return handleGenesis(); if (r.path == "/api/chain") return handleChain(r.query); if (r.path == "/") return HttpResp(200, "text/plain", std::string("hiercoin — endpoints: GET /api/status /api/node/{key} /api/balance/{key} " "/api/block/{seq} /api/chain?from=N /api/genesis /api/mempool; " "POST /api/tx/prepare /api/tx /api/block\n")); } if (r.method == "POST") { if (r.path == "/api/tx/prepare") return handleTxPrepare(r.body); if (r.path == "/api/tx") return handleTxSubmit(r.body); if (r.path == "/api/block") return handleBlockPush(r.body); } return jerr(404, "not found"); } }; // =============================================================== cmd static void usage() { fprintf(stderr, "usage: hiercoin keygen | init | run | join | sign | replay | selftest (use -h per command)\n"); exit(2); } // tiny flag parser: -name value or -name=value. struct Flags { std::map vals; std::map help; std::string cmd; void def(const std::string& name, const std::string& dflt, const std::string& h) { vals[name] = dflt; help[name] = h; } void parse(int argc, char** argv) { for (int i = 0; i < argc; i++) { std::string a = argv[i]; if (a == "-h" || a == "--help") { fprintf(stderr, "usage: hiercoin %s [flags]\n", cmd.c_str()); for (const auto& kv : vals) fprintf(stderr, " -%s %s\n %s (default %s)\n", kv.first.c_str(), "value", help[kv.first].c_str(), kv.second.empty() ? "\"\"" : kv.second.c_str()); exit(2); } if (a.size() < 2 || a[0] != '-') { fprintf(stderr, "unexpected argument %s\n", a.c_str()); exit(2); } std::string name = a.substr(a[1] == '-' ? 2 : 1); std::string val; size_t eq = name.find('='); if (eq != std::string::npos) { val = name.substr(eq + 1); name = name.substr(0, eq); } else { if (i + 1 >= argc) { fprintf(stderr, "flag -%s needs a value\n", name.c_str()); exit(2); } val = argv[++i]; } if (!vals.count(name)) { fprintf(stderr, "unknown flag -%s\n", name.c_str()); exit(2); } vals[name] = val; } } std::string s(const std::string& n) const { return vals.at(n); } u64 u(const std::string& n) const { return parseU64(vals.at(n)); } }; static std::string trim(const std::string& s) { size_t a = s.find_first_not_of(" \t\r\n"); if (a == std::string::npos) return ""; size_t b = s.find_last_not_of(" \t\r\n"); return s.substr(a, b - a + 1); } // loadSeed: hex string or @file containing it. static Seed loadSeed(const std::string& spec) { std::string s = spec; if (!spec.empty() && spec[0] == '@') { Bytes b = readFileAll(spec.substr(1)); s = trim(std::string(b.begin(), b.end())); } Bytes b; try { b = hexDecode(s); } catch (...) { throw Err("seed must be 32 hex bytes"); } if (b.size() != 32) throw Err("seed must be 32 hex bytes"); return toArr<32>(b); } static std::string seedPath(const std::string& dir) { return dir + "/validator.seed"; } static std::string onionPath(const std::string& dir) { return dir + "/onion"; } static std::string logPath(const std::string& dir) { return dir + "/chain.log"; } static void writeFile(const std::string& path, const std::string& content, mode_t mode) { int fd = open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, mode); if (fd < 0) throw Err(path + ": " + strerror(errno)); if (write(fd, content.data(), content.size()) != ssize_t(content.size())) { close(fd); throw Err(path + ": write failed"); } close(fd); } // saveOnions / loadOnions persist the node's onions, one // " " line each. Kept 0600 next to the validator key. struct OnionSpec { Hash32 seed{}; u64 depth = 0; }; static void saveOnions(const std::string& path, const std::vector& specs) { std::string b; for (const auto& sp : specs) b += hex(sp.seed) + " " + std::to_string(sp.depth) + "\n"; writeFile(path, b, 0600); } static std::vector> loadOnions(const std::string& path, const PubKey& candidate) { Bytes data = readFileAll(path); std::string s(data.begin(), data.end()); std::vector> onions; size_t pos = 0; while (pos <= s.size()) { size_t nl = s.find('\n', pos); std::string line = trim(nl == std::string::npos ? s.substr(pos) : s.substr(pos, nl - pos)); pos = nl == std::string::npos ? s.size() + 1 : nl + 1; if (line.empty()) continue; size_t sp = line.find(' '); if (sp == std::string::npos) throw Err("onion file: bad line"); Hash32 seed = toArr<32>(hexN(line.substr(0, sp), 32)); u64 depth = parseU64(trim(line.substr(sp + 1))); onions.push_back(std::make_unique(candidate, seed, depth)); } if (onions.empty()) throw Err("onion file: no onions"); return onions; } static void cmdKeygen() { Seed seed = GenSeed(); PubKey pub = PubFromSeed(seed); printf("seed %s\npub %s\n", hex(seed).c_str(), hex(pub).c_str()); } static void cmdSign(int argc, char** argv) { Flags fs; fs.cmd = "sign"; fs.def("seed", "", "hex seed or @file"); fs.def("msg", "", "hex message to sign"); fs.parse(argc, argv); Seed priv = loadSeed(fs.s("seed")); Bytes m = hexDecode(fs.s("msg")); Sig sig = SignMsg(priv, m); printf("%s\n", hex(sig).c_str()); } static void cmdInit(int argc, char** argv) { Flags fs; fs.cmd = "init"; fs.def("dir", "hiercoin-data", "data directory"); fs.def("root-pub", "", "root person pubkey hex (default: validator key)"); fs.def("onion-depth", std::to_string(PeriodSeconds / SlotSeconds), "per-onion depth for the two genesis votes (slots each can validate)"); fs.parse(argc, argv); std::string dir = fs.s("dir"); u64 depth = fs.u("onion-depth"); if (mkdir(dir.c_str(), 0755) != 0 && errno != EEXIST) throw Err(dir + ": " + strerror(errno)); Seed priv = GenSeed(); PubKey pub = PubFromSeed(priv); writeFile(seedPath(dir), hex(priv) + "\n", 0600); PubKey root = pub; if (!fs.s("root-pub").empty()) root = pKey(fs.s("root-pub")); // Genesis onions: one independent hash chain per seeded trie (see // the Go node for why independence matters). Hash32 seed0 = GenSeed(), seed1 = GenSeed(); saveOnions(onionPath(dir), {OnionSpec{seed0, depth}, OnionSpec{seed1, depth}}); Onion onion0(pub, seed0, depth), onion1(pub, seed1, depth); u64 t0 = u64(::time(nullptr)); auto ch = NewChain(root, t0, priv, onion0.commit(), onion1.commit()); t0 = ch->state->time; // slot-aligned by NewChain CreateStore(logPath(dir), root, t0, onion0.commit(), onion1.commit(), ch->blocks[0]->header); printf("initialized %s\n t0 %llu (slot %llu)\n root %s\n validator %s\n seed %s\n onions %s (2 × depth %llu)\n", dir.c_str(), (unsigned long long)t0, (unsigned long long)slotOf(t0), hex(root).c_str(), hex(pub).c_str(), seedPath(dir).c_str(), onionPath(dir).c_str(), (unsigned long long)depth); } static std::vector parsePeers(const std::string& s) { std::vector out; size_t pos = 0; while (pos <= s.size()) { size_t c = s.find(',', pos); std::string p = trim(c == std::string::npos ? s.substr(pos) : s.substr(pos, c - pos)); pos = c == std::string::npos ? s.size() + 1 : c + 1; while (!p.empty() && p.back() == '/') p.pop_back(); if (!p.empty()) out.push_back(p); } return out; } static void cmdRun(int argc, char** argv) { Flags fs; fs.cmd = "run"; fs.def("dir", "hiercoin-data", "data directory"); fs.def("listen", "127.0.0.1:8080", "listen address"); fs.def("peers", "", "comma-separated peer base URLs (http://host:port)"); fs.parse(argc, argv); std::string dir = fs.s("dir"); Seed priv = loadSeed("@" + seedPath(dir)); auto onions = loadOnions(onionPath(dir), PubFromSeed(priv)); auto [store, chain] = OpenStore(logPath(dir)); logf("replayed %zu block(s), seq %llu, population %llu, committed votes %llu, onions %zu", chain->blocks.size() - 1, (unsigned long long)chain->state->seq, (unsigned long long)chain->state->tree->population(), (unsigned long long)chain->state->election->committedCount(), onions.size()); auto srv = std::make_unique(); srv->chain = std::move(chain); srv->store = std::move(store); srv->valPriv = priv; srv->onions = std::move(onions); srv->lastSlot = slotOf(srv->chain->state->time); srv->peers = parsePeers(fs.s("peers")); Server* s = srv.get(); std::thread([s] { s->produceLoop(); }).detach(); if (!s->peers.empty()) { std::thread([s] { s->pollLoop(); }).detach(); std::string joined; for (size_t i = 0; i < s->peers.size(); i++) joined += (i ? ", " : "") + s->peers[i]; logf("peers: %s", joined.c_str()); } logf("listening on http://%s (slot %llus, one block per slot when selected)", fs.s("listen").c_str(), (unsigned long long)SlotSeconds); HttpServer http; http.handler = [s](const HttpReq& r) { return s->route(r); }; http.listenOn(fs.s("listen")); http.serve(); } // cmdJoin bootstraps a fresh data directory from a running peer. static void cmdJoin(int argc, char** argv) { Flags fs; fs.cmd = "join"; fs.def("dir", "hiercoin-data", "data directory"); fs.def("peer", "", "peer base URL to bootstrap from (required)"); fs.def("onion-depth", std::to_string(PeriodSeconds / SlotSeconds), "onion depth for this node's future votes"); fs.parse(argc, argv); std::string dir = fs.s("dir"); std::string peer = fs.s("peer"); if (peer.empty()) throw Err("join: -peer is required"); if (mkdir(dir.c_str(), 0755) != 0 && errno != EEXIST) throw Err(dir + ": " + strerror(errno)); while (!peer.empty() && peer.back() == '/') peer.pop_back(); auto resp = httpGet(peer + "/api/genesis"); if (!resp || resp->first != 200) throw Err("join: cannot fetch genesis from " + peer); // The record arrives with the log's length prefix; write verbatim // and let OpenStore do the full verification. int fd = open(logPath(dir).c_str(), O_CREAT | O_EXCL | O_WRONLY, 0644); if (fd < 0) throw Err(logPath(dir) + ": " + strerror(errno)); if (write(fd, resp->second.data(), resp->second.size()) != ssize_t(resp->second.size())) throw Err("join: write failed"); close(fd); std::unique_ptr chain; try { auto [st, ch] = OpenStore(logPath(dir)); chain = std::move(ch); } catch (const std::exception& e) { throw Err(std::string("join: peer genesis rejected: ") + e.what()); } Seed priv = GenSeed(); PubKey pub = PubFromSeed(priv); writeFile(seedPath(dir), hex(priv) + "\n", 0600); Hash32 oseed = GenSeed(); u64 depth = fs.u("onion-depth"); saveOnions(onionPath(dir), {OnionSpec{oseed, depth}}); Onion onion(pub, oseed, depth); Hash32 gh = chain->blocks[0]->header.hash(); printf("joined %s\n genesis %s (t0 %llu)\n validator %s\n vote for me: commit %s (depth %llu)\n next: run -dir %s -peers %s\n", dir.c_str(), hexEncode(gh.data(), 8).c_str(), (unsigned long long)chain->state->genesis, hex(pub).c_str(), hex(onion.commit()).c_str(), (unsigned long long)depth, dir.c_str(), peer.c_str()); } // cmdReplay opens the log, fully verifying every block, and prints a // summary — a standalone audit of a chain.log from any node. static void cmdReplay(int argc, char** argv) { Flags fs; fs.cmd = "replay"; fs.def("dir", "hiercoin-data", "data directory"); fs.parse(argc, argv); auto [store, chain] = OpenStore(logPath(fs.s("dir"))); const State& st = *chain->state; printf("verified %zu block(s)\n seq %llu\n time %llu\n tip %s\n genesis %s\n people %s\n utxo %s\n election %s\n population %llu\n utxos %zu\n supply %s\n unclaimed %s\n", chain->blocks.size() - 1, (unsigned long long)st.seq, (unsigned long long)st.time, hex(chain->tipHash()).c_str(), hex(chain->blocks[0]->header.hash()).c_str(), hex(st.tree->rootHash()).c_str(), hex(st.utxo->rootHash()).c_str(), hex(st.election->rootHash()).c_str(), (unsigned long long)st.tree->population(), st.utxo->len(), u128str(st.supplyAt(st.time)).c_str(), u128str(st.unclaimedAt(st.time)).c_str()); } // ========================================================== selftest // Deterministic internal tests: fixed keys and times, all transaction // types, period boundaries with validator handover, wire round-trips, // a reorg, and a store reopen. Exits non-zero on any failure. static void expect(bool cond, const std::string& what) { if (!cond) throw Err("selftest: FAILED: " + what); } static Seed seedN(u8 n) { Seed s{}; s[0] = n; s[31] = 0x5A; return s; } static void cmdSelftest() { // --- amount math vectors expect(DecayPow(0) == Scale, "decay^0 == SCALE"); expect(DecayPow(1) == DecayPerSecond, "decay^1 == decay"); u64 dy = DecayPow(SecondsPerYear); expect(dy < u64(0.8 * 1e16) && dy > u64(0.7999 * 1e16), "decay^YEAR just under 0.8 SCALE"); expect(RentOwed(0) == 0 && RentOwed(1) == RentPerSecond, "rent base cases"); expect(ClaimableAt(1, 0, 0) == 0, "claimable at t=0"); printf("decay^YEAR = %llu (< 0.8e16)\n", (unsigned long long)dy); // --- deterministic genesis Seed valPriv = seedN(1); PubKey valPub = PubFromSeed(valPriv); u64 depth = 4096 + 7; // crosses one onion checkpoint stride Hash32 os0{}, os1{}; os0[1] = 1; os1[1] = 2; auto onion0 = std::make_unique(valPub, os0, depth); auto onion1 = std::make_unique(valPub, os1, depth); expect(onion0->commit() == OnionCommit(valPub, os0, depth), "onion commit consistency"); u64 t0 = normTimeFor(1'750'000'000ULL) + 120; // deterministic, slot-aligned auto ch = NewChain(valPub, t0, valPriv, onion0->commit(), onion1->commit()); expect(ch->state->genesis == t0 && ch->state->time == t0, "genesis time"); std::vector> onions; onions.push_back(std::move(onion0)); onions.push_back(std::move(onion1)); // --- empty block production + verify determinism u64 T = t0 + SlotSeconds; BlockPtr b1 = ch->produce({}, T, valPriv, onions); expect(b1->header.seq == 1, "seq 1"); { Bytes enc = EncodeBlock(*b1); BlockPtr dec = DecodeBlock(enc); expect(dec->header.hash() == b1->header.hash(), "block wire round-trip"); } // --- claim after ~30 days Seed aliceP = seedN(2); PubKey alice = PubFromSeed(aliceP); T = t0 + 30 * 24 * 3600; // still slot-aligned (multiple of 60) T -= T % SlotSeconds; u128 claimable = ClaimableAt(1, t0, T); expect(claimable > 0, "claimable grows"); auto cl = std::make_shared(); cl->key = valPub; // root person == validator here cl->amount = claimable / 2; cl->nonce = 0; cl->sig = SignMsg(valPriv, cl->sigHash()); BlockPtr b2 = ch->produce({cl}, T, valPriv, onions); expect(ch->state->utxo->len() == 2, "claim UTXO + fee UTXO"); u128 supply = ch->state->supplyAt(T); u128 uncl = ch->state->unclaimedAt(T); // exact accounting at the claim instant: claim + fee outputs carry // the full accrual (supply == claimable up to per-UTXO floor // rounding in the normalize/denormalize round trip), and the // node's own unclaimed just reset to ~0. expect(supply <= claimable && claimable - supply < 10, "claim mints the full accrual"); expect(uncl < 10, "unclaimed resets after claim"); expect(supply + uncl <= tokens(1), "sum bounded by population × TOKEN"); // --- add a child (with template), then transfer to it auto add = std::make_shared(); add->parent = valPub; add->childKey = alice; add->tmpl.key = alice; add->tmpl.leaf = true; add->tmpl.treeCount = 1; add->hashv = add->tmpl.hash(); add->nonce = 1; // after the claim bumped it add->deadline = T + 3600; add->consent = SignMsg(aliceP, ConsentMsg(add->hashv, add->deadline, add->parent)); add->sig = SignMsg(valPriv, add->sigHash()); T += SlotSeconds; ch->produce({add}, T, valPriv, onions); expect(ch->state->tree->population() == 2, "population 2"); // transfer half of the claimed UTXO to alice Outpoint claimOp{cl->id(), 0}; const Entry* ce = ch->state->utxo->get(claimOp); expect(ce != nullptr, "claim utxo exists"); T += SlotSeconds; u128 val = ce->value(T), rent = ce->rent(T); expect(val > rent, "claim utxo not expired"); auto tr = std::make_shared(); tr->inputs = {claimOp}; tr->outputs = {Output{(val - rent) / 2, alice}}; tr->sigs = {SignMsg(valPriv, tr->sigHash())}; ch->produce({tr}, T, valPriv, onions); const Entry* ae = ch->state->utxo->get(Outpoint{tr->id(), 0}); expect(ae && ae->owner == alice, "alice received transfer"); // --- vote claims are gated to once per period, and genesis marks // the root's first claim as spent (last_vote = t0): claiming in // the genesis period must fail, then succeed after the boundary. { auto early = std::make_shared(); early->key = valPub; early->nonce = 2; early->sig = SignMsg(valPriv, early->sigHash()); StatePtr trial = ch->state->clone(); bool rejected = false; try { trial->applyTxs({TxPtr(early)}, T + SlotSeconds); } catch (const Err&) { rejected = true; } expect(rejected, "vote claim in genesis period rejected"); } // cross the first boundary: genesis commit1 takes over as the // active election; next_election opens fresh. u64 B1 = periodStart(t0) + PeriodSeconds; T = B1 + SlotSeconds; ch->produce({}, T, valPriv, onions); // ~half a million skipped slots fold into rand expect(ch->state->election->committedCount() == 1, "genesis commit1 active in period 1"); expect(ch->state->nextElection->len() == 0, "fresh next_election"); // vote claims for both persons, then mix + commit alice's onion Hash32 aliceSeed{}; aliceSeed[2] = 9; u64 adep = 2048; auto aliceOnion = std::make_unique(alice, aliceSeed, adep); auto vc1 = std::make_shared(); vc1->key = valPub; vc1->nonce = 2; vc1->sig = SignMsg(valPriv, vc1->sigHash()); auto vc2 = std::make_shared(); vc2->key = alice; vc2->nonce = 0; vc2->sig = SignMsg(aliceP, vc2->sigHash()); T += SlotSeconds; ch->produce({vc1, vc2}, T, valPriv, onions); expect(ch->state->nextElection->len() == 2, "2 vote tokens in next_election"); // mix the two claims together, commit one for alice, keep one // uncommitted (discarded at the boundary). auto vt = std::make_shared(); vt->inputs = {Outpoint{vc1->id(), 0}, Outpoint{vc2->id(), 0}}; VoteOutput o1; o1.committed = true; o1.commit = aliceOnion->commit(); o1.mixed = 2; VoteOutput o2; o2.committed = false; o2.amount = 1; o2.owner = alice; o2.mixed = 1; vt->outputs = {o1, o2}; Hash32 vsh = vt->sigHash(); vt->sigs = {SignMsg(valPriv, vsh), SignMsg(aliceP, vsh)}; T += SlotSeconds; ch->produce({vt}, T, valPriv, onions); expect(ch->state->nextElection->committedCount() == 1, "alice's commit in next_election"); expect(ch->state->nextElection->len() == 2, "committed + uncommitted leftover"); // --- cross the second boundary: alice's commit is now the ONLY // active entry, so only she can produce; the genesis validator's // onions must fail. u64 Tb = B1 + PeriodSeconds + SlotSeconds; std::vector> aliceOnions; aliceOnions.push_back(std::move(aliceOnion)); { bool valFailed = false; try { BuildBlock(*ch->state, {}, Tb, valPriv, onions); } catch (const Err&) { valFailed = true; } expect(valFailed, "genesis validator cannot produce in period 2"); } auto [bb, after] = BuildBlock(*ch->state, {}, Tb, aliceP, aliceOnions); StatePtr vs = VerifyBlock(*ch->state, *bb); expect(vs->lastHash == after->lastHash, "boundary block verifies"); expect(bb->header.validator == alice, "alice validates period 2"); // adopt via chain (single-block branch on top of tip) ch->tryAdopt({bb}); expect(ch->state->lastHash == bb->header.hash(), "adopt extended tip"); // --- fork choice: build two competing branches, longer one wins { StatePtr base = ch->state; u64 Tf = Tb + SlotSeconds; auto whoCan = [&](u64 t, const State& st) -> std::pair { try { return BuildBlock(st, {}, t, aliceP, aliceOnions); } catch (const Err&) { return BuildBlock(st, {}, t, valPriv, onions); } }; auto [s1, s1st] = whoCan(Tf + SlotSeconds, *base); // short branch: skips one slot auto [l1, l1st] = whoCan(Tf, *base); // long branch: fills both auto [l2, l2st] = whoCan(Tf + SlotSeconds, *l1st); ch->tryAdopt({s1}); expect(ch->tipHash() == s1->header.hash(), "short branch adopted first"); ch->tryAdopt({l1, l2}); // strictly more blocks from the same fork → wins expect(ch->tipHash() == l2->header.hash(), "fork choice: fewest skips wins"); bool rejected = false; try { ch->tryAdopt({s1}); } catch (const Err&) { rejected = true; } expect(rejected, "shorter branch refused"); } // --- store round-trip: write everything, reopen, verify std::string dir = "/tmp/hiercoin-selftest"; unlink(logPath(dir).c_str()); mkdir(dir.c_str(), 0755); { auto store = CreateStore(logPath(dir), valPub, t0, OnionCommit(valPub, os0, depth), OnionCommit(valPub, os1, depth), ch->blocks[0]->header); for (size_t i = 1; i < ch->blocks.size(); i++) store->append(*ch->blocks[i]); } { auto [store2, chain2] = OpenStore(logPath(dir)); expect(chain2->tipHash() == ch->tipHash(), "store reopen replays to same tip"); expect(chain2->state->tree->rootHash() == ch->state->tree->rootHash(), "same people root"); expect(chain2->state->utxo->rootHash() == ch->state->utxo->rootHash(), "same utxo root"); } // --- remove: alice leaves, her UBI auto-mints { auto lv = std::make_shared(); lv->child = alice; PNode* an = ch->state->tree->get(alice); lv->nonce = an->nonce; lv->sig = SignMsg(aliceP, lv->sigHash()); u64 Tl = ch->state->time + SlotSeconds; auto [lb, lst] = [&] { try { return BuildBlock(*ch->state, {lv}, Tl, aliceP, aliceOnions); } catch (const Err&) { return BuildBlock(*ch->state, {lv}, Tl, valPriv, onions); } }(); ch->tryAdopt({lb}); expect(ch->state->tree->population() == 1, "alice left"); expect(ch->state->tree->get(alice) == nullptr, "alice gone from tree"); const Entry* ub = ch->state->utxo->get(Outpoint{lv->id(), 0}); expect(ub && ub->owner == alice, "leave auto-minted alice's UBI"); } // --- json tx round trip { Json j = fromTx(*tr); TxPtr back = toTx(j); expect(back->id() == tr->id(), "json transfer round-trip"); Json ja = fromTx(*add); TxPtr backA = toTx(ja); expect(backA->id() == add->id(), "json add round-trip (template hash rederived)"); } printf("selftest OK — %zu blocks, tip %s\n", ch->blocks.size() - 1, hexShort(ch->tipHash()).c_str()); } int main(int argc, char** argv) { signal(SIGPIPE, SIG_IGN); if (argc < 2) usage(); std::string cmd = argv[1]; try { if (cmd == "keygen") cmdKeygen(); else if (cmd == "init") cmdInit(argc - 2, argv + 2); else if (cmd == "run") cmdRun(argc - 2, argv + 2); else if (cmd == "join") cmdJoin(argc - 2, argv + 2); else if (cmd == "sign") cmdSign(argc - 2, argv + 2); else if (cmd == "replay") cmdReplay(argc - 2, argv + 2); else if (cmd == "selftest") cmdSelftest(); else usage(); } catch (const std::exception& e) { fprintf(stderr, "%s\n", e.what()); return 1; } return 0; }