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