/*
* Copyright 2011 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.feathercoin.core;
import com.google.feathercoin.discovery.PeerDiscovery;
import com.google.feathercoin.discovery.PeerDiscoveryException;
import com.google.feathercoin.store.MemoryBlockStore;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
public class PeerGroupTest extends TestWithPeerGroup {
static final NetworkParameters params = NetworkParameters.unitTests();
@Override
@Before
public void setUp() throws Exception {
super.setUp(new MemoryBlockStore(NetworkParameters.unitTests()));
peerGroup.addWallet(wallet);
}
@After
public void shutDown() throws Exception {
peerGroup.stopAndWait();
}
@Test
public void listener() throws Exception {
AbstractPeerEventListener listener = new AbstractPeerEventListener() {
};
peerGroup.addEventListener(listener);
assertTrue(peerGroup.removeEventListener(listener));
}
@Test
public void peerDiscoveryPolling() throws Exception {
// Check that if peer discovery fails, we keep trying until we have some nodes to talk with.
final Semaphore sem = new Semaphore(0);
final boolean[] result = new boolean[1];
result[0] = false;
peerGroup.addPeerDiscovery(new PeerDiscovery() {
public InetSocketAddress[] getPeers(long unused, TimeUnit unused2) throws PeerDiscoveryException {
if (result[0] == false) {
// Pretend we are not connected to the internet.
result[0] = true;
throw new PeerDiscoveryException("test failure");
} else {
// Return a bogus address.
sem.release();
return new InetSocketAddress[]{new InetSocketAddress("localhost", 0)};
}
}
public void shutdown() {
}
});
peerGroup.startAndWait();
sem.acquire();
// 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[0]);
}
@Test
public void receiveTxBroadcast() throws Exception {
// Check that when we receive transactions on all our peers, we do the right thing.
peerGroup.startAndWait();
// Create a couple of peers.
FakeChannel p1 = connectPeer(1);
FakeChannel 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);
BigInteger value = Utils.toNanoCoins(1, 0);
Transaction t1 = TestUtils.createFakeTx(unitTestParams, value, address);
InventoryMessage inv = new InventoryMessage(unitTestParams);
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(unitTestParams, getdata.getItems()));
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
peerGroup.stopAndWait();
}
@Test
public void singleDownloadPeer1() throws Exception {
// Check that we don't attempt to retrieve blocks on multiple peers.
peerGroup.startAndWait();
// Create a couple of peers.
FakeChannel p1 = connectPeer(1);
FakeChannel 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 = TestUtils.createFakeBlock(blockStore).block;
blockChain.add(b1);
Block b2 = TestUtils.makeSolvedTestBlock(b1);
Block b3 = TestUtils.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);
assertTrue(outbound(p1) instanceof GetDataMessage);
assertNull(outbound(p2));
// 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);
assertTrue(outbound(p2) instanceof GetDataMessage);
peerGroup.stop();
}
@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.startAndWait();
// Create a couple of peers.
FakeChannel p1 = connectPeer(1);
// Set up a little block chain.
Block b1 = TestUtils.createFakeBlock(blockStore).block;
Block b2 = TestUtils.makeSolvedTestBlock(b1);
Block b3 = TestUtils.makeSolvedTestBlock(b2);
// Expect a zero hash getblocks on p1. This is how the process starts.
peerGroup.startBlockChainDownload(new AbstractPeerEventListener() {
});
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.
FakeChannel p2 = connectPeer(2);
Message message = (Message)outbound(p2);
assertNull(message == null ? "" : message.toString(), message);
peerGroup.stop();
}
@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.
final Transaction[] event = new Transaction[2];
peerGroup.addEventListener(new AbstractPeerEventListener() {
@Override
public void onTransaction(Peer peer, Transaction t) {
event[0] = t;
}
});
FakeChannel p1 = connectPeer(1);
FakeChannel p2 = connectPeer(2);
FakeChannel p3 = connectPeer(3);
Transaction tx = TestUtils.createFakeTx(params, Utils.toNanoCoins(20, 0), address);
InventoryMessage inv = new InventoryMessage(params);
inv.addTransaction(tx);
// Peer 2 advertises the tx but does not receive it yet.
inbound(p2, inv);
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);
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()));
tx.getConfidence().addEventListener(new TransactionConfidence.Listener() {
public void onConfidenceChanged(Transaction tx) {
event[1] = tx;
}
});
// A straggler reports in.
inbound(p3, inv);
assertEquals(tx, event[1]);
assertEquals(3, tx.getConfidence().numBroadcastPeers());
assertTrue(tx.getConfidence().wasBroadcastBy(peerOf(p3).getAddress()));
}
@Test
public void announce() throws Exception {
// Make sure we can create spends, and that they are announced. Then do the same with offline mode.
// Set up connections and block chain.
FakeChannel p1 = connectPeer(1, new VersionMessage(params, 2));
FakeChannel p2 = connectPeer(2);
assertNotNull(peerGroup.getDownloadPeer());
control.replay();
peerGroup.setMinBroadcastConnections(2);
// Send ourselves a bit of money.
Block b1 = TestUtils.makeSolvedTestBlock(blockStore, address);
inbound(p1, b1);
assertNull(outbound(p1));
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
// Check that the wallet informs us of changes in confidence as the transaction ripples across the network.
final Transaction[] transactions = new Transaction[1];
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
transactions[0] = tx;
}
});
// Now create a spend, and expect the announcement on p1.
Address dest = new ECKey().toAddress(params);
Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, Utils.toNanoCoins(1, 0));
assertNotNull(sendResult.tx);
assertFalse(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
assertEquals(transactions[0].getConfidence().numBroadcastPeers(), 1);
transactions[0] = null;
Transaction t1 = (Transaction) outbound(p1);
assertNotNull(t1);
// 49 WDC in change.
assertEquals(Utils.toNanoCoins(49, 0), t1.getValueSentToMe(wallet));
// The future won't complete until it's heard back from the network on p2.
InventoryMessage inv = new InventoryMessage(params);
inv.addTransaction(t1);
inbound(p2, inv);
assertTrue(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
assertEquals(transactions[0].getConfidence().numBroadcastPeers(), 2);
// Confirm it.
Block b2 = TestUtils.createFakeBlock(blockStore, t1).block;
inbound(p1, b2);
assertNull(outbound(p1));
// Do the same thing with an offline transaction.
peerGroup.removeWallet(wallet);
Transaction t3 = wallet.sendCoinsOffline(Wallet.SendRequest.to(dest, Utils.toNanoCoins(2, 0)));
assertNull(outbound(p1)); // Nothing sent.
// Add the wallet to the peer group (simulate initialization). Transactions should be announced.
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);
assertEquals(t3.getHash(), inv1.getItems().get(0).hash);
// Peer asks for the transaction, and get it.
GetDataMessage getdata = new GetDataMessage(params);
getdata.addItem(inv1.getItems().get(0));
inbound(p1, getdata);
Transaction t4 = (Transaction) outbound(p1);
assertEquals(t3, t4);
FakeChannel p3 = connectPeer(3);
assertTrue(outbound(p3) instanceof InventoryMessage);
control.verify();
}
@Test
public void testWalletCatchupTime() throws Exception {
// Check the fast catchup time was initialized to something around the current runtime. The wallet was
// already added to the peer in setup.
long time = new Date().getTime() / 1000;
assertTrue(peerGroup.getFastCatchupTimeSecs() > time - 10000);
Wallet w2 = new Wallet(params);
ECKey key1 = new ECKey();
key1.setCreationTimeSeconds(time - 86400); // One day ago.
w2.addKey(key1);
peerGroup.addWallet(w2);
assertEquals(peerGroup.getFastCatchupTimeSecs(), time - 86400);
// Adding a key to the wallet should update the fast catchup time.
ECKey key2 = new ECKey();
key2.setCreationTimeSeconds(time - 100000);
w2.addKey(key2);
assertEquals(peerGroup.getFastCatchupTimeSecs(), time - 100000);
}
@Test
public void noPings() throws Exception {
peerGroup.startAndWait();
peerGroup.setPingIntervalMsec(0);
VersionMessage versionMessage = new VersionMessage(params, 2);
versionMessage.clientVersion = Pong.MIN_PROTOCOL_VERSION;
connectPeer(1, versionMessage);
assertFalse(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
}
@Test
public void pings() throws Exception {
peerGroup.startAndWait();
peerGroup.setPingIntervalMsec(100);
VersionMessage versionMessage = new VersionMessage(params, 2);
versionMessage.clientVersion = Pong.MIN_PROTOCOL_VERSION;
FakeChannel p1 = connectPeer(1, versionMessage);
Ping ping = (Ping) outbound(p1);
inbound(p1, new Pong(ping.getNonce()));
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.startAndWait();
VersionMessage versionMessage2 = new VersionMessage(params, 2);
versionMessage2.clientVersion = 60000;
VersionMessage versionMessage3 = new VersionMessage(params, 3);
versionMessage3.clientVersion = 60000;
assertNull(peerGroup.getDownloadPeer());
Peer a = PeerGroup.peerFromChannel(connectPeer(1, versionMessage2));
assertEquals(2, peerGroup.getMostCommonChainHeight());
assertEquals(a, peerGroup.getDownloadPeer());
PeerGroup.peerFromChannel(connectPeer(2, versionMessage2));
assertEquals(2, peerGroup.getMostCommonChainHeight());
assertEquals(a, peerGroup.getDownloadPeer()); // No change.
Peer c = PeerGroup.peerFromChannel(connectPeer(3, versionMessage3));
assertEquals(2, peerGroup.getMostCommonChainHeight());
assertEquals(a, peerGroup.getDownloadPeer()); // No change yet.
PeerGroup.peerFromChannel(connectPeer(4, versionMessage3));
assertEquals(3, peerGroup.getMostCommonChainHeight());
assertEquals(c, peerGroup.getDownloadPeer()); // Switch to first peer advertising new height.
// New peer with a higher protocol version but same chain height.
VersionMessage versionMessage4 = new VersionMessage(params, 3);
versionMessage4.clientVersion = 100000;
Peer d = PeerGroup.peerFromChannel(connectPeer(5, versionMessage4));
assertEquals(d, peerGroup.getDownloadPeer());
}
}