// Hiercoin — Fractal Social Hierarchy UTXO with UBI by demurrage. // Rust port of the reference Go node (hiercoin.go); consensus-, wire- // and API-compatible with the Go and C++ implementations (verified // byte-for-byte — see README). 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: sha2 + ed25519-dalek (crates.io), std only otherwise. // // cargo build --release // // 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) #![allow(clippy::too_many_arguments)] use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::fmt; use std::sync::{Mutex, OnceLock}; // =============================================================== util pub type Hash32 = [u8; 32]; pub type PubKey = [u8; 32]; pub type Sig = [u8; 64]; pub type Seed = [u8; 32]; pub const ZERO32: Hash32 = [0u8; 32]; pub const ZERO_SIG: Sig = [0u8; 64]; // Err is the single error type. The Go node distinguishes returned // errors from panics (which kill the process); here both become Err // results, propagated to 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. #[derive(Debug, Clone)] pub struct Err(pub String); impl fmt::Display for Err { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl std::error::Error for Err {} impl From<&str> for Err { fn from(s: &str) -> Self { Err(s.to_string()) } } impl From for Err { fn from(s: String) -> Self { Err(s) } } impl From for Err { fn from(e: std::io::Error) -> Self { Err(e.to_string()) } } pub type R = Result; macro_rules! bail { ($($t:tt)*) => { return Result::Err(Err(format!($($t)*))) }; } pub fn hex(b: &[u8]) -> String { const D: &[u8; 16] = b"0123456789abcdef"; let mut s = String::with_capacity(2 * b.len()); for &x in b { s.push(D[(x >> 4) as usize] as char); s.push(D[(x & 15) as usize] as char); } s } pub fn hex_short(h: &Hash32) -> String { hex(&h[..8]) } fn hex_val(c: u8) -> i32 { match c { b'0'..=b'9' => (c - b'0') as i32, b'a'..=b'f' => (c - b'a') as i32 + 10, b'A'..=b'F' => (c - b'A') as i32 + 10, _ => -1, } } // hex_decode decodes s; errors on bad characters or odd length. pub fn hex_decode(s: &str) -> R> { let b = s.as_bytes(); if b.len() % 2 != 0 { bail!("hex: odd length"); } let mut out = Vec::with_capacity(b.len() / 2); for i in 0..b.len() / 2 { let (a, c) = (hex_val(b[2 * i]), hex_val(b[2 * i + 1])); if a < 0 || c < 0 { bail!("hex: bad character"); } out.push(((a << 4) | c) as u8); } Ok(out) } // hex_n mirrors the Go helper: exactly n bytes or error. pub fn hex_n(s: &str, n: usize) -> R> { let b = hex_decode(s)?; if b.len() != n { bail!("want {} bytes, got {}", n, b.len()); } Ok(b) } pub fn to32(b: &[u8]) -> R<[u8; 32]> { if b.len() != 32 { bail!("bad length"); } let mut a = [0u8; 32]; a.copy_from_slice(b); Ok(a) } pub fn to64(b: &[u8]) -> R<[u8; 64]> { if b.len() != 64 { bail!("bad length"); } let mut a = [0u8; 64]; a.copy_from_slice(b); Ok(a) } pub fn p_key(s: &str) -> R { to32(&hex_n(s, 32)?) } pub fn p_sig(s: &str) -> R { if s.is_empty() { return Ok(ZERO_SIG); // allowed unsigned (for /tx/prepare) } to64(&hex_n(s, 64)?) } // i-flavoured print for the (rare) signed spendable values. pub fn spend_str(value: u128, rent: u128) -> String { if value >= rent { format!("{}", value - rent) } else { format!("-{}", rent - value) } } // parse_amount mirrors Go's pAmount: base-10, non-negative, ≤128 bits. pub fn parse_amount(s: &str) -> R { let t = s.strip_prefix('+').unwrap_or(s); if t.is_empty() || !t.bytes().all(|c| c.is_ascii_digit()) { bail!("bad amount"); } t.parse::().map_err(|_| Err("bad amount".into())) } pub fn parse_u64(s: &str) -> R { if s.is_empty() || !s.bytes().all(|c| c.is_ascii_digit()) { bail!("bad number"); } s.parse::().map_err(|_| Err("number overflow".into())) } // =============================================================== 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`). #[derive(Default)] pub struct Buf { pub b: Vec, } impl Buf { pub fn u8b(&mut self, x: u8) { self.b.push(x); } pub fn u32b(&mut self, x: u32) { self.b.extend_from_slice(&x.to_be_bytes()); } pub fn u64b(&mut self, x: u64) { self.b.extend_from_slice(&x.to_be_bytes()); } // u128b writes exactly 16 big-endian bytes (amounts are unsigned // 128-bit by definition; the type enforces the range the Go node // panics on). pub fn u128b(&mut self, x: u128) { self.b.extend_from_slice(&x.to_be_bytes()); } pub fn bytes(&mut self, p: &[u8]) { self.b.extend_from_slice(p); } pub fn boolb(&mut self, v: bool) { self.u8b(if v { 1 } else { 0 }); } } // H is SHA-256 over the given bytes. pub fn h(p: &[u8]) -> Hash32 { let d = Sha256::digest(p); let mut o = [0u8; 32]; o.copy_from_slice(&d); o } // Two-part convenience (hash onions: H(candidate || layer)). pub fn h2(a: &PubKey, b: &Hash32) -> Hash32 { let mut buf = [0u8; 64]; buf[..32].copy_from_slice(a); buf[32..].copy_from_slice(b); h(&buf) } // ============================================================ 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. pub const SCALE: u64 = 10_000_000_000_000_000; pub const TOKEN: u64 = 10_000_000_000_000_000; // 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. pub const DECAY_PER_SECOND: u64 = 9_999_999_929_290_076; // RENT_PER_SECOND is the trie rent: 1000 base units/second per UTXO. pub const RENT_PER_SECOND: u64 = 1000; // RENT_DENOM = SCALE − decay, the per-second fixed-point loss. pub const RENT_DENOM: u64 = SCALE - DECAY_PER_SECOND; pub const SECONDS_PER_YEAR: u64 = 31_557_600; // Julian year // Consensus timing, fixed by the spec (see the Go node for the full // divisibility rationale: slot | period | NormPeriod). pub const SLOT_SECONDS: u64 = 60; pub const PERIOD_SECONDS: u64 = SECONDS_PER_YEAR; // MAX_MIX caps a vote token's total hop budget (see spec / Go node). pub const MAX_MIX: u32 = 10; // norm_time sits on multiples of 4 × YEAR from Unix 0. pub const NORM_PERIOD: u64 = 4 * SECONDS_PER_YEAR; pub fn norm_time_for(t: u64) -> u64 { t - t % NORM_PERIOD } // --- 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. fn mul128x64(a: u128, b: u64) -> [u64; 3] { let (a0, a1) = (a as u64, (a >> 64) as u64); let p0 = a0 as u128 * b as u128; let p1 = a1 as u128 * b as u128 + (p0 >> 64); [p0 as u64, p1 as u64, (p1 >> 64) as u64] } fn div192by64(x: &[u64; 3], d: u64) -> [u64; 3] { let mut q = [0u64; 3]; let mut rem: u128 = 0; for i in (0..3).rev() { let cur = (rem << 64) | x[i] as u128; q[i] = (cur / d as u128) as u64; rem = cur % d as u128; } q } // mul_scale computes floor(a*b / SCALE) for b ≤ SCALE (every call // site passes a decay power). The quotient is then ≤ a, so it always // fits u128. pub fn mul_scale(a: u128, b: u64) -> u128 { let q = div192by64(&mul128x64(a, b), SCALE); debug_assert!(q[2] == 0); ((q[1] as u128) << 64) | q[0] as u128 } static POW_CACHE: OnceLock>> = OnceLock::new(); // decay_pow 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. pub fn decay_pow(dt: u64) -> u64 { let cache = POW_CACHE.get_or_init(|| Mutex::new(HashMap::new())); if let Some(&v) = cache.lock().unwrap().get(&dt) { return v; } let mut res = SCALE; let mut base = DECAY_PER_SECOND; let mut e = dt; while e > 0 { if e & 1 == 1 { res = (res as u128 * base as u128 / SCALE as u128) as u64; } if e > 1 { base = (base as u128 * base as u128 / SCALE as u128) as u64; } e >>= 1; } cache.lock().unwrap().insert(dt, res); 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 error surfaces at the same transaction/block boundary. pub fn normalize(amount: u128, t: u64, norm: u64) -> R { if t < norm { bail!("Normalize: time before norm_time"); } let p = decay_pow(t - norm); let q = div192by64(&mul128x64(amount, SCALE), p); if q[2] != 0 { bail!("normalized amount out of range"); } Ok(((q[1] as u128) << 64) | q[0] as u128) } // value_at converts a normalized value back to its real value at T: // floor(norm * decay^(T-norm_time) / SCALE). pub fn value_at(norm_val: u128, t: u64, norm_time: u64) -> R { if t < norm_time { bail!("ValueAt: time before norm_time"); } Ok(mul_scale(norm_val, decay_pow(t - norm_time))) } // rent_owed: rent × (SCALE − decay^dt) / (SCALE − decay), floor. // rent_owed(0) = 0, rent_owed(1) = rent; bounded by rent×SCALE/denom. pub fn rent_owed(dt: u64) -> u128 { if dt == 0 { return 0; } let n = (SCALE - decay_pow(dt)) as u128 * RENT_PER_SECOND as u128; n / RENT_DENOM as u128 } pub fn tokens(n: u64) -> u128 { n as u128 * TOKEN as u128 } // claimable_at: contribution × TOKEN × (1 − decay^(T − last_ubi)). pub fn claimable_at(own: u64, last_ubi: u64, t: u64) -> u128 { if t <= last_ubi || own == 0 { return 0; } let ct = tokens(own); ct - mul_scale(ct, decay_pow(t - last_ubi)) } // checked u128 add — Go's big.Int grows silently and the overflow is // caught at the next 128-bit encode or BitLen check; an error here // lands at the same boundary. pub fn add_checked(a: u128, b: u128, what: &str) -> R { a.checked_add(b).ok_or_else(|| Err(format!("{} overflow", what))) } // ============================================================== keys use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey}; // A private key is carried as its 32-byte seed (same as Go's // ed25519.PrivateKey.Seed()); dalek's SigningKey IS the seed. pub fn pub_from_seed(seed: &Seed) -> PubKey { SigningKey::from_bytes(seed).verifying_key().to_bytes() } pub fn gen_seed() -> R { let mut s = [0u8; 32]; std::fs::File::open("/dev/urandom")? .read_exact(&mut s) .map_err(|e| Err(format!("rand: {}", e)))?; Ok(s) } pub fn sign_msg(seed: &Seed, msg: &[u8]) -> Sig { SigningKey::from_bytes(seed).sign(msg).to_bytes() } pub fn verify_sig(pubkey: &PubKey, msg: &[u8], sig: &Sig) -> bool { let Ok(vk) = VerifyingKey::from_bytes(pubkey) else { return false; }; vk.verify(msg, &ed25519_dalek::Signature::from_bytes(sig)).is_ok() } // ================================================================ tx // Opcodes. Every signature in the system covers an encoding that // begins with an opcode ("all signatures include an opcode"). pub const OP_CLAIM: u8 = 0x01; pub const OP_TRANSFER: u8 = 0x02; pub const OP_ADD: u8 = 0x03; pub const OP_REMOVE: u8 = 0x04; pub const OP_MOVE: u8 = 0x05; pub const OP_LEAVE: u8 = 0x06; pub const OP_REKEY: u8 = 0x07; pub const OP_PRUNE: u8 = 0x08; pub const OP_VOTE: u8 = 0x09; // election mix/commit pub const OP_VOTE_CLAIM: u8 = 0x0A; // mint this period's vote tokens pub const OP_HEADER: u8 = 0xF0; // Output as it appears inside a transaction: consensus stamps `time` // at processing, so transactions only carry (amount, owner). #[derive(Clone, Debug, Default)] pub struct Output { pub amount: u128, pub owner: PubKey, } fn encode_outputs(w: &mut Buf, outs: &[Output]) { w.u32b(outs.len() as u32); for o in outs { w.u128b(o.amount); w.bytes(&o.owner); } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Outpoint { pub tx: Hash32, pub index: u32, } // The transaction types, one variant per opcode. ID excludes all // signatures (anti-malleability); for single-signer transactions it // doubles as the signing hash. #[derive(Clone, Debug)] pub struct Claim { pub key: PubKey, pub amount: u128, pub nonce: u64, pub sig: Sig, } #[derive(Clone, Debug)] pub struct Transfer { pub inputs: Vec, pub outputs: Vec, pub sigs: Vec, // one per input, over sig_hash } #[derive(Clone, Debug, Default)] pub struct Prune { pub inputs: Vec, // no signatures: validity is objective } // 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). #[derive(Clone, Debug, Default)] pub struct NodeTemplate { pub key: PubKey, pub leaf: bool, pub nonce: u64, pub last_ubi: u64, pub last_vote: u64, pub tree_count: u64, pub tree_ubi: u128, pub children: Vec, } impl NodeTemplate { pub fn count_nodes(&self) -> usize { 1 + self.children.iter().map(|c| c.count_nodes()).sum::() } pub fn hash(&self) -> Hash32 { let ch: Vec = self.children.iter().map(|c| c.hash()).collect(); node_hash(&self.key, &ch, self.leaf, self.nonce, self.last_ubi, self.last_vote, self.tree_count, self.tree_ubi) } } #[derive(Clone, Debug)] pub struct Add { pub parent: PubKey, pub child_key: PubKey, pub hashv: Hash32, pub nonce: u64, // parent's nonce pub deadline: u64, // block time after which consent expires pub consent: Sig, // by child_key over consent_msg pub sig: Sig, // by parent over sig_hash pub tmpl: NodeTemplate, // transport of the subtree data; bound via hashv } // consent_msg: child signs hash + deadline + parent pubkey. pub fn consent_msg(hash: &Hash32, deadline: u64, parent: &PubKey) -> Vec { let mut w = Buf::default(); w.bytes(hash); w.u64b(deadline); w.bytes(parent); w.b } #[derive(Clone, Debug)] pub struct Remove { pub parent: PubKey, pub child: PubKey, pub nonce: u64, // parent's nonce pub sig: Sig, } #[derive(Clone, Debug)] pub struct Rekey { pub old_key: PubKey, pub new_key: PubKey, pub nonce: u64, pub sig: Sig, // by old_key } #[derive(Clone, Debug)] pub struct Move { pub child: PubKey, pub new_parent: PubKey, pub nonce: u64, // new parent's nonce pub deadline: u64, pub consent: Sig, // by child over move_consent_msg pub sig: Sig, // by new_parent } // move_consent_msg: child signs deadline + new_parent. pub fn move_consent_msg(deadline: u64, new_parent: &PubKey) -> Vec { let mut w = Buf::default(); w.u64b(deadline); w.bytes(new_parent); w.b } #[derive(Clone, Debug)] pub struct Leave { pub child: PubKey, pub nonce: u64, // child's nonce pub sig: Sig, } #[derive(Clone, Debug)] pub struct VoteClaim { pub key: PubKey, pub nonce: u64, pub sig: Sig, } // VoteOutput mirrors VoteEntry minus the outpoint (assigned at // processing as (txid, index)). #[derive(Clone, Debug, Default)] pub struct VoteOutput { pub committed: bool, pub amount: u64, // uncommitted only; committed is 1 pub owner: PubKey, pub has_owner: bool, // committed only pub mixed: u32, // declared by every output: 1..MAX_MIX pub commit: Hash32, } // Vote mixes and commits existing uncommitted entries in // next_election; token conservation is exact: Σ inputs = Σ outputs. #[derive(Clone, Debug, Default)] pub struct Vote { pub inputs: Vec, pub outputs: Vec, pub sigs: Vec, // one per input, by the entry's owner } // Manual Default impls (Rust <1.79 lacks Default for [u8; 64]). macro_rules! impl_zero_default { ($t:ident { $($f:ident : $v:expr),* $(,)? }) => { impl Default for $t { fn default() -> Self { $t { $($f: $v),* } } } }; } impl_zero_default!(Claim { key: [0; 32], amount: 0, nonce: 0, sig: ZERO_SIG }); impl_zero_default!(Transfer { inputs: Vec::new(), outputs: Vec::new(), sigs: Vec::new() }); impl_zero_default!(Add { parent: [0; 32], child_key: [0; 32], hashv: ZERO32, nonce: 0, deadline: 0, consent: ZERO_SIG, sig: ZERO_SIG, tmpl: NodeTemplate::default() }); impl_zero_default!(Remove { parent: [0; 32], child: [0; 32], nonce: 0, sig: ZERO_SIG }); impl_zero_default!(Rekey { old_key: [0; 32], new_key: [0; 32], nonce: 0, sig: ZERO_SIG }); impl_zero_default!(Move { child: [0; 32], new_parent: [0; 32], nonce: 0, deadline: 0, consent: ZERO_SIG, sig: ZERO_SIG }); impl_zero_default!(Leave { child: [0; 32], nonce: 0, sig: ZERO_SIG }); impl_zero_default!(VoteClaim { key: [0; 32], nonce: 0, sig: ZERO_SIG }); #[derive(Clone, Debug)] pub enum Tx { Claim(Claim), Transfer(Transfer), Prune(Prune), Add(Add), Remove(Remove), Rekey(Rekey), Move(Move), Leave(Leave), Vote(Vote), VoteClaim(VoteClaim), } fn enc_inputs(w: &mut Buf, ins: &[Outpoint]) { w.u32b(ins.len() as u32); for i in ins { w.bytes(&i.tx); w.u32b(i.index); } } impl Tx { pub fn opc(&self) -> u8 { match self { Tx::Claim(_) => OP_CLAIM, Tx::Transfer(_) => OP_TRANSFER, Tx::Prune(_) => OP_PRUNE, Tx::Add(_) => OP_ADD, Tx::Remove(_) => OP_REMOVE, Tx::Rekey(_) => OP_REKEY, Tx::Move(_) => OP_MOVE, Tx::Leave(_) => OP_LEAVE, Tx::Vote(_) => OP_VOTE, Tx::VoteClaim(_) => OP_VOTE_CLAIM, } } // body is the hashed encoding; signatures are excluded. pub fn body(&self) -> Vec { let mut w = Buf::default(); w.u8b(self.opc()); match self { Tx::Claim(t) => { w.bytes(&t.key); w.u128b(t.amount); w.u64b(t.nonce); } Tx::Transfer(t) => { enc_inputs(&mut w, &t.inputs); encode_outputs(&mut w, &t.outputs); } Tx::Prune(t) => enc_inputs(&mut w, &t.inputs), Tx::Add(t) => { w.bytes(&t.parent); w.bytes(&t.child_key); w.bytes(&t.hashv); w.u64b(t.nonce); w.u64b(t.deadline); } Tx::Remove(t) => { w.bytes(&t.parent); w.bytes(&t.child); w.u64b(t.nonce); } Tx::Rekey(t) => { w.bytes(&t.old_key); w.bytes(&t.new_key); w.u64b(t.nonce); } Tx::Move(t) => { w.bytes(&t.child); w.bytes(&t.new_parent); w.u64b(t.nonce); w.u64b(t.deadline); } Tx::Leave(t) => { w.bytes(&t.child); w.u64b(t.nonce); } Tx::Vote(t) => { enc_inputs(&mut w, &t.inputs); w.u32b(t.outputs.len() as u32); for o in &t.outputs { w.boolb(o.committed); if o.committed { w.bytes(&o.commit); w.boolb(o.has_owner); w.bytes(&o.owner); w.u32b(o.mixed); } else { w.u64b(o.amount); w.bytes(&o.owner); w.u32b(o.mixed); } } } Tx::VoteClaim(t) => { w.bytes(&t.key); w.u64b(t.nonce); } } w.b } pub fn id(&self) -> Hash32 { h(&self.body()) } pub fn sig_hash(&self) -> Hash32 { self.id() } } // ============================================================== tree // node_hash is the spec's H(Node): SHA-256(key || leaf || children || // nonce || last_ubi || last_vote || tree_count || tree_ubi). pub fn node_hash(key: &PubKey, child_hashes: &[Hash32], leaf: bool, nonce: u64, last_ubi: u64, last_vote: u64, tree_count: u64, tree_ubi: u128) -> Hash32 { let mut w = Buf::default(); w.bytes(key); w.boolb(leaf); for ch in child_hashes { w.bytes(ch); } w.u64b(nonce); w.u64b(last_ubi); w.u64b(last_vote); w.u64b(tree_count); w.u128b(tree_ubi); h(&w.b) } // PNode is a node in the people tree; own is the node's own person // contribution, own_ubi = normalize(own × TOKEN, last_ubi, norm_time). // The tree is stored in an arena and nodes reference each other by // index — cloning the arena clones the whole tree with all links // intact (the Rust translation of the Go/C++ pointer tree). #[derive(Clone, Debug, Default)] pub struct PNode { pub key: PubKey, pub leaf: bool, pub nonce: u64, pub last_ubi: u64, pub last_vote: u64, pub own: u64, pub own_ubi: u128, pub children: Vec, pub parent: Option, pub tree_count: u64, pub tree_ubi: u128, pub hashv: Hash32, } // RemovedPerson: person entry collected while removing a subtree, // used to auto-mint accrued UBI. #[derive(Clone, Debug)] pub struct RemovedPerson { pub key: PubKey, pub own: u64, pub last_ubi: u64, } pub const MAX_TEMPLATE_NODES: usize = 4096; // PeopleTree is the population register. #[derive(Clone, Debug, Default)] pub struct PeopleTree { arena: Vec>, free: Vec, root: u32, by_key: HashMap, } impl PeopleTree { fn alloc(&mut self, n: PNode) -> u32 { if let Some(i) = self.free.pop() { self.arena[i as usize] = Some(n); i } else { self.arena.push(Some(n)); (self.arena.len() - 1) as u32 } } fn n(&self, i: u32) -> &PNode { self.arena[i as usize].as_ref().expect("stale tree index") } fn nm(&mut self, i: u32) -> &mut PNode { self.arena[i as usize].as_mut().expect("stale tree index") } // make: single root node whose UBI accrual starts at t0. pub fn make(root_key: &PubKey, own: u64, t0: u64, norm: u64) -> R { let mut t = PeopleTree::default(); let mut r = PNode { key: *root_key, own, last_ubi: t0, last_vote: t0, // same entry-stamp rule as Add: first vote next period own_ubi: normalize(tokens(own), t0, norm)?, ..Default::default() }; recompute_node(&mut r, &[]); let i = t.alloc(r); t.root = i; t.by_key.insert(*root_key, i); Ok(t) } pub fn get_idx(&self, k: &PubKey) -> Option { self.by_key.get(k).copied() } pub fn get(&self, k: &PubKey) -> Option<&PNode> { self.get_idx(k).map(|i| self.n(i)) } pub fn node(&self, i: u32) -> &PNode { self.n(i) } pub fn node_mut(&mut self, i: u32) -> &mut PNode { self.nm(i) } pub fn root_hash(&self) -> Hash32 { self.n(self.root).hashv } pub fn population(&self) -> u64 { self.n(self.root).tree_count } fn recompute(&mut self, i: u32) -> R<()> { let kids: Vec<(u64, u128, Hash32)> = self.n(i).children.iter() .map(|&c| { let cn = self.n(c); (cn.tree_count, cn.tree_ubi, cn.hashv) }) .collect(); let node = self.nm(i); let mut tc = node.own; let mut tu = node.own_ubi; let mut ch = Vec::with_capacity(kids.len()); for (ctc, ctu, chh) in kids { tc += ctc; tu = add_checked(tu, ctu, "tree_ubi")?; ch.push(chh); } node.tree_count = tc; node.tree_ubi = tu; node.hashv = node_hash(&node.key, &ch, node.leaf, node.nonce, node.last_ubi, node.last_vote, tc, tu); Ok(()) } // bubble recomputes aggregates and hashes from i up to the root. pub fn bubble(&mut self, mut i: u32) -> R<()> { loop { self.recompute(i)?; match self.n(i).parent { Some(p) => i = p, None => return Ok(()), } } } // unclaimed_at: tree_count × TOKEN − tree_ubi × decay^(T − norm), // clamped at 0. pub fn unclaimed_at(&self, t: u64, norm: u64) -> R { let root = self.n(self.root); let total = tokens(root.tree_count); let v = value_at(root.tree_ubi, t, norm)?; Ok(if total > v { total - v } else { 0 }) } // check_template: key uniqueness (globally and within the // template), leaf consistency, tree_count consistency, size cap. pub fn check_template(&self, tpl: &NodeTemplate) -> R<()> { if tpl.count_nodes() > MAX_TEMPLATE_NODES { bail!("template too large"); } let mut seen: std::collections::HashSet = Default::default(); self.check_tpl_walk(tpl, &mut seen) } fn check_tpl_walk(&self, n: &NodeTemplate, seen: &mut std::collections::HashSet) -> R<()> { if seen.contains(&n.key) { bail!("duplicate key in template: {}", hex(&n.key[..4])); } if self.by_key.contains_key(&n.key) { bail!("key already in tree: {}", hex(&n.key[..4])); } seen.insert(n.key); if n.leaf && !n.children.is_empty() { bail!("leaf node with children"); } let mut sum: u64 = 0; for c in &n.children { self.check_tpl_walk(c, seen)?; sum = sum.checked_add(c.tree_count) .ok_or_else(|| Err(format!("tree_count overflow at {}", hex(&n.key[..4]))))?; } if sum > n.tree_count { bail!("tree_count {} below children sum {} at {}", n.tree_count, sum, hex(&n.key[..4])); } Ok(()) } // do_add validates and attaches a subtree under parent_key; // last_ubi and last_vote are set to block_time for every node (per // spec) and tree_ubi is recomputed. The caller increments the // parent's nonce BEFORE calling (a single bubble covers all). pub fn do_add(&mut self, parent_key: &PubKey, tpl: &NodeTemplate, block_time: u64, norm: u64) -> R<()> { let p = self.get_idx(parent_key).ok_or_else(|| Err("parent not in tree".into()))?; if self.n(p).leaf { bail!("parent is a leaf"); } self.check_template(tpl)?; let child = self.build_tpl(tpl, Some(p), block_time, norm)?; self.nm(p).children.push(child); self.bubble(p) } fn build_tpl(&mut self, tp: &NodeTemplate, parent: Option, block_time: u64, norm: u64) -> R { let sum: u64 = tp.children.iter().map(|c| c.tree_count).sum(); let own = tp.tree_count - sum; // ≥ 0, ensured by check_template let n = PNode { key: tp.key, leaf: tp.leaf, nonce: tp.nonce, own, last_ubi: block_time, last_vote: block_time, own_ubi: normalize(tokens(own), block_time, norm)?, parent, ..Default::default() }; let i = self.alloc(n); for c in &tp.children { let ci = self.build_tpl(c, Some(i), block_time, norm)?; self.nm(i).children.push(ci); } self.recompute(i)?; self.by_key.insert(tp.key, i); Ok(i) } // do_remove detaches child_key's subtree from parent_key and // returns all persons (own > 0) in pre-order. The caller // increments the parent's nonce before calling. pub fn do_remove(&mut self, parent_key: &PubKey, child_key: &PubKey) -> R> { let p = self.get_idx(parent_key).ok_or_else(|| Err("parent not in tree".into()))?; let c = self.get_idx(child_key).ok_or_else(|| Err("child not in tree".into()))?; if self.n(c).parent != Some(p) { bail!("not a child of parent"); } let mut persons = Vec::new(); self.collect_free(c, &mut persons); let pc = &mut self.nm(p).children; pc.retain(|&x| x != c); self.bubble(p)?; Ok(persons) } fn collect_free(&mut self, i: u32, persons: &mut Vec) { let (key, own, last_ubi, kids) = { let n = self.n(i); (n.key, n.own, n.last_ubi, n.children.clone()) }; if own > 0 { persons.push(RemovedPerson { key, own, last_ubi }); } self.by_key.remove(&key); for c in kids { self.collect_free(c, persons); } self.arena[i as usize] = None; self.free.push(i); } // do_rekey changes a node's key; the node's nonce is incremented // here (the caller verifies the signature). pub fn do_rekey(&mut self, old_k: &PubKey, new_k: &PubKey) -> R<()> { let i = self.get_idx(old_k).ok_or_else(|| Err("node not in tree".into()))?; if self.by_key.contains_key(new_k) { bail!("new key already in tree"); } self.by_key.remove(old_k); { let n = self.nm(i); n.key = *new_k; n.nonce += 1; } self.by_key.insert(*new_k, i); self.bubble(i) } // do_move detaches child_key from its current parent and attaches // it under new_parent_key; all state is preserved. pub fn do_move(&mut self, child_key: &PubKey, new_parent_key: &PubKey) -> R<()> { let c = self.get_idx(child_key).ok_or_else(|| Err("child not in tree".into()))?; let old = self.n(c).parent.ok_or_else(|| Err("cannot move the root".into()))?; let np = self.get_idx(new_parent_key).ok_or_else(|| Err("new parent not in tree".into()))?; if self.n(np).leaf { bail!("new parent is a leaf"); } let mut walk = Some(np); while let Some(p) = walk { if p == c { bail!("new parent is inside child's subtree"); } walk = self.n(p).parent; } self.nm(old).children.retain(|&x| x != c); self.nm(c).parent = Some(np); self.nm(np).children.push(c); self.bubble(old)?; self.bubble(np) } } fn recompute_node(n: &mut PNode, kids: &[(u64, u128, Hash32)]) { let mut tc = n.own; let mut tu = n.own_ubi; let mut ch = Vec::with_capacity(kids.len()); for &(ctc, ctu, chh) in kids { tc += ctc; tu += ctu; ch.push(chh); } n.tree_count = tc; n.tree_ubi = tu; n.hashv = node_hash(&n.key, &ch, n.leaf, n.nonce, n.last_ubi, n.last_vote, tc, tu); } // ============================================================== 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. #[derive(Clone, Debug)] pub struct Entry { pub op: Outpoint, pub amount: u128, pub norm: u128, pub time: u64, // creation (block) time pub owner: PubKey, } impl Entry { // value: gross value at T = amount × decay^(T − time) / SCALE. pub fn value(&self, t: u64) -> R { if t < self.time { bail!("Entry.Value: time before creation"); } Ok(mul_scale(self.amount, decay_pow(t - self.time))) } // rent: accumulated rent owed at T. pub fn rent(&self, t: u64) -> u128 { rent_owed(t.saturating_sub(self.time)) } // spendable may be ≤ 0 — that is expiry; callers compare the two // sides (value vs rent) since amounts here are unsigned. pub fn expired(&self, t: u64) -> R { Ok(self.value(t)? <= self.rent(t)) } } pub fn op_key(o: &Outpoint) -> Hash32 { let mut w = Buf::default(); w.bytes(&o.tx); w.u32b(o.index); h(&w.b) } fn bit_at(k: &Hash32, d: usize) -> u8 { (k[d >> 3] >> (7 - (d & 7))) & 1 } // 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. Leaves cache their hash/sum (entries are // immutable once inserted), so the trie itself only stores keys. #[derive(Clone, Debug)] enum TNode { Leaf { key: Hash32, h: Hash32, sum: u128 }, Node { l: Option>, r: Option>, h: Hash32, sum: u128 }, } impl TNode { fn sum(&self) -> u128 { match self { TNode::Leaf { sum, .. } | TNode::Node { sum, .. } => *sum, } } fn hash(&self) -> Hash32 { match self { TNode::Leaf { h, .. } | TNode::Node { h, .. } => *h, } } fn key(&self) -> &Hash32 { match self { TNode::Leaf { key, .. } => key, _ => unreachable!(), } } fn is_leaf(&self) -> bool { matches!(self, TNode::Leaf { .. }) } } fn t_leaf(key: Hash32, e: &Entry) -> Box { let mut w = Buf::default(); w.u8b(0x00); w.bytes(&key); w.u128b(e.amount); w.u64b(e.time); w.bytes(&e.owner); Box::new(TNode::Leaf { key, h: h(&w.b), sum: e.norm }) } fn t_fix(l: &Option>, r: &Option>) -> R<(Hash32, u128)> { let mut sum: u128 = 0; let mut lh = ZERO32; let mut rh = ZERO32; if let Some(n) = l { sum = add_checked(sum, n.sum(), "utxo sum")?; lh = n.hash(); } if let Some(n) = r { sum = add_checked(sum, n.sum(), "utxo sum")?; rh = n.hash(); } let mut w = Buf::default(); w.u8b(0x01); w.bytes(&lh); w.bytes(&rh); w.u128b(sum); Ok((h(&w.b), sum)) } fn t_mk_node(l: Option>, r: Option>) -> R> { let (hv, sum) = t_fix(&l, &r)?; Ok(Box::new(TNode::Node { l, r, h: hv, sum })) } // split_leaves builds the internal chain from depth d down to the // first bit where the two keys diverge. fn t_split(a: Box, b: Box, d: usize) -> R> { let (ba, bb) = (bit_at(a.key(), d), bit_at(b.key(), d)); if ba == bb { let c = t_split(a, b, d + 1)?; if ba == 0 { t_mk_node(Some(c), None) } else { t_mk_node(None, Some(c)) } } else if ba == 0 { t_mk_node(Some(a), Some(b)) } else { t_mk_node(Some(b), Some(a)) } } fn t_insert(n: Option>, d: usize, lf: Box) -> R> { let Some(n) = n else { return Ok(lf) }; if n.is_leaf() { if n.key() == lf.key() { bail!("duplicate utxo key"); } return t_split(n, lf, d); } let TNode::Node { l, r, .. } = *n else { unreachable!() }; if bit_at(lf.key(), d) == 0 { t_mk_node(Some(t_insert(l, d + 1, lf)?), r) } else { t_mk_node(l, Some(t_insert(r, d + 1, lf)?)) } } // t_delete removes key and collapses now-redundant internals. // Returns (new subtree root, whether the leaf was found). fn t_delete(n: Option>, d: usize, key: &Hash32) -> R<(Option>, bool)> { let Some(n) = n else { return Ok((None, false)) }; if n.is_leaf() { if n.key() == key { return Ok((None, true)); } return Ok((Some(n), false)); } let TNode::Node { l, r, .. } = *n else { unreachable!() }; let (l, r, rem) = if bit_at(key, d) == 0 { let (nl, rem) = t_delete(l, d + 1, key)?; (nl, r, rem) } else { let (nr, rem) = t_delete(r, d + 1, key)?; (l, nr, rem) }; if !rem { return Ok((Some(t_mk_node(l, r)?), false)); } match (l, r) { (None, None) => Ok((None, true)), (None, Some(x)) if x.is_leaf() => Ok((Some(x), true)), (Some(x), None) if x.is_leaf() => Ok((Some(x), true)), (l, r) => Ok((Some(t_mk_node(l, r)?), true)), } } // UTXOSet combines the Merkle sum trie (commitment) with a direct map // (O(1) validation lookups). Both are kept in sync. #[derive(Clone, Debug, Default)] pub struct UtxoSet { root: Option>, entries: HashMap, } impl UtxoSet { pub fn insert(&mut self, o: Outpoint, amount: u128, norm: u128, time: u64, owner: PubKey) -> R<()> { if self.entries.contains_key(&o) { bail!("outpoint already exists"); } // (amount/norm range checks are enforced by the u128 type and // by normalize; mirrors the Go BitLen checks.) let e = Entry { op: o, amount, norm, time, owner }; let lf = t_leaf(op_key(&o), &e); self.root = Some(t_insert(self.root.take(), 0, lf)?); self.entries.insert(o, e); Ok(()) } pub fn get(&self, o: &Outpoint) -> Option<&Entry> { self.entries.get(o) } // spend removes the entry from trie + map. pub fn spend(&mut self, o: &Outpoint) -> R<()> { if !self.entries.contains_key(o) { bail!("output missing or already spent"); } let (nr, rem) = t_delete(self.root.take(), 0, &op_key(o))?; if !rem { bail!("trie desync"); // cannot happen } self.root = nr; self.entries.remove(o); Ok(()) } pub fn sum(&self) -> u128 { self.root.as_ref().map_or(0, |n| n.sum()) } pub fn root_hash(&self) -> Hash32 { self.root.as_ref().map_or(ZERO32, |n| n.hash()) } pub fn len(&self) -> usize { self.entries.len() } pub fn iter(&self) -> impl Iterator { self.entries.values() } } // ========================================================== 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. #[derive(Clone, Debug)] pub struct VoteEntry { pub op: Outpoint, pub committed: bool, pub amount: u64, // uncommitted only (committed is implicitly 1) pub owner: PubKey, // uncommitted: required; committed: fee-share hint pub has_owner: bool, // committed only pub mixed: u32, // uncommitted entries only: 1..MAX_MIX pub commit: Hash32, } impl Default for VoteEntry { fn default() -> Self { VoteEntry { op: Outpoint::default(), committed: false, amount: 0, owner: [0; 32], has_owner: false, mixed: 0, commit: ZERO32 } } } // VNode mirrors TNode with a committed-entry count as the aggregate; // leaves additionally remember their outpoint so positional selection // can hand back the entry. #[derive(Clone, Debug)] enum VNode { Leaf { key: Hash32, op: Outpoint, h: Hash32, count: u64 }, Node { l: Option>, r: Option>, h: Hash32, count: u64 }, } impl VNode { fn count(&self) -> u64 { match self { VNode::Leaf { count, .. } | VNode::Node { count, .. } => *count, } } fn hash(&self) -> Hash32 { match self { VNode::Leaf { h, .. } | VNode::Node { h, .. } => *h, } } fn key(&self) -> &Hash32 { match self { VNode::Leaf { key, .. } => key, _ => unreachable!(), } } fn is_leaf(&self) -> bool { matches!(self, VNode::Leaf { .. }) } } fn v_leaf(key: Hash32, e: &VoteEntry) -> Box { let mut w = Buf::default(); let count; if e.committed { count = 1; w.u8b(0x02); w.bytes(&key); w.bytes(&e.commit); w.boolb(e.has_owner); w.bytes(&e.owner); } else { count = 0; w.u8b(0x03); w.bytes(&key); w.u64b(e.amount); w.bytes(&e.owner); w.u32b(e.mixed); } Box::new(VNode::Leaf { key, op: e.op, h: h(&w.b), count }) } fn v_mk_node(l: Option>, r: Option>) -> Box { let mut count = 0; let mut lh = ZERO32; let mut rh = ZERO32; if let Some(n) = &l { count += n.count(); lh = n.hash(); } if let Some(n) = &r { count += n.count(); rh = n.hash(); } let mut w = Buf::default(); w.u8b(0x04); w.bytes(&lh); w.bytes(&rh); w.u64b(count); Box::new(VNode::Node { l, r, h: h(&w.b), count }) } fn v_split(a: Box, b: Box, d: usize) -> Box { let (ba, bb) = (bit_at(a.key(), d), bit_at(b.key(), d)); if ba == bb { let c = v_split(a, b, d + 1); if ba == 0 { v_mk_node(Some(c), None) } else { v_mk_node(None, Some(c)) } } else if ba == 0 { v_mk_node(Some(a), Some(b)) } else { v_mk_node(Some(b), Some(a)) } } fn v_insert(n: Option>, d: usize, lf: Box) -> R> { let Some(n) = n else { return Ok(lf) }; if n.is_leaf() { if n.key() == lf.key() { bail!("duplicate vote entry"); } return Ok(v_split(n, lf, d)); } let VNode::Node { l, r, .. } = *n else { unreachable!() }; if bit_at(lf.key(), d) == 0 { Ok(v_mk_node(Some(v_insert(l, d + 1, lf)?), r)) } else { Ok(v_mk_node(l, Some(v_insert(r, d + 1, lf)?))) } } fn v_delete(n: Option>, d: usize, key: &Hash32) -> (Option>, bool) { let Some(n) = n else { return (None, false) }; if n.is_leaf() { if n.key() == key { return (None, true); } return (Some(n), false); } let VNode::Node { l, r, .. } = *n else { unreachable!() }; let (l, r, rem) = if bit_at(key, d) == 0 { let (nl, rem) = v_delete(l, d + 1, key); (nl, r, rem) } else { let (nr, rem) = v_delete(r, d + 1, key); (l, nr, rem) }; if !rem { return (Some(v_mk_node(l, r)), false); } match (l, r) { (None, None) => (None, true), (None, Some(x)) if x.is_leaf() => (Some(x), true), (Some(x), None) if x.is_leaf() => (Some(x), true), (l, r) => (Some(v_mk_node(l, r)), true), } } // VoteTrie: Merkle count trie + direct map, kept in sync. #[derive(Clone, Debug, Default)] pub struct VoteTrie { root: Option>, entries: HashMap, } impl VoteTrie { pub fn insert(&mut self, ent: VoteEntry) -> R<()> { if self.entries.contains_key(&ent.op) { bail!("duplicate vote entry"); } let lf = v_leaf(op_key(&ent.op), &ent); self.root = Some(v_insert(self.root.take(), 0, lf)?); self.entries.insert(ent.op, ent); Ok(()) } pub fn get(&self, o: &Outpoint) -> Option<&VoteEntry> { self.entries.get(o) } // spend removes and returns the entry. pub fn spend(&mut self, o: &Outpoint) -> R { if !self.entries.contains_key(o) { bail!("vote entry missing"); } let (nr, rem) = v_delete(self.root.take(), 0, &op_key(o)); if !rem { bail!("vote trie desync"); // cannot happen } self.root = nr; Ok(self.entries.remove(o).unwrap()) } // update_commit replaces a committed entry's commit with the // revealed layer (delete + reinsert; count unchanged). pub fn update_commit(&mut self, o: &Outpoint, commit: &Hash32) -> R<()> { let mut e = self.spend(o)?; e.commit = *commit; self.insert(e) } pub fn committed_count(&self) -> u64 { self.root.as_ref().map_or(0, |n| n.count()) } pub fn len(&self) -> usize { self.entries.len() } pub fn root_hash(&self) -> Hash32 { self.root.as_ref().map_or(ZERO32, |n| n.hash()) } // select walks the cumulative counts to the committed entry at // position pos (0-based). pub fn select(&self, mut pos: u64) -> Option { let mut n = self.root.as_deref(); while let Some(VNode::Node { l, r, .. }) = n { if let Some(ln) = l { if pos < ln.count() { n = Some(ln); continue; } pos -= ln.count(); } n = r.as_deref(); } match n { Some(VNode::Leaf { op, .. }) => { let e = self.entries.get(op)?; if e.committed { Some(e.clone()) } else { None } } _ => None, } } // select_rand is the spec's `rand mod total_committed`. pub fn select_rand(&self, r: &Hash32) -> Option { let n = self.committed_count(); if n == 0 { return None; } let mut pos: u128 = 0; for &b in r { pos = ((pos << 8) | b as u128) % n as u128; } self.select(pos as u64) } pub fn iter(&self) -> impl Iterator { self.entries.values() } } // ========================================================== election // slot_of/slot_time index the absolute Unix-0-anchored 60 s grid; // header seq counts slots since genesis instead (gaps where skipped). pub fn slot_of(t: u64) -> u64 { t / SLOT_SECONDS } pub fn slot_time(s: u64) -> u64 { s * SLOT_SECONDS } // period_start is the start of the election period containing t. pub fn period_start(t: u64) -> u64 { t - t % PERIOD_SECONDS } // phase_open: the first half of the period; at the midpoint // next_election locks. pub fn phase_open(t: u64) -> bool { t % PERIOD_SECONDS < PERIOD_SECONDS / 2 } // hash_u64 is the spec's H(seq) for skipped-slot rand mixing. pub fn hash_u64(x: u64) -> Hash32 { h(&x.to_be_bytes()) } pub fn xor32(a: &Hash32, b: &Hash32) -> Hash32 { let mut o = [0u8; 32]; for i in 0..32 { o[i] = a[i] ^ b[i]; } o } // sel_rand accumulates the selection rand for a block at seq `to`, // folding in H(seq) of every skipped slot strictly between. pub fn sel_rand(mut r: Hash32, prev_seq: u64, to: u64) -> Hash32 { let mut s = prev_seq + 1; while s < to { r = xor32(&r, &hash_u64(s)); s += 1; } r } // --------------------------------------------------------- hash onion // onion_commit: o_0 = seed, o_i = H(candidate || o_{i-1}), // commit = o_depth. pub fn onion_commit(candidate: &PubKey, seed: &Hash32, depth: u64) -> Hash32 { let mut o = *seed; for _ in 0..depth { o = h2(candidate, &o); } o } // Onion is the producer side: it can find the layer below any commit // on its chain. Checkpoints every ONION_STRIDE layers bound the work // per reveal. pub const ONION_STRIDE: u64 = 4096; pub struct Onion { pub candidate: PubKey, pub seed: Hash32, pub depth: u64, cps: Vec, // cps[j] = layer at position j*ONION_STRIDE pos: u64, pos_valid: bool, } impl Onion { pub fn new(candidate: PubKey, seed: Hash32, depth: u64) -> Onion { let mut cps = vec![seed]; let mut l = seed; for i in 1..=depth { l = h2(&candidate, &l); if i % ONION_STRIDE == 0 { cps.push(l); } } Onion { candidate, seed, depth, cps, pos: 0, pos_valid: false } } fn layer_at(&self, i: u64) -> Hash32 { let j = i / ONION_STRIDE; let mut l = self.cps[j as usize]; for _ in j * ONION_STRIDE..i { l = h2(&self.candidate, &l); } l } pub fn commit(&self) -> Hash32 { self.layer_at(self.depth) } // reveal returns the layer directly below `current`, or None if // `current` is not on this onion or the onion is exhausted. pub fn reveal(&mut self, current: &Hash32) -> Option { if !self.pos_valid || self.layer_at(self.pos) != *current { self.pos_valid = false; let mut l = self.seed; let mut i = 0u64; loop { if l == *current { self.pos = i; self.pos_valid = true; break; } if i == self.depth { break; } l = h2(&self.candidate, &l); i += 1; } if !self.pos_valid { return None; } } if self.pos == 0 { return None; // exhausted } let r = self.layer_at(self.pos - 1); self.pos -= 1; // the commit becomes r once the block applies Some(r) } } // ============================================================= state pub const MAX_OUTPUTS: usize = 1024; pub const MAX_INPUTS: usize = 1024; // fee_outpoint identifies the synthetic per-block fee output. pub fn fee_outpoint(seq: u64) -> Outpoint { let mut w = Buf::default(); w.bytes(b"fee"); w.u64b(seq); Outpoint { tx: h(&w.b), index: 0 } } // genesis_vote_outpoint identifies the committed votes genesis seeds // the election tries with (index 0 → election_trie, 1 → next). pub fn genesis_vote_outpoint(index: u32) -> Outpoint { Outpoint { tx: h(b"genesis vote"), index } } // State is the full chain state between blocks. #[derive(Clone, Debug, Default)] pub struct State { pub norm_time: u64, // reference time for normalized values pub time: u64, // time of the last applied block pub seq: u64, // slots since genesis of the last applied block pub genesis: u64, // genesis block time (on the Unix 60 s grid) pub last_hash: Hash32, pub rnd: Hash32, // last header's rand; seeds the next selection pub tree: PeopleTree, pub utxo: UtxoSet, pub election: VoteTrie, // active: selects one validator per slot pub next_election: VoteTrie, // being built; activates at the boundary } impl State { pub fn seq_at(&self, t: u64) -> u64 { (t - self.genesis) / SLOT_SECONDS } // ------------------------------------------------------- helpers // check_outputs: positive amounts, sane count; returns the sum. fn check_outputs(outs: &[Output], allow_empty: bool) -> R { if outs.len() > MAX_OUTPUTS { bail!("too many outputs"); } if outs.is_empty() && !allow_empty { bail!("no outputs"); } let mut sum: u128 = 0; for o in outs { if o.amount == 0 { bail!("invalid output amount"); } sum = sum.checked_add(o.amount).ok_or_else(|| Err("output sum overflow".into()))?; } Ok(sum) } // mint_outputs inserts outs as (txid, i) at block time T. fn mint_outputs(&mut self, txid: &Hash32, outs: &[Output], t: u64) -> R<()> { for (i, o) in outs.iter().enumerate() { let norm = normalize(o.amount, t, self.norm_time)?; self.utxo.insert(Outpoint { tx: *txid, index: i as u32 }, o.amount, norm, t, o.owner)?; } Ok(()) } // --------------------------------------------------------- Claim // apply_claim returns the fee: claimable(T) minus the claimed // amount; last_ubi advances to T regardless. fn apply_claim(&mut self, tx: &Tx, c: &Claim, t: u64) -> R { let i = self.tree.get_idx(&c.key).ok_or_else(|| Err("claim: key not in tree".into()))?; let (own, last_ubi, nonce) = { let n = self.tree.node(i); (n.own, n.last_ubi, n.nonce) }; if own == 0 { bail!("claim: node has no person contribution"); } if c.nonce != nonce { bail!("claim: bad nonce (have {} want {})", c.nonce, nonce); } if !verify_sig(&c.key, &tx.sig_hash(), &c.sig) { bail!("claim: bad signature"); } let want = claimable_at(own, last_ubi, t); if want == 0 { bail!("claim: nothing claimable"); } if c.amount == 0 { bail!("claim: bad amount"); } if c.amount > want { bail!("claim: amount {} exceeds claimable {}", c.amount, want); } self.mint_outputs(&tx.id(), &[Output { amount: c.amount, owner: c.key }], t)?; let own_ubi = normalize(tokens(own), t, self.norm_time)?; { let n = self.tree.node_mut(i); n.last_ubi = t; n.own_ubi = own_ubi; n.nonce += 1; } self.tree.bubble(i)?; Ok(want - c.amount) } // ------------------------------------------------------ Transfer // apply_transfer returns everything the validator collects: // Σ input gross value − Σ outputs. Validation per spec: // Σ outputs ≤ Σ input spendable at block time. fn apply_transfer(&mut self, tx: &Tx, t: &Transfer, tt: u64) -> R { if t.inputs.is_empty() || t.inputs.len() > MAX_INPUTS { bail!("transfer: bad input count"); } if t.sigs.len() != t.inputs.len() { bail!("transfer: need one signature per input"); } let mut seen = std::collections::HashSet::new(); let mut ents = Vec::with_capacity(t.inputs.len()); for op in &t.inputs { if !seen.insert(*op) { bail!("transfer: duplicate input"); } let e = self.utxo.get(op).ok_or_else(|| Err("transfer: input missing or spent".into()))?; ents.push(e.clone()); } let sh = tx.sig_hash(); for (i, e) in ents.iter().enumerate() { if !verify_sig(&e.owner, &sh, &t.sigs[i]) { bail!("transfer: bad signature for input {}", i); } } let out_sum = Self::check_outputs(&t.outputs, true)?; let mut in_value: u128 = 0; // Σ gross values let mut in_rent: u128 = 0; // Σ rent_owed (spendable = value − rent, signed) for e in &ents { in_value = add_checked(in_value, e.value(tt)?, "transfer input")?; in_rent = add_checked(in_rent, e.rent(tt), "transfer rent")?; } // outputs ≤ Σ spendable ⇔ out_sum + ΣRent ≤ ΣValue. let ok = in_value >= in_rent && out_sum <= in_value - in_rent; if !ok { bail!("transfer: outputs {} exceed spendable {}", out_sum, spend_str(in_value, in_rent)); } for op in &t.inputs { self.utxo.spend(op)?; } self.mint_outputs(&tx.id(), &t.outputs, tt)?; // fee + collected rent = gross − outputs (≥ 0 by the check). Ok(in_value - out_sum) } // --------------------------------------------------------- Prune // apply_prune removes UTXOs with spendable(T) ≤ 0 and returns // their remaining gross value. fn apply_prune(&mut self, p: &Prune, t: u64) -> R { if p.inputs.is_empty() || p.inputs.len() > MAX_INPUTS { bail!("prune: bad input count"); } let mut seen = std::collections::HashSet::new(); let mut collected: u128 = 0; for op in &p.inputs { if !seen.insert(*op) { bail!("prune: duplicate input"); } let e = self.utxo.get(op).ok_or_else(|| Err("prune: output missing or spent".into()))?; if !e.expired(t)? { bail!("prune: output not expired (spendable {})", spend_str(e.value(t)?, e.rent(t))); } collected = add_checked(collected, e.value(t)?, "prune")?; } for op in &p.inputs { self.utxo.spend(op)?; } Ok(collected) } // ----------------------------------------------------------- Add fn apply_add(&mut self, tx: &Tx, a: &Add, t: u64) -> R<()> { let p = self.tree.get_idx(&a.parent).ok_or_else(|| Err("add: parent not in tree".into()))?; let p_nonce = self.tree.node(p).nonce; if a.nonce != p_nonce { bail!("add: bad nonce (have {} want {})", a.nonce, p_nonce); } if t > a.deadline { bail!("add: consent expired"); } if !verify_sig(&a.parent, &tx.sig_hash(), &a.sig) { bail!("add: bad parent signature"); } if a.tmpl.key != a.child_key { bail!("add: template root key mismatch"); } if a.tmpl.hash() != a.hashv { bail!("add: template hash mismatch"); } if !verify_sig(&a.child_key, &consent_msg(&a.hashv, a.deadline, &a.parent), &a.consent) { bail!("add: bad child consent"); } self.tree.node_mut(p).nonce += 1; self.tree.do_add(&a.parent, &a.tmpl, t, self.norm_time) .map_err(|e| Err(format!("add: {}", e))) } // -------------------------------------------------------- Remove fn mint_removed(&mut self, txid: &Hash32, persons: &[RemovedPerson], t: u64) -> R<()> { // Auto-mint accrued UBI to each removed person, in pre-order; // zero-claimable persons are skipped. let mut idx: u32 = 0; for pr in persons { let amt = claimable_at(pr.own, pr.last_ubi, t); if amt == 0 { continue; } let norm = normalize(amt, t, self.norm_time)?; self.utxo.insert(Outpoint { tx: *txid, index: idx }, amt, norm, t, pr.key)?; idx += 1; } Ok(()) } fn apply_remove(&mut self, tx: &Tx, r: &Remove, t: u64) -> R<()> { let p = self.tree.get_idx(&r.parent).ok_or_else(|| Err("remove: parent not in tree".into()))?; let p_nonce = self.tree.node(p).nonce; if r.nonce != p_nonce { bail!("remove: bad nonce (have {} want {})", r.nonce, p_nonce); } if !verify_sig(&r.parent, &tx.sig_hash(), &r.sig) { bail!("remove: bad signature"); } self.tree.node_mut(p).nonce += 1; let persons = self.tree.do_remove(&r.parent, &r.child) .map_err(|e| Err(format!("remove: {}", e)))?; self.mint_removed(&tx.id(), &persons, t) } // --------------------------------------------------------- Rekey fn apply_rekey(&mut self, tx: &Tx, r: &Rekey) -> R<()> { let i = self.tree.get_idx(&r.old_key).ok_or_else(|| Err("rekey: node not in tree".into()))?; let nonce = self.tree.node(i).nonce; if r.nonce != nonce { bail!("rekey: bad nonce (have {} want {})", r.nonce, nonce); } if !verify_sig(&r.old_key, &tx.sig_hash(), &r.sig) { bail!("rekey: bad signature"); } self.tree.do_rekey(&r.old_key, &r.new_key) } // ---------------------------------------------------------- Move fn apply_move(&mut self, tx: &Tx, m: &Move, t: u64) -> R<()> { let c = self.tree.get_idx(&m.child).ok_or_else(|| Err("move: child not in tree".into()))?; if self.tree.node(c).parent.is_none() { bail!("move: cannot move the root"); } let np = self.tree.get_idx(&m.new_parent).ok_or_else(|| Err("move: new parent not in tree".into()))?; let np_nonce = self.tree.node(np).nonce; if m.nonce != np_nonce { bail!("move: bad nonce (have {} want {})", m.nonce, np_nonce); } if t > m.deadline { bail!("move: consent expired"); } if !verify_sig(&m.new_parent, &tx.sig_hash(), &m.sig) { bail!("move: bad new parent signature"); } if !verify_sig(&m.child, &move_consent_msg(m.deadline, &m.new_parent), &m.consent) { bail!("move: bad child consent"); } self.tree.node_mut(np).nonce += 1; self.tree.do_move(&m.child, &m.new_parent) } // --------------------------------------------------------- Leave fn apply_leave(&mut self, tx: &Tx, l: &Leave, t: u64) -> R<()> { let c = self.tree.get_idx(&l.child).ok_or_else(|| Err("leave: child not in tree".into()))?; let (nonce, parent) = { let n = self.tree.node(c); (n.nonce, n.parent) }; let parent = parent.ok_or_else(|| Err("leave: root cannot leave".into()))?; if l.nonce != nonce { bail!("leave: bad nonce (have {} want {})", l.nonce, nonce); } if !verify_sig(&l.child, &tx.sig_hash(), &l.sig) { bail!("leave: bad signature"); } let parent_key = self.tree.node(parent).key; let persons = self.tree.do_remove(&parent_key, &l.child) .map_err(|e| Err(format!("leave: {}", e)))?; self.mint_removed(&tx.id(), &persons, t) } // ----------------------------------------------------- VoteClaim fn apply_vote_claim(&mut self, tx: &Tx, c: &VoteClaim, t: u64) -> R<()> { if !phase_open(t) { bail!("vote claim: next_election is locked (second half of period)"); } let i = self.tree.get_idx(&c.key).ok_or_else(|| Err("vote claim: key not in tree".into()))?; let (own, nonce, last_vote) = { let n = self.tree.node(i); (n.own, n.nonce, n.last_vote) }; if own == 0 { bail!("vote claim: node has no person contribution"); } if c.nonce != nonce { bail!("vote claim: bad nonce (have {} want {})", c.nonce, nonce); } if last_vote >= period_start(t) { bail!("vote claim: already claimed this period"); } if !verify_sig(&c.key, &tx.sig_hash(), &c.sig) { bail!("vote claim: bad signature"); } self.next_election.insert(VoteEntry { op: Outpoint { tx: tx.id(), index: 0 }, amount: own, owner: c.key, mixed: 0, ..Default::default() })?; { let n = self.tree.node_mut(i); n.last_vote = t; n.nonce += 1; } self.tree.bubble(i) } // ---------------------------------------------------------- Vote fn apply_vote(&mut self, tx: &Tx, v: &Vote, t: u64) -> R<()> { if !phase_open(t) { bail!("vote: next_election is locked (second half of period)"); } if v.inputs.is_empty() || v.inputs.len() > MAX_INPUTS { bail!("vote: bad input count"); } if v.outputs.is_empty() || v.outputs.len() > MAX_OUTPUTS { bail!("vote: bad output count"); } if v.sigs.len() != v.inputs.len() { bail!("vote: need one signature per input"); } let sh = tx.sig_hash(); // Inputs: uncommitted, in next_election, signed by owner. let mut in_sum: u128 = 0; let mut in_mix: u64 = 0; let mut seen = std::collections::HashSet::new(); for (i, op) in v.inputs.iter().enumerate() { if !seen.insert(*op) { bail!("vote: duplicate input"); } let e = self.next_election.get(op).ok_or_else(|| Err("vote: input missing or spent".into()))?; if e.committed { bail!("vote: input is committed (locked)"); } if !verify_sig(&e.owner, &sh, &v.sigs[i]) { bail!("vote: bad signature for input {}", i); } in_sum += e.amount as u128; in_mix += e.mixed as u64; } let mut out_sum: u128 = 0; let mut out_mix: u64 = 0; for o in &v.outputs { if o.mixed < 1 || o.mixed > MAX_MIX { bail!("vote: output mixed {} outside 1..{}", o.mixed, MAX_MIX); } out_mix += o.mixed as u64; if o.committed { out_sum += 1; // committed amount is 1 } else { if o.amount == 0 { bail!("vote: zero-amount output"); } out_sum += o.amount as u128; } } if in_sum != out_sum { bail!("vote: inputs {} != outputs {}", in_sum, out_sum); } // Spec: sum(output mixed) ≥ sum(input mixed) + count(outputs). if out_mix < in_mix + v.outputs.len() as u64 { bail!("vote: mixed budget {} < {} required", out_mix, in_mix + v.outputs.len() as u64); } // Apply. for op in &v.inputs { self.next_election.spend(op)?; } let txid = tx.id(); for (i, o) in v.outputs.iter().enumerate() { let mut e = VoteEntry { op: Outpoint { tx: txid, index: i as u32 }, committed: o.committed, owner: o.owner, ..Default::default() }; if o.committed { e.amount = 1; e.has_owner = o.has_owner; e.commit = o.commit; } else { e.amount = o.amount; e.mixed = o.mixed; } self.next_election.insert(e)?; } Ok(()) } // apply_txs applies transactions in order and returns total fees. pub fn apply_txs(&mut self, txs: &[Tx], t: u64) -> R { let mut fees: u128 = 0; for (i, tx) in txs.iter().enumerate() { let r: R = match tx { Tx::Claim(c) => self.apply_claim(tx, c, t), Tx::Transfer(tr) => self.apply_transfer(tx, tr, t), Tx::Prune(p) => self.apply_prune(p, t), Tx::Add(a) => self.apply_add(tx, a, t).map(|_| 0), Tx::Remove(rm) => self.apply_remove(tx, rm, t).map(|_| 0), Tx::Rekey(rk) => self.apply_rekey(tx, rk).map(|_| 0), Tx::Move(m) => self.apply_move(tx, m, t).map(|_| 0), Tx::Leave(l) => self.apply_leave(tx, l, t).map(|_| 0), Tx::Vote(v) => self.apply_vote(tx, v, t).map(|_| 0), Tx::VoteClaim(vc) => self.apply_vote_claim(tx, vc, t).map(|_| 0), }; match r { Ok(f) => fees = add_checked(fees, f, "fees")?, Result::Err(e) => bail!("tx {}: {}", i, e), } } Ok(fees) } // supply_at: real UTXO supply at T from the trie root. pub fn supply_at(&self, t: u64) -> R { value_at(self.utxo.sum(), t, self.norm_time) } // unclaimed_at: unclaimed UBI at T from the tree root. pub fn unclaimed_at(&self, t: u64) -> R { self.tree.unclaimed_at(t, self.norm_time) } } // ============================================================= block use std::sync::Arc; // Header per spec; seq starts at 0 at genesis and increments by 1 per // slot — skipped slots leave gaps; time = genesis_time + seq × slot. #[derive(Clone, Debug)] pub struct Header { pub seq: u64, pub time: u64, pub people_tree: Hash32, pub utxo_trie: Hash32, pub election_trie: Hash32, pub next_election: Hash32, pub prev: Hash32, pub validator: PubKey, pub rnd: Hash32, // selection rand XOR revealed onion layer pub sig: Sig, } impl Default for Header { fn default() -> Self { Header { seq: 0, time: 0, people_tree: ZERO32, utxo_trie: ZERO32, election_trie: ZERO32, next_election: ZERO32, prev: ZERO32, validator: [0; 32], rnd: ZERO32, sig: ZERO_SIG } } } impl Header { pub fn encode(&self, with_sig: bool) -> Vec { let mut w = Buf::default(); w.u8b(OP_HEADER); w.u64b(self.seq); w.u64b(self.time); w.bytes(&self.people_tree); w.bytes(&self.utxo_trie); w.bytes(&self.election_trie); w.bytes(&self.next_election); w.bytes(&self.prev); w.bytes(&self.validator); w.bytes(&self.rnd); if with_sig { w.bytes(&self.sig); } w.b } // sig_hash: what the validator signs (opcode incl., sig excl.). pub fn sig_hash(&self) -> Hash32 { h(&self.encode(false)) } // hash identifies the block (signature included). pub fn hash(&self) -> Hash32 { h(&self.encode(true)) } } // Block: header + the transactions that produce its state; the block // commits to the *resulting* state, verification re-executes. #[derive(Clone, Debug, Default)] pub struct Block { pub header: Header, pub txs: Vec, } // advance_periods applies every period boundary crossed in (from, to]: // next_election becomes the active trie, a fresh one opens — // unconditionally (voting is a liveness requirement). fn advance_periods(ns: &mut State, from: u64, to: u64) { let mut b = period_start(from).wrapping_add(PERIOD_SECONDS); while b <= to && b >= PERIOD_SECONDS { ns.election = std::mem::take(&mut ns.next_election); b = b.wrapping_add(PERIOD_SECONDS); } } // pre_select 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. fn pre_select(prev: &State, t: u64) -> R<(State, Hash32, VoteEntry)> { if t % SLOT_SECONDS != 0 { bail!("block time not on the slot grid"); } if t <= prev.time { bail!("block time not after previous block"); } let mut ns = prev.clone(); advance_periods(&mut ns, prev.time, t); let r = sel_rand(ns.rnd, prev.seq, prev.seq_at(t)); let entry = ns.election.select_rand(&r) .ok_or_else(|| Err("no committed votes in election trie".into()))?; Ok((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. fn finish(ns: &mut State, txs: &[Tx], t: u64, validator: &PubKey, r: &Hash32, reveal: &Hash32, entry: &VoteEntry) -> R<()> { if h2(validator, reveal) != entry.commit { bail!("reveal does not match the selected commit"); } ns.election.update_commit(&entry.op, reveal)?; ns.rnd = xor32(r, reveal); let fees = ns.apply_txs(txs, t)?; if fees > 0 { let norm = normalize(fees, t, ns.norm_time)?; ns.utxo.insert(fee_outpoint(ns.seq_at(t)), fees, norm, t, *validator) .map_err(|e| Err(format!("fee mint: {}", e)))?; } Ok(()) } // build_block 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. pub fn build_block(prev: &State, txs: Vec, t: u64, val_priv: &Seed, onions: &mut [Onion]) -> R<(Arc, State)> { let pubkey = pub_from_seed(val_priv); let (mut ns, r, entry) = pre_select(prev, t)?; let mut reveal = None; for o in onions.iter_mut() { if let Some(rv) = o.reveal(&entry.commit) { reveal = Some(rv); break; } } let Some(reveal) = reveal else { bail!("build: slot {} not ours (or onion exhausted)", slot_of(t)); }; finish(&mut ns, &txs, t, &pubkey, &r, &reveal, &entry)?; let mut h = Header { seq: prev.seq_at(t), time: t, people_tree: ns.tree.root_hash(), utxo_trie: ns.utxo.root_hash(), election_trie: ns.election.root_hash(), next_election: ns.next_election.root_hash(), prev: prev.last_hash, validator: pubkey, rnd: ns.rnd, ..Default::default() }; h.sig = sign_msg(val_priv, &h.sig_hash()); ns.seq = h.seq; ns.time = t; ns.last_hash = h.hash(); Ok((Arc::new(Block { header: h, txs }), ns)) } // verify_block 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. pub fn verify_block(prev: &State, b: &Block) -> R { let hd = &b.header; if hd.seq != prev.seq_at(hd.time) { bail!("verify: seq {} is not slots-since-genesis {}", hd.seq, prev.seq_at(hd.time)); } if hd.prev != prev.last_hash { bail!("verify: prev hash mismatch"); } let (mut ns, r, entry) = pre_select(prev, hd.time).map_err(|e| Err(format!("verify: {}", e)))?; let reveal = xor32(&r, &hd.rnd); finish(&mut ns, &b.txs, hd.time, &hd.validator, &r, &reveal, &entry) .map_err(|e| Err(format!("verify: {}", e)))?; if !verify_sig(&hd.validator, &hd.sig_hash(), &hd.sig) { bail!("verify: bad validator signature"); } if ns.tree.root_hash() != hd.people_tree { bail!("verify: people_tree root mismatch"); } if ns.utxo.root_hash() != hd.utxo_trie { bail!("verify: utxo_trie root mismatch"); } if ns.election.root_hash() != hd.election_trie { bail!("verify: election_trie root mismatch"); } if ns.next_election.root_hash() != hd.next_election { bail!("verify: next_election root mismatch"); } ns.seq = hd.seq; ns.time = hd.time; ns.last_hash = hd.hash(); Ok(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. pub struct Chain { pub state: State, pub blocks: Vec>, pub final_state: State, // state after blocks[final_idx]; never reorged pub final_idx: usize, } impl Chain { pub fn tip_hash(&self) -> Hash32 { self.state.last_hash } // horizon: the period boundary BEFORE the one the tip sits in. pub fn horizon(&self) -> u64 { let p = period_start(self.state.time); if p < PERIOD_SECONDS { 0 } else { p - PERIOD_SECONDS } } // advance_finality folds newly-final blocks into the snapshot. pub fn advance_finality(&mut self) { let h = self.horizon(); while self.final_idx + 1 < self.blocks.len() && self.blocks[self.final_idx + 1].header.time < h { match verify_block(&self.final_state, &self.blocks[self.final_idx + 1]) { Ok(ns) => { self.final_state = ns; self.final_idx += 1; } Result::Err(e) => { // cannot happen: block was verified on append panic!("finality replay diverged: {}", e); } } } } // state_at: the state after blocks[i] (i ≥ final_idx), replaying // from the finality snapshot when needed. pub fn state_at(&self, i: usize) -> R { if i + 1 == self.blocks.len() { return Ok(self.state.clone()); } let mut st = self.final_state.clone(); for j in self.final_idx + 1..=i { st = verify_block(&st, &self.blocks[j])?; } Ok(st) } // produce builds the next block, self-verifies it, and advances. pub fn produce(&mut self, txs: Vec, t: u64, val_priv: &Seed, onions: &mut [Onion]) -> R> { let (b, ns) = build_block(&self.state, txs, t, val_priv, onions)?; let vs = verify_block(&self.state, &b) .map_err(|e| Err(format!("self-verify failed: {}", e)))?; if vs.last_hash != ns.last_hash { bail!("self-verify: state divergence"); } self.state = ns; self.blocks.push(b.clone()); self.advance_finality(); Ok(b) } // try_adopt 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. pub fn try_adopt(&mut self, branch: &[Arc]) -> R { if branch.is_empty() { bail!("adopt: empty branch"); } let mut fork_idx = None; let mut i = self.blocks.len(); while i > self.final_idx { i -= 1; if self.blocks[i].header.hash() == branch[0].header.prev { fork_idx = Some(i); break; } } let fork_idx = fork_idx .ok_or_else(|| Err("adopt: fork point unknown or below the finality horizon".into()))?; if fork_idx + 1 + branch.len() <= self.blocks.len() { bail!("adopt: branch has {} blocks from the fork, ours has {} — not strictly better", branch.len(), self.blocks.len() - fork_idx - 1); } let mut st = self.state_at(fork_idx)?; for b in branch { st = verify_block(&st, b).map_err(|e| Err(format!("adopt: {}", e)))?; } self.blocks.truncate(fork_idx + 1); self.blocks.extend(branch.iter().cloned()); self.state = st; self.advance_finality(); Ok(fork_idx) } } // new_chain 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. pub fn new_chain(root_key: &PubKey, mut t0: u64, val_priv: &Seed, commit0: &Hash32, commit1: &Hash32) -> R { let val_key = pub_from_seed(val_priv); t0 -= t0 % SLOT_SECONDS; let norm = norm_time_for(t0); let mut st = State { norm_time: norm, time: t0, genesis: t0, tree: PeopleTree::make(root_key, 1, t0, norm)?, ..Default::default() }; st.election.insert(VoteEntry { op: genesis_vote_outpoint(0), committed: true, commit: *commit0, ..Default::default() })?; st.next_election.insert(VoteEntry { op: genesis_vote_outpoint(1), committed: true, commit: *commit1, ..Default::default() })?; let mut hd = Header { seq: 0, time: t0, people_tree: st.tree.root_hash(), utxo_trie: st.utxo.root_hash(), election_trie: st.election.root_hash(), next_election: st.next_election.root_hash(), validator: val_key, ..Default::default() }; hd.sig = sign_msg(val_priv, &hd.sig_hash()); st.last_hash = hd.hash(); let final_state = st.clone(); Ok(Chain { state: st, blocks: vec![Arc::new(Block { header: hd, txs: Vec::new() })], final_state, final_idx: 0, }) } // ============================================================== wire // Rdr is the decoding counterpart of Buf: it never fails mid-parse, // it accumulates the first error and returns zero values after it. pub struct Rdr<'a> { p: &'a [u8], pub err: Option, } static ZEROS64: [u8; 64] = [0u8; 64]; impl<'a> Rdr<'a> { pub fn new(data: &'a [u8]) -> Rdr<'a> { Rdr { p: data, err: None } } fn need(&mut self, k: usize) -> &'a [u8] { if self.err.is_some() { return &ZEROS64[..k.min(64)]; } if self.p.len() < k { self.err = Some("wire: truncated".into()); return &ZEROS64[..k.min(64)]; } let (q, rest) = self.p.split_at(k); self.p = rest; q } pub fn u8v(&mut self) -> u8 { self.need(1)[0] } pub fn u32v(&mut self) -> u32 { let q = self.need(4); u32::from_be_bytes([q[0], q[1], q[2], q[3]]) } pub fn u64v(&mut self) -> u64 { let q = self.need(8); let mut b = [0u8; 8]; b.copy_from_slice(q); u64::from_be_bytes(b) } pub fn u128v(&mut self) -> u128 { let q = self.need(16); let mut b = [0u8; 16]; b.copy_from_slice(q); u128::from_be_bytes(b) } pub fn boolv(&mut self) -> bool { self.u8v() != 0 } pub fn h32(&mut self) -> Hash32 { let q = self.need(32); let mut b = [0u8; 32]; b.copy_from_slice(q); b } pub fn key(&mut self) -> PubKey { self.h32() } pub fn sig(&mut self) -> Sig { let q = self.need(64); let mut b = [0u8; 64]; b.copy_from_slice(q); b } pub fn done(&self) -> R<()> { if let Some(e) = &self.err { bail!("{}", e); } if !self.p.is_empty() { bail!("wire: trailing bytes"); } Ok(()) } } // ------------------------------------------------------------- outputs fn decode_outputs(r: &mut Rdr) -> Vec { let n = r.u32v(); if n as usize > MAX_OUTPUTS { r.err = Some("wire: too many outputs".into()); return Vec::new(); } let mut outs = Vec::with_capacity(n as usize); for _ in 0..n { if r.err.is_some() { break; } outs.push(Output { amount: r.u128v(), owner: r.key() }); } outs } // ------------------------------------------------------------ template fn encode_template(w: &mut Buf, t: &NodeTemplate) { w.bytes(&t.key); w.boolb(t.leaf); w.u64b(t.nonce); w.u64b(t.last_ubi); w.u64b(t.last_vote); w.u64b(t.tree_count); w.u128b(t.tree_ubi); w.u32b(t.children.len() as u32); for c in &t.children { encode_template(w, c); } } fn decode_template(r: &mut Rdr, budget: &mut i32) -> NodeTemplate { *budget -= 1; if *budget < 0 { r.err = Some("wire: template too large".into()); return NodeTemplate::default(); } let mut t = NodeTemplate { key: r.key(), leaf: r.boolv(), nonce: r.u64v(), last_ubi: r.u64v(), last_vote: r.u64v(), tree_count: r.u64v(), tree_ubi: r.u128v(), children: Vec::new(), }; let n = r.u32v(); if n as usize > MAX_TEMPLATE_NODES { r.err = Some("wire: template too large".into()); return t; } for _ in 0..n { if r.err.is_some() { break; } t.children.push(decode_template(r, budget)); } t } // ------------------------------------------------------------------ tx // encode_tx serializes a transaction: hashed body first, then the // signatures, then (Add) the template — identical to the Go wire. pub fn encode_tx(t: &Tx) -> Vec { let mut w = Buf::default(); w.bytes(&t.body()); match t { Tx::Claim(c) => w.bytes(&c.sig), Tx::Transfer(tr) => { for s in &tr.sigs { w.bytes(s); } } Tx::Prune(_) => {} // no signatures: validity is objective Tx::Add(a) => { w.bytes(&a.consent); w.bytes(&a.sig); encode_template(&mut w, &a.tmpl); } Tx::Remove(rm) => w.bytes(&rm.sig), Tx::Rekey(rk) => w.bytes(&rk.sig), Tx::Move(m) => { w.bytes(&m.consent); w.bytes(&m.sig); } Tx::Leave(l) => w.bytes(&l.sig), Tx::Vote(v) => { for s in &v.sigs { w.bytes(s); } } Tx::VoteClaim(vc) => w.bytes(&vc.sig), } w.b } fn decode_tx_inner(r: &mut Rdr) -> Option { let op = r.u8v(); match op { OP_CLAIM => Some(Tx::Claim(Claim { key: r.key(), amount: r.u128v(), nonce: r.u64v(), sig: r.sig(), })), OP_TRANSFER => { let n = r.u32v(); if n as usize > MAX_INPUTS { r.err = Some("wire: too many inputs".into()); return None; } let mut t = Transfer::default(); for _ in 0..n { if r.err.is_some() { break; } t.inputs.push(Outpoint { tx: r.h32(), index: r.u32v() }); } t.outputs = decode_outputs(r); for _ in 0..n { if r.err.is_some() { break; } t.sigs.push(r.sig()); } Some(Tx::Transfer(t)) } OP_PRUNE => { let n = r.u32v(); if n as usize > MAX_INPUTS { r.err = Some("wire: too many inputs".into()); return None; } let mut t = Prune::default(); for _ in 0..n { if r.err.is_some() { break; } t.inputs.push(Outpoint { tx: r.h32(), index: r.u32v() }); } Some(Tx::Prune(t)) } OP_ADD => { let mut t = Add { parent: r.key(), child_key: r.key(), hashv: r.h32(), nonce: r.u64v(), deadline: r.u64v(), consent: r.sig(), sig: r.sig(), tmpl: NodeTemplate::default(), }; let mut budget = MAX_TEMPLATE_NODES as i32; t.tmpl = decode_template(r, &mut budget); Some(Tx::Add(t)) } OP_REMOVE => Some(Tx::Remove(Remove { parent: r.key(), child: r.key(), nonce: r.u64v(), sig: r.sig(), })), OP_REKEY => Some(Tx::Rekey(Rekey { old_key: r.key(), new_key: r.key(), nonce: r.u64v(), sig: r.sig(), })), OP_MOVE => Some(Tx::Move(Move { child: r.key(), new_parent: r.key(), nonce: r.u64v(), deadline: r.u64v(), consent: r.sig(), sig: r.sig(), })), OP_LEAVE => Some(Tx::Leave(Leave { child: r.key(), nonce: r.u64v(), sig: r.sig(), })), OP_VOTE_CLAIM => Some(Tx::VoteClaim(VoteClaim { key: r.key(), nonce: r.u64v(), sig: r.sig(), })), OP_VOTE => { let ni = r.u32v(); if ni as usize > MAX_INPUTS { r.err = Some("wire: too many inputs".into()); return None; } let mut t = Vote::default(); for _ in 0..ni { if r.err.is_some() { break; } t.inputs.push(Outpoint { tx: r.h32(), index: r.u32v() }); } let no = r.u32v(); if no as usize > MAX_OUTPUTS { r.err = Some("wire: too many outputs".into()); return None; } for _ in 0..no { if r.err.is_some() { break; } let mut o = VoteOutput { committed: r.boolv(), ..Default::default() }; if o.committed { o.commit = r.h32(); o.has_owner = 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(o); } for _ in 0..ni { if r.err.is_some() { break; } t.sigs.push(r.sig()); } Some(Tx::Vote(t)) } _ => { r.err = Some(format!("wire: unknown opcode 0x{:02x}", op)); None } } } // decode_tx parses exactly one transaction. pub fn decode_tx(data: &[u8]) -> R { let mut r = Rdr::new(data); let t = decode_tx_inner(&mut r); r.done()?; t.ok_or_else(|| Err("wire: bad tx".into())) } // --------------------------------------------------------------- block // encode_block: header (with sig) + u32 tx count + per tx u32 len + bytes. pub fn encode_block(b: &Block) -> Vec { let mut w = Buf::default(); w.bytes(&b.header.encode(true)); w.u32b(b.txs.len() as u32); for t in &b.txs { let tb = encode_tx(t); w.u32b(tb.len() as u32); w.bytes(&tb); } w.b } fn decode_header(r: &mut Rdr) -> Header { let op = r.u8v(); if op != OP_HEADER && r.err.is_none() { r.err = Some(format!("wire: bad header opcode 0x{:02x}", op)); } Header { seq: r.u64v(), time: r.u64v(), people_tree: r.h32(), utxo_trie: r.h32(), election_trie: r.h32(), next_election: r.h32(), prev: r.h32(), validator: r.key(), rnd: r.h32(), sig: r.sig(), } } pub const MAX_TX_BYTES: usize = 1 << 22; // 4 MiB per tx, sanity cap pub fn decode_block(data: &[u8]) -> R { let mut r = Rdr::new(data); let header = decode_header(&mut r); let mut txs = Vec::new(); let n = r.u32v(); for i in 0..n { if r.err.is_some() { break; } let l = r.u32v(); if l as usize > MAX_TX_BYTES { bail!("wire: tx too large"); } let tb = r.need(l as usize); if r.err.is_some() { break; } txs.push(decode_tx(tb).map_err(|e| Err(format!("wire: tx {}: {}", i, e)))?); } r.done()?; Ok(Block { header, txs }) } // ============================================================= 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 | encode_block(block N) use std::fs::{File, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; pub struct Store { f: File, pub offsets: Vec, // start offset of record i pub end: u64, // end of the last record pub genesis_rec: Vec, } fn write_rec(f: &mut File, rec: &[u8]) -> R<()> { f.write_all(&(rec.len() as u32).to_be_bytes()) .and_then(|_| f.write_all(rec)) .map_err(|e| Err(format!("store: write failed: {}", e))) } impl Store { pub fn append(&mut self, b: &Block) -> R<()> { let rec = encode_block(b); write_rec(&mut self.f, &rec)?; self.offsets.push(self.end); self.end += 4 + rec.len() as u64; self.f.sync_all().map_err(|e| Err(format!("store: fsync failed: {}", e))) } // reorg truncates the log to its first `keep` records and appends // the adopted branch. keep counts records including genesis. pub fn reorg(&mut self, keep: usize, branch: &[Arc]) -> R<()> { if keep < 1 || keep > self.offsets.len() { bail!("store: bad reorg keep count"); } let cut = if keep < self.offsets.len() { self.offsets[keep] } else { self.end }; self.f.set_len(cut).map_err(|e| Err(format!("store: truncate failed: {}", e)))?; self.f.seek(SeekFrom::Start(cut)).map_err(|e| Err(format!("store: seek failed: {}", e)))?; self.offsets.truncate(keep); self.end = cut; for b in branch { let rec = encode_block(b); write_rec(&mut self.f, &rec)?; self.offsets.push(self.end); self.end += 4 + rec.len() as u64; } self.f.sync_all().map_err(|e| Err(format!("store: fsync failed: {}", e))) } } // genesis_state rebuilds and validates the genesis state from the // stored parameters + header (the header is checked, not trusted). fn genesis_state(root_key: &PubKey, t0: u64, commit0: &Hash32, commit1: &Hash32, hd: &Header) -> R { let norm = norm_time_for(t0); let mut st = State { norm_time: norm, time: t0, genesis: t0, tree: PeopleTree::make(root_key, 1, t0, norm)?, ..Default::default() }; st.election.insert(VoteEntry { op: genesis_vote_outpoint(0), committed: true, commit: *commit0, ..Default::default() })?; st.next_election.insert(VoteEntry { op: genesis_vote_outpoint(1), committed: true, commit: *commit1, ..Default::default() })?; if hd.seq != 0 || hd.time != t0 || hd.prev != ZERO32 { bail!("genesis: bad seq/time/prev"); } if t0 % SLOT_SECONDS != 0 { bail!("genesis: t0 not on the slot grid"); } if hd.rnd != ZERO32 { bail!("genesis: rand must be zero"); } if hd.people_tree != st.tree.root_hash() || hd.utxo_trie != st.utxo.root_hash() || hd.election_trie != st.election.root_hash() || hd.next_election != st.next_election.root_hash() { bail!("genesis: root mismatch"); } if !verify_sig(&hd.validator, &hd.sig_hash(), &hd.sig) { bail!("genesis: bad validator signature"); } st.last_hash = hd.hash(); Ok(st) } // create_store writes a fresh log for the given genesis. pub fn create_store(path: &str, root_key: &PubKey, t0: u64, commit0: &Hash32, commit1: &Hash32, gen: &Header) -> R { let mut f = OpenOptions::new().create_new(true).write(true).open(path) .map_err(|e| Err(format!("store: {}: {}", path, e)))?; let mut w = Buf::default(); w.bytes(root_key); w.u64b(t0); w.bytes(commit0); w.bytes(commit1); w.bytes(&gen.encode(true)); write_rec(&mut f, &w.b)?; f.sync_all().map_err(|e| Err(format!("store: fsync failed: {}", e)))?; let end = 4 + w.b.len() as u64; Ok(Store { f, offsets: vec![0], end, genesis_rec: w.b }) } // open_store reads the log, replays and verifies every block, and // returns the store (positioned for appends) plus the chain. pub fn open_store(path: &str) -> R<(Store, Chain)> { let mut data = Vec::new(); File::open(path) .and_then(|mut f| f.read_to_end(&mut data)) .map_err(|e| Err(format!("{}: {}", path, e)))?; let mut off: usize = 0; let mut offsets: Vec = Vec::new(); let mut next = |offsets: &mut Vec| -> R> { if off == data.len() { return Ok(None); } if data.len() - off < 4 { bail!("store: truncated length"); } let l = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize; if data.len() - off - 4 < l { bail!("store: truncated record"); } offsets.push(off as u64); let rec = &data[off + 4..off + 4 + l]; off += 4 + l; Ok(Some(rec)) }; let rec = next(&mut offsets)?.ok_or_else(|| Err("store: genesis record: missing".into()))?; let gen_rec = rec.to_vec(); let mut r = Rdr::new(rec); let root_key = r.key(); let t0 = r.u64v(); let commit0 = r.h32(); let commit1 = r.h32(); let gh = decode_header(&mut r); r.done().map_err(|e| Err(format!("store: genesis record: {}", e)))?; let st = genesis_state(&root_key, t0, &commit0, &commit1, &gh)?; let final_state = st.clone(); let mut ch = Chain { state: st, blocks: vec![Arc::new(Block { header: gh, txs: Vec::new() })], final_state, final_idx: 0, }; while let Some(rec) = next(&mut offsets)? { let blk = decode_block(rec) .map_err(|e| Err(format!("store: record after slot {}: {}", ch.state.seq, e)))?; let ns = verify_block(&ch.state, &blk) .map_err(|e| Err(format!("store: block {}: {}", blk.header.seq, e)))?; ch.state = ns; ch.blocks.push(Arc::new(blk)); } ch.advance_finality(); let f = OpenOptions::new().append(true).open(path) .map_err(|e| Err(format!("store: {}: {}", path, e)))?; Ok((Store { f, offsets, end: off as u64, genesis_rec: gen_rec }, ch)) } // ============================================================== json // A small ordered JSON value: enough for this node's API. Numbers keep // their raw literal so u64 values round-trip exactly. Output shape // (2-space indent, trailing newline) matches the Go/C++ nodes. #[derive(Clone, Debug, Default)] pub enum Json { #[default] Null, Bool(bool), Num(String), // raw literal Str(String), Arr(Vec), Obj(Vec<(String, Json)>), } impl Json { pub fn num_u64(v: u64) -> Json { Json::Num(v.to_string()) } pub fn s(v: impl Into) -> Json { Json::Str(v.into()) } pub fn obj() -> Json { Json::Obj(Vec::new()) } pub fn arr() -> Json { Json::Arr(Vec::new()) } pub fn set(&mut self, k: &str, v: Json) -> &mut Json { if let Json::Obj(o) = self { o.push((k.to_string(), v)); } self } pub fn add(&mut self, v: Json) -> &mut Json { if let Json::Arr(a) = self { a.push(v); } self } pub fn get(&self, k: &str) -> Option<&Json> { if let Json::Obj(o) = self { o.iter().find(|(kk, _)| kk == k).map(|(_, v)| v) } else { None } } // typed accessors with Go-style zero defaults on absence pub fn gs(&self, k: &str) -> String { match self.get(k) { Some(Json::Str(s)) => s.clone(), _ => String::new(), } } pub fn gu(&self, k: &str) -> R { match self.get(k) { Some(Json::Num(n)) => parse_u64(n), _ => Ok(0), } } pub fn flag(&self, k: &str) -> bool { matches!(self.get(k), Some(Json::Bool(true))) } pub fn ga(&self, k: &str) -> &[Json] { match self.get(k) { Some(Json::Arr(a)) => a, _ => &[], } } fn escape_to(o: &mut String, s: &str) { for c in s.chars() { match c { '"' => o.push_str("\\\""), '\\' => o.push_str("\\\\"), '\n' => o.push_str("\\n"), '\r' => o.push_str("\\r"), '\t' => o.push_str("\\t"), c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)), c => o.push(c), } } } fn dump_to(&self, o: &mut String, indent: usize, depth: usize) { let pad = " ".repeat(indent * (depth + 1)); let pad0 = " ".repeat(indent * depth); match self { Json::Null => o.push_str("null"), Json::Bool(b) => o.push_str(if *b { "true" } else { "false" }), Json::Num(n) => o.push_str(n), Json::Str(s) => { o.push('"'); Self::escape_to(o, s); o.push('"'); } Json::Arr(a) => { if a.is_empty() { o.push_str("[]"); return; } o.push_str("[\n"); for (i, v) in a.iter().enumerate() { o.push_str(&pad); v.dump_to(o, indent, depth + 1); if i + 1 < a.len() { o.push(','); } o.push('\n'); } o.push_str(&pad0); o.push(']'); } Json::Obj(m) => { if m.is_empty() { o.push_str("{}"); return; } o.push_str("{\n"); for (i, (k, v)) in m.iter().enumerate() { o.push_str(&pad); o.push('"'); Self::escape_to(o, k); o.push_str("\": "); v.dump_to(o, indent, depth + 1); if i + 1 < m.len() { o.push(','); } o.push('\n'); } o.push_str(&pad0); o.push('}'); } } } pub fn dump(&self) -> String { let mut o = String::new(); self.dump_to(&mut o, 2, 0); o.push('\n'); o } } // --- parser struct JParser<'a> { p: &'a [u8], i: usize, depth: usize, } impl<'a> JParser<'a> { fn ws(&mut self) { while self.i < self.p.len() && matches!(self.p[self.i], b' ' | b'\t' | b'\n' | b'\r') { self.i += 1; } } fn peek(&self) -> Option { self.p.get(self.i).copied() } fn parse(&mut self) -> R { self.ws(); let v = self.value()?; self.ws(); if self.i != self.p.len() { bail!("json: trailing data"); } Ok(v) } fn value(&mut self) -> R { self.depth += 1; if self.depth > 128 { bail!("json: too deep"); } self.ws(); let Some(c) = self.peek() else { bail!("json: unexpected end") }; let v = match c { b'{' => self.obj_v()?, b'[' => self.arr_v()?, b'"' => Json::Str(self.str_v()?), b't' => { self.lit(b"true")?; Json::Bool(true) } b'f' => { self.lit(b"false")?; Json::Bool(false) } b'n' => { self.lit(b"null")?; Json::Null } _ => self.num_v()?, }; self.depth -= 1; Ok(v) } fn lit(&mut self, s: &[u8]) -> R<()> { if self.p.len() - self.i < s.len() || &self.p[self.i..self.i + s.len()] != s { bail!("json: bad literal"); } self.i += s.len(); Ok(()) } fn obj_v(&mut self) -> R { let mut m = Vec::new(); self.i += 1; // { self.ws(); if self.peek() == Some(b'}') { self.i += 1; return Ok(Json::Obj(m)); } loop { self.ws(); if self.peek() != Some(b'"') { bail!("json: expected key"); } let k = self.str_v()?; self.ws(); if self.peek() != Some(b':') { bail!("json: expected :"); } self.i += 1; m.push((k, self.value()?)); self.ws(); match self.peek() { Some(b',') => { self.i += 1; continue; } Some(b'}') => { self.i += 1; return Ok(Json::Obj(m)); } _ => bail!("json: expected , or }}"), } } } fn arr_v(&mut self) -> R { let mut a = Vec::new(); self.i += 1; // [ self.ws(); if self.peek() == Some(b']') { self.i += 1; return Ok(Json::Arr(a)); } loop { a.push(self.value()?); self.ws(); match self.peek() { Some(b',') => { self.i += 1; continue; } Some(b']') => { self.i += 1; return Ok(Json::Arr(a)); } _ => bail!("json: expected , or ]"), } } } fn str_v(&mut self) -> R { self.i += 1; // " let mut s = Vec::::new(); while let Some(c) = self.peek() { if c == b'"' { self.i += 1; return String::from_utf8(s).map_err(|_| Err("json: bad utf-8".into())); } self.i += 1; if c != b'\\' { s.push(c); continue; } let Some(x) = self.peek() else { bail!("json: bad escape") }; self.i += 1; match x { b'"' => s.push(b'"'), b'\\' => s.push(b'\\'), b'/' => s.push(b'/'), b'b' => s.push(0x08), b'f' => s.push(0x0C), b'n' => s.push(b'\n'), b'r' => s.push(b'\r'), b't' => s.push(b'\t'), b'u' => { let mut cp = self.hex4()?; // surrogate pair if (0xD800..=0xDBFF).contains(&cp) && self.p.len() - self.i >= 6 && self.p[self.i] == b'\\' && self.p[self.i + 1] == b'u' { let save = self.i; self.i += 2; if let Ok(lo) = self.hex4() { if (0xDC00..=0xDFFF).contains(&lo) { cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } else { self.i = save; } } else { self.i = save; } } // encode UTF-8 if cp < 0x80 { s.push(cp as u8); } else if cp < 0x800 { s.push(0xC0 | (cp >> 6) as u8); s.push(0x80 | (cp & 0x3F) as u8); } else if cp < 0x10000 { s.push(0xE0 | (cp >> 12) as u8); s.push(0x80 | ((cp >> 6) & 0x3F) as u8); s.push(0x80 | (cp & 0x3F) as u8); } else { s.push(0xF0 | (cp >> 18) as u8); s.push(0x80 | ((cp >> 12) & 0x3F) as u8); s.push(0x80 | ((cp >> 6) & 0x3F) as u8); s.push(0x80 | (cp & 0x3F) as u8); } } _ => bail!("json: bad escape"), } } bail!("json: unterminated string"); } fn hex4(&mut self) -> R { if self.p.len() - self.i < 4 { bail!("json: bad \\u"); } let mut cp: u32 = 0; for _ in 0..4 { let h = hex_val(self.p[self.i]); if h < 0 { bail!("json: bad \\u"); } cp = cp << 4 | h as u32; self.i += 1; } Ok(cp) } fn num_v(&mut self) -> R { let start = self.i; if self.peek() == Some(b'-') { self.i += 1; } while let Some(c) = self.peek() { if c.is_ascii_digit() || matches!(c, b'.' | b'e' | b'E' | b'+' | b'-') { self.i += 1; } else { break; } } if self.i == start { bail!("json: bad number"); } Ok(Json::Num(String::from_utf8_lossy(&self.p[start..self.i]).into_owned())) } } pub fn json_parse(s: &[u8]) -> R { JParser { p: s, i: 0, depth: 0 }.parse() } // ============================================================== http // A deliberately plain HTTP/1.1 layer over std::net: enough for the // JSON API, the block push, and peer sync — the same surface the Go // node exposes with net/http. use std::net::{TcpListener, TcpStream, ToSocketAddrs}; use std::time::Duration; pub struct HttpReq { pub method: String, pub path: String, pub query: String, pub body: Vec, } pub struct HttpResp { pub code: i32, pub ctype: String, pub body: Vec, } impl HttpResp { pub fn new(code: i32, ctype: &str, body: impl Into>) -> HttpResp { HttpResp { code, ctype: ctype.to_string(), body: body.into() } } pub fn json(code: i32, j: &Json) -> HttpResp { HttpResp::new(code, "application/json", j.dump().into_bytes()) } pub fn err(code: i32, msg: &str) -> HttpResp { let mut j = Json::obj(); j.set("error", Json::s(msg)); HttpResp::json(code, &j) } } const MAX_HTTP_HEAD: usize = 64 << 10; const MAX_HTTP_BODY: usize = 64 << 20; fn set_timeouts(s: &TcpStream, sec: u64) { let _ = s.set_read_timeout(Some(Duration::from_secs(sec))); let _ = s.set_write_timeout(Some(Duration::from_secs(sec))); } // read_head reads into buf until \r\n\r\n; returns header end offset. fn read_head(s: &mut TcpStream, buf: &mut Vec) -> Option { let mut tmp = [0u8; 4096]; loop { if let Some(hit) = find_sub(buf, b"\r\n\r\n") { return Some(hit + 4); } if buf.len() >= MAX_HTTP_HEAD { return None; } match s.read(&mut tmp) { Ok(0) | Result::Err(_) => return None, Ok(n) => buf.extend_from_slice(&tmp[..n]), } } } fn find_sub(hay: &[u8], needle: &[u8]) -> Option { hay.windows(needle.len()).position(|w| w == needle) } fn read_body_n(s: &mut TcpStream, buf: &[u8], already: usize, want: usize) -> Option> { let mut out = buf[already..].to_vec(); let mut tmp = [0u8; 8192]; while out.len() < want { let take = tmp.len().min(want - out.len()); match s.read(&mut tmp[..take]) { Ok(0) | Result::Err(_) => return None, Ok(n) => out.extend_from_slice(&tmp[..n]), } } (out.len() == want).then_some(out) } // header parsing shared by server + client: returns lowercase name → // value pairs from the head section after the first line. fn parse_headers(head: &[u8], eol: usize, hend: usize) -> Vec<(String, String)> { let mut hs = Vec::new(); let mut pos = eol + 2; while pos + 2 <= hend.saturating_sub(2) { let Some(nl) = find_sub(&head[pos..hend], b"\r\n") else { break }; let line = &head[pos..pos + nl]; pos += nl + 2; let Some(col) = line.iter().position(|&c| c == b':') else { continue }; let name = String::from_utf8_lossy(&line[..col]).to_lowercase(); let val = String::from_utf8_lossy(&line[col + 1..]).trim_start().to_string(); hs.push((name, val)); } hs } // ------------------------------------------------------------ server pub struct HttpServer { listener: TcpListener, } impl HttpServer { pub fn listen(addr: &str) -> R { let mut a = addr.to_string(); if a.starts_with(':') { a = format!("0.0.0.0{}", a); } let listener = TcpListener::bind(&a).map_err(|e| Err(format!("listen: bind {}: {}", a, e)))?; Ok(HttpServer { listener }) } pub fn serve(&self, handler: Arc HttpResp + Send + Sync>) -> ! { loop { let Ok((stream, _)) = self.listener.accept() else { continue }; let h = handler.clone(); std::thread::spawn(move || handle_conn(stream, h)); } } } fn handle_conn(mut s: TcpStream, handler: Arc HttpResp + Send + Sync>) { set_timeouts(&s, 30); let mut buf = Vec::new(); let Some(hend) = read_head(&mut s, &mut buf) else { return }; let Some(eol) = find_sub(&buf, b"\r\n") else { return }; let line = String::from_utf8_lossy(&buf[..eol]).into_owned(); let mut it = line.split(' '); let (Some(method), Some(target)) = (it.next(), it.next()) else { return }; let (path, query) = match target.find('?') { Some(q) => (&target[..q], &target[q + 1..]), None => (target, ""), }; let req_path = path.to_string(); let req_query = query.to_string(); let req_method = method.to_string(); let mut clen: usize = 0; for (name, val) in parse_headers(&buf, eol, hend) { if name == "content-length" { match parse_u64(&val) { Ok(v) => clen = v as usize, Result::Err(_) => return, } } } if clen > MAX_HTTP_BODY { return; } let body = if clen > 0 { match read_body_n(&mut s, &buf, hend, clen) { Some(b) => b, None => return, } } else { Vec::new() }; let req = HttpReq { method: req_method, path: req_path, query: req_query, body }; let resp = if req.method == "OPTIONS" { HttpResp::new(200, "text/plain", Vec::new()) } else { handler(&req) }; let stat = match resp.code { 200 => "OK", 400 => "Bad Request", 404 => "Not Found", 409 => "Conflict", 422 => "Unprocessable Entity", _ => "Internal Server Error", }; let head = format!( "HTTP/1.1 {} {}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", resp.code, stat, resp.ctype, resp.body.len() ); let _ = s.write_all(head.as_bytes()).and_then(|_| s.write_all(&resp.body)); } // ------------------------------------------------------------ client // parse_url: http://host[:port]/path — the only scheme peers use. fn parse_url(url: &str) -> Option<(String, u16, String)> { let rest = url.strip_prefix("http://")?; let (hostport, path) = match rest.find('/') { Some(i) => (&rest[..i], rest[i..].to_string()), None => (rest, "/".to_string()), }; let (host, port) = match hostport.rfind(':') { Some(i) => (&hostport[..i], hostport[i + 1..].parse::().ok()?), None => (hostport, 80), }; if host.is_empty() { return None; } Some((host.to_string(), port, path)) } fn dial_timeout(host: &str, port: u16, sec: u64) -> Option { let addrs = (host, port).to_socket_addrs().ok()?; for a in addrs { if let Ok(s) = TcpStream::connect_timeout(&a, Duration::from_secs(sec)) { return Some(s); } } None } // http_request 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 None on transport failure. pub fn http_request(method: &str, url: &str, body: &[u8], ctype: &str) -> Option<(i32, Vec)> { let (host, port, path) = parse_url(url)?; let mut s = dial_timeout(&host, port, 5)?; set_timeouts(&s, 5); let mut req = format!( "{} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nAccept-Encoding: identity\r\n", method, path, host ); if !body.is_empty() || method == "POST" { req += &format!("Content-Type: {}\r\nContent-Length: {}\r\n", ctype, body.len()); } req += "\r\n"; s.write_all(req.as_bytes()).ok()?; s.write_all(body).ok()?; let mut buf = Vec::new(); let hend = read_head(&mut s, &mut buf)?; let eol = find_sub(&buf, b"\r\n")?; let status = String::from_utf8_lossy(&buf[..eol]).into_owned(); let code: i32 = status.split(' ').nth(1)?.parse().ok()?; let mut chunked = false; let mut have_len = false; let mut clen: usize = 0; for (name, val) in parse_headers(&buf, eol, hend) { if name == "content-length" { have_len = true; clen = val.parse().unwrap_or(0); } else if name == "transfer-encoding" && val.to_lowercase().contains("chunked") { chunked = true; } } let out: Vec; if chunked { // decode chunked from buf tail + socket let mut rest = buf[hend..].to_vec(); let mut pos = 0usize; let mut o = Vec::new(); let mut tmp = [0u8; 8192]; loop { let nl = loop { if let Some(nl) = find_sub(&rest[pos..], b"\r\n") { break nl; } match s.read(&mut tmp) { Ok(0) | Result::Err(_) => return None, Ok(n) => rest.extend_from_slice(&tmp[..n]), } }; let line = String::from_utf8_lossy(&rest[pos..pos + nl]).into_owned(); let size = usize::from_str_radix(line.trim().split(';').next()?.trim(), 16).ok()?; pos += nl + 2; while rest.len() < pos + size + 2 { match s.read(&mut tmp) { Ok(0) | Result::Err(_) => return None, Ok(n) => rest.extend_from_slice(&tmp[..n]), } } if size == 0 { break; } o.extend_from_slice(&rest[pos..pos + size]); pos += size + 2; } out = o; } else if have_len { if clen > MAX_HTTP_BODY { return None; } out = read_body_n(&mut s, &buf, hend, clen)?; } else { // read to EOF let mut o = buf[hend..].to_vec(); let mut tmp = [0u8; 8192]; loop { match s.read(&mut tmp) { Ok(0) => break, Ok(n) => o.extend_from_slice(&tmp[..n]), Result::Err(_) => break, } } out = o; } Some((code, out)) } // ============================================================ server // -------------------------------------------------------- JSON <-> tx fn p_outputs(js: &[Json]) -> R> { let mut outs = Vec::new(); for o in js { outs.push(Output { amount: parse_amount(&o.gs("amount"))?, owner: p_key(&o.gs("owner"))? }); } Ok(outs) } fn p_inputs(js: &[Json]) -> R> { let mut ins = Vec::new(); for i in js { ins.push(Outpoint { tx: to32(&hex_n(&i.gs("tx"), 32)?)?, index: i.gu("index")? as u32 }); } Ok(ins) } fn p_template(j: &Json) -> R { let mut t = NodeTemplate { key: p_key(&j.gs("key"))?, leaf: j.flag("leaf"), nonce: j.gu("nonce")?, last_ubi: j.gu("last_ubi")?, last_vote: j.gu("last_vote")?, tree_count: j.gu("tree_count")?, ..Default::default() }; let tu = j.gs("tree_ubi"); if !tu.is_empty() { t.tree_ubi = parse_amount(&tu).map_err(|e| Err(format!("tree_ubi: {}", e)))?; } for c in j.ga("children") { t.children.push(p_template(c)?); } Ok(t) } fn p_sigs(js: &[Json]) -> R> { let mut ss = Vec::new(); for s in js { let Json::Str(v) = s else { bail!("bad sig") }; ss.push(p_sig(v)?); } Ok(ss) } // to_tx builds a transaction from JSON. For Add, the committed hash is // derived from the template. fn to_tx(j: &Json) -> R { let typ = j.gs("type"); match typ.as_str() { "claim" => Ok(Tx::Claim(Claim { key: p_key(&j.gs("key"))?, amount: parse_amount(&j.gs("amount"))?, nonce: j.gu("nonce")?, sig: p_sig(&j.gs("sig"))?, })), "vote_claim" => Ok(Tx::VoteClaim(VoteClaim { key: p_key(&j.gs("key"))?, nonce: j.gu("nonce")?, sig: p_sig(&j.gs("sig"))?, })), "transfer" => Ok(Tx::Transfer(Transfer { inputs: p_inputs(j.ga("inputs"))?, outputs: p_outputs(j.ga("outputs"))?, sigs: p_sigs(j.ga("sigs"))?, })), "prune" => Ok(Tx::Prune(Prune { inputs: p_inputs(j.ga("inputs"))? })), "vote" => { let mut t = Vote { inputs: p_inputs(j.ga("inputs"))?, ..Default::default() }; for o in j.ga("vote_outputs") { let mut vo = VoteOutput { committed: o.flag("committed"), ..Default::default() }; if vo.committed { vo.commit = to32(&hex_n(&o.gs("commit"), 32)?)?; vo.amount = 1; vo.mixed = o.gu("mixed")? as u32; let ow = o.gs("owner"); if !ow.is_empty() { vo.owner = p_key(&ow)?; vo.has_owner = true; } } else { vo.owner = p_key(&o.gs("owner"))?; vo.amount = o.gu("amount")?; vo.mixed = o.gu("mixed")? as u32; } t.outputs.push(vo); } t.sigs = p_sigs(j.ga("sigs"))?; Ok(Tx::Vote(t)) } "add" => { let Some(tj @ Json::Obj(_)) = j.get("template") else { bail!("add: template required"); }; let tmpl = p_template(tj)?; let hashv = tmpl.hash(); Ok(Tx::Add(Add { parent: p_key(&j.gs("parent"))?, child_key: p_key(&j.gs("child_key"))?, tmpl, hashv, nonce: j.gu("nonce")?, deadline: j.gu("deadline")?, consent: p_sig(&j.gs("consent"))?, sig: p_sig(&j.gs("sig"))?, })) } "remove" => Ok(Tx::Remove(Remove { parent: p_key(&j.gs("parent"))?, child: p_key(&j.gs("child"))?, nonce: j.gu("nonce")?, sig: p_sig(&j.gs("sig"))?, })), "rekey" => Ok(Tx::Rekey(Rekey { old_key: p_key(&j.gs("old"))?, new_key: p_key(&j.gs("new"))?, nonce: j.gu("nonce")?, sig: p_sig(&j.gs("sig"))?, })), "move" => Ok(Tx::Move(Move { child: p_key(&j.gs("child"))?, new_parent: p_key(&j.gs("new_parent"))?, nonce: j.gu("nonce")?, deadline: j.gu("deadline")?, consent: p_sig(&j.gs("consent"))?, sig: p_sig(&j.gs("sig"))?, })), "leave" => Ok(Tx::Leave(Leave { child: p_key(&j.gs("child"))?, nonce: j.gu("nonce")?, sig: p_sig(&j.gs("sig"))?, })), _ => bail!("unknown tx type \"{}\"", typ), } } fn j_template(t: &NodeTemplate) -> Json { let mut j = Json::obj(); j.set("key", Json::s(hex(&t.key))); j.set("leaf", Json::Bool(t.leaf)); if t.nonce != 0 { j.set("nonce", Json::num_u64(t.nonce)); } if t.last_ubi != 0 { j.set("last_ubi", Json::num_u64(t.last_ubi)); } if t.last_vote != 0 { j.set("last_vote", Json::num_u64(t.last_vote)); } j.set("tree_count", Json::num_u64(t.tree_count)); j.set("tree_ubi", Json::s(format!("{}", t.tree_ubi))); if !t.children.is_empty() { let mut ch = Json::arr(); for c in &t.children { ch.add(j_template(c)); } j.set("children", ch); } j } // from_tx renders a transaction as the API JSON (used by /block, // /mempool and peer tx forwarding — parseable by Go and C++ alike). fn from_tx(t: &Tx) -> Json { let sigs_of = |ss: &[Sig]| { let mut a = Json::arr(); for s in ss { a.add(Json::s(hex(s))); } a }; let ins_of = |ins: &[Outpoint]| { let mut a = Json::arr(); for i in ins { let mut o = Json::obj(); o.set("tx", Json::s(hex(&i.tx))); o.set("index", Json::num_u64(i.index as u64)); a.add(o); } a }; let outs_of = |outs: &[Output]| { let mut a = Json::arr(); for o in outs { let mut jo = Json::obj(); jo.set("amount", Json::s(format!("{}", o.amount))); jo.set("owner", Json::s(hex(&o.owner))); a.add(jo); } a }; let mut j = Json::obj(); match t { Tx::Claim(v) => { j.set("type", Json::s("claim")); j.set("key", Json::s(hex(&v.key))); j.set("nonce", Json::num_u64(v.nonce)); j.set("amount", Json::s(format!("{}", v.amount))); j.set("sig", Json::s(hex(&v.sig))); } Tx::VoteClaim(v) => { j.set("type", Json::s("vote_claim")); j.set("key", Json::s(hex(&v.key))); j.set("nonce", Json::num_u64(v.nonce)); j.set("sig", Json::s(hex(&v.sig))); } Tx::Transfer(v) => { j.set("type", Json::s("transfer")); j.set("nonce", Json::num_u64(0)); j.set("outputs", outs_of(&v.outputs)); j.set("inputs", ins_of(&v.inputs)); j.set("sigs", sigs_of(&v.sigs)); } Tx::Prune(v) => { j.set("type", Json::s("prune")); j.set("nonce", Json::num_u64(0)); j.set("inputs", ins_of(&v.inputs)); } Tx::Vote(v) => { j.set("type", Json::s("vote")); j.set("nonce", Json::num_u64(0)); j.set("inputs", ins_of(&v.inputs)); let mut vo = Json::arr(); for o in &v.outputs { let mut e = Json::obj(); e.set("committed", Json::Bool(o.committed)); if o.committed { e.set("commit", Json::s(hex(&o.commit))); e.set("mixed", Json::num_u64(o.mixed as u64)); if o.has_owner { e.set("owner", Json::s(hex(&o.owner))); } } else { e.set("amount", Json::num_u64(o.amount)); e.set("owner", Json::s(hex(&o.owner))); e.set("mixed", Json::num_u64(o.mixed as u64)); } vo.add(e); } j.set("vote_outputs", vo); j.set("sigs", sigs_of(&v.sigs)); } Tx::Add(v) => { j.set("type", Json::s("add")); j.set("parent", Json::s(hex(&v.parent))); j.set("child_key", Json::s(hex(&v.child_key))); j.set("nonce", Json::num_u64(v.nonce)); j.set("deadline", Json::num_u64(v.deadline)); j.set("template", j_template(&v.tmpl)); j.set("consent", Json::s(hex(&v.consent))); j.set("sig", Json::s(hex(&v.sig))); } Tx::Remove(v) => { j.set("type", Json::s("remove")); j.set("parent", Json::s(hex(&v.parent))); j.set("child", Json::s(hex(&v.child))); j.set("nonce", Json::num_u64(v.nonce)); j.set("sig", Json::s(hex(&v.sig))); } Tx::Rekey(v) => { j.set("type", Json::s("rekey")); j.set("old", Json::s(hex(&v.old_key))); j.set("new", Json::s(hex(&v.new_key))); j.set("nonce", Json::num_u64(v.nonce)); j.set("sig", Json::s(hex(&v.sig))); } Tx::Move(v) => { j.set("type", Json::s("move")); j.set("child", Json::s(hex(&v.child))); j.set("new_parent", Json::s(hex(&v.new_parent))); j.set("nonce", Json::num_u64(v.nonce)); j.set("deadline", Json::num_u64(v.deadline)); j.set("consent", Json::s(hex(&v.consent))); j.set("sig", Json::s(hex(&v.sig))); } Tx::Leave(v) => { j.set("type", Json::s("leave")); j.set("child", Json::s(hex(&v.child))); j.set("nonce", Json::num_u64(v.nonce)); j.set("sig", Json::s(hex(&v.sig))); } } j } fn tx_type_name(t: &Tx) -> &'static str { match t { Tx::Claim(_) => "claim", Tx::Transfer(_) => "transfer", Tx::Prune(_) => "prune", Tx::Add(_) => "add", Tx::Remove(_) => "remove", Tx::Rekey(_) => "rekey", Tx::Move(_) => "move", Tx::Leave(_) => "leave", Tx::Vote(_) => "vote", Tx::VoteClaim(_) => "vote_claim", } } // ------------------------------------------------------------ logging pub fn wall_time() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0) } static LOG_MU: Mutex<()> = Mutex::new(()); fn logf(msg: &str) { let t = wall_time(); let _g = LOG_MU.lock(); eprintln!("{:02}:{:02}:{:02} {}", (t / 3600) % 24, (t / 60) % 60, t % 60, msg); } macro_rules! logf { ($($t:tt)*) => { logf(&format!($($t)*)) }; } // ------------------------------------------------------------- Server // Server: JSON API + mempool + block production loop + the network // layer. One mutex guards chain, store, mempool and onions; all // network I/O happens outside the lock. pub struct Inner { pub chain: Chain, pub store: Store, pub mempool: Vec, pub onions: Vec, pub last_slot: u64, // last slot we attempted, produced or not pub seen: std::collections::HashSet, // tx ids accepted this session } impl Inner { // now_t is the block clock: wall time, never before the chain tip. fn now_t(&self) -> u64 { wall_time().max(self.chain.state.time) } } pub struct Srv { pub inner: Mutex, pub val_priv: Seed, pub peers: Vec, } pub type Server = Arc; // expired_prune scans for UTXOs with spendable(T) ≤ 0 and builds a // Prune collecting them (or None). Inputs sorted for a deterministic // tx. fn expired_prune(st: &State, t: u64) -> Option { let mut ops: Vec = Vec::new(); for e in st.utxo.iter() { if ops.len() >= MAX_INPUTS { break; } if e.expired(t).unwrap_or(false) { ops.push(e.op); } } if ops.is_empty() { return None; } ops.sort(); Some(Tx::Prune(Prune { inputs: ops })) } impl Srv { // ---------------------------------------------- production loop fn produce_loop(self: &Arc) -> ! { loop { std::thread::sleep(Duration::from_secs(1)); let mut g = self.inner.lock().unwrap(); let slot = slot_of(g.now_t()); if slot > g.last_slot && slot_time(slot) > g.chain.state.time { g.last_slot = slot; self.produce_locked(&mut g, slot_time(slot)); } } } fn produce_locked(self: &Arc, g: &mut Inner, t: u64) { // 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. let mut scratch = g.chain.state.clone(); let mut keep: Vec = Vec::new(); if let Some(prune) = expired_prune(&g.chain.state, t) { match scratch.apply_txs(std::slice::from_ref(&prune), t) { Ok(_) => keep.push(prune), Result::Err(e) => logf!("drop prune: {}", e), // cannot happen by construction } } for tx in g.mempool.iter() { let mut trial = scratch.clone(); if let Result::Err(e) = trial.apply_txs(std::slice::from_ref(tx), t) { logf!("drop tx {}: {}", hex(&tx.id()), e); continue; } scratch = trial; keep.push(tx.clone()); } g.mempool.clear(); let b = match g.chain.produce(keep.clone(), t, &self.val_priv, &mut g.onions) { Ok(b) => b, Result::Err(e) => { // Not our slot, onion exhausted, or a real failure: // the slot passes unfilled — a skip. logf!("seq {} skipped: {}", g.chain.state.seq_at(t), e); if !keep.is_empty() { keep.append(&mut g.mempool); g.mempool = keep; // retry next slot } return; } }; if let Result::Err(e) = g.store.append(&b) { logf!("store append failed: {}", e); // cannot continue safely std::process::abort(); } logf!("block {} @ {}: {} tx, people {} utxo {}", b.header.seq, b.header.time, b.txs.len(), hex(&b.header.people_tree[..6]), hex(&b.header.utxo_trie[..6])); let body = encode_block(&b); let ps = self.peers.clone(); std::thread::spawn(move || { for p in &ps { http_request("POST", &format!("{}/api/block", p), &body, "application/octet-stream"); } }); } // -------------------------------------------------------- network // forward_tx relays a freshly accepted transaction to every peer. fn forward_tx(&self, j: String) { let ps = self.peers.clone(); std::thread::spawn(move || { for p in &ps { http_request("POST", &format!("{}/api/tx", p), j.as_bytes(), "application/json"); } }); } // 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. fn ingest(&self, blocks: Vec>) -> R<()> { if blocks.is_empty() { return Ok(()); } let mut g = self.inner.lock().unwrap(); if blocks.last().unwrap().header.time > g.now_t() + SLOT_SECONDS { bail!("ingest: block from the future"); } let fork_idx = g.chain.try_adopt(&blocks)?; if let Result::Err(e) = g.store.reorg(fork_idx + 1, &blocks) { logf!("store reorg failed: {}", e); // cannot continue safely std::process::abort(); } logf!("adopted {} block(s) from peer, tip seq {} @ {}", blocks.len(), g.chain.state.seq, g.chain.state.time); Ok(()) } // read_records parses a stream of length-prefixed block records. fn read_records(data: &[u8]) -> R>> { let mut out = Vec::new(); let mut off = 0usize; while off < data.len() { if data.len() - off < 4 { bail!("sync: truncated length"); } let l = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize; off += 4; if data.len() - off < l { bail!("sync: truncated record"); } out.push(Arc::new(decode_block(&data[off..off + l])?)); off += l; } Ok(out) } // fetch_chain pulls blocks with seq > from, following the // server's batching until the stream dries up. fn fetch_chain(&self, peer: &str, mut from: u64) -> R>> { let mut out: Vec> = Vec::new(); loop { let Some((_, body)) = http_request("GET", &format!("{}/api/chain?from={}", peer, from), &[], "text/plain") else { bail!("sync: fetch failed"); }; let blocks = Self::read_records(&body)?; if blocks.is_empty() { return Ok(out); } from = blocks.last().unwrap().header.seq; out.extend(blocks); } } // sync_peer compares tips with one peer and, when the peer's // chain is better, fetches and adopts it. Returns true on // progress. fn sync_peer(&self, peer: &str) -> R { let Some((_, body)) = http_request("GET", &format!("{}/api/status", peer), &[], "text/plain") else { bail!("status fetch failed"); }; let st = json_parse(&body)?; let st_blocks = st.gu("blocks")?; let st_tip = st.gs("tip_hash"); let st_gen = st.gs("genesis_hash"); let (our_genesis, our_blocks, our_tip, from_seq, final_seq, final_idx) = { let g = self.inner.lock().unwrap(); (g.chain.blocks[0].header.hash(), g.chain.blocks.len() as u64, g.chain.tip_hash(), g.chain.state.seq, g.chain.blocks[g.chain.final_idx].header.seq, g.chain.final_idx) }; if st_gen != hex(&our_genesis) { bail!("sync: peer has a different genesis"); } if st_blocks <= our_blocks || st_tip == hex(&our_tip) { return Ok(false); // nothing better there } // Fast path: everything above our tip; if the first block // doesn't extend us we diverged → refetch the reorg window. let mut blocks = self.fetch_chain(peer, from_seq)?; if blocks.is_empty() { return Ok(false); } if blocks[0].header.prev != our_tip { blocks = self.fetch_chain(peer, final_seq)?; let have: std::collections::HashSet = { let g = self.inner.lock().unwrap(); g.chain.blocks[final_idx..].iter().map(|b| b.header.hash()).collect() }; let mut cut = 0; while cut < blocks.len() && have.contains(&blocks[cut].header.hash()) { cut += 1; } blocks.drain(..cut); if blocks.is_empty() { return Ok(false); } } self.ingest(blocks)?; Ok(true) } // poll_loop keeps us in sync with every peer. fn poll_loop(self: &Arc) -> ! { loop { std::thread::sleep(Duration::from_secs(2)); for p in &self.peers { loop { match self.sync_peer(p) { Ok(true) => continue, Ok(false) => break, Result::Err(e) => { logf!("sync {}: {}", p, e); break; } } } } } } // ------------------------------------------------------- handlers fn h_status(&self) -> R { let g = self.inner.lock().unwrap(); let st = &g.chain.state; let t = g.now_t(); let supply = st.supply_at(t)?; let uncl = st.unclaimed_at(t)?; let sum = supply + uncl; let target = st.tree.population() as u128 * TOKEN as u128; let mut j = Json::obj(); j.set("seq", Json::num_u64(st.seq)); j.set("blocks", Json::num_u64(g.chain.blocks.len() as u64)); j.set("time", Json::num_u64(st.time)); j.set("now", Json::num_u64(t)); j.set("norm_time", Json::num_u64(st.norm_time)); j.set("rand", Json::s(hex(&st.rnd))); j.set("tip_hash", Json::s(hex(&g.chain.tip_hash()))); j.set("genesis_hash", Json::s(hex(&g.chain.blocks[0].header.hash()))); j.set("period_start", Json::num_u64(period_start(st.time))); j.set("phase", Json::s(if phase_open(st.time) { "open" } else { "locked" })); j.set("committed", Json::num_u64(st.election.committed_count())); j.set("committed_next", Json::num_u64(st.next_election.committed_count())); j.set("election_root", Json::s(hex(&st.election.root_hash()))); j.set("people_root", Json::s(hex(&st.tree.root_hash()))); j.set("utxo_root", Json::s(hex(&st.utxo.root_hash()))); j.set("population", Json::num_u64(st.tree.population())); j.set("utxo_count", Json::num_u64(st.utxo.len() as u64)); j.set("supply", Json::s(format!("{}", supply))); j.set("unclaimed", Json::s(format!("{}", uncl))); j.set("sum", Json::s(format!("{}", sum))); j.set("target", Json::s(format!("{}", target))); j.set("mempool", Json::num_u64(g.mempool.len() as u64)); j.set("token", Json::s(format!("{}", TOKEN))); j.set("rent_per_second", Json::s(format!("{}", RENT_PER_SECOND))); j.set("slot_seconds", Json::num_u64(SLOT_SECONDS)); j.set("period_seconds", Json::num_u64(PERIOD_SECONDS)); j.set("max_mix", Json::num_u64(MAX_MIX as u64)); Ok(HttpResp::json(200, &j)) } fn h_node(&self, key_hex: &str) -> R { let k = match p_key(key_hex) { Ok(k) => k, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; let g = self.inner.lock().unwrap(); let Some(i) = g.chain.state.tree.get_idx(&k) else { return Ok(HttpResp::err(404, "key not in tree")); }; let tree = &g.chain.state.tree; let n = tree.node(i); let t = g.now_t(); let mut j = Json::obj(); j.set("key", Json::s(key_hex)); j.set("leaf", Json::Bool(n.leaf)); j.set("own", Json::num_u64(n.own)); j.set("nonce", Json::num_u64(n.nonce)); j.set("last_ubi", Json::num_u64(n.last_ubi)); j.set("last_vote", Json::num_u64(n.last_vote)); j.set("tree_count", Json::num_u64(n.tree_count)); j.set("claimable_now", Json::s(format!("{}", claimable_at(n.own, n.last_ubi, t)))); j.set("now", Json::num_u64(t)); j.set("parent", Json::s(match n.parent { Some(p) => hex(&tree.node(p).key), None => String::new(), })); let mut ch = Json::arr(); for &c in &n.children { ch.add(Json::s(hex(&tree.node(c).key))); } j.set("children", ch); Ok(HttpResp::json(200, &j)) } fn h_balance(&self, key_hex: &str) -> R { let k = match p_key(key_hex) { Ok(k) => k, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; let g = self.inner.lock().unwrap(); let t = g.now_t(); let mut total: u128 = 0; let mut spend_total: u128 = 0; let mut us: Vec<(String, u32, u128, u128, u64)> = Vec::new(); for e in g.chain.state.utxo.iter() { if e.owner != k { continue; } let v = e.value(t)?; let rent = e.rent(t); total += v; if v > rent { spend_total += v - rent; } us.push((hex(&e.op.tx), e.op.index, v, rent, e.time)); } us.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); let mut arr = Json::arr(); for (tx, index, v, rent, created) in us { let mut e = Json::obj(); e.set("tx", Json::s(tx)); e.set("index", Json::num_u64(index as u64)); e.set("value_now", Json::s(format!("{}", v))); e.set("rent_owed", Json::s(format!("{}", rent))); e.set("spendable", Json::s(spend_str(v, rent))); if v <= rent { e.set("expired", Json::Bool(true)); } e.set("created", Json::num_u64(created)); arr.add(e); } let mut j = Json::obj(); j.set("now", Json::num_u64(t)); j.set("total", Json::s(format!("{}", total))); j.set("spendable", Json::s(format!("{}", spend_total))); j.set("utxos", arr); Ok(HttpResp::json(200, &j)) } fn h_block(&self, seq_str: &str) -> R { let seq = match parse_u64(seq_str) { Ok(s) => s, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; let g = self.inner.lock().unwrap(); let bs = &g.chain.blocks; let i = bs.partition_point(|b| b.header.seq < seq); if i == bs.len() || bs[i].header.seq != seq { return Ok(HttpResp::err(404, "no block at that slot (skipped, or beyond the tip)")); } let b = &bs[i]; let hd = &b.header; let mut j = Json::obj(); j.set("seq", Json::num_u64(hd.seq)); j.set("time", Json::num_u64(hd.time)); j.set("people_tree", Json::s(hex(&hd.people_tree))); j.set("utxo_trie", Json::s(hex(&hd.utxo_trie))); j.set("prev", Json::s(hex(&hd.prev))); j.set("validator", Json::s(hex(&hd.validator))); j.set("election_trie", Json::s(hex(&hd.election_trie))); j.set("rand", Json::s(hex(&hd.rnd))); j.set("sig", Json::s(hex(&hd.sig))); j.set("hash", Json::s(hex(&hd.hash()))); let mut txs = Json::arr(); for t in &b.txs { txs.add(from_tx(t)); } j.set("txs", txs); Ok(HttpResp::json(200, &j)) } fn h_mempool(&self) -> R { let g = self.inner.lock().unwrap(); let mut arr = Json::arr(); for t in &g.mempool { let mut e = Json::obj(); e.set("id", Json::s(hex(&t.id()))); e.set("type", Json::s(tx_type_name(t))); arr.add(e); } Ok(HttpResp::json(200, &arr)) } // h_tx_prepare: submit an UNSIGNED tx, get back the exact bytes // to sign (hex). Nothing is queued. fn h_tx_prepare(&self, body: &[u8]) -> R { let t = match json_parse(body).and_then(|j| to_tx(&j)) { Ok(t) => t, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; let id = t.id(); let mut resp = Json::obj(); resp.set("id", Json::s(hex(&id))); resp.set("sign_message", Json::s(hex(&id))); // SigHash == ID let note = if matches!(t, Tx::Vote(_)) { "sign with ed25519 over sign_message bytes; one sig per input in 'sigs'; POST /tx" } else { "sign with ed25519 over sign_message bytes; put hex signature in 'sig' (or 'sigs', one per input) and POST /tx" }; resp.set("note", Json::s(note)); if let Tx::Add(a) = &t { resp.set("consent_message", Json::s(hex(&consent_msg(&a.hashv, a.deadline, &a.parent)))); resp.set("template_hash", Json::s(hex(&a.hashv))); } if let Tx::Move(m) = &t { resp.set("consent_message", Json::s(hex(&move_consent_msg(m.deadline, &m.new_parent)))); } Ok(HttpResp::json(200, &resp)) } fn h_tx_submit(&self, body: &[u8]) -> R { let t = match json_parse(body).and_then(|j| to_tx(&j)) { Ok(t) => t, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; let id = t.id(); let mut forwarded = String::new(); { let mut g = self.inner.lock().unwrap(); if g.seen.contains(&id) { let mut j = Json::obj(); j.set("id", Json::s(hex(&id))); j.set("status", Json::s("known")); return Ok(HttpResp::json(200, &j)); } // Dry-run against confirmed state + current mempool, at // the earliest possible inclusion time. let t_now = g.now_t(); let mut trial = g.chain.state.clone(); for p in &g.mempool { let _ = trial.apply_txs(std::slice::from_ref(p), t_now); // best effort; conflicts re-checked at production } if let Result::Err(e) = trial.apply_txs(std::slice::from_ref(&t), t_now) { return Ok(HttpResp::err(422, &e.0)); } if !self.peers.is_empty() { forwarded = from_tx(&t).dump(); } g.mempool.push(t); g.seen.insert(id); } // Relay so the tx reaches whichever validator wins a slot. if !forwarded.is_empty() { self.forward_tx(forwarded); } let mut j = Json::obj(); j.set("id", Json::s(hex(&id))); j.set("status", Json::s("queued")); Ok(HttpResp::json(200, &j)) } // h_genesis serves the verbatim genesis record. fn h_genesis(&self) -> R { let g = self.inner.lock().unwrap(); let mut w = Buf::default(); w.u32b(g.store.genesis_rec.len() as u32); w.bytes(&g.store.genesis_rec); Ok(HttpResp::new(200, "application/octet-stream", w.b)) } // h_chain streams length-prefixed block records with seq > from, // capped per request. fn h_chain(&self, query: &str) -> R { let mut from = 0u64; if let Some(v) = query.strip_prefix("from=") { from = match parse_u64(v) { Ok(v) => v, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; } let mut w = Buf::default(); { let g = self.inner.lock().unwrap(); let bs = &g.chain.blocks; let mut i = bs.partition_point(|b| b.header.seq <= from); let mut count = 0usize; let mut total = 0usize; while i < bs.len() && count < 2048 && total < (4usize << 20) { let rec = encode_block(&bs[i]); w.u32b(rec.len() as u32); w.bytes(&rec); total += 4 + rec.len(); i += 1; count += 1; } } Ok(HttpResp::new(200, "application/octet-stream", w.b)) } // h_block_push accepts one pushed block (wire format). A block // that doesn't fit right now is a 409; the poll loop resolves any // real divergence. fn h_block_push(&self, body: &[u8]) -> R { if body.len() > MAX_TX_BYTES { return Ok(HttpResp::err(400, "block too large")); } let b = match decode_block(body) { Ok(b) => b, Result::Err(e) => return Ok(HttpResp::err(400, &e.0)), }; if let Result::Err(e) = self.ingest(vec![Arc::new(b)]) { return Ok(HttpResp::err(409, &e.0)); } let mut j = Json::obj(); j.set("status", Json::s("accepted")); Ok(HttpResp::json(200, &j)) } fn route(&self, r: &HttpReq) -> HttpResp { let path_arg = |prefix: &str| -> Option { let rest = r.path.strip_prefix(prefix)?; (!rest.is_empty() && !rest.contains('/')).then(|| rest.to_string()) }; let out: R = (|| { if r.method == "GET" { if r.path == "/api/status" { return self.h_status(); } if let Some(k) = path_arg("/api/node/") { return self.h_node(&k); } if let Some(k) = path_arg("/api/balance/") { return self.h_balance(&k); } if let Some(s) = path_arg("/api/block/") { return self.h_block(&s); } if r.path == "/api/mempool" { return self.h_mempool(); } if r.path == "/api/genesis" { return self.h_genesis(); } if r.path == "/api/chain" { return self.h_chain(&r.query); } if r.path == "/" { return Ok(HttpResp::new(200, "text/plain", "hiercoin node (rust)\n\nGET /api/status /api/node/{key} /api/balance/{key} /api/block/{seq} /api/mempool /api/genesis /api/chain?from=N\nPOST /api/tx/prepare /api/tx /api/block\n".as_bytes().to_vec())); } } if r.method == "POST" { if r.path == "/api/tx/prepare" { return self.h_tx_prepare(&r.body); } if r.path == "/api/tx" { return self.h_tx_submit(&r.body); } if r.path == "/api/block" { return self.h_block_push(&r.body); } } Ok(HttpResp::err(404, "not found")) })(); out.unwrap_or_else(|e| HttpResp::err(500, &e.0)) } } // =============================================================== cmd // outln prints a line, ignoring broken pipes (e.g. `| head`). fn outln(s: &str) { let _ = writeln!(std::io::stdout(), "{}", s); } fn usage() -> ! { eprintln!("usage: hiercoin keygen | init | run | join | sign | replay | selftest (use -h per command)"); std::process::exit(2); } // tiny flag parser: -name value or -name=value. struct Flags { cmd: &'static str, names: Vec<&'static str>, vals: HashMap<&'static str, String>, help: HashMap<&'static str, &'static str>, } impl Flags { fn new(cmd: &'static str) -> Flags { Flags { cmd, names: Vec::new(), vals: HashMap::new(), help: HashMap::new() } } fn def(&mut self, name: &'static str, dflt: &str, h: &'static str) { self.names.push(name); self.vals.insert(name, dflt.to_string()); self.help.insert(name, h); } fn parse(&mut self, args: &[String]) { let mut i = 0; while i < args.len() { let a = &args[i]; if a == "-h" || a == "--help" { eprintln!("usage: hiercoin {} [flags]", self.cmd); for n in &self.names { let d = &self.vals[n]; eprintln!(" -{} value\n {} (default {})", n, self.help[n], if d.is_empty() { "\"\"" } else { d }); } std::process::exit(2); } if a.len() < 2 || !a.starts_with('-') { eprintln!("unexpected argument {}", a); std::process::exit(2); } let mut name = a.trim_start_matches('-').to_string(); let val; if let Some(eq) = name.find('=') { val = name[eq + 1..].to_string(); name.truncate(eq); } else { i += 1; if i >= args.len() { eprintln!("flag -{} needs a value", name); std::process::exit(2); } val = args[i].clone(); } let Some(k) = self.names.iter().find(|n| **n == name) else { eprintln!("unknown flag -{}", name); std::process::exit(2); }; self.vals.insert(k, val); i += 1; } } fn s(&self, n: &str) -> String { self.vals[n].clone() } fn u(&self, n: &str) -> R { parse_u64(&self.vals[n]) } } // load_seed: hex string or @file containing it. fn load_seed(spec: &str) -> R { let s = if let Some(path) = spec.strip_prefix('@') { std::fs::read_to_string(path).map_err(|e| Err(format!("{}: {}", path, e)))? } else { spec.to_string() }; let b = hex_decode(s.trim()).map_err(|_| Err("seed must be 32 hex bytes".into()))?; if b.len() != 32 { bail!("seed must be 32 hex bytes"); } to32(&b) } fn seed_path(dir: &str) -> String { format!("{}/validator.seed", dir) } fn onion_path(dir: &str) -> String { format!("{}/onion", dir) } fn log_path(dir: &str) -> String { format!("{}/chain.log", dir) } fn write_file(path: &str, content: &str, mode: u32) -> R<()> { use std::os::unix::fs::OpenOptionsExt; let mut f = OpenOptions::new().create(true).truncate(true).write(true).mode(mode) .open(path).map_err(|e| Err(format!("{}: {}", path, e)))?; f.write_all(content.as_bytes()).map_err(|e| Err(format!("{}: write failed: {}", path, e))) } fn mkdir(dir: &str) -> R<()> { match std::fs::create_dir(dir) { Ok(()) => Ok(()), Result::Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), Result::Err(e) => Result::Err(Err(format!("{}: {}", dir, e))), } } // save_onions / load_onions persist the node's onions, one // " " line each. Kept 0600 next to the validator key. fn save_onions(path: &str, specs: &[(Hash32, u64)]) -> R<()> { let mut b = String::new(); for (seed, depth) in specs { b += &format!("{} {}\n", hex(seed), depth); } write_file(path, &b, 0o600) } fn load_onions(path: &str, candidate: &PubKey) -> R> { let data = std::fs::read_to_string(path).map_err(|e| Err(format!("{}: {}", path, e)))?; let mut onions = Vec::new(); for line in data.lines() { let line = line.trim(); if line.is_empty() { continue; } let Some(sp) = line.find(' ') else { bail!("onion file: bad line") }; let seed = to32(&hex_n(&line[..sp], 32)?)?; let depth = parse_u64(line[sp + 1..].trim())?; onions.push(Onion::new(*candidate, seed, depth)); } if onions.is_empty() { bail!("onion file: no onions"); } Ok(onions) } fn cmd_keygen() -> R<()> { let seed = gen_seed()?; outln(&format!("seed {}\npub {}", hex(&seed), hex(&pub_from_seed(&seed)))); Ok(()) } fn cmd_sign(args: &[String]) -> R<()> { let mut fs = Flags::new("sign"); fs.def("seed", "", "hex seed or @file"); fs.def("msg", "", "hex message to sign"); fs.parse(args); let priv_seed = load_seed(&fs.s("seed"))?; let m = hex_decode(&fs.s("msg"))?; outln(&format!("{}", hex(&sign_msg(&priv_seed, &m)))); Ok(()) } fn cmd_init(args: &[String]) -> R<()> { let mut fs = Flags::new("init"); fs.def("dir", "hiercoin-data", "data directory"); fs.def("root-pub", "", "root person pubkey hex (default: validator key)"); let dflt_depth = (PERIOD_SECONDS / SLOT_SECONDS).to_string(); fs.def("onion-depth", &dflt_depth, "per-onion depth for the two genesis votes (slots each can validate)"); fs.parse(args); let dir = fs.s("dir"); let depth = fs.u("onion-depth")?; mkdir(&dir)?; let priv_seed = gen_seed()?; let pubkey = pub_from_seed(&priv_seed); write_file(&seed_path(&dir), &format!("{}\n", hex(&priv_seed)), 0o600)?; let root = if fs.s("root-pub").is_empty() { pubkey } else { p_key(&fs.s("root-pub"))? }; // Genesis onions: one independent hash chain per seeded trie (see // the Go node for why independence matters). let (seed0, seed1) = (gen_seed()?, gen_seed()?); save_onions(&onion_path(&dir), &[(seed0, depth), (seed1, depth)])?; let onion0 = Onion::new(pubkey, seed0, depth); let onion1 = Onion::new(pubkey, seed1, depth); let ch = new_chain(&root, wall_time(), &priv_seed, &onion0.commit(), &onion1.commit())?; let t0 = ch.state.time; // slot-aligned by new_chain create_store(&log_path(&dir), &root, t0, &onion0.commit(), &onion1.commit(), &ch.blocks[0].header)?; outln(&format!("initialized {}\n t0 {} (slot {})\n root {}\n validator {}\n seed {}\n onions {} (2 × depth {})", dir, t0, slot_of(t0), hex(&root), hex(&pubkey), seed_path(&dir), onion_path(&dir), depth)); Ok(()) } fn parse_peers(s: &str) -> Vec { s.split(',') .map(|p| p.trim().trim_end_matches('/').to_string()) .filter(|p| !p.is_empty()) .collect() } fn cmd_run(args: &[String]) -> R<()> { let mut fs = Flags::new("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(args); let dir = fs.s("dir"); let priv_seed = load_seed(&format!("@{}", seed_path(&dir)))?; let onions = load_onions(&onion_path(&dir), &pub_from_seed(&priv_seed))?; let (store, chain) = open_store(&log_path(&dir))?; logf!("replayed {} block(s), seq {}, population {}, committed votes {}, onions {}", chain.blocks.len() - 1, chain.state.seq, chain.state.tree.population(), chain.state.election.committed_count(), onions.len()); let last_slot = slot_of(chain.state.time); let srv: Server = Arc::new(Srv { inner: Mutex::new(Inner { chain, store, mempool: Vec::new(), onions, last_slot, seen: Default::default(), }), val_priv: priv_seed, peers: parse_peers(&fs.s("peers")), }); { let s = srv.clone(); std::thread::spawn(move || s.produce_loop()); } if !srv.peers.is_empty() { let s = srv.clone(); std::thread::spawn(move || s.poll_loop()); logf!("peers: {}", srv.peers.join(", ")); } logf!("listening on http://{} (slot {}s, one block per slot when selected)", fs.s("listen"), SLOT_SECONDS); let http = HttpServer::listen(&fs.s("listen"))?; let s = srv.clone(); http.serve(Arc::new(move |r: &HttpReq| s.route(r))); } // cmd_join bootstraps a fresh data directory from a running peer. fn cmd_join(args: &[String]) -> R<()> { let mut fs = Flags::new("join"); fs.def("dir", "hiercoin-data", "data directory"); fs.def("peer", "", "peer base URL to bootstrap from (required)"); let dflt_depth = (PERIOD_SECONDS / SLOT_SECONDS).to_string(); fs.def("onion-depth", &dflt_depth, "onion depth for this node's future votes"); fs.parse(args); let dir = fs.s("dir"); let mut peer = fs.s("peer"); if peer.is_empty() { bail!("join: -peer is required"); } mkdir(&dir)?; while peer.ends_with('/') { peer.pop(); } let Some((code, body)) = http_request("GET", &format!("{}/api/genesis", peer), &[], "text/plain") else { bail!("join: cannot fetch genesis from {}", peer); }; if code != 200 { bail!("join: cannot fetch genesis from {}", peer); } // The record arrives with the log's length prefix; write verbatim // and let open_store do the full verification. { let mut f = OpenOptions::new().create_new(true).write(true).open(log_path(&dir)) .map_err(|e| Err(format!("{}: {}", log_path(&dir), e)))?; f.write_all(&body).map_err(|e| Err(format!("join: write failed: {}", e)))?; } let (_, chain) = open_store(&log_path(&dir)) .map_err(|e| Err(format!("join: peer genesis rejected: {}", e)))?; let priv_seed = gen_seed()?; let pubkey = pub_from_seed(&priv_seed); write_file(&seed_path(&dir), &format!("{}\n", hex(&priv_seed)), 0o600)?; let oseed = gen_seed()?; let depth = fs.u("onion-depth")?; save_onions(&onion_path(&dir), &[(oseed, depth)])?; let onion = Onion::new(pubkey, oseed, depth); let gh = chain.blocks[0].header.hash(); outln(&format!("joined {}\n genesis {} (t0 {})\n validator {}\n vote for me: commit {} (depth {})\n next: run -dir {} -peers {}", dir, hex(&gh[..8]), chain.state.genesis, hex(&pubkey), hex(&onion.commit()), depth, dir, peer)); Ok(()) } // cmd_replay opens the log, fully verifying every block, and prints a // summary — a standalone audit of a chain.log from any node. fn cmd_replay(args: &[String]) -> R<()> { let mut fs = Flags::new("replay"); fs.def("dir", "hiercoin-data", "data directory"); fs.parse(args); let (_, chain) = open_store(&log_path(&fs.s("dir")))?; let st = &chain.state; outln(&format!("verified {} block(s)\n seq {}\n time {}\n tip {}\n genesis {}\n people {}\n utxo {}\n election {}\n population {}\n utxos {}\n supply {}\n unclaimed {}", chain.blocks.len() - 1, st.seq, st.time, hex(&chain.tip_hash()), hex(&chain.blocks[0].header.hash()), hex(&st.tree.root_hash()), hex(&st.utxo.root_hash()), hex(&st.election.root_hash()), st.tree.population(), st.utxo.len(), st.supply_at(st.time)?, st.unclaimed_at(st.time)?)); Ok(()) } // ========================================================== 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. The scenario is bit-identical to the // C++ node's selftest and the Go cross-driver's crossgen, so the // resulting chain.log can be byte-compared across implementations. fn expect(cond: bool, what: &str) -> R<()> { if !cond { bail!("selftest: FAILED: {}", what); } Ok(()) } fn seed_n(n: u8) -> Seed { let mut s = [0u8; 32]; s[0] = n; s[31] = 0x5A; s } fn cmd_selftest() -> R<()> { // --- amount math vectors expect(decay_pow(0) == SCALE, "decay^0 == SCALE")?; expect(decay_pow(1) == DECAY_PER_SECOND, "decay^1 == decay")?; let dy = decay_pow(SECONDS_PER_YEAR); expect(dy < 8_000_000_000_000_000 && dy > 7_999_000_000_000_000, "decay^YEAR just under 0.8 SCALE")?; expect(rent_owed(0) == 0 && rent_owed(1) == RENT_PER_SECOND as u128, "rent base cases")?; expect(claimable_at(1, 0, 0) == 0, "claimable at t=0")?; outln(&format!("decay^YEAR = {} (< 0.8e16)", dy)); // --- deterministic genesis let val_priv = seed_n(1); let val_pub = pub_from_seed(&val_priv); let depth = 4096 + 7; // crosses one onion checkpoint stride let mut os0 = [0u8; 32]; let mut os1 = [0u8; 32]; os0[1] = 1; os1[1] = 2; let onion0 = Onion::new(val_pub, os0, depth); let onion1 = Onion::new(val_pub, os1, depth); expect(onion0.commit() == onion_commit(&val_pub, &os0, depth), "onion commit consistency")?; let t0 = norm_time_for(1_750_000_000) + 120; // deterministic, slot-aligned let mut ch = new_chain(&val_pub, t0, &val_priv, &onion0.commit(), &onion1.commit())?; expect(ch.state.genesis == t0 && ch.state.time == t0, "genesis time")?; let mut onions = vec![onion0, onion1]; // --- empty block production + verify determinism let mut t = t0 + SLOT_SECONDS; let b1 = ch.produce(Vec::new(), t, &val_priv, &mut onions)?; expect(b1.header.seq == 1, "seq 1")?; { let enc = encode_block(&b1); let dec = decode_block(&enc)?; expect(dec.header.hash() == b1.header.hash(), "block wire round-trip")?; } // --- claim after ~30 days let alice_p = seed_n(2); let alice = pub_from_seed(&alice_p); t = t0 + 30 * 24 * 3600; // still slot-aligned (multiple of 60) t -= t % SLOT_SECONDS; let claimable = claimable_at(1, t0, t); expect(claimable > 0, "claimable grows")?; let mut cl = Claim { key: val_pub, amount: claimable / 2, nonce: 0, ..Default::default() }; let cl_tx = { let mut tx = Tx::Claim(cl.clone()); cl.sig = sign_msg(&val_priv, &tx.sig_hash()); tx = Tx::Claim(cl.clone()); tx }; ch.produce(vec![cl_tx.clone()], t, &val_priv, &mut onions)?; expect(ch.state.utxo.len() == 2, "claim UTXO + fee UTXO")?; let supply = ch.state.supply_at(t)?; let uncl = ch.state.unclaimed_at(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 let mut add = Add { parent: val_pub, child_key: alice, tmpl: NodeTemplate { key: alice, leaf: true, tree_count: 1, ..Default::default() }, nonce: 1, // after the claim bumped it deadline: t + 3600, ..Default::default() }; add.hashv = add.tmpl.hash(); add.consent = sign_msg(&alice_p, &consent_msg(&add.hashv, add.deadline, &add.parent)); let add_tx = { let tx = Tx::Add(add.clone()); add.sig = sign_msg(&val_priv, &tx.sig_hash()); Tx::Add(add.clone()) }; t += SLOT_SECONDS; ch.produce(vec![add_tx.clone()], t, &val_priv, &mut onions)?; expect(ch.state.tree.population() == 2, "population 2")?; // transfer half of the claimed UTXO to alice let claim_op = Outpoint { tx: cl_tx.id(), index: 0 }; let ce = ch.state.utxo.get(&claim_op).cloned(); expect(ce.is_some(), "claim utxo exists")?; let ce = ce.unwrap(); t += SLOT_SECONDS; let (val, rent) = (ce.value(t)?, ce.rent(t)); expect(val > rent, "claim utxo not expired")?; let mut tr = Transfer { inputs: vec![claim_op], outputs: vec![Output { amount: (val - rent) / 2, owner: alice }], ..Default::default() }; let tr_tx = { let tx = Tx::Transfer(tr.clone()); tr.sigs = vec![sign_msg(&val_priv, &tx.sig_hash())]; Tx::Transfer(tr.clone()) }; ch.produce(vec![tr_tx.clone()], t, &val_priv, &mut onions)?; let ae = ch.state.utxo.get(&Outpoint { tx: tr_tx.id(), index: 0 }).cloned(); expect(ae.map_or(false, |e| e.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. { let mut early = VoteClaim { key: val_pub, nonce: 2, ..Default::default() }; let etx = { let tx = Tx::VoteClaim(early.clone()); early.sig = sign_msg(&val_priv, &tx.sig_hash()); Tx::VoteClaim(early) }; let mut trial = ch.state.clone(); let rejected = trial.apply_txs(&[etx], t + SLOT_SECONDS).is_err(); expect(rejected, "vote claim in genesis period rejected")?; } // cross the first boundary: genesis commit1 takes over as the // active election; next_election opens fresh. let b1time = period_start(t0) + PERIOD_SECONDS; t = b1time + SLOT_SECONDS; ch.produce(Vec::new(), t, &val_priv, &mut onions)?; // ~half a million skipped slots fold into rand expect(ch.state.election.committed_count() == 1, "genesis commit1 active in period 1")?; expect(ch.state.next_election.len() == 0, "fresh next_election")?; // vote claims for both persons, then mix + commit alice's onion let mut alice_seed = [0u8; 32]; alice_seed[2] = 9; let alice_onion = Onion::new(alice, alice_seed, 2048); let mut vc1 = VoteClaim { key: val_pub, nonce: 2, ..Default::default() }; let vc1_tx = { let tx = Tx::VoteClaim(vc1.clone()); vc1.sig = sign_msg(&val_priv, &tx.sig_hash()); Tx::VoteClaim(vc1) }; let mut vc2 = VoteClaim { key: alice, nonce: 0, ..Default::default() }; let vc2_tx = { let tx = Tx::VoteClaim(vc2.clone()); vc2.sig = sign_msg(&alice_p, &tx.sig_hash()); Tx::VoteClaim(vc2) }; t += SLOT_SECONDS; ch.produce(vec![vc1_tx.clone(), vc2_tx.clone()], t, &val_priv, &mut onions)?; expect(ch.state.next_election.len() == 2, "2 vote tokens in next_election")?; // mix the two claims together, commit one for alice, keep one // uncommitted (discarded at the boundary). let mut vt = Vote { inputs: vec![Outpoint { tx: vc1_tx.id(), index: 0 }, Outpoint { tx: vc2_tx.id(), index: 0 }], outputs: vec![ VoteOutput { committed: true, commit: alice_onion.commit(), mixed: 2, ..Default::default() }, VoteOutput { committed: false, amount: 1, owner: alice, mixed: 1, ..Default::default() }, ], ..Default::default() }; let vt_tx = { let tx = Tx::Vote(vt.clone()); let vsh = tx.sig_hash(); vt.sigs = vec![sign_msg(&val_priv, &vsh), sign_msg(&alice_p, &vsh)]; Tx::Vote(vt) }; t += SLOT_SECONDS; ch.produce(vec![vt_tx], t, &val_priv, &mut onions)?; expect(ch.state.next_election.committed_count() == 1, "alice's commit in next_election")?; expect(ch.state.next_election.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. let tb = b1time + PERIOD_SECONDS + SLOT_SECONDS; let mut alice_onions = vec![alice_onion]; { let val_failed = build_block(&ch.state, Vec::new(), tb, &val_priv, &mut onions).is_err(); expect(val_failed, "genesis validator cannot produce in period 2")?; } let (bb, after) = build_block(&ch.state, Vec::new(), tb, &alice_p, &mut alice_onions)?; let vs = verify_block(&ch.state, &bb)?; expect(vs.last_hash == after.last_hash, "boundary block verifies")?; expect(bb.header.validator == alice, "alice validates period 2")?; // adopt via chain (single-block branch on top of tip) ch.try_adopt(&[bb.clone()])?; expect(ch.state.last_hash == bb.header.hash(), "adopt extended tip")?; // --- fork choice: build two competing branches, longer one wins { let base = ch.state.clone(); let tf = tb + SLOT_SECONDS; let mut who_can = |tt: u64, st: &State| -> R<(Arc, State)> { match build_block(st, Vec::new(), tt, &alice_p, &mut alice_onions) { Ok(r) => Ok(r), Result::Err(_) => build_block(st, Vec::new(), tt, &val_priv, &mut onions), } }; let (s1, _) = who_can(tf + SLOT_SECONDS, &base)?; // short branch: skips one slot let (l1, l1st) = who_can(tf, &base)?; // long branch: fills both let (l2, _) = who_can(tf + SLOT_SECONDS, &l1st)?; ch.try_adopt(&[s1.clone()])?; expect(ch.tip_hash() == s1.header.hash(), "short branch adopted first")?; ch.try_adopt(&[l1, l2.clone()])?; // strictly more blocks from the same fork → wins expect(ch.tip_hash() == l2.header.hash(), "fork choice: fewest skips wins")?; let rejected = ch.try_adopt(&[s1]).is_err(); expect(rejected, "shorter branch refused")?; } // --- store round-trip: write everything, reopen, verify let dir = "/tmp/hiercoin-selftest-rs"; let _ = std::fs::remove_file(log_path(dir)); mkdir(dir)?; { let mut store = create_store(&log_path(dir), &val_pub, t0, &onion_commit(&val_pub, &os0, depth), &onion_commit(&val_pub, &os1, depth), &ch.blocks[0].header)?; for b in &ch.blocks[1..] { store.append(b)?; } } { let (_, chain2) = open_store(&log_path(dir))?; expect(chain2.tip_hash() == ch.tip_hash(), "store reopen replays to same tip")?; expect(chain2.state.tree.root_hash() == ch.state.tree.root_hash(), "same people root")?; expect(chain2.state.utxo.root_hash() == ch.state.utxo.root_hash(), "same utxo root")?; } // --- remove: alice leaves, her UBI auto-mints { let an_nonce = ch.state.tree.get(&alice).map(|n| n.nonce).unwrap(); let mut lv = Leave { child: alice, nonce: an_nonce, ..Default::default() }; let lv_tx = { let tx = Tx::Leave(lv.clone()); lv.sig = sign_msg(&alice_p, &tx.sig_hash()); Tx::Leave(lv) }; let tl = ch.state.time + SLOT_SECONDS; let (lb, _) = match build_block(&ch.state, vec![lv_tx.clone()], tl, &alice_p, &mut alice_onions) { Ok(r) => r, Result::Err(_) => build_block(&ch.state, vec![lv_tx.clone()], tl, &val_priv, &mut onions)?, }; ch.try_adopt(&[lb])?; expect(ch.state.tree.population() == 1, "alice left")?; expect(ch.state.tree.get(&alice).is_none(), "alice gone from tree")?; let ub = ch.state.utxo.get(&Outpoint { tx: lv_tx.id(), index: 0 }).cloned(); expect(ub.map_or(false, |e| e.owner == alice), "leave auto-minted alice's UBI")?; } // --- json tx round trip { let j = from_tx(&tr_tx); let back = to_tx(&j)?; expect(back.id() == tr_tx.id(), "json transfer round-trip")?; let ja = from_tx(&add_tx); let back_a = to_tx(&ja)?; expect(back_a.id() == add_tx.id(), "json add round-trip (template hash rederived)")?; } outln(&format!("selftest OK — {} blocks, tip {}", ch.blocks.len() - 1, hex_short(&ch.tip_hash()))); Ok(()) } fn main() { let args: Vec = std::env::args().collect(); if args.len() < 2 { usage(); } let rest = &args[2..]; let r = match args[1].as_str() { "keygen" => cmd_keygen(), "init" => cmd_init(rest), "run" => cmd_run(rest), "join" => cmd_join(rest), "sign" => cmd_sign(rest), "replay" => cmd_replay(rest), "selftest" => cmd_selftest(), _ => usage(), }; if let Result::Err(e) = r { eprintln!("{}", e); std::process::exit(1); } }