When confirming a transaction as sent, move connected newly spent transactions from unspent->spent. Introduce a method to do this, so as to avoid duplication with updateForSpends(). Add a getPoolSize() method and use it in unit tests to verify the pools at various points. Resolves issue 72.

This commit is contained in:
Mike Hearn
2011-09-05 13:06:33 +00:00
parent 91fe7cdefb
commit 9d5af32a9c
3 changed files with 74 additions and 10 deletions

View File

@@ -204,6 +204,17 @@ public class Transaction extends Message implements Serializable {
return null; 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 * 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. * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.

View File

@@ -330,12 +330,19 @@ public class Wallet implements Serializable {
* there's no need to go through and do it again. * there's no need to go through and do it again.
*/ */
private void updateForSpends(Transaction tx) throws VerificationException { private void updateForSpends(Transaction tx) throws VerificationException {
// tx is on the best chain by this point.
for (TransactionInput input : tx.inputs) { for (TransactionInput input : tx.inputs) {
TransactionInput.ConnectionResult result = input.connect(unspent, false); TransactionInput.ConnectionResult result = input.connect(unspent, false);
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
// Doesn't spend any of our outputs or is coinbase. // Not found in the unspent map. Try again with the spent map.
continue; result = input.connect(spent, false);
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { 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 // 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). // 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 // 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. // us to use. Move if not.
Transaction connected = input.outpoint.fromTx; Transaction connected = input.outpoint.fromTx;
if (connected.getValueSentToMe(this, false).equals(BigInteger.ZERO)) { maybeMoveTxToSpent(connected, "prevtx");
// 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); /** 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. // Mark the outputs of the used transcations as spent, so we don't try and spend it again.
for (TransactionInput input : tx.inputs) { for (TransactionInput input : tx.inputs) {
TransactionOutput connectedOutput = input.outpoint.getConnectedOutput(); TransactionOutput connectedOutput = input.outpoint.getConnectedOutput();
Transaction connectedTx = connectedOutput.parentTransaction;
connectedOutput.markAsSpent(input); 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. // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain.
pending.put(tx.getHash(), tx); 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 * 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.<p> * the first address in the wallet, so you must have added at least one key.<p>

View File

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