diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 542eb564..cf723658 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -204,6 +204,17 @@ public class Transaction extends Message implements Serializable { return null; } + /** + * @return true if every output is marked as spent. + */ + public boolean isEveryOutputSpent() { + for (TransactionOutput output : outputs) { + if (output.isAvailableForSpending()) + return false; + } + return true; + } + /** * These constants are a part of a scriptSig signature on the inputs. They define the details of how a * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated. diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index 4b6f1518..ecc505b0 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -330,12 +330,19 @@ public class Wallet implements Serializable { * there's no need to go through and do it again. */ private void updateForSpends(Transaction tx) throws VerificationException { + // tx is on the best chain by this point. for (TransactionInput input : tx.inputs) { TransactionInput.ConnectionResult result = input.connect(unspent, false); if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { - // Doesn't spend any of our outputs or is coinbase. - continue; - } else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { + // Not found in the unspent map. Try again with the spent map. + result = input.connect(spent, false); + if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { + // Doesn't spend any of our outputs or is coinbase. + continue; + } + } + + if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { // Double spend! This must have overridden a pending tx, or the block is bad (contains transactions // that illegally double spend: should never occur if we are connected to an honest node). // @@ -370,14 +377,21 @@ public class Wallet implements Serializable { // The outputs are already marked as spent by the connect call above, so check if there are any more for // us to use. Move if not. Transaction connected = input.outpoint.fromTx; - if (connected.getValueSentToMe(this, false).equals(BigInteger.ZERO)) { - // There's nothing left I can spend in this transaction. - if (unspent.remove(connected.getHash()) != null) { - log.info(" prevtx <-unspent"); - log.info(" prevtx ->spent"); - spent.put(connected.getHash(), connected); - } + maybeMoveTxToSpent(connected, "prevtx"); + } + } + } + + /** If the transactions outputs are all marked as spent, and it's in the unspent map, move it. */ + private void maybeMoveTxToSpent(Transaction tx, String context) { + if (tx.isEveryOutputSpent()) { + // There's nothing left I can spend in this transaction. + if (unspent.remove(tx.getHash()) != null) { + if (log.isInfoEnabled()) { + log.info(" " + context + " <-unspent"); + log.info(" " + context + " ->spent"); } + spent.put(tx.getHash(), tx); } } } @@ -411,12 +425,36 @@ public class Wallet implements Serializable { // Mark the outputs of the used transcations as spent, so we don't try and spend it again. for (TransactionInput input : tx.inputs) { TransactionOutput connectedOutput = input.outpoint.getConnectedOutput(); + Transaction connectedTx = connectedOutput.parentTransaction; connectedOutput.markAsSpent(input); + maybeMoveTxToSpent(connectedTx, "spent tx"); } // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain. pending.put(tx.getHash(), tx); } + // This is used only for unit testing, it's an internal API. + enum Pool { + UNSPENT, + SPENT, + PENDING, + INACTIVE, + DEAD, + ALL, + } + + int getPoolSize(Pool pool) { + switch (pool) { + case UNSPENT: return unspent.size(); + case SPENT: return spent.size(); + case PENDING: return pending.size(); + case INACTIVE: return inactive.size(); + case DEAD: return dead.size(); + case ALL: return unspent.size() + spent.size() + pending.size() + inactive.size() + dead.size(); + } + throw new RuntimeException("Unreachable"); + } + /** * Statelessly creates a transaction that sends the given number of nanocoins to address. The change is sent to * the first address in the wallet, so you must have added at least one key.

diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 0f5e69fe..4101f595 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -56,16 +56,25 @@ public class WalletTest { wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); ECKey k2 = new ECKey(); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = wallet.createSend(k2.toAddress(params), v2); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); // Do some basic sanity checks. assertEquals(1, t2.inputs.size()); assertEquals(myAddress, t2.inputs.get(0).getScriptSig().getFromAddress()); // We have NOT proven that the signature is correct! + + wallet.confirmSend(t2); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING)); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT)); + assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL)); } @Test @@ -76,10 +85,14 @@ public class WalletTest { wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = createFakeTx(params, v2, myAddress); wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.INACTIVE)); + assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL)); assertEquals(v1, wallet.getBalance()); } @@ -207,6 +220,8 @@ public class WalletTest { 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. + assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); Address someOtherGuy = new ECKey().toAddress(params); Transaction outbound1 = wallet.createSend(someOtherGuy, coinHalf); wallet.confirmSend(outbound1);