/*
* Copyright 2012 Matt Corallo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.digitalcoin.core;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
/**
* <p>A Bloom filter is a probabilistic data structure which can be sent to another client so that it can avoid
* sending us transactions that aren't relevant to our set of keys. This allows for significantly more efficient
* use of available network bandwidth and CPU time.</p>
*
* <p>Because a Bloom filter is probabilistic, it has a configurable false positive rate. So the filter will sometimes
* match transactions that weren't inserted into it, but it will never fail to match transactions that were. This is
* a useful privacy feature - if you have spare bandwidth the false positive rate can be increased so the remote peer
* gets a noisy picture of what transactions are relevant to your wallet.</p>
*/
public class BloomFilter extends Message {
/** The BLOOM_UPDATE_* constants control when the bloom filter is auto-updated by the peer using
it as a filter, either never, for all outputs or only for pay-2-pubkey outputs (default) */
public enum BloomUpdate {
UPDATE_NONE, // 0
UPDATE_ALL, // 1
/** Only adds outpoints to the filter if the output is a pay-to-pubkey/pay-to-multisig script */
UPDATE_P2PUBKEY_ONLY //2
}
private byte[] data;
private long hashFuncs;
private long nTweak;
private byte nFlags;
// Same value as the reference client
// A filter of 20,000 items and a false positive rate of 0.1% or one of 10,000 items and 0.0001% is just under 36,000 bytes
private static final long MAX_FILTER_SIZE = 36000;
// There is little reason to ever have more hash functions than 50 given a limit of 36,000 bytes
private static final int MAX_HASH_FUNCS = 50;
/**
* Construct a BloomFilter by deserializing payloadBytes
*/
public BloomFilter(NetworkParameters params, byte[] payloadBytes) throws ProtocolException {
super(params, payloadBytes, 0);
}
/**
* Constructs a filter with the given parameters which is updated on pay2pubkey outputs only.
*/
public BloomFilter(int elements, double falsePositiveRate, long randomNonce) {
this(elements, falsePositiveRate, randomNonce, BloomUpdate.UPDATE_P2PUBKEY_ONLY);
}
/**
* <p>Constructs a new Bloom Filter which will provide approximately the given false positive
* rate when the given number of elements have been inserted.</p>
*
* <p>If the filter would otherwise be larger than the maximum allowed size, it will be
* automatically downsized to the maximum size.</p>
*
* <p>To check the theoretical false positive rate of a given filter, use {@link BloomFilter#getFalsePositiveRate(int)}</p>
*
* <p>The anonymity of which coins are yours to any peer which you send a BloomFilter to is
* controlled by the false positive rate.</p>
*
* <p>For reference, as of block 187,000, the total number of addresses used in the chain was roughly 4.5 million.</p>
*
* <p>Thus, if you use a false positive rate of 0.001 (0.1%), there will be, on average, 4,500 distinct public
* keys/addresses which will be thought to be yours by nodes which have your bloom filter, but which are not
* actually yours.</p>
*
* <p>Keep in mind that a remote node can do a pretty good job estimating the order of magnitude of the false positive
* rate of a given filter you provide it when considering the anonymity of a given filter.</p>
*
* <p>In order for filtered block download to function efficiently, the number of matched transactions in any given
* block should be less than (with some headroom) the maximum size of the MemoryPool used by the Peer
* doing the downloading (default is {@link MemoryPool#MAX_SIZE}). See the comment in processBlock(FilteredBlock)
* for more information on this restriction.</p>
*
* <p>randomNonce is a tweak for the hash function used to prevent some theoretical DoS attacks.
* It should be a random value, however secureness of the random value is of no great consequence.</p>
*
* <p>updateFlag is used to control filter behavior</p>
*/
public BloomFilter(int elements, double falsePositiveRate, long randomNonce, BloomUpdate updateFlag) {
// The following formulas were stolen from Wikipedia's page on Bloom Filters (with the addition of min(..., MAX_...))
// Size required for a given number of elements and false-positive rate
int size = Math.min((int)(-1 / (Math.pow(Math.log(2), 2)) * elements * Math.log(falsePositiveRate)),
(int)MAX_FILTER_SIZE * 8) / 8;
data = new byte[size <= 0 ? 1 : size];
// Optimal number of hash functions for a given filter size and element count.
hashFuncs = Math.min((int)(data.length * 8 / (double)elements * Math.log(2)), MAX_HASH_FUNCS);
this.nTweak = randomNonce;
this.nFlags = (byte)(0xff & updateFlag.ordinal());
}
/**
* Returns the theoretical false positive rate of this filter if were to contain the given number of elements.
*/
public double getFalsePositiveRate(int elements) {
return Math.pow(1 - Math.pow(Math.E, -1.0 * (hashFuncs * elements) / (data.length * 8)), hashFuncs);
}
@Override
public String toString() {
return "Bloom Filter of size " + data.length + " with " + hashFuncs + " hash functions.";
}
@Override
void parse() throws ProtocolException {
data = readByteArray();
if (data.length > MAX_FILTER_SIZE)
throw new ProtocolException ("Bloom filter out of size range.");
hashFuncs = readUint32();
if (hashFuncs > MAX_HASH_FUNCS)
throw new ProtocolException("Bloom filter hash function count out of range");
nTweak = readUint32();
nFlags = readBytes(1)[0];
length = cursor - offset;
}
/**
* Serializes this message to the provided stream. If you just want the raw bytes use digitalcoinSerialize().
*/
void digitalcoinSerializeToStream(OutputStream stream) throws IOException {
stream.write(new VarInt(data.length).encode());
stream.write(data);
Utils.uint32ToByteStreamLE(hashFuncs, stream);
Utils.uint32ToByteStreamLE(nTweak, stream);
stream.write(nFlags);
}
@Override
protected void parseLite() throws ProtocolException {
// Do nothing, lazy parsing isn't useful for bloom filters.
}
private int ROTL32 (int x, int r) {
return (x << r) | (x >>> (32 - r));
}
private int hash(int hashNum, byte[] object) {
// The following is MurmurHash3 (x86_32), see http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp
int h1 = (int)(hashNum * 0xFBA4C795L + nTweak);
final int c1 = 0xcc9e2d51;
final int c2 = 0x1b873593;
int numBlocks = (object.length / 4) * 4;
// body
for(int i = 0; i < numBlocks; i += 4) {
int k1 = (object[i] & 0xFF) |
((object[i+1] & 0xFF) << 8) |
((object[i+2] & 0xFF) << 16) |
((object[i+3] & 0xFF) << 24);
k1 *= c1;
k1 = ROTL32(k1,15);
k1 *= c2;
h1 ^= k1;
h1 = ROTL32(h1,13);
h1 = h1*5+0xe6546b64;
}
int k1 = 0;
switch(object.length & 3)
{
case 3:
k1 ^= (object[numBlocks + 2] & 0xff) << 16;
// Fall through.
case 2:
k1 ^= (object[numBlocks + 1] & 0xff) << 8;
// Fall through.
case 1:
k1 ^= (object[numBlocks] & 0xff);
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
// Fall through.
default:
// Do nothing.
break;
}
// finalization
h1 ^= object.length;
h1 ^= h1 >>> 16;
h1 *= 0x85ebca6b;
h1 ^= h1 >>> 13;
h1 *= 0xc2b2ae35;
h1 ^= h1 >>> 16;
return (int)((h1&0xFFFFFFFFL) % (data.length * 8));
}
/**
* Returns true if the given object matches the filter
* (either because it was inserted, or because we have a false-positive)
*/
public boolean contains(byte[] object) {
for (int i = 0; i < hashFuncs; i++) {
if (!Utils.checkBitLE(data, hash(i, object)))
return false;
}
return true;
}
/**
* Insert the given arbitrary data into the filter
*/
public void insert(byte[] object) {
for (int i = 0; i < hashFuncs; i++)
Utils.setBitLE(data, hash(i, object));
}
/**
* Copies filter into this.
* filter must have the same size, hash function count and nTweak or an exception will be thrown.
*/
public void merge(BloomFilter filter) {
Preconditions.checkArgument(filter.data.length == this.data.length &&
filter.hashFuncs == this.hashFuncs &&
filter.nTweak == this.nTweak);
for (int i = 0; i < data.length; i++)
this.data[i] |= filter.data[i];
}
@Override
public boolean equals(Object other) {
return other instanceof BloomFilter &&
((BloomFilter) other).hashFuncs == this.hashFuncs &&
((BloomFilter) other).nTweak == this.nTweak &&
Arrays.equals(((BloomFilter) other).data, this.data);
}
@Override
public int hashCode() {
return Objects.hashCode(hashFuncs, nTweak, Arrays.hashCode(data));
}
}