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 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.

View file

@ -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.<p>

View file

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