utxo_test.go raw
1 package utxo
2
3 import (
4 "errors"
5 "sync"
6 "testing"
7
8 "git.smesh.lol/tinywallet/pkg/store"
9 )
10
11 // memPersistence keeps the store's durable side in memory.
12 type memPersistence struct {
13 mu sync.Mutex
14 items map[string][]byte
15 }
16
17 func (m *memPersistence) Load() (map[string][]byte, error) {
18 m.mu.Lock()
19 defer m.mu.Unlock()
20
21 out := make(map[string][]byte, len(m.items))
22
23 for key, value := range m.items {
24 out[key] = append([]byte{}, value...)
25 }
26
27 return out, nil
28 }
29
30 func (m *memPersistence) Write(batch []store.Op) error {
31 m.mu.Lock()
32 defer m.mu.Unlock()
33
34 for _, op := range batch {
35 if op.Delete {
36 delete(m.items, op.Key)
37
38 continue
39 }
40
41 m.items[op.Key] = append([]byte{}, op.Value...)
42 }
43
44 return nil
45 }
46
47 // open builds a table over a persisted cache.
48 func open(t *testing.T, persist store.Persistence) (*Table, *store.Cache) {
49 t.Helper()
50
51 cache, err := store.OpenManual(persist)
52
53 if err != nil {
54 t.Fatal(err)
55 }
56
57 table, err := Open(cache)
58
59 if err != nil {
60 t.Fatal(err)
61 }
62
63 return table, cache
64 }
65
66 // coin builds a coin with a recognisable id.
67 func coin(id byte, vout uint32, value uint64, height uint32) Coin {
68 var txid [32]byte
69 txid[0] = id
70
71 return Coin{TxID: txid, Vout: vout, Value: value, Height: height}
72 }
73
74 // TestAddAndList: a coin survives, with its value and height.
75 func TestAddAndList(t *testing.T) {
76 table, _ := open(t, &memPersistence{items: map[string][]byte{}})
77
78 if err := table.Add(coin(1, 0, 50000, 10)); err != nil {
79 t.Fatal(err)
80 }
81
82 if err := table.Add(coin(2, 3, 70000, 12)); err != nil {
83 t.Fatal(err)
84 }
85
86 coins, err := table.Unspent()
87
88 if err != nil {
89 t.Fatal(err)
90 }
91
92 if len(coins) != 2 {
93 t.Fatalf("%d coins", len(coins))
94 }
95
96 // Newest height first.
97 if coins[0].Height != 12 || coins[1].Height != 10 {
98 t.Fatalf("the order is %d then %d", coins[0].Height, coins[1].Height)
99 }
100
101 if coins[0].Value != 70000 || coins[0].Vout != 3 {
102 t.Fatalf("the coin is %+v", coins[0])
103 }
104 }
105
106 // TestSpend: spending removes exactly one coin, and spending it twice is an error.
107 func TestSpend(t *testing.T) {
108 table, _ := open(t, &memPersistence{items: map[string][]byte{}})
109
110 first := coin(1, 0, 50000, 10)
111 second := coin(2, 0, 50000, 11)
112
113 for _, entry := range []Coin{first, second} {
114 if err := table.Add(entry); err != nil {
115 t.Fatal(err)
116 }
117 }
118
119 if err := table.Spend(first.TxID, first.Vout); err != nil {
120 t.Fatal(err)
121 }
122
123 coins, err := table.Unspent()
124
125 if err != nil {
126 t.Fatal(err)
127 }
128
129 if len(coins) != 1 || coins[0].TxID != second.TxID {
130 t.Fatalf("the remaining coins are %+v", coins)
131 }
132
133 if err := table.Spend(first.TxID, first.Vout); !errors.Is(err, ErrSpent) {
134 t.Fatalf("spending a spent coin: %v", err)
135 }
136 }
137
138 // TestReopenSeesTheCoins: the table is the store, and the store is what survives a reload.
139 func TestReopenSeesTheCoins(t *testing.T) {
140 persist := &memPersistence{items: map[string][]byte{}}
141
142 table, cache := open(t, persist)
143
144 if err := table.Add(coin(7, 1, 12345, 99)); err != nil {
145 t.Fatal(err)
146 }
147
148 if err := cache.Close(); err != nil {
149 t.Fatal(err)
150 }
151
152 reopened, _ := open(t, persist)
153
154 coins, err := reopened.Unspent()
155
156 if err != nil {
157 t.Fatal(err)
158 }
159
160 if len(coins) != 1 || coins[0].Value != 12345 || coins[0].Height != 99 {
161 t.Fatalf("the reopened table holds %+v", coins)
162 }
163 }
164
165 // TestAddingTwiceReplaces: a reorg can move a confirmation, and the newer answer stands.
166 func TestAddingTwiceReplaces(t *testing.T) {
167 table, _ := open(t, &memPersistence{items: map[string][]byte{}})
168
169 if err := table.Add(coin(1, 0, 50000, 10)); err != nil {
170 t.Fatal(err)
171 }
172
173 if err := table.Add(coin(1, 0, 50000, 20)); err != nil {
174 t.Fatal(err)
175 }
176
177 coins, err := table.Unspent()
178
179 if err != nil {
180 t.Fatal(err)
181 }
182
183 if len(coins) != 1 || coins[0].Height != 20 {
184 t.Fatalf("the table holds %+v", coins)
185 }
186 }
187
188 // TestOpenNeedsAStore: nothing to persist into is not a table.
189 func TestOpenNeedsAStore(t *testing.T) {
190 if _, err := Open(nil); err == nil {
191 t.Fatal("a table opened over no store")
192 }
193 }
194