state/state_test.go
860 lines
1package state23import (4"crypto/sha256"5"errors"6"fmt"7"testing"8"time"910"peoplecoin/coin"11"peoplecoin/params"12"peoplecoin/period"13"peoplecoin/rng"14"peoplecoin/sig"15"peoplecoin/wire"16)1718// ---------------------------------------------------------------------19// Harness2021type validatorInfo struct {22key *sig.Keypair23nonce Hash24}2526type sim struct {27t *testing.T28st *State29people []*sig.Keypair30validators map[uint64]map[Address]validatorInfo // period → voter → who they voted for31pending map[Address]validatorInfo // votes in ops not yet in a block32tokens map[Address][3]Hash // address → election, reg, invite UTXO IDs33preimages map[uint32]map[Address]Hash // period → address → RNG preimage committed34}3536func keyFor(name string) *sig.Keypair { return sig.FromSeed(sha256.Sum256([]byte(name))) }3738func genesisTimeOf(s *sim) uint64 { return s.st.Time - s.st.Slot*params.SlotSeconds }3940func addrOf(k *sig.Keypair) Address { return Address(k.Public) }4142func h(s string) Hash { return sha256.Sum256([]byte(s)) }4344func newSim(t *testing.T, n int) *sim {45s := &sim{46t: t,47validators: map[uint64]map[Address]validatorInfo{0: {}},48pending: map[Address]validatorInfo{},49tokens: map[Address][3]Hash{},50preimages: map[uint32]map[Address]Hash{},51}52registry := make([]Address, n)53for i := 0; i < n; i++ {54s.people = append(s.people, keyFor(fmt.Sprintf("person-%d", i)))55registry[i] = addrOf(s.people[i])56}57// Person 0 holds the genesis vote (for validator key "val-A") and an58// election token for period 1.59valA := keyFor("val-A")60nonce := h("nonce-genesis")61s.validators[0][registry[0]] = validatorInfo{valA, nonce}62tokenID := h("genesis-election-token")63genesisTime := uint64(time.Date(2027, 1, 2, 7, 0, 0, 0, time.UTC).Unix()) // a Saturday 07:00 UTC64st, err := Genesis(genesisTime, registry,65[]GenesisVote{{Voter: registry[0], Commit: rng.ElectionCommit(Address(valA.Public), nonce)}},66[]GenesisToken{{ID: tokenID, Entry: wire.TokenEntry{TokenID: params.TokenElection, Owner: registry[0]}}})67if err != nil {68t.Fatal(err)69}70s.st = st71s.tokens[registry[0]] = [3]Hash{tokenID}72return s73}7475// tryBlock assembles a block for slot with ops on top of the current state76// and checks that Apply accepts exactly what Assemble produced.77func (s *sim) tryBlock(slot uint64, ops ...Op) (*Block, *State, error) {78voter, _, ok := s.st.AtSlot(slot).Validator(slot)79if !ok {80return nil, nil, ErrNoValidator81}82v, ok := s.validators[period.Of(slot)][voter]83if !ok {84s.t.Fatalf("slot %d: no validator key for voter %x", slot, voter[:4])85}86b, post, err := s.st.Assemble(slot, ops, v.key, v.nonce)87if err != nil {88return nil, nil, err89}90applied, err := s.st.Apply(b)91if err != nil {92s.t.Fatalf("slot %d: Apply rejected an assembled block: %v", slot, err)93}94if applied.Roots() != post.Roots() || applied.PrevHash != post.PrevHash || applied.Number != post.Number {95s.t.Fatalf("slot %d: Apply and Assemble disagree", slot)96}97// Wire round trip.98dec, err := DecodeBlock(b.Bytes())99if err != nil {100s.t.Fatalf("slot %d: block decode: %v", slot, err)101}102if applied2, err := s.st.Apply(dec); err != nil || applied2.Roots() != post.Roots() {103s.t.Fatalf("slot %d: decoded block does not apply identically: %v", slot, err)104}105return b, post, nil106}107108func (s *sim) block(slot uint64, ops ...Op) *Block {109b, post, err := s.tryBlock(slot, ops...)110if err != nil {111s.t.Fatalf("slot %d: %v", slot, err)112}113s.st = post114// Votes in this block decide next period's validators.115next := period.Of(slot) + 1116if s.validators[next] == nil {117s.validators[next] = map[Address]validatorInfo{}118}119for _, op := range ops {120if v, ok := op.(*Vote); ok {121for voter, info := range s.pending {122if rng.ElectionCommit(Address(info.key.Public), info.nonce) == v.Commit {123s.validators[next][voter] = info124}125}126}127}128s.pending = map[Address]validatorInfo{}129return b130}131132func (s *sim) mustFail(want error, slot uint64, ops ...Op) {133s.t.Helper()134_, _, err := s.tryBlock(slot, ops...)135if !errors.Is(err, want) {136s.t.Fatalf("slot %d: got %v, want %v", slot, err, want)137}138}139140func (s *sim) per(slot uint64) uint32 { return period.Number(slot) }141142func (s *sim) verify(k *sig.Keypair, slot uint64) Op {143op := &Verify{RegAddr: addrOf(k)}144m, _ := op.Message(s.per(slot))145op.Sig = k.Sign(m[:])146return op147}148149func (s *sim) verified(k *sig.Keypair, slot uint64) Op {150op := &Verified{RegAddr: addrOf(k)}151m, _ := op.Message(s.per(slot))152op.Sig = k.Sign(m[:])153txh, _ := TxHash(op, s.per(slot))154s.tokens[addrOf(k)] = [3]Hash{wire.UTXOID(txh, 0), wire.UTXOID(txh, 1), wire.UTXOID(txh, 2)}155return op156}157158func (s *sim) vote(k *sig.Keypair, validator *sig.Keypair, nonce Hash) Op {159op := &Vote{TokenUTXO: s.tokens[addrOf(k)][0], Commit: rng.ElectionCommit(Address(validator.Public), nonce)}160m, _ := op.Message(0)161op.Sig = k.Sign(m[:])162s.pending[addrOf(k)] = validatorInfo{validator, nonce}163return op164}165166func (s *sim) register(k *sig.Keypair, ids ...Hash) Op {167op := &Register{TokenUTXOs: ids}168m, _ := op.Message(0)169op.Sig = k.Sign(m[:])170return op171}172173// commitOp builds a Commit without recording the preimage (for ops that174// are expected to fail).175func (s *sim) commitOp(k *sig.Keypair, slot uint64, preimage Hash) Op {176op := &Commit{RngAddr: addrOf(k), Commit: rng.Commit(preimage)}177m, _ := op.Message(s.per(slot))178op.Sig = k.Sign(m[:])179return op180}181182func (s *sim) commit(k *sig.Keypair, slot uint64, preimage Hash) Op {183op := s.commitOp(k, slot, preimage)184if s.preimages[s.per(slot)] == nil {185s.preimages[s.per(slot)] = map[Address]Hash{}186}187s.preimages[s.per(slot)][addrOf(k)] = preimage188return op189}190191func (s *sim) reveal(k *sig.Keypair, slot uint64, preimage Hash) Op {192op := &Reveal{RngAddr: addrOf(k), Preimage: preimage}193m, _ := op.Message(s.per(slot))194op.Sig = k.Sign(m[:])195return op196}197198// mix signs a one-to-one mix of tokens owned by signers into outputs.199func (s *sim) mix(tokenID uint8, inputs []Hash, signers []*sig.Keypair, outputs []wire.MixOutput) Op {200op := &Mix{TokenID: tokenID, Inputs: inputs, Outputs: outputs}201m, _ := op.Message(0)202for _, k := range signers {203op.Sigs = append(op.Sigs, k.Sign(m[:]))204}205return op206}207208func (s *sim) transfer(inputs []Hash, signers []*sig.Keypair, outputs []wire.TransferOutput) *Transfer {209op := &Transfer{Inputs: inputs, Outputs: outputs}210m, _ := op.Message(0)211for _, k := range signers {212op.Sigs = append(op.Sigs, k.Sign(m[:]))213}214return op215}216217func single(k *sig.Keypair, amount uint64) wire.TransferOutput {218return wire.TransferOutput{Amount: amount, Owners: []Address{addrOf(k)}, Threshold: 1}219}220221// ---------------------------------------------------------------------222223func TestThreePeriods(t *testing.T) {224const n = 9 // odd: one person has no partner each period225s := newSim(t, n)226P := s.people227sp := params.SlotsPerPeriod228229// ---- Period 0 -------------------------------------------------------230231// The first block, at slot 1 on top of the genesis state: person 0232// spends the genesis election token on a vote for period 1.233valA, valB := keyFor("val-A"), keyFor("val-B")234b1 := s.block(1, s.vote(P[0], valA, h("nonce-genesis")))235if b1.Header.Number != 1 || b1.Header.Prev != (Hash{}) || s.st.Number != 1 || s.st.Slot != 1 {236t.Fatal("first block")237}238if s.st.PrevHash != b1.Header.Hash() || s.st.Election[1].Count() != 1 || s.st.Token.Count() != 0 {239t.Fatal("tip after first block")240}241if b1.Header.Time != genesisTimeOf(s)+60 || s.st.Time != b1.Header.Time {242t.Fatal("time of the first block")243}244// A Verify signed for another period fails its signature.245s.mustFail(ErrSignature, 99, s.verify(P[3], 99+sp))246247// Pairs under seed 0. Everyone attests, including the odd one out.248var paired []*sig.Keypair249var odd *sig.Keypair250var ops []Op251for _, k := range P {252if _, ok := s.st.Partner(addrOf(k)); ok {253paired = append(paired, k)254} else {255odd = k256}257ops = append(ops, s.verify(k, 100))258}259if len(paired) != n-1 || odd == nil {260t.Fatalf("%d paired of %d", len(paired), n)261}262// Partner symmetry through the state.263for _, k := range paired {264p, _ := s.st.Partner(addrOf(k))265q, _ := s.st.Partner(p)266if q != addrOf(k) {267t.Fatal("partner not symmetric")268}269}270s.block(100, ops...)271for _, k := range P {272e, _ := s.st.RegEntry(0, addrOf(k))273if !e.Verify || e.Verified {274t.Fatal("verify flag")275}276}277278// A flag cannot be set twice.279s.mustFail(ErrAlreadySet, 101, s.verify(P[0], 101))280281// Verified: pays tokens to everyone with a partner; the odd one out fails.282ops = ops[:0]283for _, k := range paired {284ops = append(ops, s.verified(k, 200))285}286s.block(200, ops...)287s.mustFail(ErrNoPartner, 201, s.verified(odd, 201))288s.mustFail(ErrAlreadySet, 201, s.verified(P[0], 201))289if s.st.Rng[1].Count() != uint64(n-1) || s.st.Token.Count() != uint64(3*(n-1)) {290t.Fatalf("after Verified: rng1=%d tokens=%d", s.st.Rng[1].Count(), s.st.Token.Count())291}292for _, k := range paired {293for i, id := range []uint8{params.TokenElection, params.TokenRegistration, params.TokenInvite} {294e, ok := s.st.TokenEntry(s.tokens[addrOf(k)][i])295if !ok || e.TokenID != id || e.Owner != addrOf(k) || e.MixCount != 0 {296t.Fatalf("minted token %d wrong", i)297}298}299}300301// Votes for period 1: even voters pick val-A, odd pick val-B. Person 0302// already voted in block 1, so a second vote from the minted token is303// rejected: the address is occupied.304ops = ops[:0]305nVotes0 := 1306for i, k := range paired {307if k == P[0] {308s.mustFail(ErrOccupied, 300, s.vote(k, valB, h("second")))309continue310}311v := valA312if i%2 == 1 {313v = valB314}315ops = append(ops, s.vote(k, v, h(fmt.Sprintf("nonce-%d", i))))316nVotes0++317}318// A vote with a registration token.319badVote := &Vote{TokenUTXO: s.tokens[addrOf(paired[0])][1]}320m, _ := badVote.Message(0)321badVote.Sig = paired[0].Sign(m[:])322s.mustFail(ErrTokenType, 300, badVote)323s.block(300, ops...)324if s.st.Election[1].Count() != uint64(nVotes0) {325t.Fatalf("votes: %d, want %d", s.st.Election[1].Count(), nVotes0)326}327// Voting twice with the (spent) token: input gone.328s.mustFail(ErrMissingInput, 301, s.vote(paired[1], valA, h("x")))329330// Registration. One person (anon) mixes the registration token to a331// fresh key first — the anonymization the spec describes; the others332// register directly. Three people give their invite tokens to the odd333// one out.334anon := paired[0]335fresh1 := keyFor("fresh-1")336mixOp := s.mix(params.TokenRegistration, []Hash{s.tokens[addrOf(anon)][1]}, []*sig.Keypair{anon},337[]wire.MixOutput{{MixCount: 1, Owner: addrOf(fresh1)}})338mixTx, _ := TxHash(mixOp, 0)339fresh1Reg := wire.UTXOID(mixTx, 0)340341// Wrong mix count sum is rejected; so is a count over the cap.342badMix := s.mix(params.TokenRegistration, []Hash{s.tokens[addrOf(paired[1])][1]}, []*sig.Keypair{paired[1]},343[]wire.MixOutput{{MixCount: 2, Owner: addrOf(paired[1])}})344s.mustFail(ErrMixCount, 400, badMix)345overCap := s.mix(params.TokenRegistration, []Hash{s.tokens[addrOf(paired[1])][1]}, []*sig.Keypair{paired[1]},346[]wire.MixOutput{{MixCount: params.MaxMixes + 1, Owner: addrOf(paired[1])}})347s.mustFail(ErrMixCap, 400, overCap)348wrongSigner := s.mix(params.TokenRegistration, []Hash{s.tokens[addrOf(paired[1])][1]}, []*sig.Keypair{paired[2]},349[]wire.MixOutput{{MixCount: 1, Owner: addrOf(paired[1])}})350s.mustFail(ErrSignature, 400, wrongSigner)351352givers := paired[len(paired)-3:]353var inviteIn []Hash354for _, g := range givers {355inviteIn = append(inviteIn, s.tokens[addrOf(g)][2])356}357giveOp := s.mix(params.TokenInvite, inviteIn, givers, []wire.MixOutput{358{MixCount: 1, Owner: addrOf(odd)}, {MixCount: 1, Owner: addrOf(odd)}, {MixCount: 1, Owner: addrOf(odd)}})359giveTx, _ := TxHash(giveOp, 0)360s.block(400, mixOp, giveOp)361if e, ok := s.st.TokenEntry(fresh1Reg); !ok || e.Owner != addrOf(fresh1) || e.MixCount != 1 || e.TokenID != params.TokenRegistration {362t.Fatal("mixed registration token")363}364365ops = ops[:0]366ops = append(ops, s.register(fresh1, fresh1Reg))367for _, k := range paired {368if k == anon {369continue370}371ops = append(ops, s.register(k, s.tokens[addrOf(k)][1]))372}373invites := []Hash{wire.UTXOID(giveTx, 0), wire.UTXOID(giveTx, 1), wire.UTXOID(giveTx, 2)}374ops = append(ops, s.register(odd, invites...))375// Two invites are not enough; an invite plus a registration token is376// the wrong type.377s.mustFail(ErrTokenCount, 500, s.register(odd, invites[:2]...))378s.mustFail(ErrTokenType, 500, s.register(odd, invites[0]))379s.block(500, ops...)380if s.st.Reg[1].Count() != uint64(n) {381t.Fatalf("registry for period 1: %d", s.st.Reg[1].Count())382}383if !s.st.Reg[1].Has(addrOf(fresh1)) || s.st.Reg[1].Has(addrOf(anon)) {384t.Fatal("anon should be registered under the fresh key")385}386// Registering again: entry occupied — but the token is gone first.387s.mustFail(ErrMissingInput, 501, s.register(odd, invites...))388389// RNG commits for period 1. A commit signed for the wrong period fails.390replay := &Commit{RngAddr: addrOf(paired[0]), Commit: h("c")}391m, _ = replay.Message(1)392replay.Sig = paired[0].Sign(m[:])393s.mustFail(ErrSignature, 599, replay)394ops = ops[:0]395for i, k := range paired {396ops = append(ops, s.commit(k, 600, h(fmt.Sprintf("preimage-0-%d", i))))397}398s.mustFail(ErrCommitZero, 600, func() Op {399op := &Commit{RngAddr: addrOf(paired[0])}400m, _ := op.Message(0)401op.Sig = paired[0].Sign(m[:])402return op403}())404s.block(600, ops...)405s.mustFail(ErrCommitSet, 601, s.commitOp(paired[0], 601, h("again")))406s.mustFail(ErrNotRegistered, 601, s.commitOp(odd, 601, h("odd")))407408// Phase 1: Verify is over, Verified still possible (but everyone is done).409s.mustFail(ErrPhase, params.SlotsPerPhase+5, s.verify(P[0], params.SlotsPerPhase+5))410// Phase 2: nothing to reveal yet — rng_trie[0] is empty at genesis.411s.mustFail(ErrNotRegistered, 2*params.SlotsPerPhase+5, s.reveal(paired[0], 2*params.SlotsPerPhase+5, s.preimages[0][addrOf(paired[0])]))412s.mustFail(ErrPhase, 2*params.SlotsPerPhase+5, s.commitOp(paired[0], 2*params.SlotsPerPhase+5, h("late")))413s.block(2*params.SlotsPerPhase + 5)414415// ---- Period 1 -------------------------------------------------------416417// Seed 0 (nobody revealed). Validators are now the eight period-0 voters.418p1 := sp + 10419pre := s.st.AtSlot(p1)420if pre.Period != 1 || pre.Seed != (Hash{}) || pre.Election[0].Count() != uint64(nVotes0) || pre.Reg[0].Count() != uint64(n) || pre.Rng[0].Count() != uint64(n-1) {421t.Fatalf("period 1 pre-state: period=%d elections=%d reg=%d rng=%d", pre.Period, pre.Election[0].Count(), pre.Reg[0].Count(), pre.Rng[0].Count())422}423if pre.Token.Count() != 0 || pre.Reg[1].Count() != 0 || pre.Rng[1].Count() != 0 || pre.Election[1].Count() != 0 {424t.Fatal("rotation should clear [1] and the tokens")425}426// Different slots are owned by different voters.427owners := map[Address]bool{}428for slot := p1; slot < p1+100; slot++ {429v, _, _ := pre.Validator(slot)430owners[v] = true431}432if len(owners) < 4 {433t.Fatalf("only %d voters own the first 100 slots", len(owners))434}435s.block(p1)436if s.st.Period != 1 {437t.Fatal("tip period")438}439440// New registry: paired minus anon plus fresh1 plus odd. New pairs.441people1 := []*sig.Keypair{fresh1, odd}442for _, k := range paired {443if k != anon {444people1 = append(people1, k)445}446}447ops = ops[:0]448var paired1 []*sig.Keypair449for _, k := range people1 {450ops = append(ops, s.verify(k, p1+50))451if _, ok := s.st.AtSlot(p1).Partner(addrOf(k)); ok {452paired1 = append(paired1, k)453}454}455s.block(p1+50, ops...)456ops = ops[:0]457for i, k := range paired1 {458ops = append(ops, s.verified(k, p1+60))459ops = append(ops, s.commitOp(k, p1+60, h(fmt.Sprintf("preimage-1-%d", i))))460}461// Commit before Verified in the same block fails: it validates against462// pre-block state, where the RNG entry does not exist yet.463s.mustFail(ErrNotRegistered, p1+60, ops...)464ops = ops[:0]465for _, k := range paired1 {466ops = append(ops, s.verified(k, p1+60))467}468s.block(p1+60, ops...)469ops = ops[:0]470for i, k := range paired1 {471ops = append(ops, s.commit(k, p1+61, h(fmt.Sprintf("preimage-1-%d", i))))472}473s.block(p1+61, ops...)474// Everyone votes again, and registers, so the chain continues.475ops = ops[:0]476for i, k := range paired1 {477v := valA478if i%2 == 0 {479v = valB480}481ops = append(ops, s.vote(k, v, h(fmt.Sprintf("nonce-1-%d", i))))482ops = append(ops, s.register(k, s.tokens[addrOf(k)][1]))483}484s.block(p1+70, ops...)485486// Phase 2 of period 1: the eight period-0 commits are revealed; the487// UBI is paid; the tally fills.488r1 := sp + 2*params.SlotsPerPhase + 10489ops = ops[:0]490for _, k := range paired {491ops = append(ops, s.reveal(k, r1, s.preimages[0][addrOf(k)]))492}493s.mustFail(ErrPreimage, r1, s.reveal(paired[0], r1, h("wrong")))494// Revealing a period-1 commit (still in rng_trie[1]) is not possible.495s.mustFail(ErrNotRegistered, r1, s.reveal(odd, r1, h("preimage-1-0")))496s.block(r1, ops...)497s.mustFail(ErrNoCommit, r1+1, s.reveal(paired[0], r1+1, s.preimages[0][addrOf(paired[0])]))498if s.st.Coin.Count() != uint64(n-1) || s.st.Tally.Total() != uint64(n-1) {499t.Fatalf("after reveals: coins=%d votes=%d", s.st.Coin.Count(), s.st.Tally.Total())500}501ubi := map[Address]Hash{}502for _, k := range paired {503txh, _ := TxHash(s.reveal(k, r1, s.preimages[0][addrOf(k)]), s.per(r1))504id := wire.UTXOID(txh, 0)505e, ok := s.st.CoinEntry(id)506if !ok || e.Amount != params.UBIPerPeriod || e.Slot != uint32(r1) || len(e.Owners) != 1 || e.Owners[0] != addrOf(k) {507t.Fatal("UBI entry")508}509ubi[addrOf(k)] = id510}511// Reveal is a phase-2 operation.512s.mustFail(ErrPhase, sp+3*params.SlotsPerPhase+1, s.reveal(paired[1], sp+3*params.SlotsPerPhase+1, h("x")))513514// ---- Period 2 -------------------------------------------------------515516winner, _ := s.st.Tally.Winner()517p2 := 2*sp + 5518pre = s.st.AtSlot(p2)519if pre.Seed != rng.Seed(winner) {520t.Fatal("seed should be the tally winner")521}522t.Logf("period 2 seed number: %d (from %d reveals)", winner, s.st.Tally.Total())523524// Transfers. paired[0] pays paired[1] 1 000 000, keeping the rest; the525// remainder after outputs is the fee, split between validator and voter.526a, b := paired[0], paired[1]527spend, _ := s.st.CoinEntry(ubi[addrOf(a)])528tt := coin.Elapsed(spend.Slot, p2)529spendable := coin.Spendable(spend.Amount, tt)530tx := s.transfer([]Hash{ubi[addrOf(a)]}, []*sig.Keypair{a}, []wire.TransferOutput{single(b, 1_000_000), single(a, spendable-1_000_000-500)})531blk := s.block(p2, tx)532txh, _ := TxHash(tx, 0)533if _, ok := s.st.CoinEntry(ubi[addrOf(a)]); ok {534t.Fatal("input not spent")535}536if e, ok := s.st.CoinEntry(wire.UTXOID(txh, 0)); !ok || e.Amount != 1_000_000 || e.Owners[0] != addrOf(b) || e.Slot != uint32(p2) {537t.Fatal("output 0")538}539fee := coin.Effective(spend.Amount, tt) - (spendable - 500)540valFee, ok := s.st.CoinEntry(wire.FeeUTXOID(blk.Header.Number, 0))541voterFee, ok2 := s.st.CoinEntry(wire.FeeUTXOID(blk.Header.Number, 1))542if !ok || !ok2 || valFee.Amount+voterFee.Amount != fee || valFee.Amount < voterFee.Amount || valFee.Amount-voterFee.Amount > 1 {543t.Fatalf("fee split: %v %v of %d", valFee, voterFee, fee)544}545if valFee.Owners[0] != blk.Header.Validator {546t.Fatal("validator fee owner")547}548voter, _, _ := s.st.Validator(p2)549if voterFee.Owners[0] != voter {550t.Fatal("voter fee owner")551}552553// Overspend, zero output, double spend within a block, bad signer.554s.mustFail(ErrInsufficient, p2+1, s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b}, []wire.TransferOutput{single(a, params.UBIPerPeriod)}))555s.mustFail(ErrAmount, p2+1, s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b}, []wire.TransferOutput{single(a, 0)}))556s.mustFail(ErrSignature, p2+1, s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{a}, []wire.TransferOutput{single(a, 10)}))557s.mustFail(ErrSignature, p2+1, s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b, b}, []wire.TransferOutput{single(a, 10)}))558s.mustFail(ErrConflict, p2+1,559s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b}, []wire.TransferOutput{single(a, 10)}),560s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b}, []wire.TransferOutput{single(a, 11)}))561s.mustFail(ErrDuplicate, p2+1, s.transfer([]Hash{ubi[addrOf(b)], ubi[addrOf(b)]}, []*sig.Keypair{b, b}, []wire.TransferOutput{single(a, 10)}))562// The same transaction twice in one block.563dup := s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b}, []wire.TransferOutput{single(a, 10)})564s.mustFail(ErrDuplicateTx, p2+1, dup, dup)565566// Multisig: 2-of-3 output, spent with signatures in owner order.567c := paired[2]568msig := wire.TransferOutput{Amount: 500_000, Owners: []Address{addrOf(a), addrOf(b), addrOf(c)}, Threshold: 2}569tx2 := s.transfer([]Hash{ubi[addrOf(b)]}, []*sig.Keypair{b}, []wire.TransferOutput{msig})570s.block(p2+1, tx2)571tx2h, _ := TxHash(tx2, 0)572msigID := wire.UTXOID(tx2h, 0)573// One signature is too few; two out of order fail (b then a); a and c work.574s.mustFail(ErrSignature, p2+2, s.transfer([]Hash{msigID}, []*sig.Keypair{a}, []wire.TransferOutput{single(c, 10)}))575s.mustFail(ErrSignature, p2+2, s.transfer([]Hash{msigID}, []*sig.Keypair{b, a}, []wire.TransferOutput{single(c, 10)}))576s.mustFail(ErrSignature, p2+2, s.transfer([]Hash{msigID}, []*sig.Keypair{a, b, c}, []wire.TransferOutput{single(c, 10)}))577dustTx := s.transfer([]Hash{msigID}, []*sig.Keypair{a, c}, []wire.TransferOutput{single(c, 400_000), single(c, 10)})578s.block(p2+2, dustTx)579dustH, _ := TxHash(dustTx, 0)580dustID := wire.UTXOID(dustH, 1)581582// Prune. Ten base units are gone long before rent reaches ten: each583// multiplication in the decay chain floors, so a tiny amount loses a584// unit per set bit of t, and at t = 1023 (ten bits) it is worth zero,585// which rent 0 meets. Find the first prunable slot from the coin rules586// rather than assuming one.587var tMin uint64588for tMin = 1; !coin.Prunable(10, tMin); tMin++ {589}590t.Logf("10 base units prunable from t = %d", tMin)591s.mustFail(ErrNotPrunable, p2+2+1, &Prune{UTXO: dustID})592s.mustFail(ErrNotPrunable, p2+2+tMin-1, &Prune{UTXO: dustID})593s.block(p2+2+tMin, &Prune{UTXO: dustID})594if s.st.Coin.Has(dustID) {595t.Fatal("dust not pruned")596}597s.mustFail(ErrMissingInput, p2+2+tMin+1, &Prune{UTXO: dustID})598// The pruned value went to the block's fees.599if _, ok := s.st.CoinEntry(wire.FeeUTXOID(s.st.Number, 0)); ok {600t.Fatal("pruning a zero-value entry should pay no fee")601}602603// Period 2's validators and registry are period 1's votes and604// registrations; nobody voted in period 2, so period 3 has no605// validators and the chain ends there.606if s.st.Election[0].Count() != uint64(len(paired1)) || s.st.Reg[0].Count() != uint64(len(paired1)) || s.st.Rng[0].Count() != uint64(len(paired1)) {607t.Fatalf("period 2: elections=%d reg=%d rng=%d, want %d", s.st.Election[0].Count(), s.st.Reg[0].Count(), s.st.Rng[0].Count(), len(paired1))608}609p3 := 3*sp + 1610if pre := s.st.AtSlot(p3); pre.Period != 3 || pre.Election[0].Count() != 0 {611t.Fatal("period 3 should have no validators")612}613if _, _, err := s.tryBlock(p3); !errors.Is(err, ErrNoValidator) {614t.Fatalf("period 3: %v", err)615}616t.Logf("tip: block %d, slot %d, %d coins, %d tokens", s.st.Number, s.st.Slot, s.st.Coin.Count(), s.st.Token.Count())617}618619// ---------------------------------------------------------------------620// Header rules621622func TestHeaderRules(t *testing.T) {623s := newSim(t, 4)624good := s.block(5)625base := s.st626627// Build a valid next block, then break it in every way.628voter, _, _ := base.Validator(6)629v := s.validators[0][voter]630fresh := func() *Block {631b, _, err := base.Assemble(6, []Op{s.verify(s.people[0], 6)}, v.key, v.nonce)632if err != nil {633t.Fatal(err)634}635return b636}637expect := func(want error, mutate func(b *Block)) {638t.Helper()639b := fresh()640mutate(b)641if _, err := base.Apply(b); !errors.Is(err, want) {642t.Fatalf("got %v, want %v", err, want)643}644}645resign := func(b *Block) {646sh := b.Header.SigningHash()647b.Header.Sig = v.key.Sign(sh[:])648}649if _, err := base.Apply(fresh()); err != nil {650t.Fatal(err)651}652// time is exactly the slot's start, one minute after block 5's.653if fresh().Header.Time != good.Header.Time+60 {654t.Fatal("time")655}656expect(ErrNumber, func(b *Block) { b.Header.Number = 3; resign(b) })657expect(ErrSlot, func(b *Block) { b.Header.Slot = 5; resign(b) })658expect(ErrTime, func(b *Block) { b.Header.Time += 60; resign(b) })659expect(ErrTime, func(b *Block) { b.Header.Time += 1; resign(b) })660expect(ErrTime, func(b *Block) { b.Header.Time -= 1; resign(b) })661expect(ErrPrev, func(b *Block) { b.Header.Prev = h("x"); resign(b) })662expect(ErrValidator, func(b *Block) { b.Header.Nonce = h("x"); resign(b) })663expect(ErrValidator, func(b *Block) {664other := keyFor("other")665b.Header.Validator = addrOf(other)666sh := b.Header.SigningHash()667b.Header.Sig = other.Sign(sh[:])668})669expect(ErrRoots, func(b *Block) { b.Header.Coin = h("x"); resign(b) })670expect(ErrRoots, func(b *Block) { b.Header.TxTrie = h("x"); resign(b) })671expect(ErrHeaderSig, func(b *Block) { b.Header.Sig[3] ^= 1 })672expect(ErrHeaderSig, func(b *Block) {673other := keyFor("other")674sh := b.Header.SigningHash()675b.Header.Sig = other.Sign(sh[:])676})677// Ops missing from the block but in the roots, and vice versa.678expect(ErrRoots, func(b *Block) { b.Ops = nil })679expect(ErrRoots, func(b *Block) { b.Ops = append(b.Ops, s.verify(s.people[1], 6)) })680expect(ErrMissingInput, func(b *Block) { b.Ops = append(b.Ops, &Prune{UTXO: h("nothing")}) })681682// Applying the previous block again on the new tip.683if _, err := base.Apply(good); !errors.Is(err, ErrNumber) {684t.Fatalf("replayed block: %v", err)685}686687// Header wire round trip and hash stability.688b := fresh()689dec, err := DecodeHeader(b.Header.Bytes())690if err != nil || *dec != b.Header || dec.Hash() != b.Header.Hash() {691t.Fatal("header round trip")692}693if _, err := DecodeHeader(b.Header.Bytes()[:HeaderSize-1]); err == nil {694t.Fatal("short header accepted")695}696if len(b.Header.Bytes()) != HeaderSize {697t.Fatal("header size")698}699}700701func TestNoValidatorsEndsChain(t *testing.T) {702// Nobody votes in period 0, so period 1 has no validators.703s := newSim(t, 4)704s.block(7)705if _, _, err := s.tryBlock(params.SlotsPerPeriod + 1); !errors.Is(err, ErrNoValidator) {706t.Fatalf("got %v", err)707}708if _, _, ok := s.st.AtSlot(params.SlotsPerPeriod + 1).Validator(params.SlotsPerPeriod + 1); ok {709t.Fatal("no validator expected")710}711}712713func TestGenesisRules(t *testing.T) {714if _, err := Genesis(1000, nil, nil, nil); !errors.Is(err, ErrGenesis) {715t.Fatal("genesis without votes should fail")716}717a := addrOf(keyFor("a"))718if _, err := Genesis(1000, []Address{a, a}, []GenesisVote{{Voter: a}}, nil); !errors.Is(err, ErrGenesis) {719t.Fatal("duplicate registry entry should fail")720}721st, err := Genesis(1000, []Address{a}, []GenesisVote{{Voter: a}}, nil)722if err != nil || st.Reg[0].Count() != 1 || st.Period != 0 || st.Slot != 0 || st.Number != 0 || st.Time != 1000 {723t.Fatal("genesis")724}725if st.TimeOf(10) != 1600 || st.SlotAt(1600) != 10 || st.SlotAt(1659) != 10 || st.SlotAt(500) != 0 {726t.Fatal("tip clock")727}728if _, ok := st.Partner(a); ok {729t.Fatal("a registry of one has no pairs")730}731// State is immutable across AtSlot.732rot := st.AtSlot(2 * params.SlotsPerPeriod)733if rot.Period != 2 || rot.Election[0].Count() != 0 || st.Election[0].Count() != 1 || st.Period != 0 {734t.Fatal("rotation must not touch the original")735}736}737738func TestOpEncoding(t *testing.T) {739k := keyFor("k")740var sg [64]byte741sg[0] = 7742ops := []Op{743&Transfer{Inputs: []Address{h("i")}, Outputs: []wire.TransferOutput{{Amount: 5, Owners: []Address{addrOf(k), h("o")}, Threshold: 2}}, Sigs: [][64]byte{sg, sg}},744&Mix{TokenID: 2, Inputs: []Address{h("i"), h("j")}, Outputs: []wire.MixOutput{{MixCount: 1, Owner: h("a")}, {MixCount: 3, Owner: h("b")}}, Sigs: [][64]byte{sg, sg}},745&Vote{TokenUTXO: h("t"), Commit: h("c"), Sig: sg},746&Register{TokenUTXOs: []Address{h("1"), h("2"), h("3")}, Sig: sg},747&Verify{RegAddr: h("r"), Sig: sg},748&Verified{RegAddr: h("r"), Sig: sg},749&Commit{RngAddr: h("r"), Commit: h("c"), Sig: sg},750&Reveal{RngAddr: h("r"), Preimage: h("p"), Sig: sg},751&Prune{UTXO: h("u")},752}753for _, op := range ops {754enc := op.Bytes()755if enc[0] != op.Opcode() {756t.Fatal("opcode prefix")757}758dec, err := DecodeOp(enc)759if err != nil {760t.Fatalf("decode 0x%02x: %v", op.Opcode(), err)761}762if string(dec.Bytes()) != string(enc) {763t.Fatalf("round trip 0x%02x", op.Opcode())764}765if _, err := DecodeOp(append(enc, 0)); err == nil {766t.Fatalf("trailing byte accepted for 0x%02x", op.Opcode())767}768if _, err := DecodeOp(enc[:len(enc)-1]); err == nil {769t.Fatalf("truncation accepted for 0x%02x", op.Opcode())770}771m0, _ := op.Message(0)772m1, _ := op.Message(1)773// The tx hash is the message, and signatures are not part of it.774if txh, _ := TxHash(op, 0); txh != m0 {775t.Fatal("tx hash must be the signing message")776}777resigned, _ := DecodeOp(enc)778if sigs := resigned.Signatures(); len(sigs) > 0 {779tampered := resigned.Bytes()780tampered[len(tampered)-1] ^= 0xff // last byte of the last signature781r2, err := DecodeOp(tampered)782if err != nil {783t.Fatal(err)784}785if h2, _ := TxHash(r2, 0); h2 != m0 {786t.Fatal("tx hash must not depend on signatures")787}788}789switch op.(type) {790case *Verify, *Verified, *Commit, *Reveal:791if m0 == m1 {792t.Fatal("period must be part of the message")793}794default:795if m0 != m1 {796t.Fatal("period must not affect the message")797}798}799}800if _, err := DecodeOp([]byte{0x42}); !errors.Is(err, ErrOpcode) {801t.Fatal("unknown opcode")802}803if _, err := DecodeOp(nil); err == nil {804t.Fatal("empty")805}806blk := &Block{Ops: ops}807dec, err := DecodeBlock(blk.Bytes())808if err != nil || len(dec.Ops) != len(ops) || string(dec.Bytes()) != string(blk.Bytes()) {809t.Fatalf("block round trip: %v", err)810}811}812813func TestForkChoice(t *testing.T) {814a, b := &State{Number: 5, Slot: 50}, &State{Number: 5, Slot: 52}815if !Better(a, b) || Better(b, a) {816t.Fatal("fewer skipped slots wins at equal number")817}818c := &State{Number: 6, Slot: 90}819if !Better(c, a) || Better(a, c) {820t.Fatal("higher number wins")821}822if Better(a, a) {823t.Fatal("a tip is not better than itself")824}825}826827// BenchmarkApplyTransfers measures validating a block of single-signature828// transfers; the cost is dominated by Ed25519 verification.829func BenchmarkApplyTransfers(b *testing.B) {830const nTx = 1000831s := newSim(&testing.T{}, 4)832// Seed the coin trie directly with nTx UTXOs owned by distinct keys.833st := s.st834keys := make([]*sig.Keypair, nTx)835ids := make([]Hash, nTx)836for i := range keys {837keys[i] = keyFor(fmt.Sprintf("bench-%d", i))838ids[i] = h(fmt.Sprintf("utxo-%d", i))839e := wire.CoinEntry{Owners: []Address{addrOf(keys[i])}, Threshold: 1, Amount: params.TOKEN, Slot: 0}840st.Coin, _ = st.Coin.Insert(ids[i], e.Bytes())841}842ops := make([]Op, nTx)843for i := range ops {844ops[i] = s.transfer([]Hash{ids[i]}, []*sig.Keypair{keys[i]}, []wire.TransferOutput{single(keys[(i+1)%nTx], params.TOKEN/2)})845}846voter, _, _ := st.Validator(1)847v := s.validators[0][voter]848blk, _, err := st.Assemble(1, ops, v.key, v.nonce)849if err != nil {850b.Fatal(err)851}852b.ResetTimer()853for i := 0; i < b.N; i++ {854if _, err := st.Apply(blk); err != nil {855b.Fatal(err)856}857}858b.ReportMetric(float64(nTx)*float64(b.N)/b.Elapsed().Seconds(), "tx/s")859}