From 293591bf245bc9515d6cd7d2623dbe8cbfb321b0 Mon Sep 17 00:00:00 2001 From: Oscar Guindzberg Date: Wed, 11 Mar 2015 19:57:58 -0300 Subject: [PATCH] Support double spend forwarding This adds a new IN_CONFLICT transaction confidence type, meaning there is another transaction (or several other transactions) spending one (or several) of its inputs but nor this transaction nor the other/s transaction/s are included in the best chain. --- .../bitcoinj/core/TransactionConfidence.java | 18 +- .../main/java/org/bitcoinj/core/Wallet.java | 186 ++++++-- .../store/WalletProtobufSerializer.java | 1 + .../org/bitcoinj/testing/FakeTxBuilder.java | 33 +- .../main/java/org/bitcoinj/wallet/Protos.java | 9 + .../java/org/bitcoinj/core/WalletTest.java | 406 +++++++++++++++++- 6 files changed, 609 insertions(+), 44 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java b/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java index aef63bc1..e8394212 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java @@ -96,6 +96,15 @@ public class TransactionConfidence { */ DEAD(4), + /** + * If IN_CONFLICT, then it means there is another transaction (or several other transactions) spending one + * (or several) of its inputs but nor this transaction nor the other/s transaction/s are included in the best chain. + * The other/s transaction/s should be IN_CONFLICT too. + * IN_CONFLICT can be thought as an intermediary state between a) PENDING and BUILDING or b) PENDING and DEAD. + * Another common name for this situation is "double spend". + */ + IN_CONFLICT(5), + /** * If a transaction hasn't been broadcast yet, or there's no record of it, its confidence is UNKNOWN. */ @@ -260,7 +269,7 @@ public class TransactionConfidence { if (confidenceType != ConfidenceType.DEAD) { overridingTransaction = null; } - if (confidenceType == ConfidenceType.PENDING) { + if (confidenceType == ConfidenceType.PENDING || confidenceType == ConfidenceType.IN_CONFLICT) { depth = 0; appearedAtChainHeight = -1; } @@ -323,6 +332,9 @@ public class TransactionConfidence { case PENDING: builder.append("Pending/unconfirmed."); break; + case IN_CONFLICT: + builder.append("In conflict."); + break; case BUILDING: builder.append(String.format("Appeared in best chain at height %d, depth %d.", getAppearedAtChainHeight(), getDepthInBlocks())); @@ -377,12 +389,12 @@ public class TransactionConfidence { * store this information. * * @return the transaction that double spent this one - * @throws IllegalStateException if confidence type is not OVERRIDDEN_BY_DOUBLE_SPEND. + * @throws IllegalStateException if confidence type is not DEAD. */ public synchronized Transaction getOverridingTransaction() { if (getConfidenceType() != ConfidenceType.DEAD) throw new IllegalStateException("Confidence type is " + getConfidenceType() + - ", not OVERRIDDEN_BY_DOUBLE_SPEND"); + ", not DEAD"); return overridingTransaction; } diff --git a/core/src/main/java/org/bitcoinj/core/Wallet.java b/core/src/main/java/org/bitcoinj/core/Wallet.java index f806abe4..cf00beeb 100644 --- a/core/src/main/java/org/bitcoinj/core/Wallet.java +++ b/core/src/main/java/org/bitcoinj/core/Wallet.java @@ -105,9 +105,9 @@ public class Wallet extends BaseTaggableObject // The various pools below give quick access to wallet-relevant transactions by the state they're in: // // Pending: Transactions that didn't make it into the best chain yet. Pending transactions can be killed if a - // double-spend against them appears in the best chain, in which case they move to the dead pool. - // If a double-spend appears in the pending state as well, currently we just ignore the second - // and wait for the miners to resolve the race. + // double spend against them appears in the best chain, in which case they move to the dead pool. + // If a double spend appears in the pending state as well, we update the confidence type + // of all txns in conflict to IN_CONFLICT and wait for the miners to resolve the race. // Unspent: Transactions that appeared in the best chain and have outputs we can spend. Note that we store the // entire transaction in memory even though for spending purposes we only really need the outputs, the // reason being that this simplifies handling of re-orgs. It would be worth fixing this in future. @@ -1692,6 +1692,7 @@ public class Wallet extends BaseTaggableObject // We only care about transactions that: // - Send us coins // - Spend our coins + // - Double spend a tx in our wallet if (!isTransactionRelevant(tx)) { log.debug("Received tx that isn't relevant to this wallet, discarding."); return false; @@ -1715,41 +1716,64 @@ public class Wallet extends BaseTaggableObject try { return tx.getValueSentFromMe(this).signum() > 0 || tx.getValueSentToMe(this).signum() > 0 || - checkForDoubleSpendAgainstPending(tx, false); + !findDoubleSpendsAgainst(tx, transactions).isEmpty(); } finally { lock.unlock(); } } /** - * Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if + * Finds transactions in the specified candidates that double spend "tx". Not a general check, but it can work even if * the double spent inputs are not ours. + * @return The set of transactions that double spend "tx". */ - private boolean checkForDoubleSpendAgainstPending(Transaction tx, boolean takeAction) { + private Set findDoubleSpendsAgainst(Transaction tx, Map candidates) { checkState(lock.isHeldByCurrentThread()); + if (tx.isCoinBase()) return Sets.newHashSet(); // Compile a set of outpoints that are spent by tx. HashSet outpoints = new HashSet(); for (TransactionInput input : tx.getInputs()) { outpoints.add(input.getOutpoint()); } // Now for each pending transaction, see if it shares any outpoints with this tx. - LinkedList doubleSpentTxns = Lists.newLinkedList(); - for (Transaction p : pending.values()) { + Set doubleSpendTxns = Sets.newHashSet(); + for (Transaction p : candidates.values()) { for (TransactionInput input : p.getInputs()) { // This relies on the fact that TransactionOutPoint equality is defined at the protocol not object // level - outpoints from two different inputs that point to the same output compare the same. TransactionOutPoint outpoint = input.getOutpoint(); if (outpoints.contains(outpoint)) { - // It does, it's a double spend against the pending pool, which makes it relevant. - if (!doubleSpentTxns.isEmpty() && doubleSpentTxns.getLast() == p) continue; - doubleSpentTxns.add(p); + // It does, it's a double spend against the candidates, which makes it relevant. + doubleSpendTxns.add(p); } } } - if (takeAction && !doubleSpentTxns.isEmpty()) { - killTx(tx, doubleSpentTxns); + return doubleSpendTxns; + } + + /** + * Adds to txSet all the txns in txPool spending outputs of txns in txSet, + * and all txns spending the outputs of those txns, recursively. + */ + void addTransactionsDependingOn(Set txSet, Set txPool) { + Map txQueue = new LinkedHashMap(); + for (Transaction tx : txSet) { + txQueue.put(tx.getHash(), tx); + } + while(!txQueue.isEmpty()) { + Transaction tx = txQueue.remove(txQueue.keySet().iterator().next()); + for (Transaction anotherTx : txPool) { + if (anotherTx.equals(tx)) continue; + for (TransactionInput input : anotherTx.getInputs()) { + if (input.getOutpoint().getHash().equals(tx.getHash())) { + if (txQueue.get(anotherTx.getHash()) == null) { + txQueue.put(anotherTx.getHash(), anotherTx); + txSet.add(anotherTx); + } + } + } + } } - return !doubleSpentTxns.isEmpty(); } /** @@ -1824,6 +1848,9 @@ public class Wallet extends BaseTaggableObject log.info(" <-pending"); if (bestChain) { + boolean wasDead = dead.remove(txHash) != null; + if (wasDead) + log.info(" <-dead"); if (wasPending) { // Was pending and is now confirmed. Disconnect the outputs in case we spent any already: they will be // re-connected by processTxFromBestChain below. @@ -1835,7 +1862,7 @@ public class Wallet extends BaseTaggableObject } } } - processTxFromBestChain(tx, wasPending); + processTxFromBestChain(tx, wasPending || wasDead); } else { checkState(sideChain); // Transactions that appear in a side chain will have that appearance recorded below - we assume that @@ -1849,7 +1876,7 @@ public class Wallet extends BaseTaggableObject // Ignore the case where a tx appears on a side chain at the same time as the best chain (this is // quite normal and expected). Sha256Hash hash = tx.getHash(); - if (!unspent.containsKey(hash) && !spent.containsKey(hash)) { + if (!unspent.containsKey(hash) && !spent.containsKey(hash) && !dead.containsKey(hash)) { // Otherwise put it (possibly back) into pending. // Committing it updates the spent flags and inserts into the pool as well. commitTx(tx); @@ -1866,6 +1893,22 @@ public class Wallet extends BaseTaggableObject // this method has been called by BlockChain for all relevant transactions. Otherwise we'd double // count. ignoreNextNewBlock.add(txHash); + + // When a tx is received from the best chain, if other txns that spend this tx are IN_CONFLICT, + // change its confidence to PENDING (Unless they are also spending other txns IN_CONFLICT). + // Consider dependency chains. + Set currentTxDependencies = Sets.newHashSet(tx); + addTransactionsDependingOn(currentTxDependencies, getTransactions(true)); + currentTxDependencies.remove(tx); + List currentTxDependenciesSorted = sortTxnsByDependency(currentTxDependencies); + for (Transaction txDependency : currentTxDependenciesSorted) { + if (txDependency.getConfidence().getConfidenceType().equals(ConfidenceType.IN_CONFLICT)) { + if (isNotSpendingTxnsInConfidenceType(txDependency, ConfidenceType.IN_CONFLICT)) { + txDependency.getConfidence().setConfidenceType(ConfidenceType.PENDING); + confidenceChanged.put(txDependency, TransactionConfidence.Listener.ChangeReason.TYPE); + } + } + } } } @@ -1909,6 +1952,53 @@ public class Wallet extends BaseTaggableObject hardSaveOnNextBlock = true; } + /** Finds if tx is NOT spending other txns which are in the specified confidence type */ + private boolean isNotSpendingTxnsInConfidenceType(Transaction tx, ConfidenceType confidenceType) { + for (TransactionInput txInput : tx.getInputs()) { + Transaction connectedTx = this.getTransaction(txInput.getOutpoint().getHash()); + if (connectedTx != null && connectedTx.getConfidence().getConfidenceType().equals(confidenceType)) { + return false; + } + } + return true; + } + + /** + * Creates and returns a new List with the same txns as inputSet + * but txns are sorted by depencency (a topological sort). + * If tx B spends tx A, then tx A should be before tx B on the returned List. + * Several invocations to this method with the same inputSet could result in lists with txns in different order, + * as there is no guarantee on the order of the returned txns besides what was already stated. + */ + List sortTxnsByDependency(Set inputSet) { + ArrayList result = new ArrayList(inputSet); + for (int i = 0; i < result.size()-1; i++) { + boolean txAtISpendsOtherTxInTheList; + do { + txAtISpendsOtherTxInTheList = false; + for (int j = i+1; j < result.size(); j++) { + if (spends(result.get(i), result.get(j))) { + Transaction transactionAtI = result.remove(i); + result.add(j, transactionAtI); + txAtISpendsOtherTxInTheList = true; + break; + } + } + } while (txAtISpendsOtherTxInTheList); + } + return result; + } + + /** Finds whether txA spends txB */ + boolean spends(Transaction txA, Transaction txB) { + for (TransactionInput txInput : txA.getInputs()) { + if (txInput.getOutpoint().getHash().equals(txB.getHash())) { + return true; + } + } + return false; + } + private void informConfidenceListenersIfNotReorganizing() { if (insideReorg) return; @@ -2032,7 +2122,12 @@ public class Wallet extends BaseTaggableObject addWalletTransaction(Pool.SPENT, tx); } - checkForDoubleSpendAgainstPending(tx, true); + // Kill txns in conflict with this tx + Set doubleSpendTxns = findDoubleSpendsAgainst(tx, pending); + if (!doubleSpendTxns.isEmpty()) { + // no need to addTransactionsDependingOn(doubleSpendTxns) because killTxns() already kills dependencies; + killTxns(doubleSpendTxns, tx); + } } /** @@ -2078,7 +2173,7 @@ public class Wallet extends BaseTaggableObject // Can be: // (1) We already marked this output as spent when we saw the pending transaction (most likely). // Now it's being confirmed of course, we cannot mark it as spent again. - // (2) A double spend from chain: this will be handled later by checkForDoubleSpendAgainstPending. + // (2) A double spend from chain: this will be handled later by findDoubleSpendsAgainst()/killTxns(). // // In any case, nothing to do here. } else { @@ -2137,8 +2232,8 @@ public class Wallet extends BaseTaggableObject } // Updates the wallet when a double spend occurs. overridingTx can be null for the case of coinbases - private void killTx(@Nullable Transaction overridingTx, List killedTx) { - LinkedList work = new LinkedList(killedTx); + private void killTxns(Set txnsToKill, @Nullable Transaction overridingTx) { + LinkedList work = new LinkedList(txnsToKill); while (!work.isEmpty()) { final Transaction tx = work.poll(); log.warn("TX {} killed{}", tx.getHashAsString(), @@ -2152,7 +2247,7 @@ public class Wallet extends BaseTaggableObject for (TransactionInput deadInput : tx.getInputs()) { Transaction connected = deadInput.getOutpoint().fromTx; if (connected == null) continue; - if (connected.getConfidence().getConfidenceType() != ConfidenceType.DEAD) { + if (connected.getConfidence().getConfidenceType() != ConfidenceType.DEAD && deadInput.getConnectedOutput().getSpentBy() != null && deadInput.getConnectedOutput().getSpentBy().equals(deadInput)) { checkState(myUnspents.add(deadInput.getConnectedOutput())); log.info("Added to UNSPENTS: {} in {}", deadInput.getConnectedOutput(), deadInput.getConnectedOutput().getParentTransaction().getHash()); } @@ -2240,12 +2335,42 @@ public class Wallet extends BaseTaggableObject // move any transactions that are now fully spent to the spent map so we can skip them when creating future // spends. updateForSpends(tx, false); - // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain. - // This also registers txConfidenceListener so wallet listeners get informed. - log.info("->pending: {}", tx.getHashAsString()); - tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); - confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE); - addWalletTransaction(Pool.PENDING, tx); + + Set doubleSpendPendingTxns = findDoubleSpendsAgainst(tx, pending); + Set doubleSpendUnspentTxns = findDoubleSpendsAgainst(tx, unspent); + Set doubleSpendSpentTxns = findDoubleSpendsAgainst(tx, spent); + + if (!doubleSpendUnspentTxns.isEmpty() || + !doubleSpendSpentTxns.isEmpty() || + !isNotSpendingTxnsInConfidenceType(tx, ConfidenceType.DEAD)) { + // tx is a double spend against a tx already in the best chain or spends outputs of a DEAD tx. + // Add tx to the dead pool and schedule confidence listener notifications. + log.info("->dead: {}", tx.getHashAsString()); + tx.getConfidence().setConfidenceType(ConfidenceType.DEAD); + confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE); + addWalletTransaction(Pool.DEAD, tx); + } else if (!doubleSpendPendingTxns.isEmpty() || + !isNotSpendingTxnsInConfidenceType(tx, ConfidenceType.IN_CONFLICT)) { + // tx is a double spend against a pending tx or spends outputs of a tx already IN_CONFLICT. + // Add tx to the pending pool. Update the confidence type of tx, the txns in conflict with tx and all + // their dependencies to IN_CONFLICT and schedule confidence listener notifications. + log.info("->pending (IN_CONFLICT): {}", tx.getHashAsString()); + addWalletTransaction(Pool.PENDING, tx); + doubleSpendPendingTxns.add(tx); + addTransactionsDependingOn(doubleSpendPendingTxns, getTransactions(true)); + for (Transaction doubleSpendTx : doubleSpendPendingTxns) { + doubleSpendTx.getConfidence().setConfidenceType(ConfidenceType.IN_CONFLICT); + confidenceChanged.put(doubleSpendTx, TransactionConfidence.Listener.ChangeReason.TYPE); + } + } else { + // No conflict detected. + // Add to the pending pool and schedule confidence listener notifications. + log.info("->pending: {}", tx.getHashAsString()); + tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); + confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE); + addWalletTransaction(Pool.PENDING, tx); + } + // Mark any keys used in the outputs as "used", this allows wallet UI's to auto-advance the current key // they are showing to the user in qr codes etc. markKeysAsUsed(tx); @@ -2494,10 +2619,10 @@ public class Wallet extends BaseTaggableObject } } - private static void addWalletTransactionsToSet(Set txs, + private static void addWalletTransactionsToSet(Set txns, Pool poolType, Collection pool) { for (Transaction tx : pool) { - txs.add(new WalletTransaction(poolType, tx)); + txns.add(new WalletTransaction(poolType, tx)); } } @@ -4274,7 +4399,7 @@ public class Wallet extends BaseTaggableObject // this coinbase tx. Some can just go pending forever, like the Satoshi client. However we // can do our best. log.warn("Coinbase killed by re-org: {}", tx.getHashAsString()); - killTx(null, ImmutableList.of(tx)); + killTxns(ImmutableSet.of(tx), null); } else { for (TransactionOutput output : tx.getOutputs()) { TransactionInput input = output.getSpentBy(); @@ -4299,6 +4424,7 @@ public class Wallet extends BaseTaggableObject // there's another re-org. if (tx.isCoinBase()) continue; log.info(" ->pending {}", tx.getHash()); + tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); // Wipe height/depth/work data. confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE); addWalletTransaction(Pool.PENDING, tx); diff --git a/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java b/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java index 2e058397..b547c119 100644 --- a/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java +++ b/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java @@ -733,6 +733,7 @@ public class WalletProtobufSerializer { // These two are equivalent (must be able to read old wallets). case NOT_IN_BEST_CHAIN: confidenceType = ConfidenceType.PENDING; break; case PENDING: confidenceType = ConfidenceType.PENDING; break; + case IN_CONFLICT: confidenceType = ConfidenceType.IN_CONFLICT; break; case UNKNOWN: // Fall through. default: diff --git a/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java b/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java index 01d82a94..ea40191f 100644 --- a/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java +++ b/core/src/main/java/org/bitcoinj/testing/FakeTxBuilder.java @@ -175,13 +175,13 @@ public class FakeTxBuilder { Coin value = COIN; Address someBadGuy = new ECKey().toAddress(params); - doubleSpends.t1 = new Transaction(params); - TransactionOutput o1 = new TransactionOutput(params, doubleSpends.t1, value, to); - doubleSpends.t1.addOutput(o1); - doubleSpends.prevTx = new Transaction(params); TransactionOutput prevOut = new TransactionOutput(params, doubleSpends.prevTx, value, someBadGuy); doubleSpends.prevTx.addOutput(prevOut); + + doubleSpends.t1 = new Transaction(params); + TransactionOutput o1 = new TransactionOutput(params, doubleSpends.t1, value, to); + doubleSpends.t1.addOutput(o1); doubleSpends.t1.addInput(prevOut); doubleSpends.t2 = new Transaction(params); @@ -209,14 +209,14 @@ public class FakeTxBuilder { return createFakeBlock(blockStore, version, timeSeconds, 0, transactions); } - /** Emulates receiving a valid block that builds on top of the chain. */ - public static BlockPair createFakeBlock(BlockStore blockStore, long version, + /** Emulates receiving a valid block */ + public static BlockPair createFakeBlock(BlockStore blockStore, StoredBlock previousStoredBlock, long version, long timeSeconds, int height, Transaction... transactions) { try { - Block chainHead = blockStore.getChainHead().getHeader(); - Address to = new ECKey().toAddress(chainHead.getParams()); - Block b = chainHead.createNextBlock(to, version, timeSeconds, height); + Block previousBlock = previousStoredBlock.getHeader(); + Address to = new ECKey().toAddress(previousBlock.getParams()); + Block b = previousBlock.createNextBlock(to, version, timeSeconds, height); // Coinbase tx was already added. for (Transaction tx : transactions) { tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK); @@ -225,7 +225,7 @@ public class FakeTxBuilder { b.solve(); BlockPair pair = new BlockPair(); pair.block = b; - pair.storedBlock = blockStore.getChainHead().build(b); + pair.storedBlock = previousStoredBlock.build(b); blockStore.put(pair.storedBlock); blockStore.setChainHead(pair.storedBlock); return pair; @@ -236,6 +236,19 @@ public class FakeTxBuilder { } } + public static BlockPair createFakeBlock(BlockStore blockStore, StoredBlock previousStoredBlock, int height, Transaction... transactions) { + return createFakeBlock(blockStore, previousStoredBlock, Block.BLOCK_VERSION_BIP66, Utils.currentTimeSeconds(), height, transactions); + } + + /** Emulates receiving a valid block that builds on top of the chain. */ + public static BlockPair createFakeBlock(BlockStore blockStore, long version, long timeSeconds, int height, Transaction... transactions) { + try { + return createFakeBlock(blockStore, blockStore.getChainHead(), version, timeSeconds, height, transactions); + } catch (BlockStoreException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + /** Emulates receiving a valid block that builds on top of the chain. */ public static BlockPair createFakeBlock(BlockStore blockStore, int height, Transaction... transactions) { diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index 75c79948..cf6116b1 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -6766,6 +6766,14 @@ public final class Protos { * */ DEAD(4, 4), + /** + * IN_CONFLICT = 5; + * + *
+       * There is another transaction spending one of this transaction inputs.
+       * 
+ */ + IN_CONFLICT(5, 5), ; /** @@ -6815,6 +6823,7 @@ public final class Protos { case 2: return PENDING; case 3: return NOT_IN_BEST_CHAIN; case 4: return DEAD; + case 5: return IN_CONFLICT; default: return null; } } diff --git a/core/src/test/java/org/bitcoinj/core/WalletTest.java b/core/src/test/java/org/bitcoinj/core/WalletTest.java index 4464c7d1..fcb52f79 100644 --- a/core/src/test/java/org/bitcoinj/core/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/core/WalletTest.java @@ -19,6 +19,7 @@ package org.bitcoinj.core; import org.bitcoinj.core.listeners.AbstractWalletEventListener; import org.bitcoinj.core.listeners.WalletCoinEventListener; +import org.bitcoinj.core.TransactionConfidence.ConfidenceType; import org.bitcoinj.core.Wallet.SendRequest; import org.bitcoinj.crypto.*; import org.bitcoinj.script.Script; @@ -747,7 +748,7 @@ public class WalletTest extends TestWithWallet { send2 = params.getDefaultSerializer().makeTransaction(send2.bitcoinSerialize()); // Broadcast send1, it's now pending. wallet.commitTx(send1); - assertEquals(ZERO, wallet.getBalance()); + assertEquals(ZERO, wallet.getBalance()); // change of 10 cents is not yet mined so not included in the balance. // Receive a block that overrides the send1 using send2. sendMoneyToWallet(send2, AbstractBlockChain.NewBlockType.BEST_CHAIN); // send1 got rolled back and replaced with a smaller send that only used one of our received coins, thus ... @@ -863,6 +864,409 @@ public class WalletTest extends TestWithWallet { assertEquals(5, eventWalletChanged[0]); } + @Test + public void doubleSpendWeCreate() throws Exception { + // Test we keep pending double spends in IN_CONFLICT until one of them is included in a block + // and we handle reorgs and dependency chains properly. + // The following graph shows the txns we use in this test and how they are related + // (Eg txA1 spends txARoot outputs, txC1 spends txA1 and txB1 outputs, etc). + // txARoot (10) -> txA1 (1) -+ + // |--> txC1 (0.10) -> txD1 (0.01) + // txBRoot (100) -> txB1 (11) -+ + // + // txARoot (10) -> txA2 (2) -+ + // |--> txC2 (0.20) -> txD2 (0.02) + // txBRoot (100) -> txB2 (22) -+ + // + // txARoot (10) -> txA3 (3) + // + // txA1 is in conflict with txA2 and txA3. txB1 is in conflict with txB2. + + CoinSelector originalCoinSelector = wallet.getCoinSelector(); + try { + wallet.allowSpendingUnconfirmedTransactions(); + final Address address = new ECKey().toAddress(params); + + Transaction txARoot = sendMoneyToWallet(valueOf(10, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + SendRequest a1Req = SendRequest.to(address, valueOf(1, 0)); + a1Req.tx.addInput(txARoot.getOutput(0)); + a1Req.shuffleOutputs = false; + wallet.completeTx(a1Req); + Transaction txA1 = a1Req.tx; + SendRequest a2Req = SendRequest.to(address, valueOf(2, 0)); + a2Req.tx.addInput(txARoot.getOutput(0)); + a2Req.shuffleOutputs = false; + wallet.completeTx(a2Req); + Transaction txA2 = a2Req.tx; + SendRequest a3Req = SendRequest.to(address, valueOf(3, 0)); + a3Req.tx.addInput(txARoot.getOutput(0)); + a3Req.shuffleOutputs = false; + wallet.completeTx(a3Req); + Transaction txA3 = a3Req.tx; + wallet.commitTx(txA1); + wallet.commitTx(txA2); + wallet.commitTx(txA3); + + Transaction txBRoot = sendMoneyToWallet(valueOf(100, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + SendRequest b1Req = SendRequest.to(address, valueOf(11, 0)); + b1Req.tx.addInput(txBRoot.getOutput(0)); + b1Req.shuffleOutputs = false; + wallet.completeTx(b1Req); + Transaction txB1 = b1Req.tx; + SendRequest b2Req = SendRequest.to(address, valueOf(22, 0)); + b2Req.tx.addInput(txBRoot.getOutput(0)); + b2Req.shuffleOutputs = false; + wallet.completeTx(b2Req); + Transaction txB2 = b2Req.tx; + wallet.commitTx(txB1); + wallet.commitTx(txB2); + + SendRequest c1Req = SendRequest.to(address, valueOf(0, 10)); + c1Req.tx.addInput(txA1.getOutput(1)); + c1Req.tx.addInput(txB1.getOutput(1)); + c1Req.shuffleOutputs = false; + wallet.completeTx(c1Req); + Transaction txC1 = c1Req.tx; + SendRequest c2Req = SendRequest.to(address, valueOf(0, 20)); + c2Req.tx.addInput(txA2.getOutput(1)); + c2Req.tx.addInput(txB2.getOutput(1)); + c2Req.shuffleOutputs = false; + wallet.completeTx(c2Req); + Transaction txC2 = c2Req.tx; + wallet.commitTx(txC1); + wallet.commitTx(txC2); + + SendRequest d1Req = SendRequest.to(address, valueOf(0, 1)); + d1Req.tx.addInput(txC1.getOutput(1)); + d1Req.shuffleOutputs = false; + wallet.completeTx(d1Req); + Transaction txD1 = d1Req.tx; + SendRequest d2Req = SendRequest.to(address, valueOf(0, 2)); + d2Req.tx.addInput(txC2.getOutput(1)); + d2Req.shuffleOutputs = false; + wallet.completeTx(d2Req); + Transaction txD2 = d2Req.tx; + wallet.commitTx(txD1); + wallet.commitTx(txD2); + + assertInConflict(txA1); + assertInConflict(txA2); + assertInConflict(txA3); + assertInConflict(txB1); + assertInConflict(txB2); + assertInConflict(txC1); + assertInConflict(txC2); + assertInConflict(txD1); + assertInConflict(txD2); + + // Add a block to the block store. The rest of the blocks in this test will be on top of this one. + FakeTxBuilder.BlockPair blockPair0 = createFakeBlock(blockStore, 1); + + // A block was mined including txA1 + FakeTxBuilder.BlockPair blockPair1 = createFakeBlock(blockStore, 2, txA1); + wallet.receiveFromBlock(txA1, blockPair1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0); + wallet.notifyNewBestBlock(blockPair1.storedBlock); + assertSpent(txA1); + assertDead(txA2); + assertDead(txA3); + assertInConflict(txB1); + assertInConflict(txB2); + assertInConflict(txC1); + assertDead(txC2); + assertInConflict(txD1); + assertDead(txD2); + + // A reorg: previous block "replaced" by new block containing txA1 and txB1 + FakeTxBuilder.BlockPair blockPair2 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA1, txB1); + wallet.receiveFromBlock(txA1, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0); + wallet.receiveFromBlock(txB1, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 1); + wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair1.storedBlock), + Lists.newArrayList(blockPair2.storedBlock)); + assertSpent(txA1); + assertDead(txA2); + assertDead(txA3); + assertSpent(txB1); + assertDead(txB2); + assertPending(txC1); + assertDead(txC2); + assertPending(txD1); + assertDead(txD2); + + // A reorg: previous block "replaced" by new block containing txA1, txB1 and txC1 + FakeTxBuilder.BlockPair blockPair3 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA1, txB1, + txC1); + wallet.receiveFromBlock(txA1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0); + wallet.receiveFromBlock(txB1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 1); + wallet.receiveFromBlock(txC1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 2); + wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair2.storedBlock), + Lists.newArrayList(blockPair3.storedBlock)); + assertSpent(txA1); + assertDead(txA2); + assertDead(txA3); + assertSpent(txB1); + assertDead(txB2); + assertSpent(txC1); + assertDead(txC2); + assertPending(txD1); + assertDead(txD2); + + // A reorg: previous block "replaced" by new block containing txB1 + FakeTxBuilder.BlockPair blockPair4 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txB1); + wallet.receiveFromBlock(txB1, blockPair4.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0); + wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair3.storedBlock), + Lists.newArrayList(blockPair4.storedBlock)); + assertPending(txA1); + assertDead(txA2); + assertDead(txA3); + assertSpent(txB1); + assertDead(txB2); + assertPending(txC1); + assertDead(txC2); + assertPending(txD1); + assertDead(txD2); + + // A reorg: previous block "replaced" by new block containing txA2 + FakeTxBuilder.BlockPair blockPair5 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA2); + wallet.receiveFromBlock(txA2, blockPair5.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0); + wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair4.storedBlock), + Lists.newArrayList(blockPair5.storedBlock)); + assertDead(txA1); + assertUnspent(txA2); + assertDead(txA3); + assertPending(txB1); + assertDead(txB2); + assertDead(txC1); + assertDead(txC2); + assertDead(txD1); + assertDead(txD2); + + // A reorg: previous block "replaced" by new empty block + FakeTxBuilder.BlockPair blockPair6 = createFakeBlock(blockStore, blockPair0.storedBlock, 2); + wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair5.storedBlock), + Lists.newArrayList(blockPair6.storedBlock)); + assertDead(txA1); + assertPending(txA2); + assertDead(txA3); + assertPending(txB1); + assertDead(txB2); + assertDead(txC1); + assertDead(txC2); + assertDead(txD1); + assertDead(txD2); + } finally { + wallet.setCoinSelector(originalCoinSelector); + } + + } + + @Test + public void doubleSpendWeReceive() throws Exception { + FakeTxBuilder.DoubleSpends doubleSpends = FakeTxBuilder.createFakeDoubleSpendTxns(params, myAddress); + // doubleSpends.t1 spends to our wallet. doubleSpends.t2 double spends somewhere else. + + final Address notMyaddress = new ECKey().toAddress(params); + + Transaction t1b = new Transaction(params); + TransactionOutput t1bo = new TransactionOutput(params, t1b, valueOf(0, 50), notMyaddress); + t1b.addOutput(t1bo); + t1b.addInput(doubleSpends.t1.getOutput(0)); + + wallet.receivePending(doubleSpends.t1, null); + wallet.receivePending(doubleSpends.t2, null); + wallet.receivePending(t1b, null); + assertInConflict(doubleSpends.t1); + assertInConflict(doubleSpends.t1); + assertInConflict(t1b); + + // Add a block to the block store. The rest of the blocks in this test will be on top of this one. + FakeTxBuilder.BlockPair blockPair0 = createFakeBlock(blockStore, 1); + + // A block was mined including doubleSpends.t1 + FakeTxBuilder.BlockPair blockPair1 = createFakeBlock(blockStore, 2, doubleSpends.t1); + wallet.receiveFromBlock(doubleSpends.t1, blockPair1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0); + wallet.notifyNewBestBlock(blockPair1.storedBlock); + assertSpent(doubleSpends.t1); + assertDead(doubleSpends.t2); + assertPending(t1b); + + // A reorg: previous block "replaced" by new block containing doubleSpends.t2 + FakeTxBuilder.BlockPair blockPair2 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, doubleSpends.t2); + wallet.receiveFromBlock(doubleSpends.t2, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0); + wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair1.storedBlock), + Lists.newArrayList(blockPair2.storedBlock)); + assertDead(doubleSpends.t1); + assertSpent(doubleSpends.t2); + assertDead(t1b); + } + + @Test + public void doubleSpendForBuildingTx() throws Exception { + CoinSelector originalCoinSelector = wallet.getCoinSelector(); + try { + wallet.allowSpendingUnconfirmedTransactions(); + + sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + final Address address = new ECKey().toAddress(params); + Transaction send1 = checkNotNull(wallet.createSend(address, valueOf(1, 0))); + Transaction send2 = checkNotNull(wallet.createSend(address, valueOf(1, 20))); + + FakeTxBuilder.BlockPair bp1 = createFakeBlock(blockStore, 1, send1); + wallet.receiveFromBlock(send1, bp1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0); + wallet.notifyNewBestBlock(bp1.storedBlock); + assertUnspent(send1); + + wallet.receivePending(send2, null); + assertUnspent(send1); + assertDead(send2); + + } finally { + wallet.setCoinSelector(originalCoinSelector); + } + } + + @Test + public void txSpendingDeadTx() throws Exception { + CoinSelector originalCoinSelector = wallet.getCoinSelector(); + try { + wallet.allowSpendingUnconfirmedTransactions(); + + sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + final Address address = new ECKey().toAddress(params); + Transaction send1 = checkNotNull(wallet.createSend(address, valueOf(1, 0))); + Transaction send2 = checkNotNull(wallet.createSend(address, valueOf(1, 20))); + wallet.commitTx(send1); + assertPending(send1); + Transaction send1b = checkNotNull(wallet.createSend(address, valueOf(0, 50))); + + FakeTxBuilder.BlockPair bp1 = createFakeBlock(blockStore, 1, send2); + wallet.receiveFromBlock(send2, bp1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0); + wallet.notifyNewBestBlock(bp1.storedBlock); + assertDead(send1); + assertUnspent(send2); + + wallet.receivePending(send1b, null); + assertDead(send1); + assertUnspent(send2); + assertDead(send1b); + + } finally { + wallet.setCoinSelector(originalCoinSelector); + } + } + + private void assertInConflict(Transaction tx) { + assertEquals(ConfidenceType.IN_CONFLICT, tx.getConfidence().getConfidenceType()); + assertTrue(wallet.pending.containsKey(tx.getHash())); + } + + private void assertPending(Transaction tx) { + assertEquals(ConfidenceType.PENDING, tx.getConfidence().getConfidenceType()); + assertTrue(wallet.pending.containsKey(tx.getHash())); + } + + private void assertSpent(Transaction tx) { + assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType()); + assertTrue(wallet.spent.containsKey(tx.getHash())); + } + + private void assertUnspent(Transaction tx) { + assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType()); + assertTrue(wallet.unspent.containsKey(tx.getHash())); + } + + private void assertDead(Transaction tx) { + assertEquals(ConfidenceType.DEAD, tx.getConfidence().getConfidenceType()); + assertTrue(wallet.dead.containsKey(tx.getHash())); + } + + @Test + public void testAddTransactionsDependingOn() throws Exception { + CoinSelector originalCoinSelector = wallet.getCoinSelector(); + try { + wallet.allowSpendingUnconfirmedTransactions(); + sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + final Address address = new ECKey().toAddress(params); + Transaction send1 = checkNotNull(wallet.createSend(address, valueOf(1, 0))); + Transaction send2 = checkNotNull(wallet.createSend(address, valueOf(1, 20))); + wallet.commitTx(send1); + Transaction send1b = checkNotNull(wallet.createSend(address, valueOf(0, 50))); + wallet.commitTx(send1b); + Transaction send1c = checkNotNull(wallet.createSend(address, valueOf(0, 25))); + wallet.commitTx(send1c); + wallet.commitTx(send2); + Set txns = new HashSet(); + txns.add(send1); + wallet.addTransactionsDependingOn(txns, wallet.getTransactions(true)); + assertEquals(3, txns.size()); + assertTrue(txns.contains(send1)); + assertTrue(txns.contains(send1b)); + assertTrue(txns.contains(send1c)); + } finally { + wallet.setCoinSelector(originalCoinSelector); + } + } + + @Test + public void sortTxnsByDependency() throws Exception { + CoinSelector originalCoinSelector = wallet.getCoinSelector(); + try { + wallet.allowSpendingUnconfirmedTransactions(); + Transaction send1 = sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + final Address address = new ECKey().toAddress(params); + Transaction send1a = checkNotNull(wallet.createSend(address, valueOf(1, 0))); + wallet.commitTx(send1a); + Transaction send1b = checkNotNull(wallet.createSend(address, valueOf(0, 50))); + wallet.commitTx(send1b); + Transaction send1c = checkNotNull(wallet.createSend(address, valueOf(0, 25))); + wallet.commitTx(send1c); + Transaction send1d = checkNotNull(wallet.createSend(address, valueOf(0, 12))); + wallet.commitTx(send1d); + Transaction send1e = checkNotNull(wallet.createSend(address, valueOf(0, 06))); + wallet.commitTx(send1e); + + Transaction send2 = sendMoneyToWallet(valueOf(200, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN); + + SendRequest req2a = SendRequest.to(address, valueOf(100, 0)); + req2a.tx.addInput(send2.getOutput(0)); + req2a.shuffleOutputs = false; + wallet.completeTx(req2a); + Transaction send2a = req2a.tx; + + SendRequest req2b = SendRequest.to(address, valueOf(50, 0)); + req2b.tx.addInput(send2a.getOutput(1)); + req2b.shuffleOutputs = false; + wallet.completeTx(req2b); + Transaction send2b = req2b.tx; + + SendRequest req2c = SendRequest.to(address, valueOf(25, 0)); + req2c.tx.addInput(send2b.getOutput(1)); + req2c.shuffleOutputs = false; + wallet.completeTx(req2c); + Transaction send2c = req2c.tx; + + Set unsortedTxns = new HashSet(); + unsortedTxns.add(send1a); + unsortedTxns.add(send1b); + unsortedTxns.add(send1c); + unsortedTxns.add(send1d); + unsortedTxns.add(send1e); + unsortedTxns.add(send2a); + unsortedTxns.add(send2b); + unsortedTxns.add(send2c); + List sortedTxns = wallet.sortTxnsByDependency(unsortedTxns); + + assertEquals(8, sortedTxns.size()); + assertTrue(sortedTxns.indexOf(send1a) < sortedTxns.indexOf(send1b)); + assertTrue(sortedTxns.indexOf(send1b) < sortedTxns.indexOf(send1c)); + assertTrue(sortedTxns.indexOf(send1c) < sortedTxns.indexOf(send1d)); + assertTrue(sortedTxns.indexOf(send1d) < sortedTxns.indexOf(send1e)); + assertTrue(sortedTxns.indexOf(send2a) < sortedTxns.indexOf(send2b)); + assertTrue(sortedTxns.indexOf(send2b) < sortedTxns.indexOf(send2c)); + } finally { + wallet.setCoinSelector(originalCoinSelector); + } + } + @Test public void pending1() throws Exception { // Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate.