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);