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