diff --git a/gcs/builder/builder.go b/gcs/builder/builder.go new file mode 100644 index 0000000..d2bb8a1 --- /dev/null +++ b/gcs/builder/builder.go @@ -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) +} diff --git a/gcs/builder/builder_test.go b/gcs/builder/builder_test.go new file mode 100644 index 0000000..838e194 --- /dev/null +++ b/gcs/builder/builder_test.go @@ -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) + } +}