dcrd/gcs/gcs.go
2019-12-30 13:54:41 -06:00

552 lines
17 KiB
Go

// Copyright (c) 2016-2017 The btcsuite developers
// Copyright (c) 2016-2017 The Lightning Network Developers
// Copyright (c) 2018-2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package gcs
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"sort"
"sync"
"github.com/dchest/siphash"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/crypto/blake256"
"github.com/decred/dcrd/wire"
)
// modReduceV1 is the reduction method used in version 1 filters and simply
// consists of x mod N.
func modReduceV1(x, N uint64) uint64 {
return x % N
}
// chooseReduceFunc chooses which reduction method to use based on the filter
// version and returns the appropriate function to perform it.
func chooseReduceFunc(version uint16) func(uint64, uint64) uint64 {
if version == 1 {
return modReduceV1
}
return fastReduce
}
// KeySize is the size of the byte array required for key material for the
// SipHash keyed hash function.
const KeySize = 16
// uint64s implements sort.Interface for *[]uint64
type uint64s []uint64
func (s *uint64s) Len() int { return len(*s) }
func (s *uint64s) Less(i, j int) bool { return (*s)[i] < (*s)[j] }
func (s *uint64s) Swap(i, j int) { (*s)[i], (*s)[j] = (*s)[j], (*s)[i] }
// filter describes a versioned immutable filter that can be built from a set of
// data elements, serialized, deserialized, and queried in a thread-safe manner.
//
// It is used internally to implement the exported filter version types.
//
// See FilterV1 and FilterV2 for more details.
type filter struct {
version uint16
n uint32
b uint8
modulusNM uint64
filterNData []byte
filterData []byte // Slice into filterNData with raw filter bytes.
}
// newFilter builds a new GCS filter with the specified version and provided
// tunable parameters that contains every item of the passed data as a member of
// the set.
//
// B is the tunable bits parameter for constructing the filter that is used as
// the bin size in the underlying Golomb coding with a value of 2^B. The
// optimal value of B to minimize the size of the filter for a given false
// positive rate 1/M is floor(log_2(M) - 0.055256). The maximum allowed value
// for B is 32.
//
// M is the inverse of the target false positive rate for the filter. The
// optimal value of M to minimize the size of the filter for a given B is
// ceil(1.497137 * 2^B).
//
// key is a key used in the SipHash function used to hash each data element
// prior to inclusion in the filter. This helps thwart would be attackers
// attempting to choose elements that intentionally cause false positives.
//
// The general process for determining optimal parameters for B and M to
// minimize the size of the filter is to start with the desired false positive
// rate and calculate B per the aforementioned formula accordingly. Then, if
// the application permits the false positive rate to be varied, calculate the
// optimal value of M via the formula provided under the description of M.
//
// NOTE: Since this function must only be used internally, it will panic if
// called with a value of B greater than 32.
func newFilter(version uint16, B uint8, M uint64, key [KeySize]byte, data [][]byte) (*filter, error) {
if B > 32 {
panic(fmt.Sprintf("B value of %d is greater than max allowed 32", B))
}
switch version {
case 1, 2:
default:
panic(fmt.Sprintf("version %d filters are not supported", version))
}
// Note that some of the entries might end up hashing to duplicates and
// being removed below, so it is important to perform this check on the
// raw data to maintain consensus.
numEntries := uint64(len(data))
if numEntries > math.MaxInt32 {
str := fmt.Sprintf("unable to create filter with %d entries greater "+
"than max allowed %d", len(data), math.MaxInt32)
return nil, makeError(ErrNTooBig, str)
}
// Insert the hash of each data element into a slice while removing any
// duplicates in the data for filters after version 1.
k0 := binary.LittleEndian.Uint64(key[0:8])
k1 := binary.LittleEndian.Uint64(key[8:16])
values := make([]uint64, 0, numEntries)
switch {
case version == 1:
for _, d := range data {
v := siphash.Hash(k0, k1, d)
values = append(values, v)
}
case version > 1:
seen := make(map[uint64]struct{}, numEntries*2)
for _, d := range data {
if len(d) == 0 {
continue
}
v := siphash.Hash(k0, k1, d)
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
values = append(values, v)
}
}
numEntries = uint64(len(values))
// Create the filter object and insert metadata.
modBMask := uint64(1<<B) - 1
f := filter{
version: version,
n: uint32(numEntries),
b: B,
modulusNM: numEntries * M,
}
// Nothing to do for an empty filter.
if len(values) == 0 {
return &f, nil
}
// Reduce the hash of each data element to the range [0,N*M) and sort it.
reduceFn := chooseReduceFunc(version)
for i, v := range values {
values[i] = reduceFn(v, f.modulusNM)
}
sort.Sort((*uint64s)(&values))
// Every entry will have f.b bits for the remainder portion and a quotient
// that is expected to be 1 on average with an exponentially decreasing
// probability for each subsequent value with reasonably optimal parameters.
// A quotient of 1 takes 2 bits in unary to encode and a quotient of 2 takes
// 3 bits. Since the first two terms dominate, a reasonable expected size
// in bytes is:
// (NB + 2N/2 + 3N/2) / 8
var b bitWriter
sizeHint := (numEntries*uint64(f.b) + numEntries + 3*numEntries>>1) >> 3
b.bytes = make([]byte, 0, sizeHint)
// Write the sorted list of values into the filter bitstream using Golomb
// coding.
var quotient, prevValue, remainder uint64
for _, v := range values {
delta := v - prevValue
prevValue = v
// Calculate the remainder of the difference between this value and the
// previous when dividing by 2^B.
//
// r = d % 2^B
remainder = delta & modBMask
// Calculate the quotient of the difference between this value and the
// previous when dividing by 2^B.
//
// q = floor(d / 2^B)
quotient = (delta - remainder) >> f.b
// Write the quotient into the bitstream in unary. The average value
// will be around 1 for reasonably optimal parameters (which is encoded
// as 2 bits - 0b10).
for quotient > 0 {
b.writeOne()
quotient--
}
b.writeZero()
// Write the remainder into the bitstream as a big-endian integer with
// B bits. Note that Golomb coding typically uses truncated binary
// encoding in order to support arbitrary bin sizes, however, since 2^B
// is necessarily fixed to a power of 2, it is equivalent to a regular
// binary code.
b.writeNBits(remainder, uint(f.b))
}
// Save the filter data internally as n + filter bytes along with the raw
// filter data as a slice into it.
switch version {
case 1:
ndata := make([]byte, 4+len(b.bytes))
binary.BigEndian.PutUint32(ndata, f.n)
copy(ndata[4:], b.bytes)
f.filterNData = ndata
f.filterData = ndata[4:]
case 2:
var buf bytes.Buffer
nSize := wire.VarIntSerializeSize(uint64(f.n))
buf.Grow(nSize + len(b.bytes))
// The errors are ignored here since they can't realistically fail due
// to writing into an allocated buffer.
_ = wire.WriteVarInt(&buf, 0, uint64(f.n))
_, _ = buf.Write(b.bytes)
f.filterNData = buf.Bytes()
f.filterData = f.filterNData[nSize:]
}
return &f, nil
}
// Bytes returns the serialized format of the GCS filter which includes N, but
// does not include other parameters such as the false positive rate or the key.
func (f *filter) Bytes() []byte {
return f.filterNData
}
// N returns the size of the data set used to build the filter.
func (f *filter) N() uint32 {
return f.n
}
// readFullUint64 reads a value represented by the sum of a unary multiple of
// the Golomb coding bin size (2^B) and a big-endian B-bit remainder.
func (f *filter) readFullUint64(b *bitReader) (uint64, error) {
v, err := b.readUnary()
if err != nil {
return 0, err
}
rem, err := b.readNBits(uint(f.b))
if err != nil {
return 0, err
}
// Add the multiple and the remainder.
return v<<f.b + rem, nil
}
// 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.filterData) == 0 || len(data) == 0 {
return false
}
// Hash the search term with the same parameters as the filter.
reduceFn := chooseReduceFunc(f.version)
k0 := binary.LittleEndian.Uint64(key[0:8])
k1 := binary.LittleEndian.Uint64(key[8:16])
term := siphash.Hash(k0, k1, data)
term = reduceFn(term, f.modulusNM)
// Go through the search filter and look for the desired value.
b := newBitReader(f.filterData)
var lastValue uint64
for lastValue <= term {
// Read the difference between previous and new value from
// bitstream.
value, err := f.readFullUint64(&b)
if err != nil {
return false
}
// Add the previous value to it.
value += lastValue
if value == term {
return true
}
lastValue = value
}
return false
}
// matchPool pools allocations for match data.
var matchPool sync.Pool
// MatchAny checks whether any []byte value is likely (within collision
// 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 {
// An empty filter or empty data can't possibly match anything.
if len(f.filterData) == 0 || len(data) == 0 {
return false
}
// Create an uncompressed filter of the search values.
reduceFn := chooseReduceFunc(f.version)
var values *[]uint64
if v := matchPool.Get(); v != nil {
values = v.(*[]uint64)
*values = (*values)[:0]
} else {
vs := make([]uint64, 0, len(data))
values = &vs
}
defer matchPool.Put(values)
k0 := binary.LittleEndian.Uint64(key[0:8])
k1 := binary.LittleEndian.Uint64(key[8:16])
for _, d := range data {
if len(d) == 0 {
continue
}
v := siphash.Hash(k0, k1, d)
v = reduceFn(v, f.modulusNM)
*values = append(*values, v)
}
if len(*values) == 0 {
return false
}
sort.Sort((*uint64s)(values))
// Zip down the filters, comparing values until we either run out of
// values to compare in one of the filters or we reach a matching
// value.
b := newBitReader(f.filterData)
searchSize := len(data)
var searchIdx int
var filterVal uint64
nextFilterVal:
for i := uint32(0); i < f.n; i++ {
// Read the next item to compare from the filter.
delta, err := f.readFullUint64(&b)
if err != nil {
return false
}
filterVal += delta
// Iterate through the values to search until either a match is found
// or the search value exceeds the current filter value.
for ; searchIdx < searchSize; searchIdx++ {
searchVal := (*values)[searchIdx]
if searchVal == filterVal {
return true
}
// Move to the next filter item once the current search value
// exceeds it.
if searchVal > filterVal {
continue nextFilterVal
}
}
// Exit early when there are no more values to search for.
break
}
return false
}
// 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{}
}
return chainhash.Hash(blake256.Sum256(f.filterNData))
}
// FilterV1 describes an immutable filter that can be built from a set of data
// elements, serialized, deserialized, and queried in a thread-safe manner. The
// serialized form is compressed as a Golomb Coded Set (GCS) along with the
// number of members of the set. The hash function used is SipHash, a keyed
// function. The key used in building the filter is required in order to match
// filter values and is not included in the serialized form.
type FilterV1 struct {
filter
}
// P returns the filter's collision probability as a negative power of 2. For
// example, a collision probability of 1 / 2^20 is represented as 20.
func (f *FilterV1) P() uint8 {
return f.b
}
// NewFilterV1 builds a new version 1 GCS filter with a collision probability of
// 1 / 2^P for the given key and data.
func NewFilterV1(P uint8, key [KeySize]byte, data [][]byte) (*FilterV1, error) {
// Basic sanity check.
if P > 32 {
str := fmt.Sprintf("P value of %d is greater than max allowed 32", P)
return nil, makeError(ErrPTooBig, str)
}
filter, err := newFilter(1, P, 1<<P, key, data)
if err != nil {
return nil, err
}
return &FilterV1{filter: *filter}, nil
}
// FromBytesV1 deserializes a version 1 GCS filter from a known P and serialized
// filter as returned by Bytes().
func FromBytesV1(P uint8, d []byte) (*FilterV1, error) {
// Basic sanity check.
if P > 32 {
str := fmt.Sprintf("P value of %d is greater than max allowed 32", P)
return nil, makeError(ErrPTooBig, str)
}
var n uint32
var filterData []byte
if len(d) >= 4 {
n = binary.BigEndian.Uint32(d[:4])
filterData = d[4:]
} else if len(d) < 4 && len(d) != 0 {
str := "number of items serialization missing"
return nil, makeError(ErrMisserialized, str)
}
f := filter{
version: 1,
n: n,
b: P,
modulusNM: uint64(n) * uint64(1<<P),
filterNData: d,
filterData: filterData,
}
return &FilterV1{filter: f}, nil
}
// FilterV2 describes an immutable filter that can be built from a set of data
// elements, serialized, deserialized, and queried in a thread-safe manner. The
// serialized form is compressed as a Golomb Coded Set (GCS) along with the
// number of members of the set. The hash function used is SipHash, a keyed
// function. The key used in building the filter is required in order to match
// filter values and is not included in the serialized form.
//
// Version 2 filters differ from version 1 filters in four ways:
//
// 1) Support for independently specifying the false positive rate and Golomb
// coding bin size which allows minimizing the filter size
// 2) A faster (incompatible with version 1) reduction function
// 3) A more compact serialization for the number of members in the set
// 4) Deduplication of all hash collisions prior to reducing and serializing the
// deltas
type FilterV2 struct {
filter
}
// B returns the tunable bits parameter that was used to construct the filter.
// It represents the bin size in the underlying Golomb coding with a value of
// 2^B.
func (f *FilterV2) B() uint8 {
return f.b
}
// NewFilterV2 builds a new GCS filter with the provided tunable parameters that
// contains every item of the passed data as a member of the set.
//
// B is the tunable bits parameter for constructing the filter that is used as
// the bin size in the underlying Golomb coding with a value of 2^B. The
// optimal value of B to minimize the size of the filter for a given false
// positive rate 1/M is floor(log_2(M) - 0.055256). The maximum allowed value
// for B is 32. An error will be returned for larger values.
//
// M is the inverse of the target false positive rate for the filter. The
// optimal value of M to minimize the size of the filter for a given B is
// ceil(1.497137 * 2^B).
//
// key is a key used in the SipHash function used to hash each data element
// prior to inclusion in the filter. This helps thwart would be attackers
// attempting to choose elements that intentionally cause false positives.
//
// The general process for determining optimal parameters for B and M to
// minimize the size of the filter is to start with the desired false positive
// rate and calculate B per the aforementioned formula accordingly. Then, if
// the application permits the false positive rate to be varied, calculate the
// optimal value of M via the formula provided under the description of M.
func NewFilterV2(B uint8, M uint64, key [KeySize]byte, data [][]byte) (*FilterV2, error) {
// Basic sanity check.
if B > 32 {
str := fmt.Sprintf("B value of %d is greater than max allowed 32", B)
return nil, makeError(ErrBTooBig, str)
}
filter, err := newFilter(2, B, M, key, data)
if err != nil {
return nil, err
}
return &FilterV2{filter: *filter}, nil
}
// FromBytesV2 deserializes a version 2 GCS filter from a known B, M, and
// serialized filter as returned by Bytes().
func FromBytesV2(B uint8, M uint64, d []byte) (*FilterV2, error) {
if B > 32 {
str := fmt.Sprintf("B value of %d is greater than max allowed 32", B)
return nil, makeError(ErrBTooBig, str)
}
var n uint64
var filterData []byte
if len(d) > 0 {
var err error
n, err = wire.ReadVarInt(bytes.NewReader(d), 0)
if err != nil {
str := fmt.Sprintf("failed to read number of filter items: %v", err)
return nil, makeError(ErrMisserialized, str)
}
filterData = d[wire.VarIntSerializeSize(n):]
}
f := filter{
version: 2,
n: uint32(n),
b: B,
modulusNM: n * M,
filterNData: d,
filterData: filterData,
}
return &FilterV2{filter: f}, nil
}
// MakeHeaderForFilter makes a filter chain header for a filter, given the
// filter and the previous filter chain header.
func MakeHeaderForFilter(filter *FilterV1, prevHeader *chainhash.Hash) chainhash.Hash {
// In the buffer we created above we'll compute hash || prevHash as an
// intermediate value.
var filterTip [2 * chainhash.HashSize]byte
filterHash := filter.Hash()
copy(filterTip[:], filterHash[:])
copy(filterTip[chainhash.HashSize:], prevHeader[:])
// The final filter hash is the blake256 of the hash computed above.
return chainhash.Hash(blake256.Sum256(filterTip[:]))
}