diff --git a/core/src/main/java/com/google/bitcoin/core/PeerFilterProvider.java b/core/src/main/java/com/google/bitcoin/core/PeerFilterProvider.java new file mode 100644 index 00000000..4354db1e --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/core/PeerFilterProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013 Google Inc. + * + * 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.bitcoin.core; + +/** + * An interface which provides the information required to properly filter data downloaded from Peers. + * Note that an implementer is responsible for calling {@link PeerGroup#recalculateFastCatchupAndFilter()} whenever a + * change occurs which effects the data provided via this interface. + */ +public interface PeerFilterProvider { + /** + * Returns the earliest timestamp (seconds since epoch) for which full/bloom-filtered blocks must be downloaded. + * Blocks with timestamps before this time will only have headers downloaded. 0 requires that all blocks be + * downloaded, and thus this should default to {@link System#currentTimeMillis()}/1000. + */ + public long getEarliestKeyCreationTime(); + + /** + * Gets the number of elements that will be added to a bloom filter returned by + * {@link PeerFilterProvider#getBloomFilter(int, double, long)} + */ + public int getBloomFilterElementCount(); + + /** + * Gets a bloom filter that contains all the necessary elements for the listener to receive relevant transactions. + * Default value should be an empty bloom filter with the given size, falsePositiveRate, and nTweak. + */ + public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak); +} 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 d1d902ff..9180ce0f 100644 --- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java +++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java @@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; +import java.math.BigInteger; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -113,6 +114,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca private final AbstractBlockChain chain; @GuardedBy("lock") private long fastCatchupTimeSecs; private final CopyOnWriteArrayList wallets; + private final CopyOnWriteArrayList peerFilterProviders; // This event listener is added to every peer. It's here so when we announce transactions via an "inv", every // peer can fetch them. @@ -126,15 +128,12 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca private ClientBootstrap bootstrap; private int minBroadcastConnections = 0; private AbstractWalletEventListener walletEventListener = new AbstractWalletEventListener() { - @Override - public void onKeysAdded(Wallet wallet, List keys) { - lock.lock(); - try { - recalculateFastCatchupAndFilter(); - } finally { - lock.unlock(); - } + private void onChanged() { + recalculateFastCatchupAndFilter(); } + @Override public void onKeysAdded(Wallet wallet, List keys) { onChanged(); } + @Override public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); } + @Override public void onCoinsSent(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); } }; private class PeerStartupListener implements Peer.PeerLifecycleListener { @@ -205,6 +204,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca this.chain = chain; this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds(); this.wallets = new CopyOnWriteArrayList(); + this.peerFilterProviders = new CopyOnWriteArrayList(); // This default sentinel value will be overridden by one of two actions: // - adding a peer discovery source sets it to the default @@ -402,7 +402,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca // Note that the default here means that no tx invs will be received if no wallet is ever added lock.lock(); try { - ver.relayTxesBeforeFilter = chain != null && chain.shouldVerifyTransactions() && wallets.size() > 0; + ver.relayTxesBeforeFilter = chain != null && chain.shouldVerifyTransactions() && peerFilterProviders.size() > 0; } finally { lock.unlock(); } @@ -613,13 +613,34 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca Preconditions.checkNotNull(wallet); Preconditions.checkState(!wallets.contains(wallet)); wallets.add(wallet); + announcePendingWalletTransactions(Collections.singletonList(wallet), peers); + wallet.addEventListener(walletEventListener); // TODO: Run this in the current peer thread. + + addPeerFilterProvider(wallet); + } finally { + lock.unlock(); + } + } + + /** + *

