/*
* Copyright 2013 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.util.concurrent.*;
import org.bitcoinj.core.listeners.TransactionConfidenceEventListener;
import org.bitcoinj.testing.*;
import org.bitcoinj.utils.*;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.junit.*;
import org.junit.runner.*;
import org.junit.runners.*;
import java.util.*;
import java.util.concurrent.*;
import static com.google.common.base.Preconditions.*;
import static org.bitcoinj.core.Coin.*;
import static org.junit.Assert.*;
@RunWith(value = Parameterized.class)
public class TransactionBroadcastTest extends TestWithPeerGroup {
@Parameterized.Parameters
public static Collection<ClientType[]> parameters() {
return Arrays.asList(new ClientType[] {ClientType.NIO_CLIENT_MANAGER},
new ClientType[] {ClientType.BLOCKING_CLIENT_MANAGER});
}
public TransactionBroadcastTest(ClientType clientType) {
super(clientType);
}
@Override
@Before
public void setUp() throws Exception {
Utils.setMockClock(); // Use mock clock
super.setUp();
// Fix the random permutation that TransactionBroadcast uses to shuffle the peers.
TransactionBroadcast.random = new Random(0);
peerGroup.setMinBroadcastConnections(2);
peerGroup.start();
}
@Override
@After
public void tearDown() {
super.tearDown();
}
@Test
public void fourPeers() throws Exception {
InboundMessageQueuer[] channels = { connectPeer(1), connectPeer(2), connectPeer(3), connectPeer(4) };
Transaction tx = new Transaction(PARAMS);
tx.getConfidence().setSource(TransactionConfidence.Source.SELF);
TransactionBroadcast broadcast = new TransactionBroadcast(peerGroup, tx);
final AtomicDouble lastProgress = new AtomicDouble();
broadcast.setProgressCallback(new TransactionBroadcast.ProgressCallback() {
@Override
public void onBroadcastProgress(double progress) {
lastProgress.set(progress);
}
});
ListenableFuture<Transaction> future = broadcast.broadcast();
assertFalse(future.isDone());
assertEquals(0.0, lastProgress.get(), 0.0);
// We expect two peers to receive a tx message, and at least one of the others must announce for the future to
// complete successfully.
Message[] messages = {
outbound(channels[0]),
outbound(channels[1]),
outbound(channels[2]),
outbound(channels[3])
};
// 0 and 3 are randomly selected to receive the broadcast.
assertEquals(tx, messages[0]);
assertEquals(tx, messages[3]);
assertNull(messages[1]);
assertNull(messages[2]);
Threading.waitForUserCode();
assertFalse(future.isDone());
assertEquals(0.0, lastProgress.get(), 0.0);
inbound(channels[1], InventoryMessage.with(tx));
future.get();
Threading.waitForUserCode();
assertEquals(1.0, lastProgress.get(), 0.0);
// There is no response from the Peer as it has nothing to do.
assertNull(outbound(channels[1]));
}
@Test
public void lateProgressCallback() throws Exception {
// Check that if we register a progress callback on a broadcast after the broadcast has started, it's invoked
// immediately with the latest state. This avoids API users writing accidentally racy code when they use
// a convenience method like peerGroup.broadcastTransaction.
InboundMessageQueuer[] channels = { connectPeer(1), connectPeer(2), connectPeer(3), connectPeer(4) };
Transaction tx = FakeTxBuilder.createFakeTx(PARAMS, CENT, address);
tx.getConfidence().setSource(TransactionConfidence.Source.SELF);
TransactionBroadcast broadcast = peerGroup.broadcastTransaction(tx);
inbound(channels[1], InventoryMessage.with(tx));
pingAndWait(channels[1]);
final AtomicDouble p = new AtomicDouble();
broadcast.setProgressCallback(new TransactionBroadcast.ProgressCallback() {
@Override
public void onBroadcastProgress(double progress) {
p.set(progress);
}
}, Threading.SAME_THREAD);
assertEquals(1.0, p.get(), 0.01);
}
@Test
public void rejectHandling() throws Exception {
InboundMessageQueuer[] channels = { connectPeer(0), connectPeer(1), connectPeer(2), connectPeer(3), connectPeer(4) };
Transaction tx = new Transaction(PARAMS);
TransactionBroadcast broadcast = new TransactionBroadcast(peerGroup, tx);
ListenableFuture<Transaction> future = broadcast.broadcast();
// 0 and 3 are randomly selected to receive the broadcast.
assertEquals(tx, outbound(channels[1]));
assertEquals(tx, outbound(channels[2]));
assertEquals(tx, outbound(channels[4]));
RejectMessage reject = new RejectMessage(PARAMS, RejectMessage.RejectCode.DUST, tx.getHash(), "tx", "dust");
inbound(channels[1], reject);
inbound(channels[4], reject);
try {
future.get();
fail();
} catch (ExecutionException e) {
assertEquals(RejectedTransactionException.class, e.getCause().getClass());
}
}
@Test
public void retryFailedBroadcast() throws Exception {
// If we create a spend, it's sent to a peer that swallows it, and the peergroup is removed/re-added then
// the tx should be broadcast again.
InboundMessageQueuer p1 = connectPeer(1);
connectPeer(2);
// Send ourselves a bit of money.
Block b1 = FakeTxBuilder.makeSolvedTestBlock(blockStore, address);
inbound(p1, b1);
assertNull(outbound(p1));
assertEquals(FIFTY_COINS, wallet.getBalance());
// Now create a spend, and expect the announcement on p1.
Address dest = new ECKey().toAddress(PARAMS);
Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, COIN);
assertFalse(sendResult.broadcastComplete.isDone());
Transaction t1;
{
Message m;
while (!((m = outbound(p1)) instanceof Transaction));
t1 = (Transaction) m;
}
assertFalse(sendResult.broadcastComplete.isDone());
// p1 eats it :( A bit later the PeerGroup is taken down.
peerGroup.removeWallet(wallet);
peerGroup.addWallet(wallet);
// We want to hear about it again. Now, because we've disabled the randomness for the unit tests it will
// re-appear on p1 again. Of course in the real world it would end up with a different set of peers and
// select randomly so we get a second chance.
Transaction t2 = (Transaction) outbound(p1);
assertEquals(t1, t2);
}
@Test
public void peerGroupWalletIntegration() 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.
VersionMessage ver = new VersionMessage(PARAMS, 2);
ver.localServices = VersionMessage.NODE_NETWORK;
InboundMessageQueuer p1 = connectPeer(1, ver);
InboundMessageQueuer p2 = connectPeer(2);
// Send ourselves a bit of money.
Block b1 = FakeTxBuilder.makeSolvedTestBlock(blockStore, address);
inbound(p1, b1);
pingAndWait(p1);
assertNull(outbound(p1));
assertEquals(FIFTY_COINS, 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.addTransactionConfidenceEventListener(new TransactionConfidenceEventListener() {
@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, COIN);
assertNotNull(sendResult.tx);
Threading.waitForUserCode();
assertFalse(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
assertEquals(0, transactions[0].getConfidence().numBroadcastPeers());
transactions[0] = null;
Transaction t1;
{
peerGroup.waitForJobQueue();
Message m = outbound(p1);
// Hack: bloom filters are recalculated asynchronously to sending transactions to avoid lock
// inversion, so we might or might not get the filter/mempool message first or second.
while (!(m instanceof Transaction)) m = outbound(p1);
t1 = (Transaction) m;
}
assertNotNull(t1);
// 49 BTC in change.
assertEquals(valueOf(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);
pingAndWait(p2);
Threading.waitForUserCode();
assertTrue(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
assertEquals(1, transactions[0].getConfidence().numBroadcastPeers());
// Confirm it.
Block b2 = FakeTxBuilder.createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS, t1).block;
inbound(p1, b2);
pingAndWait(p1);
assertNull(outbound(p1));
// Do the same thing with an offline transaction.
peerGroup.removeWallet(wallet);
SendRequest req = SendRequest.to(dest, valueOf(2, 0));
Transaction t3 = checkNotNull(wallet.sendCoinsOffline(req));
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. No extra Bloom filter because no change address was needed.
assertEquals(t3.getHash(), outbound(p1).getHash());
}
}