diff --git a/blockchain/indexers/cfindex.go b/blockchain/indexers/cfindex.go index cf3ee11f..ac3a79dc 100644 --- a/blockchain/indexers/cfindex.go +++ b/blockchain/indexers/cfindex.go @@ -215,7 +215,7 @@ func storeFilter(dbTx database.Tx, block *dcrutil.Block, f *gcs.Filter, filterTy // every passed block. This is part of the Indexer interface. func (idx *CFIndex) ConnectBlock(dbTx database.Tx, block, parent *dcrutil.Block, view *blockchain.UtxoViewpoint) error { f, err := blockcf.Regular(block.MsgBlock()) - if err != nil && err != gcs.ErrNoData { + if err != nil { return err } @@ -225,7 +225,7 @@ func (idx *CFIndex) ConnectBlock(dbTx database.Tx, block, parent *dcrutil.Block, } f, err = blockcf.Extended(block.MsgBlock()) - if err != nil && err != gcs.ErrNoData { + if err != nil { return err } diff --git a/gcs/gcs.go b/gcs/gcs.go index 1dd3ea02..6515a5dd 100644 --- a/gcs/gcs.go +++ b/gcs/gcs.go @@ -28,9 +28,6 @@ var ( // collision probability. ErrPTooBig = errors.New("P is too large") - // ErrNoData signifies that an empty slice was passed. - ErrNoData = errors.New("no data provided") - // ErrMisserialized signifies a filter was misserialized and is missing the // N and/or P parameters of a serialized filter. ErrMisserialized = errors.New("misserialized filter") @@ -68,9 +65,6 @@ func NewFilter(P uint8, key [KeySize]byte, data [][]byte) (*Filter, error) { // Some initial parameter checks: make sure we have data from which to // build the filter, and make sure our parameters will fit the hash // function we're using. - if len(data) == 0 { - return nil, ErrNoData - } if len(data) > math.MaxInt32 { return nil, ErrNTooBig } @@ -87,6 +81,11 @@ func NewFilter(P uint8, key [KeySize]byte, data [][]byte) (*Filter, error) { modulusNP: uint64(len(data)) * modP, } + // Nothing to do for an empty filter. + if len(data) == 0 { + return &f, nil + } + // Allocate filter data. values := make([]uint64, 0, len(data)) @@ -179,6 +178,9 @@ func FromNBytes(P uint8, d []byte) (*Filter, error) { // Bytes returns the serialized format of the GCS filter, which does not // include N or P (returned by separate methods) or the key used by SipHash. func (f *Filter) Bytes() []byte { + if len(f.filterNData) == 0 { + return nil + } return f.filterNData[4:] } @@ -202,6 +204,11 @@ func (f *Filter) N() uint32 { // Match checks whether a []byte value is likely (within collision probability) // to be a member of the set represented by the filter. func (f *Filter) Match(key [KeySize]byte, data []byte) bool { + // An empty filter or empty data can't possibly match anything. + if len(f.filterNData) == 0 || len(data) == 0 { + return false + } + // Create a filter bitstream. b := newBitReader(f.filterNData[4:]) @@ -239,7 +246,8 @@ var matchPool sync.Pool // probability) to be a member of the set represented by the filter faster than // calling Match() for each value individually. func (f *Filter) MatchAny(key [KeySize]byte, data [][]byte) bool { - if len(data) == 0 { + // An empty filter or empty data can't possibly match anything. + if len(f.filterNData) == 0 || len(data) == 0 { return false } @@ -318,6 +326,11 @@ func (f *Filter) readFullUint64(b *bitReader) (uint64, error) { // Hash returns the BLAKE256 hash of the filter. func (f *Filter) Hash() chainhash.Hash { + // Empty filters have a hash of all zeroes. + if len(f.filterNData) == 0 { + return chainhash.Hash{} + } + h := blake256.New() h.Write(f.filterNData) diff --git a/gcs/gcs_test.go b/gcs/gcs_test.go index 9926f988..bdd65f92 100644 --- a/gcs/gcs_test.go +++ b/gcs/gcs_test.go @@ -11,6 +11,8 @@ import ( "encoding/binary" "math/rand" "testing" + + "github.com/decred/dcrd/chaincfg/chainhash" ) var ( @@ -171,3 +173,54 @@ func TestGCSFilterMatchAny(t *testing.T) { t.Fatal("Filter didn't match any when it should have!") } } + +// TestEmptyFilter ensures that empty filters are handled properly. +func TestEmptyFilter(t *testing.T) { + // Ensure an empty filter can be constructed without error. + f, err := NewFilter(P, key, nil) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Ensure empty filters do not have any serialization. + gotBytes := f.Bytes() + if gotBytes != nil { + t.Fatalf("filter bytes not empty -- got %x", gotBytes) + } + gotBytes = f.NBytes() + if gotBytes != nil { + t.Fatalf("filter nbytes not empty -- got %x", gotBytes) + } + + // Ensure the hash of empty filters is all zeroes. + gotHash := f.Hash() + expectedHash := chainhash.Hash{} + if gotHash != expectedHash { + t.Fatalf("unexpected filter hash -- got %s, want %s", gotHash, + expectedHash) + } + + // Ensure an empty filter does not match empty data or arbitrary data. + if f.Match(key, nil) { + t.Fatal("unexpected match of nil data") + } + if f.Match(key, []byte("test")) { + t.Fatal("unexpected match of data") + } + if f.MatchAny(key, nil) { + t.Fatal("unexpected match of nil data") + } + if f.MatchAny(key, [][]byte{[]byte("test")}) { + t.Fatal("unexpected match of data") + } + + // Ensure empty filter returns correct parameters. + gotN := f.N() + if gotN != 0 { + t.Fatalf("unexpected N -- got %d, want %d", gotN, 0) + } + gotP := f.P() + if gotP != P { + t.Fatalf("unexpected P -- got %d, want %d", gotP, P) + } +}