package btcwire import ( "bytes" "testing" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) // Compile-time assertions that every payload type implements Payload. var ( _ Payload = (*Version)(nil) _ Payload = (*VerAck)(nil) _ Payload = (*SendHeaders)(nil) _ Payload = (*WtxidRelay)(nil) _ Payload = (*SendAddrV2)(nil) _ Payload = (*Ping)(nil) _ Payload = (*Pong)(nil) _ Payload = (*FeeFilter)(nil) _ Payload = (*GetHeaders)(nil) _ Payload = (*Headers)(nil) _ Payload = (*GetCFHeaders)(nil) _ Payload = (*CFHeaders)(nil) _ Payload = (*GetCFilters)(nil) _ Payload = (*CFilter)(nil) _ Payload = (*GetCFCheckpt)(nil) _ Payload = (*CFCheckpt)(nil) _ Payload = (*Inv)(nil) _ Payload = (*GetData)(nil) _ Payload = (*NotFound)(nil) _ Payload = (*Tx)(nil) ) // testHash returns a deterministic, non-zero hash derived from b. func testHash(b byte) chainhash.Hash { var h chainhash.Hash for i := range h { h[i] = b + byte(i) } return h } // allCommands lists every command constant this package defines. var allCommands = []string{ CmdVersion, CmdVerAck, CmdPing, CmdPong, CmdGetHeaders, CmdHeaders, CmdGetCFilters, CmdCFilter, CmdGetCFHeaders, CmdCFHeaders, CmdGetCFCheckpt, CmdCFCheckpt, CmdGetData, CmdTx, CmdInv, CmdNotFound, CmdSendHeaders, CmdWtxidRelay, CmdSendAddrV2, CmdFeeFilter, } // TestPayloadFrames round trips every payload type through a complete frame, // checking the command and that re-encoding the decoded payload reproduces // the original bytes. func TestPayloadFrames(t *testing.T) { tests := []struct { command string value func() Payload zero func() Payload }{ {CmdVersion, func() Payload { v := &Version{ ProtocolVersion: ProtocolVersion, Services: ServiceNodeNetwork | ServiceNodeCompactFilters, Timestamp: 1700000000, Nonce: 0x0102030405060708, UserAgent: "nostrino:0.1.0", LastBlock: 800000, Relay: true, } v.AddrRecv = mustAddress(t, "127.0.0.1", 8333) v.AddrFrom = mustAddress(t, "2001:db8::1", 18333) return v }, func() Payload { return &Version{} }}, {CmdVerAck, func() Payload { return &VerAck{} }, func() Payload { return &VerAck{} }}, {CmdSendHeaders, func() Payload { return &SendHeaders{} }, func() Payload { return &SendHeaders{} }}, {CmdWtxidRelay, func() Payload { return &WtxidRelay{} }, func() Payload { return &WtxidRelay{} }}, {CmdSendAddrV2, func() Payload { return &SendAddrV2{} }, func() Payload { return &SendAddrV2{} }}, {CmdPing, func() Payload { return &Ping{Nonce: 42} }, func() Payload { return &Ping{} }}, {CmdPong, func() Payload { return &Pong{Nonce: 43} }, func() Payload { return &Pong{} }}, {CmdFeeFilter, func() Payload { return &FeeFilter{FeeRate: 1000} }, func() Payload { return &FeeFilter{} }}, {CmdGetHeaders, func() Payload { return &GetHeaders{ ProtocolVersion: ProtocolVersion, LocatorHashes: []chainhash.Hash{testHash(1), testHash(2)}, StopHash: testHash(3), } }, func() Payload { return &GetHeaders{} }}, {CmdHeaders, func() Payload { return &Headers{Headers: []BlockHeader{ {Version: 1, PrevBlock: testHash(4), MerkleRoot: testHash(5), Timestamp: 100, Bits: 0x1d00ffff, Nonce: 7}, {Version: 2, PrevBlock: testHash(6), MerkleRoot: testHash(7), Timestamp: 200, Bits: 0x1b0404cb, Nonce: 8}, }} }, func() Payload { return &Headers{} }}, {CmdGetCFHeaders, func() Payload { return &GetCFHeaders{FilterType: FilterTypeBasic, StartHeight: 700000, StopHash: testHash(8)} }, func() Payload { return &GetCFHeaders{} }}, {CmdCFHeaders, func() Payload { return &CFHeaders{ FilterType: FilterTypeBasic, StopHash: testHash(9), PrevFilterHeader: testHash(10), FilterHashes: []chainhash.Hash{testHash(11), testHash(12)}, } }, func() Payload { return &CFHeaders{} }}, {CmdGetCFilters, func() Payload { return &GetCFilters{FilterType: FilterTypeBasic, StartHeight: 1, StopHash: testHash(13)} }, func() Payload { return &GetCFilters{} }}, {CmdCFilter, func() Payload { return &CFilter{FilterType: FilterTypeBasic, BlockHash: testHash(14), Filter: []byte{0x00, 0x01, 0x02}} }, func() Payload { return &CFilter{} }}, {CmdGetCFCheckpt, func() Payload { return &GetCFCheckpt{FilterType: FilterTypeBasic, StopHash: testHash(15)} }, func() Payload { return &GetCFCheckpt{} }}, {CmdCFCheckpt, func() Payload { return &CFCheckpt{ FilterType: FilterTypeBasic, StopHash: testHash(16), FilterHeaders: []chainhash.Hash{testHash(17), testHash(18)}, } }, func() Payload { return &CFCheckpt{} }}, {CmdInv, func() Payload { return &Inv{InvList: []InvVect{ {Type: InvVectTypeTx, Hash: testHash(19)}, {Type: InvVectTypeWitnessTx, Hash: testHash(20)}, }} }, func() Payload { return &Inv{} }}, {CmdGetData, func() Payload { return &GetData{InvList: []InvVect{{Type: InvVectTypeFilteredBlock, Hash: testHash(21)}}} }, func() Payload { return &GetData{} }}, {CmdNotFound, func() Payload { return &NotFound{InvList: []InvVect{{Type: InvVectTypeBlock, Hash: testHash(22)}}} }, func() Payload { return &NotFound{} }}, {CmdTx, func() Payload { return &Tx{Raw: []byte{0x01, 0x00, 0x00, 0x00, 0x00}} }, func() Payload { return &Tx{} }}, } for _, tc := range tests { t.Run(tc.command, func(t *testing.T) { want, err := tc.value().EncodePayload() if err != nil { t.Fatalf("EncodePayload: %v", err) } frame, err := EncodeMessagePayload(MainNet, tc.command, tc.value()) if err != nil { t.Fatalf("EncodeMessagePayload: %v", err) } msg, err := DecodeMessage(MainNet, bytes.NewReader(frame)) if err != nil { t.Fatalf("DecodeMessage: %v", err) } if msg.Command != tc.command { t.Fatalf("command = %q, want %q", msg.Command, tc.command) } if !bytes.Equal(msg.Payload, want) { t.Fatalf("frame payload = %x, want %x", msg.Payload, want) } decoded := tc.zero() if err := msg.Decode(decoded); err != nil { t.Fatalf("Message.Decode: %v", err) } got, err := decoded.EncodePayload() if err != nil { t.Fatalf("re-EncodePayload: %v", err) } if !bytes.Equal(got, want) { t.Fatalf("re-encoded payload = %x, want %x", got, want) } }) } } // mustAddress parses host into a NetAddress or fails the test. func mustAddress(t *testing.T, host string, port uint16) NetAddress { t.Helper() addr, err := NewNetAddress(host, port) if err != nil { t.Fatalf("NewNetAddress(%q, %d): %v", host, port, err) } return *addr }