// Hiercoin — Fractal Social Hierarchy UTXO with UBI by demurrage. // // 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. Activation is unconditional — a period without // committed votes activates an empty trie and halts the chain, so // voting is a liveness requirement. Genesis seeds both election // tries with one committed vote each (the next_election entry // standing in for the root's first claim), so a fresh chain runs // single-validator until the population's votes take over at the // second period boundary, whatever phase genesis lands in. // 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 (fewest skipped slots — with a // shared genesis and clock that is exactly "most blocks"), and // finality at the previous period boundary bounding every reorg. // Standard library only, Go >= 1.22. // // go run hiercoin.go keygen // go run hiercoin.go init -dir data // go run hiercoin.go run -dir data -listen 127.0.0.1:8080 [-peers http://host:8081,...] // go run hiercoin.go join -dir data2 -peer http://host:8080 // go run hiercoin.go sign -seed @data/validator.seed -msg // // 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) package main import ( "bytes" "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/binary" "encoding/hex" "encoding/json" "errors" "flag" "fmt" "io" "log" "math/big" "net/http" "os" "path/filepath" "sort" "strings" "sync" "time" ) // =============================================================== 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. type buf struct{ b []byte } func (w *buf) u8(x byte) { w.b = append(w.b, x) } func (w *buf) u32(x uint32) { var t [4]byte binary.BigEndian.PutUint32(t[:], x) w.b = append(w.b, t[:]...) } func (w *buf) u64(x uint64) { var t [8]byte binary.BigEndian.PutUint64(t[:], x) w.b = append(w.b, t[:]...) } // u128 writes a non-negative big.Int as 16 bytes big-endian. // Panics on overflow: amounts are defined as unsigned 128-bit. func (w *buf) u128(x *big.Int) { w.b = append(w.b, U128Bytes(x)...) } func (w *buf) bytes(p []byte) { w.b = append(w.b, p...) } func (w *buf) boolb(v bool) { if v { w.u8(1) } else { w.u8(0) } } // U128Bytes encodes x as exactly 16 big-endian bytes. func U128Bytes(x *big.Int) []byte { if x.Sign() < 0 || x.BitLen() > 128 { panic("amount out of u128 range") } var out [16]byte x.FillBytes(out[:]) return out[:] } // H is SHA-256 over the concatenation of parts. func H(parts ...[]byte) [32]byte { h := sha256.New() for _, p := range parts { h.Write(p) } var o [32]byte copy(o[:], h.Sum(nil)) return o } var zero32 [32]byte // ============================================================ amount // Fixed-point scale: 1 TOKEN = 10^16 base units. var ( Scale = big.NewInt(10_000_000_000_000_000) TOKEN = big.NewInt(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. Value // fixed by the spec. DecayPerSecond = big.NewInt(9_999_999_929_290_076) // RentPerSecond is the trie rent: 1000 base units per second per // UTXO. Value fixed by the spec. RentPerSecond = big.NewInt(1000) // rentDenom = SCALE − decay, the per-second fixed-point loss. rentDenom = new(big.Int).Sub(Scale, DecayPerSecond) ) const SecondsPerYear = uint64(31_557_600) // Julian year // Consensus timing, fixed by the spec. Blocks are produced one per // slot; the election period is the validator-selection cycle. The // divisibility chain slot | period | NormPeriod guarantees that // period boundaries land exactly on slots and norm_time boundaries // land exactly on period boundaries (525 960 slots per period, 4 // periods per norm advance). const ( SlotSeconds = uint64(60) PeriodSeconds = SecondsPerYear ) // MaxMix caps a vote token's total hop budget: every output of every // Vote transaction — the final commit included — declares a mixed // value in 1..MaxMix and costs one unit. Vote transactions carry no // fee, so this is what bounds free next_election churn to // O(amount × MaxMix) transactions per token and period. A token // mixed all the way to MaxMix is dead: neither a further mix nor a // commit can pay the required MaxMix+1. const MaxMix = uint32(10) // All periodic operations use intervals that are multiples or // divisors of YEAR, aligned to Unix time 0. norm_time sits on // multiples of 4 × YEAR from Unix 0 (1970, 1974, ..., 2026, ...); // genesis picks the latest boundary at or before t0. Stored // normalized values grow by ×1.25 per year relative to this point. // A planned periodic advancement (sum ×= decay^Δ, a hard fork) // re-bases all stored values — uint128 allows over 100 years between // advances at planetary scale, so a running chain never needs it in // this implementation's lifetime. const NormPeriod = 4 * SecondsPerYear // normTimeFor returns the norm_time boundary for a genesis at t: // the largest multiple of 4 × YEAR from Unix 0 that is ≤ t. func normTimeFor(t uint64) uint64 { return t - t%NormPeriod } // mulScale computes floor(a*b / Scale) for non-negative a, b. // This is THE rounding rule of the system: floor at every step. func mulScale(a, b *big.Int) *big.Int { r := new(big.Int).Mul(a, b) return r.Quo(r, Scale) } var ( powMu sync.Mutex powCache = map[uint64]*big.Int{} ) // DecayPow returns decay^dt at scale 10^16, computed by binary // exponentiation with floor rounding at every step. Deterministic // and bit-reproducible: same dt always yields the same value. func DecayPow(dt uint64) *big.Int { powMu.Lock() if c, ok := powCache[dt]; ok { r := new(big.Int).Set(c) powMu.Unlock() return r } powMu.Unlock() res := new(big.Int).Set(Scale) base := new(big.Int).Set(DecayPerSecond) for e := dt; e > 0; e >>= 1 { if e&1 == 1 { res = mulScale(res, base) } if e > 1 { base = mulScale(base, base) } } powMu.Lock() powCache[dt] = new(big.Int).Set(res) powMu.Unlock() return res } // Normalize converts a real amount at time t to its normalized value // at reference time norm: normalized = floor(amount * Scale / decay^(t-norm)). // Since decay^x <= Scale, normalized >= amount. func Normalize(amount *big.Int, t, norm uint64) *big.Int { if t < norm { panic("Normalize: time before norm_time") } p := DecayPow(t - norm) r := new(big.Int).Mul(amount, Scale) return r.Quo(r, p) } // ValueAt converts a normalized value back to its real value at time T: // real = floor(norm * decay^(T-normTime) / Scale). func ValueAt(normVal *big.Int, T, normTime uint64) *big.Int { if T < normTime { panic("ValueAt: time before norm_time") } return mulScale(normVal, DecayPow(T-normTime)) } // RentOwed is the accumulated rent on a UTXO created dt seconds ago: // // rent_owed = rent × (SCALE − decay^dt) / (SCALE − decay) // // Rent and demurrage are coupled — each unit of rent accrues // demurrage from the moment it is deducted, which sums to the // geometric series above. Floor rounding, per the system rule. // RentOwed(0) = 0, RentOwed(1) = rent. func RentOwed(dt uint64) *big.Int { if dt == 0 { return new(big.Int) } n := new(big.Int).Sub(Scale, DecayPow(dt)) n.Mul(n, RentPerSecond) return n.Quo(n, rentDenom) } // tokens returns n * TOKEN as a fresh big.Int. func tokens(n uint64) *big.Int { return new(big.Int).Mul(new(big.Int).SetUint64(n), TOKEN) } // ClaimableAt implements the spec formula: // claimable(T) = contribution × TOKEN × (1 − decay^(T − last_ubi)). func ClaimableAt(own uint64, lastUBI, T uint64) *big.Int { if T <= lastUBI || own == 0 { return new(big.Int) } ct := tokens(own) return new(big.Int).Sub(ct, mulScale(ct, DecayPow(T-lastUBI))) } // ============================================================== keys // PubKey is a raw Ed25519 public key. type PubKey [32]byte // Sig is a raw Ed25519 signature. type Sig [64]byte var ( ZeroKey PubKey ZeroSig Sig ) func GenKey() (ed25519.PrivateKey, PubKey) { _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { panic(err) } return priv, Pub(priv) } func Pub(priv ed25519.PrivateKey) PubKey { var k PubKey copy(k[:], priv.Public().(ed25519.PublicKey)) return k } func Sign(priv ed25519.PrivateKey, msg []byte) Sig { var s Sig copy(s[:], ed25519.Sign(priv, msg)) return s } func VerifySig(pub PubKey, msg []byte, sig Sig) bool { return ed25519.Verify(pub[:], msg, sig[:]) } func (k PubKey) Short() string { return hex.EncodeToString(k[:4]) } func shortHash(h [32]byte) string { return hex.EncodeToString(h[:8]) } // ================================================================ tx // Opcodes. Every signature in the system covers an encoding that // begins with an opcode ("all signatures include an opcode"). const ( OpClaim byte = 0x01 OpTransfer byte = 0x02 OpAdd byte = 0x03 OpRemove byte = 0x04 OpMove byte = 0x05 OpLeave byte = 0x06 OpRekey byte = 0x07 OpPrune byte = 0x08 OpVote byte = 0x09 // election mix/commit OpVoteClaim byte = 0x0A // mint this period's vote tokens // 0x0B+ reserved. OpHeader byte = 0xF0 ) // Output as it appears inside a transaction. The spec's Output has a // `time` field; consensus sets it to the block time at processing, so // transactions only carry (amount, owner) and the chain stamps time. type Output struct { Amount *big.Int // real value at creation (block) time Owner PubKey } func encodeOutputs(w *buf, outs []Output) { w.u32(uint32(len(outs))) for i := range outs { w.u128(outs[i].Amount) w.bytes(outs[i].Owner[:]) } } // Tx is anything that can be included in a block. type Tx interface { // ID is the hash identifying the transaction. Signatures are // excluded from the ID (anti-malleability); for single-signer // transactions ID doubles as the signing hash. ID() [32]byte } // ---------------------------------------------------------------- Claim // Claim mints accrued UBI for a tree node with contribution > 0. type Claim struct { Key PubKey Amount *big.Int Nonce uint64 Sig Sig } func (c *Claim) body() []byte { var w buf w.u8(OpClaim) w.bytes(c.Key[:]) w.u128(c.Amount) w.u64(c.Nonce) return w.b } func (c *Claim) ID() [32]byte { return H(c.body()) } func (c *Claim) SigHash() [32]byte { return c.ID() } // -------------------------------------------------------------- Transfer type Outpoint struct { Tx [32]byte Index uint32 } // Transfer spends UTXOs and creates new ones. One signature per input; // inputs may have different owners. sum(outputs) <= sum(input values // at block time); the difference is the fee, minted to the block's // validator. type Transfer struct { Inputs []Outpoint Outputs []Output Sigs []Sig // one per input, over SigHash } func (t *Transfer) body() []byte { var w buf w.u8(OpTransfer) w.u32(uint32(len(t.Inputs))) for i := range t.Inputs { w.bytes(t.Inputs[i].Tx[:]) w.u32(t.Inputs[i].Index) } encodeOutputs(&w, t.Outputs) return w.b } func (t *Transfer) ID() [32]byte { return H(t.body()) } func (t *Transfer) SigHash() [32]byte { return t.ID() } // ----------------------------------------------------------------- Prune // Prune removes expired UTXOs — outputs where spendable(T) ≤ 0 at the // block time. The validator collects the remaining value of each. The // transaction carries no signatures: expiry is verifiable by any node // from the UTXO's own fields and the block time alone, so validity is // objective. Typically produced by the validator itself, but accepted // from anyone (the value goes to the validator either way). type Prune struct { Inputs []Outpoint } func (p *Prune) body() []byte { var w buf w.u8(OpPrune) w.u32(uint32(len(p.Inputs))) for i := range p.Inputs { w.bytes(p.Inputs[i].Tx[:]) w.u32(p.Inputs[i].Index) } return w.b } func (p *Prune) ID() [32]byte { return H(p.body()) } // ------------------------------------------------------------------- Add // NodeTemplate describes a subtree being added. Own is the node's own // full spec node: key, leaf, nonce, last_ubi, last_vote, tree_count, // tree_ubi, children. The person contribution is derived per spec: // own = tree_count − Σ children's tree_count (person=1, group=N, // org=0). On import, consensus supersedes last_ubi and last_vote // (both := block time, per spec) and recomputes tree_ubi; the other // fields are taken as transported. type NodeTemplate struct { Key PubKey Leaf bool Nonce uint64 LastUBI uint64 LastVote uint64 TreeCount uint64 TreeUBI *big.Int // nil is treated as 0 Children []NodeTemplate } func (t *NodeTemplate) treeUBI() *big.Int { if t.TreeUBI == nil { return big.NewInt(0) } return t.TreeUBI } // Hash is the spec's H(Node) of the subtree root, computed recursively // bottom-up — the exact hash function of the people tree itself. func (t *NodeTemplate) Hash() [32]byte { ch := make([][32]byte, 0, len(t.Children)) for i := range t.Children { ch = append(ch, t.Children[i].Hash()) } return nodeHash(t.Key, ch, t.Leaf, t.Nonce, t.LastUBI, t.LastVote, t.TreeCount, t.treeUBI()) } func (t *NodeTemplate) countNodes() int { n := 1 for i := range t.Children { n += t.Children[i].countNodes() } return n } // Add attaches a child subtree under a parent. The parent signs the // operation; the subtree root key signs consent over hash+deadline+parent. type Add struct { Parent PubKey ChildKey PubKey Hash [32]byte Nonce uint64 // parent's nonce Deadline uint64 // block time after which consent expires Consent Sig // by ChildKey over ConsentMsg Sig Sig // by Parent over SigHash // Transport of the subtree data; bound to the tx via Hash. Template NodeTemplate } func (a *Add) body() []byte { var w buf w.u8(OpAdd) w.bytes(a.Parent[:]) w.bytes(a.ChildKey[:]) w.bytes(a.Hash[:]) w.u64(a.Nonce) w.u64(a.Deadline) return w.b } func (a *Add) ID() [32]byte { return H(a.body()) } func (a *Add) SigHash() [32]byte { return a.ID() } // ConsentMsg: child signs hash + deadline + parent pubkey. func ConsentMsg(hash [32]byte, deadline uint64, parent PubKey) []byte { var w buf w.bytes(hash[:]) w.u64(deadline) w.bytes(parent[:]) return w.b } // ---------------------------------------------------------------- Remove // Remove deletes a child and its entire subtree. Accrued UBI for every // person in the subtree is automatically minted to their keys, as // outputs of this transaction in pre-order. type Remove struct { Parent PubKey Child PubKey Nonce uint64 // parent's nonce Sig Sig } func (r *Remove) body() []byte { var w buf w.u8(OpRemove) w.bytes(r.Parent[:]) w.bytes(r.Child[:]) w.u64(r.Nonce) return w.b } func (r *Remove) ID() [32]byte { return H(r.body()) } func (r *Remove) SigHash() [32]byte { return r.ID() } // ----------------------------------------------------------------- Rekey type Rekey struct { Old PubKey New PubKey Nonce uint64 Sig Sig // by Old } func (r *Rekey) body() []byte { var w buf w.u8(OpRekey) w.bytes(r.Old[:]) w.bytes(r.New[:]) w.u64(r.Nonce) return w.b } func (r *Rekey) ID() [32]byte { return H(r.body()) } func (r *Rekey) SigHash() [32]byte { return r.ID() } // ------------------------------------------------------------------ Move // Move transfers a child (and its subtree) from its current parent to // a new parent. State is fully preserved. The old parent is looked up // from the tree. Signed by the new parent; child signs consent with a // deadline. type Move struct { Child PubKey NewParent PubKey Nonce uint64 // new parent's nonce Deadline uint64 Consent Sig // by Child over MoveConsentMsg Sig Sig // by NewParent } func (m *Move) body() []byte { var w buf w.u8(OpMove) w.bytes(m.Child[:]) w.bytes(m.NewParent[:]) w.u64(m.Nonce) w.u64(m.Deadline) return w.b } func (m *Move) ID() [32]byte { return H(m.body()) } func (m *Move) SigHash() [32]byte { return m.ID() } // MoveConsentMsg: child signs deadline + new_parent. func MoveConsentMsg(deadline uint64, newParent PubKey) []byte { var w buf w.u64(deadline) w.bytes(newParent[:]) return w.b } // ----------------------------------------------------------------- Leave // Leave removes the child (and its subtree) from its parent, initiated // by the child. Same effect as Remove: accrued UBI is auto-minted. // The parent is looked up from the tree. type Leave struct { Child PubKey Nonce uint64 // child's nonce Sig Sig } func (l *Leave) body() []byte { var w buf w.u8(OpLeave) w.bytes(l.Child[:]) w.u64(l.Nonce) return w.b } func (l *Leave) ID() [32]byte { return H(l.body()) } func (l *Leave) SigHash() [32]byte { return l.ID() } // ============================================================== tree // PNode is a node in the people tree: a person, a group, or an org. // Own is the node's own person contribution (tree_count minus the sum // of the children's tree_counts, stored explicitly for convenience). // OwnUBI is the node's own normalized UBI base: // // OwnUBI = Normalize(Own × TOKEN, LastUBI, norm_time) // // so that Own×TOKEN×decay^(T−LastUBI) == ValueAt(OwnUBI, T) and the // aggregated TreeUBI matches the spec's tree_ubi. type PNode struct { Key PubKey Leaf bool Nonce uint64 LastUBI uint64 LastVote uint64 // timestamp of last vote claim (set once elections activate) Own uint64 OwnUBI *big.Int Children []*PNode Parent *PNode // Aggregates (cached, recomputed bottom-up). TreeCount uint64 TreeUBI *big.Int hash [32]byte } // nodeHash is the spec's node hash: SHA-256(key || leaf || children || // nonce || last_ubi || last_vote || tree_count || tree_ubi), where // children is the list of the children's node hashes. Shared by the // tree itself and by Add templates so the two can never diverge. func nodeHash(key PubKey, childHashes [][32]byte, leaf bool, nonce, lastUBI, lastVote, treeCount uint64, treeUBI *big.Int) [32]byte { var w buf w.bytes(key[:]) w.boolb(leaf) for i := range childHashes { w.bytes(childHashes[i][:]) } w.u64(nonce) w.u64(lastUBI) w.u64(lastVote) w.u64(treeCount) w.u128(treeUBI) return H(w.b) } // recompute refreshes aggregates and this node's hash from its // children (which must already be up to date). func (n *PNode) recompute() { tc := n.Own tu := new(big.Int).Set(n.OwnUBI) ch := make([][32]byte, 0, len(n.Children)) for _, c := range n.Children { tc += c.TreeCount tu.Add(tu, c.TreeUBI) ch = append(ch, c.hash) } n.TreeCount = tc n.TreeUBI = tu n.hash = nodeHash(n.Key, ch, n.Leaf, n.Nonce, n.LastUBI, n.LastVote, n.TreeCount, n.TreeUBI) } func (n *PNode) Hash() [32]byte { return n.hash } // PeopleTree is the population register. type PeopleTree struct { Root *PNode byKey map[PubKey]*PNode } // NewPeopleTree creates a tree with a single root node (typically a // person with Own=1) whose UBI accrual starts at t0. func NewPeopleTree(rootKey PubKey, own uint64, t0, norm uint64) *PeopleTree { r := &PNode{ Key: rootKey, Own: own, LastUBI: t0, LastVote: t0, // same entry-stamp rule as Add: first vote next period OwnUBI: Normalize(tokens(own), t0, norm), } r.recompute() return &PeopleTree{Root: r, byKey: map[PubKey]*PNode{rootKey: r}} } func (t *PeopleTree) Get(k PubKey) *PNode { return t.byKey[k] } func (t *PeopleTree) RootHash() [32]byte { return t.Root.hash } func (t *PeopleTree) Population() uint64 { return t.Root.TreeCount } // Bubble recomputes aggregates and hashes from n up to the root. func (t *PeopleTree) Bubble(n *PNode) { for ; n != nil; n = n.Parent { n.recompute() } } // UnclaimedAt: tree_count × TOKEN − tree_ubi × decay^(T − norm_time). func (t *PeopleTree) UnclaimedAt(T, norm uint64) *big.Int { total := tokens(t.Root.TreeCount) u := new(big.Int).Sub(total, ValueAt(t.Root.TreeUBI, T, norm)) if u.Sign() < 0 { u.SetInt64(0) } return u } const maxTemplateNodes = 4096 // checkTemplate validates a subtree template against the current tree: // key uniqueness (globally and within the template), leaf consistency, // tree_count consistency (each node's own contribution, tree_count − // Σ children's tree_count, must be ≥ 0), and a sanity cap on size. func (t *PeopleTree) checkTemplate(tpl *NodeTemplate) error { if tpl.countNodes() > maxTemplateNodes { return errors.New("template too large") } seen := map[PubKey]bool{} var walk func(n *NodeTemplate) error walk = func(n *NodeTemplate) error { if seen[n.Key] { return fmt.Errorf("duplicate key in template: %s", n.Key.Short()) } if _, ok := t.byKey[n.Key]; ok { return fmt.Errorf("key already in tree: %s", n.Key.Short()) } seen[n.Key] = true if n.Leaf && len(n.Children) > 0 { return errors.New("leaf node with children") } var sum uint64 for i := range n.Children { if err := walk(&n.Children[i]); err != nil { return err } s2 := sum + n.Children[i].TreeCount if s2 < sum { return fmt.Errorf("tree_count overflow at %s", n.Key.Short()) } sum = s2 } if sum > n.TreeCount { return fmt.Errorf("tree_count %d below children sum %d at %s", n.TreeCount, sum, n.Key.Short()) } return nil } return walk(tpl) } // DoAdd validates and attaches a subtree under parentKey. Imported // fields: key, leaf, nonce and tree_count (own contribution = // tree_count − Σ children's tree_count) come from the template; // last_ubi and last_vote are set to blockTime for every node in the // subtree (per spec) and tree_ubi is recomputed from that (the // transported values are committed to by the hash but superseded by // consensus). The caller is responsible for signature/nonce checks // and for incrementing the parent's nonce BEFORE calling (a single // bubble covers everything). func (t *PeopleTree) DoAdd(parentKey PubKey, tpl *NodeTemplate, blockTime, norm uint64) error { p := t.byKey[parentKey] if p == nil { return errors.New("parent not in tree") } if p.Leaf { return errors.New("parent is a leaf") } if err := t.checkTemplate(tpl); err != nil { return err } var build func(tp *NodeTemplate, parent *PNode) *PNode build = func(tp *NodeTemplate, parent *PNode) *PNode { var sum uint64 for i := range tp.Children { sum += tp.Children[i].TreeCount } own := tp.TreeCount - sum // ≥ 0, ensured by checkTemplate n := &PNode{ Key: tp.Key, Leaf: tp.Leaf, Nonce: tp.Nonce, Own: own, LastUBI: blockTime, LastVote: blockTime, OwnUBI: Normalize(tokens(own), blockTime, norm), Parent: parent, } for i := range tp.Children { n.Children = append(n.Children, build(&tp.Children[i], n)) } n.recompute() t.byKey[n.Key] = n return n } child := build(tpl, p) p.Children = append(p.Children, child) t.Bubble(p) return nil } // RemovedPerson is a person entry collected while removing a subtree, // used to auto-mint accrued UBI. type RemovedPerson struct { Key PubKey Own uint64 LastUBI uint64 } // DoRemove detaches childKey's subtree from parentKey and returns all // persons (Own > 0) in pre-order. The caller increments the parent's // nonce before calling. func (t *PeopleTree) DoRemove(parentKey, childKey PubKey) ([]RemovedPerson, error) { p := t.byKey[parentKey] if p == nil { return nil, errors.New("parent not in tree") } c := t.byKey[childKey] if c == nil { return nil, errors.New("child not in tree") } if c.Parent != p { return nil, errors.New("not a child of parent") } var persons []RemovedPerson var walk func(n *PNode) walk = func(n *PNode) { if n.Own > 0 { persons = append(persons, RemovedPerson{Key: n.Key, Own: n.Own, LastUBI: n.LastUBI}) } delete(t.byKey, n.Key) for _, ch := range n.Children { walk(ch) } } walk(c) for i, ch := range p.Children { if ch == c { p.Children = append(p.Children[:i], p.Children[i+1:]...) break } } c.Parent = nil t.Bubble(p) return persons, nil } // DoRekey changes a node's key. Caller verifies the signature; the // node's nonce is incremented here. func (t *PeopleTree) DoRekey(old, new PubKey) error { n := t.byKey[old] if n == nil { return errors.New("node not in tree") } if _, ok := t.byKey[new]; ok { return errors.New("new key already in tree") } delete(t.byKey, old) n.Key = new n.Nonce++ t.byKey[new] = n t.Bubble(n) return nil } // DoMove detaches childKey from its current parent and attaches it // under newParentKey. All state (last_ubi, nonce, subtree) is preserved. func (t *PeopleTree) DoMove(childKey, newParentKey PubKey) error { c := t.byKey[childKey] if c == nil { return errors.New("child not in tree") } if c.Parent == nil { return errors.New("cannot move the root") } np := t.byKey[newParentKey] if np == nil { return errors.New("new parent not in tree") } if np.Leaf { return errors.New("new parent is a leaf") } // Ensure new parent is not inside child's subtree. for p := np; p != nil; p = p.Parent { if p == c { return errors.New("new parent is inside child's subtree") } } // Detach from old parent. old := c.Parent for i, ch := range old.Children { if ch == c { old.Children = append(old.Children[:i], old.Children[i+1:]...) break } } // Attach to new parent. c.Parent = np np.Children = append(np.Children, c) t.Bubble(old) t.Bubble(np) return nil } // Clone deep-copies the tree. func (t *PeopleTree) Clone() *PeopleTree { m := make(map[PubKey]*PNode, len(t.byKey)) var cp func(n *PNode, parent *PNode) *PNode cp = func(n *PNode, parent *PNode) *PNode { c := &PNode{ Key: n.Key, Leaf: n.Leaf, Nonce: n.Nonce, LastUBI: n.LastUBI, LastVote: n.LastVote, Own: n.Own, OwnUBI: new(big.Int).Set(n.OwnUBI), Parent: parent, TreeCount: n.TreeCount, TreeUBI: new(big.Int).Set(n.TreeUBI), hash: n.hash, } for _, ch := range n.Children { c.Children = append(c.Children, cp(ch, c)) } m[c.Key] = c return c } root := cp(t.Root, nil) return &PeopleTree{Root: root, byKey: m} } // ============================================================== utxo // Entry is an unspent output as stored in the set. Amount and Time // are the spec Output's fields (real value at creation, creation // block time); Norm is the amount normalized to the current norm_time // and is what the trie sums. type Entry struct { Op Outpoint Amount *big.Int Norm *big.Int Time uint64 // creation (block) time Owner PubKey } // Value is the gross value at T: amount × decay^(T − time) / SCALE. func (e *Entry) Value(T uint64) *big.Int { if T < e.Time { panic("Entry.Value: time before creation") } return mulScale(e.Amount, DecayPow(T-e.Time)) } // Rent is the accumulated rent owed at T. Collected by the validator // whenever the UTXO is consumed — spent or pruned. func (e *Entry) Rent(T uint64) *big.Int { return RentOwed(T - e.Time) } // Spendable is the spec's spendable(T) = value − rent_owed. May be // ≤ 0: that is expiry. func (e *Entry) Spendable(T uint64) *big.Int { return new(big.Int).Sub(e.Value(T), e.Rent(T)) } // Expired reports whether spendable(T) ≤ 0. Verifiable by any node // from the entry's own fields and the block time. func (e *Entry) Expired(T uint64) bool { return e.Spendable(T).Sign() <= 0 } // The UTXO commitment is a binary Merkle sum trie keyed by // H(tx_hash || index). Internal nodes sum their children, so the root // carries the total normalized supply. The structure is canonical for // a given key set (independent of insertion order): internal nodes // exist exactly along diverging key prefixes. type tnode struct { leaf bool key [32]byte // leaf only: full key e *Entry // leaf only l, r *tnode // internal only sum *big.Int h [32]byte } func bitAt(k [32]byte, d int) int { return int(k[d>>3]>>(7-uint(d&7))) & 1 } func (n *tnode) fix() { if n.leaf { n.sum = new(big.Int).Set(n.e.Norm) var w buf w.u8(0x00) w.bytes(n.key[:]) w.u128(n.e.Amount) w.u64(n.e.Time) w.bytes(n.e.Owner[:]) n.h = H(w.b) return } n.sum = new(big.Int) lh, rh := zero32, zero32 if n.l != nil { n.sum.Add(n.sum, n.l.sum) lh = n.l.h } if n.r != nil { n.sum.Add(n.sum, n.r.sum) rh = n.r.h } var w buf w.u8(0x01) w.bytes(lh[:]) w.bytes(rh[:]) w.u128(n.sum) n.h = H(w.b) } func leafNode(key [32]byte, e *Entry) *tnode { n := &tnode{leaf: true, key: key, e: e} n.fix() return n } // splitLeaves builds the internal chain from depth d down to the first // bit where the two keys diverge. func splitLeaves(a, b *tnode, d int) *tnode { in := &tnode{} ba, bb := bitAt(a.key, d), bitAt(b.key, d) if ba == bb { c := splitLeaves(a, b, d+1) if ba == 0 { in.l = c } else { in.r = c } } else { if ba == 0 { in.l, in.r = a, b } else { in.l, in.r = b, a } } in.fix() return in } func insertRec(n *tnode, d int, lf *tnode) (*tnode, error) { if n == nil { return lf, nil } if n.leaf { if n.key == lf.key { return n, errors.New("duplicate utxo key") } return splitLeaves(n, lf, d), nil } var err error if bitAt(lf.key, d) == 0 { n.l, err = insertRec(n.l, d+1, lf) } else { n.r, err = insertRec(n.r, d+1, lf) } if err != nil { return n, err } n.fix() return n, nil } // deleteRec removes key and collapses now-redundant internals: when an // internal is left with a single leaf child, the leaf is pulled up. func deleteRec(n *tnode, d int, key [32]byte) (*tnode, *tnode) { if n == nil { return nil, nil } if n.leaf { if n.key == key { return nil, n } return n, nil } var rem *tnode if bitAt(key, d) == 0 { n.l, rem = deleteRec(n.l, d+1, key) } else { n.r, rem = deleteRec(n.r, d+1, key) } if rem == nil { return n, nil } if n.l == nil && n.r == nil { return nil, rem } if n.l == nil && n.r.leaf { return n.r, rem } if n.r == nil && n.l.leaf { return n.l, rem } n.fix() return n, rem } // UTXOSet combines the Merkle sum trie (commitment) with a direct map // (O(1) validation lookups). Both are kept in sync. type UTXOSet struct { root *tnode entries map[Outpoint]*Entry } func NewUTXOSet() *UTXOSet { return &UTXOSet{entries: map[Outpoint]*Entry{}} } func opKey(o Outpoint) [32]byte { var w buf w.bytes(o.Tx[:]) w.u32(o.Index) return H(w.b) } func (u *UTXOSet) Insert(o Outpoint, amount, norm *big.Int, time uint64, owner PubKey) error { if _, ok := u.entries[o]; ok { return errors.New("outpoint already exists") } if amount.Sign() < 0 || amount.BitLen() > 128 { return errors.New("amount out of range") } if norm.Sign() < 0 || norm.BitLen() > 128 { return errors.New("normalized amount out of range") } e := &Entry{Op: o, Amount: new(big.Int).Set(amount), Norm: new(big.Int).Set(norm), Time: time, Owner: owner} nr, err := insertRec(u.root, 0, leafNode(opKey(o), e)) if err != nil { return err } u.root = nr u.entries[o] = e return nil } func (u *UTXOSet) Get(o Outpoint) *Entry { return u.entries[o] } func (u *UTXOSet) Spend(o Outpoint) (*Entry, error) { e, ok := u.entries[o] if !ok { return nil, errors.New("output missing or already spent") } nr, rem := deleteRec(u.root, 0, opKey(o)) if rem == nil { return nil, errors.New("trie desync") } u.root = nr delete(u.entries, o) return e, nil } // Sum is the total normalized supply. Real supply at T is // ValueAt(Sum(), T, normTime). func (u *UTXOSet) Sum() *big.Int { if u.root == nil { return new(big.Int) } return new(big.Int).Set(u.root.sum) } func (u *UTXOSet) Root() [32]byte { if u.root == nil { return zero32 } return u.root.h } func (u *UTXOSet) Len() int { return len(u.entries) } func (u *UTXOSet) ForEach(f func(*Entry)) { for _, e := range u.entries { f(e) } } // Clone deep-copies the set. Trie structure is canonical for a key // set, so rebuilding by re-insertion yields an identical root. func (u *UTXOSet) Clone() *UTXOSet { c := NewUTXOSet() for _, e := range u.entries { if err := c.Insert(e.Op, e.Amount, e.Norm, e.Time, e.Owner); err != nil { panic(err) // impossible: keys unique } } return c } // ============================================================= state const ( maxOutputs = 1024 maxInputs = 1024 ) // State is the full chain state between blocks. type State struct { NormTime uint64 // reference time for normalized values: latest 4×YEAR boundary ≤ t0 Time uint64 // time of the last applied block Seq uint64 // seq of the last applied block: slots since genesis, (Time − Genesis) / slot Genesis uint64 // genesis block time (on the Unix 60 s grid) LastHash [32]byte Rand [32]byte // last header's rand; seeds the next selection Tree *PeopleTree UTXO *UTXOSet Election *VoteTrie // active: selects one validator per slot NextElection *VoteTrie // being built; activates at the period boundary } // seqAt is the spec's header seq for a block at time T: slots since // genesis. T must be on the slot grid and ≥ Genesis. func (s *State) seqAt(T uint64) uint64 { return (T - s.Genesis) / SlotSeconds } func (s *State) Clone() *State { c := *s c.Tree = s.Tree.Clone() c.UTXO = s.UTXO.Clone() c.Election = s.Election.Clone() c.NextElection = s.NextElection.Clone() return &c } // checkOutputs validates transaction outputs: positive amounts within // u128, sane count. Returns the sum of amounts. func checkOutputs(outs []Output, allowEmpty bool) (*big.Int, error) { if len(outs) > maxOutputs { return nil, errors.New("too many outputs") } if len(outs) == 0 && !allowEmpty { return nil, errors.New("no outputs") } sum := new(big.Int) for i := range outs { a := outs[i].Amount if a == nil || a.Sign() <= 0 || a.BitLen() > 128 { return nil, errors.New("invalid output amount") } sum.Add(sum, a) } if sum.BitLen() > 128 { return nil, errors.New("output sum overflow") } return sum, nil } // mintOutputs inserts outs as (txid, i) at block time T. func (s *State) mintOutputs(txid [32]byte, outs []Output, T uint64) error { for i := range outs { norm := Normalize(outs[i].Amount, T, s.NormTime) if err := s.UTXO.Insert(Outpoint{Tx: txid, Index: uint32(i)}, outs[i].Amount, norm, T, outs[i].Owner); err != nil { return err } } return nil } // ----------------------------------------------------------------- Claim // applyClaim returns a fee: claimable(T) minus the claimed amount. // Claiming always consumes the full accrual — the single output is // owned by the claimer's own tree key, and whatever is not taken goes // to the validator. // Outputs may sum to LESS than claimable — required for live drift, // since claimable grows every second and a claim is signed before its // block time is known. last_ubi advances to T regardless, so the // remainder is not claimable again; it goes to the block's fee output. func (s *State) applyClaim(c *Claim, T uint64) (*big.Int, error) { n := s.Tree.Get(c.Key) if n == nil { return nil, errors.New("claim: key not in tree") } if n.Own == 0 { return nil, errors.New("claim: node has no person contribution") } if c.Nonce != n.Nonce { return nil, fmt.Errorf("claim: bad nonce (have %d want %d)", c.Nonce, n.Nonce) } sh := c.SigHash() if !VerifySig(c.Key, sh[:], c.Sig) { return nil, errors.New("claim: bad signature") } want := ClaimableAt(n.Own, n.LastUBI, T) if want.Sign() == 0 { return nil, errors.New("claim: nothing claimable") } if c.Amount == nil || c.Amount.Sign() <= 0 { return nil, errors.New("claim: bad amount") } if c.Amount.Cmp(want) > 0 { return nil, fmt.Errorf("claim: amount %s exceeds claimable %s", c.Amount, want) } if err := s.mintOutputs(c.ID(), []Output{{Amount: c.Amount, Owner: c.Key}}, T); err != nil { return nil, fmt.Errorf("claim: %w", err) } n.LastUBI = T n.OwnUBI = Normalize(tokens(n.Own), T, s.NormTime) n.Nonce++ s.Tree.Bubble(n) return new(big.Int).Sub(want, c.Amount), nil } // -------------------------------------------------------------- Transfer // applyTransfer returns everything the validator collects: the fee // (Σ input spendable − Σ outputs) plus rent_owed on each input, which // together equal Σ input gross value − Σ outputs. Validation per // spec: sum of output amounts ≤ sum of input spendable values at // block time, where spendable = value − rent_owed. func (s *State) applyTransfer(t *Transfer, T uint64) (*big.Int, error) { if len(t.Inputs) == 0 || len(t.Inputs) > maxInputs { return nil, errors.New("transfer: bad input count") } if len(t.Sigs) != len(t.Inputs) { return nil, errors.New("transfer: need one signature per input") } seen := map[Outpoint]bool{} entries := make([]*Entry, len(t.Inputs)) for i, op := range t.Inputs { if seen[op] { return nil, errors.New("transfer: duplicate input") } seen[op] = true e := s.UTXO.Get(op) if e == nil { return nil, errors.New("transfer: input missing or spent") } entries[i] = e } sh := t.SigHash() for i, e := range entries { if !VerifySig(e.Owner, sh[:], t.Sigs[i]) { return nil, fmt.Errorf("transfer: bad signature for input %d", i) } } outSum, err := checkOutputs(t.Outputs, true) if err != nil { return nil, fmt.Errorf("transfer: %w", err) } inValue := new(big.Int) // Σ gross values inSpendable := new(big.Int) // Σ (value − rent_owed) for _, e := range entries { inValue.Add(inValue, e.Value(T)) inSpendable.Add(inSpendable, e.Spendable(T)) } if outSum.Cmp(inSpendable) > 0 { return nil, fmt.Errorf("transfer: outputs %s exceed spendable %s", outSum, inSpendable) } for _, op := range t.Inputs { if _, err := s.UTXO.Spend(op); err != nil { return nil, err } } if err := s.mintOutputs(t.ID(), t.Outputs, T); err != nil { return nil, err } // fee + collected rent = gross − outputs (≥ 0 by the check above). return new(big.Int).Sub(inValue, outSum), nil } // ----------------------------------------------------------------- Prune // applyPrune removes UTXOs whose spendable(T) ≤ 0 and returns their // remaining gross value, collected by the validator. Expiry is // verified per entry from its own fields and the block time. func (s *State) applyPrune(p *Prune, T uint64) (*big.Int, error) { if len(p.Inputs) == 0 || len(p.Inputs) > maxInputs { return nil, errors.New("prune: bad input count") } seen := map[Outpoint]bool{} collected := new(big.Int) for _, op := range p.Inputs { if seen[op] { return nil, errors.New("prune: duplicate input") } seen[op] = true e := s.UTXO.Get(op) if e == nil { return nil, errors.New("prune: output missing or spent") } if !e.Expired(T) { return nil, fmt.Errorf("prune: output not expired (spendable %s)", e.Spendable(T)) } collected.Add(collected, e.Value(T)) } for _, op := range p.Inputs { if _, err := s.UTXO.Spend(op); err != nil { return nil, err } } return collected, nil } // ------------------------------------------------------------------- Add func (s *State) applyAdd(a *Add, T uint64) error { p := s.Tree.Get(a.Parent) if p == nil { return errors.New("add: parent not in tree") } if a.Nonce != p.Nonce { return fmt.Errorf("add: bad nonce (have %d want %d)", a.Nonce, p.Nonce) } if T > a.Deadline { return errors.New("add: consent expired") } sh := a.SigHash() if !VerifySig(a.Parent, sh[:], a.Sig) { return errors.New("add: bad parent signature") } if a.Template.Key != a.ChildKey { return errors.New("add: template root key mismatch") } if a.Template.Hash() != a.Hash { return errors.New("add: template hash mismatch") } if !VerifySig(a.ChildKey, ConsentMsg(a.Hash, a.Deadline, a.Parent), a.Consent) { return errors.New("add: bad child consent") } p.Nonce++ if err := s.Tree.DoAdd(a.Parent, &a.Template, T, s.NormTime); err != nil { return fmt.Errorf("add: %w", err) } return nil } // ---------------------------------------------------------------- Remove func (s *State) applyRemove(r *Remove, T uint64) error { p := s.Tree.Get(r.Parent) if p == nil { return errors.New("remove: parent not in tree") } if r.Nonce != p.Nonce { return fmt.Errorf("remove: bad nonce (have %d want %d)", r.Nonce, p.Nonce) } sh := r.SigHash() if !VerifySig(r.Parent, sh[:], r.Sig) { return errors.New("remove: bad signature") } p.Nonce++ persons, err := s.Tree.DoRemove(r.Parent, r.Child) if err != nil { return fmt.Errorf("remove: %w", err) } // Auto-mint accrued UBI to each removed person's key, as outputs // of this transaction, in pre-order. Zero-claimable persons // (added at this very block time) are skipped. txid := r.ID() idx := uint32(0) for _, pr := range persons { amt := ClaimableAt(pr.Own, pr.LastUBI, T) if amt.Sign() == 0 { continue } norm := Normalize(amt, T, s.NormTime) if err := s.UTXO.Insert(Outpoint{Tx: txid, Index: idx}, amt, norm, T, pr.Key); err != nil { return fmt.Errorf("remove mint: %w", err) } idx++ } return nil } // ----------------------------------------------------------------- Rekey func (s *State) applyRekey(r *Rekey, T uint64) error { n := s.Tree.Get(r.Old) if n == nil { return errors.New("rekey: node not in tree") } if r.Nonce != n.Nonce { return fmt.Errorf("rekey: bad nonce (have %d want %d)", r.Nonce, n.Nonce) } sh := r.SigHash() if !VerifySig(r.Old, sh[:], r.Sig) { return errors.New("rekey: bad signature") } return s.Tree.DoRekey(r.Old, r.New) } // ------------------------------------------------------------------ Move func (s *State) applyMove(m *Move, T uint64) error { c := s.Tree.Get(m.Child) if c == nil { return errors.New("move: child not in tree") } if c.Parent == nil { return errors.New("move: cannot move the root") } np := s.Tree.Get(m.NewParent) if np == nil { return errors.New("move: new parent not in tree") } if m.Nonce != np.Nonce { return fmt.Errorf("move: bad nonce (have %d want %d)", m.Nonce, np.Nonce) } if T > m.Deadline { return errors.New("move: consent expired") } sh := m.SigHash() if !VerifySig(m.NewParent, sh[:], m.Sig) { return errors.New("move: bad new parent signature") } if !VerifySig(m.Child, MoveConsentMsg(m.Deadline, m.NewParent), m.Consent) { return errors.New("move: bad child consent") } np.Nonce++ return s.Tree.DoMove(m.Child, m.NewParent) } // ----------------------------------------------------------------- Leave func (s *State) applyLeave(l *Leave, T uint64) error { c := s.Tree.Get(l.Child) if c == nil { return errors.New("leave: child not in tree") } if c.Parent == nil { return errors.New("leave: root cannot leave") } if l.Nonce != c.Nonce { return fmt.Errorf("leave: bad nonce (have %d want %d)", l.Nonce, c.Nonce) } sh := l.SigHash() if !VerifySig(l.Child, sh[:], l.Sig) { return errors.New("leave: bad signature") } parent := c.Parent persons, err := s.Tree.DoRemove(parent.Key, l.Child) if err != nil { return fmt.Errorf("leave: %w", err) } txid := l.ID() idx := uint32(0) for _, pr := range persons { amt := ClaimableAt(pr.Own, pr.LastUBI, T) if amt.Sign() == 0 { continue } norm := Normalize(amt, T, s.NormTime) if err := s.UTXO.Insert(Outpoint{Tx: txid, Index: idx}, amt, norm, T, pr.Key); err != nil { return fmt.Errorf("leave mint: %w", err) } idx++ } return nil } // ApplyTxs applies transactions in order and returns total fees. func (s *State) ApplyTxs(txs []Tx, T uint64) (*big.Int, error) { fees := new(big.Int) for i, tx := range txs { var err error switch v := tx.(type) { case *Claim: var fee *big.Int fee, err = s.applyClaim(v, T) if err == nil { fees.Add(fees, fee) } case *Transfer: var fee *big.Int fee, err = s.applyTransfer(v, T) if err == nil { fees.Add(fees, fee) } case *Prune: var fee *big.Int fee, err = s.applyPrune(v, T) if err == nil { fees.Add(fees, fee) } case *Add: err = s.applyAdd(v, T) case *Remove: err = s.applyRemove(v, T) case *Rekey: err = s.applyRekey(v, T) case *Move: err = s.applyMove(v, T) case *Leave: err = s.applyLeave(v, T) case *Vote: err = s.applyVote(v, T) case *VoteClaim: err = s.applyVoteClaim(v, T) default: err = errors.New("unknown tx type") } if err != nil { return nil, fmt.Errorf("tx %d: %w", i, err) } } return fees, nil } // FeeOutpoint identifies the synthetic per-block fee output. The spec // says fees are collected by the validator but leaves the mechanism // open; we mint a single output per block with a deterministic // pseudo-txid derived from the block's seq (slots since genesis — // unique, since block times strictly increase on the slot grid). func FeeOutpoint(seq uint64) Outpoint { var w buf w.bytes([]byte("fee")) w.u64(seq) return Outpoint{Tx: H(w.b), Index: 0} } // SupplyAt is the real UTXO supply at time T from the trie root: // total(T) = sum × decay^(T − norm_time). Includes uncollected rent — // rent does not destroy value, it relocates it to the validator upon // collection, so the invariant total + unclaimed = population × TOKEN // holds regardless of how much rent is outstanding. func (s *State) SupplyAt(T uint64) *big.Int { return ValueAt(s.UTXO.Sum(), T, s.NormTime) } // UnclaimedAt is the unclaimed UBI at time T from the tree root. func (s *State) UnclaimedAt(T uint64) *big.Int { return s.Tree.UnclaimedAt(T, s.NormTime) } // ============================================================= block // Header per spec: // // seq, time, people_tree, utxo_trie, election_trie, next_election, // prev, validator, rand, sig // // seq starts at 0 at genesis and increments by 1 per slot — NOT a // consecutive block counter: skipped slots leave gaps in the seq // series, and time = genesis_time + seq × slot always. The slot's // validator signs; the rand field carries the chained onion reveal. type Header struct { Seq uint64 // slots since genesis, gaps where slots were skipped Time uint64 // genesis_time + seq × slot PeopleTree [32]byte UTXOTrie [32]byte ElectionTrie [32]byte // root of the active election trie NextElection [32]byte // root of the trie being built this period Prev [32]byte Validator PubKey Rand [32]byte // selection rand XOR revealed onion layer Sig Sig } func (h *Header) encode(withSig bool) []byte { var w buf w.u8(OpHeader) w.u64(h.Seq) w.u64(h.Time) w.bytes(h.PeopleTree[:]) w.bytes(h.UTXOTrie[:]) w.bytes(h.ElectionTrie[:]) w.bytes(h.NextElection[:]) w.bytes(h.Prev[:]) w.bytes(h.Validator[:]) w.bytes(h.Rand[:]) if withSig { w.bytes(h.Sig[:]) } return w.b } // SigHash is what the validator signs (opcode 0xF0 included, the // signature excluded). func (h *Header) SigHash() [32]byte { return H(h.encode(false)) } // Hash identifies the block (signature included). func (h *Header) Hash() [32]byte { return H(h.encode(true)) } // Block is a header plus the transactions that produce its state. // There is no tx root in the spec header: the block commits to the // *resulting* state, and verification re-executes the transactions. type Block struct { Header Header Txs []Tx } // preSelect runs the parts of the transition that precede the reveal: // slot-grid checks, period activation, skipped-slot rand folding, and // the positional selection of the slot's committed vote entry. Both // builder and verifier start here; the builder needs the selected // entry's commit to look up its reveal, the verifier to check the one // implied by the header. func preSelect(prev *State, T uint64) (*State, [32]byte, *VoteEntry, error) { if T%SlotSeconds != 0 { return nil, zero32, nil, errors.New("block time not on the slot grid") } if T <= prev.Time { return nil, zero32, nil, errors.New("block time not after previous block") } ns := prev.Clone() advancePeriods(ns, prev.Time, T) r := selRand(ns.Rand, prev.Seq, prev.seqAt(T)) entry := ns.Election.SelectRand(r) if entry == nil { return nil, zero32, nil, errors.New("no committed votes in election trie") } return ns, r, entry, nil } // finish is the shared tail: verify the reveal against the selected // commit, walk the onion one layer down in the trie, chain the rand, // apply the transactions, and mint the fee output to the slot's // validator. func finish(ns *State, txs []Tx, T uint64, validator PubKey, r, reveal [32]byte, entry *VoteEntry) error { if H(validator[:], reveal[:]) != entry.Commit { return errors.New("reveal does not match the selected commit") } if err := ns.Election.UpdateCommit(entry.Op, reveal); err != nil { return err } ns.Rand = xor32(r, reveal) fees, err := ns.ApplyTxs(txs, T) if err != nil { return err } if fees.Sign() > 0 { norm := Normalize(fees, T, ns.NormTime) if err := ns.UTXO.Insert(FeeOutpoint(ns.seqAt(T)), fees, norm, T, validator); err != nil { return fmt.Errorf("fee mint: %w", err) } } return nil } // BuildBlock executes txs on top of prev at slot time T and produces // a signed block plus the resulting state. The producer may hold // several onions (the two genesis chains, plus one per vote // committed for it); the selected entry's commit picks the one that // can reveal. It fails if no given onion matches — someone else's // slot — or the matching onion is exhausted; the caller treats that // as a skipped slot. func BuildBlock(prev *State, txs []Tx, T uint64, valPriv ed25519.PrivateKey, onions []*Onion) (*Block, *State, error) { pub := Pub(valPriv) ns, r, entry, err := preSelect(prev, T) if err != nil { return nil, nil, err } var reveal [32]byte ok := false for _, o := range onions { if rv, hit := o.Reveal(entry.Commit); hit { reveal, ok = rv, true break } } if !ok { return nil, nil, fmt.Errorf("build: slot %d not ours (or onion exhausted)", slotOf(T)) } if err := finish(ns, txs, T, pub, r, reveal, entry); err != nil { return nil, nil, err } h := Header{ Seq: prev.seqAt(T), Time: T, PeopleTree: ns.Tree.RootHash(), UTXOTrie: ns.UTXO.Root(), ElectionTrie: ns.Election.Root(), NextElection: ns.NextElection.Root(), Prev: prev.LastHash, Validator: pub, Rand: ns.Rand, } sh := h.SigHash() h.Sig = Sign(valPriv, sh[:]) ns.Seq = h.Seq ns.Time = T ns.LastHash = h.Hash() return &Block{Header: h, Txs: txs}, ns, nil } // VerifyBlock checks b against prev and, on success, returns the new // state. The revealed onion layer is recovered from the header: // reveal = selection_rand XOR header.rand. Verification re-executes // the transactions and requires the header roots to match exactly. func VerifyBlock(prev *State, b *Block) (*State, error) { h := &b.Header // seq counts slots since genesis; gaps are skipped slots. Strict // growth follows from preSelect's grid + monotonicity checks. if h.Seq != prev.seqAt(h.Time) { return nil, fmt.Errorf("verify: seq %d is not slots-since-genesis %d", h.Seq, prev.seqAt(h.Time)) } if h.Prev != prev.LastHash { return nil, errors.New("verify: prev hash mismatch") } ns, r, entry, err := preSelect(prev, h.Time) if err != nil { return nil, fmt.Errorf("verify: %w", err) } reveal := xor32(r, h.Rand) if err := finish(ns, b.Txs, h.Time, h.Validator, r, reveal, entry); err != nil { return nil, fmt.Errorf("verify: %w", err) } sh := h.SigHash() if !VerifySig(h.Validator, sh[:], h.Sig) { return nil, errors.New("verify: bad validator signature") } if ns.Tree.RootHash() != h.PeopleTree { return nil, errors.New("verify: people_tree root mismatch") } if ns.UTXO.Root() != h.UTXOTrie { return nil, errors.New("verify: utxo_trie root mismatch") } if ns.Election.Root() != h.ElectionTrie { return nil, errors.New("verify: election_trie root mismatch") } if ns.NextElection.Root() != h.NextElection { return nil, errors.New("verify: next_election root mismatch") } ns.Seq = h.Seq ns.Time = h.Time ns.LastHash = h.Hash() return ns, nil } // Chain holds the current state, the block history, and the finality // snapshot the spec's fork choice pivots on: blocks before the // previous election period boundary are final, so Final is the state // after the last final block and everything above it is the reorg // window (at most two periods deep). Competing branches enter through // TryAdopt. type Chain struct { State *State Blocks []*Block Final *State // state after Blocks[finalIdx]; never reorged finalIdx int // Blocks[0..finalIdx] are final } // GenesisVoteOutpoint identifies the committed votes genesis seeds // the election tries with: index 0 sits in election_trie (validates // the remainder of the genesis period), index 1 in next_election // (validates the entire following period — it stands in for the root // person's first vote claim, which NewPeopleTree marks as spent by // stamping last_vote = t0). Activation being unconditional, this is // what guarantees a selectable validator until the population's own // votes, cast during the period after genesis, take over at the // second boundary — whatever phase genesis lands in. func GenesisVoteOutpoint(index uint32) Outpoint { return Outpoint{Tx: H([]byte("genesis vote")), Index: index} } // NewChain creates genesis at t0 (aligned down to the slot grid): a // people tree with a single root person, an empty UTXO set, and both // election tries seeded with one committed vote each — commit0 in // the active trie, commit1 in next_election. The two commits must // top independent onions: layers reveal downward and any exposed // layer derives everything above it, so sharing a chain would make // the second period's reveals publicly computable. The genesis // validator is Pub(valPriv); it stays the slot validator exactly as // long as its entries are the only committed votes. func NewChain(rootKey PubKey, t0 uint64, valPriv ed25519.PrivateKey, commit0, commit1 [32]byte) (*Chain, error) { valKey := Pub(valPriv) t0 -= t0 % SlotSeconds norm := normTimeFor(t0) st := &State{ NormTime: norm, Time: t0, Seq: 0, Genesis: t0, Tree: NewPeopleTree(rootKey, 1, t0, norm), UTXO: NewUTXOSet(), Election: NewVoteTrie(), NextElection: NewVoteTrie(), } if err := st.Election.Insert(&VoteEntry{ Op: GenesisVoteOutpoint(0), Committed: true, Commit: commit0, }); err != nil { return nil, err } if err := st.NextElection.Insert(&VoteEntry{ Op: GenesisVoteOutpoint(1), Committed: true, Commit: commit1, }); err != nil { return nil, err } h := Header{ Seq: 0, Time: t0, PeopleTree: st.Tree.RootHash(), UTXOTrie: st.UTXO.Root(), ElectionTrie: st.Election.Root(), NextElection: st.NextElection.Root(), Validator: valKey, } sh := h.SigHash() h.Sig = Sign(valPriv, sh[:]) st.LastHash = h.Hash() return &Chain{State: st, Blocks: []*Block{{Header: h}}, Final: st.Clone(), finalIdx: 0}, nil } // Produce builds the next block, self-verifies it against the current // state (builder and verifier must agree bit for bit), and advances // the chain. func (c *Chain) Produce(txs []Tx, T uint64, valPriv ed25519.PrivateKey, onions []*Onion) (*Block, error) { b, ns, err := BuildBlock(c.State, txs, T, valPriv, onions) if err != nil { return nil, err } vs, err := VerifyBlock(c.State, b) if err != nil { return nil, fmt.Errorf("self-verify failed: %w", err) } if vs.LastHash != ns.LastHash { return nil, errors.New("self-verify: state divergence") } c.State = ns c.Blocks = append(c.Blocks, b) c.advanceFinality() return b, nil } // horizon is the finality cutoff: the period boundary BEFORE the one // the tip sits in. Blocks with time < horizon are final. For a chain // younger than a full period the horizon saturates at 0 — nothing but // genesis (fixed by hash) is final yet. func (c *Chain) horizon() uint64 { p := periodStart(c.State.Time) if p < PeriodSeconds { return 0 } return p - PeriodSeconds } // advanceFinality folds newly-final blocks into the Final snapshot. // Amortized O(1) per block: each block is folded exactly once. func (c *Chain) advanceFinality() { h := c.horizon() for c.finalIdx+1 < len(c.Blocks) && c.Blocks[c.finalIdx+1].Header.Time < h { ns, err := VerifyBlock(c.Final, c.Blocks[c.finalIdx+1]) if err != nil { panic("finality replay diverged: " + err.Error()) // cannot happen: block was verified on append } c.Final = ns c.finalIdx++ } } // stateAt returns the state after Blocks[i] (i ≥ finalIdx), replaying // from the finality snapshot when needed. The tip is free; anything // else costs at most a reorg window of re-verification. func (c *Chain) stateAt(i int) (*State, error) { if i == len(c.Blocks)-1 { return c.State, nil } st := c.Final.Clone() for j := c.finalIdx + 1; j <= i; j++ { ns, err := VerifyBlock(st, c.Blocks[j]) if err != nil { return nil, err } st = ns } return st, nil } // TipHash identifies the current tip block. func (c *Chain) TipHash() [32]byte { return c.State.LastHash } // TryAdopt evaluates a competing branch. branch[0].Prev must name a // block in the reorg window (fork points below the finality horizon // are refused — the spec's "no reorg is accepted past that point"). // // Fork choice per spec: "the chain with the fewest skipped slots // relative to its length wins." Both chains share genesis, so at any // evaluation instant they span the same slots-since-genesis, and // skipped = elapsed − produced with a common `elapsed`: minimizing // skips is exactly maximizing the block count. Ties keep the current // chain (first seen); the next produced block breaks them. // // On success the branch replaces everything above the fork point and // the index of the fork block is returned so the caller can truncate // its log to match. The branch is fully re-verified before anything // is touched. func (c *Chain) TryAdopt(branch []*Block) (int, error) { if len(branch) == 0 { return 0, errors.New("adopt: empty branch") } forkIdx := -1 for i := len(c.Blocks) - 1; i >= c.finalIdx; i-- { if c.Blocks[i].Header.Hash() == branch[0].Header.Prev { forkIdx = i break } } if forkIdx < 0 { return 0, errors.New("adopt: fork point unknown or below the finality horizon") } if forkIdx+1+len(branch) <= len(c.Blocks) { return 0, fmt.Errorf("adopt: branch has %d blocks from the fork, ours has %d — not strictly better", len(branch), len(c.Blocks)-forkIdx-1) } st, err := c.stateAt(forkIdx) if err != nil { return 0, err } st = st.Clone() for _, b := range branch { ns, err := VerifyBlock(st, b) if err != nil { return 0, fmt.Errorf("adopt: %w", err) } st = ns } c.Blocks = append(c.Blocks[:forkIdx+1], branch...) c.State = st c.advanceFinality() return forkIdx, nil } // ========================================================== election // The election lifecycle from the spec: vote tokens are claimed from // tree contribution, mixed and committed into next_election during // the open half of each period (each entry carrying a mixed counter // whose MaxMix cap bounds the free churn), and at the period boundary // the committed set becomes the active election_trie. Every slot, `rand // mod committed` selects one committed entry; whoever can reveal the // preimage layer of that entry's hash-onion commit is the slot's // validator. Skipped slots fold H(seq) of each missed slot into rand // so a dead selection cannot stall the RNG. // // The network layer lives in the server section: fork choice by // fewest skipped slots (Chain.TryAdopt), finality at the previous // period boundary (Chain.Final), and block push + poll sync over // HTTP between statically configured peers. // ------------------------------------------------------- slot / period // slotOf and slotTime index the absolute Unix-0-anchored 60 s grid — // used for grid-alignment checks and the production loop. Header seq // is NOT this index: per spec it counts slots since genesis // (seq = (time − genesis_time) / slot, gaps where slots were // skipped). Period boundaries are Unix-0-aligned and must land on // slots, which is why genesis time itself must sit on this grid. func slotOf(t uint64) uint64 { return t / SlotSeconds } func slotTime(s uint64) uint64 { return s * SlotSeconds } // periodStart is the start of the election period containing t. func periodStart(t uint64) uint64 { return t - t%PeriodSeconds } // phaseOpen reports whether next_election is open at time t: the // first half of the period. At the midpoint it locks; the lock-to- // activation gap is what makes the RNG's future selections // unpredictable to voters. func phaseOpen(t uint64) bool { return t%PeriodSeconds < PeriodSeconds/2 } // advancePeriods applies every period boundary crossed in (from, to]: // the committed next_election becomes the active election trie and a // fresh next_election opens — unconditionally. Voting is required: if // nothing was committed during a period, the boundary activates an // empty trie, preSelect can never pick a validator again, and the // chain halts for good (no blocks means no way to commit votes // either). Bootstrap survives this because genesis seeds // next_election too — the root's first claim, pre-placed — so the // first election the population must fill is the one built during // the period after genesis. Uncommitted leftovers are discarded at // every boundary: vote tokens are per-period. func advancePeriods(ns *State, from, to uint64) { for b := periodStart(from) + PeriodSeconds; b <= to; b += PeriodSeconds { ns.Election = ns.NextElection ns.NextElection = NewVoteTrie() } } // hashU64 is the spec's H(seq) for skipped-slot rand mixing — seq // being the skipped slot's number. func hashU64(x uint64) [32]byte { var w buf w.u64(x) return H(w.b) } func xor32(a, b [32]byte) (o [32]byte) { for i := range o { o[i] = a[i] ^ b[i] } return } // selRand accumulates the selection rand for a block at slot `to`, // given the previous block's rand and slot: every seq strictly // between them was a skipped slot and folds in H(seq). Linear in the // gap; a year offline is ~526k hashes, milliseconds. func selRand(prevRand [32]byte, prevSlot, to uint64) [32]byte { r := prevRand for s := prevSlot + 1; s < to; s++ { r = xor32(r, hashU64(s)) } return r } // --------------------------------------------------------- hash onion // OnionCommit builds the top of a hash onion for a candidate: // o_0 = seed, o_i = H(candidate || o_{i-1}), commit = o_depth. Each // reveal walks one layer down; depth is how many slots the entry can // validate. func OnionCommit(candidate PubKey, seed [32]byte, depth uint64) [32]byte { o := seed for i := uint64(0); i < depth; i++ { o = H(candidate[:], o[:]) } return o } // Onion is the producer side: it can find the layer below any commit // on its chain. Checkpoints every onionStride layers bound the work // per reveal; position discovery for an unknown commit is a linear // scan (done once, then tracked). const onionStride = 4096 type Onion struct { Candidate PubKey Seed [32]byte Depth uint64 cps [][32]byte // cps[j] = layer at position j*onionStride pos uint64 // cached position of the last matched commit posValid bool } func NewOnion(candidate PubKey, seed [32]byte, depth uint64) *Onion { o := &Onion{Candidate: candidate, Seed: seed, Depth: depth} l := seed o.cps = append(o.cps, l) for i := uint64(1); i <= depth; i++ { l = H(candidate[:], l[:]) if i%onionStride == 0 { o.cps = append(o.cps, l) } } return o } // layerAt recomputes layer i from the nearest checkpoint at or below. func (o *Onion) layerAt(i uint64) [32]byte { j := i / onionStride l := o.cps[j] for p := j * onionStride; p < i; p++ { l = H(o.Candidate[:], l[:]) } return l } // Commit is the onion top — what goes into the committed vote entry. func (o *Onion) Commit() [32]byte { return o.layerAt(o.Depth) } // Reveal returns the layer directly below `current`, or false if // `current` is not on this onion or is the seed itself (exhausted: // every layer has been revealed). Position recovery is a single // sequential walk — O(depth) hashes — so even million-layer onions // recover in milliseconds; the common case is the cached position. func (o *Onion) Reveal(current [32]byte) ([32]byte, bool) { if !o.posValid || o.layerAt(o.pos) != current { o.posValid = false l := o.Seed for i := uint64(0); ; i++ { if l == current { o.pos, o.posValid = i, true break } if i == o.Depth { break } l = H(o.Candidate[:], l[:]) } if !o.posValid { return zero32, false } } if o.pos == 0 { return zero32, false // exhausted } r := o.layerAt(o.pos - 1) o.pos-- // the commit becomes r once the block applies return r, true } // ---------------------------------------------------------- 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}, spendable into new entries — mixed starts at 0 conceptually // (a VoteClaim input counts as 0) and every hop pays at least one // unit, capped at MaxMix. Committed: {commit, owner?}, amount 1, // locked until the trie is discarded at a period boundary; per the // spec a committed entry does not store mixed (the transaction merely // declares it as the final paid hop). type VoteEntry struct { Op Outpoint Committed bool Amount uint64 // uncommitted only (committed is implicitly 1) Owner PubKey // uncommitted: required; committed: fee-share hint HasOwner bool // committed only Mixed uint32 // stored on uncommitted entries only: 1..MaxMix Commit [32]byte } // vnode mirrors tnode with a committed-entry count as the aggregate — // the cumulative sums the spec's positional selection traverses. type vnode struct { leaf bool key [32]byte e *VoteEntry l, r *vnode count uint64 h [32]byte } func (n *vnode) fix() { if n.leaf { var w buf if n.e.Committed { n.count = 1 w.u8(0x02) w.bytes(n.key[:]) w.bytes(n.e.Commit[:]) w.boolb(n.e.HasOwner) w.bytes(n.e.Owner[:]) } else { n.count = 0 w.u8(0x03) w.bytes(n.key[:]) w.u64(n.e.Amount) w.bytes(n.e.Owner[:]) w.u32(n.e.Mixed) } n.h = H(w.b) return } n.count = 0 lh, rh := zero32, zero32 if n.l != nil { n.count += n.l.count lh = n.l.h } if n.r != nil { n.count += n.r.count rh = n.r.h } var w buf w.u8(0x04) w.bytes(lh[:]) w.bytes(rh[:]) w.u64(n.count) n.h = H(w.b) } func vLeaf(key [32]byte, e *VoteEntry) *vnode { n := &vnode{leaf: true, key: key, e: e} n.fix() return n } func vSplit(a, b *vnode, d int) *vnode { in := &vnode{} ba, bb := bitAt(a.key, d), bitAt(b.key, d) if ba == bb { c := vSplit(a, b, d+1) if ba == 0 { in.l = c } else { in.r = c } } else { if ba == 0 { in.l, in.r = a, b } else { in.l, in.r = b, a } } in.fix() return in } func vInsert(n *vnode, d int, lf *vnode) (*vnode, error) { if n == nil { return lf, nil } if n.leaf { if n.key == lf.key { return n, errors.New("duplicate vote entry") } return vSplit(n, lf, d), nil } var err error if bitAt(lf.key, d) == 0 { n.l, err = vInsert(n.l, d+1, lf) } else { n.r, err = vInsert(n.r, d+1, lf) } if err != nil { return n, err } n.fix() return n, nil } func vDelete(n *vnode, d int, key [32]byte) (*vnode, *vnode) { if n == nil { return nil, nil } if n.leaf { if n.key == key { return nil, n } return n, nil } var rem *vnode if bitAt(key, d) == 0 { n.l, rem = vDelete(n.l, d+1, key) } else { n.r, rem = vDelete(n.r, d+1, key) } if rem == nil { return n, nil } if n.l == nil && n.r == nil { return nil, rem } if n.l == nil && n.r.leaf { return n.r, rem } if n.r == nil && n.l.leaf { return n.l, rem } n.fix() return n, rem } // VoteTrie: Merkle count trie + direct map, kept in sync (same // construction as UTXOSet). type VoteTrie struct { root *vnode entries map[Outpoint]*VoteEntry } func NewVoteTrie() *VoteTrie { return &VoteTrie{entries: map[Outpoint]*VoteEntry{}} } func (v *VoteTrie) Insert(e *VoteEntry) error { if _, ok := v.entries[e.Op]; ok { return errors.New("duplicate vote entry") } root, err := vInsert(v.root, 0, vLeaf(opKey(e.Op), e)) if err != nil { return err } v.root = root v.entries[e.Op] = e return nil } func (v *VoteTrie) Get(o Outpoint) *VoteEntry { return v.entries[o] } func (v *VoteTrie) Spend(o Outpoint) (*VoteEntry, error) { e, ok := v.entries[o] if !ok { return nil, errors.New("vote entry missing") } root, rem := vDelete(v.root, 0, opKey(o)) if rem == nil { return nil, errors.New("vote trie desync") // cannot happen } v.root = root delete(v.entries, o) return e, nil } // UpdateCommit replaces a committed entry's commit with the revealed // layer (delete + reinsert of the same key; count is unchanged). func (v *VoteTrie) UpdateCommit(o Outpoint, commit [32]byte) error { e, err := v.Spend(o) if err != nil { return err } ne := *e ne.Commit = commit return v.Insert(&ne) } func (v *VoteTrie) CommittedCount() uint64 { if v.root == nil { return 0 } return v.root.count } func (v *VoteTrie) Len() int { return len(v.entries) } func (v *VoteTrie) Root() [32]byte { if v.root == nil { return zero32 } return v.root.h } // Select walks the cumulative counts to the committed entry at // position pos (0-based, pos < CommittedCount). func (v *VoteTrie) Select(pos uint64) *VoteEntry { n := v.root for n != nil && !n.leaf { if n.l != nil && pos < n.l.count { n = n.l } else { if n.l != nil { pos -= n.l.count } n = n.r } } if n == nil || !n.e.Committed { return nil // pos out of range; callers bound it } return n.e } // SelectRand is the spec's `rand mod total_committed`. func (v *VoteTrie) SelectRand(r [32]byte) *VoteEntry { n := v.CommittedCount() if n == 0 { return nil } pos := new(big.Int).SetBytes(r[:]) pos.Mod(pos, new(big.Int).SetUint64(n)) return v.Select(pos.Uint64()) } func (v *VoteTrie) ForEach(f func(*VoteEntry)) { for _, e := range v.entries { f(e) } } func (v *VoteTrie) Clone() *VoteTrie { c := NewVoteTrie() for _, e := range v.entries { ne := *e if err := c.Insert(&ne); err != nil { panic("vote clone: " + err.Error()) } } return c } // ------------------------------------------------------------ Vote tx // VoteClaim mints this period's vote tokens for one node: a single // uncommitted entry {amount: contribution, owner: key, mixed: 0} in // next_election. One claim per node and period, gated by last_vote — // exactly why Add stamps it with the block time. Open phase only. type VoteClaim struct { Key PubKey Nonce uint64 Sig Sig } func (c *VoteClaim) body() []byte { var w buf w.u8(OpVoteClaim) w.bytes(c.Key[:]) w.u64(c.Nonce) return w.b } func (c *VoteClaim) ID() [32]byte { return H(c.body()) } func (c *VoteClaim) SigHash() [32]byte { return c.ID() } func (s *State) applyVoteClaim(c *VoteClaim, T uint64) error { if !phaseOpen(T) { return errors.New("vote claim: next_election is locked (second half of period)") } n := s.Tree.Get(c.Key) if n == nil { return errors.New("vote claim: key not in tree") } if n.Own == 0 { return errors.New("vote claim: node has no person contribution") } if c.Nonce != n.Nonce { return fmt.Errorf("vote claim: bad nonce (have %d want %d)", c.Nonce, n.Nonce) } if n.LastVote >= periodStart(T) { return errors.New("vote claim: already claimed this period") } sh := c.SigHash() if !VerifySig(c.Key, sh[:], c.Sig) { return errors.New("vote claim: bad signature") } if err := s.NextElection.Insert(&VoteEntry{ Op: Outpoint{Tx: c.ID(), Index: 0}, Amount: n.Own, Owner: c.Key, Mixed: 0, }); err != nil { return err } n.LastVote = T n.Nonce++ s.Tree.Bubble(n) return nil } // VoteOutput mirrors VoteEntry minus the outpoint (assigned at // processing as (txid, index)). type VoteOutput struct { Committed bool Amount uint64 // uncommitted only; committed is 1 Owner PubKey HasOwner bool // committed only Mixed uint32 // declared by every output: 1..MaxMix; stored only on uncommitted entries Commit [32]byte } // Vote mixes and commits existing uncommitted entries in // next_election; token conservation is exact: Σ inputs = Σ outputs. // Open phase only. type Vote struct { Inputs []Outpoint Outputs []VoteOutput Sigs []Sig // one per Input, by the entry's owner } // body excludes all signatures (anti-malleability); every signature — // each claim's and each input's — signs H(body), binding the outputs. func (v *Vote) body() []byte { var w buf w.u8(OpVote) w.u32(uint32(len(v.Inputs))) for i := range v.Inputs { w.bytes(v.Inputs[i].Tx[:]) w.u32(v.Inputs[i].Index) } w.u32(uint32(len(v.Outputs))) for i := range v.Outputs { o := &v.Outputs[i] w.boolb(o.Committed) if o.Committed { w.bytes(o.Commit[:]) w.boolb(o.HasOwner) w.bytes(o.Owner[:]) w.u32(o.Mixed) } else { w.u64(o.Amount) w.bytes(o.Owner[:]) w.u32(o.Mixed) } } return w.b } func (v *Vote) ID() [32]byte { return H(v.body()) } func (v *Vote) SigHash() [32]byte { return v.ID() } // applyVote validates and applies a Vote at block time T. It touches // only next_election and the claiming nodes; there is no fee — vote // tokens are not money. func (s *State) applyVote(v *Vote, T uint64) error { if !phaseOpen(T) { return errors.New("vote: next_election is locked (second half of period)") } if len(v.Inputs) == 0 || len(v.Inputs) > maxInputs { return errors.New("vote: bad input count") } if len(v.Outputs) == 0 || len(v.Outputs) > maxOutputs { return errors.New("vote: bad output count") } if len(v.Sigs) != len(v.Inputs) { return errors.New("vote: need one signature per input") } sh := v.SigHash() // Inputs: uncommitted, in next_election, signed by owner. Their // mixed values accumulate into the budget the outputs must pay // for; a fresh VoteClaim entry contributes 0. inSum := new(big.Int) inMix := uint64(0) seenOp := map[Outpoint]bool{} for i, op := range v.Inputs { if seenOp[op] { return errors.New("vote: duplicate input") } seenOp[op] = true e := s.NextElection.Get(op) if e == nil { return errors.New("vote: input missing or spent") } if e.Committed { return errors.New("vote: input is committed (locked)") } if !VerifySig(e.Owner, sh[:], v.Sigs[i]) { return fmt.Errorf("vote: bad signature for input %d", i) } inSum.Add(inSum, new(big.Int).SetUint64(e.Amount)) inMix += uint64(e.Mixed) } outSum := new(big.Int) outMix := uint64(0) for i := range v.Outputs { o := &v.Outputs[i] // Every output — committed included — declares a mixed value // in 1..MaxMix and pays one unit of budget. if o.Mixed < 1 || o.Mixed > MaxMix { return fmt.Errorf("vote: output mixed %d outside 1..%d", o.Mixed, MaxMix) } outMix += uint64(o.Mixed) if o.Committed { outSum.Add(outSum, big.NewInt(1)) // committed amount is 1 } else { if o.Amount == 0 { return errors.New("vote: zero-amount output") } outSum.Add(outSum, new(big.Int).SetUint64(o.Amount)) } } if inSum.Cmp(outSum) != 0 { return fmt.Errorf("vote: inputs %s != outputs %s", inSum, outSum) } // Spec: sum(output mixed) ≥ sum(input mixed) + count(outputs). // Every output is a paid hop, the final commit included — so a // commit is only possible while the token has budget left. if outMix < inMix+uint64(len(v.Outputs)) { return fmt.Errorf("vote: mixed budget %d < %d required", outMix, inMix+uint64(len(v.Outputs))) } // Apply. for _, op := range v.Inputs { if _, err := s.NextElection.Spend(op); err != nil { return err } } txid := v.ID() for i := range v.Outputs { o := &v.Outputs[i] e := &VoteEntry{ Op: Outpoint{Tx: txid, Index: uint32(i)}, Committed: o.Committed, Owner: o.Owner, } if o.Committed { // The declared mixed paid the hop; the entry does not // store it (spec). e.Amount = 1 e.HasOwner = o.HasOwner e.Commit = o.Commit } else { e.Amount = o.Amount e.Mixed = o.Mixed } if err := s.NextElection.Insert(e); err != nil { return err } } return nil } // ============================================================== wire // Wire format: the full transport encoding of transactions and blocks, // including signatures and (for Add) the subtree template. The hashed // tx bodies (tx.go) are reused verbatim as prefixes, so wire bytes and // consensus hashes can never drift apart. This is the format for the // on-disk block log, and later for gossip / external auditors / // validator handoff. // rdr is the decoding counterpart of buf: it never panics, it // accumulates the first error and returns zero values after it. type rdr struct { b []byte err error } func (r *rdr) need(n int) []byte { if r.err != nil { return make([]byte, n) } if len(r.b) < n { r.err = errors.New("wire: truncated") return make([]byte, n) } p := r.b[:n] r.b = r.b[n:] return p } func (r *rdr) u8() byte { return r.need(1)[0] } func (r *rdr) u32() uint32 { return binary.BigEndian.Uint32(r.need(4)) } func (r *rdr) u64() uint64 { return binary.BigEndian.Uint64(r.need(8)) } func (r *rdr) u128() *big.Int { return new(big.Int).SetBytes(r.need(16)) } func (r *rdr) boolb() bool { return r.u8() != 0 } func (r *rdr) h32() (h [32]byte) { copy(h[:], r.need(32)); return } func (r *rdr) key() (k PubKey) { copy(k[:], r.need(32)); return } func (r *rdr) sig() (s Sig) { copy(s[:], r.need(64)); return } func (r *rdr) done() error { if r.err == nil && len(r.b) > 0 { return errors.New("wire: trailing bytes") } return r.err } // ------------------------------------------------------------- outputs func decodeOutputs(r *rdr) []Output { n := r.u32() if n > maxOutputs { r.err = errors.New("wire: too many outputs") return nil } outs := make([]Output, 0, n) for i := uint32(0); i < n && r.err == nil; i++ { outs = append(outs, Output{Amount: r.u128(), Owner: r.key()}) } return outs } // ------------------------------------------------------------ template func encodeTemplate(w *buf, t *NodeTemplate) { w.bytes(t.Key[:]) w.boolb(t.Leaf) w.u64(t.Nonce) w.u64(t.LastUBI) w.u64(t.LastVote) w.u64(t.TreeCount) w.u128(t.treeUBI()) w.u32(uint32(len(t.Children))) for i := range t.Children { encodeTemplate(w, &t.Children[i]) } } func decodeTemplate(r *rdr, budget *int) NodeTemplate { *budget-- if *budget < 0 { r.err = errors.New("wire: template too large") return NodeTemplate{} } t := NodeTemplate{Key: r.key(), Leaf: r.boolb(), Nonce: r.u64(), LastUBI: r.u64(), LastVote: r.u64(), TreeCount: r.u64(), TreeUBI: r.u128()} n := r.u32() if n > uint32(maxTemplateNodes) { r.err = errors.New("wire: template too large") return t } for i := uint32(0); i < n && r.err == nil; i++ { t.Children = append(t.Children, decodeTemplate(r, budget)) } return t } // ------------------------------------------------------------------ tx // EncodeTx serializes a transaction: hashed body first, then the // signatures, then (Add) the template. func EncodeTx(t Tx) []byte { var w buf switch v := t.(type) { case *Claim: w.bytes(v.body()) w.bytes(v.Sig[:]) case *Transfer: w.bytes(v.body()) for i := range v.Sigs { // count == len(Inputs), implied by body w.bytes(v.Sigs[i][:]) } case *Prune: w.bytes(v.body()) // no signatures: validity is objective case *Add: w.bytes(v.body()) w.bytes(v.Consent[:]) w.bytes(v.Sig[:]) encodeTemplate(&w, &v.Template) case *Remove: w.bytes(v.body()) w.bytes(v.Sig[:]) case *Rekey: w.bytes(v.body()) w.bytes(v.Sig[:]) case *Move: w.bytes(v.body()) w.bytes(v.Consent[:]) w.bytes(v.Sig[:]) case *Leave: w.bytes(v.body()) w.bytes(v.Sig[:]) case *Vote: w.bytes(v.body()) for i := range v.Sigs { // one per input w.bytes(v.Sigs[i][:]) } case *VoteClaim: w.bytes(v.body()) w.bytes(v.Sig[:]) default: panic("EncodeTx: unknown tx type") } return w.b } func decodeTx(r *rdr) Tx { switch op := r.u8(); op { case OpClaim: return &Claim{Key: r.key(), Amount: r.u128(), Nonce: r.u64(), Sig: r.sig()} case OpTransfer: n := r.u32() if n > maxInputs { r.err = errors.New("wire: too many inputs") return nil } t := &Transfer{} for i := uint32(0); i < n && r.err == nil; i++ { t.Inputs = append(t.Inputs, Outpoint{Tx: r.h32(), Index: r.u32()}) } t.Outputs = decodeOutputs(r) for i := uint32(0); i < n && r.err == nil; i++ { t.Sigs = append(t.Sigs, r.sig()) } return t case OpPrune: n := r.u32() if n > maxInputs { r.err = errors.New("wire: too many inputs") return nil } p := &Prune{} for i := uint32(0); i < n && r.err == nil; i++ { p.Inputs = append(p.Inputs, Outpoint{Tx: r.h32(), Index: r.u32()}) } return p case OpAdd: a := &Add{Parent: r.key(), ChildKey: r.key(), Hash: r.h32(), Nonce: r.u64(), Deadline: r.u64(), Consent: r.sig(), Sig: r.sig()} budget := maxTemplateNodes a.Template = decodeTemplate(r, &budget) return a case OpRemove: return &Remove{Parent: r.key(), Child: r.key(), Nonce: r.u64(), Sig: r.sig()} case OpRekey: return &Rekey{Old: r.key(), New: r.key(), Nonce: r.u64(), Sig: r.sig()} case OpMove: return &Move{Child: r.key(), NewParent: r.key(), Nonce: r.u64(), Deadline: r.u64(), Consent: r.sig(), Sig: r.sig()} case OpLeave: return &Leave{Child: r.key(), Nonce: r.u64(), Sig: r.sig()} case OpVoteClaim: return &VoteClaim{Key: r.key(), Nonce: r.u64(), Sig: r.sig()} case OpVote: v := &Vote{} ni := r.u32() if ni > maxInputs { r.err = errors.New("wire: too many inputs") return nil } for i := uint32(0); i < ni && r.err == nil; i++ { v.Inputs = append(v.Inputs, Outpoint{Tx: r.h32(), Index: r.u32()}) } no := r.u32() if no > maxOutputs { r.err = errors.New("wire: too many outputs") return nil } for i := uint32(0); i < no && r.err == nil; i++ { o := VoteOutput{Committed: r.boolb()} if o.Committed { o.Commit = r.h32() o.HasOwner = r.boolb() o.Owner = r.key() o.Mixed = r.u32() o.Amount = 1 } else { o.Amount = r.u64() o.Owner = r.key() o.Mixed = r.u32() } v.Outputs = append(v.Outputs, o) } for i := uint32(0); i < ni && r.err == nil; i++ { v.Sigs = append(v.Sigs, r.sig()) } return v default: r.err = fmt.Errorf("wire: unknown opcode 0x%02x", op) return nil } } // DecodeTx parses exactly one transaction. func DecodeTx(b []byte) (Tx, error) { r := &rdr{b: b} t := decodeTx(r) if err := r.done(); err != nil { return nil, err } return t, nil } // --------------------------------------------------------------- block // EncodeBlock: header (with sigs) + u32 tx count + per tx u32 len + bytes. func EncodeBlock(b *Block) []byte { var w buf w.bytes(b.Header.encode(true)) w.u32(uint32(len(b.Txs))) for _, t := range b.Txs { tb := EncodeTx(t) w.u32(uint32(len(tb))) w.bytes(tb) } return w.b } func decodeHeader(r *rdr) Header { if op := r.u8(); op != OpHeader && r.err == nil { r.err = fmt.Errorf("wire: bad header opcode 0x%02x", op) } return Header{ Seq: r.u64(), Time: r.u64(), PeopleTree: r.h32(), UTXOTrie: r.h32(), ElectionTrie: r.h32(), NextElection: r.h32(), Prev: r.h32(), Validator: r.key(), Rand: r.h32(), Sig: r.sig(), } } const maxTxBytes = 1 << 22 // 4 MiB per tx, sanity cap func DecodeBlock(b []byte) (*Block, error) { r := &rdr{b: b} blk := &Block{Header: decodeHeader(r)} n := r.u32() for i := uint32(0); i < n && r.err == nil; i++ { l := r.u32() if l > maxTxBytes { return nil, errors.New("wire: tx too large") } tb := r.need(int(l)) if r.err != nil { break } t, err := DecodeTx(tb) if err != nil { return nil, fmt.Errorf("wire: tx %d: %w", i, err) } blk.Txs = append(blk.Txs, t) } if err := r.done(); err != nil { return nil, err } return blk, nil } // ============================================================= store // The chain persists as an append-only block log. State is never // written to disk — it is fully derived: on startup every block is // re-verified with VerifyBlock (full re-execution + root comparison), // so a node can only ever come up on a valid chain. The same record // bytes are what external auditors would consume. // // Layout: // record 0: u32 len | rootKey(32) | t0(8) | genesis header // record N: u32 len | EncodeBlock(block N) // Store is the append-only block log, with just enough bookkeeping — // the byte offset of every record — to truncate back to a fork point // when the chain reorgs. Record i corresponds to Blocks[i]; record 0 // is the genesis record (parameters + header), kept verbatim in // GenesisRec so peers can bootstrap from it. type Store struct { f *os.File offsets []int64 // start offset of record i end int64 // end of the last record (next append position) GenesisRec []byte } func writeRec(w io.Writer, rec []byte) error { var l [4]byte binary.BigEndian.PutUint32(l[:], uint32(len(rec))) if _, err := w.Write(l[:]); err != nil { return err } _, err := w.Write(rec) return err } // CreateStore writes a fresh log for the given genesis. The genesis // record carries everything a verifier needs: root person key, t0, // and both genesis votes' onion commits (election_trie and // next_election). func CreateStore(path string, rootKey PubKey, t0 uint64, commit0, commit1 [32]byte, gen *Header) (*Store, error) { f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { return nil, err } var w buf w.bytes(rootKey[:]) w.u64(t0) w.bytes(commit0[:]) w.bytes(commit1[:]) w.bytes(gen.encode(true)) if err := writeRec(f, w.b); err != nil { return nil, err } if err := f.Sync(); err != nil { return nil, err } return &Store{f: f, offsets: []int64{0}, end: int64(4 + len(w.b)), GenesisRec: w.b}, nil } // genesisState rebuilds and validates the genesis state from the // stored parameters + header (the header is checked, not trusted). func genesisState(rootKey PubKey, t0 uint64, commit0, commit1 [32]byte, h *Header) (*State, error) { norm := normTimeFor(t0) st := &State{ NormTime: norm, Time: t0, Seq: 0, Genesis: t0, Tree: NewPeopleTree(rootKey, 1, t0, norm), UTXO: NewUTXOSet(), Election: NewVoteTrie(), NextElection: NewVoteTrie(), } if err := st.Election.Insert(&VoteEntry{ Op: GenesisVoteOutpoint(0), Committed: true, Commit: commit0, }); err != nil { return nil, err } if err := st.NextElection.Insert(&VoteEntry{ Op: GenesisVoteOutpoint(1), Committed: true, Commit: commit1, }); err != nil { return nil, err } if h.Seq != 0 || h.Time != t0 || h.Prev != zero32 { return nil, errors.New("genesis: bad seq/time/prev") } if t0%SlotSeconds != 0 { return nil, errors.New("genesis: t0 not on the slot grid") } if h.Rand != zero32 { return nil, errors.New("genesis: rand must be zero") } if h.PeopleTree != st.Tree.RootHash() || h.UTXOTrie != st.UTXO.Root() || h.ElectionTrie != st.Election.Root() || h.NextElection != st.NextElection.Root() { return nil, errors.New("genesis: root mismatch") } sh := h.SigHash() if !VerifySig(h.Validator, sh[:], h.Sig) { return nil, errors.New("genesis: bad validator signature") } st.LastHash = h.Hash() return st, nil } // OpenStore reads the log, replays and verifies every block, and // returns the store (positioned for appends) plus the resulting chain. func OpenStore(path string) (*Store, *Chain, error) { data, err := os.ReadFile(path) if err != nil { return nil, nil, err } rest := data var offsets []int64 pos := int64(0) next := func() ([]byte, error) { if len(rest) == 0 { return nil, io.EOF } if len(rest) < 4 { return nil, errors.New("store: truncated length") } l := binary.BigEndian.Uint32(rest[:4]) rest = rest[4:] if uint32(len(rest)) < l { return nil, errors.New("store: truncated record") } rec := rest[:l] rest = rest[l:] offsets = append(offsets, pos) pos += int64(4 + l) return rec, nil } rec, err := next() if err != nil { return nil, nil, fmt.Errorf("store: genesis record: %w", err) } genRec := rec r := &rdr{b: rec} rootKey := r.key() t0 := r.u64() commit0 := r.h32() commit1 := r.h32() gh := decodeHeader(r) if err := r.done(); err != nil { return nil, nil, fmt.Errorf("store: genesis record: %w", err) } st, err := genesisState(rootKey, t0, commit0, commit1, &gh) if err != nil { return nil, nil, err } ch := &Chain{State: st, Blocks: []*Block{{Header: gh}}, Final: st.Clone(), finalIdx: 0} for { rec, err := next() if err == io.EOF { break } if err != nil { return nil, nil, err } blk, err := DecodeBlock(rec) if err != nil { return nil, nil, fmt.Errorf("store: record after slot %d: %w", ch.State.Seq, err) } ns, err := VerifyBlock(ch.State, blk) if err != nil { return nil, nil, fmt.Errorf("store: block %d: %w", blk.Header.Seq, err) } ch.State = ns ch.Blocks = append(ch.Blocks, blk) } // Fold the final region into the snapshot (a second verify pass // over old blocks; a snapshot record in the log would avoid it, // left as an optimization). ch.advanceFinality() f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND, 0) if err != nil { return nil, nil, err } return &Store{f: f, offsets: offsets, end: pos, GenesisRec: append([]byte(nil), genRec...)}, ch, nil } // Append durably writes one block. func (s *Store) Append(b *Block) error { rec := EncodeBlock(b) if err := writeRec(s.f, rec); err != nil { return err } s.offsets = append(s.offsets, s.end) s.end += int64(4 + len(rec)) return s.f.Sync() } // Reorg truncates the log to its first `keep` records (the fork point // and everything below) and appends the adopted branch. keep counts // records including genesis, i.e. forkIdx+1. func (s *Store) Reorg(keep int, branch []*Block) error { if keep < 1 || keep > len(s.offsets) { return errors.New("store: bad reorg keep count") } cut := s.end if keep < len(s.offsets) { cut = s.offsets[keep] } if err := s.f.Truncate(cut); err != nil { return err } // A plain fd keeps its old write offset past the truncation point; // seek so the branch lands at the cut (no-op under O_APPEND). if _, err := s.f.Seek(cut, io.SeekStart); err != nil { return err } s.offsets = s.offsets[:keep] s.end = cut for _, b := range branch { rec := EncodeBlock(b) if err := writeRec(s.f, rec); err != nil { return err } s.offsets = append(s.offsets, s.end) s.end += int64(4 + len(rec)) } return s.f.Sync() } // ============================================================ server // Server: JSON API + mempool + block production loop + the network // layer. One mutex guards chain, store and mempool; every mutation // goes through Chain.Produce or Chain.TryAdopt (both self-verifying) // followed by a durable store write. Networking is deliberately // plain: a static peer list of base URLs, best-effort push of every // produced block, and a poll every couple of seconds that compares // tips and syncs whatever chain is better under the fork choice. All // network I/O happens outside the lock. type Server struct { mu sync.Mutex chain *Chain store *Store mempool []Tx valPriv ed25519.PrivateKey onions []*Onion lastSlot uint64 // last slot we attempted, produced or not peers []string // peer base URLs, e.g. http://host:port client *http.Client seen map[[32]byte]bool // tx ids accepted this session (forward dedupe) } // nowT is the block clock: wall time, never before the chain tip // (protects against clock steps backwards). func (s *Server) nowT() uint64 { t := uint64(time.Now().Unix()) if t < s.chain.State.Time { t = s.chain.State.Time } return t } // ------------------------------------------------------ production loop // produceLoop wakes every second and attempts each slot exactly once, // at its boundary. A block is produced every slot we are selected for // — even an empty one: the spec's fork choice counts slots without // blocks as skips, and every reveal feeds the RNG. func (s *Server) produceLoop() { tick := time.NewTicker(time.Second) for range tick.C { s.mu.Lock() slot := slotOf(s.nowT()) if slot > s.lastSlot && slotTime(slot) > s.chain.State.Time { s.lastSlot = slot s.produceLocked(slotTime(slot)) } s.mu.Unlock() } } func (s *Server) produceLocked(T uint64) { prune := s.expiredPrune(T) // Select the valid subset in submission order: each candidate is // tried on a scratch clone so a failing tx cannot poison state. // The validator's own prune of expired UTXOs goes first. Mempool // txs are validated standalone; slot preconditions (grid, phase, // selection) are enforced by Produce below. scratch := s.chain.State.Clone() var keep []Tx if prune != nil { if _, err := scratch.ApplyTxs([]Tx{prune}, T); err != nil { log.Printf("drop prune: %v", err) // cannot happen by construction } else { keep = append(keep, prune) } } for _, tx := range s.mempool { trial := scratch.Clone() if _, err := trial.ApplyTxs([]Tx{tx}, T); err != nil { log.Printf("drop tx %x: %v", tx.ID(), err) continue } scratch = trial keep = append(keep, tx) } s.mempool = nil b, err := s.chain.Produce(keep, T, s.valPriv, s.onions) if err != nil { // Not our slot, onion exhausted, or a real failure. In all // cases the slot passes unfilled — a skip, folded into the // next block's rand. log.Printf("seq %d skipped: %v", s.chain.State.seqAt(T), err) if len(keep) > 0 { s.mempool = append(keep, s.mempool...) // retry next slot } return } if err := s.store.Append(b); err != nil { log.Fatalf("store append failed: %v", err) // cannot continue safely } log.Printf("block %d @ %d: %d tx, people %x utxo %x", b.Header.Seq, b.Header.Time, len(b.Txs), b.Header.PeopleTree[:6], b.Header.UTXOTrie[:6]) go s.broadcast(b) } // ----------------------------------------------------------- network // broadcast pushes a block to every peer, best effort. Peers that // cannot use it directly recover through their poll loop. func (s *Server) broadcast(b *Block) { body := EncodeBlock(b) for _, p := range s.peers { resp, err := s.client.Post(p+"/api/block", "application/octet-stream", bytes.NewReader(body)) if err != nil { continue } resp.Body.Close() } } // forwardTx relays a freshly accepted transaction to every peer so it // reaches whichever validator wins the next slot. The seen-set stops // forwarding loops: a node forwards a given tx id at most once. func (s *Server) forwardTx(j []byte) { for _, p := range s.peers { resp, err := s.client.Post(p+"/api/tx", "application/json", bytes.NewReader(j)) if err != nil { continue } resp.Body.Close() } } // ingest is the write path for blocks arriving from the network: a // one-block fast path when it extends our tip, otherwise the generic // fork-choice adoption. Blocks from the future (beyond one slot of // clock drift) are refused at this layer only — consensus itself // stays clock-free. func (s *Server) ingest(blocks []*Block) error { if len(blocks) == 0 { return nil } s.mu.Lock() defer s.mu.Unlock() if blocks[len(blocks)-1].Header.Time > s.nowT()+SlotSeconds { return errors.New("ingest: block from the future") } forkIdx, err := s.chain.TryAdopt(blocks) if err != nil { return err } if err := s.store.Reorg(forkIdx+1, blocks); err != nil { log.Fatalf("store reorg failed: %v", err) // cannot continue safely } tip := s.chain.State log.Printf("adopted %d block(s) from peer, tip seq %d @ %d", len(blocks), tip.Seq, tip.Time) return nil } // readRecords parses a stream of length-prefixed block records — the // same framing as the on-disk log and /api/chain. func readRecords(data []byte) ([]*Block, error) { var out []*Block for len(data) > 0 { if len(data) < 4 { return nil, errors.New("sync: truncated length") } l := binary.BigEndian.Uint32(data[:4]) data = data[4:] if uint32(len(data)) < l { return nil, errors.New("sync: truncated record") } b, err := DecodeBlock(data[:l]) if err != nil { return nil, err } out = append(out, b) data = data[l:] } return out, nil } // syncPeer compares tips with one peer and, when the peer's chain is // better, fetches and adopts it. Fetching happens outside the lock; // only the adoption takes it. Returns true if progress was made (the // caller loops until the peer has nothing better). func (s *Server) syncPeer(peer string) (bool, error) { resp, err := s.client.Get(peer + "/api/status") if err != nil { return false, err } var st struct { Blocks int `json:"blocks"` TipHash string `json:"tip_hash"` GenesisHash string `json:"genesis_hash"` } err = json.NewDecoder(resp.Body).Decode(&st) resp.Body.Close() if err != nil { return false, err } s.mu.Lock() ourGenesis := s.chain.Blocks[0].Header.Hash() ourBlocks := len(s.chain.Blocks) ourTip := s.chain.TipHash() fromSeq := s.chain.State.Seq finalSeq := s.chain.Blocks[s.chain.finalIdx].Header.Seq s.mu.Unlock() if st.GenesisHash != hex.EncodeToString(ourGenesis[:]) { return false, errors.New("sync: peer has a different genesis") } if st.Blocks <= ourBlocks || st.TipHash == hex.EncodeToString(ourTip[:]) { return false, nil // nothing better there } // Fast path: ask for everything above our tip; if the first block // doesn't extend us we diverged, so refetch the whole reorg // window and let TryAdopt find the fork point. blocks, err := s.fetchChain(peer, fromSeq) if err != nil { return false, err } if len(blocks) == 0 { return false, nil } if blocks[0].Header.Prev != ourTip { if blocks, err = s.fetchChain(peer, finalSeq); err != nil { return false, err } // Drop the shared prefix: TryAdopt wants the branch to start // right after a block we have. s.mu.Lock() have := map[[32]byte]int{} for i := s.chain.finalIdx; i < len(s.chain.Blocks); i++ { have[s.chain.Blocks[i].Header.Hash()] = i } s.mu.Unlock() cut := 0 for cut < len(blocks) { if _, ok := have[blocks[cut].Header.Hash()]; !ok { break } cut++ } blocks = blocks[cut:] if len(blocks) == 0 { return false, nil } } if err := s.ingest(blocks); err != nil { return false, err } return true, nil } // fetchChain pulls blocks with seq > from, following the server's // batching until the stream dries up. func (s *Server) fetchChain(peer string, from uint64) ([]*Block, error) { var out []*Block for { resp, err := s.client.Get(fmt.Sprintf("%s/api/chain?from=%d", peer, from)) if err != nil { return nil, err } data, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20)) resp.Body.Close() if err != nil { return nil, err } blocks, err := readRecords(data) if err != nil { return nil, err } if len(blocks) == 0 { return out, nil } out = append(out, blocks...) from = blocks[len(blocks)-1].Header.Seq } } // pollLoop keeps us in sync with every peer. Two seconds is far // inside a slot, so a healthy network converges well before the next // block is due. func (s *Server) pollLoop() { tick := time.NewTicker(2 * time.Second) for range tick.C { for _, p := range s.peers { for { more, err := s.syncPeer(p) if err != nil { log.Printf("sync %s: %v", p, err) break } if !more { break } } } } } // expiredPrune scans for UTXOs with spendable(T) ≤ 0 and builds a // Prune transaction collecting them (or nil if none). Inputs are // sorted for a deterministic tx; how the validator finds expired // outputs is an implementation concern per the spec — here, a scan. func (s *Server) expiredPrune(T uint64) *Prune { var ops []Outpoint s.chain.State.UTXO.ForEach(func(e *Entry) { if len(ops) < maxInputs && e.Expired(T) { ops = append(ops, e.Op) } }) if len(ops) == 0 { return nil } sort.Slice(ops, func(i, j int) bool { if c := bytes.Compare(ops[i].Tx[:], ops[j].Tx[:]); c != 0 { return c < 0 } return ops[i].Index < ops[j].Index }) return &Prune{Inputs: ops} } // -------------------------------------------------------- JSON <-> tx type jOutput struct { Amount string `json:"amount"` // decimal, base units (10^16 = 1 TOKEN) Owner string `json:"owner"` // hex 32B } type jOutpoint struct { Tx string `json:"tx"` // hex 32B Index uint32 `json:"index"` } type jTemplate struct { Key string `json:"key"` Leaf bool `json:"leaf"` Nonce uint64 `json:"nonce,omitempty"` LastUBI uint64 `json:"last_ubi,omitempty"` LastVote uint64 `json:"last_vote,omitempty"` TreeCount uint64 `json:"tree_count"` TreeUBI string `json:"tree_ubi,omitempty"` Children []jTemplate `json:"children,omitempty"` } type jVoteOutput struct { Committed bool `json:"committed"` Amount uint64 `json:"amount,omitempty"` // uncommitted only Owner string `json:"owner,omitempty"` Mixed uint32 `json:"mixed,omitempty"` // declared by every output: 1..MaxMix Commit string `json:"commit,omitempty"` // committed only } type jTx struct { Type string `json:"type"` // claim|transfer|prune|add|remove|rekey|move|leave|vote|vote_claim Key string `json:"key,omitempty"` Parent string `json:"parent,omitempty"` ChildKey string `json:"child_key,omitempty"` Child string `json:"child,omitempty"` Old string `json:"old,omitempty"` New string `json:"new,omitempty"` NewParent string `json:"new_parent,omitempty"` Validator string `json:"validator,omitempty"` Nonce uint64 `json:"nonce"` Amount string `json:"amount,omitempty"` // claim only Deadline uint64 `json:"deadline,omitempty"` Outputs []jOutput `json:"outputs,omitempty"` Inputs []jOutpoint `json:"inputs,omitempty"` Template *jTemplate `json:"template,omitempty"` VoteOutputs []jVoteOutput `json:"vote_outputs,omitempty"` Sig string `json:"sig,omitempty"` Sigs []string `json:"sigs,omitempty"` Consent string `json:"consent,omitempty"` } func hexN(s string, n int) ([]byte, error) { b, err := hex.DecodeString(s) if err != nil { return nil, err } if len(b) != n { return nil, fmt.Errorf("want %d bytes, got %d", n, len(b)) } return b, nil } func pKey(s string) (k PubKey, err error) { b, err := hexN(s, 32) if err == nil { copy(k[:], b) } return } func pSig(s string) (g Sig, err error) { if s == "" { // allowed unsigned (for /tx/prepare) return } b, err := hexN(s, 64) if err == nil { copy(g[:], b) } return } func p32(s string) (h [32]byte, err error) { b, err := hexN(s, 32) if err == nil { copy(h[:], b) } return } func pAmount(s string) (*big.Int, error) { a, ok := new(big.Int).SetString(s, 10) if !ok || a.Sign() < 0 || a.BitLen() > 128 { return nil, errors.New("bad amount") } return a, nil } func pOutputs(js []jOutput) ([]Output, error) { var outs []Output for _, o := range js { a, err := pAmount(o.Amount) if err != nil { return nil, err } k, err := pKey(o.Owner) if err != nil { return nil, err } outs = append(outs, Output{Amount: a, Owner: k}) } return outs, nil } func pTemplate(j *jTemplate) (NodeTemplate, error) { k, err := pKey(j.Key) if err != nil { return NodeTemplate{}, err } t := NodeTemplate{Key: k, Leaf: j.Leaf, Nonce: j.Nonce, LastUBI: j.LastUBI, LastVote: j.LastVote, TreeCount: j.TreeCount} if j.TreeUBI != "" { tu, err := pAmount(j.TreeUBI) if err != nil { return t, fmt.Errorf("tree_ubi: %w", err) } t.TreeUBI = tu } for i := range j.Children { c, err := pTemplate(&j.Children[i]) if err != nil { return t, err } t.Children = append(t.Children, c) } return t, nil } // toTx builds a transaction from JSON. For Add, the committed hash is // derived from the template — the parent's signature covers it, so a // tampered template simply fails signature verification. func toTx(j *jTx) (Tx, error) { switch j.Type { case "claim": k, err := pKey(j.Key) if err != nil { return nil, err } a, err := pAmount(j.Amount) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &Claim{Key: k, Amount: a, Nonce: j.Nonce, Sig: sig}, nil case "vote_claim": k, err := pKey(j.Key) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &VoteClaim{Key: k, Nonce: j.Nonce, Sig: sig}, nil case "transfer": t := &Transfer{} for _, in := range j.Inputs { h, err := p32(in.Tx) if err != nil { return nil, err } t.Inputs = append(t.Inputs, Outpoint{Tx: h, Index: in.Index}) } outs, err := pOutputs(j.Outputs) if err != nil { return nil, err } t.Outputs = outs for _, s := range j.Sigs { g, err := pSig(s) if err != nil { return nil, err } t.Sigs = append(t.Sigs, g) } return t, nil case "prune": p := &Prune{} for _, in := range j.Inputs { h, err := p32(in.Tx) if err != nil { return nil, err } p.Inputs = append(p.Inputs, Outpoint{Tx: h, Index: in.Index}) } return p, nil case "vote": v := &Vote{} for _, in := range j.Inputs { h, err := p32(in.Tx) if err != nil { return nil, err } v.Inputs = append(v.Inputs, Outpoint{Tx: h, Index: in.Index}) } for _, o := range j.VoteOutputs { vo := VoteOutput{Committed: o.Committed} if o.Committed { c, err := p32(o.Commit) if err != nil { return nil, err } vo.Commit = c vo.Amount = 1 vo.Mixed = o.Mixed if o.Owner != "" { k, err := pKey(o.Owner) if err != nil { return nil, err } vo.Owner, vo.HasOwner = k, true } } else { k, err := pKey(o.Owner) if err != nil { return nil, err } vo.Owner, vo.Amount, vo.Mixed = k, o.Amount, o.Mixed } v.Outputs = append(v.Outputs, vo) } for _, s := range j.Sigs { g, err := pSig(s) if err != nil { return nil, err } v.Sigs = append(v.Sigs, g) } return v, nil case "add": if j.Template == nil { return nil, errors.New("add: template required") } p, err := pKey(j.Parent) if err != nil { return nil, err } ck, err := pKey(j.ChildKey) if err != nil { return nil, err } tpl, err := pTemplate(j.Template) if err != nil { return nil, err } consent, err := pSig(j.Consent) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &Add{Parent: p, ChildKey: ck, Hash: tpl.Hash(), Nonce: j.Nonce, Deadline: j.Deadline, Consent: consent, Sig: sig, Template: tpl}, nil case "remove": p, err := pKey(j.Parent) if err != nil { return nil, err } c, err := pKey(j.Child) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &Remove{Parent: p, Child: c, Nonce: j.Nonce, Sig: sig}, nil case "rekey": o, err := pKey(j.Old) if err != nil { return nil, err } n, err := pKey(j.New) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &Rekey{Old: o, New: n, Nonce: j.Nonce, Sig: sig}, nil case "move": c, err := pKey(j.Child) if err != nil { return nil, err } np, err := pKey(j.NewParent) if err != nil { return nil, err } consent, err := pSig(j.Consent) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &Move{Child: c, NewParent: np, Nonce: j.Nonce, Deadline: j.Deadline, Consent: consent, Sig: sig}, nil case "leave": c, err := pKey(j.Child) if err != nil { return nil, err } sig, err := pSig(j.Sig) if err != nil { return nil, err } return &Leave{Child: c, Nonce: j.Nonce, Sig: sig}, nil } return nil, fmt.Errorf("unknown tx type %q", j.Type) } func fromTx(t Tx) *jTx { hx := func(b []byte) string { return hex.EncodeToString(b) } outs := func(os []Output) []jOutput { var r []jOutput for _, o := range os { r = append(r, jOutput{Amount: o.Amount.String(), Owner: hx(o.Owner[:])}) } return r } switch v := t.(type) { case *Claim: return &jTx{Type: "claim", Key: hx(v.Key[:]), Nonce: v.Nonce, Amount: v.Amount.String(), Sig: hx(v.Sig[:])} case *VoteClaim: return &jTx{Type: "vote_claim", Key: hx(v.Key[:]), Nonce: v.Nonce, Sig: hx(v.Sig[:])} case *Transfer: j := &jTx{Type: "transfer", Outputs: outs(v.Outputs)} for _, in := range v.Inputs { j.Inputs = append(j.Inputs, jOutpoint{Tx: hx(in.Tx[:]), Index: in.Index}) } for _, s := range v.Sigs { j.Sigs = append(j.Sigs, hx(s[:])) } return j case *Prune: j := &jTx{Type: "prune"} for _, in := range v.Inputs { j.Inputs = append(j.Inputs, jOutpoint{Tx: hx(in.Tx[:]), Index: in.Index}) } return j case *Vote: j := &jTx{Type: "vote"} for _, in := range v.Inputs { j.Inputs = append(j.Inputs, jOutpoint{Tx: hx(in.Tx[:]), Index: in.Index}) } for i := range v.Outputs { o := &v.Outputs[i] jo := jVoteOutput{Committed: o.Committed} if o.Committed { jo.Commit = hx(o.Commit[:]) jo.Mixed = o.Mixed if o.HasOwner { jo.Owner = hx(o.Owner[:]) } } else { jo.Amount = o.Amount jo.Owner = hx(o.Owner[:]) jo.Mixed = o.Mixed } j.VoteOutputs = append(j.VoteOutputs, jo) } for _, s := range v.Sigs { j.Sigs = append(j.Sigs, hx(s[:])) } return j case *Add: var tpl func(t *NodeTemplate) *jTemplate tpl = func(t *NodeTemplate) *jTemplate { j := &jTemplate{Key: hx(t.Key[:]), Leaf: t.Leaf, Nonce: t.Nonce, LastUBI: t.LastUBI, LastVote: t.LastVote, TreeCount: t.TreeCount, TreeUBI: t.treeUBI().String()} for i := range t.Children { j.Children = append(j.Children, *tpl(&t.Children[i])) } return j } return &jTx{Type: "add", Parent: hx(v.Parent[:]), ChildKey: hx(v.ChildKey[:]), Nonce: v.Nonce, Deadline: v.Deadline, Template: tpl(&v.Template), Consent: hx(v.Consent[:]), Sig: hx(v.Sig[:])} case *Remove: return &jTx{Type: "remove", Parent: hx(v.Parent[:]), Child: hx(v.Child[:]), Nonce: v.Nonce, Sig: hx(v.Sig[:])} case *Rekey: return &jTx{Type: "rekey", Old: hx(v.Old[:]), New: hx(v.New[:]), Nonce: v.Nonce, Sig: hx(v.Sig[:])} case *Move: return &jTx{Type: "move", Child: hx(v.Child[:]), NewParent: hx(v.NewParent[:]), Nonce: v.Nonce, Deadline: v.Deadline, Consent: hx(v.Consent[:]), Sig: hx(v.Sig[:])} case *Leave: return &jTx{Type: "leave", Child: hx(v.Child[:]), Nonce: v.Nonce, Sig: hx(v.Sig[:])} } return nil } // ------------------------------------------------------------ handlers func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) enc := json.NewEncoder(w) enc.SetIndent("", " ") enc.Encode(v) } func jerr(w http.ResponseWriter, code int, err error) { writeJSON(w, code, map[string]string{"error": err.Error()}) } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { s.mu.Lock() defer s.mu.Unlock() st := s.chain.State T := s.nowT() supply := st.SupplyAt(T) uncl := st.UnclaimedAt(T) sum := new(big.Int).Add(supply, uncl) target := new(big.Int).Mul(new(big.Int).SetUint64(st.Tree.Population()), TOKEN) writeJSON(w, 200, map[string]any{ "seq": st.Seq, "blocks": len(s.chain.Blocks), "time": st.Time, "now": T, "norm_time": st.NormTime, "rand": hex.EncodeToString(st.Rand[:]), "tip_hash": hex.EncodeToString(func() []byte { h := s.chain.TipHash(); return h[:] }()), "genesis_hash": hex.EncodeToString(func() []byte { h := s.chain.Blocks[0].Header.Hash(); return h[:] }()), "period_start": periodStart(st.Time), "phase": func() string { if phaseOpen(st.Time) { return "open" } return "locked" }(), "committed": st.Election.CommittedCount(), "committed_next": st.NextElection.CommittedCount(), "election_root": hex.EncodeToString(func() []byte { h := st.Election.Root(); return h[:] }()), "people_root": hex.EncodeToString(func() []byte { h := st.Tree.RootHash(); return h[:] }()), "utxo_root": hex.EncodeToString(func() []byte { h := st.UTXO.Root(); return h[:] }()), "population": st.Tree.Population(), "utxo_count": st.UTXO.Len(), "supply": supply.String(), "unclaimed": uncl.String(), "sum": sum.String(), "target": target.String(), "mempool": len(s.mempool), "token": TOKEN.String(), "rent_per_second": RentPerSecond.String(), "slot_seconds": SlotSeconds, "period_seconds": PeriodSeconds, "max_mix": MaxMix, }) } func (s *Server) handleNode(w http.ResponseWriter, r *http.Request) { k, err := pKey(r.PathValue("key")) if err != nil { jerr(w, 400, err) return } s.mu.Lock() defer s.mu.Unlock() n := s.chain.State.Tree.Get(k) if n == nil { jerr(w, 404, errors.New("key not in tree")) return } T := s.nowT() var children []string for _, c := range n.Children { children = append(children, hex.EncodeToString(c.Key[:])) } parent := "" if n.Parent != nil { parent = hex.EncodeToString(n.Parent.Key[:]) } writeJSON(w, 200, map[string]any{ "key": r.PathValue("key"), "leaf": n.Leaf, "own": n.Own, "nonce": n.Nonce, "last_ubi": n.LastUBI, "last_vote": n.LastVote, "tree_count": n.TreeCount, "claimable_now": ClaimableAt(n.Own, n.LastUBI, T).String(), "now": T, "parent": parent, "children": children, }) } func (s *Server) handleBalance(w http.ResponseWriter, r *http.Request) { k, err := pKey(r.PathValue("key")) if err != nil { jerr(w, 400, err) return } s.mu.Lock() defer s.mu.Unlock() T := s.nowT() type ju struct { Tx string `json:"tx"` Index uint32 `json:"index"` Value string `json:"value_now"` RentOwed string `json:"rent_owed"` Spendable string `json:"spendable"` Expired bool `json:"expired,omitempty"` Created uint64 `json:"created"` } total := new(big.Int) spendTotal := new(big.Int) var us []ju s.chain.State.UTXO.ForEach(func(e *Entry) { if e.Owner != k { return } v, rent, sp := e.Value(T), e.Rent(T), e.Spendable(T) total.Add(total, v) if sp.Sign() > 0 { spendTotal.Add(spendTotal, sp) } us = append(us, ju{Tx: hex.EncodeToString(e.Op.Tx[:]), Index: e.Op.Index, Value: v.String(), RentOwed: rent.String(), Spendable: sp.String(), Expired: sp.Sign() <= 0, Created: e.Time}) }) sort.Slice(us, func(i, j int) bool { if us[i].Tx != us[j].Tx { return us[i].Tx < us[j].Tx } return us[i].Index < us[j].Index }) writeJSON(w, 200, map[string]any{"now": T, "total": total.String(), "spendable": spendTotal.String(), "utxos": us}) } func (s *Server) handleBlock(w http.ResponseWriter, r *http.Request) { var seq uint64 if _, err := fmt.Sscan(r.PathValue("seq"), &seq); err != nil { jerr(w, 400, err) return } s.mu.Lock() defer s.mu.Unlock() bs := s.chain.Blocks i := sort.Search(len(bs), func(i int) bool { return bs[i].Header.Seq >= seq }) if i == len(bs) || bs[i].Header.Seq != seq { jerr(w, 404, errors.New("no block at that slot (skipped, or beyond the tip)")) return } b := bs[i] h := &b.Header hx := func(b []byte) string { return hex.EncodeToString(b) } var txs []*jTx for _, t := range b.Txs { txs = append(txs, fromTx(t)) } writeJSON(w, 200, map[string]any{ "seq": h.Seq, "time": h.Time, "people_tree": hx(h.PeopleTree[:]), "utxo_trie": hx(h.UTXOTrie[:]), "prev": hx(h.Prev[:]), "validator": hx(h.Validator[:]), "election_trie": hx(h.ElectionTrie[:]), "rand": hx(h.Rand[:]), "sig": hx(h.Sig[:]), "hash": hx(func() []byte { x := h.Hash(); return x[:] }()), "txs": txs, }) } func (s *Server) handleMempool(w http.ResponseWriter, r *http.Request) { s.mu.Lock() defer s.mu.Unlock() var out []map[string]string for _, t := range s.mempool { id := t.ID() out = append(out, map[string]string{ "id": hex.EncodeToString(id[:]), "type": fromTx(t).Type}) } writeJSON(w, 200, out) } // handleTxPrepare: submit an UNSIGNED tx, get back the exact bytes to // sign (hex). For Add, both the parent's message and the child's // consent message are returned. Nothing is queued. func (s *Server) handleTxPrepare(w http.ResponseWriter, r *http.Request) { var j jTx if err := json.NewDecoder(r.Body).Decode(&j); err != nil { jerr(w, 400, err) return } t, err := toTx(&j) if err != nil { jerr(w, 400, err) return } id := t.ID() resp := map[string]any{ "id": hex.EncodeToString(id[:]), "sign_message": hex.EncodeToString(id[:]), // SigHash == ID "note": "sign with ed25519 over sign_message bytes; put hex signature in 'sig' (or 'sigs', one per input) and POST /tx", } if a, ok := t.(*Add); ok { resp["consent_message"] = hex.EncodeToString(ConsentMsg(a.Hash, a.Deadline, a.Parent)) resp["template_hash"] = hex.EncodeToString(a.Hash[:]) } if m, ok := t.(*Move); ok { resp["consent_message"] = hex.EncodeToString(MoveConsentMsg(m.Deadline, m.NewParent)) } if _, ok := t.(*Vote); ok { resp["note"] = "sign with ed25519 over sign_message bytes; one sig per input in 'sigs'; POST /tx" } writeJSON(w, 200, resp) } func (s *Server) handleTxSubmit(w http.ResponseWriter, r *http.Request) { var j jTx if err := json.NewDecoder(r.Body).Decode(&j); err != nil { jerr(w, 400, err) return } t, err := toTx(&j) if err != nil { jerr(w, 400, err) return } id := t.ID() s.mu.Lock() if s.seen[id] { s.mu.Unlock() writeJSON(w, 200, map[string]string{"id": hex.EncodeToString(id[:]), "status": "known"}) return } // Dry-run against confirmed state + current mempool, at the // earliest possible inclusion time, for immediate feedback. T := s.nowT() trial := s.chain.State.Clone() for _, p := range s.mempool { trial.ApplyTxs([]Tx{p}, T) // best effort; conflicts re-checked at production } if _, err := trial.ApplyTxs([]Tx{t}, T); err != nil { s.mu.Unlock() jerr(w, 422, err) return } s.mempool = append(s.mempool, t) s.seen[id] = true s.mu.Unlock() // Relay so the tx reaches whichever validator wins a slot; the // seen-set above makes each node forward a given tx at most once. if len(s.peers) > 0 { if body, err := json.Marshal(fromTx(t)); err == nil { go s.forwardTx(body) } } writeJSON(w, 200, map[string]string{"id": hex.EncodeToString(id[:]), "status": "queued"}) } // handleGenesis serves the verbatim genesis record — everything a new // node needs to bootstrap (root key, t0, both onion commits, header). func (s *Server) handleGenesis(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") var l [4]byte binary.BigEndian.PutUint32(l[:], uint32(len(s.store.GenesisRec))) w.Write(l[:]) w.Write(s.store.GenesisRec) } // handleChain streams length-prefixed block records with seq > from, // capped per request; the client follows up with the last seq it got. func (s *Server) handleChain(w http.ResponseWriter, r *http.Request) { var from uint64 if v := r.URL.Query().Get("from"); v != "" { if _, err := fmt.Sscan(v, &from); err != nil { jerr(w, 400, err) return } } s.mu.Lock() bs := s.chain.Blocks i := sort.Search(len(bs), func(i int) bool { return bs[i].Header.Seq > from }) var recs [][]byte total := 0 for ; i < len(bs) && len(recs) < 2048 && total < 4<<20; i++ { rec := EncodeBlock(bs[i]) recs = append(recs, rec) total += 4 + len(rec) } s.mu.Unlock() w.Header().Set("Content-Type", "application/octet-stream") for _, rec := range recs { var l [4]byte binary.BigEndian.PutUint32(l[:], uint32(len(rec))) w.Write(l[:]) w.Write(rec) } } // handleBlockPush accepts one pushed block (wire format). A block // that doesn't fit our chain right now is a 409; the poll loop will // resolve any real divergence. func (s *Server) handleBlockPush(w http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(io.LimitReader(r.Body, maxTxBytes)) if err != nil { jerr(w, 400, err) return } b, err := DecodeBlock(data) if err != nil { jerr(w, 400, err) return } if err := s.ingest([]*Block{b}); err != nil { jerr(w, 409, err) return } writeJSON(w, 200, map[string]string{"status": "accepted"}) } func (s *Server) routes() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("GET /api/status", s.handleStatus) mux.HandleFunc("GET /api/node/{key}", s.handleNode) mux.HandleFunc("GET /api/balance/{key}", s.handleBalance) mux.HandleFunc("GET /api/block/{seq}", s.handleBlock) mux.HandleFunc("GET /api/mempool", s.handleMempool) mux.HandleFunc("GET /api/genesis", s.handleGenesis) mux.HandleFunc("GET /api/chain", s.handleChain) mux.HandleFunc("POST /api/block", s.handleBlockPush) mux.HandleFunc("POST /api/tx/prepare", s.handleTxPrepare) mux.HandleFunc("POST /api/tx", s.handleTxSubmit) mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hiercoin — endpoints: GET /api/status /api/node/{key} /api/balance/{key} /api/block/{seq} /api/chain?from=N /api/genesis /api/mempool; POST /api/tx/prepare /api/tx /api/block") }) return mux } // =============================================================== cmd // hiercoin — dictator-mode node for the Hiercoin chain. // // hiercoin keygen generate a keypair (seed + pub) // hiercoin init -dir D [-t0 N] [-root-pub HEX] // create validator key + genesis log // hiercoin run -dir D [-listen A] [-interval SEC] // replay log, serve API, produce blocks // hiercoin sign -seed HEX|@file -msg HEX ed25519-sign a message (client helper) func usage() { fmt.Fprintln(os.Stderr, "usage: hiercoin keygen | init | run | join | sign (use -h per command)") os.Exit(2) } func main() { log.SetFlags(log.Ltime) if len(os.Args) < 2 { usage() } switch os.Args[1] { case "keygen": cmdKeygen() case "init": cmdInit(os.Args[2:]) case "run": cmdRun(os.Args[2:]) case "join": cmdJoin(os.Args[2:]) case "sign": cmdSign(os.Args[2:]) default: usage() } } func cmdKeygen() { priv, pub := GenKey() fmt.Printf("seed %x\npub %x\n", priv.Seed(), pub[:]) } func loadSeed(spec string) (ed25519.PrivateKey, error) { s := spec if strings.HasPrefix(spec, "@") { b, err := os.ReadFile(spec[1:]) if err != nil { return nil, err } s = strings.TrimSpace(string(b)) } b, err := hex.DecodeString(s) if err != nil || len(b) != ed25519.SeedSize { return nil, fmt.Errorf("seed must be %d hex bytes", ed25519.SeedSize) } return ed25519.NewKeyFromSeed(b), nil } func cmdSign(args []string) { fs := flag.NewFlagSet("sign", flag.ExitOnError) seed := fs.String("seed", "", "hex seed or @file") msg := fs.String("msg", "", "hex message to sign") fs.Parse(args) priv, err := loadSeed(*seed) if err != nil { log.Fatal(err) } m, err := hex.DecodeString(*msg) if err != nil { log.Fatal(err) } sig := Sign(priv, m) fmt.Printf("%x\n", sig[:]) } func seedPath(dir string) string { return filepath.Join(dir, "validator.seed") } func onionPath(dir string) string { return filepath.Join(dir, "onion") } // saveOnions / loadOnions persist the node's onions, one // " " line each — genesis writes two (one per // seeded trie), and the operator appends a line for every vote later // committed with it as candidate. Any seed lets its holder predict // future rand values, so the file is kept 0600 next to the validator // key. type onionSpec struct { seed [32]byte depth uint64 } func saveOnions(path string, specs ...onionSpec) error { var b strings.Builder for _, sp := range specs { fmt.Fprintf(&b, "%s %d\n", hex.EncodeToString(sp.seed[:]), sp.depth) } return os.WriteFile(path, []byte(b.String()), 0o600) } func loadOnions(path string, candidate PubKey) ([]*Onion, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var onions []*Onion for _, line := range strings.Split(string(b), "\n") { line = strings.TrimSpace(line) if line == "" { continue } var seedHex string var depth uint64 if _, err := fmt.Sscanf(line, "%s %d", &seedHex, &depth); err != nil { return nil, fmt.Errorf("onion file: %w", err) } sb, err := hexN(seedHex, 32) if err != nil { return nil, fmt.Errorf("onion file: %w", err) } var seed [32]byte copy(seed[:], sb) onions = append(onions, NewOnion(candidate, seed, depth)) } if len(onions) == 0 { return nil, errors.New("onion file: no onions") } return onions, nil } func logPath(dir string) string { return filepath.Join(dir, "chain.log") } func cmdInit(args []string) { fs := flag.NewFlagSet("init", flag.ExitOnError) dir := fs.String("dir", "hiercoin-data", "data directory") rootPub := fs.String("root-pub", "", "root person pubkey hex (default: validator key)") depth := fs.Uint64("onion-depth", PeriodSeconds/SlotSeconds, "per-onion depth for the two genesis votes (slots each can validate)") fs.Parse(args) if err := os.MkdirAll(*dir, 0o755); err != nil { log.Fatal(err) } priv, pub := GenKey() if err := os.WriteFile(seedPath(*dir), []byte(hex.EncodeToString(priv.Seed())+"\n"), 0o600); err != nil { log.Fatal(err) } root := pub if *rootPub != "" { k, err := pKey(*rootPub) if err != nil { log.Fatalf("root-pub: %v", err) } root = k } // Genesis onions: one independent hash chain per seeded trie. // Entry 0 (election_trie) validates the remainder of the genesis // period — at most one period of slots minus the genesis slot — // and entry 1 (next_election) the entire following period, so // one period of depth each covers every phase genesis can land // in. Independence matters: layers reveal top-down and any // revealed layer derives all layers above it, so a shared chain // would make the second period's reveals publicly predictable. var seed0, seed1 [32]byte if _, err := rand.Read(seed0[:]); err != nil { log.Fatal(err) } if _, err := rand.Read(seed1[:]); err != nil { log.Fatal(err) } if err := saveOnions(onionPath(*dir), onionSpec{seed0, *depth}, onionSpec{seed1, *depth}); err != nil { log.Fatal(err) } onion0 := NewOnion(pub, seed0, *depth) onion1 := NewOnion(pub, seed1, *depth) t0 := uint64(time.Now().Unix()) ch, err := NewChain(root, t0, priv, onion0.Commit(), onion1.Commit()) if err != nil { log.Fatal(err) } t0 = ch.State.Time // slot-aligned by NewChain if _, err := CreateStore(logPath(*dir), root, t0, onion0.Commit(), onion1.Commit(), &ch.Blocks[0].Header); err != nil { log.Fatal(err) } fmt.Printf("initialized %s\n t0 %d (slot %d)\n root %x\n validator %x\n seed %s\n onions %s (2 × depth %d)\n", *dir, t0, slotOf(t0), root[:], pub[:], seedPath(*dir), onionPath(*dir), *depth) } func cmdRun(args []string) { fs := flag.NewFlagSet("run", flag.ExitOnError) dir := fs.String("dir", "hiercoin-data", "data directory") listen := fs.String("listen", "127.0.0.1:8080", "listen address") peersFlag := fs.String("peers", "", "comma-separated peer base URLs (http://host:port)") fs.Parse(args) priv, err := loadSeed("@" + seedPath(*dir)) if err != nil { log.Fatal(err) } onions, err := loadOnions(onionPath(*dir), Pub(priv)) if err != nil { log.Fatal(err) } store, chain, err := OpenStore(logPath(*dir)) if err != nil { log.Fatal(err) } log.Printf("replayed %d block(s), seq %d, population %d, committed votes %d, onions %d", len(chain.Blocks)-1, chain.State.Seq, chain.State.Tree.Population(), chain.State.Election.CommittedCount(), len(onions)) s := newServer(chain, store, priv, onions, parsePeers(*peersFlag)) go s.produceLoop() if len(s.peers) > 0 { go s.pollLoop() log.Printf("peers: %s", strings.Join(s.peers, ", ")) } log.Printf("listening on http://%s (slot %ds, one block per slot when selected)", *listen, SlotSeconds) log.Fatal(http.ListenAndServe(*listen, cors(s.routes()))) } func parsePeers(s string) []string { var out []string for _, p := range strings.Split(s, ",") { p = strings.TrimSuffix(strings.TrimSpace(p), "/") if p != "" { out = append(out, p) } } return out } func newServer(chain *Chain, store *Store, priv ed25519.PrivateKey, onions []*Onion, peers []string) *Server { return &Server{chain: chain, store: store, valPriv: priv, onions: onions, lastSlot: slotOf(chain.State.Time), peers: peers, client: &http.Client{Timeout: 5 * time.Second}, seen: map[[32]byte]bool{}} } // cmdJoin bootstraps a fresh data directory from a running peer: it // fetches and verifies the genesis record, generates this node's own // validator key and onion (for future candidacy — a joiner produces // nothing until the population votes for it), and writes the log. // The actual block sync happens on `run -peers ...`. func cmdJoin(args []string) { fs := flag.NewFlagSet("join", flag.ExitOnError) dir := fs.String("dir", "hiercoin-data", "data directory") peer := fs.String("peer", "", "peer base URL to bootstrap from (required)") depth := fs.Uint64("onion-depth", PeriodSeconds/SlotSeconds, "onion depth for this node's future votes (a committed entry"+ " lives one period, so one period of slots is the useful max)") fs.Parse(args) if *peer == "" { log.Fatal("join: -peer is required") } if err := os.MkdirAll(*dir, 0o755); err != nil { log.Fatal(err) } url := strings.TrimSuffix(*peer, "/") resp, err := http.Get(url + "/api/genesis") if err != nil { log.Fatal(err) } data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) resp.Body.Close() if err != nil { log.Fatal(err) } // The record arrives with the log's length prefix; write it to // chain.log verbatim and let OpenStore do the full verification. f, err := os.OpenFile(logPath(*dir), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { log.Fatal(err) } if _, err := f.Write(data); err != nil { log.Fatal(err) } f.Close() _, chain, err := OpenStore(logPath(*dir)) if err != nil { log.Fatalf("join: peer genesis rejected: %v", err) } priv, pub := GenKey() if err := os.WriteFile(seedPath(*dir), []byte(hex.EncodeToString(priv.Seed())+"\n"), 0o600); err != nil { log.Fatal(err) } var oseed [32]byte if _, err := rand.Read(oseed[:]); err != nil { log.Fatal(err) } if err := saveOnions(onionPath(*dir), onionSpec{oseed, *depth}); err != nil { log.Fatal(err) } onion := NewOnion(pub, oseed, *depth) gh := chain.Blocks[0].Header.Hash() fmt.Printf("joined %s\n genesis %x (t0 %d)\n validator %x\n vote for me: commit %x (depth %d)\n next: run -dir %s -peers %s\n", *dir, gh[:8], chain.State.Genesis, pub[:], onion.Commit(), *depth, *dir, url) } func cors(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { return } h.ServeHTTP(w, r) }) }