/* * Copyright 2011 Google Inc. * Copyright 2014 Andreas Schildbach * * 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.core; import com.google.common.base.Stopwatch; import com.google.common.collect.*; import com.google.common.net.*; import com.google.common.util.concurrent.*; import org.bitcoinj.core.listeners.*; import org.bitcoinj.net.discovery.*; import org.bitcoinj.testing.*; import org.bitcoinj.utils.*; import org.bitcoinj.wallet.Wallet; import org.junit.*; import org.junit.runner.*; import org.junit.runners.*; import java.io.*; import java.net.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import static org.bitcoinj.core.Coin.*; import static org.junit.Assert.*; // TX announcement and broadcast is tested in TransactionBroadcastTest. @RunWith(value = Parameterized.class) public class PeerGroupTest extends TestWithPeerGroup { private static final int BLOCK_HEIGHT_GENESIS = 0; private BlockingQueue<Peer> connectedPeers; private BlockingQueue<Peer> disconnectedPeers; private PeerConnectedEventListener connectedListener = new PeerConnectedEventListener() { @Override public void onPeerConnected(Peer peer, int peerCount) { connectedPeers.add(peer); } }; private PeerDisconnectedEventListener disconnectedListener = new PeerDisconnectedEventListener() { @Override public void onPeerDisconnected(Peer peer, int peerCount) { disconnectedPeers.add(peer); } }; private PreMessageReceivedEventListener preMessageReceivedListener; private Map<Peer, AtomicInteger> peerToMessageCount; @Parameterized.Parameters public static Collection<ClientType[]> parameters() { return Arrays.asList(new ClientType[] {ClientType.NIO_CLIENT_MANAGER}, new ClientType[] {ClientType.BLOCKING_CLIENT_MANAGER}); } public PeerGroupTest(ClientType clientType) { super(clientType); } @Override @Before public void setUp() throws Exception { super.setUp(); peerToMessageCount = new HashMap<Peer, AtomicInteger>(); connectedPeers = new LinkedBlockingQueue<Peer>(); disconnectedPeers = new LinkedBlockingQueue<Peer>(); preMessageReceivedListener = new PreMessageReceivedEventListener() { @Override public Message onPreMessageReceived(Peer peer, Message m) { AtomicInteger messageCount = peerToMessageCount.get(peer); if (messageCount == null) { messageCount = new AtomicInteger(0); peerToMessageCount.put(peer, messageCount); } messageCount.incrementAndGet(); // Just pass the message right through for further processing. return m; } }; } @Override @After public void tearDown() { super.tearDown(); } @Test public void listener() throws Exception { peerGroup.addConnectedEventListener(connectedListener); peerGroup.addDisconnectedEventListener(disconnectedListener); peerGroup.addPreMessageReceivedEventListener(preMessageReceivedListener); peerGroup.start(); // Create a couple of peers. InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p2 = connectPeer(2); connectedPeers.take(); connectedPeers.take(); pingAndWait(p1); pingAndWait(p2); Threading.waitForUserCode(); assertEquals(0, disconnectedPeers.size()); p1.close(); disconnectedPeers.take(); assertEquals(0, disconnectedPeers.size()); p2.close(); disconnectedPeers.take(); assertEquals(0, disconnectedPeers.size()); assertTrue(peerGroup.removeConnectedEventListener(connectedListener)); assertFalse(peerGroup.removeConnectedEventListener(connectedListener)); assertTrue(peerGroup.removeDisconnectedEventListener(disconnectedListener)); assertFalse(peerGroup.removeDisconnectedEventListener(disconnectedListener)); assertTrue(peerGroup.removePreMessageReceivedEventListener(preMessageReceivedListener)); assertFalse(peerGroup.removePreMessageReceivedEventListener(preMessageReceivedListener)); } @Test public void peerDiscoveryPolling() throws InterruptedException { // Check that if peer discovery fails, we keep trying until we have some nodes to talk with. final CountDownLatch latch = new CountDownLatch(1); final AtomicBoolean result = new AtomicBoolean(); peerGroup.addPeerDiscovery(new PeerDiscovery() { @Override public InetSocketAddress[] getPeers(long services, long unused, TimeUnit unused2) throws PeerDiscoveryException { if (!result.getAndSet(true)) { // Pretend we are not connected to the internet. throw new PeerDiscoveryException("test failure"); } else { // Return a bogus address. latch.countDown(); return new InetSocketAddress[]{new InetSocketAddress("localhost", 1)}; } } @Override public void shutdown() { } }); peerGroup.start(); latch.await(); // Check that we did indeed throw an exception. If we got here it means we threw and then PeerGroup tried // again a bit later. assertTrue(result.get()); } // Utility method to create a PeerDiscovery with a certain number of addresses. private PeerDiscovery createPeerDiscovery(int nrOfAddressesWanted, int port) { final InetSocketAddress[] addresses = new InetSocketAddress[nrOfAddressesWanted]; for (int addressNr = 0; addressNr < nrOfAddressesWanted; addressNr++) { // make each address unique by using the counter to increment the port. addresses[addressNr] = new InetSocketAddress("localhost", port + addressNr); } return new PeerDiscovery() { public InetSocketAddress[] getPeers(long services, long unused, TimeUnit unused2) throws PeerDiscoveryException { return addresses; } public void shutdown() { } }; } @Test public void multiplePeerDiscovery() throws InterruptedException { peerGroup.setMaxPeersToDiscoverCount(98); peerGroup.addPeerDiscovery(createPeerDiscovery(1, 0)); peerGroup.addPeerDiscovery(createPeerDiscovery(2, 100)); peerGroup.addPeerDiscovery(createPeerDiscovery(96, 200)); peerGroup.addPeerDiscovery(createPeerDiscovery(3, 300)); peerGroup.addPeerDiscovery(createPeerDiscovery(1, 400)); peerGroup.addDiscoveredEventListener(new PeerDiscoveredEventListener() { @Override public void onPeersDiscovered(Set<PeerAddress> peerAddresses) { assertEquals(99, peerAddresses.size()); } }); peerGroup.start(); } @Test public void receiveTxBroadcast() throws Exception { // Check that when we receive transactions on all our peers, we do the right thing. peerGroup.start(); // Create a couple of peers. InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p2 = connectPeer(2); // Check the peer accessors. assertEquals(2, peerGroup.numConnectedPeers()); Set<Peer> tmp = new HashSet<Peer>(peerGroup.getConnectedPeers()); Set<Peer> expectedPeers = new HashSet<Peer>(); expectedPeers.add(peerOf(p1)); expectedPeers.add(peerOf(p2)); assertEquals(tmp, expectedPeers); Coin value = COIN; Transaction t1 = FakeTxBuilder.createFakeTx(PARAMS, value, address); InventoryMessage inv = new InventoryMessage(PARAMS); inv.addTransaction(t1); // 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 GetDataMessage); inbound(p1, inv); assertNull(outbound(p1)); // Only one peer is used to download. inbound(p2, t1); assertNull(outbound(p1)); // Asks for dependency. GetDataMessage getdata = (GetDataMessage) outbound(p2); assertNotNull(getdata); inbound(p2, new NotFoundMessage(PARAMS, getdata.getItems())); pingAndWait(p2); assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED)); } @Test public void receiveTxBroadcastOnAddedWallet() throws Exception { // Check that when we receive transactions on all our peers, we do the right thing. peerGroup.start(); // Create a peer. InboundMessageQueuer p1 = connectPeer(1); Wallet wallet2 = new Wallet(PARAMS); ECKey key2 = wallet2.freshReceiveKey(); Address address2 = key2.toAddress(PARAMS); peerGroup.addWallet(wallet2); blockChain.addWallet(wallet2); assertEquals(BloomFilter.class, waitForOutbound(p1).getClass()); assertEquals(MemoryPoolMessage.class, waitForOutbound(p1).getClass()); Coin value = COIN; Transaction t1 = FakeTxBuilder.createFakeTx(PARAMS, value, address2); InventoryMessage inv = new InventoryMessage(PARAMS); inv.addTransaction(t1); inbound(p1, inv); assertTrue(outbound(p1) instanceof GetDataMessage); inbound(p1, t1); // Asks for dependency. GetDataMessage getdata = (GetDataMessage) outbound(p1); assertNotNull(getdata); inbound(p1, new NotFoundMessage(PARAMS, getdata.getItems())); pingAndWait(p1); assertEquals(value, wallet2.getBalance(Wallet.BalanceType.ESTIMATED)); } @Test public void singleDownloadPeer1() throws Exception { // Check that we don't attempt to retrieve blocks on multiple peers. peerGroup.start(); // Create a couple of peers. InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p2 = connectPeer(2); assertEquals(2, peerGroup.numConnectedPeers()); // Set up a little block chain. We heard about b1 but not b2 (it is pending download). b3 is solved whilst we // are downloading the chain. Block b1 = FakeTxBuilder.createFakeBlock(blockStore, BLOCK_HEIGHT_GENESIS).block; blockChain.add(b1); Block b2 = FakeTxBuilder.makeSolvedTestBlock(b1); Block b3 = FakeTxBuilder.makeSolvedTestBlock(b2); // Peer 1 and 2 receives an inv advertising a newly solved block. InventoryMessage inv = new InventoryMessage(PARAMS); inv.addBlock(b3); // Only peer 1 tries to download it. inbound(p1, inv); pingAndWait(p1); assertTrue(outbound(p1) instanceof GetDataMessage); assertNull(outbound(p2)); // Peer 1 goes away, peer 2 becomes the download peer and thus queries the remote mempool. final SettableFuture<Void> p1CloseFuture = SettableFuture.create(); peerOf(p1).addDisconnectedEventListener(new PeerDisconnectedEventListener() { @Override public void onPeerDisconnected(Peer peer, int peerCount) { p1CloseFuture.set(null); } }); closePeer(peerOf(p1)); p1CloseFuture.get(); // Peer 2 fetches it next time it hears an inv (should it fetch immediately?). inbound(p2, inv); assertTrue(outbound(p2) instanceof GetDataMessage); } @Test public void singleDownloadPeer2() throws Exception { // Check that we don't attempt multiple simultaneous block chain downloads, when adding a new peer in the // middle of an existing chain download. // Create a couple of peers. peerGroup.start(); // Create a couple of peers. InboundMessageQueuer p1 = connectPeer(1); // Set up a little block chain. Block b1 = FakeTxBuilder.createFakeBlock(blockStore, BLOCK_HEIGHT_GENESIS).block; Block b2 = FakeTxBuilder.makeSolvedTestBlock(b1); Block b3 = FakeTxBuilder.makeSolvedTestBlock(b2); // Expect a zero hash getblocks on p1. This is how the process starts. peerGroup.startBlockChainDownload(new AbstractPeerDataEventListener() { }); GetBlocksMessage getblocks = (GetBlocksMessage) outbound(p1); assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash()); // We give back an inv with some blocks in it. InventoryMessage inv = new InventoryMessage(PARAMS); inv.addBlock(b1); inv.addBlock(b2); inv.addBlock(b3); inbound(p1, inv); assertTrue(outbound(p1) instanceof GetDataMessage); // We hand back the first block. inbound(p1, b1); // Now we successfully connect to another peer. There should be no messages sent. InboundMessageQueuer p2 = connectPeer(2); Message message = outbound(p2); assertNull(message == null ? "" : message.toString(), message); } @Test public void transactionConfidence() throws Exception { // Checks that we correctly count how many peers broadcast a transaction, so we can establish some measure of // its trustworthyness assuming an untampered with internet connection. peerGroup.start(); final Transaction[] event = new Transaction[1]; final TransactionConfidence[] confEvent = new TransactionConfidence[1]; peerGroup.addOnTransactionBroadcastListener(Threading.SAME_THREAD, new OnTransactionBroadcastListener() { @Override public void onTransaction(Peer peer, Transaction t) { event[0] = t; } }); InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p2 = connectPeer(2); InboundMessageQueuer p3 = connectPeer(3); Transaction tx = FakeTxBuilder.createFakeTx(PARAMS, valueOf(20, 0), address); InventoryMessage inv = new InventoryMessage(PARAMS); inv.addTransaction(tx); assertEquals(0, tx.getConfidence().numBroadcastPeers()); assertNull(tx.getConfidence().getLastBroadcastedAt()); // Peer 2 advertises the tx but does not receive it yet. inbound(p2, inv); assertTrue(outbound(p2) instanceof GetDataMessage); assertEquals(1, tx.getConfidence().numBroadcastPeers()); assertNull(event[0]); // Peer 1 advertises the tx, we don't do anything as it's already been requested. inbound(p1, inv); assertNull(outbound(p1)); // Peer 2 gets sent the tx and requests the dependency. inbound(p2, tx); assertTrue(outbound(p2) instanceof GetDataMessage); tx = event[0]; // We want to use the canonical copy delivered by the PeerGroup from now on. assertNotNull(tx); event[0] = null; // Peer 1 (the download peer) advertises the tx, we download it. inbound(p1, inv); // returns getdata inbound(p1, tx); // returns nothing after a queue drain. // Two peers saw this tx hash. assertEquals(2, tx.getConfidence().numBroadcastPeers()); assertTrue(tx.getConfidence().wasBroadcastBy(peerOf(p1).getAddress())); assertTrue(tx.getConfidence().wasBroadcastBy(peerOf(p2).getAddress())); assertNotNull(tx.getConfidence().getLastBroadcastedAt()); tx.getConfidence().addEventListener(new TransactionConfidence.Listener() { @Override public void onConfidenceChanged(TransactionConfidence confidence, TransactionConfidence.Listener.ChangeReason reason) { confEvent[0] = confidence; } }); // A straggler reports in. inbound(p3, inv); pingAndWait(p3); Threading.waitForUserCode(); assertEquals(tx.getHash(), confEvent[0].getTransactionHash()); assertEquals(3, tx.getConfidence().numBroadcastPeers()); assertTrue(tx.getConfidence().wasBroadcastBy(peerOf(p3).getAddress())); } @Test public void testWalletCatchupTime() throws Exception { // Check the fast catchup time was initialized to something around the current runtime minus a week. // The wallet was already added to the peer in setup. final int WEEK = 86400 * 7; final long now = Utils.currentTimeSeconds(); peerGroup.start(); assertTrue(peerGroup.getFastCatchupTimeSecs() > now - WEEK - 10000); Wallet w2 = new Wallet(PARAMS); ECKey key1 = new ECKey(); key1.setCreationTimeSeconds(now - 86400); // One day ago. w2.importKey(key1); peerGroup.addWallet(w2); peerGroup.waitForJobQueue(); assertEquals(peerGroup.getFastCatchupTimeSecs(), now - 86400 - WEEK); // Adding a key to the wallet should update the fast catchup time, but asynchronously and in the background // due to the need to avoid complicated lock inversions. ECKey key2 = new ECKey(); key2.setCreationTimeSeconds(now - 100000); w2.importKey(key2); peerGroup.waitForJobQueue(); assertEquals(peerGroup.getFastCatchupTimeSecs(), now - WEEK - 100000); } @Test public void noPings() throws Exception { peerGroup.start(); peerGroup.setPingIntervalMsec(0); VersionMessage versionMessage = new VersionMessage(PARAMS, 2); versionMessage.clientVersion = NetworkParameters.ProtocolVersion.BLOOM_FILTER.getBitcoinProtocolVersion(); versionMessage.localServices = VersionMessage.NODE_NETWORK; connectPeer(1, versionMessage); peerGroup.waitForPeers(1).get(); assertFalse(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE); } @Test public void pings() throws Exception { peerGroup.start(); peerGroup.setPingIntervalMsec(100); VersionMessage versionMessage = new VersionMessage(PARAMS, 2); versionMessage.clientVersion = NetworkParameters.ProtocolVersion.BLOOM_FILTER.getBitcoinProtocolVersion(); versionMessage.localServices = VersionMessage.NODE_NETWORK; InboundMessageQueuer p1 = connectPeer(1, versionMessage); Ping ping = (Ping) waitForOutbound(p1); inbound(p1, new Pong(ping.getNonce())); pingAndWait(p1); assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE); // The call to outbound should block until a ping arrives. ping = (Ping) waitForOutbound(p1); inbound(p1, new Pong(ping.getNonce())); assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE); } @Test public void downloadPeerSelection() throws Exception { peerGroup.start(); VersionMessage versionMessage2 = new VersionMessage(PARAMS, 2); versionMessage2.clientVersion = NetworkParameters.ProtocolVersion.BLOOM_FILTER.getBitcoinProtocolVersion(); versionMessage2.localServices = VersionMessage.NODE_NETWORK; VersionMessage versionMessage3 = new VersionMessage(PARAMS, 3); versionMessage3.clientVersion = NetworkParameters.ProtocolVersion.BLOOM_FILTER.getBitcoinProtocolVersion(); versionMessage3.localServices = VersionMessage.NODE_NETWORK; assertNull(peerGroup.getDownloadPeer()); Peer a = connectPeer(1, versionMessage2).peer; assertEquals(2, peerGroup.getMostCommonChainHeight()); assertEquals(a, peerGroup.getDownloadPeer()); connectPeer(2, versionMessage2); assertEquals(2, peerGroup.getMostCommonChainHeight()); assertEquals(a, peerGroup.getDownloadPeer()); // No change. Peer c = connectPeer(3, versionMessage3).peer; assertEquals(2, peerGroup.getMostCommonChainHeight()); assertEquals(a, peerGroup.getDownloadPeer()); // No change yet. connectPeer(4, versionMessage3); assertEquals(3, peerGroup.getMostCommonChainHeight()); assertEquals(a, peerGroup.getDownloadPeer()); // Still no change. // New peer with a higher protocol version but same chain height. // TODO: When PeerGroup.selectDownloadPeer.PREFERRED_VERSION is not equal to vMinRequiredProtocolVersion, // reenable this test /*VersionMessage versionMessage4 = new VersionMessage(PARAMS, 3); versionMessage4.clientVersion = 100000; versionMessage4.localServices = VersionMessage.NODE_NETWORK; InboundMessageQueuer d = connectPeer(5, versionMessage4); assertEquals(d.peer, peerGroup.getDownloadPeer());*/ } @Test public void peerTimeoutTest() throws Exception { final int timeout = 100; peerGroup.start(); peerGroup.setConnectTimeoutMillis(timeout); final SettableFuture<Void> peerConnectedFuture = SettableFuture.create(); final SettableFuture<Void> peerDisconnectedFuture = SettableFuture.create(); peerGroup.addConnectedEventListener(Threading.SAME_THREAD, new PeerConnectedEventListener() { @Override public void onPeerConnected(Peer peer, int peerCount) { peerConnectedFuture.set(null); } }); peerGroup.addDisconnectedEventListener(Threading.SAME_THREAD, new PeerDisconnectedEventListener() { @Override public void onPeerDisconnected(Peer peer, int peerCount) { peerDisconnectedFuture.set(null); } }); // connect to peer but don't do handshake final Stopwatch watch = Stopwatch.createStarted(); // before connection so we don't get elapsed < timeout connectPeerWithoutVersionExchange(0); // wait for disconnect (plus a bit more, in case test server is overloaded) try { peerDisconnectedFuture.get(timeout + 200, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { // the checks below suffice for this case too } // check things after disconnect watch.stop(); assertFalse(peerConnectedFuture.isDone()); // should never have connected assertTrue(watch.elapsed(TimeUnit.MILLISECONDS) >= timeout); // should not disconnect before timeout assertTrue(peerDisconnectedFuture.isDone()); // but should disconnect eventually } @Test @Ignore("disabled for now as this test is too flaky") public void peerPriority() throws Exception { final List<InetSocketAddress> addresses = Lists.newArrayList( new InetSocketAddress("localhost", 2000), new InetSocketAddress("localhost", 2001), new InetSocketAddress("localhost", 2002) ); peerGroup.addConnectedEventListener(connectedListener); peerGroup.addDisconnectedEventListener(disconnectedListener); peerGroup.addPreMessageReceivedEventListener(preMessageReceivedListener); peerGroup.addPeerDiscovery(new PeerDiscovery() { @Override public InetSocketAddress[] getPeers(long services, long unused, TimeUnit unused2) throws PeerDiscoveryException { return addresses.toArray(new InetSocketAddress[addresses.size()]); } @Override public void shutdown() { } }); peerGroup.setMaxConnections(3); Utils.setMockSleep(true); blockJobs = true; jobBlocks.release(2); // startup + first peer discovery peerGroup.start(); jobBlocks.release(3); // One for each peer. handleConnectToPeer(0); handleConnectToPeer(1); handleConnectToPeer(2); connectedPeers.take(); connectedPeers.take(); connectedPeers.take(); addresses.clear(); addresses.addAll(Lists.newArrayList(new InetSocketAddress("localhost", 2003))); stopPeerServer(2); assertEquals(2002, disconnectedPeers.take().getAddress().getPort()); // peer died // discovers, connects to new peer jobBlocks.release(1); handleConnectToPeer(3); assertEquals(2003, connectedPeers.take().getAddress().getPort()); stopPeerServer(1); assertEquals(2001, disconnectedPeers.take().getAddress().getPort()); // peer died // Alternates trying two offline peers jobBlocks.release(10); assertEquals(2001, disconnectedPeers.take().getAddress().getPort()); assertEquals(2002, disconnectedPeers.take().getAddress().getPort()); assertEquals(2001, disconnectedPeers.take().getAddress().getPort()); assertEquals(2002, disconnectedPeers.take().getAddress().getPort()); assertEquals(2001, disconnectedPeers.take().getAddress().getPort()); // Peer 2 comes online startPeerServer(2); jobBlocks.release(1); handleConnectToPeer(2); assertEquals(2002, connectedPeers.take().getAddress().getPort()); jobBlocks.release(6); stopPeerServer(2); assertEquals(2002, disconnectedPeers.take().getAddress().getPort()); // peer died // Peer 2 is tried before peer 1, since it has a lower backoff due to recent success assertEquals(2002, disconnectedPeers.take().getAddress().getPort()); assertEquals(2001, disconnectedPeers.take().getAddress().getPort()); } @Test public void testBloomOnP2Pubkey() throws Exception { // Cover bug 513. When a relevant transaction with a p2pubkey output is found, the Bloom filter should be // recalculated to include that transaction hash but not re-broadcast as the remote nodes should have followed // the same procedure. However a new node that's connected should get the fresh filter. peerGroup.start(); final ECKey key = wallet.currentReceiveKey(); // Create a couple of peers. InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p2 = connectPeer(2); // Create a pay to pubkey tx. Transaction tx = FakeTxBuilder.createFakeTx(PARAMS, COIN, key); Transaction tx2 = new Transaction(PARAMS); tx2.addInput(tx.getOutput(0)); TransactionOutPoint outpoint = tx2.getInput(0).getOutpoint(); assertTrue(p1.lastReceivedFilter.contains(key.getPubKey())); assertFalse(p1.lastReceivedFilter.contains(tx.getHash().getBytes())); inbound(p1, tx); // p1 requests dep resolution, p2 is quiet. assertTrue(outbound(p1) instanceof GetDataMessage); final Sha256Hash dephash = tx.getInput(0).getOutpoint().getHash(); final InventoryItem inv = new InventoryItem(InventoryItem.Type.Transaction, dephash); inbound(p1, new NotFoundMessage(PARAMS, ImmutableList.of(inv))); assertNull(outbound(p1)); assertNull(outbound(p2)); peerGroup.waitForJobQueue(); // Now we connect p3 and there is a new bloom filter sent, that DOES match the relevant outpoint. InboundMessageQueuer p3 = connectPeer(3); assertTrue(p3.lastReceivedFilter.contains(key.getPubKey())); assertTrue(p3.lastReceivedFilter.contains(outpoint.unsafeBitcoinSerialize())); } @Test public void testBloomResendOnNewKey() throws Exception { // Check that when we add a new key to the wallet, the Bloom filter is re-calculated and re-sent but only once // we exceed the lookahead threshold. wallet.setKeyChainGroupLookaheadSize(5); wallet.setKeyChainGroupLookaheadThreshold(4); peerGroup.start(); // Create a couple of peers. InboundMessageQueuer p1 = connectPeer(1); InboundMessageQueuer p2 = connectPeer(2); peerGroup.waitForJobQueue(); BloomFilter f1 = p1.lastReceivedFilter; ECKey key = null; // We have to run ahead of the lookahead zone for this test. There should only be one bloom filter recalc. for (int i = 0; i < wallet.getKeyChainGroupLookaheadSize() + wallet.getKeyChainGroupLookaheadThreshold() + 1; i++) { key = wallet.freshReceiveKey(); } peerGroup.waitForJobQueue(); BloomFilter bf, f2 = null; while ((bf = (BloomFilter) outbound(p1)) != null) { assertEquals(MemoryPoolMessage.class, outbound(p1).getClass()); f2 = bf; } assertNotNull(key); assertNotNull(f2); assertNull(outbound(p1)); // Check the last filter received. assertNotEquals(f1, f2); assertTrue(f2.contains(key.getPubKey())); assertTrue(f2.contains(key.getPubKeyHash())); assertFalse(f1.contains(key.getPubKey())); assertFalse(f1.contains(key.getPubKeyHash())); } @Test public void waitForNumPeers1() throws Exception { ListenableFuture<List<Peer>> future = peerGroup.waitForPeers(3); peerGroup.start(); assertFalse(future.isDone()); connectPeer(1); assertFalse(future.isDone()); connectPeer(2); assertFalse(future.isDone()); assertTrue(peerGroup.waitForPeers(2).isDone()); // Immediate completion. connectPeer(3); future.get(); assertTrue(future.isDone()); } @Test public void waitForPeersOfVersion() throws Exception { final int baseVer = peerGroup.getMinRequiredProtocolVersion() + 3000; final int newVer = baseVer + 1000; ListenableFuture<List<Peer>> future = peerGroup.waitForPeersOfVersion(2, newVer); VersionMessage ver1 = new VersionMessage(PARAMS, 10); ver1.clientVersion = baseVer; ver1.localServices = VersionMessage.NODE_NETWORK; VersionMessage ver2 = new VersionMessage(PARAMS, 10); ver2.clientVersion = newVer; ver2.localServices = VersionMessage.NODE_NETWORK; peerGroup.start(); assertFalse(future.isDone()); connectPeer(1, ver1); assertFalse(future.isDone()); connectPeer(2, ver2); assertFalse(future.isDone()); assertTrue(peerGroup.waitForPeersOfVersion(1, newVer).isDone()); // Immediate completion. connectPeer(3, ver2); future.get(); assertTrue(future.isDone()); } @Test public void waitForPeersWithServiceFlags() throws Exception { ListenableFuture<List<Peer>> future = peerGroup.waitForPeersWithServiceMask(2, 3); VersionMessage ver1 = new VersionMessage(PARAMS, 10); ver1.clientVersion = 70000; ver1.localServices = VersionMessage.NODE_NETWORK; VersionMessage ver2 = new VersionMessage(PARAMS, 10); ver2.clientVersion = 70000; ver2.localServices = VersionMessage.NODE_NETWORK | 2; peerGroup.start(); assertFalse(future.isDone()); connectPeer(1, ver1); assertTrue(peerGroup.findPeersWithServiceMask(3).isEmpty()); assertFalse(future.isDone()); connectPeer(2, ver2); assertFalse(future.isDone()); assertEquals(1, peerGroup.findPeersWithServiceMask(3).size()); assertTrue(peerGroup.waitForPeersWithServiceMask(1, 0x3).isDone()); // Immediate completion. connectPeer(3, ver2); future.get(); assertTrue(future.isDone()); peerGroup.stop(); } @Test public void preferLocalPeer() throws IOException { // Because we are using the same port (8333 or 18333) that is used by Bitcoin Core // We have to consider 2 cases: // 1. Test are executed on the same machine that is running a full node // 2. Test are executed without any full node running locally // We have to avoid to connecting to real and external services in unit tests // So we skip this test in case we have already something running on port PARAMS.getPort() // Check that if we have a localhost port 8333 or 18333 then it's used instead of the p2p network. ServerSocket local = null; try { local = new ServerSocket(PARAMS.getPort(), 100, InetAddresses.forString("127.0.0.1")); } catch(BindException e) { // Port already in use, skipping this test. return; } try { peerGroup.setUseLocalhostPeerWhenPossible(true); peerGroup.start(); local.accept().close(); // Probe connect local.accept(); // Real connect // If we get here it used the local peer. Check no others are in use. assertEquals(1, peerGroup.getMaxConnections()); assertEquals(PeerAddress.localhost(PARAMS), peerGroup.getPendingPeers().get(0).getAddress()); } finally { local.close(); } } private <T extends Message> T assertNextMessageIs(InboundMessageQueuer q, Class<T> klass) throws Exception { Message outbound = waitForOutbound(q); assertEquals(klass, outbound.getClass()); return (T) outbound; } @Test public void autoRescanOnKeyExhaustion() throws Exception { // Check that if the last key that was inserted into the bloom filter is seen in some requested blocks, // that the exhausting block is discarded, a new filter is calculated and sent, and then the download resumes. final int NUM_KEYS = 9; // First, grab a load of keys from the wallet, and then recreate it so it forgets that those keys were issued. Wallet shadow = Wallet.fromSeed(wallet.getParams(), wallet.getKeyChainSeed()); List<ECKey> keys = new ArrayList<ECKey>(NUM_KEYS); for (int i = 0; i < NUM_KEYS; i++) { keys.add(shadow.freshReceiveKey()); } // Reduce the number of keys we need to work with to speed up this test. wallet.setKeyChainGroupLookaheadSize(4); wallet.setKeyChainGroupLookaheadThreshold(2); peerGroup.start(); InboundMessageQueuer p1 = connectPeer(1); assertTrue(p1.lastReceivedFilter.contains(keys.get(0).getPubKey())); assertTrue(p1.lastReceivedFilter.contains(keys.get(5).getPubKeyHash())); assertFalse(p1.lastReceivedFilter.contains(keys.get(keys.size() - 1).getPubKey())); peerGroup.startBlockChainDownload(null); assertNextMessageIs(p1, GetBlocksMessage.class); // Make some transactions and blocks that send money to the wallet thus using up all the keys. List<Block> blocks = Lists.newArrayList(); Coin expectedBalance = Coin.ZERO; Block prev = blockStore.getChainHead().getHeader(); for (ECKey key1 : keys) { Address addr = key1.toAddress(PARAMS); Block next = FakeTxBuilder.makeSolvedTestBlock(prev, FakeTxBuilder.createFakeTx(PARAMS, Coin.FIFTY_COINS, addr)); expectedBalance = expectedBalance.add(next.getTransactions().get(2).getOutput(0).getValue()); blocks.add(next); prev = next; } // Send the chain that doesn't have all the transactions in it. The blocks after the exhaustion point should all // be ignored. int epoch = wallet.getKeyChainGroupCombinedKeyLookaheadEpochs(); BloomFilter filter = new BloomFilter(PARAMS, p1.lastReceivedFilter.bitcoinSerialize()); filterAndSend(p1, blocks, filter); Block exhaustionPoint = blocks.get(3); pingAndWait(p1); assertNotEquals(epoch, wallet.getKeyChainGroupCombinedKeyLookaheadEpochs()); // 4th block was end of the lookahead zone and thus was discarded, so we got 3 blocks worth of money (50 each). assertEquals(Coin.FIFTY_COINS.multiply(3), wallet.getBalance()); assertEquals(exhaustionPoint.getPrevBlockHash(), blockChain.getChainHead().getHeader().getHash()); // Await the new filter. peerGroup.waitForJobQueue(); BloomFilter newFilter = assertNextMessageIs(p1, BloomFilter.class); assertNotEquals(filter, newFilter); assertNextMessageIs(p1, MemoryPoolMessage.class); Ping ping = assertNextMessageIs(p1, Ping.class); inbound(p1, new Pong(ping.getNonce())); // Await restart of the chain download. GetDataMessage getdata = assertNextMessageIs(p1, GetDataMessage.class); assertEquals(exhaustionPoint.getHash(), getdata.getHashOf(0)); assertEquals(InventoryItem.Type.FilteredBlock, getdata.getItems().get(0).type); List<Block> newBlocks = blocks.subList(3, blocks.size()); filterAndSend(p1, newBlocks, newFilter); assertNextMessageIs(p1, Ping.class); // It happened again. peerGroup.waitForJobQueue(); newFilter = assertNextMessageIs(p1, BloomFilter.class); assertNextMessageIs(p1, MemoryPoolMessage.class); inbound(p1, new Pong(assertNextMessageIs(p1, Ping.class).getNonce())); assertNextMessageIs(p1, GetDataMessage.class); newBlocks = blocks.subList(6, blocks.size()); filterAndSend(p1, newBlocks, newFilter); // Send a non-tx message so the peer knows the filtered block is over and force processing. inbound(p1, new Ping()); pingAndWait(p1); assertEquals(expectedBalance, wallet.getBalance()); assertEquals(blocks.get(blocks.size() - 1).getHash(), blockChain.getChainHead().getHeader().getHash()); } private void filterAndSend(InboundMessageQueuer p1, List<Block> blocks, BloomFilter filter) { for (Block block : blocks) { FilteredBlock fb = filter.applyAndUpdate(block); inbound(p1, fb); for (Transaction tx : fb.getAssociatedTransactions().values()) inbound(p1, tx); } } }