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