diff --git a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java index cdbb8978..f30be902 100644 --- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java +++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java @@ -19,6 +19,7 @@ package com.google.bitcoin.core; import com.google.bitcoin.net.BlockingClientManager; import com.google.bitcoin.net.ClientConnectionManager; +import com.google.bitcoin.net.FilterMerger; import com.google.bitcoin.net.NioClientManager; import com.google.bitcoin.net.discovery.PeerDiscovery; import com.google.bitcoin.net.discovery.PeerDiscoveryException; @@ -29,6 +30,7 @@ import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.Threading; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -138,7 +140,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac @Override public void onBlocksDownloaded(Peer peer, Block block, int blocksLeft) { double rate = checkNotNull(chain).getFalsePositiveRate(); - if (rate > bloomFilterFPRate * MAX_FP_RATE_INCREASE) { + if (rate > bloomFilterMerger.getBloomFilterFPRate() * MAX_FP_RATE_INCREASE) { log.info("Force update Bloom filter due to high false positive rate"); recalculateFastCatchupAndFilter(FilterRecalculateMode.FORCE_SEND); } @@ -240,8 +242,6 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac // Visible for testing PeerEventListener startupListener = new PeerStartupListener(); - // A bloom filter generated from all connected wallets that is given to new peers - private BloomFilter bloomFilter; /** *

A reasonable default for the bloom filter false positive rate on mainnet. FP rates are values between 0.0 and 1.0 * where 1.0 is "all transactions" i.e. 100%.

@@ -252,11 +252,9 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac public static final double DEFAULT_BLOOM_FILTER_FP_RATE = 0.0005; /** Maximum increase in FP rate before forced refresh of the bloom filter */ public static final double MAX_FP_RATE_INCREASE = 2.0f; - // The false positive rate for bloomFilter - private double bloomFilterFPRate = DEFAULT_BLOOM_FILTER_FP_RATE; - // We use a constant tweak to avoid giving up privacy when we regenerate our filter with new keys - private final long bloomFilterTweak = (long) (Math.random() * Long.MAX_VALUE); - private int lastBloomFilterElementCount; + // An object that calculates bloom filters given a list of filter providers, whilst tracking some state useful + // for privacy purposes. + private FilterMerger bloomFilterMerger; /** The default timeout between when a connection attempt begins and version message exchange completes */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 5000; @@ -357,6 +355,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac peerDiscoverers = new CopyOnWriteArraySet(); peerEventListeners = new CopyOnWriteArrayList>(); runningBroadcasts = Collections.synchronizedSet(new HashSet()); + bloomFilterMerger = new FilterMerger(DEFAULT_BLOOM_FILTER_FP_RATE); } /** @@ -864,54 +863,24 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac if (chain != null && chain.shouldVerifyTransactions()) return; log.info("Recalculating filter in mode {}", mode); - long earliestKeyTimeSecs = Long.MAX_VALUE; - int elements = 0; - boolean requiresUpdateAll = false; - for (PeerFilterProvider p : peerFilterProviders) { - earliestKeyTimeSecs = Math.min(earliestKeyTimeSecs, p.getEarliestKeyCreationTime()); - elements += p.getBloomFilterElementCount(); - requiresUpdateAll = requiresUpdateAll || p.isRequiringUpdateAllBloomFilter(); + FilterMerger.Result result = bloomFilterMerger.calculate(ImmutableList.copyOf(peerFilterProviders)); + boolean send; + switch (mode) { + case SEND_IF_CHANGED: send = result.changed; break; + case DONT_SEND: send = false; break; + case FORCE_SEND: send = true; break; + default: throw new UnsupportedOperationException(); } - - if (elements > 0) { - // We stair-step our element count so that we avoid creating a filter with different parameters - // as much as possible as that results in a loss of privacy. - // The constant 100 here is somewhat arbitrary, but makes sense for small to medium wallets - - // it will likely mean we never need to create a filter with different parameters. - lastBloomFilterElementCount = elements > lastBloomFilterElementCount ? elements + 100 : lastBloomFilterElementCount; - BloomFilter.BloomUpdate bloomFlags = - requiresUpdateAll ? BloomFilter.BloomUpdate.UPDATE_ALL : BloomFilter.BloomUpdate.UPDATE_P2PUBKEY_ONLY; - BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak, bloomFlags); - for (PeerFilterProvider p : peerFilterProviders) - filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak)); - - boolean changed = !filter.equals(bloomFilter); - boolean send = false; - - bloomFilter = filter; - - switch (mode) { - case SEND_IF_CHANGED: send = changed; break; - case DONT_SEND: send = false; break; - case FORCE_SEND: send = true; break; - } - - if (send) { - for (Peer peer : peers) - peer.setBloomFilter(filter); - // Reset the false positive estimate so that we don't send a flood of filter updates - // if the estimate temporarily overshoots our threshold. - if (chain != null) - chain.resetFalsePositiveEstimate(); - } + if (send) { + for (Peer peer : peers) + peer.setBloomFilter(result.filter); + // Reset the false positive estimate so that we don't send a flood of filter updates + // if the estimate temporarily overshoots our threshold. + if (chain != null) + chain.resetFalsePositiveEstimate(); } - // Now adjust the earliest key time backwards by a week to handle the case of clock drift. This can occur - // both in block header timestamps and if the users clock was out of sync when the key was first created - // (to within a small amount of tolerance). - earliestKeyTimeSecs -= 86400 * 7; - // Do this last so that bloomFilter is already set when it gets called. - setFastCatchupTimeSecs(earliestKeyTimeSecs); + setFastCatchupTimeSecs(result.earliestKeyTimeSecs); } finally { lock.unlock(); } @@ -929,7 +898,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac public void setBloomFilterFalsePositiveRate(double bloomFilterFPRate) { lock.lock(); try { - this.bloomFilterFPRate = bloomFilterFPRate; + bloomFilterMerger.setBloomFilterFPRate(bloomFilterFPRate); recalculateFastCatchupAndFilter(FilterRecalculateMode.SEND_IF_CHANGED); } finally { lock.unlock(); @@ -1070,7 +1039,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac // Give the peer a filter that can be used to probabilistically drop transactions that // aren't relevant to our wallet. We may still receive some false positives, which is // OK because it helps improve wallet privacy. Old nodes will just ignore the message. - if (bloomFilter != null) peer.setBloomFilter(bloomFilter); + if (bloomFilterMerger.getLastFilter() != null) peer.setBloomFilter(bloomFilterMerger.getLastFilter()); // Link the peer to the memory pool so broadcast transactions have their confidence levels updated. peer.setDownloadData(false); // TODO: The peer should calculate the fast catchup time from the added wallets here. @@ -1183,7 +1152,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac if (downloadListener != null) peer.addEventListener(downloadListener, Threading.SAME_THREAD); downloadPeer.setDownloadData(true); - downloadPeer.setDownloadParameters(fastCatchupTimeSecs, bloomFilter != null); + downloadPeer.setDownloadParameters(fastCatchupTimeSecs, bloomFilterMerger.getLastFilter() != null); } } finally { lock.unlock(); @@ -1211,7 +1180,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac Preconditions.checkState(chain == null || !chain.shouldVerifyTransactions(), "Fast catchup is incompatible with fully verifying"); fastCatchupTimeSecs = secondsSinceEpoch; if (downloadPeer != null) { - downloadPeer.setDownloadParameters(secondsSinceEpoch, bloomFilter != null); + downloadPeer.setDownloadParameters(secondsSinceEpoch, bloomFilterMerger.getLastFilter() != null); } } finally { lock.unlock(); diff --git a/core/src/main/java/com/google/bitcoin/net/FilterMerger.java b/core/src/main/java/com/google/bitcoin/net/FilterMerger.java new file mode 100644 index 00000000..a4f4bdce --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/net/FilterMerger.java @@ -0,0 +1,80 @@ +package com.google.bitcoin.net; + +import com.google.bitcoin.core.BloomFilter; +import com.google.bitcoin.core.PeerFilterProvider; +import com.google.common.collect.ImmutableList; + +// This code is unit tested by the PeerGroup tests. + +/** + *

A reusable object that will calculate, given a list of {@link com.google.bitcoin.core.PeerFilterProvider}s, a merged + * {@link com.google.bitcoin.core.BloomFilter} and earliest key time for all of them. + * Used by the {@link com.google.bitcoin.core.PeerGroup} class internally.

+ * + *

Thread safety: this class tracks the element count of the last filter it calculated and so must be synchronised + * externally or used from only one thread. It will acquire a lock on each filter in turn before performing the + * calculation because the providers may be mutated in other threads in parallel, but global consistency is required + * to produce a merged filter.

+ */ +public class FilterMerger { + // We use a constant tweak to avoid giving up privacy when we regenerate our filter with new keys + private final long bloomFilterTweak = (long) (Math.random() * Long.MAX_VALUE); + private double bloomFilterFPRate; + private int lastBloomFilterElementCount; + private BloomFilter lastFilter; + + public FilterMerger(double bloomFilterFPRate) { + this.bloomFilterFPRate = bloomFilterFPRate; + } + + public static class Result { + public BloomFilter filter; + public long earliestKeyTimeSecs; + public boolean changed; + } + + public Result calculate(ImmutableList providers) { + Result result = new Result(); + result.earliestKeyTimeSecs = Long.MAX_VALUE; + int elements = 0; + boolean requiresUpdateAll = false; + for (PeerFilterProvider p : providers) { + result.earliestKeyTimeSecs = Math.min(result.earliestKeyTimeSecs, p.getEarliestKeyCreationTime()); + elements += p.getBloomFilterElementCount(); + requiresUpdateAll = requiresUpdateAll || p.isRequiringUpdateAllBloomFilter(); + } + + if (elements > 0) { + // We stair-step our element count so that we avoid creating a filter with different parameters + // as much as possible as that results in a loss of privacy. + // The constant 100 here is somewhat arbitrary, but makes sense for small to medium wallets - + // it will likely mean we never need to create a filter with different parameters. + lastBloomFilterElementCount = elements > lastBloomFilterElementCount ? elements + 100 : lastBloomFilterElementCount; + BloomFilter.BloomUpdate bloomFlags = + requiresUpdateAll ? BloomFilter.BloomUpdate.UPDATE_ALL : BloomFilter.BloomUpdate.UPDATE_P2PUBKEY_ONLY; + BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak, bloomFlags); + for (PeerFilterProvider p : providers) + filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak)); + + result.changed = !filter.equals(lastFilter); + result.filter = lastFilter = filter; + } + // Now adjust the earliest key time backwards by a week to handle the case of clock drift. This can occur + // both in block header timestamps and if the users clock was out of sync when the key was first created + // (to within a small amount of tolerance). + result.earliestKeyTimeSecs -= 86400 * 7; + return result; + } + + public void setBloomFilterFPRate(double bloomFilterFPRate) { + this.bloomFilterFPRate = bloomFilterFPRate; + } + + public double getBloomFilterFPRate() { + return bloomFilterFPRate; + } + + public BloomFilter getLastFilter() { + return lastFilter; + } +}