0ecd90b8d6
In this commit, we decrease the default fp rate to 19, or 1/2^19. We do this as recent analysis by sipa on the bitcoin dev mailing list has shown that optimally, we can use a value of 2^19 for the fp rate, while use n=1.497137*2^P rather than n directly. As a result, we can shrink the filter size by quite a bit, while still maintaining a sane false positive value.
441 lines
13 KiB
Go
441 lines
13 KiB
Go
// 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"
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil/gcs"
|
|
)
|
|
|
|
const (
|
|
// DefaultP is the default collision probability (2^-19)
|
|
DefaultP = 19
|
|
|
|
// DefaultM is the default value used for the hash range.
|
|
DefaultM uint64 = 784931
|
|
)
|
|
|
|
// GCSBuilder is a utility class that makes building GCS filters convenient.
|
|
type GCSBuilder struct {
|
|
p uint8
|
|
|
|
m uint64
|
|
|
|
key [gcs.KeySize]byte
|
|
|
|
// data is a set of entries represented as strings. This is done to
|
|
// deduplicate items as they are added.
|
|
data map[string]struct{}
|
|
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.LittleEndian.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
|
|
}
|
|
|
|
// SetM sets the filter's modulous value after calling Builder().
|
|
func (b *GCSBuilder) SetM(m uint64) *GCSBuilder {
|
|
// Do nothing if the builder's already errored out.
|
|
if b.err != nil {
|
|
return b
|
|
}
|
|
|
|
// Basic sanity check.
|
|
if m > uint64(math.MaxUint32) {
|
|
b.err = gcs.ErrPTooBig
|
|
return b
|
|
}
|
|
|
|
b.m = m
|
|
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, Preallocate 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 b.data == nil {
|
|
b.data = make(map[string]struct{}, 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[string(data)] = struct{}{}
|
|
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.
|
|
func (b *GCSBuilder) AddScript(script []byte) *GCSBuilder {
|
|
// Do nothing if the builder's already errored out.
|
|
if b.err != nil {
|
|
return b
|
|
}
|
|
|
|
// Ignore errors and add pushed data, if any
|
|
data, _ := txscript.PushedData(script)
|
|
if len(data) == 0 {
|
|
return b
|
|
}
|
|
|
|
return b.AddEntries(data)
|
|
}
|
|
|
|
// AddWitness adds each item of the passed filter stack to the filter, and then
|
|
// adds each item as a script.
|
|
func (b *GCSBuilder) AddWitness(witness wire.TxWitness) *GCSBuilder {
|
|
// Do nothing if the builder's already errored out.
|
|
if b.err != nil {
|
|
return b
|
|
}
|
|
|
|
return b.AddEntries(witness)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// We'll ensure that all the parmaters we need to actually build the
|
|
// filter properly are set.
|
|
if b.p == 0 {
|
|
return nil, fmt.Errorf("p value is not set, cannot build")
|
|
}
|
|
if b.m == 0 {
|
|
return nil, fmt.Errorf("m value is not set, cannot build")
|
|
}
|
|
|
|
dataSlice := make([][]byte, 0, len(b.data))
|
|
for item := range b.data {
|
|
dataSlice = append(dataSlice, []byte(item))
|
|
}
|
|
|
|
return gcs.BuildGCSFilter(b.p, b.m, b.key, dataSlice)
|
|
}
|
|
|
|
// WithKeyPNM creates a GCSBuilder with specified key and the passed
|
|
// probability, modulus and estimated filter size.
|
|
func WithKeyPNM(key [gcs.KeySize]byte, p uint8, n uint32, m uint64) *GCSBuilder {
|
|
b := GCSBuilder{}
|
|
return b.SetKey(key).SetP(p).SetM(m).Preallocate(n)
|
|
}
|
|
|
|
// WithKeyPM 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 WithKeyPM(key [gcs.KeySize]byte, p uint8, m uint64) *GCSBuilder {
|
|
return WithKeyPNM(key, p, 0, m)
|
|
}
|
|
|
|
// WithKey creates a GCSBuilder with specified key. Probability is set to 19
|
|
// (2^-19 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 WithKeyPNM(key, DefaultP, 0, DefaultM)
|
|
}
|
|
|
|
// WithKeyHashPNM creates a GCSBuilder with key derived from the specified
|
|
// chainhash.Hash and the passed probability and estimated filter size.
|
|
func WithKeyHashPNM(keyHash *chainhash.Hash, p uint8, n uint32,
|
|
m uint64) *GCSBuilder {
|
|
|
|
return WithKeyPNM(DeriveKey(keyHash), p, n, m)
|
|
}
|
|
|
|
// WithKeyHashPM 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 WithKeyHashPM(keyHash *chainhash.Hash, p uint8, m uint64) *GCSBuilder {
|
|
return WithKeyHashPNM(keyHash, p, 0, m)
|
|
}
|
|
|
|
// 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 WithKeyHashPNM(keyHash, DefaultP, 0, DefaultM)
|
|
}
|
|
|
|
// WithRandomKeyPNM creates a GCSBuilder with a cryptographically random key and
|
|
// the passed probability and estimated filter size.
|
|
func WithRandomKeyPNM(p uint8, n uint32, m uint64) *GCSBuilder {
|
|
key, err := RandomKey()
|
|
if err != nil {
|
|
b := GCSBuilder{err: err}
|
|
return &b
|
|
}
|
|
return WithKeyPNM(key, p, n, m)
|
|
}
|
|
|
|
// WithRandomKeyPM 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 WithRandomKeyPM(p uint8, m uint64) *GCSBuilder {
|
|
return WithRandomKeyPNM(p, 0, m)
|
|
}
|
|
|
|
// 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 WithRandomKeyPNM(DefaultP, 0, DefaultM)
|
|
}
|
|
|
|
// BuildBasicFilter builds a basic GCS filter from a block. A basic GCS filter
|
|
// will contain all the previous outpoints spent within a block, as well as the
|
|
// data pushes within all the outputs created within a block.
|
|
func BuildBasicFilter(block *wire.MsgBlock) (*gcs.Filter, error) {
|
|
blockHash := block.BlockHash()
|
|
b := WithKeyHash(&blockHash)
|
|
|
|
// If the filter had an issue with the specified key, then we force it
|
|
// to bubble up here by calling the Key() function.
|
|
_, err := b.Key()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// In order to build a basic filter, we'll range over the entire block,
|
|
// adding the outpoint data as well as the data pushes within the
|
|
// pkScript.
|
|
for i, tx := range block.Transactions {
|
|
// Skip the inputs for the coinbase transaction
|
|
if i != 0 {
|
|
// Each each txin, we'll add a serialized version of
|
|
// the txid:index to the filters data slices.
|
|
for _, txIn := range tx.TxIn {
|
|
b.AddOutPoint(txIn.PreviousOutPoint)
|
|
}
|
|
}
|
|
|
|
// For each output in a transaction, we'll add each of the
|
|
// individual data pushes within the script.
|
|
for _, txOut := range tx.TxOut {
|
|
b.AddEntry(txOut.PkScript)
|
|
}
|
|
}
|
|
|
|
return b.Build()
|
|
}
|
|
|
|
// BuildExtFilter builds an extended GCS filter from a block. An extended
|
|
// filter supplements a regular basic filter by include all the _witness_ data
|
|
// found within a block. This includes all the data pushes within any signature
|
|
// scripts as well as each element of an input's witness stack. Additionally,
|
|
// the _hashes_ of each transaction are also inserted into the filter.
|
|
func BuildExtFilter(block *wire.MsgBlock) (*gcs.Filter, error) {
|
|
blockHash := block.BlockHash()
|
|
b := WithKeyHash(&blockHash)
|
|
|
|
// If the filter had an issue with the specified key, then we force it
|
|
// to bubble up here by calling the Key() function.
|
|
_, err := b.Key()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// In order to build an extended filter, we add the hash of each
|
|
// transaction as well as each piece of witness data included in both
|
|
// the sigScript and the witness stack of an input.
|
|
for i, tx := range block.Transactions {
|
|
// Skip the inputs for the coinbase transaction
|
|
if i != 0 {
|
|
// Next, for each input, we'll add the sigScript (if
|
|
// it's present), and also the witness stack (if it's
|
|
// present)
|
|
for _, txIn := range tx.TxIn {
|
|
if txIn.SignatureScript != nil {
|
|
b.AddScript(txIn.SignatureScript)
|
|
}
|
|
|
|
if len(txIn.Witness) != 0 {
|
|
b.AddWitness(txIn.Witness)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return b.Build()
|
|
}
|
|
|
|
// GetFilterHash returns the double-SHA256 of the filter.
|
|
func GetFilterHash(filter *gcs.Filter) (chainhash.Hash, error) {
|
|
filterData, err := filter.NBytes()
|
|
if err != nil {
|
|
return chainhash.Hash{}, err
|
|
}
|
|
|
|
return chainhash.DoubleHashH(filterData), nil
|
|
}
|
|
|
|
// MakeHeaderForFilter makes a filter chain header for a filter, given the
|
|
// filter and the previous filter chain header.
|
|
func MakeHeaderForFilter(filter *gcs.Filter, prevHeader chainhash.Hash) (chainhash.Hash, error) {
|
|
filterTip := make([]byte, 2*chainhash.HashSize)
|
|
filterHash, err := GetFilterHash(filter)
|
|
if err != nil {
|
|
return chainhash.Hash{}, err
|
|
}
|
|
|
|
// In the buffer we created above we'll compute hash || prevHash as an
|
|
// intermediate value.
|
|
copy(filterTip, filterHash[:])
|
|
copy(filterTip[chainhash.HashSize:], prevHeader[:])
|
|
|
|
// The final filter hash is the double-sha256 of the hash computed
|
|
// above.
|
|
return chainhash.DoubleHashH(filterTip), nil
|
|
}
|