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.
This commit is contained in:
Dave Collins 2019-08-19 12:37:38 -05:00
parent feb4ff55e0
commit 468f3287c2
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
3 changed files with 75 additions and 9 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
}