Link the given PeerFilterProvider to this PeerGroup. DO NOT use this for Wallets, use + * {@link PeerGroup#addWallet(Wallet)} instead.

+ * + *

Note that this should be done before chain download commences because if you add a listener with keys earlier + * than the current chain head, the relevant parts of the chain won't be redownloaded for you.

+ */ + public void addPeerFilterProvider(PeerFilterProvider provider) { + lock.lock(); + try { + Preconditions.checkNotNull(provider); + Preconditions.checkState(!peerFilterProviders.contains(provider)); + peerFilterProviders.add(provider); // Don't bother downloading block bodies before the oldest keys in all our wallets. Make sure we recalculate // if a key is added. Of course, by then we may have downloaded the chain already. Ideally adding keys would // automatically rewind the block chain and redownload the blocks to find transactions relevant to those keys, // all transparently and in the background. But we are a long way from that yet. - wallet.addEventListener(walletEventListener); // TODO: Run this in the current peer thread. recalculateFastCatchupAndFilter(); updateVersionMessageRelayTxesBeforeFilter(getVersionMessage()); } finally { @@ -632,40 +653,51 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca */ public void removeWallet(Wallet wallet) { wallets.remove(checkNotNull(wallet)); + peerFilterProviders.remove(wallet); wallet.removeEventListener(walletEventListener); } - private void recalculateFastCatchupAndFilter() { - checkState(lock.isLocked()); - // Fully verifying mode doesn't use this optimization (it can't as it needs to see all transactions). - if (chain != null && chain.shouldVerifyTransactions()) - return; - long earliestKeyTime = Long.MAX_VALUE; - int elements = 0; - for (Wallet w : wallets) { - earliestKeyTime = Math.min(earliestKeyTime, w.getEarliestKeyCreationTime()); - elements += w.getBloomFilterElementCount(); - } + /** + * Recalculates the bloom filter given to peers as well as the timestamp after which full blocks are downloaded + * (instead of only headers). + */ + public void recalculateFastCatchupAndFilter() { + lock.lock(); + try { + // Fully verifying mode doesn't use this optimization (it can't as it needs to see all transactions). + if (chain != null && chain.shouldVerifyTransactions()) + return; + long earliestKeyTime = Long.MAX_VALUE; + int elements = 0; + for (PeerFilterProvider p : peerFilterProviders) { + earliestKeyTime = Math.min(earliestKeyTime, p.getEarliestKeyCreationTime()); + elements += p.getBloomFilterElementCount(); + } - 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 filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak); - for (Wallet w : wallets) - filter.merge(w.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak)); - bloomFilter = filter; - for (Peer peer : peers) - try { - peer.setBloomFilter(filter); - } catch (IOException e) { - throw new RuntimeException(e); + 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 filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak); + for (PeerFilterProvider p : peerFilterProviders) + filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak)); + if (!filter.equals(bloomFilter)) { + bloomFilter = filter; + for (Peer peer : peers) + try { + peer.setBloomFilter(filter); + } catch (IOException e) { + throw new RuntimeException(e); + } } + } + // Do this last so that bloomFilter is already set when it gets called. + setFastCatchupTimeSecs(earliestKeyTime); + } finally { + lock.unlock(); } - // Do this last so that bloomFilter is already set when it gets called. - setFastCatchupTimeSecs(earliestKeyTime); } /** diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index c40c2839..f2e50102 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -79,7 +79,7 @@ import static com.google.common.base.Preconditions.*; * {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.core.Wallet.AutosaveEventListener)} * for more information about this.

*/ -public class Wallet implements Serializable, BlockChainListener { +public class Wallet implements Serializable, BlockChainListener, PeerFilterProvider { private static final Logger log = LoggerFactory.getLogger(Wallet.class); private static final long serialVersionUID = 2L; @@ -2527,6 +2527,7 @@ public class Wallet implements Serializable, BlockChainListener { * * If there are no keys in the wallet, the current time is returned. */ + @Override public long getEarliestKeyCreationTime() { lock.lock(); try { @@ -2876,9 +2877,7 @@ public class Wallet implements Serializable, BlockChainListener { return description; } - /** - * Gets the number of elements that will be added to a bloom filter returned by getBloomFilter - */ + @Override public int getBloomFilterElementCount() { int size = getKeychainSize() * 2; for (Transaction tx : getTransactions(false)) { @@ -2912,6 +2911,7 @@ public class Wallet implements Serializable, BlockChainListener { * * See the docs for {@link BloomFilter(int, double)} for a brief explanation of anonymity when using bloom filters. */ + @Override public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) { BloomFilter filter = new BloomFilter(size, falsePositiveRate, nTweak); lock.lock(); diff --git a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java index 259a4ce2..69d64a70 100644 --- a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java +++ b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java @@ -318,8 +318,7 @@ public class PeerGroupTest extends TestWithPeerGroup { peerGroup.addWallet(wallet); // Transaction announced to the first peer. InventoryMessage inv1 = (InventoryMessage) outbound(p1); - assertTrue(outbound(p1) instanceof BloomFilter); // Filter is recalculated. - assertTrue(outbound(p1) instanceof MemoryPoolMessage); + // Filter is still the same as it was, so it is not rebroadcast assertEquals(t3.getHash(), inv1.getItems().get(0).hash); // Peer asks for the transaction, and get it. GetDataMessage getdata = new GetDataMessage(params);