/*
* Copyright by the original author or authors.
*
* 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 org.bitcoinj.net;
import com.google.common.collect.Lists;
import org.bitcoinj.core.BloomFilter;
import org.bitcoinj.core.PeerFilterProvider;
import com.google.common.collect.ImmutableList;
import java.util.LinkedList;
// This code is unit tested by the PeerGroup tests.
/**
* <p>A reusable object that will calculate, given a list of {@link org.bitcoinj.core.PeerFilterProvider}s, a merged
* {@link org.bitcoinj.core.BloomFilter} and earliest key time for all of them.
* Used by the {@link org.bitcoinj.core.PeerGroup} class internally.</p>
*
* <p>Thread safety: threading here can be complicated. Each filter provider is given a begin event, which may acquire
* a lock (and is guaranteed to receive an end event). This class is mostly thread unsafe and is meant to be used from a
* single thread only, PeerGroup ensures this by only accessing it from the dedicated PeerGroup thread. PeerGroup does
* not hold any locks whilst this object is used, relying on the single thread to prevent multiple filters being
* calculated in parallel, thus a filter provider can do things like make blocking calls into PeerGroup from a separate
* thread. However the bloomFilterFPRate property IS thread safe, for convenience.</p>
*/
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 volatile double vBloomFilterFPRate;
private int lastBloomFilterElementCount;
private BloomFilter lastFilter;
public FilterMerger(double bloomFilterFPRate) {
this.vBloomFilterFPRate = bloomFilterFPRate;
}
public static class Result {
public BloomFilter filter;
public long earliestKeyTimeSecs;
public boolean changed;
}
public Result calculate(ImmutableList<PeerFilterProvider> providers) {
LinkedList<PeerFilterProvider> begunProviders = Lists.newLinkedList();
try {
// All providers must be in a consistent, unchanging state because the filter is a merged one that's
// large enough for all providers elements: if a provider were to get more elements in the middle of the
// calculation, we might assert or calculate the filter wrongly. Most providers use a lock here but
// snapshotting required state is also a legitimate strategy.
for (PeerFilterProvider provider : providers) {
provider.beginBloomFilterCalculation();
begunProviders.add(provider);
}
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;
double fpRate = vBloomFilterFPRate;
BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, fpRate, bloomFilterTweak, bloomFlags);
for (PeerFilterProvider p : providers)
filter.merge(p.getBloomFilter(lastBloomFilterElementCount, fpRate, 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;
} finally {
for (PeerFilterProvider provider : begunProviders) {
provider.endBloomFilterCalculation();
}
}
}
public void setBloomFilterFPRate(double bloomFilterFPRate) {
this.vBloomFilterFPRate = bloomFilterFPRate;
}
public double getBloomFilterFPRate() {
return vBloomFilterFPRate;
}
public BloomFilter getLastFilter() {
return lastFilter;
}
}