Change the wallet to relay tx confidence events instead of generating them itself, which is a bit cleaner. Centralize state that needs to be rebuilt after a Java deserialization. Resolves issue 235.

This commit is contained in:
Mike Hearn 2012-08-20 17:39:46 +02:00
parent eff88810e2
commit fedfe9d0e6
3 changed files with 63 additions and 22 deletions

View file

@ -923,7 +923,10 @@ public class PeerGroup {
// the transaction. In future when peers sync up their memory pools after they connect // the transaction. In future when peers sync up their memory pools after they connect
// we could come back and change this. // we could come back and change this.
// //
// Now tell the wallet about the transaction as it didn't get informed before. // Now tell the wallet about the transaction. If the wallet created the transaction then
// it already knows and will ignore this. If it's a transaction we received from
// somebody else via a side channel and are now broadcasting, this will put it into the
// wallet now we know it's valid.
for (Wallet wallet : wallets) { for (Wallet wallet : wallets) {
try { try {
wallet.receivePending(pinnedTx); wallet.receivePending(pinnedTx);

View file

@ -161,7 +161,7 @@ public class Wallet implements Serializable {
*/ */
private Sha256Hash lastBlockSeenHash; private Sha256Hash lastBlockSeenHash;
transient private ArrayList<WalletEventListener> eventListeners; private transient ArrayList<WalletEventListener> eventListeners;
// Auto-save code. This all should be generalized in future to not be file specific so you can easily store the // Auto-save code. This all should be generalized in future to not be file specific so you can easily store the
// wallet into a database using the same mechanism. However we need to inform stores of each specific change with // wallet into a database using the same mechanism. However we need to inform stores of each specific change with
@ -173,6 +173,10 @@ public class Wallet implements Serializable {
private transient AutosaveEventListener autosaveEventListener; private transient AutosaveEventListener autosaveEventListener;
private transient long autosaveDelayMs; private transient long autosaveDelayMs;
// 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;
/** /**
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead, * Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
* see loadFromFile. * see loadFromFile.
@ -185,7 +189,16 @@ public class Wallet implements Serializable {
inactive = new HashMap<Sha256Hash, Transaction>(); inactive = new HashMap<Sha256Hash, Transaction>();
pending = new HashMap<Sha256Hash, Transaction>(); pending = new HashMap<Sha256Hash, Transaction>();
dead = new HashMap<Sha256Hash, Transaction>(); dead = new HashMap<Sha256Hash, Transaction>();
createTransientState();
}
private void createTransientState() {
eventListeners = new ArrayList<WalletEventListener>(); eventListeners = new ArrayList<WalletEventListener>();
txConfidenceListener = new TransactionConfidence.Listener() {
public void onConfidenceChanged(Transaction tx) {
invokeOnTransactionConfidenceChanged(tx);
}
};
} }
public NetworkParameters getNetworkParameters() { public NetworkParameters getNetworkParameters() {
@ -529,7 +542,7 @@ public class Wallet implements Serializable {
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); in.defaultReadObject();
eventListeners = new ArrayList<WalletEventListener>(); createTransientState();
} }
/** /**
@ -599,12 +612,14 @@ public class Wallet implements Serializable {
TransactionConfidence.ConfidenceType currentConfidence = tx.getConfidence().getConfidenceType(); TransactionConfidence.ConfidenceType currentConfidence = tx.getConfidence().getConfidenceType();
if (currentConfidence == TransactionConfidence.ConfidenceType.UNKNOWN) { if (currentConfidence == TransactionConfidence.ConfidenceType.UNKNOWN) {
tx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN); tx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
// Manually invoke the wallet tx confidence listener here as we didn't yet commit therefore the
// txConfidenceListener wasn't added.
invokeOnTransactionConfidenceChanged(tx); invokeOnTransactionConfidenceChanged(tx);
} }
// If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This // If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This
// ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the // ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the
// timestamp on the transaction and runs event listeners. // timestamp on the transaction and registers/runs event listeners.
// //
// Note that after we return from this function, the wallet may have been modified. // Note that after we return from this function, the wallet may have been modified.
commitTx(tx); commitTx(tx);
@ -799,7 +814,6 @@ public class Wallet implements Serializable {
Set<Transaction> transactions = getTransactions(true, false); Set<Transaction> transactions = getTransactions(true, false);
for (Transaction tx : transactions) { for (Transaction tx : transactions) {
tx.getConfidence().notifyWorkDone(block); tx.getConfidence().notifyWorkDone(block);
invokeOnTransactionConfidenceChanged(tx);
} }
} }
queueAutoSave(); queueAutoSave();
@ -859,7 +873,6 @@ public class Wallet implements Serializable {
dead.put(doubleSpend.getHash(), doubleSpend); dead.put(doubleSpend.getHash(), doubleSpend);
// Inform the event listeners of the newly dead tx. // Inform the event listeners of the newly dead tx.
doubleSpend.getConfidence().setOverridingTransaction(tx); doubleSpend.getConfidence().setOverridingTransaction(tx);
invokeOnTransactionConfidenceChanged(doubleSpend);
} }
} }
@ -917,7 +930,6 @@ public class Wallet implements Serializable {
input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT); input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
// Inform the [tx] event listeners of the newly dead tx. This sets confidence type also. // Inform the [tx] event listeners of the newly dead tx. This sets confidence type also.
connected.getConfidence().setOverridingTransaction(tx); connected.getConfidence().setOverridingTransaction(tx);
invokeOnTransactionConfidenceChanged(connected);
} }
} else { } else {
// A pending transaction that tried to double spend our coins - we log and ignore it, because either // A pending transaction that tried to double spend our coins - we log and ignore it, because either
@ -996,7 +1008,7 @@ public class Wallet implements Serializable {
updateForSpends(tx, false); updateForSpends(tx, false);
// 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.
log.info("->pending: {}", tx.getHashAsString()); log.info("->pending: {}", tx.getHashAsString());
pending.put(tx.getHash(), tx); addWalletTransaction(Pool.PENDING, tx);
// Event listeners may re-enter so we cannot make assumptions about wallet state after this loop completes. // Event listeners may re-enter so we cannot make assumptions about wallet state after this loop completes.
try { try {
@ -1070,30 +1082,39 @@ public class Wallet implements Serializable {
* deserialization code, such as the {@link WalletProtobufSerializer} class. It isn't normally useful for * deserialization code, such as the {@link WalletProtobufSerializer} class. It isn't normally useful for
* applications. It does not trigger auto saving. * applications. It does not trigger auto saving.
*/ */
public synchronized void addWalletTransaction(WalletTransaction wtx) { public void addWalletTransaction(WalletTransaction wtx) {
switch (wtx.getPool()) { addWalletTransaction(wtx.getPool(), wtx.getTransaction());
}
/**
* 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.
*/
private synchronized void addWalletTransaction(Pool pool, Transaction tx) {
switch (pool) {
case UNSPENT: case UNSPENT:
unspent.put(wtx.getTransaction().getHash(), wtx.getTransaction()); unspent.put(tx.getHash(), tx);
break; break;
case SPENT: case SPENT:
spent.put(wtx.getTransaction().getHash(), wtx.getTransaction()); spent.put(tx.getHash(), tx);
break; break;
case PENDING: case PENDING:
pending.put(wtx.getTransaction().getHash(), wtx.getTransaction()); pending.put(tx.getHash(), tx);
break; break;
case DEAD: case DEAD:
dead.put(wtx.getTransaction().getHash(), wtx.getTransaction()); dead.put(tx.getHash(), tx);
break; break;
case INACTIVE: case INACTIVE:
inactive.put(wtx.getTransaction().getHash(), wtx.getTransaction()); inactive.put(tx.getHash(), tx);
break; break;
case PENDING_INACTIVE: case PENDING_INACTIVE:
pending.put(wtx.getTransaction().getHash(), wtx.getTransaction()); pending.put(tx.getHash(), tx);
inactive.put(wtx.getTransaction().getHash(), wtx.getTransaction()); inactive.put(tx.getHash(), tx);
break; break;
default: default:
throw new RuntimeException("Unknown wallet transaction type " + wtx.getPool()); throw new RuntimeException("Unknown wallet transaction type " + pool);
} }
tx.getConfidence().addEventListener(txConfidenceListener);
} }
/** /**
@ -1266,6 +1287,11 @@ public class Wallet implements Serializable {
return null; // Not enough money. return null; // Not enough money.
SendResult result = new SendResult(); SendResult result = new SendResult();
result.tx = tx; result.tx = tx;
// The tx has been committed to the pending pool by this point (via sendCoinsOffline -> commitTx), so it has
// a txConfidenceListener registered. Once the tx is broadcast the peers will update the memory pool with the
// count of seen peers, the memory pool will update the transaction confidence object, that will invoke the
// txConfidenceListener which will in turn invoke the wallets event listener onTransactionConfidenceChanged
// method.
result.broadcastComplete = peerGroup.broadcastTransaction(tx); result.broadcastComplete = peerGroup.broadcastTransaction(tx);
return result; return result;
} }
@ -1744,7 +1770,6 @@ public class Wallet implements Serializable {
// Set transaction confidence to dead and notify listeners. // Set transaction confidence to dead and notify listeners.
tx.getConfidence().setConfidenceType(ConfidenceType.DEAD); tx.getConfidence().setConfidenceType(ConfidenceType.DEAD);
invokeOnTransactionConfidenceChanged(tx);
} }
} }
@ -1892,7 +1917,6 @@ public class Wallet implements Serializable {
pending.remove(tx.getHash()); pending.remove(tx.getHash());
// This updates the tx confidence type automatically. // This updates the tx confidence type automatically.
tx.getConfidence().setOverridingTransaction(replacement); tx.getConfidence().setOverridingTransaction(replacement);
invokeOnTransactionConfidenceChanged(tx);
break; break;
} }
} }

View file

@ -303,11 +303,23 @@ public class PeerGroupTest extends TestWithNetworkConnections {
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance()); assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
// Check that the wallet informs us of changes in confidence as the transaction ripples across the network.
final Transaction[] transactions = new Transaction[1];
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
transactions[0] = tx;
}
});
// Now create a spend, and expect the announcement on p1. // Now create a spend, and expect the announcement on p1.
Address dest = new ECKey().toAddress(params); Address dest = new ECKey().toAddress(params);
Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, Utils.toNanoCoins(1, 0)); Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, Utils.toNanoCoins(1, 0));
assertNotNull(sendResult.tx); assertNotNull(sendResult.tx);
assertFalse(sendResult.broadcastComplete.isDone()); assertFalse(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
assertEquals(transactions[0].getConfidence().numBroadcastPeers(), 1);
transactions[0] = null;
Transaction t1 = (Transaction) outbound(p1); Transaction t1 = (Transaction) outbound(p1);
assertNotNull(t1); assertNotNull(t1);
// 49 BTC in change. // 49 BTC in change.
@ -317,6 +329,8 @@ public class PeerGroupTest extends TestWithNetworkConnections {
inv.addTransaction(t1); inv.addTransaction(t1);
inbound(p2, inv); inbound(p2, inv);
assertTrue(sendResult.broadcastComplete.isDone()); assertTrue(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
assertEquals(transactions[0].getConfidence().numBroadcastPeers(), 2);
// Confirm it. // Confirm it.
Block b2 = TestUtils.createFakeBlock(params, blockStore, t1).block; Block b2 = TestUtils.createFakeBlock(params, blockStore, t1).block;
inbound(p1, b2); inbound(p1, b2);