Add filter builder and some tests
This commit is contained in:
parent
53c8e22157
commit
6654eb61e4
2 changed files with 380 additions and 0 deletions
275
gcs/builder/builder.go
Normal file
275
gcs/builder/builder.go
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
// Copyright (c) 2017 The btcsuite developers
|
||||||
|
// Copyright (c) 2017 The Lightning Network Developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package builder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil/gcs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultP = 20
|
||||||
|
|
||||||
|
// GCSBuilder is a utility class that makes building GCS filters convenient.
|
||||||
|
type GCSBuilder struct {
|
||||||
|
p uint8
|
||||||
|
key [gcs.KeySize]byte
|
||||||
|
data [][]byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomKey is a utility function that returns a cryptographically random
|
||||||
|
// [gcs.KeySize]byte usable as a key for a GCS filter.
|
||||||
|
func RandomKey() ([gcs.KeySize]byte, error) {
|
||||||
|
var key [gcs.KeySize]byte
|
||||||
|
|
||||||
|
// Read a byte slice from rand.Reader.
|
||||||
|
randKey := make([]byte, gcs.KeySize)
|
||||||
|
_, err := rand.Read(randKey)
|
||||||
|
|
||||||
|
// This shouldn't happen unless the user is on a system that doesn't
|
||||||
|
// have a system CSPRNG. OK to panic in this case.
|
||||||
|
if err != nil {
|
||||||
|
return key, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the byte slice to a [gcs.KeySize]byte array and return it.
|
||||||
|
copy(key[:], randKey[:])
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveKey is a utility function that derives a key from a chainhash.Hash by
|
||||||
|
// truncating the bytes of the hash to the appopriate key size.
|
||||||
|
func DeriveKey(keyHash *chainhash.Hash) [gcs.KeySize]byte {
|
||||||
|
var key [gcs.KeySize]byte
|
||||||
|
copy(key[:], keyHash.CloneBytes()[:])
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutPointToFilterEntry is a utility function that derives a filter entry from
|
||||||
|
// a wire.OutPoint in a standardized way for use with both building and querying
|
||||||
|
// filters.
|
||||||
|
func OutPointToFilterEntry(outpoint wire.OutPoint) []byte {
|
||||||
|
// Size of the hash plus size of int32 index
|
||||||
|
data := make([]byte, chainhash.HashSize+4)
|
||||||
|
copy(data[:], outpoint.Hash.CloneBytes()[:])
|
||||||
|
binary.BigEndian.PutUint32(data[chainhash.HashSize:], outpoint.Index)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key retrieves the key with which the builder will build a filter. This is
|
||||||
|
// useful if the builder is created with a random initial key.
|
||||||
|
func (b *GCSBuilder) Key() ([gcs.KeySize]byte, error) {
|
||||||
|
// Do nothing if the builder's errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return [gcs.KeySize]byte{}, b.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the key with which the builder will build a filter to the passed
|
||||||
|
// [gcs.KeySize]byte.
|
||||||
|
func (b *GCSBuilder) SetKey(key [gcs.KeySize]byte) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(b.key[:], key[:])
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyFromHash sets the key with which the builder will build a filter to a
|
||||||
|
// key derived from the passed chainhash.Hash using DeriveKey().
|
||||||
|
func (b *GCSBuilder) SetKeyFromHash(keyHash *chainhash.Hash) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.SetKey(DeriveKey(keyHash))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetP sets the filter's probability after calling Builder().
|
||||||
|
func (b *GCSBuilder) SetP(p uint8) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic sanity check.
|
||||||
|
if p > 32 {
|
||||||
|
b.err = gcs.ErrPTooBig
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
b.p = p
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preallocate sets the estimated filter size after calling Builder() to reduce
|
||||||
|
// the probability of memory reallocations. If the builder has already had data
|
||||||
|
// added to it, SetN has no effect.
|
||||||
|
func (b *GCSBuilder) Preallocate(n uint32) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.data) == 0 {
|
||||||
|
b.data = make([][]byte, 0, n)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEntry adds a []byte to the list of entries to be included in the GCS
|
||||||
|
// filter when it's built.
|
||||||
|
func (b *GCSBuilder) AddEntry(data []byte) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
b.data = append(b.data, data)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEntries adds all the []byte entries in a [][]byte to the list of entries
|
||||||
|
// to be included in the GCS filter when it's built.
|
||||||
|
func (b *GCSBuilder) AddEntries(data [][]byte) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range data {
|
||||||
|
b.AddEntry(entry)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOutPoint adds a wire.OutPoint to the list of entries to be included in the
|
||||||
|
// GCS filter when it's built.
|
||||||
|
func (b *GCSBuilder) AddOutPoint(outpoint wire.OutPoint) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.AddEntry(OutPointToFilterEntry(outpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHash adds a chainhash.Hash to the list of entries to be included in the
|
||||||
|
// GCS filter when it's built.
|
||||||
|
func (b *GCSBuilder) AddHash(hash *chainhash.Hash) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.AddEntry(hash.CloneBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddScript adds all the data pushed in the script serialized as the passed
|
||||||
|
// []byte to the list of entries to be included in the GCS filter when it's
|
||||||
|
// built. T
|
||||||
|
func (b *GCSBuilder) AddScript(script []byte) *GCSBuilder {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := txscript.PushedData(script)
|
||||||
|
if err != nil {
|
||||||
|
b.err = err
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return b.AddEntries(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns a function which builds a GCS filter with the given parameters
|
||||||
|
// and data.
|
||||||
|
func (b *GCSBuilder) Build() (*gcs.Filter, error) {
|
||||||
|
// Do nothing if the builder's already errored out.
|
||||||
|
if b.err != nil {
|
||||||
|
return nil, b.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcs.BuildGCSFilter(b.p, b.key, b.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyPN creates a GCSBuilder with specified key and the passed
|
||||||
|
// probability and estimated filter size.
|
||||||
|
func WithKeyPN(key [gcs.KeySize]byte, p uint8, n uint32) *GCSBuilder {
|
||||||
|
b := GCSBuilder{}
|
||||||
|
return b.SetKey(key).SetP(p).Preallocate(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyP creates a GCSBuilder with specified key and the passed
|
||||||
|
// probability. Estimated filter size is set to zero, which means more
|
||||||
|
// reallocations are done when building the filter.
|
||||||
|
func WithKeyP(key [gcs.KeySize]byte, p uint8) *GCSBuilder {
|
||||||
|
return WithKeyPN(key, p, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKey creates a GCSBuilder with specified key. Probability is set to
|
||||||
|
// 20 (2^-20 collision probability). Estimated filter size is set to zero, which
|
||||||
|
// means more reallocations are done when building the filter.
|
||||||
|
func WithKey(key [gcs.KeySize]byte) *GCSBuilder {
|
||||||
|
return WithKeyPN(key, DefaultP, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyHashPN creates a GCSBuilder with key derived from the specified
|
||||||
|
// chainhash.Hash and the passed probability and estimated filter size.
|
||||||
|
func WithKeyHashPN(keyHash *chainhash.Hash, p uint8, n uint32) *GCSBuilder {
|
||||||
|
return WithKeyPN(DeriveKey(keyHash), p, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyHashP creates a GCSBuilder with key derived from the specified
|
||||||
|
// chainhash.Hash and the passed probability. Estimated filter size is set to
|
||||||
|
// zero, which means more reallocations are done when building the filter.
|
||||||
|
func WithKeyHashP(keyHash *chainhash.Hash, p uint8) *GCSBuilder {
|
||||||
|
return WithKeyHashPN(keyHash, p, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeyHash creates a GCSBuilder with key derived from the specified
|
||||||
|
// chainhash.Hash. Probability is set to 20 (2^-20 collision probability).
|
||||||
|
// Estimated filter size is set to zero, which means more reallocations are
|
||||||
|
// done when building the filter.
|
||||||
|
func WithKeyHash(keyHash *chainhash.Hash) *GCSBuilder {
|
||||||
|
return WithKeyHashPN(keyHash, DefaultP, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRandomKeyPN creates a GCSBuilder with a cryptographically random
|
||||||
|
// key and the passed probability and estimated filter size.
|
||||||
|
func WithRandomKeyPN(p uint8, n uint32) *GCSBuilder {
|
||||||
|
key, err := RandomKey()
|
||||||
|
if err != nil {
|
||||||
|
b := GCSBuilder{err: err}
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
return WithKeyPN(key, p, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRandomKeyP creates a GCSBuilder with a cryptographically random
|
||||||
|
// key and the passed probability. Estimated filter size is set to zero, which
|
||||||
|
// means more reallocations are done when building the filter.
|
||||||
|
func WithRandomKeyP(p uint8) *GCSBuilder {
|
||||||
|
return WithRandomKeyPN(p, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRandomKey creates a GCSBuilder with a cryptographically random
|
||||||
|
// key. Probability is set to 20 (2^-20 collision probability). Estimated
|
||||||
|
// filter size is set to zero, which means more reallocations are done when
|
||||||
|
// building the filter.
|
||||||
|
func WithRandomKey() *GCSBuilder {
|
||||||
|
return WithRandomKeyPN(DefaultP, 0)
|
||||||
|
}
|
105
gcs/builder/builder_test.go
Normal file
105
gcs/builder/builder_test.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) 2017 The btcsuite developers
|
||||||
|
// Copyright (c) 2017 The Lightning Network Developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package builder_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcutil/gcs/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// No need to allocate an err variable in every test
|
||||||
|
err error
|
||||||
|
|
||||||
|
// List of values for building a filter
|
||||||
|
contents = [][]byte{
|
||||||
|
[]byte("Alex"),
|
||||||
|
[]byte("Bob"),
|
||||||
|
[]byte("Charlie"),
|
||||||
|
[]byte("Dick"),
|
||||||
|
[]byte("Ed"),
|
||||||
|
[]byte("Frank"),
|
||||||
|
[]byte("George"),
|
||||||
|
[]byte("Harry"),
|
||||||
|
[]byte("Ilya"),
|
||||||
|
[]byte("John"),
|
||||||
|
[]byte("Kevin"),
|
||||||
|
[]byte("Larry"),
|
||||||
|
[]byte("Michael"),
|
||||||
|
[]byte("Nate"),
|
||||||
|
[]byte("Owen"),
|
||||||
|
[]byte("Paul"),
|
||||||
|
[]byte("Quentin"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of values for querying a filter using MatchAny()
|
||||||
|
contents2 = [][]byte{
|
||||||
|
[]byte("Alice"),
|
||||||
|
[]byte("Betty"),
|
||||||
|
[]byte("Charmaine"),
|
||||||
|
[]byte("Donna"),
|
||||||
|
[]byte("Edith"),
|
||||||
|
[]byte("Faina"),
|
||||||
|
[]byte("Georgia"),
|
||||||
|
[]byte("Hannah"),
|
||||||
|
[]byte("Ilsbeth"),
|
||||||
|
[]byte("Jennifer"),
|
||||||
|
[]byte("Kayla"),
|
||||||
|
[]byte("Lena"),
|
||||||
|
[]byte("Michelle"),
|
||||||
|
[]byte("Natalie"),
|
||||||
|
[]byte("Ophelia"),
|
||||||
|
[]byte("Peggy"),
|
||||||
|
[]byte("Queenie"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestUseBlockHash tests using a block hash as a filter key.
|
||||||
|
func TestUseBlockHash(t *testing.T) {
|
||||||
|
// Block hash #448710, pretty high difficulty.
|
||||||
|
hash, err := chainhash.NewHashFromStr("000000000000000000496d7ff9bd2c96154a8d64260e8b3b411e625712abb14c")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Hash from string failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Builder with a key hash and check that the key is derived
|
||||||
|
// correctly.
|
||||||
|
b := builder.WithKeyHash(hash)
|
||||||
|
key, err := b.Key()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Builder instantiation with key hash failed: %s",
|
||||||
|
err.Error())
|
||||||
|
}
|
||||||
|
testKey := [16]byte{0x4c, 0xb1, 0xab, 0x12, 0x57, 0x62, 0x1e, 0x41,
|
||||||
|
0x3b, 0x8b, 0x0e, 0x26, 0x64, 0x8d, 0x4a, 0x15}
|
||||||
|
if key != testKey {
|
||||||
|
t.Fatalf("Key not derived correctly from key hash:\n%s\n%s",
|
||||||
|
hex.EncodeToString(key[:]),
|
||||||
|
hex.EncodeToString(testKey[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a filter and test matches.
|
||||||
|
b.AddEntries(contents)
|
||||||
|
f, err := b.Build()
|
||||||
|
match, err := f.Match(key, []byte("Nate"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Filter match failed: %s", err)
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatal("Filter didn't match when it should have!")
|
||||||
|
}
|
||||||
|
match, err = f.Match(key, []byte("weks"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Filter match failed: %s", err)
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
t.Logf("False positive match, should be 1 in 2**%d!",
|
||||||
|
builder.DefaultP)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue