peoplecoin
Pluggable UTXO + UBI by demurrage, compatible with any population register. One person, one unit of stake. The core provides payments, the universal basic income, and consensus; the register is a module — anything that can answer “who is a verified unique human” plugs in. Two implementations ship: bitpeople (periodic anonymous verification) and hierarchy (hierarchical attestation).
Architecture
The cut sits where the spec put it: the value layer and the election mechanics are identity-agnostic. Each module answers the interface’s seven points; everything else is shared.
The core
Trie — the frozen index
A path-compressed binary Merkle radix trie over 32-byte keys, canonical for its key set.
The count aggregate is delta-maintained, never re-derived: Insert, Remove and Touch adjust it
along the path; Refresh rehashes after in-place mutation but never recounts. That makes
the frozen index a structural property — after the swap nothing issues deltas, so a revealed entry
keeps its rank, its leaf count, and its Merkle provability while its commit is consumed.
Mix
Atomic permutation within a trie: inputs consumed, outputs inserted with all fields zeroed —
uniform except mix_count. The budget rule
Σ out ≥ Σ in + count(outputs) lets counts redistribute freely, so no mapping is
visible. One honest participant breaks traceability.
UTXO — value that decays
20% annual decay is the UBI. Rent accrues per second and is settled when the coin moves; it never destroys value — it relocates it from the owner to the validator. Coins whose spendable value reaches zero can be pruned by any producer, who collects the remainder.
rent_owed(T) = rent × (SCALE − decay^(T−time)) / (SCALE − decay)
Election
Entry created (by the module) → Mix breaks the voter↔validator link → Commit sets
the onion top (requires mix_count > 0) → selection by
random_value mod count → each block’s reveal peels one layer. The peel is the shared
role; what a module derives from the nonce beyond it — hierarchy feeds it into its randomness —
is interface point 3 and lives outside the peel. The onion itself is demonstrated below —
with real hashes.
Norm advance is a planned event
Renormalization (sum ×= decay^Δ) rewrites every contribution and every coin hash —
inherently O(N), impossible inside a 60-second slot at scale. So the node runs its whole era on a
fixed epoch (uint128 gives 100+ years of headroom while norms grow), and the advance is a
coordinated, precomputable era upgrade — coordination, not consensus computation.
UTXOSet.Renormalize is the tool for the event.
The hash onion
Every election commit is the top of a multi-validator hash onion. Producing a block
peels one layer: the header carries (validator, nonce), everyone checks
H(validator ‖ nonce) = commit, and the commit becomes the nonce’s trailing
32 bytes — the next layer, possibly naming a different validator. DoS protection at
every reveal, not just the first. A 32-byte nonce is the deepest layer: the entry is consumed.
This onion is real. The hashes below are actual SHA-256 chains — peel it and check.
The interface, answered twice
| Point | bitpeople | hierarchy |
|---|---|---|
| population_count | pair-verified ∨ judged, per period | root.tree_count (persons in the tree) |
| is_verified | Merkle proof of a verified nym entry | tree membership |
| randomness | per-period seed from reveals; sequence public, onion is the DoS shield | per-block: rand ^= H(nonce) at every reveal |
| election entries | auto on Commit(n) and Judge promotion | VoteClaim, once per person per year |
| UBI minting | lump per Vote: TOKEN × (1 − decay^period) | continuous: TOKEN × (1 − decay^(T − last_ubi)) |
| header fields | six trie roots | people_tree, two election roots, rand |
| transactions | Verify · Judge · JudgeNym · Use · Commit · Vote · Reveal | Add · Remove · Move · Leave · Rekey · UBIClaim · VoteClaim · Commit |
The two UBI rows share one kernel — core.AccruedUBI(elapsed) = TOKEN × (1 − decay^elapsed);
the lump is the kernel over one period. The trigger difference is forced, not stylistic: continuous
accrual needs a per-person last_ubi, and any accumulator surviving the Mix would be a tracking
tag through it. A public tree can accrue continuously; an anonymous register must pay a uniform,
vote-gated lump to fresh keys.
bitpeople — anonymous, periodic
Every 28 days the whole population verifies pairwise on live video, pairs drawn by a Feistel shuffle over the register — no physical reordering, ever. Proofs are valid one period and untraceable between periods, because everyone passes through the mixer.
No step can be skipped. The commit is the registration act: it adds the entry to the index (one Touch per Commit), the index locks at the swap, and Reveal may then consume the commit as replay protection — count and ordering are structurally unaffected, and the revealed entry stays a valid RNG target. The election schedule is copied down at the boundary and peeled in place, block by block; the live election trie is always next period’s vote under construction.
hierarchy — attested, continuous
Your superior attested your identity; the tree maps onto civil registration. Persons are
leaves, tree_count aggregates upward, and the operations carry consent: Add and Move
need both signatures, Leave is child-initiated, Remove auto-mints the accrued UBI of everyone
detached, Rekey moves the node to a new key with a replay-counted nonce.
Two election tries swap yearly; randomness is per block, rand ^= H(nonce)
at every reveal — the onion nonce does double duty as entropy. Reveals peel the active trie
live: consumption drops the count, and rand mod count simply adapts.
What it provides — and how a registry plugs in
Payments
A complete UTXO layer: transfers, fees to block producers, pruning of expired dust. Amounts are uint128 fixed-point; supply arithmetic is fraud-provable from the sum aggregate in every header.
Basic income
Funded by demurrage, not taxation: 20% annual decay is the issuance, and each person's holdings converge to one TOKEN. No inflation lever to govern — the money supply follows the population.
Elections & consensus
60-second slots, validator selection from onion-protected commits (DoS shield at every reveal), fork choice by fewest skips, and every block replay-verifiable from genesis.
Proof of person
Membership is provable against any block header: a Merkle proof in bitpeople, the attested tree in hierarchy. External systems verify without trusting a server.
A civil registry as the population module
The hierarchy module is shaped for this: a registry is a people tree — the authority at the root, offices as organization nodes, persons as leaves. The registration acts a registry already performs become signed transactions:
| Registry event | Transaction | On-chain effect |
|---|---|---|
| New registration (birth, immigration) | Add — office + subject consent | Person leaf created; UBI starts accruing immediately |
| Deregistration (death, emigration) | Remove — office signature | Subtree detached; accrued UBI auto-minted to the persons' keys |
| Relocation between offices | Move — subject + receiving office | Node reparented; income and vote state preserved |
| Subject-initiated exit | Leave — subject signature | As Remove, self-signed |
| Lost or rotated credentials | Rekey — old + new key, replay-counted | Key replaced; last_ubi, vote state and children follow |
| Income | UBIClaim — anytime, any cadence | TOKEN × (1 − decay^elapsed) minted; claim timing is economically neutral |
| Validator election | VoteClaim → Mix → Commit — yearly | One vote per person; the mix unlinks voter from validator |
What the registry operates is small: keys for its organization nodes and signatures on the acts above. Money, consensus, elections and audit come with the core — every registration is a signed, replayable transaction, and the tree hash sits in every block header. Where no registry exists, or none is trusted, the same monetary design runs trustlessly under the bitpeople module instead: same coin semantics, a different answer to who counts.
Source — all of it
Every file, grouped by package. Click to expand; syntax coloring is applied on first open. The historical single-file reference bpc (spec v21, pre-onion) ships separately.
built in your browser from the code on this page (zip by JSZip via cdnjs)amount.go141
package core
// amount.go — hiercoin's fixed-point monetary math, ported verbatim.
//
// All UTXO values decay at 20% per YEAR (the demurrage IS the UBI),
// every UTXO pays trie rent, and all exponentiation uses binary
// exponentiation with floor rounding at every step — deterministic
// and bit-reproducible on every node.
import (
"math/big"
"sync"
)
// 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 hiercoin 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 hiercoin 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
// 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, ...).
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.
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
}
// PowScaled is DecayPow generalized to any base (used by tests to
// check the spec constant is the largest valid decay).
func PowScaled(base *big.Int, dt uint64) *big.Int {
res := new(big.Int).Set(Scale)
b := new(big.Int).Set(base)
for e := dt; e > 0; e >>= 1 {
if e&1 == 1 {
res = MulScale(res, b)
}
if e > 1 {
b = MulScale(b, b)
}
}
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)).
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.
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.
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)
}
// SlotSeconds is the shared block slot.
const SlotSeconds = uint64(60)
coin.go126
package core
import (
"errors"
"math/big"
)
// ============================================================== utxo
// CoinEntry — the payment layer, keyed by hash(tx || idx).
type CoinEntry struct {
Owner PubKey
Amount *big.Int
Norm *big.Int // amount normalized to norm_time
Time uint64
}
func (e *CoinEntry) Value(T uint64) *big.Int {
if T < e.Time {
panic("value query before creation")
}
return MulScale(e.Amount, DecayPow(T-e.Time))
}
func (e *CoinEntry) Rent(T uint64) *big.Int { return RentOwed(T - e.Time) }
func (e *CoinEntry) Spendable(T uint64) *big.Int {
return new(big.Int).Sub(e.Value(T), e.Rent(T))
}
func (e *CoinEntry) Expired(T uint64) bool { return e.Spendable(T).Sign() <= 0 }
func (e *CoinEntry) LeafHash(k [32]byte) [32]byte {
var w Buf
w.U8(0x16)
w.Bytes(k[:])
w.Bytes(e.Owner[:])
w.U128(e.Amount)
w.U64(e.Time)
return H(w.B)
}
// Outpoint → key: hash(tx || idx), per the spec.
func CoinKey(tx [32]byte, index uint32) [32]byte {
var w Buf
w.Bytes(tx[:])
w.U32(index)
return H(w.B)
}
// CoinEntry implements Entry so coins live in the same trie
// structure; CoinBranch carries sum and no count.
func (e *CoinEntry) Counted() bool { return true }
func (e *CoinEntry) HitsVal() uint32 { return 0 }
func (e *CoinEntry) MixCt() uint32 { return 0 }
func (e *CoinEntry) SumVal() *big.Int { return e.Norm }
func (e *CoinEntry) CloneEntry() Entry {
c := *e
c.Amount = new(big.Int).Set(e.Amount)
c.Norm = new(big.Int).Set(e.Norm)
return &c
}
// UTXOSet is the value trie: CoinBranch {sum, left, right}.
type UTXOSet struct{ t *Trie }
func NewUTXOSet() *UTXOSet {
return &UTXOSet{t: NewTrie(0x17, nil, false, true)}
}
func (u *UTXOSet) Insert(k [32]byte, e *CoinEntry) error { return u.t.Insert(k, e) }
func (u *UTXOSet) Get(k [32]byte) *CoinEntry {
if e := u.t.Get(k); e != nil {
return e.(*CoinEntry)
}
return nil
}
func (u *UTXOSet) Spend(k [32]byte) (*CoinEntry, error) {
e, err := u.t.Remove(k)
if err != nil {
return nil, errors.New("utxo: missing or already spent")
}
return e.(*CoinEntry), nil
}
func (u *UTXOSet) Sum() *big.Int { return u.t.Sum() }
func (u *UTXOSet) Len() int { return u.t.Len() }
func (u *UTXOSet) Root() [32]byte { return u.t.Root() }
func (u *UTXOSet) Clone() *UTXOSet { return &UTXOSet{t: u.t.Clone()} }
func (u *UTXOSet) ForEach(f func(k [32]byte, e *CoinEntry)) {
u.t.ForEach(func(k [32]byte, e Entry) { f(k, e.(*CoinEntry)) })
}
// Renormalize advances the norm_time epoch: "When norm_time
// advances by Δ: sum ×= decay^Δ". Applied per contribution (floor)
// with branch sums rebuilt exactly, so parent = left + right holds
// and sum-Merkle proofs stay sound; the root lands within one base
// unit per leaf of the ideal rescale. Entries themselves store
// {amount, time} and are never touched — which is also why the
// naive alternative (recompute norms from raw amounts) is wrong:
// after an advance every existing entry has time < norm_time, a
// negative exponent the fixed-point math has no business seeing.
// Advancement is rare by design: uint128 leaves 100+ years of
// headroom at planetary scale.
func (u *UTXOSet) Renormalize(oldNorm, newNorm uint64) {
factor := DecayPow(newNorm - oldNorm)
var walk func(n *node) *big.Int
walk = func(n *node) *big.Int {
if n == nil {
return new(big.Int)
}
n.dirty = true
if n.isLeaf() {
ce := n.entry.(*CoinEntry)
ce.Norm = MulScale(ce.Norm, factor)
n.sum = new(big.Int).Set(ce.Norm)
return n.sum
}
n.sum = new(big.Int).Add(walk(n.left), walk(n.right))
return n.sum
}
walk(u.t.root)
}
core_test.go117
package core
import (
"math/big"
"testing"
)
// TestOnion — the multi-validator hash onion: peels through
// different validators in order, rejects wrong material, consumes
// at the deepest (32-byte) layer.
func TestOnion(t *testing.T) {
var vals []PubKey
var privs [][32]byte
for i := 0; i < 4; i++ {
_, p := GenKey()
vals = append(vals, p)
privs = append(privs, [32]byte(p))
}
_ = privs
ctr := byte(0)
det := func(p []byte) {
for i := range p {
p[i] = ctr
ctr++
}
}
commit, layers := BuildOnion(vals, det)
if len(layers) != 4 {
t.Fatal("layer count")
}
cur := commit
for i, l := range layers {
// Wrong validator fails.
wrong := vals[(i+1)%4]
if _, ok := VerifyPeel(cur, wrong, l.Nonce); ok && wrong != l.Validator {
t.Fatalf("layer %d peeled by the wrong validator", i)
}
// Wrong nonce fails.
bad := append([]byte{}, l.Nonce...)
bad[0] ^= 1
if _, ok := VerifyPeel(cur, l.Validator, bad); ok {
t.Fatalf("layer %d peeled with a tampered nonce", i)
}
next, ok := VerifyPeel(cur, l.Validator, l.Nonce)
if !ok {
t.Fatalf("layer %d rejected valid reveal", i)
}
if i < 3 {
if len(l.Nonce) != 64 || next == Zero32 {
t.Fatalf("layer %d: expected 64-byte nonce with successor", i)
}
} else {
if len(l.Nonce) != 32 || next != Zero32 {
t.Fatal("deepest layer must consume (32-byte nonce, zero successor)")
}
}
cur = next
}
}
// TestNormAdvance — the planned-event tool: norm ×= decay^Δ per
// contribution, exact branch sums, real values untouched.
func TestNormAdvance(t *testing.T) {
old := uint64(14) * NormPeriod
newN := old + NormPeriod
u := NewUTXOSet()
var keys [][32]byte
var oldNorms []*big.Int
for i := 0; i < 7; i++ {
_, p := GenKey()
amt := new(big.Int).Mul(big.NewInt(int64(i+1)), big.NewInt(1_000_000_000))
tm := old + uint64(i)*100000
k := CoinKey(H([]byte{byte(i)}, []byte("norm")), 0)
nv := Normalize(amt, tm, old)
if err := u.Insert(k, &CoinEntry{Owner: p, Amount: amt, Norm: nv, Time: tm}); err != nil {
t.Fatal(err)
}
keys = append(keys, k)
oldNorms = append(oldNorms, new(big.Int).Set(nv))
}
T := newN + 1000*SlotSeconds
valBefore := u.Get(keys[3]).Value(T)
sumBefore := u.Sum()
u.Renormalize(old, newN)
factor := DecayPow(NormPeriod)
total := new(big.Int)
for i, k := range keys {
want := MulScale(oldNorms[i], factor)
if u.Get(k).Norm.Cmp(want) != 0 {
t.Fatalf("leaf %d rescale wrong", i)
}
total.Add(total, want)
}
if u.Sum().Cmp(total) != 0 {
t.Fatal("branch sums not exact over rescaled leaves")
}
ideal := MulScale(sumBefore, factor)
diff := new(big.Int).Sub(ideal, u.Sum())
if diff.Sign() < 0 || diff.Cmp(big.NewInt(int64(len(keys)))) > 0 {
t.Fatalf("sum ×= decay^Δ off by %s", diff)
}
if u.Get(keys[3]).Value(T).Cmp(valBefore) != 0 {
t.Fatal("rescale changed a real value")
}
}
// TestDecayConstant — decay^YEAR ≈ 0.8: 20% annual demurrage.
func TestDecayConstant(t *testing.T) {
p := DecayPow(SecondsPerYear)
want := new(big.Int).Quo(new(big.Int).Mul(Scale, big.NewInt(8)), big.NewInt(10))
diff := new(big.Int).Sub(p, want)
diff.Abs(diff)
lim := new(big.Int).Quo(Scale, big.NewInt(1_000_000))
if diff.Cmp(lim) > 0 {
t.Fatalf("decay^YEAR = %s, want ≈ %s", p, want)
}
}
election.go97
package core
import "math/big"
// =========================================================== election
// ElectionEntry — commit = hash(validator || nonce).
type ElectionEntry struct {
MixCount uint32
Commit [32]byte
}
func (e *ElectionEntry) LeafHash(k [32]byte) [32]byte {
var w Buf
w.U8(0x14)
w.Bytes(k[:])
w.U32(e.MixCount)
w.Bytes(e.Commit[:])
return H(w.B)
}
func (e *ElectionEntry) Counted() bool { return e.Commit != [32]byte{} }
func (e *ElectionEntry) HitsVal() uint32 { return 0 }
func (e *ElectionEntry) SumVal() *big.Int { return nil }
func (e *ElectionEntry) MixCt() uint32 { return e.MixCount }
func (e *ElectionEntry) CloneEntry() Entry { c := *e; return &c }
func FreshElection(mc uint32) Entry { return &ElectionEntry{MixCount: mc} }
func NewElectionTrie() *Trie { return NewTrie(0x15, FreshElection, false, false) }
// ======================================================== hash onion
//
// Multi-validator hash onion: each layer can name a different
// validator — DoS protection at every reveal, not just the first.
//
// layer_0 = H(val_Z ‖ nonce_Z) (deepest: 32-byte nonce)
// layer_1 = H(val_Y ‖ nonce_Y ‖ layer_0)
// commit = H(val_X ‖ nonce_X ‖ layer_1)
//
// The voter constructs the onion and gives each validator their
// (nonce ‖ next_layer) privately — 64 bytes, or 32 at the deepest
// layer. The block header carries (validator, nonce); verification
// is H(validator ‖ nonce) == commit, after which commit becomes the
// last 32 bytes of the nonce. A 32-byte nonce consumes the entry.
// OnionLayer is one validator's private reveal material.
type OnionLayer struct {
Validator PubKey
Nonce []byte // 64 bytes, or 32 at the deepest layer
}
// BuildOnion constructs an onion naming the given validators from
// the outermost layer inward, returning the commit (the top) and
// each layer's reveal material.
func BuildOnion(validators []PubKey, rand func([]byte)) (commit [32]byte, layers []OnionLayer) {
n := len(validators)
layers = make([]OnionLayer, n)
below := [32]byte{}
for i := n - 1; i >= 0; i-- {
var r [32]byte
rand(r[:])
var nonce []byte
if i == n-1 {
nonce = append([]byte{}, r[:]...) // deepest: 32 bytes
} else {
nonce = append(append([]byte{}, r[:]...), below[:]...) // 64
}
layers[i] = OnionLayer{Validator: validators[i], Nonce: nonce}
below = H(validators[i][:], nonce)
}
return below, layers
}
// VerifyPeel checks a reveal against a commit and returns the
// commit's successor: the nonce's trailing 32 bytes, or zero when
// the deepest (32-byte) nonce consumed the onion. A zero commit is
// "consumed" in every module's semantics — uncounted, unprovable.
//
// Peeling is the election's shared role. Anything else a module
// derives from the nonce — hierarchy feeds it into its per-block
// randomness — is module RNG (interface point 3) and lives outside
// the peel.
func VerifyPeel(commit [32]byte, validator PubKey, nonce []byte) ([32]byte, bool) {
if len(nonce) != 32 && len(nonce) != 64 {
return Zero32, false
}
if H(validator[:], nonce) != commit {
return Zero32, false
}
if len(nonce) == 32 {
return Zero32, true // onion spent
}
var next [32]byte
copy(next[:], nonce[32:])
return next, true
}
enc.go81
package core
// enc.go — canonical encoding + hashing primitives.
//
// Buf is a tiny canonical encoder: fixed-width big-endian fields.
// Every hash and signature in the system is computed over encodings
// produced by this type, so the byte layout here IS the consensus
// format. Ported from hiercoin.
import (
"crypto/sha256"
"encoding/binary"
"math/big"
)
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. SHA-256 is the only
// hash in the system (both parent specs).
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
func Xor32(a, b [32]byte) (o [32]byte) {
for i := range o {
o[i] = a[i] ^ b[i]
}
return
}
func U64be(x uint64) []byte {
var t [8]byte
binary.BigEndian.PutUint64(t[:], x)
return t[:]
}
keys.go48
package core
// keys.go — Ed25519, the signature scheme of both parent specs.
// Every signature in the system covers an encoding that begins with
// an opcode ("all signatures include an opcode" — hiercoin).
import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
)
// PubKey is a raw Ed25519 public key. In bitpeople terms this is a
// nym address; in hiercoin terms a node/owner key. Addresses and
// keys are the same 32-byte type throughout.
type PubKey [32]byte
// Sig is a raw Ed25519 signature.
type Sig [64]byte
func GenKey() (ed25519.PrivateKey, PubKey) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
var p PubKey
copy(p[:], pub)
return priv, p
}
func Pub(priv ed25519.PrivateKey) PubKey {
var p PubKey
copy(p[:], priv.Public().(ed25519.PublicKey))
return p
}
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]) }
mix.go121
package core
// Mix — the shared generic primitive: atomic permutation within a
// trie. Phase gating and trie routing belong to the population
// module; the core validates and applies.
import (
"errors"
"fmt"
)
const (
OpTransfer byte = 0x01
OpPrune byte = 0x02
OpMix byte = 0x20
MaxInputs = 1024
MaxOutputs = 1024
)
type Tx interface {
Body() []byte
TxID() [32]byte
}
// =============================================================== mix
// Mix — the generic primitive: atomic permutation within a trie.
// Inputs consumed (uncommitted/unused only — commit and use end an
// entry's mixable life), outputs recreated under fresh keys with all
// fields zeroed except the declared mix_count.
type Mix struct {
TrieID byte
Inputs [][32]byte
Outputs []MixOutput
Sigs []Sig
}
type MixOutput struct {
Key [32]byte
MixCount uint32
}
func (t *Mix) Body() []byte {
var w Buf
w.U8(OpMix)
w.U8(t.TrieID)
w.U32(uint32(len(t.Inputs)))
for i := range t.Inputs {
w.Bytes(t.Inputs[i][:])
}
w.U32(uint32(len(t.Outputs)))
for i := range t.Outputs {
w.Bytes(t.Outputs[i].Key[:])
w.U32(t.Outputs[i].MixCount)
}
return w.B
}
func (t *Mix) TxID() [32]byte { return H(t.Body()) }
// ApplyMix validates and applies a Mix against its routed trie —
// the population module resolves TrieID and gates the phase.
func ApplyMix(trie *Trie, t *Mix) error {
n := len(t.Inputs)
if n < 2 || n > MaxInputs {
return errors.New("mix: need at least 2 inputs")
}
if len(t.Outputs) != n || len(t.Sigs) != n {
return errors.New("mix: inputs, outputs and sigs must match in count")
}
id := t.TxID()
inSum := uint64(0)
seen := map[[32]byte]struct{}{}
for i, k := range t.Inputs {
if _, dup := seen[k]; dup {
return errors.New("mix: duplicate input")
}
seen[k] = struct{}{}
e := trie.Get(k)
if e == nil {
return errors.New("mix: input missing or consumed")
}
if e.Counted() {
return errors.New("mix: input already committed/used")
}
if !VerifySig(PubKey(k), id[:], t.Sigs[i]) {
return fmt.Errorf("mix: bad signature for input %d", i)
}
inSum += uint64(e.MixCt())
}
outSum := uint64(0)
oseen := map[[32]byte]struct{}{}
for i := range t.Outputs {
o := &t.Outputs[i]
if _, dup := oseen[o.Key]; dup {
return errors.New("mix: duplicate output key")
}
oseen[o.Key] = struct{}{}
if _, wasIn := seen[o.Key]; wasIn {
return errors.New("mix: output key reuses an input key")
}
if trie.Get(o.Key) != nil {
return errors.New("mix: output key not fresh")
}
outSum += uint64(o.MixCount)
}
if outSum < inSum+uint64(n) {
return fmt.Errorf("mix: mix_count budget %d < %d required", outSum, inSum+uint64(n))
}
for _, k := range t.Inputs {
if _, err := trie.Remove(k); err != nil {
return err
}
}
for i := range t.Outputs {
if err := trie.Insert(t.Outputs[i].Key, trie.Fresh(t.Outputs[i].MixCount)); err != nil {
return err
}
}
return nil
}
transfer.go129
package core
// Transfer and coin pruning — standard UTXO semantics. The
// difference between input value and outputs is the fee, and the
// rent is part of it: rent does not destroy value, it relocates it
// from the UTXO owner to the validator upon collection.
import (
"errors"
"fmt"
"math/big"
)
// ========================================================= utxo layer
type Outpoint struct {
Tx [32]byte
Index uint32
}
type Output struct {
Amount *big.Int
Owner PubKey
}
// Transfer — standard UTXO semantics; the difference is the fee.
type Transfer struct {
Inputs []Outpoint
Outputs []Output
Sigs []Sig
}
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)
}
w.U32(uint32(len(t.Outputs)))
for i := range t.Outputs {
w.U128(t.Outputs[i].Amount)
w.Bytes(t.Outputs[i].Owner[:])
}
return w.B
}
func (t *Transfer) TxID() [32]byte { return H(t.Body()) }
// ApplyTransfer spends and creates; returns the fee (value minus
// outputs — the rent relocates to the validator here).
func ApplyTransfer(u *UTXOSet, t *Transfer, T, normTime uint64) (*big.Int, error) {
if len(t.Inputs) == 0 || len(t.Inputs) > MaxInputs {
return nil, errors.New("transfer: bad input count")
}
if len(t.Outputs) == 0 || len(t.Outputs) > MaxOutputs {
return nil, errors.New("transfer: bad output count")
}
if len(t.Sigs) != len(t.Inputs) {
return nil, errors.New("transfer: need one signature per input")
}
id := t.TxID()
outSum := new(big.Int)
for i := range t.Outputs {
if t.Outputs[i].Amount == nil || t.Outputs[i].Amount.Sign() <= 0 ||
t.Outputs[i].Amount.BitLen() > 128 {
return nil, errors.New("transfer: bad output amount")
}
outSum.Add(outSum, t.Outputs[i].Amount)
}
inVal, inSpend := new(big.Int), new(big.Int)
seen := map[[32]byte]struct{}{}
for i, op := range t.Inputs {
k := CoinKey(op.Tx, op.Index)
if _, dup := seen[k]; dup {
return nil, errors.New("transfer: duplicate input")
}
seen[k] = struct{}{}
e := u.Get(k)
if e == nil {
return nil, errors.New("transfer: input missing or spent")
}
if !VerifySig(e.Owner, id[:], t.Sigs[i]) {
return nil, fmt.Errorf("transfer: bad signature for input %d", i)
}
inVal.Add(inVal, e.Value(T))
inSpend.Add(inSpend, e.Spendable(T))
}
if outSum.Cmp(inSpend) > 0 {
return nil, fmt.Errorf("transfer: outputs %s exceed spendable %s", outSum, inSpend)
}
for _, op := range t.Inputs {
if _, err := u.Spend(CoinKey(op.Tx, op.Index)); err != nil {
return nil, err
}
}
for i := range t.Outputs {
if err := u.Insert(CoinKey(id, uint32(i)), &CoinEntry{
Owner: t.Outputs[i].Owner, Amount: t.Outputs[i].Amount,
Norm: Normalize(t.Outputs[i].Amount, T, normTime), Time: T,
}); err != nil {
return nil, err
}
}
return new(big.Int).Sub(inVal, outSum), nil
}
// PruneCoins removes expired UTXOs; the validator collects the
// remaining value — economically incentivized cleanup.
func PruneCoins(u *UTXOSet, keys [][32]byte, T uint64) (*big.Int, error) {
if len(keys) == 0 || len(keys) > MaxInputs {
return nil, errors.New("prune: bad key count")
}
fee := new(big.Int)
for _, k := range keys {
e := u.Get(k)
if e == nil {
return nil, errors.New("prune: utxo missing")
}
if !e.Expired(T) {
return nil, errors.New("prune: utxo not expired")
}
fee.Add(fee, e.Value(T))
if _, err := u.Spend(k); err != nil {
return nil, err
}
}
return fee, nil
}
trie.go528
package core
// trie.go — the spec's binary Merkle trie, structurally: a
// path-compressed binary radix trie over 32-byte keys.
//
// - Canonical for a given key set: every branch sits at the first
// bit position where its subtree's keys differ, so the shape is
// independent of insertion order.
// - O(log N) expected insert/remove/lookup for hash keys; Merkle
// proofs are the path hashes — proof-of-unique-human per spec.
// - Branch aggregates are type-specific: count (+ max_seed_hits
// for nym tries) on identity tries, sum on the UTXO trie.
//
// The count aggregate is delta-maintained, never re-derived: Insert,
// Remove and Touch adjust it along the path, while Refresh (in-place
// mutation of flags, seed_hits, or commit consumption by Reveal)
// recomputes hashes and max_seed_hits but not counts. That is the
// locked index as structure: after the swap nothing issues count
// deltas on an active trie, so "count and ordering are unaffected".
import (
"errors"
"math/big"
)
// Entry is a trie payload.
type Entry interface {
LeafHash(key [32]byte) [32]byte
Counted() bool // the registration act: counts toward the index
HitsVal() uint32 // seed_hits (nym entries; 0 elsewhere)
SumVal() *big.Int // normalized value (coin entries; nil elsewhere)
MixCt() uint32
CloneEntry() Entry
}
func bitAt(k [32]byte, i int) int { return int(k[i>>3]>>(7-uint(i&7))) & 1 }
func firstDiffBit(a, b [32]byte) int {
for i := 0; i < 32; i++ {
if x := a[i] ^ b[i]; x != 0 {
d := i * 8
for x&0x80 == 0 {
x <<= 1
d++
}
return d
}
}
return -1
}
type node struct {
// leaf fields (entry != nil)
key [32]byte
entry Entry
// branch fields
bit int
left, right *node
// aggregates and cached hash
count uint64 // delta-maintained index membership (uint64: v21,
// the register must cover 10+ billion people)
hits uint32 // max seed_hits in subtree (lazy, with hash)
sum *big.Int // value tries only (delta-maintained)
hash [32]byte
dirty bool
}
func (n *node) isLeaf() bool { return n.entry != nil }
// Trie configuration mirrors the spec's per-type branch structs.
type Trie struct {
tag byte // branch hash domain
fresh func(mixCount uint32) Entry
withHits bool // NymBranch carries max_seed_hits
withSum bool // CoinBranch carries sum (and no count)
root *node
n int
}
// NewTrie configures a trie per its spec branch struct: withHits
// adds max_seed_hits to the branch hash, withSum switches to the
// value-trie form (sum, no count).
func NewTrie(tag byte, fresh func(uint32) Entry, withHits, withSum bool) *Trie {
return &Trie{tag: tag, fresh: fresh, withHits: withHits, withSum: withSum}
}
// Fresh builds a zeroed entry with the given mix_count (Mix outputs).
func (t *Trie) Fresh(mc uint32) Entry { return t.fresh(mc) }
func c01(e Entry) uint64 {
if e.Counted() {
return 1
}
return 0
}
func (t *Trie) sumOf(e Entry) *big.Int {
if !t.withSum {
return nil
}
if v := e.SumVal(); v != nil {
return new(big.Int).Set(v)
}
return new(big.Int)
}
// findLeaf walks by key bits to the leaf the key would occupy.
func (t *Trie) findLeaf(k [32]byte) *node {
n := t.root
for n != nil && !n.isLeaf() {
if bitAt(k, n.bit) == 0 {
n = n.left
} else {
n = n.right
}
}
return n
}
func (t *Trie) Get(k [32]byte) Entry {
if l := t.findLeaf(k); l != nil && l.key == k {
return l.entry
}
return nil
}
// Insert adds a fresh entry — the key must not exist.
func (t *Trie) Insert(k [32]byte, e Entry) error {
nl := &node{key: k, entry: e, count: c01(e), sum: t.sumOf(e), dirty: true}
if t.root == nil {
t.root = nl
t.n = 1
return nil
}
probe := t.findLeaf(k)
if probe.key == k {
return errors.New("trie: key already exists")
}
d := firstDiffBit(k, probe.key)
// Splice a branch at bit d: descend while branches sit above d.
link := &t.root
for {
n := *link
if n.isLeaf() || n.bit > d {
break
}
if bitAt(k, n.bit) == 0 {
link = &n.left
} else {
link = &n.right
}
}
old := *link
br := &node{bit: d, count: old.count, dirty: true}
if t.withSum {
br.sum = new(big.Int)
if old.sum != nil {
br.sum.Set(old.sum)
}
}
if bitAt(k, d) == 0 {
br.left, br.right = nl, old
} else {
br.left, br.right = old, nl
}
*link = br
// Path deltas from the root down to (and excluding) the new
// leaf — the leaf was created with its own aggregates.
dc, ds := nl.count, nl.sum
n := t.root
for n != nl {
n.count += dc
if t.withSum && ds != nil {
n.sum.Add(n.sum, ds)
}
n.dirty = true
if n.isLeaf() {
break
}
if bitAt(k, n.bit) == 0 {
n = n.left
} else {
n = n.right
}
}
t.n++
return nil
}
// Remove consumes an entry (Mix inputs, Prune, UTXO spends).
func (t *Trie) Remove(k [32]byte) (Entry, error) {
var removed Entry
var dc uint64
var ds *big.Int
var rec func(n *node) (*node, bool)
rec = func(n *node) (*node, bool) {
if n == nil {
return nil, false
}
if n.isLeaf() {
if n.key != k {
return n, false
}
removed, dc, ds = n.entry, n.count, n.sum
return nil, true
}
child, sib := &n.left, n.right
if bitAt(k, n.bit) == 1 {
child, sib = &n.right, n.left
}
nc, found := rec(*child)
if !found {
return n, false
}
if nc == nil {
return sib, true // collapse the branch
}
*child = nc
n.count -= dc
if t.withSum && ds != nil {
n.sum.Sub(n.sum, ds)
}
n.dirty = true
return n, true
}
nr, found := rec(t.root)
if !found {
return nil, errors.New("trie: key missing")
}
t.root = nr
t.n--
return removed, nil
}
// Touch re-indexes a key after its registration act (Commit / Use):
// the count delta propagates along the path. This is how the index
// is established, one act at a time; active tries never Touch.
func (t *Trie) Touch(k [32]byte) {
l := t.findLeaf(k)
if l == nil || l.key != k {
return
}
newC := c01(l.entry)
delta := int64(newC) - int64(l.count)
n := t.root
for {
n.count = uint64(int64(n.count) + delta)
n.dirty = true
if n.isLeaf() {
break
}
if bitAt(k, n.bit) == 0 {
n = n.left
} else {
n = n.right
}
}
}
// Refresh marks a key's path for rehash after an in-place mutation
// that must not move the index: flags, seed_hits, commit consumption.
func (t *Trie) Refresh(k [32]byte) {
n := t.root
for n != nil {
n.dirty = true
if n.isLeaf() {
return
}
if bitAt(k, n.bit) == 0 {
n = n.left
} else {
n = n.right
}
}
}
func (t *Trie) Count() uint64 {
if t.root == nil {
return 0
}
return t.root.count
}
func (t *Trie) Len() int { return t.n }
func (t *Trie) Sum() *big.Int {
if t.root == nil || t.root.sum == nil {
return new(big.Int)
}
return new(big.Int).Set(t.root.sum)
}
// Rank: a counted key's position in key order — the in-order
// traversal via the count aggregates, O(depth).
func (t *Trie) Rank(k [32]byte) (uint64, bool) {
rank := uint64(0)
n := t.root
for n != nil {
if n.isLeaf() {
if n.key == k && n.count == 1 {
return rank, true
}
return 0, false
}
if bitAt(k, n.bit) == 0 {
n = n.left
} else {
rank += n.left.count
n = n.right
}
}
return 0, false
}
// Select: the counted key at a given rank — the inverse walk.
func (t *Trie) Select(rank uint64) ([32]byte, bool) {
if t.root == nil || rank >= t.root.count {
return [32]byte{}, false
}
n := t.root
for !n.isLeaf() {
if rank < n.left.count {
n = n.left
} else {
rank -= n.left.count
n = n.right
}
}
return n.key, true
}
// CommittedKeys: the counted set in key order (in-order walk).
func (t *Trie) CommittedKeys() [][32]byte {
out := make([][32]byte, 0, t.Count())
var walk func(n *node)
walk = func(n *node) {
if n == nil {
return
}
if n.isLeaf() {
if n.count == 1 {
out = append(out, n.key)
}
return
}
walk(n.left)
walk(n.right)
}
walk(t.root)
return out
}
func (t *Trie) ForEach(f func(k [32]byte, e Entry)) {
var walk func(n *node)
walk = func(n *node) {
if n == nil {
return
}
if n.isLeaf() {
f(n.key, n.entry)
return
}
walk(n.left)
walk(n.right)
}
walk(t.root)
}
// rehash recomputes dirty hashes and max_seed_hits bottom-up.
func (t *Trie) rehash(n *node) {
if n == nil || !n.dirty {
return
}
if n.isLeaf() {
n.hits = n.entry.HitsVal()
n.hash = n.entry.LeafHash(n.key)
n.dirty = false
return
}
t.rehash(n.left)
t.rehash(n.right)
n.hits = n.left.hits
if n.right.hits > n.hits {
n.hits = n.right.hits
}
n.hash = t.branchHash(n.count, n.hits, n.sum, n.left.hash, n.right.hash)
n.dirty = false
}
// branchHash encodes exactly the spec's per-type branch struct —
// count is uint64 (v21).
func (t *Trie) branchHash(count uint64, hits uint32, sum *big.Int, l, r [32]byte) [32]byte {
var w Buf
w.U8(t.tag)
if t.withSum {
w.U128(sum)
} else {
w.U64(count)
if t.withHits {
w.U32(hits)
}
}
w.Bytes(l[:])
w.Bytes(r[:])
return H(w.B)
}
// SelectMaxHits walks the max_seed_hits aggregate to the winner:
// highest seed_hits, leftmost (= lowest key = lowest rank) on ties.
// This is what NymBranch carries the field for — O(log N) instead
// of scanning the population.
func (t *Trie) SelectMaxHits() ([32]byte, uint32, bool) {
if t.root == nil {
return [32]byte{}, 0, false
}
t.rehash(t.root) // hits are maintained lazily with the hashes
if t.root.hits == 0 {
return [32]byte{}, 0, false
}
n := t.root
for !n.isLeaf() {
if n.left.hits >= n.right.hits {
n = n.left
} else {
n = n.right
}
}
return n.key, n.hits, true
}
func (t *Trie) Root() [32]byte {
if t.root == nil {
return Zero32
}
t.rehash(t.root)
return t.root.hash
}
func (t *Trie) Clone() *Trie {
c := &Trie{tag: t.tag, fresh: t.fresh, withHits: t.withHits, withSum: t.withSum, n: t.n}
var cp func(n *node) *node
cp = func(n *node) *node {
if n == nil {
return nil
}
m := *n
if n.entry != nil {
m.entry = n.entry.CloneEntry()
}
if n.sum != nil {
m.sum = new(big.Int).Set(n.sum)
}
m.left, m.right = cp(n.left), cp(n.right)
return &m
}
c.root = cp(t.root)
return c
}
// ============================================================ proofs
// ProofStep is one branch on the path from a leaf to the root, seen
// from the proven key's side.
type ProofStep struct {
Bit int
SibHash [32]byte
SibCount uint64
SibHits uint32
SibSum *big.Int
}
// MerkleProof returns the leaf's frozen count and the sibling chain,
// leaf-to-root. With the trie root from a header, the pair proves
// the entry — a counted, verified nym entry is a
// proof-of-unique-human.
func (t *Trie) MerkleProof(k [32]byte) (leafCount uint64, steps []ProofStep, err error) {
t.rehash(t.root)
var path []*node
n := t.root
for n != nil && !n.isLeaf() {
path = append(path, n)
if bitAt(k, n.bit) == 0 {
n = n.left
} else {
n = n.right
}
}
if n == nil || n.key != k {
return 0, nil, errors.New("trie: key missing")
}
for i := len(path) - 1; i >= 0; i-- {
br := path[i]
sib := br.right
if bitAt(k, br.bit) == 1 {
sib = br.left
}
steps = append(steps, ProofStep{
Bit: br.bit, SibHash: sib.hash, SibCount: sib.count,
SibHits: sib.hits, SibSum: sib.sum,
})
}
return n.count, steps, nil
}
// VerifyProof recomputes the root from an entry, its frozen leaf
// count, and the sibling chain.
func (t *Trie) VerifyProof(root [32]byte, k [32]byte, e Entry, leafCount uint64, steps []ProofStep) bool {
h := e.LeafHash(k)
count, hits := leafCount, e.HitsVal()
sum := t.sumOf(e)
for _, s := range steps {
var l, r [32]byte
if bitAt(k, s.Bit) == 0 {
l, r = h, s.SibHash
} else {
l, r = s.SibHash, h
}
count += s.SibCount
if s.SibHits > hits {
hits = s.SibHits
}
if t.withSum && s.SibSum != nil {
sum.Add(sum, s.SibSum)
}
h = t.branchHash(count, hits, sum, l, r)
}
return h == root
}
bpc_test.go1026
package bitpeople
import (
"bytes"
"crypto/ed25519"
"math/big"
mrand "math/rand"
"sort"
"testing"
"peoplecoin/core"
)
// fixture builds a live state mid-Event with N committed nyms.
func fixture(t *testing.T, n int) (*State, []ed25519.PrivateKey, [][32]byte) {
t.Helper()
s := &State{
NormTime: core.NormTimeFor(14 * core.NormPeriod),
Time: 14 * core.NormPeriod, Genesis: 14 * core.NormPeriod,
Nym: NewNymTrie(), NextNym: NewNymTrie(),
Invite: NewInviteTrie(), NextInvite: NewInviteTrie(),
Election: core.NewElectionTrie(), UTXO: core.NewUTXOSet(),
Seed: core.H([]byte("fixture seed")),
XorAcc: map[[32]byte][32]byte{},
}
privs := make([]ed25519.PrivateKey, n)
pres := make([][32]byte, n)
for i := 0; i < n; i++ {
priv, pub := core.GenKey()
privs[i] = priv
pres[i] = core.H([]byte{byte(i)}, []byte("pre"))
if err := s.Nym.Insert([32]byte(pub), &NymEntry{Commit: core.H(pres[i][:])}); err != nil {
t.Fatal(err)
}
}
return s, privs, pres
}
func eventT(s *State) uint64 { return s.Genesis + 2*core.SlotSeconds }
func rngT(s *State) uint64 { return s.Genesis + EventSeconds + core.SlotSeconds }
func TestDecayConstant(t *testing.T) {
y := core.DecayPow(core.SecondsPerYear)
target := new(big.Int).Div(new(big.Int).Mul(core.Scale, big.NewInt(8)), big.NewInt(10))
if y.Cmp(target) >= 0 {
t.Fatalf("decay^YEAR = %s, want < 0.8×Scale", y)
}
lo := new(big.Int).Div(new(big.Int).Mul(core.Scale, big.NewInt(79)), big.NewInt(100))
if y.Cmp(lo) < 0 {
t.Fatalf("decay^YEAR = %s, way below 0.79×Scale", y)
}
}
// TestMixRules: the generic Mix constraints.
func TestMixRules(t *testing.T) {
s, privs, _ := fixture(t, 2)
T := eventT(s)
// Two next_nym entries via Verify pair completion is overkill —
// insert directly.
k1, k2 := [32]byte(core.Pub(privs[0])), [32]byte(core.Pub(privs[1]))
s.NextNym.Insert(k1, freshNym(0))
s.NextNym.Insert(k2, freshNym(0))
mk := func(outMC1, outMC2 uint32) (*core.Mix, []ed25519.PrivateKey) {
o1p, o1 := core.GenKey()
o2p, o2 := core.GenKey()
m := &core.Mix{TrieID: TrieNextNym,
Inputs: [][32]byte{k1, k2},
Outputs: []core.MixOutput{
{Key: [32]byte(o1), MixCount: outMC1},
{Key: [32]byte(o2), MixCount: outMC2},
}}
id := m.TxID()
m.Sigs = []core.Sig{core.Sign(privs[0], id[:]), core.Sign(privs[1], id[:])}
return m, []ed25519.PrivateKey{o1p, o2p}
}
// Budget too small: 0+0 inputs need ≥ 2 out.
bad, _ := mk(1, 0)
if err := s.applyMix(bad, T); err == nil {
t.Fatal("under-budget mix accepted")
}
// Single input.
single := &core.Mix{TrieID: TrieNextNym, Inputs: [][32]byte{k1},
Outputs: []core.MixOutput{{Key: [32]byte(core.Pub(privs[0])), MixCount: 5}}}
id := single.TxID()
single.Sigs = []core.Sig{core.Sign(privs[0], id[:])}
if err := s.applyMix(single, T); err == nil {
t.Fatal("1-input mix accepted (minimum is 2)")
}
// Legal.
good, outs := mk(1, 1)
if err := s.applyMix(good, T); err != nil {
t.Fatalf("legal mix rejected: %v", err)
}
// Output key not fresh (reuse a live key).
dup := &core.Mix{TrieID: TrieNextNym,
Inputs: [][32]byte{[32]byte(core.Pub(outs[0])), [32]byte(core.Pub(outs[1]))},
Outputs: []core.MixOutput{
{Key: [32]byte(core.Pub(outs[0])), MixCount: 2},
{Key: [32]byte(core.Pub(privs[0])), MixCount: 2},
}}
id = dup.TxID()
dup.Sigs = []core.Sig{core.Sign(outs[0], id[:]), core.Sign(outs[1], id[:])}
if err := s.applyMix(dup, T); err == nil {
t.Fatal("output reusing an input key accepted")
}
// Committed inputs are locked.
ce, _ := s.NextNym.Get([32]byte(core.Pub(outs[0]))).(*NymEntry)
ce.Commit = core.H([]byte("x"))
s.NextNym.Touch([32]byte(core.Pub(outs[0])))
locked, _ := mk(2, 2)
locked.Inputs = [][32]byte{[32]byte(core.Pub(outs[0])), [32]byte(core.Pub(outs[1]))}
id = locked.TxID()
locked.Sigs = []core.Sig{core.Sign(outs[0], id[:]), core.Sign(outs[1], id[:])}
if err := s.applyMix(locked, T); err == nil {
t.Fatal("committed input accepted into a mix")
}
}
// TestIncentiveChain: each step requires the previous.
func TestIncentiveChain(t *testing.T) {
s, privs, _ := fixture(t, 2)
T := eventT(s)
k1 := [32]byte(core.Pub(privs[0]))
s.NextNym.Insert(k1, freshNym(0))
// Commit(n) without mixing.
c := mkCommitN(privs[0], core.H([]byte("c")))
if err := s.applyCommitN(c, T); err == nil {
t.Fatal("commit_n with mix_count 0 accepted")
}
// Mix, then commit.
e, _ := s.NextNym.Get(k1).(*NymEntry)
e.MixCount = 1 // stand-in for a mix
if err := s.applyCommitN(c, T); err != nil {
t.Fatalf("commit_n rejected: %v", err)
}
// Election entry auto-created, mix_count 0 → Commit(e) blocked.
ce := mkCommitE(privs[0], core.H([]byte("v")))
if err := s.applyCommitE(ce, T); err == nil {
t.Fatal("commit_e with mix_count 0 accepted")
}
ee, _ := s.Election.Get(k1).(*core.ElectionEntry)
if ee == nil {
t.Fatal("commit_n did not create the election entry")
}
// Vote before Commit(e).
v := mkVote(privs[0])
if err := s.applyVote(v, T); err == nil {
t.Fatal("vote on uncommitted election entry accepted")
}
ee.MixCount = 1
if err := s.applyCommitE(ce, T); err != nil {
t.Fatalf("commit_e rejected: %v", err)
}
if err := s.applyVote(v, T); err != nil {
t.Fatalf("vote rejected: %v", err)
}
if s.UTXO.Len() != 1 {
t.Fatal("vote did not mint UBI")
}
// Replay: same Vote → same txid → UTXO key collision.
if err := s.applyVote(mkVote(privs[0]), T); err == nil {
t.Fatal("vote replay accepted")
}
got := s.UTXO.Get(core.CoinKey(v.TxID(), 0))
if got == nil || got.Amount.Cmp(UBIPerPeriod()) != 0 {
t.Fatal("UBI amount wrong")
}
}
// TestRevealTargeting: snapshot ranks, commit consumption, xor fold.
func TestRevealTargeting(t *testing.T) {
s, _, pres := fixture(t, 2)
T := rngT(s)
k0, _ := s.Nym.Select(0)
k1, _ := s.Nym.Select(1)
rootBefore := s.Nym.Root()
countBefore := s.Nym.Count()
// Match fixture preimages to snapshot order by commit.
var pre0, pre1 [32]byte
for _, p := range pres {
if core.H(p[:]) == s.Nym.Get(k0).(*NymEntry).Commit {
pre0 = p
}
if core.H(p[:]) == s.Nym.Get(k1).(*NymEntry).Commit {
pre1 = p
}
}
// Reveal outside RNG.
if err := s.applyReveal(&Reveal{Key: k0, Preimage: pre0}, eventT(s)); err == nil {
t.Fatal("reveal accepted in event phase")
}
// Wrong preimage.
if err := s.applyReveal(&Reveal{Key: k0, Preimage: core.H([]byte("no"))}, T); err == nil {
t.Fatal("wrong preimage accepted")
}
// Valid: target = (0 + uint(pre)) mod 2.
if err := s.applyReveal(&Reveal{Key: k0, Preimage: pre0}, T); err != nil {
t.Fatalf("reveal rejected: %v", err)
}
tr := new(big.Int).SetBytes(pre0[:])
tr.Mod(tr, big.NewInt(2))
want, _ := s.Nym.Select(tr.Uint64())
we := s.Nym.Get(want).(*NymEntry)
if we.SeedHits != 1 {
t.Fatal("hit did not land on the computed target")
}
if s.XorAcc[want] != pre0 {
t.Fatal("xor accumulator did not fold the preimage")
}
// The reveal consumed the commit — that IS the replay
// protection — while the frozen index is untouched: count and
// ordering are the swap-time comm list, so the revealed entry
// keeps its rank and remains a valid RNG target. N never
// shrinks during the RNG phase.
if s.Nym.Get(k0).(*NymEntry).Commit != ([32]byte{}) {
t.Fatal("reveal did not consume the commit")
}
if err := s.applyReveal(&Reveal{Key: k0, Preimage: pre0}, T); err == nil {
t.Fatal("double reveal accepted")
}
if s.Nym.Count() != countBefore {
t.Fatal("consuming the commit altered the frozen count")
}
if r0, ok := s.Nym.Rank(k0); !ok || r0 != 0 {
t.Fatal("revealed entry lost its rank — index not frozen")
}
if s.Nym.Root() == rootBefore {
t.Fatal("consumption and seed_hits should change leaf hashes (root)")
}
// Second revealer, then the winner check.
if err := s.applyReveal(&Reveal{Key: k1, Preimage: pre1}, T); err != nil {
t.Fatalf("second reveal rejected: %v", err)
}
seed := computeSeed(s)
if seed == ([32]byte{}) {
t.Fatal("seed empty")
}
// Winner = most hits, lowest rank ties; recompute expectation.
h0 := s.Nym.Get(k0).(*NymEntry).SeedHits
h1 := s.Nym.Get(k1).(*NymEntry).SeedHits
var expect [32]byte
if h1 > h0 {
expect = s.XorAcc[k1]
} else {
expect = s.XorAcc[k0]
}
if seed != expect {
t.Fatal("computeSeed did not pick max-hits / lowest-rank winner")
}
}
// TestJudgePromotion: court routing and the atomic promote.
func TestJudgePromotion(t *testing.T) {
s, privs, _ := fixture(t, 2)
T := eventT(s)
_, invitee := core.GenKey()
s.Invite.Insert([32]byte(invitee), &InviteEntry{Used: true})
// A non-member of the court pair (fresh key) is rejected.
stranger, _ := core.GenKey()
if err := s.applyJudge(mkJudge(stranger, [32]byte(invitee)), T); err == nil {
t.Fatal("stranger judge accepted")
}
if err := s.applyJudge(mkJudge(privs[0], [32]byte(invitee)), T); err != nil {
t.Fatalf("first judge rejected: %v", err)
}
// Same side twice.
if err := s.applyJudge(mkJudge(privs[0], [32]byte(invitee)), T); err == nil {
t.Fatal("same court member judging twice accepted")
}
if s.NextNym.Get([32]byte(invitee)) != nil {
t.Fatal("promotion fired on a single judge")
}
if err := s.applyJudge(mkJudge(privs[1], [32]byte(invitee)), T); err != nil {
t.Fatalf("second judge rejected: %v", err)
}
if s.NextNym.Get([32]byte(invitee)) == nil {
t.Fatal("no next_nym entry after promotion")
}
if s.Election.Get([32]byte(invitee)) == nil {
t.Fatal("no election entry after promotion")
}
// v15: the entry stays, fully judged — no operation alters the
// invite index during the period.
inv, _ := s.Invite.Get([32]byte(invitee)).(*InviteEntry)
if inv == nil || !inv.CourtJudgedA || !inv.CourtJudgedB {
t.Fatal("promoted invite entry missing or flags not set")
}
if r, ok := s.inviteRank([32]byte(invitee)); !ok || r != 0 {
t.Fatal("promoted invite lost its rank — index not frozen")
}
// A third judge bounces on the flags: no double promotion.
if err := s.applyJudge(mkJudge(privs[0], [32]byte(invitee)), T); err == nil {
t.Fatal("third judge accepted — double promotion possible")
}
if s.UTXO.Len() != 1 {
t.Fatal("onboarding UBI not minted exactly once")
}
if s.Pop != 1 {
t.Fatalf("population_count = %d, want 1", s.Pop)
}
}
// TestForkChoice: fewest skips wins; worse branches rejected.
func TestForkChoice(t *testing.T) {
v1, v2 := newVal("a"), newVal("b")
_, pa := core.GenKey()
_, pb := core.GenKey()
c1 := mkOnion([]*valSvc{v1}, 12)
c2 := mkOnion([]*valSvc{v2}, 12)
mkChain := func() *Chain {
ch, err := NewChain(GenesisConfig{
T0: 14 * core.NormPeriod,
Nyms: []GenesisNym{
{Key: pa, Commit: core.H([]byte("ca"))},
{Key: pb, Commit: core.H([]byte("cb"))},
},
Seed: core.H([]byte("fork seed")),
Schedule: [][32]byte{c1, c2},
})
if err != nil {
t.Fatal(err)
}
return ch
}
// Owner of each slot alternates pseudo-randomly; find slots.
main := mkChain()
produceAt := func(c *Chain, slot uint64) *Block {
T := c.State.Genesis + slot*core.SlotSeconds
b, _, err := produceAny(c, []*valSvc{v1, v2}, T, nil)
if err != nil {
t.Fatal(err)
}
return b
}
produceAt(main, 4) // sparse: skips 1-3
fork := mkChain()
b1 := produceAt(fork, 1)
b2 := produceAt(fork, 2)
// Denser branch (fewer skips at same-or-later tip) adopts.
ok, err := main.TryAdopt([]*Block{b1, b2})
if err != nil || !ok {
t.Fatalf("denser branch not adopted: %v", err)
}
if main.State.Seq != 2 || main.skips() != 0 {
t.Fatalf("post-adopt tip seq=%d skips=%d", main.State.Seq, main.skips())
}
// A sparser branch is refused.
sparse := mkChain()
sb := produceAt(sparse, 9)
ok, err = main.TryAdopt([]*Block{sb})
if err != nil || ok {
t.Fatal("sparser branch adopted")
}
// Tampered block rejected.
tampered := *b2
evil := core.H([]byte("evil"))
tampered.Header.Nonce = evil[:]
if _, err := main.TryAdopt([]*Block{b1, &tampered}); err == nil {
t.Fatal("tampered branch verified")
}
}
// TestFullDemo: the whole scenario replays and detects tampering.
func TestFullDemo(t *testing.T) {
chain, err := RunDemo()
if err != nil {
t.Fatalf("demo: %v", err)
}
s := chain.genesisState.Clone()
for i, b := range chain.Blocks {
ns, err := VerifyBlock(s, b)
if err != nil {
t.Fatalf("replay block %d: %v", i, err)
}
s = ns
}
if s.LastHash != chain.State.LastHash {
t.Fatal("replay tip differs")
}
// The tip sits just past the period-3 boundary: population_count
// has reset, the copied-down snapshots carry the result.
if chain.State.Nym.Count() != 4 {
t.Fatalf("final N = %d, want 4", chain.State.Nym.Count())
}
if len(chain.State.Schedule) != 5 {
t.Fatalf("final schedule = %d commits, want 5", len(chain.State.Schedule))
}
if chain.State.Pop != 0 {
t.Fatalf("population_count = %d, want 0 right after activation", chain.State.Pop)
}
// Tamper with a mid-chain tx.
s = chain.genesisState.Clone()
for i, b := range chain.Blocks {
if len(b.Txs) > 0 {
if v, ok := b.Txs[0].(*Verify); ok {
bad := *v
bad.Key[0] ^= 1
tb := *b
tb.Txs = append([]core.Tx{&bad}, b.Txs[1:]...)
if _, err := VerifyBlock(s, &tb); err == nil {
t.Fatal("tampered tx verified")
}
break
}
}
ns, err := VerifyBlock(s, b)
if err != nil {
t.Fatalf("replay block %d: %v", i, err)
}
s = ns
}
}
// TestCommitlessDoesNotExist — the commit is the registration act:
// indexing is established by commits in next_nym and locked at the
// swap. An entry carried through the swap with commit == 0 was
// never registered — no rank, not in N, cannot verify, cannot be
// judged, cannot reveal. The protocol cannot reference it; it
// self-cleans at the next swap.
func TestCommitlessDoesNotExist(t *testing.T) {
s, privs, _ := fixture(t, 2)
T := eventT(s)
lazyPriv, lazyPub := core.GenKey()
if err := s.Nym.Insert([32]byte(lazyPub), &NymEntry{MixCount: 1}); err != nil {
t.Fatal(err)
}
// Not indexed: N unchanged, no rank.
if s.Nym.Count() != 2 {
t.Fatalf("N = %d, want 2 — commitless entry entered the index", s.Nym.Count())
}
if _, ok := s.nymRank([32]byte(lazyPub)); ok {
t.Fatal("commitless entry has a rank")
}
// Cannot verify (no rank → no pair).
if err := s.applyVerify(mkVerify(lazyPriv), T); err == nil {
t.Fatal("commitless entry verified")
}
// Cannot be judged in (no rank → no court routing).
if err := s.applyJudgeNym(mkJudgeNym(privs[0], [32]byte(lazyPub)), T); err == nil {
t.Fatal("commitless entry judged")
}
// Cannot reveal (nothing committed).
if err := s.applyReveal(&Reveal{Key: [32]byte(lazyPub)}, rngT(s)); err == nil {
t.Fatal("commitless entry revealed")
}
// The register is untouched by its presence: the real pair
// completes exactly as with N = 2.
if err := s.applyVerify(mkVerify(privs[0]), T); err != nil {
t.Fatal(err)
}
if err := s.applyVerify(mkVerify(privs[1]), T); err != nil {
t.Fatal(err)
}
if s.Pop != 2 {
t.Fatalf("population_count = %d, want 2", s.Pop)
}
}
// TestTrieCanonical: the trie is canonical for a key set — shape and
// root independent of insertion order, and remove restores exactly
// the never-inserted state.
func TestTrieCanonical(t *testing.T) {
keys := make([][32]byte, 40)
for i := range keys {
keys[i] = core.H([]byte{byte(i)}, []byte("canon"))
}
build := func(order []int) *core.Trie {
tr := NewNymTrie()
for _, i := range order {
e := &NymEntry{}
if i%3 != 0 {
e.Commit = core.H(keys[i][:]) // mixed committed/uncommitted
}
if err := tr.Insert(keys[i], e); err != nil {
t.Fatal(err)
}
}
return tr
}
fwd := make([]int, 40)
rev := make([]int, 40)
for i := range fwd {
fwd[i], rev[i] = i, 39-i
}
a, b := build(fwd), build(rev)
if a.Root() != b.Root() {
t.Fatal("root depends on insertion order")
}
if a.Count() != b.Count() {
t.Fatal("count depends on insertion order")
}
// Rank/Select agree and invert.
for r := uint64(0); r < a.Count(); r++ {
ka, _ := a.Select(r)
kb, _ := b.Select(r)
if ka != kb {
t.Fatal("select depends on insertion order")
}
if got, ok := a.Rank(ka); !ok || got != r {
t.Fatal("rank does not invert select")
}
}
// Insert + remove ≡ never inserted.
extra := core.H([]byte("extra"))
before := a.Root()
if err := a.Insert(extra, &NymEntry{Commit: core.H([]byte("x"))}); err != nil {
t.Fatal(err)
}
if a.Root() == before {
t.Fatal("insert did not change the root")
}
if _, err := a.Remove(extra); err != nil {
t.Fatal(err)
}
if a.Root() != before {
t.Fatal("remove did not restore the canonical root")
}
}
// TestMerkleProof: proof-of-unique-human — a counted, verified nym
// entry proven against the header's nym_trie root, surviving reveal
// (frozen leaf count), failing on tampering.
func TestMerkleProof(t *testing.T) {
s, privs, pres := fixture(t, 5)
T := eventT(s)
// Verify a full pair so one entry is verified.
var target [32]byte
for r := uint64(0); r < 5; r++ {
k, _ := s.Nym.Select(r)
pr, ok := s.partnerRank(r)
if !ok {
continue
}
pk, _ := s.Nym.Select(pr)
var kp, pp ed25519.PrivateKey
for _, p := range privs {
if [32]byte(core.Pub(p)) == k {
kp = p
}
if [32]byte(core.Pub(p)) == pk {
pp = p
}
}
if err := s.applyVerify(mkVerify(kp), T); err != nil {
t.Fatal(err)
}
if err := s.applyVerify(mkVerify(pp), T); err != nil {
t.Fatal(err)
}
target = k
break
}
root := s.Nym.Root()
lc, steps, err := s.Nym.MerkleProof(target)
if err != nil {
t.Fatal(err)
}
entry := s.Nym.Get(target).(*NymEntry).CloneEntry().(*NymEntry)
if !s.Nym.VerifyProof(root, target, entry, lc, steps) {
t.Fatal("valid proof rejected")
}
// Tampered entry (claiming verified where root says otherwise).
bad := entry.CloneEntry().(*NymEntry)
bad.SeedHits = 99
if s.Nym.VerifyProof(root, target, bad, lc, steps) {
t.Fatal("tampered entry proven")
}
// Wrong root.
if s.Nym.VerifyProof(core.H([]byte("other")), target, entry, lc, steps) {
t.Fatal("proof verified against a foreign root")
}
// After the target reveals, the commit is consumed but the leaf
// count is frozen: a fresh proof against the new root carries
// leafCount 1 for an entry whose Counted() is false.
var pre [32]byte
for _, p := range pres {
if core.H(p[:]) == entry.Commit {
pre = p
}
}
if err := s.applyReveal(&Reveal{Key: target, Preimage: pre}, rngT(s)); err != nil {
t.Fatalf("reveal: %v", err)
}
root2 := s.Nym.Root()
lc2, steps2, err := s.Nym.MerkleProof(target)
if err != nil {
t.Fatal(err)
}
e2 := s.Nym.Get(target).(*NymEntry)
if e2.Counted() || lc2 != 1 {
t.Fatalf("expected consumed commit with frozen leaf count 1, got committed=%v count=%d",
e2.Counted(), lc2)
}
if !s.Nym.VerifyProof(root2, target, e2.CloneEntry(), lc2, steps2) {
t.Fatal("post-reveal proof rejected")
}
}
// TestNormAdvance — the v19 rule: when norm_time advances by Δ,
// sum ×= decay^Δ. Applied per contribution with exact branch sums;
// real values (amount × decay^(T−time)) are untouched by the
// bookkeeping rescale.
func TestNormAdvance(t *testing.T) {
old := uint64(14) * core.NormPeriod
newN := old + core.NormPeriod // Δ = 4 years
u := core.NewUTXOSet()
var keys [][32]byte
var oldNorms []*big.Int
for i := 0; i < 7; i++ {
_, p := core.GenKey()
amt := new(big.Int).Mul(big.NewInt(int64(i+1)), big.NewInt(1_000_000_000))
tm := old + uint64(i)*PeriodSeconds
k := core.CoinKey(core.H([]byte{byte(i)}, []byte("norm")), 0)
nv := core.Normalize(amt, tm, old)
if err := u.Insert(k, &core.CoinEntry{Owner: p, Amount: amt, Norm: nv, Time: tm}); err != nil {
t.Fatal(err)
}
keys = append(keys, k)
oldNorms = append(oldNorms, new(big.Int).Set(nv))
}
T := newN + 1000*core.SlotSeconds
supplyBefore := core.ValueAt(u.Sum(), T, old)
sumBefore := u.Sum()
valBefore := u.Get(keys[3]).Value(T)
det := u.Clone()
u.Renormalize(old, newN)
det.Renormalize(old, newN)
if u.Root() != det.Root() {
t.Fatal("renormalization is not deterministic")
}
// Per contribution: norm ×= decay^Δ, floored.
factor := core.DecayPow(core.NormPeriod)
total := new(big.Int)
for i, k := range keys {
want := core.MulScale(oldNorms[i], factor)
got := u.Get(k).Norm
if got.Cmp(want) != 0 {
t.Fatalf("leaf %d: norm %s, want %s", i, got, want)
}
total.Add(total, got)
}
// Branch sums exact: root == Σ leaves.
if u.Sum().Cmp(total) != 0 {
t.Fatal("root sum is not the exact sum of rescaled contributions")
}
// Root within one unit per leaf of the ideal rescale.
ideal := core.MulScale(sumBefore, factor)
diff := new(big.Int).Sub(ideal, u.Sum())
if diff.Sign() < 0 || diff.Cmp(big.NewInt(int64(len(keys)))) > 0 {
t.Fatalf("sum ×= decay^Δ off by %s (allow 0..%d)", diff, len(keys))
}
// Real values untouched; supply-at-T continuous within rounding.
if u.Get(keys[3]).Value(T).Cmp(valBefore) != 0 {
t.Fatal("renormalization changed a real value")
}
supplyAfter := core.ValueAt(u.Sum(), T, newN)
d2 := new(big.Int).Sub(supplyBefore, supplyAfter)
if d2.Sign() < 0 || d2.Cmp(big.NewInt(int64(len(keys)))) > 0 {
t.Fatalf("supply jumped by %s across the advance", d2)
}
}
// TestFixedEpochAcrossBoundary — the node never renormalizes in the
// transition function: a 4×YEAR boundary passes mid-period, the era
// epoch stays fixed, coins minted after the boundary normalize
// against it (norms grow — that is the uint128 headroom being
// consumed), values stay coherent and the chain replays. The
// planned-event tool (Renormalize) is tested separately in
// TestNormAdvance.
func TestFixedEpochAcrossBoundary(t *testing.T) {
B := uint64(15) * core.NormPeriod
T0 := B - PeriodSeconds/2 // Event = [T0, B): boundary at RNG start
era := core.NormTimeFor(T0)
v := newVal("v")
a := &person{name: "a"}
b := &person{name: "b"}
for _, p := range []*person{a, b} {
p.fresh()
p.rollover()
p.fresh()
}
chain, err := NewChain(GenesisConfig{
T0: T0,
Nyms: []GenesisNym{
{Key: a.pub, Commit: core.H(a.pre[:])},
{Key: b.pub, Commit: core.H(b.pre[:])},
},
Seed: core.H([]byte("fixed epoch")),
Schedule: [][32]byte{mkOnion([]*valSvc{v}, 12)},
})
if err != nil {
t.Fatal(err)
}
at := func(slot uint64, txs []core.Tx) {
t.Helper()
if _, err := chain.Produce(txs, T0+slot*core.SlotSeconds, v.priv, v.nonces); err != nil {
t.Fatal(err)
}
}
at(1, []core.Tx{mkVerify(a.priv), mkVerify(b.priv)})
mixN, outs := mkMix(TrieNextNym, chain.State, []ed25519.PrivateKey{a.priv, b.priv})
at(2, []core.Tx{mixN})
at(3, []core.Tx{mkCommitN(outs[0], core.H([]byte("c0"))), mkCommitN(outs[1], core.H([]byte("c1")))})
mixE, eouts := mkMix(TrieElection, chain.State, outs)
at(4, []core.Tx{mixE})
at(5, []core.Tx{mkCommitE(eouts[0], mkOnion([]*valSvc{v}, 12)), mkCommitE(eouts[1], mkOnion([]*valSvc{v}, 12))})
at(6, []core.Tx{mkVote(eouts[0]), mkVote(eouts[1])})
// Cross the 4×YEAR boundary (still period 0, RNG phase): the
// epoch must NOT move, and the utxo trie must be untouched.
rootPre := chain.State.UTXO.Root()
past := (B-T0)/core.SlotSeconds + 1
at(past, nil)
if chain.State.NormTime != era {
t.Fatalf("norm_time moved to %d; the era epoch is fixed at %d", chain.State.NormTime, era)
}
if chain.State.UTXO.Root() != rootPre {
t.Fatal("crossing the boundary rewrote the utxo trie")
}
// A coin created after the boundary normalizes against the old
// era epoch: time − norm_time > 4 years, norm > amount (headroom
// consumption), value at T exact.
voteID := mkVote(eouts[0]).TxID()
spendT := T0 + (past+2)*core.SlotSeconds
src := chain.State.UTXO.Get(core.CoinKey(voteID, 0))
half := new(big.Int).Rsh(src.Spendable(spendT), 1)
_, dst := core.GenKey()
tr := &core.Transfer{
Inputs: []core.Outpoint{{Tx: voteID, Index: 0}},
Outputs: []core.Output{{Amount: half, Owner: dst}},
}
trID := tr.TxID()
tr.Sigs = []core.Sig{core.Sign(eouts[0], trID[:])}
at(past+2, []core.Tx{tr})
ne := chain.State.UTXO.Get(core.CoinKey(trID, 0))
if ne.Time <= era+core.NormPeriod {
t.Fatal("test setup: new coin not past the boundary")
}
if ne.Norm.Cmp(ne.Amount) <= 0 {
t.Fatal("post-boundary norm should exceed amount (growing norms)")
}
if ne.Value(ne.Time).Cmp(ne.Amount) != 0 {
t.Fatal("value at creation time must equal amount")
}
// Full replay: fixed-epoch behavior is consensus-deterministic.
s := chain.genesisState.Clone()
for i, blk := range chain.Blocks {
ns, err := VerifyBlock(s, blk)
if err != nil {
t.Fatalf("replay block %d: %v", i, err)
}
s = ns
}
if s.LastHash != chain.State.LastHash {
t.Fatal("replay tip differs")
}
}
// TestMaxHitsAggregate — the aggregate walk agrees with a linear
// scan (max hits, lowest key on ties) across random patterns, and
// hits landing only on counted targets means the winner is counted.
func TestMaxHitsAggregate(t *testing.T) {
tr := NewNymTrie()
keys := make([][32]byte, 33)
for i := range keys {
keys[i] = core.H([]byte{byte(i)}, []byte("hits"))
e := &NymEntry{Commit: core.H(keys[i][:])}
if err := tr.Insert(keys[i], e); err != nil {
t.Fatal(err)
}
}
scan := func() ([32]byte, uint32, bool) {
best, bh, found := [32]byte{}, uint32(0), false
tr.ForEach(func(k [32]byte, e core.Entry) {
if h := e.HitsVal(); h > bh {
best, bh, found = k, h, true
}
})
return best, bh, found
}
// All zero: no winner.
if _, _, ok := tr.SelectMaxHits(); ok {
t.Fatal("winner with zero hits")
}
// Random patterns with deliberate ties.
pat := []struct {
idx int
hits uint32
}{{5, 3}, {12, 7}, {30, 7}, {2, 1}, {19, 7}, {0, 2}}
for _, p := range pat {
tr.Get(keys[p.idx]).(*NymEntry).SeedHits = p.hits
tr.Refresh(keys[p.idx])
wk, wh, wok := tr.SelectMaxHits()
sk, sh, sok := scan()
if wok != sok || wh != sh || wk != sk {
t.Fatalf("aggregate walk disagrees with scan after setting %d=%d", p.idx, p.hits)
}
}
// The tie (12, 30, 19 all at 7) must resolve to the lowest KEY.
wk, _, _ := tr.SelectMaxHits()
low := keys[12]
for _, i := range []int{19, 30} {
if string(keys[i][:]) < string(low[:]) {
low = keys[i]
}
}
if wk != low {
t.Fatal("tie not broken by lowest key (lowest rank)")
}
}
// TestTrieDifferential — randomized differential test of the radix
// trie against a plain reference model. Index membership is modeled
// exactly per the frozen-index semantics: set at Insert and Touch,
// UNTOUCHED by in-place mutation + Refresh (including commit
// consumption). Checks count, rank/select, key order, root
// determinism, Merkle proofs and the max-hits walk along the way.
func TestTrieDifferential(t *testing.T) {
rng := mrand.New(mrand.NewSource(7))
for round := 0; round < 3; round++ {
tr := NewNymTrie()
entries := map[[32]byte]*NymEntry{}
indexed := map[[32]byte]bool{}
pool := make([][32]byte, 300)
for i := range pool {
pool[i] = core.H([]byte{byte(round), byte(i), byte(i >> 8)}, []byte("diff"))
}
sortedIndexed := func() [][32]byte {
var out [][32]byte
for k, ok := range indexed {
if ok {
out = append(out, k)
}
}
sort.Slice(out, func(i, j int) bool {
return bytes.Compare(out[i][:], out[j][:]) < 0
})
return out
}
check := func() {
t.Helper()
want := sortedIndexed()
if tr.Count() != uint64(len(want)) {
t.Fatalf("count %d, model %d", tr.Count(), len(want))
}
got := tr.CommittedKeys()
for i := range want {
if got[i] != want[i] {
t.Fatal("committed keys diverge from model")
}
if r, ok := tr.Rank(want[i]); !ok || r != uint64(i) {
t.Fatal("rank diverges from model")
}
if k, ok := tr.Select(uint64(i)); !ok || k != want[i] {
t.Fatal("select diverges from model")
}
}
if tr.Clone().Root() != tr.Root() {
t.Fatal("root not deterministic under clone")
}
// A random present key must prove against the root.
if len(entries) > 0 {
var some [32]byte
for k := range entries {
some = k
break
}
lc, steps, err := tr.MerkleProof(some)
if err != nil {
t.Fatal(err)
}
if !tr.VerifyProof(tr.Root(), some, entries[some].CloneEntry(), lc, steps) {
t.Fatal("proof of present key rejected")
}
}
// Max-hits walk vs scan over live entries.
var bk [32]byte
bh := uint32(0)
for _, k := range append([][32]byte{}, poolSorted(pool, entries)...) {
if e := entries[k]; e != nil && e.SeedHits > bh {
bk, bh = k, e.SeedHits
}
}
wk, wh, wok := tr.SelectMaxHits()
if (bh > 0) != wok || (wok && (wh != bh || wk != bk)) {
t.Fatal("max-hits walk diverges from scan")
}
}
for op := 0; op < 2500; op++ {
k := pool[rng.Intn(len(pool))]
e := entries[k]
switch rng.Intn(6) {
case 0, 1: // insert
ne := &NymEntry{}
if rng.Intn(2) == 0 {
ne.Commit = core.H(k[:], []byte{byte(op)})
}
err := tr.Insert(k, ne)
if e != nil {
if err == nil {
t.Fatal("duplicate insert accepted")
}
} else {
if err != nil {
t.Fatal(err)
}
entries[k] = ne
indexed[k] = ne.Counted()
}
case 2: // remove
_, err := tr.Remove(k)
if e == nil {
if err == nil {
t.Fatal("remove of absent key accepted")
}
} else {
if err != nil {
t.Fatal(err)
}
delete(entries, k)
delete(indexed, k)
}
case 3: // commit + touch (index established)
if e != nil && e.Commit == ([32]byte{}) {
e.Commit = core.H(k[:], []byte{0xCC, byte(op)})
tr.Touch(k)
indexed[k] = true
}
case 4: // in-place mutation + refresh: index frozen
if e != nil {
e.SeedHits += uint32(rng.Intn(3))
e.Verified = !e.Verified
tr.Refresh(k)
}
case 5: // consume commit + refresh: index STILL frozen
if e != nil && e.Commit != ([32]byte{}) {
e.Commit = [32]byte{}
tr.Refresh(k)
// model: indexed[k] unchanged — the whole point
}
}
if op%250 == 0 {
check()
}
}
check()
// Tear down: remove everything → canonical empty root.
for k := range entries {
if _, err := tr.Remove(k); err != nil {
t.Fatal(err)
}
}
if tr.Root() != core.Zero32 || tr.Count() != 0 || tr.Len() != 0 {
t.Fatal("teardown did not restore the empty trie")
}
}
}
// poolSorted returns the pool keys present in entries, key order —
// scan order for the max-hits cross-check (lowest key on ties).
func poolSorted(pool [][32]byte, entries map[[32]byte]*NymEntry) [][32]byte {
var out [][32]byte
for _, k := range pool {
if entries[k] != nil {
out = append(out, k)
}
}
sort.Slice(out, func(i, j int) bool { return bytes.Compare(out[i][:], out[j][:]) < 0 })
return out
}
// TestGenesisValidation — config errors are rejected at genesis, not
// discovered as dead state later.
func TestGenesisValidation(t *testing.T) {
_, pa := core.GenKey()
_, pb := core.GenKey()
ok := GenesisConfig{
T0: 14 * core.NormPeriod,
Nyms: []GenesisNym{
{Key: pa, Commit: core.H([]byte("a"))},
{Key: pb, Commit: core.H([]byte("b"))},
},
Seed: core.H([]byte("g")),
Schedule: [][32]byte{core.H([]byte("s"))},
}
if _, err := NewChain(ok); err != nil {
t.Fatalf("valid genesis rejected: %v", err)
}
bad := ok
bad.Nyms = []GenesisNym{{Key: pa, Commit: [32]byte{}}}
if _, err := NewChain(bad); err == nil {
t.Fatal("zero-commit genesis nym accepted (unindexed person)")
}
bad = ok
bad.Nyms = []GenesisNym{{Key: core.PubKey{}, Commit: core.H([]byte("a"))}}
if _, err := NewChain(bad); err == nil {
t.Fatal("zero-key genesis nym accepted")
}
bad = ok
bad.Schedule = [][32]byte{{}}
if _, err := NewChain(bad); err == nil {
t.Fatal("zero schedule commit accepted (permanent skips)")
}
bad = ok
bad.Schedule = nil
if _, err := NewChain(bad); err == nil {
t.Fatal("empty schedule accepted (chain halted at birth)")
}
bad = ok
bad.T0 = ok.T0 + 1
if _, err := NewChain(bad); err == nil {
t.Fatal("off-grid genesis time accepted")
}
}
consensus.go540
package bitpeople
// consensus.go — phases, the period boundary, and block production.
//
// Event (14 d) → RNG (7 d) → Pause (7 d), derived from time.
//
// At each boundary, everything derivable is computed once and copied
// down — never materialized as extra tries:
//
// seed := bitpeople RNG result of the closing period
// schedule := the election trie's committed values in key order —
// hash(seed ‖ seq) mod count indexes straight into it
// nym snap := the new nym trie's committed keys in key order —
// ranks for virtual pairs, courts and RNG targeting
// invite snap := the used invites in key order — court assignment
//
// then next_nym → nym, next_invite → invite (root swaps), the
// election trie resets to empty, and fresh next tries open. The live
// tries only track consumption during the period; all rank math
// anchors to the snapshots.
import (
"crypto/ed25519"
"errors"
"fmt"
"math/big"
"peoplecoin/core"
)
const (
// PeriodSeconds is the bitpeople period: 28 days.
PeriodSeconds = uint64(28 * 24 * 3600)
EventSeconds = uint64(14 * 24 * 3600)
RNGSeconds = uint64(7 * 24 * 3600) // then Pause fills the rest
)
// UBIPerPeriod: TOKEN × (1 − decay^period_duration) — minted on Vote.
func UBIPerPeriod() *big.Int {
p := core.DecayPow(PeriodSeconds)
return new(big.Int).Quo(new(big.Int).Mul(core.TOKEN,
new(big.Int).Sub(core.Scale, p)), core.Scale)
}
func offsetOf(s *State, T uint64) uint64 { return (T - s.Genesis) % PeriodSeconds }
func inEvent(s *State, T uint64) bool { return offsetOf(s, T) < EventSeconds }
func inRNG(s *State, T uint64) bool {
o := offsetOf(s, T)
return o >= EventSeconds && o < EventSeconds+RNGSeconds
}
func phaseName(s *State, T uint64) string {
switch {
case inEvent(s, T):
return "event"
case inRNG(s, T):
return "rng"
default:
return "pause"
}
}
func (s *State) periodStart(T uint64) uint64 { return T - offsetOf(s, T) }
// State — six tries plus the copied-down derived data.
type State struct {
NormTime uint64 // the era's norm epoch — fixed for the deployment
Time uint64
Seq uint64
Genesis uint64
LastHash [32]byte
Nym *core.Trie
NextNym *core.Trie
Invite *core.Trie
NextInvite *core.Trie
Election *core.Trie
UTXO *core.UTXOSet
// Copied down at the boundary. Only the election schedule needs
// copying — that trie resets. Every other index is the live
// trie: established in the mixer by the registration act
// (commit / use), locked at the swap, and untouched by every
// period operation — Verify/JudgeNym/Judge set flags, Reveal
// consumes the commit payload, nothing inserts, removes or
// re-indexes (v15).
Seed [32]byte
Schedule [][32]byte // committed election values, key order
// Derived, maintained incrementally during the period —
// recomputable from the chain, never part of a root.
Pop uint64 // population_count
InvitesIssued uint64
XorAcc map[[32]byte][32]byte // reveal XOR per target key
}
func (s *State) Clone() *State {
c := *s
c.Nym = s.Nym.Clone()
c.NextNym = s.NextNym.Clone()
c.Invite = s.Invite.Clone()
c.NextInvite = s.NextInvite.Clone()
c.Election = s.Election.Clone()
c.UTXO = s.UTXO.Clone()
c.Schedule = append([][32]byte(nil), s.Schedule...)
c.XorAcc = make(map[[32]byte][32]byte, len(s.XorAcc))
for k, v := range s.XorAcc {
c.XorAcc[k] = v
}
return &c
}
func (s *State) seqAt(T uint64) uint64 { return (T - s.Genesis) / core.SlotSeconds }
// SupplyAt: sum × decay^(T − norm_time).
func (s *State) SupplyAt(T uint64) *big.Int {
return core.ValueAt(s.UTXO.Sum(), T, s.NormTime)
}
// ====================================================== rank helpers
func (s *State) nymRank(k [32]byte) (uint64, bool) { return s.Nym.Rank(k) }
func (s *State) inviteRank(k [32]byte) (uint64, bool) { return s.Invite.Rank(k) }
func (s *State) nymN() uint64 { return s.Nym.Count() }
func (s *State) posOfRank(r uint64) uint64 {
return FeistelPos(r, s.Seed, s.nymN())
}
// partnerRank: the rank at the adjacent virtual position, if any.
func (s *State) partnerRank(r uint64) (uint64, bool) {
N := s.nymN()
pp := s.posOfRank(r) ^ 1
if pp >= N {
return 0, false // odd N: the last position is lone
}
return FeistelPosInv(pp, s.Seed, N), true
}
// courtSide checks that member sits in the court pair assigned to a
// target rank (target mod pair_count), returning the member's side:
// 0 = a (even virtual position), 1 = b.
func (s *State) courtSide(member [32]byte, targetRank uint64) (int, error) {
pc := s.nymN() / 2
if pc == 0 {
return 0, errors.New("no full pairs to serve as court")
}
c := targetRank % pc
mr, ok := s.nymRank(member)
if !ok {
return 0, errors.New("court member not in the boundary snapshot")
}
mp := s.posOfRank(mr)
if mp/2 != c {
return 0, errors.New("signer is not a member of the assigned court pair")
}
return int(mp % 2), nil
}
// ==================================================== period boundary
// computeSeed: the counted... the committed nym with the most
// seed_hits wins (lowest rank breaks ties); the seed is the XOR of
// the reveals that targeted it. Deterministic fallback if none.
func computeSeed(s *State) [32]byte {
winner, _, ok := s.Nym.SelectMaxHits()
if !ok {
return core.H([]byte{0xE0}, s.Seed[:]) // no reveals: deterministic fallback
}
return s.XorAcc[winner]
}
// activate runs one boundary: copy down, swap, reset.
func activate(s *State) {
seed := computeSeed(s)
// Schedule: the election trie's committed values in key order.
sched := make([][32]byte, 0, s.Election.Count())
for _, k := range s.Election.CommittedKeys() {
e := s.Election.Get(k).(*core.ElectionEntry)
sched = append(sched, e.Commit)
}
s.Nym = s.NextNym
s.NextNym = NewNymTrie()
s.Invite = s.NextInvite
s.NextInvite = NewInviteTrie()
s.Election = core.NewElectionTrie()
s.Seed = seed
s.Schedule = sched
s.Pop = 0
s.InvitesIssued = 0
s.XorAcc = map[[32]byte][32]byte{}
}
func advancePeriods(s *State, from, to uint64) {
for b := s.periodStart(from) + PeriodSeconds; b <= to; b += PeriodSeconds {
activate(s)
}
}
// ============================================================= blocks
type Header struct {
Seq uint64
Time uint64
NymT [32]byte
NextNymT [32]byte
InviteT [32]byte
NextInvT [32]byte
ElectionT [32]byte
UTXOT [32]byte
Prev [32]byte
Validator core.PubKey
Nonce []byte // onion reveal: 64 bytes, 32 at the deepest layer
Sig core.Sig
}
func (h *Header) encode(withSig bool) []byte {
var w core.Buf
w.U8(OpHeader)
w.U64(h.Seq)
w.U64(h.Time)
w.Bytes(h.NymT[:])
w.Bytes(h.NextNymT[:])
w.Bytes(h.InviteT[:])
w.Bytes(h.NextInvT[:])
w.Bytes(h.ElectionT[:])
w.Bytes(h.UTXOT[:])
w.Bytes(h.Prev[:])
w.Bytes(h.Validator[:])
w.U8(byte(len(h.Nonce)))
w.Bytes(h.Nonce)
if withSig {
w.Bytes(h.Sig[:])
}
return w.B
}
func (h *Header) SigHash() [32]byte { return core.H(h.encode(false)) }
func (h *Header) Hash() [32]byte { return core.H(h.encode(true)) }
type Block struct {
Header Header
Txs []core.Tx
}
// selectedCommit: hash(seed || seq) mod count into the schedule.
// The whole sequence is public at the boundary; DoS protection comes
// from the onion, not from seed unpredictability. Returns the index
// because the reveal peels the schedule entry in place.
func selectedCommit(s *State, seq uint64) (int, [32]byte, error) {
n := uint64(len(s.Schedule))
if n == 0 {
return 0, [32]byte{}, errors.New("empty schedule: chain halted")
}
r := core.H(s.Seed[:], core.U64be(seq))
i := new(big.Int).SetBytes(r[:])
i.Mod(i, new(big.Int).SetUint64(n))
return int(i.Uint64()), s.Schedule[i.Uint64()], nil
}
func preProcess(prev *State, T uint64) (*State, int, [32]byte, error) {
if T%core.SlotSeconds != 0 {
return nil, 0, core.Zero32, errors.New("block time not on the slot grid")
}
if T <= prev.Time {
return nil, 0, core.Zero32, errors.New("block time not after previous block")
}
ns := prev.Clone()
advancePeriods(ns, prev.Time, T)
// Note: norm_time does NOT advance here. Renormalization is an
// inherently O(N) full-state rewrite (every contribution and
// every coin hash changes) — it cannot fit a 60-second slot at
// scale, and every verifier would have to pay it to accept the
// block. It is a planned era event instead: the rescale is a
// pure function of state, so everyone precomputes and verifies
// the post-advance root at leisure and switches at an agreed
// point — coordination, not consensus computation. The node
// runs its whole era on one fixed epoch; uint128 gives 100+
// years of headroom while norms grow. UTXOSet.Renormalize is
// the tool for the event.
idx, sel, err := selectedCommit(ns, prev.seqAt(T))
if err != nil {
return nil, 0, core.Zero32, err
}
return ns, idx, sel, nil
}
func finish(ns *State, txs []core.Tx, T uint64, validator core.PubKey) error {
fees, err := ns.ApplyTxs(txs, T)
if err != nil {
return err
}
if fees.Sign() > 0 {
feeID := core.H([]byte{0xFE}, core.U64be(ns.seqAt(T)))
if err := ns.UTXO.Insert(core.CoinKey(feeID, 0), &core.CoinEntry{
Owner: validator, Amount: fees,
Norm: core.Normalize(fees, T, ns.NormTime), Time: T,
}); err != nil {
return fmt.Errorf("fee mint: %w", err)
}
}
return nil
}
// BuildBlock produces a block if the validator holds the reveal
// material for the selected commit: the header carries (validator,
// nonce) with H(validator ‖ nonce) == commit, and the reveal peels
// the schedule entry to the onion's next layer — or consumes it
// when the deepest (32-byte) nonce is spent.
func BuildBlock(prev *State, txs []core.Tx, T uint64, valPriv ed25519.PrivateKey,
nonces map[[32]byte][]byte) (*Block, *State, error) {
ns, idx, sel, err := preProcess(prev, T)
if err != nil {
return nil, nil, err
}
nonce, ok := nonces[sel]
if !ok {
return nil, nil, errors.New("build: selected commit is not ours (or consumed)")
}
pub := core.Pub(valPriv)
next, ok := core.VerifyPeel(sel, pub, nonce)
if !ok {
return nil, nil, errors.New("build: reveal material does not match the commit")
}
ns.Schedule[idx] = next // zero = onion spent: the slot skips from here
if err := finish(ns, txs, T, pub); err != nil {
return nil, nil, err
}
h := Header{
Seq: prev.seqAt(T), Time: T,
NymT: ns.Nym.Root(), NextNymT: ns.NextNym.Root(),
InviteT: ns.Invite.Root(), NextInvT: ns.NextInvite.Root(),
ElectionT: ns.Election.Root(), UTXOT: ns.UTXO.Root(),
Prev: prev.LastHash, Validator: pub, Nonce: nonce,
}
sh := h.SigHash()
h.Sig = core.Sign(valPriv, sh[:])
ns.Seq, ns.Time, ns.LastHash = h.Seq, T, h.Hash()
return &Block{Header: h, Txs: txs}, ns, nil
}
// VerifyBlock re-executes and checks the selection proof and all six
// roots.
func VerifyBlock(prev *State, b *Block) (*State, error) {
h := &b.Header
if h.Seq != prev.seqAt(h.Time) {
return nil, errors.New("verify: seq is not slots-since-genesis")
}
if h.Prev != prev.LastHash {
return nil, errors.New("verify: prev hash mismatch")
}
ns, idx, sel, err := preProcess(prev, h.Time)
if err != nil {
return nil, fmt.Errorf("verify: %w", err)
}
next, ok := core.VerifyPeel(sel, h.Validator, h.Nonce)
if !ok {
return nil, errors.New("verify: reveal does not peel the selected commit")
}
ns.Schedule[idx] = next
if err := finish(ns, b.Txs, h.Time, h.Validator); err != nil {
return nil, fmt.Errorf("verify: %w", err)
}
sh := h.SigHash()
if !core.VerifySig(h.Validator, sh[:], h.Sig) {
return nil, errors.New("verify: bad validator signature")
}
roots := [][2][32]byte{
{ns.Nym.Root(), h.NymT}, {ns.NextNym.Root(), h.NextNymT},
{ns.Invite.Root(), h.InviteT}, {ns.NextInvite.Root(), h.NextInvT},
{ns.Election.Root(), h.ElectionT}, {ns.UTXO.Root(), h.UTXOT},
}
for i, p := range roots {
if p[0] != p[1] {
return nil, fmt.Errorf("verify: root %d mismatch", i)
}
}
ns.Seq, ns.Time, ns.LastHash = h.Seq, h.Time, h.Hash()
return ns, nil
}
// ============================================================== chain
type Chain struct {
State *State
Blocks []*Block
Final *State
finalIdx int
genesisState *State
}
type GenesisNym struct {
Key core.PubKey
Commit [32]byte
}
type GenesisConfig struct {
T0 uint64
Nyms []GenesisNym
Seed [32]byte
Schedule [][32]byte // initial validator commits: hash(validator || nonce)
}
func NewChain(cfg GenesisConfig) (*Chain, error) {
if cfg.T0%core.SlotSeconds != 0 {
return nil, errors.New("genesis: time not on the slot grid")
}
if len(cfg.Schedule) == 0 {
return nil, errors.New("genesis: empty schedule")
}
for _, sc := range cfg.Schedule {
if sc == ([32]byte{}) {
return nil, errors.New("genesis: zero schedule commit is unprovable (permanent skips)")
}
}
s := &State{
NormTime: core.NormTimeFor(cfg.T0),
Time: cfg.T0, Genesis: cfg.T0,
Nym: NewNymTrie(), NextNym: NewNymTrie(),
Invite: NewInviteTrie(), NextInvite: NewInviteTrie(),
Election: core.NewElectionTrie(), UTXO: core.NewUTXOSet(),
Seed: cfg.Seed, Schedule: append([][32]byte(nil), cfg.Schedule...),
XorAcc: map[[32]byte][32]byte{},
}
for _, n := range cfg.Nyms {
if n.Key == (core.PubKey{}) {
return nil, errors.New("genesis: zero nym key")
}
if n.Commit == ([32]byte{}) {
return nil, errors.New("genesis: nym without commit would be unindexed")
}
if err := s.Nym.Insert([32]byte(n.Key), &NymEntry{Commit: n.Commit}); err != nil {
return nil, err
}
}
gh := Header{
Seq: 0, Time: cfg.T0,
NymT: s.Nym.Root(), NextNymT: s.NextNym.Root(),
InviteT: s.Invite.Root(), NextInvT: s.NextInvite.Root(),
ElectionT: s.Election.Root(), UTXOT: s.UTXO.Root(),
Nonce: cfg.Seed[:],
}
s.LastHash = gh.Hash()
return &Chain{State: s, Final: s.Clone(), finalIdx: -1, genesisState: s.Clone()}, nil
}
func (c *Chain) Produce(txs []core.Tx, T uint64, valPriv ed25519.PrivateKey, nonces map[[32]byte][]byte) (*Block, error) {
b, ns, err := BuildBlock(c.State, txs, T, valPriv, nonces)
if err != nil {
return nil, err
}
c.State = ns
c.Blocks = append(c.Blocks, b)
c.advanceFinality()
return b, nil
}
// horizon: finality at the previous period boundary.
func (c *Chain) horizon() uint64 {
ps := c.State.periodStart(c.State.Time)
if ps < PeriodSeconds+c.State.Genesis {
return c.State.Genesis
}
return ps - PeriodSeconds
}
func (c *Chain) advanceFinality() {
hz := c.horizon()
for c.finalIdx+1 < len(c.Blocks) && c.Blocks[c.finalIdx+1].Header.Time < hz {
ns, err := VerifyBlock(c.Final, c.Blocks[c.finalIdx+1])
if err != nil {
panic("finality: own chain does not verify: " + err.Error())
}
c.Final = ns
c.finalIdx++
}
}
func (c *Chain) stateAt(i int) (*State, error) {
s := c.Final.Clone()
for j := c.finalIdx + 1; j <= i; j++ {
ns, err := VerifyBlock(s, c.Blocks[j])
if err != nil {
return nil, err
}
s = ns
}
return s, nil
}
func (c *Chain) skips() uint64 { return c.State.Seq - uint64(len(c.Blocks)) }
// TryAdopt: fewest skipped slots wins; ties favor the longer chain.
func (c *Chain) TryAdopt(branch []*Block) (bool, error) {
if len(branch) == 0 {
return false, errors.New("adopt: empty branch")
}
attach := -2
if branch[0].Header.Prev == c.Final.LastHash {
attach = c.finalIdx
} else {
for j := c.finalIdx + 1; j < len(c.Blocks); j++ {
if branch[0].Header.Prev == c.Blocks[j].Header.Hash() {
attach = j
break
}
}
}
if attach == -2 {
return false, errors.New("adopt: branch does not attach above the finality horizon")
}
s, err := c.stateAt(attach)
if err != nil {
return false, err
}
for _, b := range branch {
ns, err := VerifyBlock(s, b)
if err != nil {
return false, fmt.Errorf("adopt: %w", err)
}
s = ns
}
candBlocks := uint64(attach+1) + uint64(len(branch))
candSkips := s.Seq - candBlocks
better := candSkips < c.skips() ||
(candSkips == c.skips() && s.Seq > c.State.Seq)
if !better {
return false, nil
}
c.Blocks = append(c.Blocks[:attach+1], branch...)
c.State = s
c.advanceFinality()
return true, nil
}
demo.go608
package bitpeople
// demo.go — three periods of the combined spec, end to end:
//
// period 0: genesis pair verifies (auto next_nym + auto invite
// token), Use hands the token to carol, the mixers run,
// Commit(n) → election → Commit(e) → Vote → UBI, a dust
// transfer, reveals
// period 1: pair verifies, court judges carol in (atomic promote
// with onboarding UBI), the chain runs for three, dave
// invited, reveals
// period 2: odd population — the lone nym is judged in by its
// court (JudgeNym), dave judged in, an unused invite
// token pruned, the expired dust pruned, reveals
//
// Blocks are sparse; every unproduced slot is a skipped slot.
import (
"crypto/ed25519"
"crypto/rand"
"errors"
"fmt"
"math/big"
"strings"
"peoplecoin/core"
)
type person struct {
name string
// current period's nym identity
priv ed25519.PrivateKey
pub core.PubKey
pre [32]byte // preimage of the current nym commit
// pending identity, chosen during the current period's mixing
nextPriv ed25519.PrivateKey
nextPub core.PubKey
nextPre [32]byte
}
func (p *person) rollover() {
p.priv, p.pub, p.pre = p.nextPriv, p.nextPub, p.nextPre
p.nextPriv, p.nextPub = nil, core.PubKey{}
p.nextPre = [32]byte{}
}
func (p *person) fresh() {
p.nextPriv, p.nextPub = core.GenKey()
if _, err := rand.Read(p.nextPre[:]); err != nil {
panic(err)
}
}
type demoWorld struct{ names map[core.PubKey]string }
func (w *demoWorld) tag(pub core.PubKey, label string) { w.names[pub] = label }
func (w *demoWorld) who(k [32]byte) string {
if n, ok := w.names[core.PubKey(k)]; ok {
return n
}
return core.PubKey(k).Short()
}
type valSvc struct {
name string
priv ed25519.PrivateKey
pub core.PubKey
nonces map[[32]byte][]byte // schedule commit → onion layer nonce
}
func newVal(name string) *valSvc {
priv, pub := core.GenKey()
return &valSvc{name: name, priv: priv, pub: pub, nonces: map[[32]byte][]byte{}}
}
// register stores one onion layer's reveal material under the commit
// value the schedule will show at this validator's turn:
// H(validator ‖ nonce).
func (v *valSvc) register(l core.OnionLayer) {
v.nonces[core.H(v.pub[:], l.Nonce)] = l.Nonce
}
// mkOnion builds a depth-layer multi-validator onion cycling the
// given validators, hands each their layer privately, and returns
// the top — an election commit or a genesis schedule entry.
func mkOnion(vals []*valSvc, depth int) [32]byte {
seq := make([]core.PubKey, depth)
for i := range seq {
seq[i] = vals[i%len(vals)].pub
}
commit, layers := core.BuildOnion(seq, func(p []byte) {
if _, err := rand.Read(p); err != nil {
panic(err)
}
})
for _, l := range layers {
for _, v := range vals {
if v.pub == l.Validator {
v.register(l)
}
}
}
return commit
}
func produceAny(c *Chain, vals []*valSvc, T uint64, txs []core.Tx) (*Block, *valSvc, error) {
for _, v := range vals {
b, err := c.Produce(txs, T, v.priv, v.nonces)
if err == nil {
return b, v, nil
}
if !strings.Contains(err.Error(), "not ours") {
return nil, nil, err
}
}
return nil, nil, errors.New("no validator service owns this slot")
}
// ------------------------------------------------------------ makers
func signed[T core.Tx](t T, priv ed25519.PrivateKey, set func(core.Sig)) T {
id := t.TxID()
set(core.Sign(priv, id[:]))
return t
}
func mkVerify(priv ed25519.PrivateKey) *Verify {
t := &Verify{Key: core.Pub(priv)}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
func mkJudge(priv ed25519.PrivateKey, target [32]byte) *Judge {
t := &Judge{Key: core.Pub(priv), Target: target}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
func mkJudgeNym(priv ed25519.PrivateKey, target [32]byte) *JudgeNym {
t := &JudgeNym{Key: core.Pub(priv), Target: target}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
func mkUse(priv ed25519.PrivateKey, invitee core.PubKey) *Use {
t := &Use{Key: [32]byte(core.Pub(priv)), NewKey: invitee}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
func mkCommitN(priv ed25519.PrivateKey, commit [32]byte) *CommitN {
t := &CommitN{Key: core.Pub(priv), Commit: commit}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
func mkCommitE(priv ed25519.PrivateKey, commit [32]byte) *CommitE {
t := &CommitE{Key: core.Pub(priv), Commit: commit}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
func mkVote(priv ed25519.PrivateKey) *Vote {
t := &Vote{Key: core.Pub(priv)}
return signed(t, priv, func(s core.Sig) { t.Sig = s })
}
// mkMix builds an n→n mix: inputs signed by their keys, outputs are
// fresh keys with mix_count = input mix_count sum / n + 1 spread.
func mkMix(trieID byte, s *State, privs []ed25519.PrivateKey) (*core.Mix, []ed25519.PrivateKey) {
trie := s.mixTrie(trieID)
n := len(privs)
t := &core.Mix{TrieID: trieID}
inSum := uint64(0)
for _, p := range privs {
k := [32]byte(core.Pub(p))
t.Inputs = append(t.Inputs, k)
inSum += uint64(trie.Get(k).MixCt())
}
outPrivs := make([]ed25519.PrivateKey, n)
// Spread the budget: total = inSum + n, split as evenly as possible.
total := inSum + uint64(n)
for i := 0; i < n; i++ {
mc := total / uint64(n)
if uint64(i) < total%uint64(n) {
mc++
}
priv, pub := core.GenKey()
outPrivs[i] = priv
t.Outputs = append(t.Outputs, core.MixOutput{Key: [32]byte(pub), MixCount: uint32(mc)})
}
id := t.TxID()
for _, p := range privs {
t.Sigs = append(t.Sigs, core.Sign(p, id[:]))
}
return t, outPrivs
}
// ------------------------------------------------------------ report
func printBoundary(w *demoWorld, s *State, period uint64) {
fmt.Printf("│ period %d seed=%s N=%d schedule=%d commits invites to judge=%d\n",
period, core.ShortHash(s.Seed), s.nymN(), len(s.Schedule), s.Invite.Count())
N := s.nymN()
for p := uint64(0); p < (N+1)/2; p++ {
a, b := "—", "—"
for r := uint64(0); r < N; r++ {
switch s.posOfRank(r) {
case 2 * p:
k, _ := s.Nym.Select(r)
a = w.who(k)
case 2*p + 1:
k, _ := s.Nym.Select(r)
b = w.who(k)
}
}
fmt.Printf("│ virtual pair %d: a=%-12s b=%-12s\n", p, a, b)
}
pc := N / 2
for i, k := range s.Invite.CommittedKeys() {
fmt.Printf("│ used invite: %s (court = pair %d)\n", w.who(k), uint64(i)%maxU64(pc, 1))
}
}
func fmtTok(v *big.Int) string {
f := new(big.Float).Quo(new(big.Float).SetInt(v), new(big.Float).SetInt(core.TOKEN))
return f.Text('f', 6)
}
// RunDemo drives the scenario and returns the chain for replay.
func RunDemo() (*Chain, error) {
w := &demoWorld{names: map[core.PubKey]string{}}
t0 := uint64(14) * core.NormPeriod // 2026-01-01, slot-aligned
slotT := func(period, slot uint64) uint64 { return t0 + period*PeriodSeconds + slot*core.SlotSeconds }
eventEnd := EventSeconds / core.SlotSeconds // slot index of RNG start
alice := &person{name: "alice"}
bob := &person{name: "bob"}
carol := &person{name: "carol"}
dave := &person{name: "dave"}
for _, p := range []*person{alice, bob, carol, dave} {
p.fresh()
p.rollover() // current identity for their first period
p.fresh() // pending
}
w.tag(alice.pub, "alice/p0")
w.tag(bob.pub, "bob/p0")
v1 := newVal("val-1")
v2 := newVal("val-2")
vals := []*valSvc{v1, v2}
seed0 := core.H([]byte("bpc demo genesis seed"))
chain, err := NewChain(GenesisConfig{
T0: t0,
Nyms: []GenesisNym{
{Key: alice.pub, Commit: core.H(alice.pre[:])},
{Key: bob.pub, Commit: core.H(bob.pre[:])},
},
Seed: seed0,
Schedule: [][32]byte{mkOnion([]*valSvc{v1, v2}, 16)},
})
if err != nil {
return nil, err
}
produce := func(period, slot uint64, txs []core.Tx, label string) error {
T := slotT(period, slot)
b, v, err := produceAny(chain, vals, T, txs)
if err != nil {
return fmt.Errorf("%s: %w", label, err)
}
fmt.Printf(" block seq=%-6d p%d/%-6s validator=%-6s txs=%-2d %s\n",
b.Header.Seq, period, phaseName(chain.State, T), v.name, len(txs), label)
return nil
}
pop := func() { fmt.Printf(" » population_count = %d\n", chain.State.Pop) }
fmt.Println("┌─ genesis")
printBoundary(w, chain.State, 0)
fmt.Println("└─")
// ======================================================= period 0
if err := produce(0, 1, []core.Tx{mkVerify(alice.priv), mkVerify(bob.priv)},
"event: pair verifies → auto next_nym ×2 + auto invite token"); err != nil {
return nil, err
}
pop()
// The auto invite token sits at the lower-position member's key.
s := chain.State
var inviter *person
for _, p := range []*person{alice, bob} {
if s.NextInvite.Get([32]byte(p.pub)) != nil {
inviter = p
}
}
w.tag(carol.pub, "carol/p1")
if err := produce(0, 3, []core.Tx{mkUse(inviter.priv, carol.pub)},
fmt.Sprintf("event: %s hands the invite token to carol (Use = rekey)", inviter.name)); err != nil {
return nil, err
}
// Mix next_nym; the mix output keys become next period's nyms.
mixN, outs := mkMix(TrieNextNym, chain.State, []ed25519.PrivateKey{alice.priv, bob.priv})
alice.nextPriv, alice.nextPub = outs[0], core.Pub(outs[0])
bob.nextPriv, bob.nextPub = outs[1], core.Pub(outs[1])
w.tag(alice.nextPub, "alice/p1")
w.tag(bob.nextPub, "bob/p1")
if err := produce(0, 5, []core.Tx{mixN}, "event: Mix(next_nym) — fresh keys, link broken"); err != nil {
return nil, err
}
// Commit(n) with next period's RNG commits → election entries.
if err := produce(0, 7, []core.Tx{
mkCommitN(alice.nextPriv, core.H(alice.nextPre[:])),
mkCommitN(bob.nextPriv, core.H(bob.nextPre[:])),
}, "event: Commit(n) ×2 → auto election entries"); err != nil {
return nil, err
}
// Mix the election trie, then Commit(e) with validator commits.
mixE, eouts := mkMix(TrieElection, chain.State, []ed25519.PrivateKey{alice.nextPriv, bob.nextPriv})
if err := produce(0, 9, []core.Tx{mixE}, "event: Mix(election) — voter↔validator link broken"); err != nil {
return nil, err
}
if err := produce(0, 11, []core.Tx{
mkCommitE(eouts[0], mkOnion([]*valSvc{v1}, 16)),
mkCommitE(eouts[1], mkOnion([]*valSvc{v2}, 16)),
}, "event: Commit(e) ×2 = hash(validator ‖ nonce)"); err != nil {
return nil, err
}
// Vote → UBI, minted to the post-mix election keys.
if err := produce(0, 13, []core.Tx{mkVote(eouts[0]), mkVote(eouts[1])},
"event: Vote ×2 → UBI minted"); err != nil {
return nil, err
}
// A dust transfer for the later prune.
s = chain.State
voteID := mkVote(eouts[0]).TxID()
ubiKey := core.CoinKey(voteID, 0)
_, throwP := core.GenKey()
w.tag(throwP, "dust")
spendT := slotT(0, 15)
dustAmt := big.NewInt(50_000_000)
change := new(big.Int).Sub(s.UTXO.Get(ubiKey).Spendable(spendT), dustAmt)
tr := &core.Transfer{
Inputs: []core.Outpoint{{Tx: voteID, Index: 0}},
Outputs: []core.Output{{Amount: dustAmt, Owner: throwP}, {Amount: change, Owner: core.Pub(eouts[0])}},
}
trID := tr.TxID()
tr.Sigs = []core.Sig{core.Sign(eouts[0], trID[:])}
if err := produce(0, 15, []core.Tx{tr}, "event: transfer (dust output, expires in ~14 h)"); err != nil {
return nil, err
}
// RNG: reveals.
if err := produce(0, eventEnd+1, []core.Tx{
&Reveal{Key: [32]byte(alice.pub), Preimage: alice.pre},
&Reveal{Key: [32]byte(bob.pub), Preimage: bob.pre},
}, "rng: reveals ×2"); err != nil {
return nil, err
}
// ======================================================= period 1
alice.rollover()
alice.fresh()
bob.rollover()
bob.fresh()
if err := produce(1, 1, []core.Tx{mkVerify(alice.priv), mkVerify(bob.priv)},
"boundary + event: pair verifies"); err != nil {
return nil, err
}
fmt.Println("┌─ activation")
printBoundary(w, chain.State, 1)
fmt.Println("└─")
pop()
// Court (the only pair) judges carol in: atomic promote.
if err := produce(1, 3, []core.Tx{
mkJudge(alice.priv, [32]byte(carol.pub)),
mkJudge(bob.priv, [32]byte(carol.pub)),
}, "event: court judges carol → next_nym + election + UBI, atomically"); err != nil {
return nil, err
}
pop()
// This period's auto invite token → dave.
s = chain.State
inviter = nil
for _, p := range []*person{alice, bob} {
if s.NextInvite.Get([32]byte(p.pub)) != nil {
inviter = p
}
}
w.tag(dave.pub, "dave/p2")
if err := produce(1, 5, []core.Tx{mkUse(inviter.priv, dave.pub)},
fmt.Sprintf("event: %s hands this period's token to dave", inviter.name)); err != nil {
return nil, err
}
// Mix next_nym ×3 (carol's judged entry joins the mixer).
mixN1, outs1 := mkMix(TrieNextNym, chain.State,
[]ed25519.PrivateKey{alice.priv, bob.priv, carol.priv})
alice.nextPriv, alice.nextPub = outs1[0], core.Pub(outs1[0])
bob.nextPriv, bob.nextPub = outs1[1], core.Pub(outs1[1])
carol.nextPriv, carol.nextPub = outs1[2], core.Pub(outs1[2])
carol.fresh2()
w.tag(alice.nextPub, "alice/p2")
w.tag(bob.nextPub, "bob/p2")
w.tag(carol.nextPub, "carol/p2")
if err := produce(1, 7, []core.Tx{mixN1}, "event: Mix(next_nym) ×3"); err != nil {
return nil, err
}
if err := produce(1, 9, []core.Tx{
mkCommitN(alice.nextPriv, core.H(alice.nextPre[:])),
mkCommitN(bob.nextPriv, core.H(bob.nextPre[:])),
mkCommitN(carol.nextPriv, core.H(carol.nextPre[:])),
}, "event: Commit(n) ×3"); err != nil {
return nil, err
}
// Election: three Commit(n) entries + carol's onboarding entry.
mixE1, eouts1 := mkMix(TrieElection, chain.State,
[]ed25519.PrivateKey{alice.nextPriv, bob.nextPriv, carol.nextPriv, carol.priv})
if err := produce(1, 11, []core.Tx{mixE1}, "event: Mix(election) ×4"); err != nil {
return nil, err
}
votes := []core.Tx{}
commits := []core.Tx{}
for i, ep := range eouts1 {
v := vals[i%2]
commits = append(commits, mkCommitE(ep, mkOnion([]*valSvc{v}, 16)))
votes = append(votes, mkVote(ep))
}
if err := produce(1, 13, commits, "event: Commit(e) ×4"); err != nil {
return nil, err
}
if err := produce(1, 15, votes, "event: Vote ×4 → UBI (carol's onboarding entry included)"); err != nil {
return nil, err
}
if err := produce(1, eventEnd+1, []core.Tx{
&Reveal{Key: [32]byte(alice.pub), Preimage: alice.pre},
&Reveal{Key: [32]byte(bob.pub), Preimage: bob.pre},
}, "rng: reveals ×2 (carol has no nym commit her first period)"); err != nil {
return nil, err
}
// ======================================================= period 2
alice.rollover()
alice.fresh()
bob.rollover()
bob.fresh()
carol.rollover()
carol.fresh()
if err := produce(2, 1, nil, "boundary: activation (N=3, odd)"); err != nil {
return nil, err
}
fmt.Println("┌─ activation")
printBoundary(w, chain.State, 2)
fmt.Println("└─")
// Identify the full pair and the lone position.
s = chain.State
N := s.nymN()
var pairA, pairB, lone *person
for _, p := range []*person{alice, bob, carol} {
r, _ := s.nymRank([32]byte(p.pub))
switch s.posOfRank(r) {
case 0:
pairA = p
case 1:
pairB = p
default:
lone = p
}
}
_ = N
if err := produce(2, 1+1, []core.Tx{
mkVerify(pairA.priv), mkVerify(pairB.priv),
}, fmt.Sprintf("event: pair 0 (%s, %s) verifies", pairA.name, pairB.name)); err != nil {
return nil, err
}
pop()
if err := produce(2, 3, []core.Tx{
mkJudgeNym(pairA.priv, [32]byte(lone.pub)),
mkJudgeNym(pairB.priv, [32]byte(lone.pub)),
}, fmt.Sprintf("event: court (pair 0) judges lone %s → next_nym", lone.name)); err != nil {
return nil, err
}
pop()
if err := produce(2, 5, []core.Tx{
mkJudge(pairA.priv, [32]byte(dave.pub)),
mkJudge(pairB.priv, [32]byte(dave.pub)),
}, "event: court judges dave in"); err != nil {
return nil, err
}
pop()
// Prune the expired dust.
s = chain.State
var dustKey [32]byte
s.UTXO.ForEach(func(k [32]byte, e *core.CoinEntry) {
if w.who([32]byte(e.Owner)) == "dust" {
dustKey = k
}
})
if err := produce(2, 7, []core.Tx{&Prune{TrieID: TrieUTXO, Keys: [][32]byte{dustKey}}},
"event: prune expired dust (producer collects)"); err != nil {
return nil, err
}
// Mix + commit ×4 (dave joins), election ×5.
mixN2, outs2 := mkMix(TrieNextNym, chain.State,
[]ed25519.PrivateKey{pairA.priv, pairB.priv, lone.priv, dave.priv})
people2 := []*person{pairA, pairB, lone, dave}
for i, p := range people2 {
p.nextPriv, p.nextPub = outs2[i], core.Pub(outs2[i])
p.fresh2()
w.tag(p.nextPub, p.name+"/p3")
}
if err := produce(2, 9, []core.Tx{mixN2}, "event: Mix(next_nym) ×4"); err != nil {
return nil, err
}
var cN []core.Tx
for _, p := range people2 {
cN = append(cN, mkCommitN(p.nextPriv, core.H(p.nextPre[:])))
}
if err := produce(2, 11, cN, "event: Commit(n) ×4"); err != nil {
return nil, err
}
elecPrivs := []ed25519.PrivateKey{dave.priv}
for _, p := range people2 {
elecPrivs = append(elecPrivs, p.nextPriv)
}
mixE2, eouts2 := mkMix(TrieElection, chain.State, elecPrivs)
if err := produce(2, 13, []core.Tx{mixE2}, "event: Mix(election) ×5"); err != nil {
return nil, err
}
commits2, votes2 := []core.Tx{}, []core.Tx{}
for i, ep := range eouts2 {
commits2 = append(commits2, mkCommitE(ep, mkOnion([]*valSvc{vals[i%2]}, 16)))
votes2 = append(votes2, mkVote(ep))
}
if err := produce(2, 15, commits2, "event: Commit(e) ×5"); err != nil {
return nil, err
}
if err := produce(2, 17, votes2, "event: Vote ×5 → UBI"); err != nil {
return nil, err
}
// This period's invite token stays unused → prune it in Pause.
var reveals []core.Tx
for _, p := range []*person{pairA, pairB, lone} {
reveals = append(reveals, &Reveal{Key: [32]byte(p.pub), Preimage: p.pre})
}
if err := produce(2, eventEnd+1, reveals, "rng: reveals ×3"); err != nil {
return nil, err
}
s = chain.State
var staleTok [][32]byte
s.NextInvite.ForEach(func(k [32]byte, e core.Entry) {
if !e.Counted() {
staleTok = append(staleTok, k)
}
})
if len(staleTok) > 0 {
if err := produce(2, (EventSeconds+RNGSeconds)/core.SlotSeconds+1,
[]core.Tx{&Prune{TrieID: TrieNextInvite, Keys: staleTok}},
"pause: prune the unused invite token"); err != nil {
return nil, err
}
}
// ======================================================= period 3
if err := produce(3, 1, nil, "boundary: activation"); err != nil {
return nil, err
}
fmt.Println("┌─ activation")
printBoundary(w, chain.State, 3)
fmt.Println("└─")
T := chain.State.Time
fmt.Printf("supply(T) = %s TOKEN (Vote-minted UBI + fees to validators)\n", fmtTok(chain.State.SupplyAt(T)))
fmt.Printf("ubi/period = %s TOKEN\n", fmtTok(UBIPerPeriod()))
fmt.Printf("blocks=%d tip seq=%d skipped slots=%d (sparse demo) final idx=%d\n",
len(chain.Blocks), chain.State.Seq, chain.skips(), chain.finalIdx)
// Full replay through VerifyBlock — the demo self-verifies.
fmt.Print("replaying full chain through VerifyBlock… ")
rs := chain.genesisState.Clone()
for i, b := range chain.Blocks {
ns, err := VerifyBlock(rs, b)
if err != nil {
return nil, fmt.Errorf("replay block %d: %w", i, err)
}
rs = ns
}
if rs.LastHash != chain.State.LastHash {
return nil, errors.New("replay tip differs from build tip")
}
fmt.Printf("ok (%d blocks, tip %s)\n", len(chain.Blocks), core.ShortHash(rs.LastHash))
return chain, nil
}
// fresh2 regenerates only the pending preimage/keys when the pending
// key was assigned externally (from a mix output).
func (p *person) fresh2() {
if _, err := rand.Read(p.nextPre[:]); err != nil {
panic(err)
}
}
entries.go84
package bitpeople
import (
"math/big"
"peoplecoin/core"
)
// ============================================================== nym
// NymEntry — nym_trie and next_nym_trie share the shape. In the
// active trie: verified/judged/seed_hits live, commit is the RNG
// commit. In the mixer: mix_count and commit (set by Commit(n)).
type NymEntry struct {
MixCount uint32
Verified bool
JudgedA bool
JudgedB bool
Commit [32]byte
SeedHits uint32
}
func (e *NymEntry) LeafHash(k [32]byte) [32]byte {
var w core.Buf
w.U8(0x10)
w.Bytes(k[:])
w.U32(e.MixCount)
w.Boolb(e.Verified)
w.Boolb(e.JudgedA)
w.Boolb(e.JudgedB)
w.Bytes(e.Commit[:])
w.U32(e.SeedHits)
return core.H(w.B)
}
// committed: the nym tries' own count definition (v14: aggregates
// are type-specific), commit != 0 per the nym_trie section. The
// commit IS the registration act — indexing is established by commits, so
// an entry that never commits has no rank, no pair, no court, no
// RNG: the protocol cannot even reference it. It self-cleans (no
// verify → no next_nym entry → gone at the next swap) or is pruned.
func (e *NymEntry) Counted() bool { return e.Commit != [32]byte{} }
func (e *NymEntry) HitsVal() uint32 { return e.SeedHits }
func (e *NymEntry) SumVal() *big.Int { return nil }
func (e *NymEntry) MixCt() uint32 { return e.MixCount }
func (e *NymEntry) CloneEntry() core.Entry { c := *e; return &c }
func freshNym(mc uint32) core.Entry { return &NymEntry{MixCount: mc} }
func NewNymTrie() *core.Trie { return core.NewTrie(0x11, freshNym, true, false) }
// ============================================================ invite
// InviteEntry — invite tokens. `used` doubles as the count aggregate
// (court ranks run over used invites in key order).
type InviteEntry struct {
MixCount uint32
Used bool
CourtJudgedA bool
CourtJudgedB bool
}
func (e *InviteEntry) LeafHash(k [32]byte) [32]byte {
var w core.Buf
w.U8(0x12)
w.Bytes(k[:])
w.U32(e.MixCount)
w.Boolb(e.Used)
w.Boolb(e.CourtJudgedA)
w.Boolb(e.CourtJudgedB)
return core.H(w.B)
}
// committed: used — the invite analog of commit != 0 (the entry has
// no commit field); court ranks run over real invites.
func (e *InviteEntry) Counted() bool { return e.Used }
func (e *InviteEntry) HitsVal() uint32 { return 0 }
func (e *InviteEntry) SumVal() *big.Int { return nil }
func (e *InviteEntry) MixCt() uint32 { return e.MixCount }
func (e *InviteEntry) CloneEntry() core.Entry { c := *e; return &c }
func freshInvite(mc uint32) core.Entry { return &InviteEntry{MixCount: mc} }
func NewInviteTrie() *core.Trie { return core.NewTrie(0x13, freshInvite, false, false) }
feistel.go120
package bitpeople
// feistel.go — bitpeople's Feistel shuffle (Horst Feistel, 1973).
//
// A guaranteed (not probabilistic) permutation: every position can be
// computed independently from the seed, without computing the full
// shuffle. A single round always produces unique outputs — the half
// that is hashed passes through unchanged, so the hash result is the
// same for inputs sharing that half, and XOR with the same value
// cannot make two different values equal. Each round's output is
// indistinguishable from fresh input, so any number of rounds
// preserves uniqueness.
//
// Per the bitpeople spec:
// - width = smallest even number of bits ≥ log2(N), split in halves
// - 3 rounds: L', R' = R, L XOR f(R, key_i)
// - round_key_i = sha256(seed || i)
// - f(R, key) = sha256(R || key), truncated to half-width bits
// - cycle-walking: if result ≥ N, apply again until < N. Since the
// permutation is a bijection on the full 2^width domain, no two
// chains can merge, so the first in-range value is unique.
//
// Encoding choices fixed by this implementation (the spec leaves them
// open): the round index i is a single byte 0..2, and R is encoded as
// 8 bytes big-endian before hashing; the truncation takes the first
// 8 bytes of the digest as a big-endian uint64 masked to half-width.
import (
"peoplecoin/core"
"encoding/binary"
"math/bits"
)
// feistelWidth returns (halfWidth, halfMask) for a domain of size N ≥ 2.
func feistelWidth(N uint64) (uint, uint64) {
w := uint(bits.Len64(N - 1)) // ceil(log2(N)) for N ≥ 2
if w == 0 {
w = 1
}
if w%2 == 1 {
w++
}
hw := w / 2
return hw, (uint64(1) << hw) - 1
}
// feistel3 runs the three Feistel rounds over the 2^(2·hw) domain.
func feistel3(x uint64, hw uint, mask uint64, seed [32]byte) uint64 {
L := x >> hw
R := x & mask
for i := byte(0); i < 3; i++ {
key := core.H(seed[:], []byte{i}) // round_key_i = sha256(seed || i)
var rb [8]byte
binary.BigEndian.PutUint64(rb[:], R)
fh := core.H(rb[:], key[:]) // f(R, key) = sha256(R || key)
f := binary.BigEndian.Uint64(fh[:8]) & mask
L, R = R, L^f
}
return (L << hw) | R
}
// FeistelPos is the shuffle: new_position = feistel(nym_id, seed, N),
// with cycle-walking to stay inside [0, N).
func FeistelPos(id uint64, seed [32]byte, N uint64) uint64 {
if N <= 1 {
return 0
}
if id >= N {
panic("FeistelPos: id out of range")
}
hw, mask := feistelWidth(N)
x := id
for {
x = feistel3(x, hw, mask, seed)
if x < N {
return x
}
}
}
// feistel3Inv runs the rounds backwards — the inverse permutation.
// Forward round: L', R' = R, L ^ f(R). Backward: R = L', L = R' ^ f(L').
func feistel3Inv(x uint64, hw uint, mask uint64, seed [32]byte) uint64 {
L := x >> hw
R := x & mask
for i := byte(2); ; i-- {
key := core.H(seed[:], []byte{i})
var rb [8]byte
binary.BigEndian.PutUint64(rb[:], L) // forward's R is backward's L
fh := core.H(rb[:], key[:])
f := binary.BigEndian.Uint64(fh[:8]) & mask
L, R = R^f, L
if i == 0 {
break
}
}
return (L << hw) | R
}
// FeistelPosInv is the inverse of FeistelPos, with the same
// cycle-walking: repeatedly invert until the value falls in [0, N).
// Because the permutation is a bijection on the padded domain, this
// terminates and agrees with the forward walk.
func FeistelPosInv(pos uint64, seed [32]byte, N uint64) uint64 {
if N <= 1 {
return 0
}
if pos >= N {
panic("feistel: position out of range")
}
hw, mask := feistelWidth(N)
x := pos
for {
x = feistel3Inv(x, hw, mask, seed)
if x < N {
return x
}
}
}
feistel_inv_test.go19
package bitpeople
import (
"crypto/rand"
"testing"
)
func TestFeistelInverse(t *testing.T) {
for _, n := range []uint64{2, 3, 5, 8, 31, 100, 257} {
var seed [32]byte
rand.Read(seed[:])
for i := uint64(0); i < n; i++ {
p := FeistelPos(i, seed, n)
if FeistelPosInv(p, seed, n) != i {
t.Fatalf("N=%d: inv(pos(%d)) != %d", n, i, i)
}
}
}
}
tx.go623
package bitpeople
// tx.go — the transaction set of the combined spec, with the
// automatic side effects that drive the incentive chain:
//
// Verify → next_nym entries when the pair completes,
// + an invite token for eligible nyms (automatic)
// Judge → invitee promoted: next_nym + election + UBI, atomically
// JudgeNym → next_nym entry when both court members have judged
// Commit(n)→ election entry
// Vote → UBI minted to the post-mix address
//
// Rank lookups read the live tries: every index is locked at the
// swap. Mix and Transfer are the shared core primitives; this file
// gates and routes them.
import (
"errors"
"fmt"
"math/big"
"peoplecoin/core"
)
const (
OpVerify byte = 0x10
OpJudge byte = 0x11
OpJudgeNym byte = 0x12
OpUse byte = 0x14
OpCommitN byte = 0x30
OpCommitE byte = 0x31
OpVote byte = 0x32
OpReveal byte = 0x40
OpHeader byte = 0xF0
)
const (
maxInputs = 1024
maxOutputs = 1024
TrieUTXO byte = 0
TrieNextNym byte = 1
TrieNextInvite byte = 2
TrieElection byte = 3
)
// ============================================================ verify
// Verify — sets verified on the signer's nym_trie entry. When the
// virtual pair completes, both members automatically get next_nym
// entries (key = current address, zeroed), and an invite token is
// issued to the pair's lower-position member while the period's
// quota (population_count / 2, min 1) allows.
type Verify struct {
Key core.PubKey
Sig core.Sig
}
func (t *Verify) Body() []byte {
var w core.Buf
w.U8(OpVerify)
w.Bytes(t.Key[:])
return w.B
}
func (t *Verify) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyVerify(t *Verify, T uint64) error {
if !inEvent(s, T) {
return errors.New("verify: outside event phase")
}
e, _ := s.Nym.Get([32]byte(t.Key)).(*NymEntry)
if e == nil {
return errors.New("verify: no nym entry")
}
if e.Verified {
return errors.New("verify: already verified")
}
r, ok := s.nymRank([32]byte(t.Key))
if !ok {
return errors.New("verify: nym not in the boundary snapshot")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("verify: bad signature")
}
e.Verified = true
s.Nym.Refresh([32]byte(t.Key))
// Pair completion?
pr, hasPartner := s.partnerRank(r)
if !hasPartner {
return nil // lone position (odd N): the JudgeNym path
}
pk, _ := s.Nym.Select(pr)
pe, _ := s.Nym.Get(pk).(*NymEntry)
if pe == nil || !pe.Verified {
return nil
}
// Both verified: auto next_nym entries.
if err := s.NextNym.Insert([32]byte(t.Key), freshNym(0)); err != nil {
return fmt.Errorf("verify: %w", err)
}
if err := s.NextNym.Insert(pk, freshNym(0)); err != nil {
return fmt.Errorf("verify: %w", err)
}
s.Pop += 2
// Automatic invite issuance to the lower virtual position, while
// the growth cap (population_count / 2, min 1) allows.
if s.InvitesIssued < maxU64(1, s.Pop/2) {
low := [32]byte(t.Key)
if s.posOfRank(pr) < s.posOfRank(r) {
low = pk
}
if err := s.NextInvite.Insert(low, freshInvite(0)); err == nil {
s.InvitesIssued++
}
}
return nil
}
// ============================================================= judge
// Judge — a court pair member confirms a used invite from the
// previous period. Court assignment: invite at rank i (among used
// invites — the index locked at the swap) → court pair
// i mod pair_count. When both members have judged, the invitee is
// promoted atomically: next_nym + election + UBI. The entry stays,
// fully judged, until the swap discards the trie — no operation
// alters the invite index during the period (v15), and the flags
// themselves prevent double promotion.
type Judge struct {
Key core.PubKey // court member (nym_trie)
Target [32]byte // invite key (invite_trie)
Sig core.Sig
}
func (t *Judge) Body() []byte {
var w core.Buf
w.U8(OpJudge)
w.Bytes(t.Key[:])
w.Bytes(t.Target[:])
return w.B
}
func (t *Judge) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyJudge(t *Judge, T uint64) error {
if !inEvent(s, T) {
return errors.New("judge: outside event phase")
}
inv, _ := s.Invite.Get(t.Target).(*InviteEntry)
if inv == nil || !inv.Used {
return errors.New("judge: no used invite at target")
}
i, ok := s.inviteRank(t.Target)
if !ok {
return errors.New("judge: invite not in the boundary snapshot")
}
side, err := s.courtSide([32]byte(t.Key), i)
if err != nil {
return fmt.Errorf("judge: %w", err)
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("judge: bad signature")
}
if side == 0 {
if inv.CourtJudgedA {
return errors.New("judge: side a already judged")
}
inv.CourtJudgedA = true
} else {
if inv.CourtJudgedB {
return errors.New("judge: side b already judged")
}
inv.CourtJudgedB = true
}
s.Invite.Refresh(t.Target)
if !(inv.CourtJudgedA && inv.CourtJudgedB) {
return nil
}
// Promotion, atomic: next_nym + election + UBI. The invite
// entry remains in place, both flags set.
if err := s.NextNym.Insert(t.Target, freshNym(0)); err != nil {
return fmt.Errorf("judge: %w", err)
}
if err := s.Election.Insert(t.Target, core.FreshElection(0)); err != nil {
return fmt.Errorf("judge: %w", err)
}
ubi := UBIPerPeriod()
if err := s.UTXO.Insert(core.CoinKey(id, 0), &core.CoinEntry{
Owner: core.PubKey(t.Target), Amount: ubi,
Norm: core.Normalize(ubi, T, s.NormTime), Time: T,
}); err != nil {
return fmt.Errorf("judge: %w", err)
}
s.Pop++
return nil
}
// ========================================================== judgenym
// JudgeNym — a court pair member confirms a nym that cannot pair-
// verify (odd population's lone position, or a failed pair). Court:
// nym rank mod pair_count. Both sides set → next_nym entry.
type JudgeNym struct {
Key core.PubKey // court member
Target [32]byte // nym key
Sig core.Sig
}
func (t *JudgeNym) Body() []byte {
var w core.Buf
w.U8(OpJudgeNym)
w.Bytes(t.Key[:])
w.Bytes(t.Target[:])
return w.B
}
func (t *JudgeNym) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyJudgeNym(t *JudgeNym, T uint64) error {
if !inEvent(s, T) {
return errors.New("judge_nym: outside event phase")
}
e, _ := s.Nym.Get(t.Target).(*NymEntry)
if e == nil {
return errors.New("judge_nym: no nym entry at target")
}
tr, ok := s.nymRank(t.Target)
if !ok {
return errors.New("judge_nym: target not in the boundary snapshot")
}
side, err := s.courtSide([32]byte(t.Key), tr)
if err != nil {
return fmt.Errorf("judge_nym: %w", err)
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("judge_nym: bad signature")
}
if side == 0 {
if e.JudgedA {
return errors.New("judge_nym: side a already judged")
}
e.JudgedA = true
} else {
if e.JudgedB {
return errors.New("judge_nym: side b already judged")
}
e.JudgedB = true
}
s.Nym.Refresh(t.Target)
if e.JudgedA && e.JudgedB {
if err := s.NextNym.Insert(t.Target, freshNym(0)); err != nil {
return fmt.Errorf("judge_nym: %w", err)
}
s.Pop++
}
return nil
}
// =============================================================== use
// Use — the inviter hands the (mixed) invite token to the invitee by
// rekeying the next_invite entry to the invitee's key and setting
// used = true.
type Use struct {
Key [32]byte // current token key
NewKey core.PubKey // invitee's address
Sig core.Sig
}
func (t *Use) Body() []byte {
var w core.Buf
w.U8(OpUse)
w.Bytes(t.Key[:])
w.Bytes(t.NewKey[:])
return w.B
}
func (t *Use) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyUse(t *Use, T uint64) error {
if !inEvent(s, T) {
return errors.New("use: outside event phase")
}
inv, _ := s.NextInvite.Get(t.Key).(*InviteEntry)
if inv == nil {
return errors.New("use: no invite token at key")
}
if inv.Used {
return errors.New("use: token already used")
}
if t.NewKey == (core.PubKey{}) || s.NextInvite.Get([32]byte(t.NewKey)) != nil {
return errors.New("use: invitee key not fresh")
}
id := t.TxID()
if !core.VerifySig(core.PubKey(t.Key), id[:], t.Sig) {
return errors.New("use: bad signature")
}
if _, err := s.NextInvite.Remove(t.Key); err != nil {
return err
}
return s.NextInvite.Insert([32]byte(t.NewKey),
&InviteEntry{MixCount: inv.MixCount, Used: true})
}
// =============================================================== mix
// applyMix routes and phase-gates the core Mix.
func (s *State) applyMix(t *core.Mix, T uint64) error {
if !inEvent(s, T) {
return errors.New("mix: outside event phase")
}
trie := s.mixTrie(t.TrieID)
if trie == nil {
return errors.New("mix: trie does not support mixing")
}
return core.ApplyMix(trie, t)
}
func (s *State) mixTrie(id byte) *core.Trie {
switch id {
case TrieNextNym:
return s.NextNym
case TrieNextInvite:
return s.NextInvite
case TrieElection:
return s.Election
}
return nil
}
// ============================================================ commit
// CommitN — sets the RNG commit on a next_nym entry. Requires
// mix_count > 0 (everyone passes through the mixer). Automatically
// creates the election entry at the same (post-mix) address.
type CommitN struct {
Key core.PubKey
Commit [32]byte
Sig core.Sig
}
func (t *CommitN) Body() []byte {
var w core.Buf
w.U8(OpCommitN)
w.Bytes(t.Key[:])
w.Bytes(t.Commit[:])
return w.B
}
func (t *CommitN) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyCommitN(t *CommitN, T uint64) error {
if !inEvent(s, T) {
return errors.New("commit_n: outside event phase (deadline = RNG start)")
}
e, _ := s.NextNym.Get([32]byte(t.Key)).(*NymEntry)
if e == nil {
return errors.New("commit_n: no next_nym entry")
}
if e.MixCount == 0 {
return errors.New("commit_n: mix_count must be > 0")
}
if e.Commit != ([32]byte{}) {
return errors.New("commit_n: already committed")
}
if t.Commit == ([32]byte{}) {
return errors.New("commit_n: zero commit")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("commit_n: bad signature")
}
if err := s.Election.Insert([32]byte(t.Key), core.FreshElection(0)); err != nil {
return fmt.Errorf("commit_n: %w", err)
}
e.Commit = t.Commit
s.NextNym.Touch([32]byte(t.Key)) // the index grows by this commit
return nil
}
// CommitE — sets commit = hash(validator || nonce) on an election
// entry. Requires mix_count > 0. The voter gives the nonce to the
// validator privately.
type CommitE struct {
Key core.PubKey
Commit [32]byte
Sig core.Sig
}
func (t *CommitE) Body() []byte {
var w core.Buf
w.U8(OpCommitE)
w.Bytes(t.Key[:])
w.Bytes(t.Commit[:])
return w.B
}
func (t *CommitE) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyCommitE(t *CommitE, T uint64) error {
if !inEvent(s, T) {
return errors.New("commit_e: outside event phase (deadline = RNG start)")
}
e, _ := s.Election.Get([32]byte(t.Key)).(*core.ElectionEntry)
if e == nil {
return errors.New("commit_e: no election entry")
}
if e.MixCount == 0 {
return errors.New("commit_e: mix_count must be > 0")
}
if e.Commit != ([32]byte{}) {
return errors.New("commit_e: already committed")
}
if t.Commit == ([32]byte{}) {
return errors.New("commit_e: zero commit")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("commit_e: bad signature")
}
e.Commit = t.Commit
s.Election.Touch([32]byte(t.Key))
return nil
}
// ============================================================== vote
// Vote — triggers the UBI mint to the election entry's address. The
// mint is keyed hash(txid || 0); a replayed Vote produces the same
// txid (deterministic signatures), collides, and is rejected — the
// idempotence is structural.
type Vote struct {
Key core.PubKey
Sig core.Sig
}
func (t *Vote) Body() []byte {
var w core.Buf
w.U8(OpVote)
w.Bytes(t.Key[:])
return w.B
}
func (t *Vote) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyVote(t *Vote, T uint64) error {
if !inEvent(s, T) {
return errors.New("vote: outside event phase")
}
e, _ := s.Election.Get([32]byte(t.Key)).(*core.ElectionEntry)
if e == nil {
return errors.New("vote: no election entry")
}
if e.Commit == ([32]byte{}) {
return errors.New("vote: entry has not committed")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("vote: bad signature")
}
ubi := UBIPerPeriod()
return s.UTXO.Insert(core.CoinKey(id, 0), &core.CoinEntry{
Owner: t.Key, Amount: ubi,
Norm: core.Normalize(ubi, T, s.NormTime), Time: T,
})
}
// ============================================================ reveal
// Reveal — RNG phase. Preimage matching the nym entry's commit;
// target = (revealer_rank + uint(preimage)) mod count. The index was
// established by commits in next_nym and locked at the swap — counts
// are delta-maintained and Reveal issues no deltas — so the commit
// field is consumable: Reveal zeroes it, and commit == 0 IS the
// replay protection. Without it a block producer could re-include a
// chosen reveal 2k+1 times, inflating its target's hits while the
// XOR folds collapse to one — seed grinding. The revealed entry
// keeps its rank and remains a valid target: N never shrinks.
type Reveal struct {
Key [32]byte
Preimage [32]byte
}
func (t *Reveal) Body() []byte {
var w core.Buf
w.U8(OpReveal)
w.Bytes(t.Key[:])
w.Bytes(t.Preimage[:])
return w.B
}
func (t *Reveal) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyReveal(t *Reveal, T uint64) error {
if !inRNG(s, T) {
return errors.New("reveal: outside RNG phase")
}
e, _ := s.Nym.Get(t.Key).(*NymEntry)
if e == nil {
return errors.New("reveal: no nym entry")
}
if e.Commit == ([32]byte{}) {
return errors.New("reveal: no commit (or already revealed)")
}
if core.H(t.Preimage[:]) != e.Commit {
return errors.New("reveal: preimage does not match commit")
}
rr, ok := s.nymRank(t.Key)
if !ok {
return errors.New("reveal: revealer has no rank")
}
N := s.nymN()
tgt := new(big.Int).SetBytes(t.Preimage[:])
tgt.Add(tgt, new(big.Int).SetUint64(rr))
tgt.Mod(tgt, new(big.Int).SetUint64(N))
targetKey, _ := s.Nym.Select(tgt.Uint64())
te, _ := s.Nym.Get(targetKey).(*NymEntry)
if te == nil {
return errors.New("reveal: target lookup failed")
}
te.SeedHits++
s.Nym.Refresh(targetKey)
s.XorAcc[targetKey] = core.Xor32(s.XorAcc[targetKey], t.Preimage)
e.Commit = [32]byte{} // consumed; the locked index is untouched (no Touch)
s.Nym.Refresh(t.Key)
return nil
}
// ========================================================= utxo layer
// Prune — expired UTXOs anytime (the producer collects); stale
// identity entries after the commit deadline.
type Prune struct {
TrieID byte
Keys [][32]byte
}
func (t *Prune) Body() []byte {
var w core.Buf
w.U8(core.OpPrune)
w.U8(t.TrieID)
w.U32(uint32(len(t.Keys)))
for i := range t.Keys {
w.Bytes(t.Keys[i][:])
}
return w.B
}
func (t *Prune) TxID() [32]byte { return core.H(t.Body()) }
func (s *State) applyPrune(t *Prune, T uint64) (*big.Int, error) {
switch t.TrieID {
case TrieUTXO:
return core.PruneCoins(s.UTXO, t.Keys, T)
case TrieNextNym, TrieNextInvite:
if inEvent(s, T) {
return nil, errors.New("prune: identity entries prunable after the commit deadline")
}
trie := s.mixTrie(t.TrieID)
for _, k := range t.Keys {
e := trie.Get(k)
if e == nil {
return nil, errors.New("prune: entry missing")
}
if e.Counted() {
return nil, errors.New("prune: entry is not stale")
}
if _, err := trie.Remove(k); err != nil {
return nil, err
}
}
return new(big.Int), nil
}
return nil, errors.New("prune: bad trie id")
}
// ========================================================== dispatch
func (s *State) ApplyTxs(txs []core.Tx, T uint64) (*big.Int, error) {
fees := new(big.Int)
for i, tx := range txs {
var (
f *big.Int
err error
)
switch t := tx.(type) {
case *Verify:
err = s.applyVerify(t, T)
case *Judge:
err = s.applyJudge(t, T)
case *JudgeNym:
err = s.applyJudgeNym(t, T)
case *Use:
err = s.applyUse(t, T)
case *core.Mix:
err = s.applyMix(t, T)
case *CommitN:
err = s.applyCommitN(t, T)
case *CommitE:
err = s.applyCommitE(t, T)
case *Vote:
err = s.applyVote(t, T)
case *Reveal:
err = s.applyReveal(t, T)
case *core.Transfer:
f, err = core.ApplyTransfer(s.UTXO, t, T, s.NormTime)
case *Prune:
f, err = s.applyPrune(t, T)
default:
err = errors.New("unknown transaction type")
}
if err != nil {
return nil, fmt.Errorf("tx %d: %w", i, err)
}
if f != nil {
fees.Add(fees, f)
}
}
return fees, nil
}
func maxU64(a, b uint64) uint64 {
if a > b {
return a
}
return b
}
consensus.go311
package hierarchy
// consensus.go — hierarchy's chain: per-block randomness that
// evolves with every onion reveal (rand' = rand XOR H(nonce)), two
// election tries with a yearly swap, no phases, fork choice by
// fewest skips, finality at the previous election-period boundary.
import (
"crypto/ed25519"
"errors"
"fmt"
"math/big"
"peoplecoin/core"
)
// ElectionPeriodSeconds — hierarchy elects yearly.
const ElectionPeriodSeconds = core.SecondsPerYear
type State struct {
Genesis uint64
NormTime uint64 // the era's norm epoch — fixed for the deployment
Seq uint64
Time uint64
LastHash [32]byte
People *PeopleTree
Election *core.Trie // active: selection + peeling
NextElection *core.Trie // building: VoteClaim + Mix + Commit
UTXO *core.UTXOSet
Rand [32]byte // evolves per block: rand ^= H(nonce)
}
func (s *State) Clone() *State {
c := *s
c.People = s.People.Clone()
c.Election = s.Election.Clone()
c.NextElection = s.NextElection.Clone()
c.UTXO = s.UTXO.Clone()
return &c
}
func (s *State) electionPeriod(T uint64) uint64 {
return (T - s.Genesis) / ElectionPeriodSeconds
}
func (s *State) periodStart(T uint64) uint64 {
return s.Genesis + s.electionPeriod(T)*ElectionPeriodSeconds
}
func (s *State) seqAt(T uint64) uint64 { return (T - s.Genesis) / core.SlotSeconds }
// advancePeriods swaps the election tries at every yearly boundary
// crossed: next_election becomes active, a fresh next starts.
func advancePeriods(s *State, from, to uint64) {
for b := s.periodStart(from) + ElectionPeriodSeconds; b <= to; b += ElectionPeriodSeconds {
s.Election = s.NextElection
s.NextElection = core.NewElectionTrie()
}
}
// selected — rand mod count into the ACTIVE election trie; returns
// the entry key so the peel writes back into the trie itself.
func selected(s *State) ([32]byte, [32]byte, error) {
n := s.Election.Count()
if n == 0 {
return core.Zero32, core.Zero32, errors.New("empty election: chain halted")
}
i := new(big.Int).SetBytes(s.Rand[:])
i.Mod(i, new(big.Int).SetUint64(n))
k, _ := s.Election.Select(i.Uint64())
e, _ := s.Election.Get(k).(*core.ElectionEntry)
return k, e.Commit, nil
}
func preProcess(prev *State, T uint64) (*State, [32]byte, [32]byte, error) {
if T%core.SlotSeconds != 0 || T <= prev.Time {
return nil, core.Zero32, core.Zero32, errors.New("preprocess: time must land on a future slot")
}
ns := prev.Clone()
advancePeriods(ns, prev.Time, T)
k, sel, err := selected(ns)
if err != nil {
return nil, core.Zero32, core.Zero32, err
}
return ns, k, sel, nil
}
// storePeel writes the commit's successor into the active trie —
// the shared peel, nothing else. A zero successor (onion spent)
// drops the entry from the count via Touch; rand mod count adapts.
func storePeel(ns *State, key [32]byte, next [32]byte) {
ns.Election.Get(key).(*core.ElectionEntry).Commit = next
ns.Election.Touch(key)
}
// evolveRand is hierarchy's module RNG (interface point 3): each
// reveal's nonce feeds the per-block randomness. Separate from the
// peel — the peel is the election's shared role.
func evolveRand(ns *State, nonce []byte) {
ns.Rand = core.Xor32(ns.Rand, core.H(nonce))
}
func finish(ns *State, txs []core.Tx, T uint64, validator core.PubKey) error {
fees, err := ns.ApplyTxs(txs, T)
if err != nil {
return err
}
if fees.Sign() > 0 {
feeID := core.H([]byte{0xFD}, core.U64be(ns.seqAt(T)))
if err := ns.UTXO.Insert(core.CoinKey(feeID, 0), &core.CoinEntry{
Owner: validator, Amount: fees,
Norm: core.Normalize(fees, T, ns.NormTime), Time: T,
}); err != nil {
return fmt.Errorf("fee mint: %w", err)
}
}
return nil
}
type Header struct {
Seq uint64
Time uint64
PeopleT [32]byte
ElectionT [32]byte
NextElT [32]byte
UTXOT [32]byte
Rand [32]byte
Prev [32]byte
Validator core.PubKey
Nonce []byte // onion reveal: 64 bytes, 32 at the deepest layer
Sig core.Sig
}
func (h *Header) encode(withSig bool) []byte {
var w core.Buf
w.U8(OpHeader)
w.U64(h.Seq)
w.U64(h.Time)
w.Bytes(h.PeopleT[:])
w.Bytes(h.ElectionT[:])
w.Bytes(h.NextElT[:])
w.Bytes(h.UTXOT[:])
w.Bytes(h.Rand[:])
w.Bytes(h.Prev[:])
w.Bytes(h.Validator[:])
w.U8(byte(len(h.Nonce)))
w.Bytes(h.Nonce)
if withSig {
w.Bytes(h.Sig[:])
}
return w.B
}
func (h *Header) SigHash() [32]byte { return core.H(h.encode(false)) }
func (h *Header) Hash() [32]byte { return core.H(h.encode(true)) }
type Block struct {
Header Header
Txs []core.Tx
}
func BuildBlock(prev *State, txs []core.Tx, T uint64, valPriv ed25519.PrivateKey,
nonces map[[32]byte][]byte) (*Block, *State, error) {
ns, key, sel, err := preProcess(prev, T)
if err != nil {
return nil, nil, err
}
nonce, ok := nonces[sel]
if !ok {
return nil, nil, errors.New("build: selected commit is not ours (or consumed)")
}
pub := core.Pub(valPriv)
next, ok := core.VerifyPeel(sel, pub, nonce)
if !ok {
return nil, nil, errors.New("build: reveal material does not match the commit")
}
storePeel(ns, key, next)
evolveRand(ns, nonce)
if err := finish(ns, txs, T, pub); err != nil {
return nil, nil, err
}
h := Header{
Seq: prev.seqAt(T), Time: T,
PeopleT: ns.People.Hash(), ElectionT: ns.Election.Root(),
NextElT: ns.NextElection.Root(), UTXOT: ns.UTXO.Root(),
Rand: ns.Rand, Prev: prev.LastHash, Validator: pub, Nonce: nonce,
}
sh := h.SigHash()
h.Sig = core.Sign(valPriv, sh[:])
ns.Seq, ns.Time, ns.LastHash = h.Seq, T, h.Hash()
return &Block{Header: h, Txs: txs}, ns, nil
}
func VerifyBlock(prev *State, b *Block) (*State, error) {
h := &b.Header
if h.Seq != prev.seqAt(h.Time) {
return nil, errors.New("verify: seq is not slots-since-genesis")
}
if h.Prev != prev.LastHash {
return nil, errors.New("verify: prev hash mismatch")
}
ns, key, sel, err := preProcess(prev, h.Time)
if err != nil {
return nil, fmt.Errorf("verify: %w", err)
}
next, ok := core.VerifyPeel(sel, h.Validator, h.Nonce)
if !ok {
return nil, errors.New("verify: reveal does not peel the selected commit")
}
storePeel(ns, key, next)
evolveRand(ns, h.Nonce)
if err := finish(ns, b.Txs, h.Time, h.Validator); err != nil {
return nil, fmt.Errorf("verify: %w", err)
}
sh := h.SigHash()
if !core.VerifySig(h.Validator, sh[:], h.Sig) {
return nil, errors.New("verify: bad validator signature")
}
if ns.Rand != h.Rand {
return nil, errors.New("verify: rand does not evolve as rand ^= H(nonce)")
}
roots := [][2][32]byte{
{ns.People.Hash(), h.PeopleT}, {ns.Election.Root(), h.ElectionT},
{ns.NextElection.Root(), h.NextElT}, {ns.UTXO.Root(), h.UTXOT},
}
for i, p := range roots {
if p[0] != p[1] {
return nil, fmt.Errorf("verify: root %d mismatch", i)
}
}
ns.Seq, ns.Time, ns.LastHash = h.Seq, h.Time, h.Hash()
return ns, nil
}
// ============================================================ chain
type GenesisBootstrap struct {
Key [32]byte
Commit [32]byte // onion top
}
type GenesisConfig struct {
T0 uint64
Root core.PubKey
Persons []core.PubKey
Bootstrap []GenesisBootstrap // initial active-election entries
Rand [32]byte
}
type Chain struct {
genesisState *State
Blocks []*Block
State *State
}
func NewChain(cfg GenesisConfig) (*Chain, error) {
if cfg.T0%core.SlotSeconds != 0 {
return nil, errors.New("genesis: time must sit on the slot grid")
}
if len(cfg.Bootstrap) == 0 {
return nil, errors.New("genesis: empty bootstrap (chain halted at birth)")
}
s := &State{
Genesis: cfg.T0, NormTime: core.NormTimeFor(cfg.T0), Time: cfg.T0,
People: NewPeopleTree(cfg.Root, false, cfg.T0),
Election: core.NewElectionTrie(), NextElection: core.NewElectionTrie(),
UTXO: core.NewUTXOSet(), Rand: cfg.Rand,
}
for _, p := range cfg.Persons {
if err := s.People.Add(cfg.Root, p, true, cfg.T0); err != nil {
return nil, err
}
}
for _, b := range cfg.Bootstrap {
if b.Commit == core.Zero32 {
return nil, errors.New("genesis: zero bootstrap commit is unprovable")
}
e := &core.ElectionEntry{MixCount: 1, Commit: b.Commit}
if err := s.Election.Insert(b.Key, e); err != nil {
return nil, err
}
}
gh := Header{Time: cfg.T0, PeopleT: s.People.Hash(),
ElectionT: s.Election.Root(), UTXOT: s.UTXO.Root(), Rand: cfg.Rand,
Nonce: cfg.Rand[:]}
s.LastHash = gh.Hash()
return &Chain{genesisState: s.Clone(), State: s}, nil
}
func (c *Chain) Produce(txs []core.Tx, T uint64, priv ed25519.PrivateKey,
nonces map[[32]byte][]byte) (*Block, error) {
b, ns, err := BuildBlock(c.State, txs, T, priv, nonces)
if err != nil {
return nil, err
}
c.Blocks = append(c.Blocks, b)
c.State = ns
return b, nil
}
// Replay verifies the whole chain from genesis.
func (c *Chain) Replay() (*State, error) {
s := c.genesisState.Clone()
for i, b := range c.Blocks {
ns, err := VerifyBlock(s, b)
if err != nil {
return nil, fmt.Errorf("replay block %d: %w", i, err)
}
s = ns
}
return s, nil
}
demo.go223
package hierarchy
// demo.go — a compact hierarchy scenario: an org registers persons,
// UBI accrues continuously, vote claims feed the mixed onion
// election, the yearly boundary swaps the tries, and rand evolves
// with every reveal. Ends with a full replay.
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"math/big"
"strings"
"peoplecoin/core"
)
type valSvc struct {
name string
priv ed25519.PrivateKey
pub core.PubKey
nonces map[[32]byte][]byte
}
func newVal(name string) *valSvc {
priv, pub := core.GenKey()
return &valSvc{name: name, priv: priv, pub: pub, nonces: map[[32]byte][]byte{}}
}
func (v *valSvc) register(l core.OnionLayer) {
v.nonces[core.H(v.pub[:], l.Nonce)] = l.Nonce
}
func mkOnion(vals []*valSvc, depth int) [32]byte {
seq := make([]core.PubKey, depth)
for i := range seq {
seq[i] = vals[i%len(vals)].pub
}
commit, layers := core.BuildOnion(seq, func(p []byte) {
if _, err := rand.Read(p); err != nil {
panic(err)
}
})
for _, l := range layers {
for _, v := range vals {
if v.pub == l.Validator {
v.register(l)
}
}
}
return commit
}
func produceAny(c *Chain, vals []*valSvc, T uint64, txs []core.Tx) (*valSvc, error) {
for _, v := range vals {
if _, err := c.Produce(txs, T, v.priv, v.nonces); err == nil {
return v, nil
} else if !strings.Contains(err.Error(), "not ours") {
return nil, err
}
}
return nil, fmt.Errorf("no validator can produce slot at %d", T)
}
func sig(priv ed25519.PrivateKey, id [32]byte) core.Sig { return core.Sign(priv, id[:]) }
func fmtTok(v *big.Int) string {
q := new(big.Int).Quo(v, core.TOKEN)
r := new(big.Int).Mod(v, core.TOKEN)
return fmt.Sprintf("%s.%04s", q, new(big.Int).Quo(r, big.NewInt(1e12)))
}
// RunDemo builds and replays the hierarchy chain.
func RunDemo() (*Chain, error) {
c, err := runDemo()
if err != nil {
fmt.Println("DEMO FAILED:", err)
}
return c, err
}
func runDemo() (*Chain, error) {
day := uint64(24 * 3600)
T0 := uint64(14) * core.NormPeriod
v1, v2 := newVal("v1"), newVal("v2")
rootPriv, rootPub := core.GenKey()
alicePriv, alice := core.GenKey()
bobPriv, bob := core.GenKey()
chain, err := NewChain(GenesisConfig{
T0: T0, Root: rootPub, Persons: []core.PubKey{alice, bob},
Bootstrap: []GenesisBootstrap{{Key: core.H([]byte("boot-0")), Commit: mkOnion([]*valSvc{v1, v2}, 16)}},
Rand: core.H([]byte("hierarchy genesis rand")),
})
if err != nil {
return nil, err
}
fmt.Printf("genesis: org + %d persons, population %d, rand %s\n",
2, chain.State.People.Population(), core.ShortHash(chain.State.Rand))
at := func(T uint64, txs []core.Tx, note string) error {
v, err := produceAny(chain, []*valSvc{v1, v2}, T, txs)
if err != nil {
return fmt.Errorf("%s: %w", note, err)
}
fmt.Printf(" slot %-8d %-34s producer %s rand %s\n",
(T-T0)/core.SlotSeconds, note, v.name, core.ShortHash(chain.State.Rand))
return nil
}
// Day 30: alice claims a month of accrual.
u1 := &UBIClaim{Key: alice, Seq: T0}
u1.Sig = sig(alicePriv, u1.TxID())
if err := at(T0+30*day, []core.Tx{u1}, "alice UBIClaim"); err != nil {
return nil, err
}
fmt.Printf(" alice accrued %s TOKEN over 30 days\n",
fmtTok(chain.State.UTXO.Get(core.CoinKey(u1.TxID(), 0)).Amount))
// Day 60: org S under root; carol joins under S.
orgSPriv, orgS := core.GenKey()
carolPriv, carol := core.GenKey()
a1 := &Add{Parent: rootPub, Child: orgS, Leaf: false}
a1.ParentSig, a1.ChildSig = sig(rootPriv, a1.TxID()), sig(orgSPriv, a1.TxID())
a2 := &Add{Parent: orgS, Child: carol, Leaf: true}
a2.ParentSig, a2.ChildSig = sig(orgSPriv, a2.TxID()), sig(carolPriv, a2.TxID())
if err := at(T0+60*day, []core.Tx{a1, a2}, "Add org S + carol"); err != nil {
return nil, err
}
fmt.Printf(" population %d (tree %s)\n",
chain.State.People.Population(), core.ShortHash(chain.State.People.Hash()))
// Day 120: vote claims for period 0 → next_election.
var vcs []core.Tx
for _, p := range []struct {
priv ed25519.PrivateKey
pub core.PubKey
}{{alicePriv, alice}, {bobPriv, bob}, {carolPriv, carol}} {
vc := &VoteClaim{Key: p.pub, Period: 0}
vc.Sig = sig(p.priv, vc.TxID())
vcs = append(vcs, vc)
}
if err := at(T0+120*day, vcs, "3× VoteClaim"); err != nil {
return nil, err
}
// Day 121: mix the three vote entries to fresh keys.
ins := [][32]byte{[32]byte(alice), [32]byte(bob), [32]byte(carol)}
privs := []ed25519.PrivateKey{alicePriv, bobPriv, carolPriv}
var eprivs []ed25519.PrivateKey
mix := &core.Mix{TrieID: TrieElection}
for range ins {
np, npub := core.GenKey()
eprivs = append(eprivs, np)
mix.Outputs = append(mix.Outputs, core.MixOutput{Key: [32]byte(npub), MixCount: 1})
}
mix.Inputs = ins
mid := mix.TxID()
for _, p := range privs {
mix.Sigs = append(mix.Sigs, core.Sign(p, mid[:]))
}
if err := at(T0+121*day, []core.Tx{mix}, "Mix(next_election)"); err != nil {
return nil, err
}
// Day 122: onion commits; carol moves to root; bob rekeys.
var cms []core.Tx
pick := []*valSvc{v1, v2}
for i, ep := range eprivs {
cm := &Commit{Key: core.Pub(ep), Commit: mkOnion([]*valSvc{pick[i%2]}, 16)}
cm.Sig = sig(ep, cm.TxID())
cms = append(cms, cm)
}
mv := &Move{Child: carol, NewParent: rootPub}
mv.ChildSig, mv.NewParentSig = sig(carolPriv, mv.TxID()), sig(rootPriv, mv.TxID())
bob2Priv, bob2 := core.GenKey()
rk := &Rekey{Old: bob, New: bob2, Nonce: 0}
rk.OldSig, rk.NewSig = sig(bobPriv, rk.TxID()), sig(bob2Priv, rk.TxID())
if err := at(T0+122*day, append(cms, mv, rk), "3× Commit + Move + Rekey"); err != nil {
return nil, err
}
// Cross the yearly boundary: next_election becomes active.
yr := ElectionPeriodSeconds
if err := at(T0+yr+core.SlotSeconds, nil, "first block of period 1"); err != nil {
return nil, err
}
fmt.Printf(" election swapped: active count %d\n", chain.State.Election.Count())
// dave joins and immediately leaves — accrued UBI auto-minted.
davePriv, dave := core.GenKey()
a3 := &Add{Parent: rootPub, Child: dave, Leaf: true}
a3.ParentSig, a3.ChildSig = sig(rootPriv, a3.TxID()), sig(davePriv, a3.TxID())
if err := at(T0+yr+10*day, []core.Tx{a3}, "Add dave"); err != nil {
return nil, err
}
lv := &Leave{Child: dave}
lv.ChildSig = sig(davePriv, lv.TxID())
if err := at(T0+yr+40*day, []core.Tx{lv}, "dave Leave (auto-mint)"); err != nil {
return nil, err
}
auto := chain.State.UTXO.Get(core.CoinKey(lv.TxID(), 0))
fmt.Printf(" dave's accrual auto-minted: %s TOKEN, population %d\n",
fmtTok(auto.Amount), chain.State.People.Population())
// bob2 claims with the rekeyed identity (LastUBI followed).
n := chain.State.People.Get(bob2)
u2 := &UBIClaim{Key: bob2, Seq: n.LastUBI}
u2.Sig = sig(bob2Priv, u2.TxID())
if err := at(T0+yr+41*day, []core.Tx{u2}, "bob2 UBIClaim (post-rekey)"); err != nil {
return nil, err
}
// Full replay.
tip, err := chain.Replay()
if err != nil {
return nil, err
}
fmt.Printf("replay: ok (%d blocks, tip %s), population %d, supply %s TOKEN\n",
len(chain.Blocks), core.ShortHash(tip.LastHash), tip.People.Population(),
fmtTok(core.ValueAt(tip.UTXO.Sum(), tip.Time, tip.NormTime)))
return chain, nil
}
hier_test.go177
package hierarchy
import (
"math/big"
"testing"
"peoplecoin/core"
)
// TestTreeOps — add/move/rekey/remove with aggregates, cycle guard
// and canonical hashing.
func TestTreeOps(t *testing.T) {
_, root := core.GenKey()
pt := NewPeopleTree(root, false, 1000)
_, a := core.GenKey()
_, b := core.GenKey()
_, s := core.GenKey()
if err := pt.Add(root, a, true, 1000); err != nil {
t.Fatal(err)
}
if err := pt.Add(root, s, false, 1000); err != nil {
t.Fatal(err)
}
if err := pt.Add(s, b, true, 1000); err != nil {
t.Fatal(err)
}
if pt.Population() != 2 {
t.Fatalf("population %d, want 2", pt.Population())
}
if err := pt.Add(a, b, true, 1000); err == nil {
t.Fatal("person accepted children")
}
if err := pt.Move(s, s); err == nil {
t.Fatal("self-move accepted")
}
if err := pt.Move(root, s); err == nil {
t.Fatal("root moved / cycle accepted")
}
h1 := pt.Hash()
if err := pt.Move(b, root); err != nil {
t.Fatal(err)
}
if pt.Hash() == h1 {
t.Fatal("move did not change the tree hash")
}
_, b2 := core.GenKey()
if err := pt.Rekey(b, b2); err != nil {
t.Fatal(err)
}
if pt.Get(b) != nil || pt.Get(b2) == nil || pt.Get(b2).Nonce != 1 {
t.Fatal("rekey did not transfer the node")
}
removed, err := pt.Remove(s)
if err != nil || len(removed) != 0 {
t.Fatalf("remove of empty org: %v (%d persons)", err, len(removed))
}
if pt.Population() != 2 {
t.Fatal("population after removing empty org")
}
}
// TestClaimable — the continuous formula matches first principles:
// one year of accrual on contribution 1 is TOKEN × (1 − 0.8).
func TestClaimable(t *testing.T) {
acc := ClaimableAt(1, 0, core.SecondsPerYear)
want := new(big.Int).Quo(new(big.Int).Mul(core.TOKEN, big.NewInt(2)), big.NewInt(10))
diff := new(big.Int).Sub(acc, want)
diff.Abs(diff)
lim := new(big.Int).Quo(core.TOKEN, big.NewInt(100_000))
if diff.Cmp(lim) > 0 {
t.Fatalf("claimable(1yr) = %s, want ≈ %s", acc, want)
}
if ClaimableAt(1, 500, 500).Sign() != 0 {
t.Fatal("zero elapsed must accrue zero")
}
}
// TestHierChain — rand evolves as rand ^= H(nonce) per block, a
// 1-layer bootstrap is consumed (count drops), the yearly swap
// activates committed entries, and the whole chain replays.
func TestHierChain(t *testing.T) {
T0 := uint64(14) * core.NormPeriod
v1, v2 := newVal("v1"), newVal("v2")
rootPriv, root := core.GenKey()
alicePriv, alice := core.GenKey()
bobPriv, bob := core.GenKey()
_ = rootPriv
oneShot := mkOnion([]*valSvc{v2}, 1) // consumed after one reveal
deep := mkOnion([]*valSvc{v1}, 12)
chain, err := NewChain(GenesisConfig{
T0: T0, Root: root, Persons: []core.PubKey{alice, bob},
Bootstrap: []GenesisBootstrap{
{Key: core.H([]byte("deep")), Commit: deep},
{Key: core.H([]byte("one")), Commit: oneShot},
},
Rand: core.H([]byte("t rand")),
})
if err != nil {
t.Fatal(err)
}
prevRand := chain.State.Rand
produce := func(slot uint64, txs []core.Tx) {
t.Helper()
if _, err := produceAny(chain, []*valSvc{v1, v2}, T0+slot*core.SlotSeconds, txs); err != nil {
t.Fatal(err)
}
b := chain.Blocks[len(chain.Blocks)-1]
if chain.State.Rand != core.Xor32(prevRand, core.H(b.Header.Nonce)) {
t.Fatal("rand did not evolve as rand ^= H(nonce)")
}
prevRand = chain.State.Rand
}
// Produce until the one-shot entry has been consumed.
consumed := false
for slot := uint64(1); slot < 60 && !consumed; slot++ {
if _, err := produceAny(chain, []*valSvc{v1, v2}, T0+slot*core.SlotSeconds, nil); err != nil {
continue
}
b := chain.Blocks[len(chain.Blocks)-1]
if chain.State.Rand != core.Xor32(prevRand, core.H(b.Header.Nonce)) {
t.Fatal("rand did not evolve as rand ^= H(nonce)")
}
prevRand = chain.State.Rand
if chain.State.Election.Count() == 1 {
consumed = true
}
}
if !consumed {
t.Fatal("one-shot onion never consumed in 60 slots")
}
// Vote claims + mix + commits before the boundary.
var vcs []core.Tx
privs := []ed25519pair{{alicePriv, alice}, {bobPriv, bob}}
for _, p := range privs {
vc := &VoteClaim{Key: p.pub, Period: 0}
vc.Sig = core.Sign(p.priv, mustID(vc))
vcs = append(vcs, vc)
}
produce(100, vcs)
mix := &core.Mix{TrieID: TrieElection,
Inputs: [][32]byte{[32]byte(alice), [32]byte(bob)}}
var eprivs []ed25519pair
for i := 0; i < 2; i++ {
np, npub := core.GenKey()
eprivs = append(eprivs, ed25519pair{np, npub})
mix.Outputs = append(mix.Outputs, core.MixOutput{Key: [32]byte(npub), MixCount: 1})
}
mid := mix.TxID()
for _, p := range privs {
mix.Sigs = append(mix.Sigs, core.Sign(p.priv, mid[:]))
}
produce(101, []core.Tx{mix})
var cms []core.Tx
for i, ep := range eprivs {
vv := []*valSvc{v1, v2}[i]
cm := &Commit{Key: ep.pub, Commit: mkOnion([]*valSvc{vv}, 12)}
cm.Sig = core.Sign(ep.priv, mustID(cm))
cms = append(cms, cm)
}
produce(102, cms)
// Cross the yearly boundary: the two committed entries activate.
yearSlots := ElectionPeriodSeconds / core.SlotSeconds
produce(yearSlots+1, nil)
if chain.State.Election.Count() != 2 {
t.Fatalf("post-swap active count %d, want 2", chain.State.Election.Count())
}
if _, err := chain.Replay(); err != nil {
t.Fatal(err)
}
}
type ed25519pair struct {
priv []byte
pub core.PubKey
}
func mustID(tx core.Tx) []byte { id := tx.TxID(); return id[:] }
tree.go232
package hierarchy
// tree.go — the People Tree: hierarchical attestation. Your
// superior attested your identity; maps onto civil registration.
// Every node is a person (leaf) or a group/organization. tree_count
// aggregates persons upward; population = root.tree_count.
import (
"errors"
"math/big"
"peoplecoin/core"
)
type Node struct {
Key core.PubKey
Leaf bool // person
Children []core.PubKey
Nonce uint64 // rekey counter
LastUBI uint64
LastVote uint64 // last claimed election period + 1 (0 = never)
}
func (n *Node) clone() *Node {
c := *n
c.Children = append([]core.PubKey(nil), n.Children...)
return &c
}
type PeopleTree struct {
Root core.PubKey
nodes map[core.PubKey]*Node
parent map[core.PubKey]core.PubKey
}
func NewPeopleTree(root core.PubKey, rootIsPerson bool, t0 uint64) *PeopleTree {
pt := &PeopleTree{Root: root, nodes: map[core.PubKey]*Node{}, parent: map[core.PubKey]core.PubKey{}}
pt.nodes[root] = &Node{Key: root, Leaf: rootIsPerson, LastUBI: t0}
return pt
}
func (pt *PeopleTree) Get(k core.PubKey) *Node { return pt.nodes[k] }
func (pt *PeopleTree) Len() int { return len(pt.nodes) }
// Population: persons in the whole tree (root.tree_count).
func (pt *PeopleTree) Population() uint64 {
var walk func(k core.PubKey) uint64
walk = func(k core.PubKey) uint64 {
n := pt.nodes[k]
c := uint64(0)
if n.Leaf {
c = 1
}
for _, ch := range n.Children {
c += walk(ch)
}
return c
}
return walk(pt.Root)
}
// Add — parent adds child with consent (both signatures verified by
// the transaction layer).
func (pt *PeopleTree) Add(parent, child core.PubKey, leaf bool, t0 uint64) error {
p := pt.nodes[parent]
if p == nil {
return errors.New("tree: parent missing")
}
if p.Leaf {
return errors.New("tree: a person cannot hold children")
}
if pt.nodes[child] != nil {
return errors.New("tree: key already in the tree")
}
pt.nodes[child] = &Node{Key: child, Leaf: leaf, LastUBI: t0}
pt.parent[child] = parent
p.Children = append(p.Children, child)
return nil
}
// Remove — detach child and its entire subtree; returns the removed
// person nodes so accrued UBI can be auto-minted.
func (pt *PeopleTree) Remove(child core.PubKey) ([]*Node, error) {
par, ok := pt.parent[child]
if !ok {
return nil, errors.New("tree: node missing or is the root")
}
p := pt.nodes[par]
for i, c := range p.Children {
if c == child {
p.Children = append(p.Children[:i], p.Children[i+1:]...)
break
}
}
var removed []*Node
var walk func(k core.PubKey)
walk = func(k core.PubKey) {
n := pt.nodes[k]
for _, ch := range n.Children {
walk(ch)
}
if n.Leaf {
removed = append(removed, n)
}
delete(pt.nodes, k)
delete(pt.parent, k)
}
walk(child)
return removed, nil
}
// Move — child moves to a new parent with consent; state preserved.
func (pt *PeopleTree) Move(child, newParent core.PubKey) error {
if _, ok := pt.parent[child]; !ok {
return errors.New("tree: node missing or is the root")
}
np := pt.nodes[newParent]
if np == nil || np.Leaf {
return errors.New("tree: new parent missing or is a person")
}
// No cycles: newParent must not sit inside child's subtree.
for k := newParent; ; {
if k == child {
return errors.New("tree: move would create a cycle")
}
par, ok := pt.parent[k]
if !ok {
break
}
k = par
}
old := pt.nodes[pt.parent[child]]
for i, c := range old.Children {
if c == child {
old.Children = append(old.Children[:i], old.Children[i+1:]...)
break
}
}
pt.parent[child] = newParent
np.Children = append(np.Children, child)
return nil
}
// Rekey — the node changes key; nonce increments (replay guard),
// children and state follow.
func (pt *PeopleTree) Rekey(old, new core.PubKey) error {
n := pt.nodes[old]
if n == nil {
return errors.New("tree: node missing")
}
if pt.nodes[new] != nil {
return errors.New("tree: new key already in the tree")
}
n.Key = new
n.Nonce++
pt.nodes[new] = n
delete(pt.nodes, old)
if par, ok := pt.parent[old]; ok {
pt.parent[new] = par
delete(pt.parent, old)
ps := pt.nodes[par]
for i, c := range ps.Children {
if c == old {
ps.Children[i] = new
break
}
}
} else {
pt.Root = new
}
for _, ch := range n.Children {
pt.parent[ch] = new
}
return nil
}
// Hash folds the tree deterministically: each node hashes its
// fields, its tree_count aggregate and its children's hashes in
// order — the people_tree root for the block header.
func (pt *PeopleTree) Hash() [32]byte {
var fold func(k core.PubKey) ([32]byte, uint64)
fold = func(k core.PubKey) ([32]byte, uint64) {
n := pt.nodes[k]
var w core.Buf
w.U8(0x50)
w.Bytes(n.Key[:])
w.Boolb(n.Leaf)
w.U64(n.Nonce)
w.U64(n.LastUBI)
w.U64(n.LastVote)
count := uint64(0)
if n.Leaf {
count = 1
}
var kids core.Buf
for _, ch := range n.Children {
h, c := fold(ch)
count += c
kids.Bytes(h[:])
}
w.U64(count)
w.U32(uint32(len(n.Children)))
w.Bytes(kids.B)
return core.H(w.B), count
}
h, _ := fold(pt.Root)
return h
}
func (pt *PeopleTree) Clone() *PeopleTree {
c := &PeopleTree{Root: pt.Root, nodes: map[core.PubKey]*Node{}, parent: map[core.PubKey]core.PubKey{}}
for k, n := range pt.nodes {
c.nodes[k] = n.clone()
}
for k, v := range pt.parent {
c.parent[k] = v
}
return c
}
// ClaimableAt — the hierarchy UBI: continuous accrual since the
// last claim. contribution × TOKEN × (1 − decay^(T − last_ubi)).
func ClaimableAt(contribution uint64, lastUBI, T uint64) *big.Int {
if T <= lastUBI {
return new(big.Int)
}
p := core.DecayPow(T - lastUBI)
acc := new(big.Int).Sub(core.Scale, p)
acc.Mul(acc, core.TOKEN)
acc.Quo(acc, core.Scale)
return acc.Mul(acc, new(big.Int).SetUint64(contribution))
}
tx.go363
package hierarchy
// tx.go — hierarchy transactions. Tree operations carry the consent
// signatures the spec requires; UBIClaim settles continuous accrual;
// VoteClaim + Mix + Commit feed the shared onion election.
import (
"errors"
"fmt"
"math/big"
"peoplecoin/core"
)
const (
OpAdd byte = 0x60
OpRemove byte = 0x61
OpMove byte = 0x62
OpLeave byte = 0x63
OpRekey byte = 0x64
OpUBIClaim byte = 0x65
OpVClaim byte = 0x66
OpCommit byte = 0x67
OpHeader byte = 0xF1
TrieElection byte = 3 // Mix routing (matches bitpeople's id)
)
type Add struct {
Parent, Child core.PubKey
Leaf bool
ParentSig core.Sig
ChildSig core.Sig
}
func (t *Add) Body() []byte {
var w core.Buf
w.U8(OpAdd)
w.Bytes(t.Parent[:])
w.Bytes(t.Child[:])
w.Boolb(t.Leaf)
return w.B
}
func (t *Add) TxID() [32]byte { return core.H(t.Body()) }
type Remove struct {
Child core.PubKey
ParentSig core.Sig
}
func (t *Remove) Body() []byte {
var w core.Buf
w.U8(OpRemove)
w.Bytes(t.Child[:])
return w.B
}
func (t *Remove) TxID() [32]byte { return core.H(t.Body()) }
type Move struct {
Child, NewParent core.PubKey
ChildSig core.Sig
NewParentSig core.Sig
}
func (t *Move) Body() []byte {
var w core.Buf
w.U8(OpMove)
w.Bytes(t.Child[:])
w.Bytes(t.NewParent[:])
return w.B
}
func (t *Move) TxID() [32]byte { return core.H(t.Body()) }
type Leave struct {
Child core.PubKey
ChildSig core.Sig
}
func (t *Leave) Body() []byte {
var w core.Buf
w.U8(OpLeave)
w.Bytes(t.Child[:])
return w.B
}
func (t *Leave) TxID() [32]byte { return core.H(t.Body()) }
type Rekey struct {
Old, New core.PubKey
Nonce uint64 // must match the node's current nonce (replay)
OldSig core.Sig
NewSig core.Sig
}
func (t *Rekey) Body() []byte {
var w core.Buf
w.U8(OpRekey)
w.Bytes(t.Old[:])
w.Bytes(t.New[:])
w.U64(t.Nonce)
return w.B
}
func (t *Rekey) TxID() [32]byte { return core.H(t.Body()) }
type UBIClaim struct {
Key core.PubKey
Seq uint64 // last_ubi at claim time (replay guard)
Sig core.Sig
}
func (t *UBIClaim) Body() []byte {
var w core.Buf
w.U8(OpUBIClaim)
w.Bytes(t.Key[:])
w.U64(t.Seq)
return w.B
}
func (t *UBIClaim) TxID() [32]byte { return core.H(t.Body()) }
type VoteClaim struct {
Key core.PubKey
Period uint64
Sig core.Sig
}
func (t *VoteClaim) Body() []byte {
var w core.Buf
w.U8(OpVClaim)
w.Bytes(t.Key[:])
w.U64(t.Period)
return w.B
}
func (t *VoteClaim) TxID() [32]byte { return core.H(t.Body()) }
// Commit — sets the onion top on a mixed next_election entry.
type Commit struct {
Key core.PubKey
Commit [32]byte
Sig core.Sig
}
func (t *Commit) Body() []byte {
var w core.Buf
w.U8(OpCommit)
w.Bytes(t.Key[:])
w.Bytes(t.Commit[:])
return w.B
}
func (t *Commit) TxID() [32]byte { return core.H(t.Body()) }
// ============================================================ apply
func (s *State) applyAdd(t *Add, T uint64) error {
id := t.TxID()
if !core.VerifySig(t.Parent, id[:], t.ParentSig) || !core.VerifySig(t.Child, id[:], t.ChildSig) {
return errors.New("add: consent requires both signatures")
}
return s.People.Add(t.Parent, t.Child, t.Leaf, T)
}
func (s *State) applyRemove(t *Remove, T uint64) error {
par, ok := s.People.parent[t.Child]
if !ok {
return errors.New("remove: node missing or is the root")
}
id := t.TxID()
if !core.VerifySig(par, id[:], t.ParentSig) {
return errors.New("remove: parent signature required")
}
removed, err := s.People.Remove(t.Child)
if err != nil {
return err
}
return s.mintAccrued(removed, id, T)
}
func (s *State) applyLeave(t *Leave, T uint64) error {
id := t.TxID()
if !core.VerifySig(t.Child, id[:], t.ChildSig) {
return errors.New("leave: child signature required")
}
removed, err := s.People.Remove(t.Child)
if err != nil {
return err
}
return s.mintAccrued(removed, id, T)
}
// mintAccrued — "Accrued UBI auto-minted" on removal: each removed
// person's claimable value lands at their key.
func (s *State) mintAccrued(removed []*Node, id [32]byte, T uint64) error {
for i, n := range removed {
acc := ClaimableAt(1, n.LastUBI, T)
if acc.Sign() <= 0 {
continue
}
if err := s.UTXO.Insert(core.CoinKey(id, uint32(i)), &core.CoinEntry{
Owner: n.Key, Amount: acc,
Norm: core.Normalize(acc, T, s.NormTime), Time: T,
}); err != nil {
return fmt.Errorf("auto-mint: %w", err)
}
}
return nil
}
func (s *State) applyMove(t *Move) error {
id := t.TxID()
if !core.VerifySig(t.Child, id[:], t.ChildSig) || !core.VerifySig(t.NewParent, id[:], t.NewParentSig) {
return errors.New("move: consent requires both signatures")
}
return s.People.Move(t.Child, t.NewParent)
}
func (s *State) applyRekey(t *Rekey) error {
n := s.People.Get(t.Old)
if n == nil {
return errors.New("rekey: node missing")
}
if n.Nonce != t.Nonce {
return errors.New("rekey: nonce mismatch (replay)")
}
id := t.TxID()
if !core.VerifySig(t.Old, id[:], t.OldSig) || !core.VerifySig(t.New, id[:], t.NewSig) {
return errors.New("rekey: both keys must sign")
}
return s.People.Rekey(t.Old, t.New)
}
func (s *State) applyUBIClaim(t *UBIClaim, T uint64) error {
n := s.People.Get(t.Key)
if n == nil || !n.Leaf {
return errors.New("ubi: person not in the tree")
}
if n.LastUBI != t.Seq {
return errors.New("ubi: stale claim (replay)")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("ubi: bad signature")
}
acc := ClaimableAt(1, n.LastUBI, T)
if acc.Sign() <= 0 {
return errors.New("ubi: nothing accrued")
}
if err := s.UTXO.Insert(core.CoinKey(id, 0), &core.CoinEntry{
Owner: t.Key, Amount: acc,
Norm: core.Normalize(acc, T, s.NormTime), Time: T,
}); err != nil {
return err
}
n.LastUBI = T
return nil
}
func (s *State) applyVoteClaim(t *VoteClaim, T uint64) error {
n := s.People.Get(t.Key)
if n == nil || !n.Leaf {
return errors.New("voteclaim: person not in the tree")
}
period := s.electionPeriod(T)
// LastVote stores period+1 so the zero value means "never".
if t.Period != period || n.LastVote > period {
return errors.New("voteclaim: wrong or already-claimed period")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("voteclaim: bad signature")
}
if err := s.NextElection.Insert([32]byte(t.Key), core.FreshElection(0)); err != nil {
return err
}
n.LastVote = period + 1
return nil
}
func (s *State) applyCommit(t *Commit) error {
e, _ := s.NextElection.Get([32]byte(t.Key)).(*core.ElectionEntry)
if e == nil {
return errors.New("commit: no next-election entry at key")
}
if e.Commit != core.Zero32 {
return errors.New("commit: already committed")
}
if e.MixCount == 0 {
return errors.New("commit: entry must be mixed first")
}
if t.Commit == core.Zero32 {
return errors.New("commit: zero commit")
}
id := t.TxID()
if !core.VerifySig(t.Key, id[:], t.Sig) {
return errors.New("commit: bad signature")
}
e.Commit = t.Commit
s.NextElection.Touch([32]byte(t.Key))
return nil
}
// ApplyTxs — no phases: hierarchy operations are continuous.
func (s *State) ApplyTxs(txs []core.Tx, T uint64) (*big.Int, error) {
fees := new(big.Int)
seen := map[[32]byte]struct{}{}
for _, tx := range txs {
id := tx.TxID()
if _, dup := seen[id]; dup {
return nil, errors.New("apply: duplicate tx in block")
}
seen[id] = struct{}{}
var err error
var f *big.Int
switch t := tx.(type) {
case *Add:
err = s.applyAdd(t, T)
case *Remove:
err = s.applyRemove(t, T)
case *Move:
err = s.applyMove(t)
case *Leave:
err = s.applyLeave(t, T)
case *Rekey:
err = s.applyRekey(t)
case *UBIClaim:
err = s.applyUBIClaim(t, T)
case *VoteClaim:
err = s.applyVoteClaim(t, T)
case *Commit:
err = s.applyCommit(t)
case *core.Mix:
if t.TrieID != TrieElection {
err = errors.New("mix: hierarchy mixes the election trie only")
} else {
err = core.ApplyMix(s.NextElection, t)
}
case *core.Transfer:
f, err = core.ApplyTransfer(s.UTXO, t, T, s.NormTime)
case *Prune:
f, err = core.PruneCoins(s.UTXO, t.Keys, T)
default:
err = errors.New("apply: unknown tx type")
}
if err != nil {
return nil, fmt.Errorf("tx %x: %w", id[:4], err)
}
if f != nil {
fees.Add(fees, f)
}
}
return fees, nil
}
// Prune — hierarchy prunes expired UTXOs only.
type Prune struct{ Keys [][32]byte }
func (t *Prune) Body() []byte {
var w core.Buf
w.U8(core.OpPrune)
w.U32(uint32(len(t.Keys)))
for i := range t.Keys {
w.Bytes(t.Keys[i][:])
}
return w.B
}
func (t *Prune) TxID() [32]byte { return core.H(t.Body()) }
main.go25
// peoplecoin — pluggable UTXO + UBI by demurrage over a population-module
// interface. Two modules share one core: bitpeople (periodic
// anonymous verification) and hierarchy (hierarchical attestation).
package main
import (
"flag"
"fmt"
"peoplecoin/bitpeople"
"peoplecoin/hierarchy"
)
func main() {
module := flag.String("module", "both", "bitpeople | hierarchy | both")
flag.Parse()
if *module == "bitpeople" || *module == "both" {
fmt.Println("════════════════ population module: bitpeople ════════════════")
bitpeople.RunDemo()
}
if *module == "hierarchy" || *module == "both" {
fmt.Println("════════════════ population module: hierarchy ════════════════")
hierarchy.RunDemo()
}
}
go.mod3
module peoplecoin
go 1.22
README.md71
# peoplecoin — pluggable UTXO + UBI by demurrage over a population-register interface
> **Status: this is the project.** Coin + UBI + "one person, one
> unit of stake". Supersedes bpc (the v21 reference); the bitpeople
> module is bpc ported onto the core, plus the multi-validator hash
> onion.
One core, two population modules. The core provides payments (UTXO
with 20% annual demurrage as the UBI), the generic Mix, and the
election with a **multi-validator hash onion**. The register is a
module: anything that can answer the interface's seven points
(population_count, is_verified, randomness source, when election
entries are created, when UBI is minted, header fields, transactions)
plugs in.
```
go test ./... # 21 tests across three packages
go run . # both demos; -module bitpeople|hierarchy
```
## Packages
| Package | Contents |
|---|---|
| `core` | Binary Merkle radix trie (delta-maintained uint64 counts, max_seed_hits, sum, Merkle proofs), Mix, UTXO/Transfer/Prune (rent relocates value to the validator — nothing is destroyed), the norm tool (`sum ×= decay^Δ`, a planned era event), the election: entry + the **hash onion** (BuildOnion/VerifyPeel — successor commit, zero = consumed) |
| `bitpeople` | Periodic anonymous verification: nym/invite tries, Feistel pairing, phases, the incentive chain, per-period seed with the copied-down schedule **peeled in place** per reveal |
| `hierarchy` | Hierarchical attestation: the People Tree (Add/Remove/Move/Leave/Rekey with consent signatures, tree_count aggregates), continuous UBI (`ClaimableAt` — resurrected from bitpeople's dead code), VoteClaim, **two election tries** with a yearly swap, per-block randomness `rand ^= H(nonce)` |
## The onion
`layer_0 = H(val_Z‖nonce_Z)`, `layer_i = H(val‖nonce‖below)`. Each
layer can name a different validator — DoS protection at *every*
reveal, not just the first. The header nonce is 64 bytes
(`random_32 ‖ next_commit`), 32 at the deepest layer = consumption.
`VerifyPeel` is the shared role and returns the successor (zero =
consumed); that hierarchy additionally feeds the nonce into its
randomness (`evolveRand`) is interface point 3 and stands separate.
In bitpeople the copied-down schedule is peeled (derived state — its
election trie resets at the boundary); in hierarchy the active trie
is peeled live (count drops on consumption, `rand mod count` adapts —
the sequence evolves anyway).
## Documented module choices
Bitpeople: everything from the single-file implementation bpc holds
(frozen indexes, commit as the registration act, fixed era epoch,
uint64 counts). Hierarchy: `LastVote` stores period+1 (zero =
never), Remove/Leave auto-mint accrued UBI per removed person, Rekey
preserves LastUBI/LastVote through a nonce-counted key move, cycle
guard in Move. The UBI accrual is one shared kernel in core
(`AccruedUBI`); the modules differ only in trigger and cadence, and
that difference is forced rather than stylistic: an anonymous
register cannot carry a per-person accumulator — a `last_ubi`
surviving the Mix would be a tracking tag through it — so bitpeople
pays a uniform vote-gated lump to fresh keys, while hierarchy's
public tree accrues continuously. The modules import `core` qualified — every
`core.ApplyMix`, `core.VerifyPeel`, `core.Trie` displays the
core/module boundary at the call site, which is the point of the
architecture. (An earlier dot-import shortcut was reverted: it
caused three name collisions during the build alone, and hid the
interface the restructure exists to demonstrate.)
## Known limits
Same as bpc: Clone is O(N) per block (production form: persistent
nodes), block_size is TBD in the spec, no network layer. The
hierarchy People Tree hashes as a deterministic fold (not a Merkle
trie — the spec's Node is an explicit tree); Merkle proofs of tree
membership are the next step if proof-of-person is to be verified
externally. The hierarchy module is new and tested at demo depth;
bitpeople carries the full ported 18-test suite.
Run it
go test ./... // 21 tests across three packages
go run . // both demos, each ending in a full replay through VerifyBlock
go run . -module hierarchy