client_test.go raw
1 package neutrino
2
3 import (
4 "context"
5 "errors"
6 "net"
7 "testing"
8
9 "git.smesh.lol/nostrino/pkg/btcwire"
10 "git.smesh.lol/nostrino/pkg/chain"
11 )
12
13 // TestOneFilterHeaderRequestCoversAtMostTheProtocolLimit: a getcfheaders range is capped at
14 // MaxFilterHashesPerMsg blocks by BIP157, and a peer that is asked for more drops the connection
15 // ("peer requested too many cfilters/cfheaders"). A chain taller than the cap must therefore be
16 // walked in chunks, which is what a regtest rig with 24000 blocks exercises.
17 func TestOneFilterHeaderRequestCoversAtMostTheProtocolLimit(t *testing.T) {
18 const perRequest = uint32(btcwire.MaxFilterHashesPerMsg)
19
20 if perRequest != 2000 {
21 t.Fatalf("MaxFilterHashesPerMsg is %d, BIP157 says 2000", perRequest)
22 }
23
24 cases := []struct {
25 name string
26 start, tip uint32
27 want uint32
28 }{
29 {"a single block", 0, 0, 0},
30 {"shorter than a request", 0, 99, 99},
31 {"exactly one request", 0, perRequest - 1, perRequest - 1},
32 {"one block past a request", 0, perRequest, perRequest - 1},
33 {"a later full request", perRequest, 2*perRequest + 5, 2*perRequest - 1},
34 {"the last partial request", 24000, 24102, 24102},
35 }
36
37 for _, tc := range cases {
38 t.Run(tc.name, func(t *testing.T) {
39 got := filterHeaderStop(tc.start, tc.tip)
40
41 if got != tc.want {
42 t.Fatalf("stop for start %d, tip %d = %d, want %d", tc.start, tc.tip, got, tc.want)
43 }
44
45 if covered := got - tc.start + 1; covered > perRequest {
46 t.Fatalf("one request covers %d blocks, the limit is %d", covered, perRequest)
47 }
48 })
49 }
50 }
51
52 // TestBlockNeedsAHeader: a height the store has no header for is a named refusal, because the
53 // client has nothing verified to ask about.
54 func TestBlockNeedsAHeader(t *testing.T) {
55 store := NewMemory()
56 profile, err := chain.ByName("regtest")
57
58 if err != nil {
59 t.Fatal(err)
60 }
61
62 client, err := New(Config{
63 Profile: profile,
64 Store: store,
65 Dial: func(context.Context) (net.Conn, error) { return nil, errors.New("no peer") },
66 })
67
68 if err != nil {
69 t.Fatal(err)
70 }
71
72 if _, err := client.Block(context.Background(), 42); !errors.Is(err, ErrNoHeader) {
73 t.Fatalf("a height with no header: %v", err)
74 }
75 }
76
77 // TestBroadcastNeedsAPeer: there is nowhere to send a transaction without a connection, and an
78 // empty transaction is refused before it becomes a frame.
79 func TestBroadcastNeedsAPeer(t *testing.T) {
80 profile, err := chain.ByName("regtest")
81
82 if err != nil {
83 t.Fatal(err)
84 }
85
86 client, err := New(Config{
87 Profile: profile,
88 Store: NewMemory(),
89 Dial: func(context.Context) (net.Conn, error) { return nil, errors.New("no peer") },
90 })
91
92 if err != nil {
93 t.Fatal(err)
94 }
95
96 if err := client.Broadcast(context.Background(), []byte{1, 2, 3}); err == nil {
97 t.Fatal("a transaction was broadcast with no peer")
98 }
99
100 if err := client.Broadcast(context.Background(), nil); err == nil {
101 t.Fatal("an empty transaction was broadcast")
102 }
103
104 if err := client.Broadcast(context.Background(), make([]byte, MaxTransactionPayload+1)); !errors.Is(err, ErrTransactionTooLarge) {
105 t.Fatalf("an oversized transaction: %v", err)
106 }
107 }
108