From 468f3287c2fb952c0d9cc73d4c590489d796d37f Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 19 Aug 2019 12:37:38 -0500 Subject: [PATCH] gcs: Support empty filters. This adds support for empty filters versus being an error along with a full set of tests to ensure the empty filter works as intended. It is part of the onging process to cleanup and improve the gcs module to the quality level required by consensus code for ultimate inclusion in header commitments. --- blockchain/indexers/cfindex.go | 4 +-- gcs/gcs.go | 27 ++++++++++++----- gcs/gcs_test.go | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) 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) + } +}