From 35d6084bbf25960d3af5e72374f25c6613944a08 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Sun, 20 Jan 2013 23:23:50 +0100
Subject: [PATCH] Refactor some of the wallet/peer interaction. Analyze
dependencies of relevant pending transactions. The Peer object now asks each
connected Wallet if it cares about a transaction. If it does, then
receivePending() is not called immediately, but rather after downloading of
dependencies and with those dependencies. If any dependencies are time
locked, a new wallet property controls whether they are discarded or not.
---
.../java/com/google/bitcoin/core/Peer.java | 51 +++++--
.../com/google/bitcoin/core/PeerGroup.java | 10 +-
.../java/com/google/bitcoin/core/Wallet.java | 114 ++++++++++++----
.../google/bitcoin/core/PeerGroupTest.java | 3 +
.../com/google/bitcoin/core/PeerTest.java | 129 ++++++++++++++++--
.../com/google/bitcoin/core/WalletTest.java | 25 ++--
.../store/WalletProtobufSerializerTest.java | 2 +-
7 files changed, 278 insertions(+), 56 deletions(-)
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 bcf95332..e21b9cb5 100644
--- a/core/src/main/java/com/google/bitcoin/core/Peer.java
+++ b/core/src/main/java/com/google/bitcoin/core/Peer.java
@@ -69,6 +69,7 @@ public class Peer {
private int blocksAnnounced;
// A class that tracks recent transactions that have been broadcast across the network, counts how many
// peers announced them and updates the transaction confidence data. It is passed to each Peer.
+ // TODO: Make this final and unsynchronized.
private MemoryPool memoryPool;
// Each wallet added to the peer will be notified of downloaded transaction data.
private CopyOnWriteArrayList wallets;
@@ -415,13 +416,43 @@ public class Peer {
// We may get back a different transaction object.
tx = memoryPool.seen(tx, getAddress());
}
- if (maybeHandleRequestedData(tx))
+ final Transaction fTx = tx;
+ if (maybeHandleRequestedData(fTx))
return;
// Tell all wallets about this tx so they can check if it's relevant or not.
for (ListIterator it = wallets.listIterator(); it.hasNext();) {
- Wallet wallet = it.next();
+ final Wallet wallet = it.next();
try {
- wallet.receivePending(tx);
+ if (wallet.isPendingTransactionRelevant(fTx)) {
+ // This transaction seems interesting to us, so let's download its dependencies. This has several
+ // purposes: we can check that the sender isn't attacking us by engaging in protocol abuse games,
+ // like depending on a time-locked transaction that will never confirm, or building huge chains
+ // of unconfirmed transactions (again - so they don't confirm and the money can be taken
+ // back with a Finney attack). Knowing the dependencies also lets us store them in a serialized
+ // wallet so we always have enough data to re-announce to the network and get the payment into
+ // the chain, in case the sender goes away and the network starts to forget.
+ // TODO: Not all the above things are implemented.
+
+ Futures.addCallback(downloadDependencies(fTx), new FutureCallback>() {
+ public void onSuccess(List dependencies) {
+ try {
+ log.info("Dependency download complete!");
+ wallet.receivePending(fTx, dependencies);
+ } catch (VerificationException e) {
+ log.error("Wallet failed to process pending transaction {}", fTx.getHashAsString());
+ log.error("Error was: ", e);
+ // Not much more we can do at this point.
+ }
+ }
+
+ public void onFailure(Throwable throwable) {
+ log.error("Could not download dependencies of tx {}", fTx.getHashAsString());
+ log.error("Error was: ", throwable);
+ // Not much more we can do at this point.
+ }
+ });
+
+ }
} catch (VerificationException e) {
log.error("Wallet failed to verify tx", e);
// Carry on, listeners may still want to know.
@@ -446,7 +477,7 @@ public class Peer {
*
* For example, if tx has 2 inputs that connect to transactions A and B, and transaction B is unconfirmed and
* has one input connecting to transaction C that is unconfirmed, and transaction C connects to transaction D
- * that is in the chain, then this method will return either {B, C} or {C, B}.
+ * that is in the chain, then this method will return either {B, C} or {C, B}. No ordering is guaranteed.
*
* This method is useful for apps that want to learn about how long an unconfirmed transaction might take
* to confirm, by checking for unexpectedly time locked transactions, unusually deep dependency trees or fee-paying
@@ -492,11 +523,13 @@ public class Peer {
for (TransactionInput input : tx.getInputs()) {
// There may be multiple inputs that connect to the same transaction.
Sha256Hash hash = input.getOutpoint().getHash();
- Transaction dep = memoryPool.get(hash);
- if (dep == null) {
- needToRequest.add(hash);
- } else {
- dependencies.add(dep);
+ synchronized (this) {
+ Transaction dep = memoryPool.get(hash);
+ if (dep == null) {
+ needToRequest.add(hash);
+ } else {
+ dependencies.add(dep);
+ }
}
}
results.addAll(dependencies);
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 51e8d54b..18cc7d16 100644
--- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
+++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java
@@ -1074,7 +1074,10 @@ public class PeerGroup extends AbstractIdleService {
synchronized (PeerGroup.this) {
for (Wallet wallet : wallets) {
try {
- wallet.receivePending(pinnedTx);
+ if (wallet.isPendingTransactionRelevant(pinnedTx)) {
+ // Assumption here is there are no dependencies of the created transaction.
+ wallet.receivePending(pinnedTx, null);
+ }
} catch (Throwable t) {
future.setException(t);
return;
@@ -1106,7 +1109,10 @@ public class PeerGroup extends AbstractIdleService {
// wallet now we know it's valid.
for (Wallet wallet : wallets) {
try {
- wallet.receivePending(pinnedTx);
+ if (wallet.isPendingTransactionRelevant(pinnedTx)) {
+ // Assumption here is there are no dependencies of the created transaction.
+ wallet.receivePending(pinnedTx, null);
+ }
} catch (Throwable t) {
future.setException(t);
return;
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 9d6de798..8499e913 100644
--- a/core/src/main/java/com/google/bitcoin/core/Wallet.java
+++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java
@@ -192,6 +192,8 @@ public class Wallet implements Serializable, BlockChainListener {
// in receive() via Transaction.setBlockAppearance(). As the BlockChain always calls notifyNewBestBlock even if
// it sent transactions to the wallet, without this we'd double count.
private transient HashSet ignoreNextNewBlock;
+ // Whether or not to ignore nLockTime > 0 transactions that are received to the mempool.
+ private boolean acceptTimeLockedTransactions;
/**
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
@@ -226,6 +228,7 @@ public class Wallet implements Serializable, BlockChainListener {
invokeOnWalletChanged();
}
};
+ acceptTimeLockedTransactions = false;
}
public NetworkParameters getNetworkParameters() {
@@ -290,6 +293,28 @@ public class Wallet implements Serializable, BlockChainListener {
saveToFile(temp, f);
}
+ /**
+ * Whether or not the wallet will ignore transactions that have a lockTime parameter > 0. By default, all such
+ * transactions are ignored, because they are useful only in special protocols and such a transaction may not
+ * confirm as fast as an app typically expects. By setting this property to true, you are acknowledging that
+ * you understand what time-locked transactions are, and that your code is capable of handling them without risk.
+ * For instance you are not providing anything valuable in return for an unconfirmed transaction that has a lock
+ * time far in the future (which opens you up to Finney attacks).
+ *
+ * Note that this property is not serialized. So you have to set it to true each time you load or create a
+ * wallet.
+ */
+ public void setAcceptTimeLockedTransactions(boolean acceptTimeLockedTransactions) {
+ this.acceptTimeLockedTransactions = acceptTimeLockedTransactions;
+ }
+
+ /**
+ * See {@link Wallet#setAcceptTimeLockedTransactions(boolean)} for an explanation of this property.
+ */
+ public boolean doesAcceptTimeLockedTransactions() {
+ return acceptTimeLockedTransactions;
+ }
+
// Auto-saving can be done on a background thread if the user wishes it, this is to avoid stalling threads calling
// into the wallet on serialization/disk access all the time which is important in GUI apps where you don't want
// the main thread to ever wait on disk (otherwise you lose a lot of responsiveness). The primary case where it
@@ -640,35 +665,35 @@ public class Wallet implements Serializable, BlockChainListener {
receive(tx, block, blockType, false);
}
+ protected static class AnalysisResult {
+ // Which tx, if any, had a non-zero lock time.
+ Transaction timeLocked;
+ // In future, depth, fees, if any are non-standard, anything else that's interesting ...
+ }
+
/**
- * Called when we have found a transaction (via network broadcast or otherwise) that is relevant to this wallet
+ * Called when we have found a transaction (via network broadcast or otherwise) that is relevant to this wallet
* and want to record it. Note that we cannot verify these transactions at all, they may spend fictional
* coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon,
* and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not
- * be double spent as an optimization.
+ * be double spent as an optimization.
*
- * @param tx
- * @throws VerificationException
+ * Before this method is called, {@link Wallet#isPendingTransactionRelevant(Transaction)} should have been
+ * called to decide whether the wallet cares about the transaction - if it does, then this method expects the
+ * transaction and any dependencies it has which are still in the memory pool.
*/
- public synchronized void receivePending(Transaction tx) throws VerificationException {
- // Can run in a peer thread.
-
- // Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
- // between pools.
- EnumSet containingPools = getContainingPools(tx);
- if (!containingPools.equals(EnumSet.noneOf(Pool.class))) {
- log.debug("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString());
+ public synchronized void receivePending(Transaction tx, List dependencies) throws VerificationException {
+ // Can run in a peer thread. This method will only be called if a prior call to isPendingTransactionRelevant
+ // returned true, so we already know by this point that it sends coins to or from our wallet, or is a double
+ // spend against one of our other pending transactions.
+ //
+ // Do a brief risk analysis of the transaction and its dependencies to check for any possible attacks.
+ AnalysisResult analysis = analyzeTransactionAndDependencies(tx, dependencies);
+ if (analysis.timeLocked != null && !doesAcceptTimeLockedTransactions()) {
+ log.warn("Transaction {}, dependency of {} has a time lock value of {}", new Object[] {
+ analysis.timeLocked.getHashAsString(), tx.getHashAsString(), analysis.timeLocked.getLockTime()});
return;
}
-
- // We only care about transactions that:
- // - Send us coins
- // - Spend our coins
- if (!isTransactionRelevant(tx)) {
- log.debug("Received tx that isn't relevant to this wallet, discarding.");
- return;
- }
-
BigInteger valueSentToMe = tx.getValueSentToMe(this);
BigInteger valueSentFromMe = tx.getValueSentFromMe(this);
if (log.isInfoEnabled()) {
@@ -676,7 +701,6 @@ public class Wallet implements Serializable, BlockChainListener {
" and sends us %s BTC", tx.getHashAsString(), Utils.bitcoinValueToFriendlyString(valueSentFromMe),
Utils.bitcoinValueToFriendlyString(valueSentToMe)));
}
-
// Mark the tx as having been seen but is not yet in the chain. This will normally have been done already by
// the Peer before we got to this point, but in some cases (unit tests, other sources of transactions) it may
// have been missed out.
@@ -687,7 +711,6 @@ public class Wallet implements Serializable, BlockChainListener {
// txConfidenceListener wasn't added.
invokeOnTransactionConfidenceChanged(tx);
}
-
// If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This
// ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the
// timestamp on the transaction and registers/runs event listeners.
@@ -696,6 +719,51 @@ public class Wallet implements Serializable, BlockChainListener {
commitTx(tx);
}
+ private AnalysisResult analyzeTransactionAndDependencies(Transaction tx, List dependencies) {
+ AnalysisResult result = new AnalysisResult();
+ if (tx.getLockTime() > 0)
+ result.timeLocked = tx;
+ if (dependencies != null) {
+ for (Transaction dep : dependencies) {
+ if (dep.getLockTime() > 0) {
+ result.timeLocked = dep;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * This method is used by a {@link Peer} to find out if a transaction that has been announced is interesting,
+ * that is, whether we should bother downloading its dependencies and exploring the transaction to decide how
+ * risky it is. If this method returns true then {@link Wallet#receivePending(Transaction, java.util.List)}
+ * will soon be called with the transactions dependencies as well.
+ */
+ boolean isPendingTransactionRelevant(Transaction tx) throws ScriptException {
+ // Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
+ // between pools.
+ EnumSet containingPools = getContainingPools(tx);
+ if (!containingPools.equals(EnumSet.noneOf(Pool.class))) {
+ log.debug("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString());
+ return false;
+ }
+
+ // We only care about transactions that:
+ // - Send us coins
+ // - Spend our coins
+ if (!isTransactionRelevant(tx)) {
+ log.debug("Received tx that isn't relevant to this wallet, discarding.");
+ return false;
+ }
+
+ if (tx.getLockTime() > 0 && !acceptTimeLockedTransactions) {
+ log.warn("Received transaction {} with a lock time of {}, but not configured to accept these, discarding",
+ tx.getHashAsString(), tx.getLockTime());
+ return false;
+ }
+ return true;
+ }
+
// Boilerplate that allows event listeners to delete themselves during execution, and auto locks the listener.
private void invokeOnCoinsReceived(final Transaction tx, final BigInteger balance, final BigInteger newBalance) {
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker() {
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 7c41063c..18f6b479 100644
--- a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java
@@ -119,6 +119,9 @@ public class PeerGroupTest extends TestWithPeerGroup {
assertNull(outbound(p2)); // Only one peer is used to download.
inbound(p1, t1);
assertNull(outbound(p2));
+ // Asks for dependency.
+ GetDataMessage getdata = (GetDataMessage) outbound(p1);
+ inbound(p1, new NotFoundMessage(unitTestParams, getdata.getItems()));
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
peerGroup.stop();
}
diff --git a/core/src/test/java/com/google/bitcoin/core/PeerTest.java b/core/src/test/java/com/google/bitcoin/core/PeerTest.java
index 812ecc02..2ea52b7f 100644
--- a/core/src/test/java/com/google/bitcoin/core/PeerTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/PeerTest.java
@@ -26,6 +26,7 @@ import org.junit.Test;
import java.io.IOException;
import java.math.BigInteger;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
@@ -252,11 +253,13 @@ public class PeerTest extends TestWithNetworkConnections {
inv.addItem(item);
inbound(peer, inv);
// Peer hasn't seen it before, so will ask for it.
-
- GetDataMessage message = (GetDataMessage) event.getValue().getMessage();
- assertEquals(1, message.getItems().size());
- assertEquals(tx.getHash(), message.getItems().get(0).hash);
+ GetDataMessage getdata = (GetDataMessage) outbound();
+ assertEquals(1, getdata.getItems().size());
+ assertEquals(tx.getHash(), getdata.getItems().get(0).hash);
inbound(peer, tx);
+ // Ask for the dependency, it's not in the mempool (in chain).
+ getdata = (GetDataMessage) outbound();
+ inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@@ -475,19 +478,15 @@ public class PeerTest extends TestWithNetworkConnections {
@Test
public void recursiveDownload() throws Exception {
- // Our peer announces a normal transaction that depends on a different transaction that is time locked such
- // that it will never confirm. There's not currently any use case for doing that to us, so it's an attack.
- //
- // Peer should notice this by downloading all transaction dependencies and searching for timelocked ones.
- // Also, if a dependency chain is absurdly deep, the wallet shouldn't hear about it because it may just be
- // a different way to achieve the same thing (a payment that will not confirm for a very long time).
+ // Check that we can download all dependencies of an unconfirmed relevant transaction from the mempool.
+ ECKey to = new ECKey();
control.replay();
connect();
final Transaction[] onTx = new Transaction[1];
peer.addEventListener(new AbstractPeerEventListener() {
@Override
- public void onTransaction(Peer peer, Transaction t) {
+ public void onTransaction(Peer peer1, Transaction t) {
onTx[0] = t;
}
});
@@ -496,7 +495,6 @@ public class PeerTest extends TestWithNetworkConnections {
// t1 -> t2 -> [t5]
// -> t3 -> t4 -> [t6]
// The ones in brackets are assumed to be in the chain and are represented only by hashes.
- ECKey to = new ECKey();
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), to);
Sha256Hash t5 = t2.getInput(0).getOutpoint().getHash();
Transaction t4 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), new ECKey());
@@ -559,6 +557,113 @@ public class PeerTest extends TestWithNetworkConnections {
assertTrue(results.contains(t4));
}
+ @Test
+ public void timeLockedTransaction() throws Exception {
+ // Test that if we receive a relevant transaction that has a lock time, it doesn't result in a notification
+ // until we explicitly opt in to seeing those.
+ control.replay();
+ connect();
+
+ // Initial setup.
+ ECKey key = new ECKey();
+ Wallet wallet = new Wallet(unitTestParams);
+ wallet.addKey(key);
+ peer.addWallet(wallet);
+ final Transaction[] vtx = new Transaction[1];
+ wallet.addEventListener(new AbstractWalletEventListener() {
+ @Override
+ public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
+ vtx[0] = tx;
+ }
+ });
+ // Send a normal relevant transaction, it's received correctly.
+ Transaction t1 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), key);
+ inbound(peer, t1);
+ GetDataMessage getdata = (GetDataMessage) outbound();
+ inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
+ assertNotNull(vtx[0]);
+ vtx[0] = null;
+ // Send a timelocked transaction, nothing happens.
+ Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(2, 0), key);
+ t2.setLockTime(999999);
+ inbound(peer, t2);
+ assertNull(vtx[0]);
+ // Now we want to hear about them. Send another, we are told about it.
+ wallet.setAcceptTimeLockedTransactions(true);
+ inbound(peer, t2);
+ getdata = (GetDataMessage) outbound();
+ inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
+ assertEquals(t2, vtx[0]);
+ }
+
+ @Test
+ public void rejectTimeLockedDependency() throws Exception {
+ // Check that we also verify the lock times of dependencies. Otherwise an attacker could still build a tx that
+ // looks legitimate and useful but won't actually ever confirm, by sending us a normal tx that spends a
+ // timelocked tx.
+ checkTimeLockedDependency(false);
+ }
+
+ @Test
+ public void acceptTimeLockedDependency() throws Exception {
+ checkTimeLockedDependency(true);
+ }
+
+ private void checkTimeLockedDependency(boolean shouldAccept) throws Exception {
+ // Initial setup.
+ control.replay();
+ connect();
+ ECKey key = new ECKey();
+ Wallet wallet = new Wallet(unitTestParams);
+ wallet.addKey(key);
+ wallet.setAcceptTimeLockedTransactions(shouldAccept);
+ peer.addWallet(wallet);
+ final Transaction[] vtx = new Transaction[1];
+ wallet.addEventListener(new AbstractWalletEventListener() {
+ @Override
+ public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
+ vtx[0] = tx;
+ }
+ });
+ // t1 -> t2 [locked] -> t3 (not available)
+ Transaction t2 = new Transaction(unitTestParams);
+ t2.setLockTime(999999);
+ // Add a fake input to t3 that goes nowhere.
+ Sha256Hash t3 = Sha256Hash.create("abc".getBytes(Charset.forName("UTF-8")));
+ t2.addInput(new TransactionInput(unitTestParams, t2, new byte[]{}, new TransactionOutPoint(unitTestParams, 0,
+ t3)));
+ t2.addOutput(Utils.toNanoCoins(1, 0), new ECKey());
+ Transaction t1 = new Transaction(unitTestParams);
+ t1.addInput(t2.getOutput(0));
+ t1.addOutput(Utils.toNanoCoins(1, 0), key); // Make it relevant.
+ // Announce t1.
+ InventoryMessage inv = new InventoryMessage(unitTestParams);
+ inv.addTransaction(t1);
+ inbound(peer, inv);
+ // Send it.
+ GetDataMessage getdata = (GetDataMessage) outbound();
+ assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
+ inbound(peer, t1);
+ // Nothing arrived at our event listener yet.
+ assertNull(vtx[0]);
+ // We request t2.
+ getdata = (GetDataMessage) outbound();
+ assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
+ inbound(peer, t2);
+ // We request t3.
+ getdata = (GetDataMessage) outbound();
+ assertEquals(t3, getdata.getItems().get(0).hash);
+ // Can't find it: bottom of tree.
+ NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
+ notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t3));
+ inbound(peer, notFound);
+ // We're done but still not notified because it was timelocked.
+ if (shouldAccept)
+ assertNotNull(vtx[0]);
+ else
+ assertNull(vtx[0]);
+ }
+
// TODO: Use generics here to avoid unnecessary casting.
private Message outbound() {
List messages = event.getValues();
diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
index 670fb5fb..e245aabd 100644
--- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
@@ -443,7 +443,7 @@ public class WalletTest {
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
// t1 spends to our wallet. t2 double spends somewhere else.
- wallet.receivePending(doubleSpends.t1);
+ wallet.receivePending(doubleSpends.t1, null);
assertEquals(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN,
doubleSpends.t1.getConfidence().getConfidenceType());
BlockPair bp3 = createFakeBlock(params, blockStore, doubleSpends.t2);
@@ -484,12 +484,13 @@ public class WalletTest {
}
});
- wallet.receivePending(t1);
+ if (wallet.isPendingTransactionRelevant(t1))
+ wallet.receivePending(t1, null);
assertTrue(flags[0]);
assertTrue(flags[1]); // is pending
flags[0] = false;
// Check we don't get notified if we receive it again.
- wallet.receivePending(t1);
+ assertFalse(wallet.isPendingTransactionRelevant(t1));
assertFalse(flags[0]);
// Now check again, that we should NOT be notified when we receive it via a block (we were already notified).
// However the confidence should be updated.
@@ -513,7 +514,8 @@ public class WalletTest {
flags[0] = false;
flags[1] = false;
Transaction irrelevant = createFakeTx(params, nanos, new ECKey().toAddress(params));
- wallet.receivePending(irrelevant);
+ if (wallet.isPendingTransactionRelevant(irrelevant))
+ wallet.receivePending(irrelevant, null);
assertFalse(flags[0]);
assertEquals(2, walletChanged[0]);
}
@@ -542,7 +544,8 @@ public class WalletTest {
BigInteger halfNanos = Utils.toNanoCoins(0, 50);
Transaction t2 = wallet.createSend(new ECKey().toAddress(params), halfNanos);
// Now receive it as pending.
- wallet.receivePending(t2);
+ if (wallet.isPendingTransactionRelevant(t2))
+ wallet.receivePending(t2, null);
// We received an onCoinsSent() callback.
assertEquals(t2, txn[0]);
assertEquals(nanos, bigints[0]);
@@ -590,7 +593,8 @@ public class WalletTest {
});
assertEquals(BigInteger.ZERO, wallet.getBalance());
- wallet.receivePending(t1);
+ if (wallet.isPendingTransactionRelevant(t1))
+ wallet.receivePending(t1, null);
assertEquals(t1, called[0]);
assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now receive a double spend on the main chain.
@@ -724,7 +728,8 @@ public class WalletTest {
wallet.addKey(key1);
BigInteger value = toNanoCoins(5, 0);
Transaction t1 = createFakeTx(params, value, key1);
- wallet.receivePending(t1);
+ if (wallet.isPendingTransactionRelevant(t1))
+ wallet.receivePending(t1, null);
// TX should have been seen as relevant.
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertEquals(BigInteger.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
@@ -754,7 +759,8 @@ public class WalletTest {
assertFalse(hash1.equals(hash2)); // File has changed.
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
- wallet.receivePending(t1);
+ if (wallet.isPendingTransactionRelevant(t1))
+ wallet.receivePending(t1, null);
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
assertFalse(hash2.equals(hash3)); // File has changed again.
@@ -803,7 +809,8 @@ public class WalletTest {
results[0] = results[1] = null;
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
- wallet.receivePending(t1);
+ if (wallet.isPendingTransactionRelevant(t1))
+ wallet.receivePending(t1, null);
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
assertTrue(hash2.equals(hash3)); // File has NOT changed.
assertNull(results[0]);
diff --git a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java
index 89b8e7c3..9ce456e5 100644
--- a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java
+++ b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java
@@ -87,7 +87,7 @@ public class WalletProtobufSerializerTest {
// Check that we can serialize double spends correctly, as this is a slightly tricky case.
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
// t1 spends to our wallet.
- myWallet.receivePending(doubleSpends.t1);
+ myWallet.receivePending(doubleSpends.t1, null);
// t2 rolls back t1 and spends somewhere else.
myWallet.receiveFromBlock(doubleSpends.t2, null, BlockChain.NewBlockType.BEST_CHAIN);
Wallet wallet1 = roundTrip(myWallet);