// Copyright (c) 2016-2017 The btcsuite developers // Copyright (c) 2016-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 gcs import ( "bytes" "fmt" "io" "sort" "github.com/aead/siphash" "github.com/btcsuite/btcd/wire" "github.com/kkdai/bstream" ) // Inspired by https://github.com/rasky/gcs var ( // ErrNTooBig signifies that the filter can't handle N items. ErrNTooBig = fmt.Errorf("N is too big to fit in uint32") // ErrPTooBig signifies that the filter can't handle `1/2**P` // collision probability. ErrPTooBig = fmt.Errorf("P is too big to fit in uint32") ) const ( // KeySize is the size of the byte array required for key material for // the SipHash keyed hash function. KeySize = 16 // varIntProtoVer is the protocol version to use for serializing N as a // VarInt. varIntProtoVer uint32 = 0 ) // fastReduction calculates a mapping that's more ore less equivalent to: x mod // N. However, instead of using a mod operation, which using a non-power of two // will lead to slowness on many processors due to unnecessary division, we // instead use a "multiply-and-shift" trick which eliminates all divisions, // described in: // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ // // * v * N >> log_2(N) // // In our case, using 64-bit integers, log_2 is 64. As most processors don't // support 128-bit arithmetic natively, we'll be super portable and unfold the // operation into several operations with 64-bit arithmetic. As inputs, we the // number to reduce, and our modulus N divided into its high 32-bits and lower // 32-bits. func fastReduction(v, nHi, nLo uint64) uint64 { // First, we'll spit the item we need to reduce into its higher and // lower bits. vhi := v >> 32 vlo := uint64(uint32(v)) // Then, we distribute multiplication over each part. vnphi := vhi * nHi vnpmid := vhi * nLo npvmid := nHi * vlo vnplo := vlo * nLo // We calculate the carry bit. carry := (uint64(uint32(vnpmid)) + uint64(uint32(npvmid)) + (vnplo >> 32)) >> 32 // Last, we add the high bits, the middle bits, and the carry. v = vnphi + (vnpmid >> 32) + (npvmid >> 32) + carry return v } // Filter 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), but does not // include N or P to allow the user to encode the metadata separately if // necessary. 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 Filter struct { n uint32 p uint8 modulusNP uint64 filterData []byte } // BuildGCSFilter builds a new GCS filter with the collision probability of // `1/(2**P)`, key `key`, and including every `[]byte` in `data` as a member of // the set. func BuildGCSFilter(P uint8, M uint64, 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 uint64(len(data)) >= (1 << 32) { return nil, ErrNTooBig } if P > 32 { return nil, ErrPTooBig } // Create the filter object and insert metadata. f := Filter{ n: uint32(len(data)), p: P, } // First we'll compute the value of m, which is the modulus we use // within our finite field. We want to compute: mScalar * 2^P. We use // math.Round in order to round the value up, rather than down. f.modulusNP = uint64(f.n) * M // Shortcut if the filter is empty. if f.n == 0 { return &f, nil } // Build the filter. values := make(uint64Slice, 0, len(data)) b := bstream.NewBStreamWriter(0) // Insert the hash (fast-ranged over a space of N*P) of each data // element into a slice and sort the slice. This can be greatly // optimized with native 128-bit multiplication, but we're going to be // fully portable for now. // // First, we cache the high and low bits of modulusNP for the // multiplication of 2 64-bit integers into a 128-bit integer. nphi := f.modulusNP >> 32 nplo := uint64(uint32(f.modulusNP)) for _, d := range data { // For each datum, we assign the initial hash to a uint64. v := siphash.Sum64(d, &key) v = fastReduction(v, nphi, nplo) values = append(values, v) } sort.Sort(values) // Write the sorted list of values into the filter bitstream, // compressing it using Golomb coding. var value, lastValue, remainder uint64 for _, v := range values { // Calculate the difference between this value and the last, // modulo P. remainder = (v - lastValue) & ((uint64(1) << f.p) - 1) // Calculate the difference between this value and the last, // divided by P. value = (v - lastValue - remainder) >> f.p lastValue = v // Write the P multiple into the bitstream in unary; the // average should be around 1 (2 bits - 0b10). for value > 0 { b.WriteBit(true) value-- } b.WriteBit(false) // Write the remainder as a big-endian integer with enough bits // to represent the appropriate collision probability. b.WriteBits(remainder, int(f.p)) } // Copy the bitstream into the filter object and return the object. f.filterData = b.Bytes() return &f, nil } // FromBytes deserializes a GCS filter from a known N, P, and serialized filter // as returned by Bytes(). func FromBytes(N uint32, P uint8, M uint64, d []byte) (*Filter, error) { // Basic sanity check. if P > 32 { return nil, ErrPTooBig } // Create the filter object and insert metadata. f := &Filter{ n: N, p: P, } // First we'll compute the value of m, which is the modulus we use // within our finite field. We want to compute: mScalar * 2^P. We use // math.Round in order to round the value up, rather than down. f.modulusNP = uint64(f.n) * M // Copy the filter. f.filterData = make([]byte, len(d)) copy(f.filterData, d) return f, nil } // FromNBytes deserializes a GCS filter from a known P, and serialized N and // filter as returned by NBytes(). func FromNBytes(P uint8, M uint64, d []byte) (*Filter, error) { buffer := bytes.NewBuffer(d) N, err := wire.ReadVarInt(buffer, varIntProtoVer) if err != nil { return nil, err } if N >= (1 << 32) { return nil, ErrNTooBig } return FromBytes(uint32(N), P, M, buffer.Bytes()) } // 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, error) { filterData := make([]byte, len(f.filterData)) copy(filterData, f.filterData) return filterData, nil } // NBytes returns the serialized format of the GCS filter with N, which does // not include P (returned by a separate method) or the key used by SipHash. func (f *Filter) NBytes() ([]byte, error) { var buffer bytes.Buffer buffer.Grow(wire.VarIntSerializeSize(uint64(f.n)) + len(f.filterData)) err := wire.WriteVarInt(&buffer, varIntProtoVer, uint64(f.n)) if err != nil { return nil, err } _, err = buffer.Write(f.filterData) if err != nil { return nil, err } return buffer.Bytes(), nil } // PBytes returns the serialized format of the GCS filter with P, which does // not include N (returned by a separate method) or the key used by SipHash. func (f *Filter) PBytes() ([]byte, error) { filterData := make([]byte, len(f.filterData)+1) filterData[0] = f.p copy(filterData[1:], f.filterData) return filterData, nil } // NPBytes returns the serialized format of the GCS filter with N and P, which // does not include the key used by SipHash. func (f *Filter) NPBytes() ([]byte, error) { var buffer bytes.Buffer buffer.Grow(wire.VarIntSerializeSize(uint64(f.n)) + 1 + len(f.filterData)) err := wire.WriteVarInt(&buffer, varIntProtoVer, uint64(f.n)) if err != nil { return nil, err } err = buffer.WriteByte(f.p) if err != nil { return nil, err } _, err = buffer.Write(f.filterData) if err != nil { return nil, err } return buffer.Bytes(), nil } // P returns the filter's collision probability as a negative power of 2 (that // is, a collision probability of `1/2**20` is represented as 20). func (f *Filter) P() uint8 { return f.p } // N returns the size of the data set used to build the filter. func (f *Filter) N() uint32 { return f.n } // 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, error) { // Create a filter bitstream. filterData, err := f.Bytes() if err != nil { return false, err } b := bstream.NewBStreamReader(filterData) // We take the high and low bits of modulusNP for the multiplication // of 2 64-bit integers into a 128-bit integer. nphi := f.modulusNP >> 32 nplo := uint64(uint32(f.modulusNP)) // Then we hash our search term with the same parameters as the filter. term := siphash.Sum64(data, &key) term = fastReduction(term, nphi, nplo) // Go through the search filter and look for the desired value. var lastValue uint64 for lastValue < term { // Read the difference between previous and new value from // bitstream. value, err := f.readFullUint64(b) if err != nil { if err == io.EOF { return false, nil } return false, err } // Add the previous value to it. value += lastValue if value == term { return true, nil } lastValue = value } return false, nil } // MatchAny returns 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, error) { // TODO(conner): add real heuristics to query optimization switch { case len(data) >= int(f.N()/2): return f.HashMatchAny(key, data) default: return f.ZipMatchAny(key, data) } } // ZipMatchAny returns 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. // // NOTE: This method should outperform HashMatchAny when the number of query // entries is smaller than the number of filter entries. func (f *Filter) ZipMatchAny(key [KeySize]byte, data [][]byte) (bool, error) { // Basic anity check. if len(data) == 0 { return false, nil } // Create a filter bitstream. filterData, err := f.Bytes() if err != nil { return false, err } b := bstream.NewBStreamReader(filterData) // Create an uncompressed filter of the search values. values := make(uint64Slice, 0, len(data)) // First, we cache the high and low bits of modulusNP for the // multiplication of 2 64-bit integers into a 128-bit integer. nphi := f.modulusNP >> 32 nplo := uint64(uint32(f.modulusNP)) for _, d := range data { // For each datum, we assign the initial hash to a uint64. v := siphash.Sum64(d, &key) // We'll then reduce the value down to the range of our // modulus. v = fastReduction(v, nphi, nplo) values = append(values, v) } sort.Sort(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. var lastValue1, lastValue2 uint64 lastValue2 = values[0] i := 1 for lastValue1 != lastValue2 { // Check which filter to advance to make sure we're comparing // the right values. switch { case lastValue1 > lastValue2: // Advance filter created from search terms or return // false if we're at the end because nothing matched. if i < len(values) { lastValue2 = values[i] i++ } else { return false, nil } case lastValue2 > lastValue1: // Advance filter we're searching or return false if // we're at the end because nothing matched. value, err := f.readFullUint64(b) if err != nil { if err == io.EOF { return false, nil } return false, err } lastValue1 += value } } // If we've made it this far, an element matched between filters so we // return true. return true, nil } // HashMatchAny returns 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. // // NOTE: This method should outperform MatchAny if the number of query entries // approaches the number of filter entries, len(data) >= f.N(). func (f *Filter) HashMatchAny(key [KeySize]byte, data [][]byte) (bool, error) { // Basic sanity check. if len(data) == 0 { return false, nil } // Create a filter bitstream. filterData, err := f.Bytes() if err != nil { return false, err } b := bstream.NewBStreamReader(filterData) var ( values = make(map[uint32]struct{}, f.N()) lastValue uint64 ) // First, decompress the filter and construct an index of the keys // contained within the filter. Index construction terminates after all // values have been read from the bitstream. for { // Read the next diff value from the filter, add it to the // last value, and set the new value in the index. value, err := f.readFullUint64(b) if err == nil { lastValue += value values[uint32(lastValue)] = struct{}{} continue } else if err == io.EOF { break } return false, err } // We cache the high and low bits of modulusNP for the multiplication of // 2 64-bit integers into a 128-bit integer. nphi := f.modulusNP >> 32 nplo := uint64(uint32(f.modulusNP)) // Finally, run through the provided data items, querying the index to // determine if the filter contains any elements of interest. for _, d := range data { // For each datum, we assign the initial hash to // a uint64. v := siphash.Sum64(d, &key) // We'll then reduce the value down to the range // of our modulus. v = fastReduction(v, nphi, nplo) if _, ok := values[uint32(v)]; !ok { continue } return true, nil } return false, nil } // readFullUint64 reads a value represented by the sum of a unary multiple of // the filter's P modulus (`2**P`) and a big-endian P-bit remainder. func (f *Filter) readFullUint64(b *bstream.BStream) (uint64, error) { var quotient uint64 // Count the 1s until we reach a 0. c, err := b.ReadBit() if err != nil { return 0, err } for c { quotient++ c, err = b.ReadBit() if err != nil { return 0, err } } // Read P bits. remainder, err := b.ReadBits(int(f.p)) if err != nil { return 0, err } // Add the multiple and the remainder. v := (quotient << f.p) + remainder return v, nil }