package btcwire import ( "bytes" "errors" "io" "reflect" "testing" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) func TestFilterHeader(t *testing.T) { filterHash := testHash(0x11) prevFilterHeader := testHash(0x22) got := FilterHeader(filterHash, prevFilterHeader) // SHA256d(filterHash || prevFilterHeader): the new filter hash comes // first. var concat [2 * chainhash.HashSize]byte copy(concat[:chainhash.HashSize], filterHash[:]) copy(concat[chainhash.HashSize:], prevFilterHeader[:]) want := chainhash.DoubleHashH(concat[:]) if got != want { t.Fatalf("FilterHeader = %s, want %s", got, want) } // The arguments are not interchangeable. swapped := FilterHeader(prevFilterHeader, filterHash) if swapped == got { t.Fatal("FilterHeader is symmetric in its arguments") } // A tag-style order (previous header first) would produce the swapped // value, so verify the concatenation order explicitly. var reversed [2 * chainhash.HashSize]byte copy(reversed[:chainhash.HashSize], prevFilterHeader[:]) copy(reversed[chainhash.HashSize:], filterHash[:]) if want := chainhash.DoubleHashH(reversed[:]); swapped != want { t.Fatalf("swapped FilterHeader = %s, want %s", swapped, want) } } func TestGetCFHeadersRoundTrip(t *testing.T) { want := &GetCFHeaders{ FilterType: FilterTypeBasic, StartHeight: 700000, StopHash: testHash(0x33), } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } if got, wantLen := len(payload), 1+4+chainhash.HashSize; got != wantLen { t.Fatalf("payload length = %d, want %d", got, wantLen) } if payload[0] != FilterTypeBasic { t.Fatalf("filter type byte = %d, want %d", payload[0], FilterTypeBasic) } // Start height, little endian. if !bytes.Equal(payload[1:5], []byte{0x60, 0xae, 0x0a, 0x00}) { t.Fatalf("start height bytes = %x, want 60ae0a00", payload[1:5]) } var got GetCFHeaders if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("decoded = %+v, want %+v", got, *want) } } func TestCFHeadersRoundTrip(t *testing.T) { tests := []struct { name string hashes []chainhash.Hash }{ {"empty", []chainhash.Hash{}}, {"one", []chainhash.Hash{testHash(0x44)}}, {"several", []chainhash.Hash{testHash(0x45), testHash(0x46), testHash(0x47)}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { want := &CFHeaders{ FilterType: FilterTypeBasic, StopHash: testHash(0x48), PrevFilterHeader: testHash(0x49), FilterHashes: tc.hashes, } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } if got, wantLen := len(payload), 1+2*chainhash.HashSize+1+len(tc.hashes)*chainhash.HashSize; got != wantLen { t.Fatalf("payload length = %d, want %d", got, wantLen) } var got CFHeaders if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("decoded = %+v, want %+v", got, *want) } }) } } func TestCFHeadersLimits(t *testing.T) { h := &CFHeaders{FilterHashes: make([]chainhash.Hash, MaxFilterHashesPerMsg+1)} if _, err := h.EncodePayload(); !errors.Is(err, ErrTooManyItems) { t.Fatalf("EncodePayload error = %v, want ErrTooManyItems", err) } // 2001 encoded as 0xfd 0xd1 0x07. payload := []byte{FilterTypeBasic} payload = append(payload, make([]byte, 2*chainhash.HashSize)...) payload = append(payload, 0xfd, byte((MaxFilterHashesPerMsg+1)&0xff), byte((MaxFilterHashesPerMsg+1)>>8)) var got CFHeaders if err := got.DecodePayload(payload); !errors.Is(err, ErrTooManyItems) { t.Fatalf("DecodePayload error = %v, want ErrTooManyItems", err) } } func TestCFHeadersTruncated(t *testing.T) { want := &CFHeaders{ FilterType: FilterTypeBasic, StopHash: testHash(0x50), PrevFilterHeader: testHash(0x51), FilterHashes: []chainhash.Hash{testHash(0x52), testHash(0x53)}, } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } for n := 0; n < len(payload); n++ { var got CFHeaders if err := got.DecodePayload(payload[:n]); err == nil { t.Fatalf("decoding %d of %d bytes succeeded", n, len(payload)) } } } func TestGetCFiltersRoundTrip(t *testing.T) { want := &GetCFilters{ FilterType: FilterTypeBasic, StartHeight: 1, StopHash: testHash(0x54), } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } if got, wantLen := len(payload), 1+4+chainhash.HashSize; got != wantLen { t.Fatalf("payload length = %d, want %d", got, wantLen) } var got GetCFilters if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("decoded = %+v, want %+v", got, *want) } } func TestCFilterRoundTrip(t *testing.T) { tests := []struct { name string filter []byte }{ {"empty filter", []byte{}}, {"header only", []byte{0x00}}, {"basic filter", append([]byte{0x01, 0x02}, bytes.Repeat([]byte{0x99}, 100)...)}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { want := &CFilter{ FilterType: FilterTypeBasic, BlockHash: testHash(0x55), Filter: tc.filter, } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } if got, wantLen := len(payload), 1+chainhash.HashSize+1+len(tc.filter); got != wantLen { t.Fatalf("payload length = %d, want %d", got, wantLen) } var got CFilter if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if got.FilterType != want.FilterType || got.BlockHash != want.BlockHash { t.Fatalf("decoded = %+v, want %+v", got, *want) } if !bytes.Equal(got.Filter, tc.filter) { t.Fatalf("filter = %x, want %x", got.Filter, tc.filter) } }) } } func TestCFilterTruncated(t *testing.T) { want := &CFilter{FilterType: FilterTypeBasic, BlockHash: testHash(0x56), Filter: []byte{1, 2, 3}} payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } for n := 0; n < len(payload); n++ { var got CFilter if err := got.DecodePayload(payload[:n]); err == nil { t.Fatalf("decoding %d of %d bytes succeeded", n, len(payload)) } } // A declared filter length longer than the remaining bytes is truncated. payload[1+chainhash.HashSize] = 0xff var got CFilter if err := got.DecodePayload(payload); !errors.Is(err, io.ErrUnexpectedEOF) { t.Fatalf("error = %v, want io.ErrUnexpectedEOF", err) } } func TestGetCFCheckptRoundTrip(t *testing.T) { want := &GetCFCheckpt{FilterType: FilterTypeBasic, StopHash: testHash(0x57)} payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } if got, wantLen := len(payload), 1+chainhash.HashSize; got != wantLen { t.Fatalf("payload length = %d, want %d", got, wantLen) } var got GetCFCheckpt if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("decoded = %+v, want %+v", got, *want) } } func TestCFCheckptRoundTrip(t *testing.T) { tests := []struct { name string headers []chainhash.Hash }{ {"empty", []chainhash.Hash{}}, {"one", []chainhash.Hash{testHash(0x58)}}, {"two", []chainhash.Hash{testHash(0x59), testHash(0x5a)}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { want := &CFCheckpt{ FilterType: FilterTypeBasic, StopHash: testHash(0x5b), FilterHeaders: tc.headers, } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } if got, wantLen := len(payload), 1+chainhash.HashSize+1+len(tc.headers)*chainhash.HashSize; got != wantLen { t.Fatalf("payload length = %d, want %d", got, wantLen) } var got CFCheckpt if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("decoded = %+v, want %+v", got, *want) } }) } } func TestCFCheckptTruncated(t *testing.T) { want := &CFCheckpt{ FilterType: FilterTypeBasic, StopHash: testHash(0x5c), FilterHeaders: []chainhash.Hash{testHash(0x5d)}, } payload, err := want.EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } for n := 0; n < len(payload); n++ { var got CFCheckpt if err := got.DecodePayload(payload[:n]); err == nil { t.Fatalf("decoding %d of %d bytes succeeded", n, len(payload)) } } } func TestFilterTypeNotValidated(t *testing.T) { // Unknown filter types are carried through unchanged; rejecting them is // the caller's decision. const unknown uint8 = 42 payload, err := (&GetCFCheckpt{FilterType: unknown, StopHash: testHash(0x5e)}).EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } var got GetCFCheckpt if err := got.DecodePayload(payload); err != nil { t.Fatalf("DecodePayload: %v", err) } if got.FilterType != unknown { t.Fatalf("FilterType = %d, want %d", got.FilterType, unknown) } }