store_test.go raw
1 package neutrino
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "git.smesh.lol/nostrino/pkg/btcwire"
9 "git.smesh.lol/nostrino/pkg/chain"
10 "git.smesh.lol/nostrino/pkg/crypto/chainhash"
11 )
12
13 func mine(t *testing.T, prev chainhash.Hash, tag byte) btcwire.BlockHeader {
14 t.Helper()
15 h := btcwire.BlockHeader{
16 Version: 1,
17 PrevBlock: prev,
18 MerkleRoot: chainhash.DoubleHashH([]byte{tag}),
19 Timestamp: 1296688602 + uint32(tag),
20 Bits: 0x207fffff,
21 }
22 for nonce := uint32(0); nonce < 1_000_000; nonce++ {
23 h.Nonce = nonce
24 if h.CheckProofOfWork() {
25 return h
26 }
27 }
28 t.Fatalf("could not mine header %d", tag)
29 return btcwire.BlockHeader{}
30 }
31
32 func TestMemoryAnchor(t *testing.T) {
33 genesis := chain.Regtest.GenesisHeader
34 store := NewMemory()
35 anchor := Anchor{Height: 0, BlockHash: chain.Regtest.GenesisHash, FilterHeight: -1}
36 if err := store.SetAnchor(anchor); err != nil {
37 t.Fatal(err)
38 }
39 if _, ok := store.Header(0); ok {
40 t.Fatal("anchor should not fabricate a header")
41 }
42 if tip, hash, ok := store.HeaderTip(); !ok || tip != 0 || hash != chain.Regtest.GenesisHash {
43 t.Fatalf("tip %d %s ok=%v", tip, hash, ok)
44 }
45 if err := store.SetAnchor(anchor); err == nil {
46 t.Fatal("second anchor accepted")
47 }
48 if err := store.PutHeaders(0, []btcwire.BlockHeader{genesis}); err != nil {
49 t.Fatalf("genesis: %v", err)
50 }
51 next := mine(t, genesis.BlockHash(), 1)
52 if err := store.PutHeaders(1, []btcwire.BlockHeader{next}); err != nil {
53 t.Fatalf("header 1: %v", err)
54 }
55 if err := store.PutHeaders(5, []btcwire.BlockHeader{mine(t, next.BlockHash(), 2)}); err == nil {
56 t.Fatal("non-contiguous header batch accepted")
57 }
58 bad := mine(t, chainhash.Hash{}, 3)
59 if err := store.PutHeaders(2, []btcwire.BlockHeader{bad}); err == nil {
60 t.Fatal("unlinked header accepted")
61 }
62 }
63
64 func TestMemoryAnchorFilterChain(t *testing.T) {
65 store := NewMemory()
66 fh := chainhash.DoubleHashH([]byte("checkpoint filter header"))
67 anchor := Anchor{
68 Height: 10,
69 BlockHash: chainhash.DoubleHashH([]byte("checkpoint block")),
70 FilterHeight: 10,
71 FilterHeader: fh,
72 }
73 if err := store.SetAnchor(anchor); err != nil {
74 t.Fatal(err)
75 }
76 if tip, got, ok := store.FilterHeaderTip(); !ok || tip != 10 || got != fh {
77 t.Fatalf("filter tip %d %s ok=%v", tip, got, ok)
78 }
79 // A batch starting at 10 would repeat the anchor; the only valid start is 11.
80 fhash := chainhash.DoubleHashH([]byte("filter 11"))
81 newHeader := btcwire.FilterHeader(fhash, fh)
82 if err := store.PutFilterHeaders(10, []chainhash.Hash{fhash}, []chainhash.Hash{newHeader}); err == nil {
83 t.Fatal("filter header batch at the anchor accepted")
84 }
85 if err := store.PutFilterHeaders(11, []chainhash.Hash{fhash}, []chainhash.Hash{newHeader}); err != nil {
86 t.Fatalf("filter header 11: %v", err)
87 }
88 if tip, got, ok := store.FilterHeaderTip(); !ok || tip != 11 || got != newHeader {
89 t.Fatalf("filter tip after put %d %s ok=%v", tip, got, ok)
90 }
91 // A filter hash that does not fold into the stored header must be rejected.
92 wrong := chainhash.DoubleHashH([]byte("wrong"))
93 if err := store.PutFilterHeaders(12, []chainhash.Hash{wrong}, []chainhash.Hash{chainhash.DoubleHashH([]byte("x"))}); err == nil {
94 t.Fatal("inconsistent filter header accepted")
95 }
96 }
97
98 func TestFileStoreAnchorRoundTrip(t *testing.T) {
99 dir := t.TempDir()
100 store, err := OpenFileStore(dir)
101 if err != nil {
102 t.Fatal(err)
103 }
104 anchor := Anchor{
105 Height: 100,
106 BlockHash: chainhash.DoubleHashH([]byte("anchor block")),
107 FilterHeight: 100,
108 FilterHeader: chainhash.DoubleHashH([]byte("anchor filter")),
109 }
110 if err := store.SetAnchor(anchor); err != nil {
111 t.Fatal(err)
112 }
113 if err := store.Close(); err != nil {
114 t.Fatal(err)
115 }
116 reopened, err := OpenFileStore(dir)
117 if err != nil {
118 t.Fatal(err)
119 }
120 defer reopened.Close()
121 got, ok := reopened.Anchor()
122 if !ok || got != anchor {
123 t.Fatalf("anchor %#v ok=%v, want %#v", got, ok, anchor)
124 }
125 if tip, h, ok := reopened.FilterHeaderTip(); !ok || tip != 100 || h != anchor.FilterHeader {
126 t.Fatalf("filter tip %d %s ok=%v", tip, h, ok)
127 }
128 if tip, h, ok := reopened.HeaderTip(); !ok || tip != 100 || h != anchor.BlockHash {
129 t.Fatalf("header tip %d %s ok=%v", tip, h, ok)
130 }
131 }
132
133 func TestFileStoreTruncatesPartialTail(t *testing.T) {
134 dir := t.TempDir()
135 store, err := OpenFileStore(dir)
136 if err != nil {
137 t.Fatal(err)
138 }
139 genesis := chain.Regtest.GenesisHeader
140 if err := store.SetAnchor(Anchor{Height: 0, BlockHash: chain.Regtest.GenesisHash, FilterHeight: -1}); err != nil {
141 t.Fatal(err)
142 }
143 if err := store.PutHeaders(0, []btcwire.BlockHeader{genesis}); err != nil {
144 t.Fatal(err)
145 }
146 if err := store.Close(); err != nil {
147 t.Fatal(err)
148 }
149 path := filepath.Join(dir, "headers.bin")
150 f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0o600)
151 if err != nil {
152 t.Fatal(err)
153 }
154 if _, err := f.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); err != nil {
155 t.Fatal(err)
156 }
157 f.Close()
158
159 reopened, err := OpenFileStore(dir)
160 if err != nil {
161 t.Fatalf("reopen after partial tail: %v", err)
162 }
163 next := mine(t, genesis.BlockHash(), 9)
164 if err := reopened.PutHeaders(1, []btcwire.BlockHeader{next}); err != nil {
165 t.Fatalf("append after truncation: %v", err)
166 }
167 reopened.Close()
168
169 again, err := OpenFileStore(dir)
170 if err != nil {
171 t.Fatal(err)
172 }
173 defer again.Close()
174 if hdr, ok := again.Header(1); !ok || hdr.BlockHash() != next.BlockHash() {
175 t.Fatal("header 1 did not survive the reopen")
176 }
177 }
178