diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index 08d2355a..4f635666 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -338,10 +338,13 @@ public class Wallet implements Serializable { // A -> spent by B [pending] // \-> spent by C [chain] Transaction doubleSpent = input.outpoint.fromTx; // == A + assert doubleSpent != null; int index = (int) input.outpoint.index; TransactionOutput output = doubleSpent.outputs.get(index); TransactionInput spentBy = output.getSpentBy(); + assert spentBy != null; Transaction connected = spentBy.parentTransaction; + assert connected != null; if (pending.containsKey(connected.getHash())) { log.info("Saw double spend from chain override pending tx {}", connected.getHashAsString()); log.info(" <-pending ->dead"); @@ -404,15 +407,7 @@ public class Wallet implements Serializable { TransactionOutput connectedOutput = input.outpoint.getConnectedOutput(); connectedOutput.markAsSpent(input); } - // Some of the outputs probably send coins back to us, eg for change or because this transaction is just - // consolidating the wallet. Mark any output that is NOT back to us as spent. Then add this TX to the - // pending pool. - for (TransactionOutput output : tx.outputs) { - if (!output.isMine(this)) { - // This output didn't go to us, so by definition it is now spent. - output.markAsSpent(null); - } - } + // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain. pending.put(tx.getHash(), tx); } diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 761a7435..0f5e69fe 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -184,6 +184,9 @@ public class WalletTest { Address someOtherGuy = new ECKey().toAddress(params); TransactionOutput output = new TransactionOutput(params, tx, Utils.toNanoCoins(0, 5), someOtherGuy); tx.addOutput(output); + // Note that tx is no longer valid: it spends more than it imports. However checking transactions balance + // correctly isn't possible in SPV mode because value is a property of outputs not inputs. Without all + // transactions you can't check they add up. wallet.receive(tx, null, BlockChain.NewBlockType.BEST_CHAIN); // Now the other guy creates a transaction which spends that change. Transaction tx2 = new Transaction(params); @@ -193,6 +196,29 @@ public class WalletTest { assertEquals(Utils.toNanoCoins(0, 0), tx2.getValueSentFromMe(wallet)); } + @Test + public void bounce() throws Exception { + // This test covers bug 64 (False double spends). Check that if we create a spend and it's immediately sent + // back to us, this isn't considered as a double spend. + BigInteger coin1 = Utils.toNanoCoins(1, 0); + BigInteger coinHalf = Utils.toNanoCoins(0, 50); + // Start by giving us 1 coin. + Transaction inbound1 = createFakeTx(params, coin1, myAddress); + wallet.receive(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN); + // Send half to some other guy. Sending only half then waiting for a confirm is important to ensure the tx is + // in the unspent pool, not pending or spent. + Address someOtherGuy = new ECKey().toAddress(params); + Transaction outbound1 = wallet.createSend(someOtherGuy, coinHalf); + wallet.confirmSend(outbound1); + wallet.receive(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN); + // That other guy gives us the coins right back. + Transaction inbound2 = new Transaction(params); + inbound2.addOutput(new TransactionOutput(params, inbound2, coinHalf, myAddress)); + inbound2.addInput(outbound1.outputs.get(0)); + wallet.receive(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(coin1, wallet.getBalance()); + } + @Test public void finneyAttack() throws Exception { // A Finney attack is where a miner includes a transaction spending coins to themselves but does not