package neutrino import ( "os" "path/filepath" "testing" "git.smesh.lol/nostrino/pkg/btcwire" "git.smesh.lol/nostrino/pkg/chain" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) func mine(t *testing.T, prev chainhash.Hash, tag byte) btcwire.BlockHeader { t.Helper() h := btcwire.BlockHeader{ Version: 1, PrevBlock: prev, MerkleRoot: chainhash.DoubleHashH([]byte{tag}), Timestamp: 1296688602 + uint32(tag), Bits: 0x207fffff, } for nonce := uint32(0); nonce < 1_000_000; nonce++ { h.Nonce = nonce if h.CheckProofOfWork() { return h } } t.Fatalf("could not mine header %d", tag) return btcwire.BlockHeader{} } func TestMemoryAnchor(t *testing.T) { genesis := chain.Regtest.GenesisHeader store := NewMemory() anchor := Anchor{Height: 0, BlockHash: chain.Regtest.GenesisHash, FilterHeight: -1} if err := store.SetAnchor(anchor); err != nil { t.Fatal(err) } if _, ok := store.Header(0); ok { t.Fatal("anchor should not fabricate a header") } if tip, hash, ok := store.HeaderTip(); !ok || tip != 0 || hash != chain.Regtest.GenesisHash { t.Fatalf("tip %d %s ok=%v", tip, hash, ok) } if err := store.SetAnchor(anchor); err == nil { t.Fatal("second anchor accepted") } if err := store.PutHeaders(0, []btcwire.BlockHeader{genesis}); err != nil { t.Fatalf("genesis: %v", err) } next := mine(t, genesis.BlockHash(), 1) if err := store.PutHeaders(1, []btcwire.BlockHeader{next}); err != nil { t.Fatalf("header 1: %v", err) } if err := store.PutHeaders(5, []btcwire.BlockHeader{mine(t, next.BlockHash(), 2)}); err == nil { t.Fatal("non-contiguous header batch accepted") } bad := mine(t, chainhash.Hash{}, 3) if err := store.PutHeaders(2, []btcwire.BlockHeader{bad}); err == nil { t.Fatal("unlinked header accepted") } } func TestMemoryAnchorFilterChain(t *testing.T) { store := NewMemory() fh := chainhash.DoubleHashH([]byte("checkpoint filter header")) anchor := Anchor{ Height: 10, BlockHash: chainhash.DoubleHashH([]byte("checkpoint block")), FilterHeight: 10, FilterHeader: fh, } if err := store.SetAnchor(anchor); err != nil { t.Fatal(err) } if tip, got, ok := store.FilterHeaderTip(); !ok || tip != 10 || got != fh { t.Fatalf("filter tip %d %s ok=%v", tip, got, ok) } // A batch starting at 10 would repeat the anchor; the only valid start is 11. fhash := chainhash.DoubleHashH([]byte("filter 11")) newHeader := btcwire.FilterHeader(fhash, fh) if err := store.PutFilterHeaders(10, []chainhash.Hash{fhash}, []chainhash.Hash{newHeader}); err == nil { t.Fatal("filter header batch at the anchor accepted") } if err := store.PutFilterHeaders(11, []chainhash.Hash{fhash}, []chainhash.Hash{newHeader}); err != nil { t.Fatalf("filter header 11: %v", err) } if tip, got, ok := store.FilterHeaderTip(); !ok || tip != 11 || got != newHeader { t.Fatalf("filter tip after put %d %s ok=%v", tip, got, ok) } // A filter hash that does not fold into the stored header must be rejected. wrong := chainhash.DoubleHashH([]byte("wrong")) if err := store.PutFilterHeaders(12, []chainhash.Hash{wrong}, []chainhash.Hash{chainhash.DoubleHashH([]byte("x"))}); err == nil { t.Fatal("inconsistent filter header accepted") } } func TestFileStoreAnchorRoundTrip(t *testing.T) { dir := t.TempDir() store, err := OpenFileStore(dir) if err != nil { t.Fatal(err) } anchor := Anchor{ Height: 100, BlockHash: chainhash.DoubleHashH([]byte("anchor block")), FilterHeight: 100, FilterHeader: chainhash.DoubleHashH([]byte("anchor filter")), } if err := store.SetAnchor(anchor); err != nil { t.Fatal(err) } if err := store.Close(); err != nil { t.Fatal(err) } reopened, err := OpenFileStore(dir) if err != nil { t.Fatal(err) } defer reopened.Close() got, ok := reopened.Anchor() if !ok || got != anchor { t.Fatalf("anchor %#v ok=%v, want %#v", got, ok, anchor) } if tip, h, ok := reopened.FilterHeaderTip(); !ok || tip != 100 || h != anchor.FilterHeader { t.Fatalf("filter tip %d %s ok=%v", tip, h, ok) } if tip, h, ok := reopened.HeaderTip(); !ok || tip != 100 || h != anchor.BlockHash { t.Fatalf("header tip %d %s ok=%v", tip, h, ok) } } func TestFileStoreTruncatesPartialTail(t *testing.T) { dir := t.TempDir() store, err := OpenFileStore(dir) if err != nil { t.Fatal(err) } genesis := chain.Regtest.GenesisHeader if err := store.SetAnchor(Anchor{Height: 0, BlockHash: chain.Regtest.GenesisHash, FilterHeight: -1}); err != nil { t.Fatal(err) } if err := store.PutHeaders(0, []btcwire.BlockHeader{genesis}); err != nil { t.Fatal(err) } if err := store.Close(); err != nil { t.Fatal(err) } path := filepath.Join(dir, "headers.bin") f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0o600) if err != nil { t.Fatal(err) } if _, err := f.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); err != nil { t.Fatal(err) } f.Close() reopened, err := OpenFileStore(dir) if err != nil { t.Fatalf("reopen after partial tail: %v", err) } next := mine(t, genesis.BlockHash(), 9) if err := reopened.PutHeaders(1, []btcwire.BlockHeader{next}); err != nil { t.Fatalf("append after truncation: %v", err) } reopened.Close() again, err := OpenFileStore(dir) if err != nil { t.Fatal(err) } defer again.Close() if hdr, ok := again.Header(1); !ok || hdr.BlockHash() != next.BlockHash() { t.Fatal("header 1 did not survive the reopen") } }