From cf754d09bfbc9922c2e8544ac19bbd876b16c5e7 Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 10 Apr 2014 22:29:00 -0400 Subject: [PATCH] Initial implementation of BIP0037. Implement filteradd, filterclear, filterload, and merkleblock. --- invvect.go | 14 +-- message.go | 48 ++++++--- message_test.go | 11 ++ msgfilteradd.go | 103 +++++++++++++++++++ msgfilteradd_test.go | 169 +++++++++++++++++++++++++++++++ msgfilterclear.go | 58 +++++++++++ msgfilterclear_test.go | 72 +++++++++++++ msgfilterload.go | 160 +++++++++++++++++++++++++++++ msgfilterload_test.go | 223 +++++++++++++++++++++++++++++++++++++++++ msgmerkleblock.go | 163 ++++++++++++++++++++++++++++++ msgmerkleblock_test.go | 109 ++++++++++++++++++++ 11 files changed, 1108 insertions(+), 22 deletions(-) create mode 100644 msgfilteradd.go create mode 100644 msgfilteradd_test.go create mode 100644 msgfilterclear.go create mode 100644 msgfilterclear_test.go create mode 100644 msgfilterload.go create mode 100644 msgfilterload_test.go create mode 100644 msgmerkleblock.go create mode 100644 msgmerkleblock_test.go diff --git a/invvect.go b/invvect.go index b0dbb697..e9b74dcb 100644 --- a/invvect.go +++ b/invvect.go @@ -23,16 +23,18 @@ type InvType uint32 // These constants define the various supported inventory vector types. const ( - InvTypeError InvType = 0 - InvTypeTx InvType = 1 - InvTypeBlock InvType = 2 + InvTypeError InvType = 0 + InvTypeTx InvType = 1 + InvTypeBlock InvType = 2 + InvTypeFilteredBlock InvType = 3 ) // Map of service flags back to their constant names for pretty printing. var ivStrings = map[InvType]string{ - InvTypeError: "ERROR", - InvTypeTx: "MSG_TX", - InvTypeBlock: "MSG_BLOCK", + InvTypeError: "ERROR", + InvTypeTx: "MSG_TX", + InvTypeBlock: "MSG_BLOCK", + InvTypeFilteredBlock: "MSG_FILTERED_BLOCK", } // String returns the InvType in human-readable form. diff --git a/message.go b/message.go index 1b502444..e007a594 100644 --- a/message.go +++ b/message.go @@ -26,22 +26,26 @@ const maxMessagePayload = (1024 * 1024 * 32) // 32MB // Commands used in bitcoin message headers which describe the type of message. const ( - cmdVersion = "version" - cmdVerAck = "verack" - cmdGetAddr = "getaddr" - cmdAddr = "addr" - cmdGetBlocks = "getblocks" - cmdInv = "inv" - cmdGetData = "getdata" - cmdNotFound = "notfound" - cmdBlock = "block" - cmdTx = "tx" - cmdGetHeaders = "getheaders" - cmdHeaders = "headers" - cmdPing = "ping" - cmdPong = "pong" - cmdAlert = "alert" - cmdMemPool = "mempool" + cmdVersion = "version" + cmdVerAck = "verack" + cmdGetAddr = "getaddr" + cmdAddr = "addr" + cmdGetBlocks = "getblocks" + cmdInv = "inv" + cmdGetData = "getdata" + cmdNotFound = "notfound" + cmdBlock = "block" + cmdTx = "tx" + cmdGetHeaders = "getheaders" + cmdHeaders = "headers" + cmdPing = "ping" + cmdPong = "pong" + cmdAlert = "alert" + cmdMemPool = "mempool" + cmdFilterAdd = "filteradd" + cmdFilterClear = "filterclear" + cmdFilterLoad = "filterload" + cmdMerkleBlock = "merkleblock" ) // Message is an interface that describes a bitcoin message. A type that @@ -108,6 +112,18 @@ func makeEmptyMessage(command string) (Message, error) { case cmdMemPool: msg = &MsgMemPool{} + case cmdFilterAdd: + msg = &MsgFilterAdd{} + + case cmdFilterClear: + msg = &MsgFilterClear{} + + case cmdFilterLoad: + msg = &MsgFilterLoad{} + + case cmdMerkleBlock: + msg = &MsgMerkleBlock{} + default: return nil, fmt.Errorf("unhandled command [%s]", command) } diff --git a/message_test.go b/message_test.go index c79f4e8d..bfbc8e1e 100644 --- a/message_test.go +++ b/message_test.go @@ -68,6 +68,13 @@ func TestMessage(t *testing.T) { msgHeaders := btcwire.NewMsgHeaders() msgAlert := btcwire.NewMsgAlert([]byte("payload"), []byte("signature")) msgMemPool := btcwire.NewMsgMemPool() + msgFilterAdd := btcwire.NewMsgFilterAdd([]byte{0x01}) + msgFilterClear := btcwire.NewMsgFilterClear() + msgFilterLoad := btcwire.NewMsgFilterLoad([]byte{0x01}, 10, 0, btcwire.BloomUpdateNone) + + // + bh := btcwire.NewBlockHeader(&btcwire.ShaHash{}, &btcwire.ShaHash{}, 0, 0) + msgMerkleBlock := btcwire.NewMsgMerkleBlock(bh) tests := []struct { in btcwire.Message // Value to encode @@ -92,6 +99,10 @@ func TestMessage(t *testing.T) { {msgHeaders, msgHeaders, pver, btcwire.MainNet, 25}, {msgAlert, msgAlert, pver, btcwire.MainNet, 42}, {msgMemPool, msgMemPool, pver, btcwire.MainNet, 24}, + {msgFilterAdd, msgFilterAdd, pver, btcwire.MainNet, 26}, + {msgFilterClear, msgFilterClear, pver, btcwire.MainNet, 24}, + {msgFilterLoad, msgFilterLoad, pver, btcwire.MainNet, 35}, + {msgMerkleBlock, msgMerkleBlock, pver, btcwire.MainNet, 110}, } t.Logf("Running %d tests", len(tests)) diff --git a/msgfilteradd.go b/msgfilteradd.go new file mode 100644 index 00000000..5e05177b --- /dev/null +++ b/msgfilteradd.go @@ -0,0 +1,103 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire + +import ( + "fmt" + "io" +) + +const ( + // MaxFilterAddDataSize is the maximum byte size of a data + // element to add to the Bloom filter. It is equal to the + // maximum element size of a script. + MaxFilterAddDataSize = 520 +) + +// MsgFilterAdd implements the Message interface and represents a bitcoin filteradd +// message which is used to add a data element to an existing Bloom filter. +// +// This message was not added until protocol version BIP0037Version. +type MsgFilterAdd struct { + Data []byte +} + +// BtcDecode decodes r using the bitcoin protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgFilterAdd) BtcDecode(r io.Reader, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("filteradd message invalid for protocol "+ + "version %d", pver) + return messageError("MsgFilterAdd.BtcDecode", str) + } + + size, err := readVarInt(r, pver) + if err != nil { + return err + } + + if size > MaxFilterAddDataSize { + str := fmt.Sprintf("filteradd size too large for message "+ + "[size %v, max %v]", size, MaxFilterAddDataSize) + return messageError("MsgFilterAdd.BtcDecode", str) + } + + msg.Data = make([]byte, size) + _, err = io.ReadFull(r, msg.Data) + if err != nil { + return err + } + + return nil +} + +// BtcEncode encodes the receiver to w using the bitcoin protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgFilterAdd) BtcEncode(w io.Writer, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("filteradd message invalid for protocol "+ + "version %d", pver) + return messageError("MsgFilterAdd.BtcEncode", str) + } + + size := len(msg.Data) + if size > MaxFilterAddDataSize { + str := fmt.Sprintf("filteradd size too large for message "+ + "[size %v, max %v]", size, MaxFilterAddDataSize) + return messageError("MsgFilterAdd.BtcEncode", str) + } + + err := writeVarInt(w, pver, uint64(size)) + if err != nil { + return err + } + + err = writeElement(w, msg.Data) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgFilterAdd) Command() string { + return cmdFilterAdd +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgFilterAdd) MaxPayloadLength(pver uint32) uint32 { + return MaxVarIntPayload + MaxFilterAddDataSize +} + +// NewMsgFilterAdd returns a new bitcoin filteradd message that conforms to the Message +// interface. See MsgFilterAdd for details. +func NewMsgFilterAdd(data []byte) *MsgFilterAdd { + return &MsgFilterAdd{ + Data: data, + } +} diff --git a/msgfilteradd_test.go b/msgfilteradd_test.go new file mode 100644 index 00000000..9c111fc3 --- /dev/null +++ b/msgfilteradd_test.go @@ -0,0 +1,169 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire_test + +import ( + "bytes" + "github.com/conformal/btcwire" + "io" + "testing" +) + +// TestFilterCLearLatest tests the MsgFilterAdd API against the latest protocol version. +func TestFilterAddLatest(t *testing.T) { + pver := btcwire.ProtocolVersion + + data := []byte{0x01, 0x02} + msg := btcwire.NewMsgFilterAdd(data) + + // Ensure the command is expected value. + wantCmd := "filteradd" + if cmd := msg.Command(); cmd != wantCmd { + t.Errorf("NewMsgFilterAdd: wrong command - got %v want %v", + cmd, wantCmd) + } + + // Ensure max payload is expected value for latest protocol version. + wantPayload := uint32(529) + maxPayload := msg.MaxPayloadLength(pver) + if maxPayload != wantPayload { + t.Errorf("MaxPayloadLength: wrong max payload length for "+ + "protocol version %d - got %v, want %v", pver, + maxPayload, wantPayload) + } + + // Test encode with latest protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, pver) + if err != nil { + t.Errorf("encode of MsgFilterAdd failed %v err <%v>", msg, err) + } + + // Test decode with latest protocol version. + readmsg := btcwire.MsgFilterAdd{} + err = readmsg.BtcDecode(&buf, pver) + if err != nil { + t.Errorf("decode of MsgFilterAdd failed [%v] err <%v>", buf, err) + } + + return +} + +// TestFilterAddCrossProtocol tests the MsgFilterAdd API when encoding with the latest +// protocol version and decoded with BIP0031Version. +func TestFilterAddCrossProtocol(t *testing.T) { + data := []byte{0x01, 0x02} + msg := btcwire.NewMsgFilterAdd(data) + + // Encode with old protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, btcwire.BIP0037Version-1) + if err == nil { + t.Errorf("encode of MsgFilterAdd succeeded when it shouldn't have %v", + msg) + } + + // Decode with old protocol version. + readmsg := btcwire.MsgFilterAdd{} + err = readmsg.BtcDecode(&buf, btcwire.BIP0031Version) + if err == nil { + t.Errorf("decode of MsgFilterAdd succeeded when it shouldn't have %v", + msg) + } +} + +// TestFilterAddMaxDataSize tests the MsgFilterAdd API maximum data size. +func TestFilterAddMaxDataSize(t *testing.T) { + data := bytes.Repeat([]byte{0xff}, 521) + msg := btcwire.NewMsgFilterAdd(data) + + // Encode with latest protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, btcwire.ProtocolVersion) + if err == nil { + t.Errorf("encode of MsgFilterAdd succeeded when it shouldn't have %v", + msg) + } + + // Decode with latest protocol version. + readbuf := bytes.NewReader(data) + err = msg.BtcDecode(readbuf, btcwire.ProtocolVersion) + if err == nil { + t.Errorf("decode of MsgFilterAdd succeeded when it shouldn't have %v", + msg) + } +} + +// TestFilterAddWireErrors performs negative tests against wire encode and decode +// of MsgFilterAdd to confirm error paths work correctly. +func TestFilterAddWireErrors(t *testing.T) { + pver := btcwire.ProtocolVersion + + tests := []struct { + in *btcwire.MsgFilterAdd // Value to encode + buf []byte // Wire encoding + pver uint32 // Protocol version for wire encoding + max int // Max size of fixed buffer to induce errors + writeErr error // Expected write error + readErr error // Expected read error + }{ + // Latest protocol version with intentional read/write errors. + { + &btcwire.MsgFilterAdd{Data: []byte{0x01, 0x02, 0x03, 0x04}}, + []byte{ + 0x05, // Varint for size of data + 0x02, 0x03, 0x04, // Data + }, + pver, + 2, + io.ErrShortWrite, + io.ErrUnexpectedEOF, + }, + { + &btcwire.MsgFilterAdd{Data: []byte{0x01, 0x02, 0x03, 0x04}}, + []byte{ + 0x05, // Varint for size of data + 0x02, 0x03, 0x04, // Data + }, + pver, + 0, + io.ErrShortWrite, + io.EOF, + }, + { + &btcwire.MsgFilterAdd{Data: []byte{0x01, 0x02, 0x03, 0x04}}, + []byte{ + 0x05, // Varint for size of data + 0x02, 0x03, 0x04, // Data + }, + pver, + 1, + io.ErrShortWrite, + io.EOF, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + // Encode to wire format. + w := newFixedWriter(test.max) + err := test.in.BtcEncode(w, test.pver) + if err != test.writeErr { + t.Errorf("BtcEncode #%d wrong error got: %v, want: %v", + i, err, test.writeErr) + continue + } + + // Decode from wire format. + var msg btcwire.MsgFilterAdd + r := newFixedReader(test.max, test.buf) + err = msg.BtcDecode(r, test.pver) + if err != test.readErr { + t.Errorf("BtcDecode #%d wrong error got: %v, want: %v", + i, err, test.readErr) + continue + } + } +} diff --git a/msgfilterclear.go b/msgfilterclear.go new file mode 100644 index 00000000..ea7cfcac --- /dev/null +++ b/msgfilterclear.go @@ -0,0 +1,58 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire + +import ( + "fmt" + "io" +) + +// MsgFilterClear implements the Message interface and represents a bitcoin filterclear +// message which is used to reset a Bloom filter. +// +// This message was not added until protocol version BIP0037Version. +type MsgFilterClear struct{} + +// BtcDecode decodes r using the bitcoin protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgFilterClear) BtcDecode(r io.Reader, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("filterclear message invalid for protocol "+ + "version %d", pver) + return messageError("MsgFilterClear.BtcDecode", str) + } + + return nil +} + +// BtcEncode encodes the receiver to w using the bitcoin protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgFilterClear) BtcEncode(w io.Writer, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("filterclear message invalid for protocol "+ + "version %d", pver) + return messageError("MsgFilterClear.BtcEncode", str) + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgFilterClear) Command() string { + return cmdFilterClear +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgFilterClear) MaxPayloadLength(pver uint32) uint32 { + return 0 +} + +// NewMsgFilterClear returns a new bitcoin filterclear message that conforms to the Message +// interface. See MsgFilterClear for details. +func NewMsgFilterClear() *MsgFilterClear { + return &MsgFilterClear{} +} diff --git a/msgfilterclear_test.go b/msgfilterclear_test.go new file mode 100644 index 00000000..d8cf3396 --- /dev/null +++ b/msgfilterclear_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire_test + +import ( + "bytes" + "github.com/conformal/btcwire" + "testing" +) + +// TestFilterCLearLatest tests the MsgFilterClear API against the latest protocol version. +func TestFilterClearLatest(t *testing.T) { + pver := btcwire.ProtocolVersion + + msg := btcwire.NewMsgFilterClear() + + // Ensure the command is expected value. + wantCmd := "filterclear" + if cmd := msg.Command(); cmd != wantCmd { + t.Errorf("NewMsgFilterClear: wrong command - got %v want %v", + cmd, wantCmd) + } + + // Ensure max payload is expected value for latest protocol version. + wantPayload := uint32(0) + maxPayload := msg.MaxPayloadLength(pver) + if maxPayload != wantPayload { + t.Errorf("MaxPayloadLength: wrong max payload length for "+ + "protocol version %d - got %v, want %v", pver, + maxPayload, wantPayload) + } + + // Test encode with latest protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, pver) + if err != nil { + t.Errorf("encode of MsgFilterClear failed %v err <%v>", msg, err) + } + + // Test decode with latest protocol version. + readmsg := btcwire.NewMsgFilterClear() + err = readmsg.BtcDecode(&buf, pver) + if err != nil { + t.Errorf("decode of MsgFilterClear failed [%v] err <%v>", buf, err) + } + + return +} + +// TestFilterClearCrossProtocol tests the MsgFilterClear API when encoding with the latest +// protocol version and decoded with BIP0031Version. +func TestFilterClearCrossProtocol(t *testing.T) { + msg := btcwire.NewMsgFilterClear() + + // Encode with old protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, btcwire.BIP0037Version-1) + if err == nil { + t.Errorf("encode of MsgFilterClear succeeded when it shouldn't have %v", + msg) + } + + // Decode with old protocol version. + readmsg := btcwire.NewMsgFilterClear() + err = readmsg.BtcDecode(&buf, btcwire.BIP0031Version) + if err == nil { + t.Errorf("decode of MsgFilterClear succeeded when it shouldn't have %v", + msg) + } +} diff --git a/msgfilterload.go b/msgfilterload.go new file mode 100644 index 00000000..412476b4 --- /dev/null +++ b/msgfilterload.go @@ -0,0 +1,160 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire + +import ( + "fmt" + "io" +) + +type BloomUpdateType uint8 + +const ( + // BloomUpdateNone indicates the filter is not adjusted when a match is found. + BloomUpdateNone BloomUpdateType = 0 + + // BloomUpdateAll indicates if the filter matches any data element in a + // scriptPubKey, the outpoint is serialized and inserted into the filter. + BloomUpdateAll BloomUpdateType = 1 + + // BloomUpdateP2PubkeyOnly indicates if the filter matches a data element in + // a scriptPubkey and the script is of the standard payToPybKey or payToMultiSig, + // the outpoint is inserted into the filter. + BloomUpdateP2PubkeyOnly BloomUpdateType = 2 +) + +const ( + // MaxFilterLoadHashFuncs is the maximum number of hash functions to load + // into the Bloom filter. + MaxFilterLoadHashFuncs = 50 + + // MaxFilterLoadFilterSize is the maximum size in bytes a filter may be. + MaxFilterLoadFilterSize = 36000 +) + +// MsgFilterLoad implements the Message interface and represents a bitcoin filterload +// message which is used to reset a Bloom filter. +// +// This message was not added until protocol version BIP0037Version. +type MsgFilterLoad struct { + Filter []byte + HashFuncs uint32 + Tweak uint32 + Flags BloomUpdateType +} + +// BtcDecode decodes r using the bitcoin protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgFilterLoad) BtcDecode(r io.Reader, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("filterload message invalid for protocol "+ + "version %d", pver) + return messageError("MsgFilterLoad.BtcDecode", str) + } + + // Read num filter and limit to max. + size, err := readVarInt(r, pver) + if err != nil { + return err + } + if size > MaxFilterLoadFilterSize { + str := fmt.Sprintf("filterload filter size too large for message "+ + "[size %v, max %v]", size, MaxFilterLoadFilterSize) + return messageError("MsgFilterLoad.BtcDecode", str) + } + + msg.Filter = make([]byte, size) + _, err = io.ReadFull(r, msg.Filter) + if err != nil { + return err + } + + err = readElements(r, &msg.HashFuncs, &msg.Tweak, &msg.Flags) + if err != nil { + return err + } + + if msg.HashFuncs > MaxFilterLoadHashFuncs { + str := fmt.Sprintf("too many filter hash functions for message "+ + "[count %v, max %v]", msg.HashFuncs, MaxFilterLoadHashFuncs) + return messageError("MsgFilterLoad.BtcDecode", str) + } + + return nil +} + +// BtcEncode encodes the receiver to w using the bitcoin protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgFilterLoad) BtcEncode(w io.Writer, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("filterload message invalid for protocol "+ + "version %d", pver) + return messageError("MsgFilterLoad.BtcEncode", str) + } + + size := len(msg.Filter) + if size > MaxFilterLoadFilterSize { + str := fmt.Sprintf("filterload filter size too large for message "+ + "[size %v, max %v]", size, MaxFilterLoadFilterSize) + return messageError("MsgFilterLoad.BtcEncode", str) + } + + if msg.HashFuncs > MaxFilterLoadHashFuncs { + str := fmt.Sprintf("too many filter hash functions for message "+ + "[count %v, max %v]", msg.HashFuncs, MaxFilterLoadHashFuncs) + return messageError("MsgFilterLoad.BtcEncode", str) + } + + err := writeVarInt(w, pver, uint64(size)) + if err != nil { + return err + } + err = writeElements(w, msg.Filter, msg.HashFuncs, msg.Tweak, msg.Flags) + if err != nil { + return err + } + + return nil +} + +// Serialize encodes the transaction to w using a format that suitable for +// long-term storage such as a database while respecting the Version field in +// the transaction. This function differs from BtcEncode in that BtcEncode +// encodes the transaction to the bitcoin wire protocol in order to be sent +// across the network. The wire encoding can technically differ depending on +// the protocol version and doesn't even really need to match the format of a +// stored transaction at all. As of the time this comment was written, the +// encoded transaction is the same in both instances, but there is a distinct +// difference and separating the two allows the API to be flexible enough to +// deal with changes. +func (msg *MsgFilterLoad) Serialize(w io.Writer) error { + // At the current time, there is no difference between the wire encoding + // at protocol version 0 and the stable long-term storage format. As + // a result, make use of BtcEncode. + return msg.BtcEncode(w, BIP0037Version) +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgFilterLoad) Command() string { + return cmdFilterLoad +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgFilterLoad) MaxPayloadLength(pver uint32) uint32 { + return MaxVarIntPayload + MaxFilterLoadFilterSize + 4 + 4 + 1 +} + +// NewMsgFilterLoad returns a new bitcoin filterload message that conforms to the Message +// interface. See MsgFilterLoad for details. +func NewMsgFilterLoad(filter []byte, hashFuncs uint32, tweak uint32, flags BloomUpdateType) *MsgFilterLoad { + return &MsgFilterLoad{ + Filter: filter, + HashFuncs: hashFuncs, + Tweak: tweak, + Flags: flags, + } +} diff --git a/msgfilterload_test.go b/msgfilterload_test.go new file mode 100644 index 00000000..2bf387fe --- /dev/null +++ b/msgfilterload_test.go @@ -0,0 +1,223 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire_test + +import ( + "bytes" + "github.com/conformal/btcwire" + "io" + "testing" +) + +// TestFilterCLearLatest tests the MsgFilterLoad API against the latest protocol version. +func TestFilterLoadLatest(t *testing.T) { + pver := btcwire.ProtocolVersion + + data := []byte{0x01, 0x02} + msg := btcwire.NewMsgFilterLoad(data, 10, 0, 0) + + // Ensure the command is expected value. + wantCmd := "filterload" + if cmd := msg.Command(); cmd != wantCmd { + t.Errorf("NewMsgFilterLoad: wrong command - got %v want %v", + cmd, wantCmd) + } + + // Ensure max payadd is expected value for latest protocol version. + wantPayload := uint32(36018) + maxPayload := msg.MaxPayloadLength(pver) + if maxPayload != wantPayload { + t.Errorf("MaxPayLoadLength: wrong max payload length for "+ + "protocol version %d - got %v, want %v", pver, + maxPayload, wantPayload) + } + + // Test encode with latest protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, pver) + if err != nil { + t.Errorf("encode of MsgFilterLoad failed %v err <%v>", msg, err) + } + + // Test decode with latest protocol version. + readmsg := btcwire.MsgFilterLoad{} + err = readmsg.BtcDecode(&buf, pver) + if err != nil { + t.Errorf("decode of MsgFilterLoad failed [%v] err <%v>", buf, err) + } + + return +} + +// TestFilterLoadCrossProtocol tests the MsgFilterLoad API when encoding with the latest +// protocol version and decoded with BIP0031Version. +func TestFilterLoadCrossProtocol(t *testing.T) { + data := []byte{0x01, 0x02} + msg := btcwire.NewMsgFilterLoad(data, 10, 0, 0) + + // Encode with old protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, btcwire.BIP0037Version-1) + if err == nil { + t.Errorf("encode of MsgFilterLoad succeeded when it shouldn't have %v", + msg) + } + + // Decode with old protocol version. + readmsg := btcwire.MsgFilterLoad{} + err = readmsg.BtcDecode(&buf, btcwire.BIP0031Version) + if err == nil { + t.Errorf("decode of MsgFilterLoad succeeded when it shouldn't have %v", + msg) + } +} + +// TestFilterLoadMaxFilterSize tests the MsgFilterLoad API maximum filter size. +func TestFilterLoadMaxFilterSize(t *testing.T) { + data := bytes.Repeat([]byte{0xff}, 36001) + msg := btcwire.NewMsgFilterLoad(data, 10, 0, 0) + + // Encode with latest protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, btcwire.ProtocolVersion) + if err == nil { + t.Errorf("encode of MsgFilterLoad succeeded when it shouldn't have %v", + msg) + } + + // Decode with latest protocol version. + readbuf := bytes.NewReader(data) + err = msg.BtcDecode(readbuf, btcwire.ProtocolVersion) + if err == nil { + t.Errorf("decode of MsgFilterLoad succeeded when it shouldn't have %v", + msg) + } +} + +// TestFilterLoadMaxHashFuncsSize tests the MsgFilterLoad API maximum hash functions. +func TestFilterLoadMaxHashFuncsSize(t *testing.T) { + data := bytes.Repeat([]byte{0xff}, 10) + msg := btcwire.NewMsgFilterLoad(data, 61, 0, 0) + + // Encode with latest protocol version. + var buf bytes.Buffer + err := msg.BtcEncode(&buf, btcwire.ProtocolVersion) + if err == nil { + t.Errorf("encode of MsgFilterLoad succeeded when it shouldn't have %v", + msg) + } + + newBuf := []byte{ + 0x0a, // filter size + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // filter + 0x3d, 0x00, 0x00, 0x00, // max hash funcs + 0x00, 0x00, 0x00, 0x00, // tweak + 0x00, // update Type + } + // Decode with latest protocol version. + buf1 := bytes.NewBuffer(newBuf) + readbuf := bytes.NewReader(buf1.Bytes()) + err = msg.BtcDecode(readbuf, btcwire.ProtocolVersion) + if err == nil { + t.Errorf("decode of MsgFilterLoad succeeded when it shouldn't have %v", + msg) + } +} + +// TestFilterLoadWireErrors performs negative tests against wire encode and decode +// of MsgFilterLoad to confirm error paths work correctly. +func TestFilterLoadWireErrors(t *testing.T) { + pver := btcwire.ProtocolVersion + + tests := []struct { + in *btcwire.MsgFilterLoad // Value to encode + buf []byte // Wire encoding + pver uint32 // Protocol version for wire encoding + max int // Max size of fixed buffer to induce errors + writeErr error // Expected write error + readErr error // Expected read error + }{ + // Latest protocol version with intentional read/write errors. + { + &btcwire.MsgFilterLoad{ + Filter: []byte{0x01, 0x02, 0x03, 0x04}, + HashFuncs: 10, + Tweak: 0, + Flags: btcwire.BloomUpdateNone, + }, + []byte{ + 0x04, // Varint for size of Filter + 0x01, 0x02, 0x03, 0x04, // Filter + 0x00, 0x0a, // HashFuncs + 0x00, 0x00, // Tweak + 0x00, // Flags + }, + pver, + 2, + io.ErrShortWrite, + io.ErrUnexpectedEOF, + }, + { + &btcwire.MsgFilterLoad{ + Filter: []byte{0x01, 0x02, 0x03, 0x04}, + HashFuncs: 10, + Tweak: 0, + Flags: btcwire.BloomUpdateNone, + }, + []byte{ + 0x04, // Varint for size of Filter + 0x01, 0x02, 0x03, 0x04, // Filter + 0x00, 0x0a, // HashFuncs + 0x00, 0x00, // Tweak + 0x00, // Flags + }, + pver, + 0, + io.ErrShortWrite, + io.EOF, + }, + { + &btcwire.MsgFilterLoad{ + Filter: []byte{0x01, 0x02, 0x03, 0x04}, + HashFuncs: 10, + Tweak: 0, + Flags: btcwire.BloomUpdateNone, + }, + []byte{ + 0x04, // Varint for size of Filter + 0x01, 0x02, 0x03, 0x04, // Filter + 0x00, 0x0a, // HashFuncs + 0x00, 0x00, // Tweak + 0x00, // Flags + }, + pver, + 10, + io.ErrShortWrite, + io.ErrUnexpectedEOF, + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + // Encode to wire format. + w := newFixedWriter(test.max) + err := test.in.BtcEncode(w, test.pver) + if err != test.writeErr { + t.Errorf("BtcEncode #%d wrong error got: %v, want: %v", + i, err, test.writeErr) + continue + } + + // Decode from wire format. + var msg btcwire.MsgFilterLoad + r := newFixedReader(test.max, test.buf) + err = msg.BtcDecode(r, test.pver) + if err != test.readErr { + t.Errorf("BtcDecode #%d wrong error got: %v, want: %v", + i, err, test.readErr) + continue + } + } +} diff --git a/msgmerkleblock.go b/msgmerkleblock.go new file mode 100644 index 00000000..a4cc8376 --- /dev/null +++ b/msgmerkleblock.go @@ -0,0 +1,163 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire + +import ( + "fmt" + "io" +) + +// MsgMerkleBlock implements the Message interface and represents a bitcoin merkleblock +// message which is used to reset a Bloom filter. +// +// This message was not added until protocol version BIP0037Version. +type MsgMerkleBlock struct { + Header BlockHeader + Transactions uint32 + Hashes []*ShaHash + Flags []byte +} + +// AddTxHash adds a new transaction hash to the message. +func (msg *MsgMerkleBlock) AddTxHash(hash *ShaHash) error { + if len(msg.Hashes)+1 > maxTxPerBlock { + str := fmt.Sprintf("too many tx hashes for message [max %v]", + maxTxPerBlock) + return messageError("MsgMerkleBlock.AddTxHash", str) + } + + msg.Hashes = append(msg.Hashes, hash) + return nil +} + +// BtcDecode decodes r using the bitcoin protocol encoding into the receiver. +// This is part of the Message interface implementation. +func (msg *MsgMerkleBlock) BtcDecode(r io.Reader, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("merkleblock message invalid for protocol "+ + "version %d", pver) + return messageError("MsgMerkleBlock.BtcDecode", str) + } + + err := readBlockHeader(r, pver, &msg.Header) + if err != nil { + return err + } + + err = readElement(r, &msg.Transactions) + if err != nil { + return err + } + + // Read num block locator hashes and limit to max. + count, err := readVarInt(r, pver) + if err != nil { + return err + } + if count > maxTxPerBlock { + str := fmt.Sprintf("too many transaction hashes for message "+ + "[count %v, max %v]", count, maxTxPerBlock) + return messageError("MsgMerkleBlock.BtcDecode", str) + } + + msg.Hashes = make([]*ShaHash, 0, count) + for i := uint64(0); i < count; i++ { + sha := ShaHash{} + err := readElement(r, &sha) + if err != nil { + return err + } + msg.AddTxHash(&sha) + } + + count, err = readVarInt(r, pver) + if err != nil { + return err + } + + msg.Flags = make([]byte, 0, count) + err = readElement(r, &msg.Flags) + if err != nil { + return err + } + + return nil +} + +// BtcEncode encodes the receiver to w using the bitcoin protocol encoding. +// This is part of the Message interface implementation. +func (msg *MsgMerkleBlock) BtcEncode(w io.Writer, pver uint32) error { + if pver < BIP0037Version { + str := fmt.Sprintf("merkleblock message invalid for protocol "+ + "version %d", pver) + return messageError("MsgMerkleBlock.BtcEncode", str) + } + + // Read num transaction hashes and limit to max. + count := len(msg.Hashes) + if count > maxTxPerBlock { + str := fmt.Sprintf("too many transaction hashes for message "+ + "[count %v, max %v]", count, maxTxPerBlock) + return messageError("MsgMerkleBlock.BtcDecode", str) + } + + err := writeBlockHeader(w, pver, &msg.Header) + if err != nil { + return err + } + + err = writeElement(w, msg.Transactions) + if err != nil { + return err + } + + err = writeVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + + for _, hash := range msg.Hashes { + err = writeElement(w, hash) + if err != nil { + return err + } + } + + count = len(msg.Flags) + err = writeVarInt(w, pver, uint64(count)) + if err != nil { + return err + } + + err = writeElement(w, msg.Flags) + if err != nil { + return err + } + + return nil +} + +// Command returns the protocol command string for the message. This is part +// of the Message interface implementation. +func (msg *MsgMerkleBlock) Command() string { + return cmdMerkleBlock +} + +// MaxPayloadLength returns the maximum length the payload can be for the +// receiver. This is part of the Message interface implementation. +func (msg *MsgMerkleBlock) MaxPayloadLength(pver uint32) uint32 { + return MaxBlockPayload +} + +// NewMsgMerkleBlock returns a new bitcoin merkleblock message that conforms to the Message +// interface. See MsgMerkleBlock for details. +func NewMsgMerkleBlock(bh *BlockHeader) *MsgMerkleBlock { + return &MsgMerkleBlock{ + Header: *bh, + Transactions: 0, + Hashes: make([]*ShaHash, 0), + Flags: make([]byte, 0), + } +} diff --git a/msgmerkleblock_test.go b/msgmerkleblock_test.go new file mode 100644 index 00000000..79144ce5 --- /dev/null +++ b/msgmerkleblock_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcwire_test + +import ( + "bytes" + "crypto/rand" + "github.com/conformal/btcwire" + "testing" +) + +// TestMerkleBlock tests the MsgMerkleBlock API. +func TestMerkleBlock(t *testing.T) { + pver := btcwire.ProtocolVersion + pverOld := btcwire.BIP0037Version - 1 + + // Block 1 header. + prevHash := &blockOne.Header.PrevBlock + merkleHash := &blockOne.Header.MerkleRoot + bits := blockOne.Header.Bits + nonce := blockOne.Header.Nonce + bh := btcwire.NewBlockHeader(prevHash, merkleHash, bits, nonce) + + // Ensure the command is expected value. + wantCmd := "merkleblock" + msg := btcwire.NewMsgMerkleBlock(bh) + if cmd := msg.Command(); cmd != wantCmd { + t.Errorf("NewMsgBlock: wrong command - got %v want %v", + cmd, wantCmd) + } + + // Ensure max payload is expected value for latest protocol version. + // Num addresses (varInt) + max allowed addresses. + wantPayload := uint32(1000000) + maxPayload := msg.MaxPayloadLength(pver) + if maxPayload != wantPayload { + t.Errorf("MaxPayloadLength: wrong max payload length for "+ + "protocol version %d - got %v, want %v", pver, + maxPayload, wantPayload) + } + + // Load maxTxPerBlock hashes + data := make([]byte, 32) + for i := 0; i < (btcwire.MaxBlockPayload/10)+1; i++ { + rand.Read(data) + hash, err := btcwire.NewShaHash(data) + if err != nil { + t.Errorf("NewShaHash failed: %v\n", err) + return + } + + if err = msg.AddTxHash(hash); err != nil { + t.Errorf("AddTxHash failed: %v\n", err) + return + } + } + + // Add one more Tx to test failure + rand.Read(data) + hash, err := btcwire.NewShaHash(data) + if err != nil { + t.Errorf("NewShaHash failed: %v\n", err) + return + } + + if err = msg.AddTxHash(hash); err == nil { + t.Errorf("AddTxHash succeeded when it should have failed") + return + } + + // Test encode with latest protocol version. + var buf bytes.Buffer + err = msg.BtcEncode(&buf, pver) + if err != nil { + t.Errorf("encode of MsgMerkleBlock failed %v err <%v>", msg, err) + } + + // Test encode with old protocol version. + if err = msg.BtcEncode(&buf, pverOld); err == nil { + t.Errorf("encode of MsgMerkleBlock succeeded with old protocol " + + "version when it should have failed") + return + } + + // Test decode with latest protocol version. + readmsg := btcwire.MsgMerkleBlock{} + err = readmsg.BtcDecode(&buf, pver) + if err != nil { + t.Errorf("decode of MsgMerkleBlock failed [%v] err <%v>", buf, err) + } + + // Test decode with old protocol version. + if err = readmsg.BtcDecode(&buf, pverOld); err == nil { + t.Errorf("decode of MsgMerkleBlock successed with old protocol " + + "version when it should have failed") + return + } + + // Force extra hash to test maxTxPerBlock + msg.Hashes = append(msg.Hashes, hash) + err = msg.BtcEncode(&buf, pver) + if err == nil { + t.Errorf("encode of MsgMerkleBlock succeeded with too many tx hashes " + + "when it should have failed") + return + } +}