3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-30 23:02:15 +00:00

Re-organize how transaction confidence listeners end up being called. Ensure WalletEventListener.onTransactionConfidenceChanged is always called for every building transaction after every block. Resolves issue 251.

This commit is contained in:
Mike Hearn 2012-09-27 18:43:31 +02:00
parent a9cdf99135
commit 78dedcc9ba
4 changed files with 81 additions and 37 deletions

View File

@ -242,14 +242,14 @@ public class Transaction extends ChildMessage implements Serializable {
}
/**
* Puts the given block in the internal serializable set of blocks in which this transaction appears. This is
* <p>Puts the given block in the internal serializable set of blocks in which this transaction appears. This is
* used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
* block stores do not save the transaction data at all.<p>
* block stores do not save the transaction data at all.</p>
*
* <p>If there is a re-org this will be called once for each block that was previously seen, to update which block
* is the best chain. The best chain block is guaranteed to be called last. So this must be idempotent.
* is the best chain. The best chain block is guaranteed to be called last. So this must be idempotent.</p>
*
* <p>Sets updatedAt to be the earliest valid block time where this tx was seen
* <p>Sets updatedAt to be the earliest valid block time where this tx was seen.</p>
*
* @param block The {@link StoredBlock} in which the transaction has appeared.
* @param bestChain whether to set the updatedAt timestamp from the block header (only if not already set)
@ -269,10 +269,14 @@ public class Transaction extends ChildMessage implements Serializable {
transactionConfidence.setAppearedAtChainHeight(block.getHeight());
// Reset the confidence block depth.
transactionConfidence.setDepthInBlocks(0);
transactionConfidence.setDepthInBlocks(1);
// Reset the work done.
transactionConfidence.setWorkDone(BigInteger.ZERO);
try {
transactionConfidence.setWorkDone(block.getHeader().getWork());
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
}
// The transaction is now on the best chain.
transactionConfidence.setConfidenceType(ConfidenceType.BUILDING);

View File

@ -49,7 +49,7 @@ import java.util.Set;
* <p>Alternatively, you may know that the transaction is "dead", that is, one or more of its inputs have
* been double spent and will never confirm unless there is another re-org.</p>
*
* <p>TransactionConfidence is updated via the {@link com.google.bitcoin.core.TransactionConfidence#notifyWorkDone()}
* <p>TransactionConfidence is updated via the {@link com.google.bitcoin.core.TransactionConfidence#notifyWorkDone(Block)}
* method to ensure the block depth and work done are up to date.</p>
* To make a copy that won't be changed, use {@link com.google.bitcoin.core.TransactionConfidence#duplicate()}.
*/
@ -93,8 +93,10 @@ public class TransactionConfidence implements Serializable {
public synchronized void addEventListener(Listener listener) {
Preconditions.checkNotNull(listener);
if (listeners == null)
listeners = new ArrayList<Listener>(1);
listeners.add(listener);
listeners = new ArrayList<Listener>(2);
// Dedupe registrations. This makes the wallet code simpler.
if (!listeners.contains(listener))
listeners.add(listener);
}
public synchronized void removeEventListener(Listener listener) {
@ -298,6 +300,7 @@ public class TransactionConfidence implements Serializable {
if (getConfidenceType() == ConfidenceType.BUILDING) {
this.depth++;
this.workDone = this.workDone.add(block.getWork());
runListeners();
}
}

View File

@ -176,6 +176,10 @@ public class Wallet implements Serializable {
// A listener that relays confidence changes from the transaction confidence object to the wallet event listener,
// as a convenience to API users so they don't have to register on every transaction themselves.
private transient TransactionConfidence.Listener txConfidenceListener;
// If a TX hash appears in this set then notifyNewBestBlock will ignore it, as its confidence was already set up
// in receive() via Transaction.setBlockAppearance(). As the BlockChain always calls notifyNewBestBlock even if
// it sent transactions to the wallet, without this we'd double count.
private transient HashSet<Sha256Hash> ignoreNextNewBlock;
/**
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
@ -194,6 +198,7 @@ public class Wallet implements Serializable {
private void createTransientState() {
eventListeners = new ArrayList<WalletEventListener>();
ignoreNextNewBlock = new HashSet<Sha256Hash>();
txConfidenceListener = new TransactionConfidence.Listener() {
public void onConfidenceChanged(Transaction tx) {
invokeOnTransactionConfidenceChanged(tx);
@ -723,14 +728,12 @@ public class Wallet implements Serializable {
if (valueSentToMe.equals(BigInteger.ZERO)) {
// There were no change transactions so this tx is fully spent.
log.info(" ->spent");
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
checkState(!alreadyPresent, "TX in both pending and spent pools");
addWalletTransaction(Pool.SPENT, tx);
} else {
// There was change back to us, or this tx was purely a spend back to ourselves (perhaps for
// anonymization purposes).
log.info(" ->unspent");
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
checkState(!alreadyPresent, "TX in both pending and unspent pools");
addWalletTransaction(Pool.UNSPENT, tx);
}
} else if (sideChain) {
// The transaction was accepted on an inactive side chain, but not yet by the best chain.
@ -756,9 +759,12 @@ public class Wallet implements Serializable {
// we don't need to consider this transaction inactive, we can just ignore it.
} else {
log.info(" ->inactive");
inactive.put(tx.getHash(), tx);
addWalletTransaction(Pool.INACTIVE, tx);
}
} else if (bestChain) {
// Saw a non-pending transaction appear on the best chain, ie, we are replaying the chain or a spend
// that we never saw broadcast (and did not originate) got included.
//
// This can trigger tx confidence listeners to be run in the case of double spends. We may need to
// delay the execution of the listeners until the bottom to avoid the wallet mutating during updates.
processTxFromBestChain(tx);
@ -771,9 +777,16 @@ public class Wallet implements Serializable {
// in turn allowed to re-enter the Wallet. This means we cannot assume anything about the state of the wallet
// from now on. The balance just received may already be spent.
// Mark the tx as appearing in this block so we can find it later after a re-org.
if (block != null) {
// Mark the tx as appearing in this block so we can find it later after a re-org. This also tells the tx
// confidence object about the block and sets its work done/depth appropriately.
tx.setBlockAppearance(block, bestChain);
if (bestChain) {
// Don't notify this tx of work done in notifyNewBestBlock which will be called immediately after
// this method has been called by BlockChain for all relevant transactions. Otherwise we'd double
// count.
ignoreNextNewBlock.add(txHash);
}
}
// Inform anyone interested that we have received or sent coins but only if:
@ -823,10 +836,16 @@ public class Wallet implements Serializable {
// This is so that they can update their work done and depth.
Set<Transaction> transactions = getTransactions(true, false);
for (Transaction tx : transactions) {
tx.getConfidence().notifyWorkDone(block);
if (ignoreNextNewBlock.contains(tx.getHash())) {
// tx was already processed in receive() due to it appearing in this block, so we don't want to
// notify the tx confidence of work done twice, it'd result in miscounting.
ignoreNextNewBlock.remove(tx.getHash());
} else {
tx.getConfidence().notifyWorkDone(block);
}
}
queueAutoSave();
}
queueAutoSave();
}
/**
@ -859,13 +878,11 @@ public class Wallet implements Serializable {
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
// It's sending us coins.
log.info(" new tx {} ->unspent", tx.getHashAsString());
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
checkState(!alreadyPresent, "TX was received twice");
addWalletTransaction(Pool.UNSPENT, tx);
} else if (!tx.getValueSentFromMe(this).equals(BigInteger.ZERO)) {
// It spent some of our coins and did not send us any.
log.info(" new tx {} ->spent", tx.getHashAsString());
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
checkState(!alreadyPresent, "TX was received twice");
addWalletTransaction(Pool.SPENT, tx);
} else {
// It didn't send us coins nor spend any of our coins. If we're processing it, that must be because it
// spends outpoints that are also spent by some pending transactions - maybe a double spend of somebody
@ -880,7 +897,7 @@ public class Wallet implements Serializable {
log.warn("Saw double spend from chain override pending tx {}", doubleSpend.getHashAsString());
log.warn(" <-pending ->dead");
pending.remove(doubleSpend.getHash());
dead.put(doubleSpend.getHash(), doubleSpend);
addWalletTransaction(Pool.DEAD, doubleSpend);
// Inform the event listeners of the newly dead tx.
doubleSpend.getConfidence().setOverridingTransaction(tx);
}
@ -1017,6 +1034,7 @@ public class Wallet implements Serializable {
// 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());
addWalletTransaction(Pool.PENDING, tx);
@ -1097,33 +1115,34 @@ public class Wallet implements Serializable {
}
/**
* Adds the given transaction to the given pools and registers a confidence change listener on it. Not to be used
* when moving txns between pools.
* Adds the given transaction to the given pools and registers a confidence change listener on it.
*/
private synchronized void addWalletTransaction(Pool pool, Transaction tx) {
switch (pool) {
case UNSPENT:
unspent.put(tx.getHash(), tx);
Preconditions.checkState(unspent.put(tx.getHash(), tx) == null);
break;
case SPENT:
spent.put(tx.getHash(), tx);
Preconditions.checkState(spent.put(tx.getHash(), tx) == null);
break;
case PENDING:
pending.put(tx.getHash(), tx);
Preconditions.checkState(pending.put(tx.getHash(), tx) == null);
break;
case DEAD:
dead.put(tx.getHash(), tx);
Preconditions.checkState(dead.put(tx.getHash(), tx) == null);
break;
case INACTIVE:
inactive.put(tx.getHash(), tx);
Preconditions.checkState(inactive.put(tx.getHash(), tx) == null);
break;
case PENDING_INACTIVE:
pending.put(tx.getHash(), tx);
inactive.put(tx.getHash(), tx);
Preconditions.checkState(pending.put(tx.getHash(), tx) == null);
Preconditions.checkState(inactive.put(tx.getHash(), tx) == null);
break;
default:
throw new RuntimeException("Unknown wallet transaction type " + pool);
}
// This is safe even if the listener has been added before, as TransactionConfidence ignores duplicate
// registration requests. That makes the code in the wallet simpler.
tx.getConfidence().addEventListener(txConfidenceListener);
}

View File

@ -27,6 +27,7 @@ import org.junit.Test;
import java.io.File;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -211,6 +212,7 @@ public class WalletTest {
// Test that we correctly process transactions arriving from the chain, with callbacks for inbound and outbound.
final BigInteger bigints[] = new BigInteger[4];
final Transaction txn[] = new Transaction[2];
final LinkedList<Transaction> confTxns = new LinkedList<Transaction>();
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
@ -227,14 +229,22 @@ public class WalletTest {
bigints[3] = newBalance;
txn[1] = tx;
}
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
super.onTransactionConfidenceChanged(wallet, tx);
confTxns.add(tx);
}
});
// Receive some money.
BigInteger oneCoin = Utils.toNanoCoins(1, 0);
Transaction tx1 = createFakeTx(params, oneCoin, myAddress);
StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock;
wallet.receiveFromBlock(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
BlockPair b1 = createFakeBlock(params, blockStore, tx1);
wallet.receiveFromBlock(tx1, b1.storedBlock, BlockChain.NewBlockType.BEST_CHAIN);
wallet.notifyNewBestBlock(b1.block);
assertEquals(null, txn[1]); // onCoinsSent not called.
assertEquals(tx1, confTxns.getFirst()); // onTransactionConfidenceChanged called
assertEquals(txn[0].getHash(), tx1.getHash());
assertEquals(BigInteger.ZERO, bigints[0]);
assertEquals(oneCoin, bigints[1]);
@ -246,10 +256,13 @@ public class WalletTest {
// want to get back to our previous state. We can do this by just not confirming the transaction as
// createSend is stateless.
txn[0] = txn[1] = null;
StoredBlock b2 = createFakeBlock(params, blockStore, send1).storedBlock;
wallet.receiveFromBlock(send1, b2, BlockChain.NewBlockType.BEST_CHAIN);
confTxns.clear();
BlockPair b2 = createFakeBlock(params, blockStore, send1);
wallet.receiveFromBlock(send1, b2.storedBlock, BlockChain.NewBlockType.BEST_CHAIN);
wallet.notifyNewBestBlock(b2.block);
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90");
assertEquals(null, txn[0]);
assertEquals(2, confTxns.size());
assertEquals(txn[1].getHash(), send1.getHash());
assertEquals(bitcoinValueToFriendlyString(bigints[2]), "1.00");
assertEquals(bitcoinValueToFriendlyString(bigints[3]), "0.90");
@ -257,9 +270,14 @@ public class WalletTest {
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10));
// What we'd really like to do is prove the official client would accept it .... no such luck unfortunately.
wallet.commitTx(send2);
StoredBlock b3 = createFakeBlock(params, blockStore, send2).storedBlock;
wallet.receiveFromBlock(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
BlockPair b3 = createFakeBlock(params, blockStore, send2);
wallet.receiveFromBlock(send2, b3.storedBlock, BlockChain.NewBlockType.BEST_CHAIN);
wallet.notifyNewBestBlock(b3.block);
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80");
Block b4 = createFakeBlock(params, blockStore).block;
confTxns.clear();
wallet.notifyNewBestBlock(b4);
assertEquals(3, confTxns.size());
}
@Test