From 3c606516be0f3b67b844157d168eeef995145d41 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Tue, 19 Feb 2013 15:51:34 +0100
Subject: [PATCH] Query the memory pool of nodes that support Bloom filtering.
---
.../bitcoin/core/BitcoinSerializer.java | 3 ++
.../google/bitcoin/core/FilteredBlock.java | 2 ++
.../bitcoin/core/MemoryPoolMessage.java | 32 +++++++++++++++++++
.../java/com/google/bitcoin/core/Peer.java | 13 ++++----
.../com/google/bitcoin/core/PeerGroup.java | 27 ++++++++++------
.../google/bitcoin/core/VersionMessage.java | 15 +++++++++
...ilteredBlockAndPartialMerkleTreeTests.java | 7 +---
.../google/bitcoin/core/PeerGroupTest.java | 15 ++-------
.../bitcoin/core/TestWithPeerGroup.java | 5 +++
9 files changed, 85 insertions(+), 34 deletions(-)
create mode 100644 core/src/main/java/com/google/bitcoin/core/MemoryPoolMessage.java
diff --git a/core/src/main/java/com/google/bitcoin/core/BitcoinSerializer.java b/core/src/main/java/com/google/bitcoin/core/BitcoinSerializer.java
index 47940e09..61636413 100644
--- a/core/src/main/java/com/google/bitcoin/core/BitcoinSerializer.java
+++ b/core/src/main/java/com/google/bitcoin/core/BitcoinSerializer.java
@@ -68,6 +68,7 @@ public class BitcoinSerializer {
names.put(BloomFilter.class, "filterload");
names.put(FilteredBlock.class, "merkleblock");
names.put(NotFoundMessage.class, "notfound");
+ names.put(MemoryPoolMessage.class, "mempool");
}
/**
@@ -250,6 +251,8 @@ public class BitcoinSerializer {
return new BloomFilter(params, payloadBytes);
} else if (command.equals("notfound")) {
return new NotFoundMessage(params, payloadBytes);
+ } else if (command.equals("mempool")) {
+ return new MemoryPoolMessage();
} else {
log.warn("No support for deserializing message with name {}", command);
return new UnknownMessage(params, command, payloadBytes);
diff --git a/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java b/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java
index 74ecc339..6a416cdd 100644
--- a/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java
+++ b/core/src/main/java/com/google/bitcoin/core/FilteredBlock.java
@@ -25,6 +25,8 @@ import java.util.*;
* of the block header and a {@link PartialMerkleTree} which contains the transactions which matched the filter.
*/
public class FilteredBlock extends Message {
+ /** The protocol version at which Bloom filtering started to be supported. */
+ public static final int MIN_PROTOCOL_VERSION = 70000;
private Block header;
// The PartialMerkleTree of transactions
diff --git a/core/src/main/java/com/google/bitcoin/core/MemoryPoolMessage.java b/core/src/main/java/com/google/bitcoin/core/MemoryPoolMessage.java
new file mode 100644
index 00000000..ca90ff74
--- /dev/null
+++ b/core/src/main/java/com/google/bitcoin/core/MemoryPoolMessage.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2012 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;
+
+/**
+ * The "mempool" message asks a remote peer to announce all transactions in its memory pool, possibly restricted by
+ * any Bloom filter set on the connection. The list of transaction hashes comes back in an inv message. Note that
+ * this is different to the {@link MemoryPool} object which doesn't try to keep track of all pending transactions,
+ * it's just a holding area for transactions that a part of the app may find interesting. The mempool message has
+ * no fields.
+ */
+public class MemoryPoolMessage extends Message {
+ @Override
+ void parse() throws ProtocolException {}
+
+ @Override
+ protected void parseLite() throws ProtocolException {}
+}
diff --git a/core/src/main/java/com/google/bitcoin/core/Peer.java b/core/src/main/java/com/google/bitcoin/core/Peer.java
index 46951277..6d45bd7a 100644
--- a/core/src/main/java/com/google/bitcoin/core/Peer.java
+++ b/core/src/main/java/com/google/bitcoin/core/Peer.java
@@ -835,11 +835,12 @@ public class Peer {
// the duplicate check in blockChainDownload(). But the satoshi client may change in future so
// it's better to be safe here.
if (!pendingBlockDownloads.contains(item.hash)) {
- if (getPeerVersionMessage().clientVersion > 70000 && useFilteredBlocks) {
+ if (getPeerVersionMessage().isBloomFilteringSupported() && useFilteredBlocks) {
getdata.addItem(new InventoryItem(InventoryItem.Type.FilteredBlock, item.hash));
pingAfterGetData = true;
- } else
+ } else {
getdata.addItem(item);
+ }
pendingBlockDownloads.add(item.hash);
}
}
@@ -1108,9 +1109,8 @@ public class Peer {
}
protected synchronized ListenableFuture ping(long nonce) throws IOException, ProtocolException {
- int peerVersion = getPeerVersionMessage().clientVersion;
- if (peerVersion < Pong.MIN_PROTOCOL_VERSION)
- throw new ProtocolException("Peer version is too low for measurable pings: " + peerVersion);
+ if (!getPeerVersionMessage().isPingPongSupported())
+ throw new ProtocolException("Peer version is too low for measurable pings: " + getPeerVersionMessage());
PendingPing pendingPing = new PendingPing(nonce);
pendingPings.add(pendingPing);
sendMessage(new Ping(pendingPing.nonce));
@@ -1194,7 +1194,8 @@ public class Peer {
/**
* If set to false, the peer won't try and fetch blocks and transactions it hears about. Normally, only one
- * peer should download missing blocks. Defaults to true.
+ * peer should download missing blocks. Defaults to true. Changing this value from false to true may trigger
+ * a request to the remote peer for the contents of its memory pool, if Bloom filtering is active.
*/
public void setDownloadData(boolean downloadData) {
this.downloadData.set(downloadData);
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 fc67cea8..b3284032 100644
--- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
+++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
@@ -580,12 +580,16 @@ public class PeerGroup extends AbstractIdleService {
for (Wallet w : wallets)
filter.merge(w.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak));
bloomFilter = filter;
- log.info("Sending all peers an updated Bloom Filter.");
- for (Peer peer : peers)
- try {
- // peers of a low version will simply ignore filterload messages
- peer.sendMessage(filter);
- } catch (IOException e) { }
+ for (Peer peer : peers) {
+ if (peer.getPeerVersionMessage().isBloomFilteringSupported()) {
+ try {
+ log.info("{}: Sending peer an updated Bloom Filter.", peer);
+ peer.sendMessage(filter);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
}
// Do this last so that bloomFilter is already set when it gets called.
setFastCatchupTimeSecs(earliestKeyTime);
@@ -709,13 +713,17 @@ public class PeerGroup extends AbstractIdleService {
protected synchronized void handleNewPeer(final Peer peer) {
// Runs on a netty worker thread for every peer that is newly connected. Peer is not locked at this point.
+ // Sets up the newly connected peer so it can do everything it needs to.
log.info("{}: New peer", peer);
// 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.
+ // OK because it helps improve wallet privacy. Old nodes will just ignore the message.
try {
- if (bloomFilter != null)
+ if (bloomFilter != null && peer.getPeerVersionMessage().isBloomFilteringSupported()) {
+ log.info("{}: Sending Bloom filter and querying memory pool", peer);
peer.sendMessage(bloomFilter);
+ peer.sendMessage(new MemoryPoolMessage());
+ }
} catch (IOException e) { } // That was quick...already disconnected
// Link the peer to the memory pool so broadcast transactions have their confidence levels updated.
peer.setMemoryPool(memoryPool);
@@ -742,7 +750,7 @@ public class PeerGroup extends AbstractIdleService {
// then sends us fake relevant transactions. We'll attempt to relay the bad transactions, our badness score
// in the Satoshi client will increase and we'll get disconnected.
//
- // TODO: Find a way to balance the desire to propagate useful transactions against obscure DoS attacks.
+ // TODO: Find a way to balance the desire to propagate useful transactions against DoS attacks.
announcePendingWalletTransactions(wallets, Collections.singletonList(peer));
// And set up event listeners for clients. This will allow them to find out about new transactions and blocks.
for (PeerEventListener listener : peerEventListeners) {
@@ -756,6 +764,7 @@ public class PeerGroup extends AbstractIdleService {
}
});
final PeerGroup thisGroup = this;
+ // TODO: Move this into the Peer object itself.
peer.addEventListener(new AbstractPeerEventListener() {
int filteredBlocksReceivedFromPeer = 0;
@Override
diff --git a/core/src/main/java/com/google/bitcoin/core/VersionMessage.java b/core/src/main/java/com/google/bitcoin/core/VersionMessage.java
index a7a06c7e..2c994d6d 100644
--- a/core/src/main/java/com/google/bitcoin/core/VersionMessage.java
+++ b/core/src/main/java/com/google/bitcoin/core/VersionMessage.java
@@ -293,4 +293,19 @@ public class VersionMessage extends Message {
if (component.contains("/") || component.contains("(") || component.contains(")"))
throw new IllegalArgumentException("name contains invalid characters");
}
+
+ /**
+ * Returns true if the clientVersion field is >= Pong.MIN_PROTOCOL_VERSION. If it is then ping() is usable.
+ */
+ public boolean isPingPongSupported() {
+ return clientVersion >= Pong.MIN_PROTOCOL_VERSION;
+ }
+
+ /**
+ * Returns true if the clientVersion field is >= FilteredBlock.MIN_PROTOCOL_VERSION. If it is then Bloom filtering
+ * is available and the memory pool of the remote peer will be queried when the downloadData property is true.
+ */
+ public boolean isBloomFilteringSupported() {
+ return clientVersion >= FilteredBlock.MIN_PROTOCOL_VERSION;
+ }
}
diff --git a/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java b/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java
index cc4d216a..8cbfb33b 100644
--- a/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java
+++ b/core/src/test/java/com/google/bitcoin/core/FilteredBlockAndPartialMerkleTreeTests.java
@@ -90,16 +90,11 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
// Create a peer.
FakeChannel p1 = connectPeer(1);
assertEquals(1, peerGroup.numConnectedPeers());
-
// Send an inv for block 100001
InventoryMessage inv = new InventoryMessage(unitTestParams);
inv.addBlock(block);
inbound(p1, inv);
- // First thing sent is a copy of the generated bloom filter
- //TODO assertTrue(outbound(p1).equals(filter)); (need to set the nTweak to 0xDEADBEEF)
- assertTrue(outbound(p1) instanceof BloomFilter);
-
// Check that we properly requested the correct FilteredBlock
Object getData = outbound(p1);
assertTrue(getData instanceof GetDataMessage);
@@ -107,7 +102,7 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
assertTrue(((GetDataMessage)getData).getItems().get(0).hash.equals(block.getHash()));
assertTrue(((GetDataMessage)getData).getItems().get(0).type == InventoryItem.Type.FilteredBlock);
- //Check that we then immediately pinged
+ // Check that we then immediately pinged.
Object ping = outbound(p1);
assertTrue(ping instanceof Ping);
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 ea41a23a..6a525156 100644
--- a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java
@@ -110,10 +110,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
// Note: we start with p2 here to verify that transactions are downloaded from whichever peer announces first
// which does not have to be the same as the download peer (which is really the "block download peer").
inbound(p2, inv);
- assertTrue(outbound(p2) instanceof BloomFilter);
assertTrue(outbound(p2) instanceof GetDataMessage);
inbound(p1, inv);
- assertTrue(outbound(p1) instanceof BloomFilter);
assertNull(outbound(p1)); // Only one peer is used to download.
inbound(p2, t1);
assertNull(outbound(p1));
@@ -148,11 +146,9 @@ public class PeerGroupTest extends TestWithPeerGroup {
// Only peer 1 tries to download it.
inbound(p1, inv);
- assertTrue(outbound(p1) instanceof BloomFilter);
assertTrue(outbound(p1) instanceof GetDataMessage);
- assertTrue(outbound(p2) instanceof BloomFilter);
assertNull(outbound(p2));
- // Peer 1 goes away.
+ // Peer 1 goes away, peer 2 becomes the download peer and thus queries the remote mempool.
closePeer(peerOf(p1));
// Peer 2 fetches it next time it hears an inv (should it fetch immediately?).
inbound(p2, inv);
@@ -178,7 +174,6 @@ public class PeerGroupTest extends TestWithPeerGroup {
// Expect a zero hash getblocks on p1. This is how the process starts.
peerGroup.startBlockChainDownload(new AbstractPeerEventListener() {
});
- assertTrue(outbound(p1) instanceof BloomFilter);
GetBlocksMessage getblocks = (GetBlocksMessage) outbound(p1);
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
// We give back an inv with some blocks in it.
@@ -193,7 +188,6 @@ public class PeerGroupTest extends TestWithPeerGroup {
inbound(p1, b1);
// Now we successfully connect to another peer. There should be no messages sent.
FakeChannel p2 = connectPeer(2);
- assertTrue(outbound(p2) instanceof BloomFilter);
Message message = (Message)outbound(p2);
assertNull(message == null ? "" : message.toString(), message);
peerGroup.stop();
@@ -221,14 +215,12 @@ public class PeerGroupTest extends TestWithPeerGroup {
// Peer 2 advertises the tx but does not receive it yet.
inbound(p2, inv);
- assertTrue(outbound(p2) instanceof BloomFilter);
assertTrue(outbound(p2) instanceof GetDataMessage);
assertEquals(0, tx.getConfidence().numBroadcastPeers());
assertTrue(peerGroup.getMemoryPool().maybeWasSeen(tx.getHash()));
assertNull(event[0]);
// Peer 1 advertises the tx, we don't do anything as it's already been requested.
inbound(p1, inv);
- assertTrue(outbound(p1) instanceof BloomFilter);
assertNull(outbound(p1));
// Peer 2 gets sent the tx and requests the dependency.
inbound(p2, tx);
@@ -274,7 +266,6 @@ public class PeerGroupTest extends TestWithPeerGroup {
// Send ourselves a bit of money.
Block b1 = TestUtils.makeSolvedTestBlock(blockStore, address);
inbound(p1, b1);
- assertTrue(outbound(p1) instanceof BloomFilter);
assertNull(outbound(p1));
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
@@ -325,12 +316,11 @@ public class PeerGroupTest extends TestWithPeerGroup {
GetDataMessage getdata = new GetDataMessage(params);
getdata.addItem(inv1.getItems().get(0));
inbound(p1, getdata);
- assertTrue(outbound(p1) instanceof BloomFilter);
+ assertTrue(outbound(p1) instanceof BloomFilter); // Filter is recalculated.
Transaction t4 = (Transaction) outbound(p1);
assertEquals(t3, t4);
FakeChannel p3 = connectPeer(3);
- assertTrue(outbound(p3) instanceof BloomFilter);
assertTrue(outbound(p3) instanceof InventoryMessage);
control.verify();
}
@@ -371,7 +361,6 @@ public class PeerGroupTest extends TestWithPeerGroup {
VersionMessage versionMessage = new VersionMessage(params, 2);
versionMessage.clientVersion = Pong.MIN_PROTOCOL_VERSION;
FakeChannel p1 = connectPeer(1, versionMessage);
- assertTrue(outbound(p1) instanceof BloomFilter);
Ping ping = (Ping) outbound(p1);
inbound(p1, new Pong(ping.getNonce()));
assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
diff --git a/core/src/test/java/com/google/bitcoin/core/TestWithPeerGroup.java b/core/src/test/java/com/google/bitcoin/core/TestWithPeerGroup.java
index a0e5a896..c631d9fb 100644
--- a/core/src/test/java/com/google/bitcoin/core/TestWithPeerGroup.java
+++ b/core/src/test/java/com/google/bitcoin/core/TestWithPeerGroup.java
@@ -36,6 +36,7 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
super.setUp(blockStore);
remoteVersionMessage = new VersionMessage(unitTestParams, 1);
+ remoteVersionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
ClientBootstrap bootstrap = new ClientBootstrap(new ChannelFactory() {
public void releaseExternalResources() {}
@@ -69,6 +70,10 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
FakeChannel p = (FakeChannel) peerGroup.connectTo(remoteAddress).getChannel();
assertTrue(p.nextEvent() instanceof ChannelStateEvent);
inbound(p, versionMessage);
+ if (versionMessage.isBloomFilteringSupported()) {
+ assertTrue(outbound(p) instanceof BloomFilter);
+ assertTrue(outbound(p) instanceof MemoryPoolMessage);
+ }
return p;
}
}