mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-03-10 09:20:04 +01:00
Wallet: Rewrite re-org handling to be simpler and use less code. And hopefully fix some bugs along the way.
This commit is contained in:
parent
5ae00d4e20
commit
aa883b48b1
7 changed files with 230 additions and 479 deletions
|
@ -89,94 +89,28 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
|
||||
protected final ReentrantLock lock = Locks.lock("wallet");
|
||||
|
||||
// Algorithm for movement of transactions between pools. Outbound tx = us spending coins. Inbound tx = us
|
||||
// receiving coins. If a tx is both inbound and outbound (spend with change) it is considered outbound for the
|
||||
// purposes of the explanation below.
|
||||
// The various pools below give quick access to wallet-relevant transactions by the state they're in:
|
||||
//
|
||||
// 1. Outbound tx is created by us: ->pending
|
||||
// 2. Outbound tx that was broadcast is accepted into the main chain:
|
||||
// <-pending and
|
||||
// If there is a change output ->unspent
|
||||
// If there is no change output ->spent
|
||||
// 3. Outbound tx that was broadcast is accepted into a side chain:
|
||||
// ->inactive (remains in pending).
|
||||
// 4. Inbound tx is accepted into the best chain:
|
||||
// ->unspent/spent
|
||||
// check if any pending transactions spend these outputs, if so, potentially <-unspent ->spent
|
||||
// 5. Inbound tx is accepted into a side chain:
|
||||
// ->inactive
|
||||
// Whilst it's also 'pending' in some sense, in that miners will probably try and incorporate it into the
|
||||
// best chain, we don't mark it as such here. It'll eventually show up after a re-org.
|
||||
// 6. Outbound tx that is pending shares inputs with a tx that appears in the main chain:
|
||||
// <-pending ->dead
|
||||
//
|
||||
// Re-orgs:
|
||||
// 1. Tx is present in old chain and not present in new chain
|
||||
// <-unspent/spent ->pending
|
||||
// These newly inactive transactions will (if they are relevant to us) eventually come back via receive()
|
||||
// as miners resurrect them and re-include into the new best chain.
|
||||
// 2. Tx is not present in old chain and is present in new chain
|
||||
// <-inactive and ->unspent/spent
|
||||
// 3. Tx is present in new chain and shares inputs with a pending transaction, including those that were resurrected
|
||||
// due to point (1)
|
||||
// <-pending ->dead
|
||||
//
|
||||
// Balance:
|
||||
// Take all the candidates for spending from unspent and pending. Select the ones that are actually available
|
||||
// according to our spend policy. Sum them up.
|
||||
// Pending: Transactions that didn't make it into the best chain yet. Pending transactions can be killed if a
|
||||
// double-spend against them appears in the best chain, in which case they move to the dead pool.
|
||||
// If a double-spend appears in the pending state as well, currently we just ignore the second
|
||||
// and wait for the miners to resolve the race.
|
||||
// Unspent: Transactions that appeared in the best chain and have outputs we can spend. Note that we store the
|
||||
// entire transaction in memory even though for spending purposes we only really need the outputs, the
|
||||
// reason being that this simplifies handling of re-orgs. It would be worth fixing this in future.
|
||||
// Spent: Transactions that appeared in the best chain but don't have any spendable outputs. They're stored here
|
||||
// for history browsing/auditing reasons only and in future will probably be flushed out to some other
|
||||
// kind of cold storage or just removed.
|
||||
// Dead: Transactions that we believe will never confirm get moved here, out of pending. Note that the Satoshi
|
||||
// client has no notion of dead-ness: the assumption is that double spends won't happen so there's no
|
||||
// need to notify the user about them. We take a more pessimistic approach and try to track the fact that
|
||||
// transactions have been double spent so applications can do something intelligent (cancel orders, show
|
||||
// to the user in the UI, etc). A transaction can leave dead and move into spent/unspent if there is a
|
||||
// re-org to a chain that doesn't include the double spend.
|
||||
|
||||
/**
|
||||
* Map of txhash->Transactions that have not made it into the best chain yet. They are eligible to move there but
|
||||
* are waiting for a miner to create a block on the best chain including them. These transactions inputs count as
|
||||
* spent for the purposes of calculating our balance but their outputs are not available for spending yet. This
|
||||
* means after a spend, our balance can actually go down temporarily before going up again! We should fix this to
|
||||
* allow spending of pending transactions.
|
||||
*
|
||||
* Pending transactions get announced to peers when they first connect. This means that if we're currently offline,
|
||||
* we can still create spends and upload them to the network later.
|
||||
*/
|
||||
final Map<Sha256Hash, Transaction> pending;
|
||||
|
||||
/**
|
||||
* Map of txhash->Transactions where the Transaction has unspent outputs. These are transactions we can use
|
||||
* to pay other people and so count towards our balance. Transactions only appear in this map if they are part
|
||||
* of the best chain. Transactions we have broacast that are not confirmed yet appear in pending even though they
|
||||
* may have unspent "change" outputs.<p>
|
||||
* <p/>
|
||||
* Note: for now we will not allow spends of transactions that did not make it into the block chain. The code
|
||||
* that handles this in Bitcoin C++ is complicated. Satoshis code will not allow you to spend unconfirmed coins,
|
||||
* however, it does seem to support dependency resolution entirely within the context of the memory pool so
|
||||
* theoretically you could spend zero-conf coins and all of them would be included together. To simplify we'll
|
||||
* make people wait but it would be a good improvement to resolve this in future.
|
||||
*/
|
||||
final Map<Sha256Hash, Transaction> unspent;
|
||||
|
||||
/**
|
||||
* Map of txhash->Transactions where the Transactions outputs are all fully spent. They are kept separately so
|
||||
* the time to create a spend does not grow infinitely as wallets become more used. Some of these transactions
|
||||
* may not have appeared in a block yet if they were created by us to spend coins and that spend is still being
|
||||
* worked on by miners.<p>
|
||||
* <p/>
|
||||
* Transactions only appear in this map if they are part of the best chain.
|
||||
*/
|
||||
final Map<Sha256Hash, Transaction> spent;
|
||||
|
||||
/**
|
||||
* An inactive transaction is one that is seen only in a block that is not a part of the best chain. We keep it
|
||||
* around in case a re-org promotes a different chain to be the best. In this case some (not necessarily all)
|
||||
* inactive transactions will be moved out to unspent and spent, and some might be moved in.<p>
|
||||
* <p/>
|
||||
* Note that in the case where a transaction appears in both the best chain and a side chain as well, it is not
|
||||
* placed in this map. It's an error for a transaction to be in both the inactive pool and unspent/spent.
|
||||
*/
|
||||
final Map<Sha256Hash, Transaction> inactive;
|
||||
|
||||
/**
|
||||
* A dead transaction is one that's been overridden by a double spend. Such a transaction is pending except it
|
||||
* will never confirm and so should be presented to the user in some unique way - flashing red for example. This
|
||||
* should nearly never happen in normal usage. Dead transactions can be "resurrected" by re-orgs just like any
|
||||
* other. Dead transactions are not in the pending pool.
|
||||
*/
|
||||
final Map<Sha256Hash, Transaction> dead;
|
||||
|
||||
// A list of public/private EC keys owned by this user. Access it using addKey[s], hasKey[s] and findPubKeyFromHash.
|
||||
|
@ -332,7 +266,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
keychain = new ArrayList<ECKey>();
|
||||
unspent = new HashMap<Sha256Hash, Transaction>();
|
||||
spent = new HashMap<Sha256Hash, Transaction>();
|
||||
inactive = new HashMap<Sha256Hash, Transaction>();
|
||||
pending = new HashMap<Sha256Hash, Transaction>();
|
||||
dead = new HashMap<Sha256Hash, Transaction>();
|
||||
eventListeners = new CopyOnWriteArrayList<WalletEventListener>();
|
||||
|
@ -748,12 +681,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
lock.lock();
|
||||
try {
|
||||
boolean success = true;
|
||||
// Pending and inactive can overlap, so merge them before counting
|
||||
HashSet<Transaction> pendingInactive = new HashSet<Transaction>();
|
||||
pendingInactive.addAll(pending.values());
|
||||
pendingInactive.addAll(inactive.values());
|
||||
|
||||
Set<Transaction> transactions = getTransactions(true, true);
|
||||
Set<Transaction> transactions = getTransactions(true);
|
||||
|
||||
Set<Sha256Hash> hashes = new HashSet<Sha256Hash>();
|
||||
for (Transaction tx : transactions) {
|
||||
|
@ -767,7 +695,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
success = false;
|
||||
}
|
||||
|
||||
int size2 = unspent.size() + spent.size() + pendingInactive.size() + dead.size();
|
||||
int size2 = unspent.size() + spent.size() + pending.size() + dead.size();
|
||||
if (size1 != size2) {
|
||||
log.error("Inconsistent wallet sizes: {} {}", size1, size2);
|
||||
success = false;
|
||||
|
@ -1065,9 +993,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
// Runs in a peer thread.
|
||||
checkState(lock.isLocked());
|
||||
BigInteger prevBalance = getBalance();
|
||||
|
||||
Sha256Hash txHash = tx.getHash();
|
||||
|
||||
boolean bestChain = blockType == BlockChain.NewBlockType.BEST_CHAIN;
|
||||
boolean sideChain = blockType == BlockChain.NewBlockType.SIDE_CHAIN;
|
||||
|
||||
|
@ -1075,10 +1001,9 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
BigInteger valueSentToMe = tx.getValueSentToMe(this);
|
||||
BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe);
|
||||
|
||||
if (!reorg) {
|
||||
log.info("Received tx {} for {} BTC: {}", new Object[]{sideChain ? "on a side chain" : "",
|
||||
bitcoinValueToFriendlyString(valueDifference), tx.getHashAsString()});
|
||||
}
|
||||
log.info("Received tx {} for {} BTC: {} in block {}", new Object[]{sideChain ? "on a side chain" : "",
|
||||
bitcoinValueToFriendlyString(valueDifference), tx.getHashAsString(),
|
||||
block != null ? block.getHeader().getHash() : "(unit test)"});
|
||||
|
||||
onWalletChangedSuppressions++;
|
||||
|
||||
|
@ -1086,67 +1011,49 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
// least we need to ensure we're manipulating the canonical object rather than a duplicate.
|
||||
Transaction wtx;
|
||||
if ((wtx = pending.remove(txHash)) != null) {
|
||||
log.info(" <-pending");
|
||||
// Make sure "tx" is always the canonical object we want to manipulate, send to event handlers, etc.
|
||||
tx = wtx;
|
||||
}
|
||||
boolean wasPending = wtx != null;
|
||||
|
||||
log.info(" <-pending");
|
||||
// A transaction we created appeared in a block. Probably this is a spend we broadcast that has been
|
||||
// accepted by the network.
|
||||
if (bestChain) {
|
||||
// Was confirmed.
|
||||
if (tx.isEveryOwnedOutputSpent(this)) {
|
||||
// There were no change transactions so this tx is fully spent
|
||||
log.info(" ->spent");
|
||||
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");
|
||||
addWalletTransaction(Pool.UNSPENT, tx);
|
||||
if (bestChain) {
|
||||
if (wasPending) {
|
||||
// Was pending and is now confirmed. Disconnect the outputs in case we spent any already: they will be
|
||||
// re-connected by processTxFromBestChain below.
|
||||
for (TransactionOutput output : tx.getOutputs()) {
|
||||
final TransactionInput spentBy = output.getSpentBy();
|
||||
if (spentBy != null) spentBy.disconnect();
|
||||
}
|
||||
} else if (sideChain) {
|
||||
// The transaction was accepted on an inactive side chain, but not yet by the best chain.
|
||||
log.info(" ->inactive");
|
||||
// It's OK for this to already be in the inactive pool because there can be multiple independent side
|
||||
// chains in which it appears:
|
||||
//
|
||||
// b1 --> b2
|
||||
// \-> b3
|
||||
// \-> b4 (at this point it's already present in 'inactive'
|
||||
boolean alreadyPresent = inactive.put(tx.getHash(), tx) != null;
|
||||
if (alreadyPresent)
|
||||
log.info("Saw a transaction be incorporated into multiple independent side chains");
|
||||
// Put it back into the pending pool, because 'pending' means 'waiting to be included in best chain'.
|
||||
pending.put(tx.getHash(), tx);
|
||||
}
|
||||
// TODO: This can trigger tx confidence listeners to be run in the case of double spends.
|
||||
// We should delay the execution of the listeners until the bottom to avoid the wallet mutating.
|
||||
processTxFromBestChain(tx);
|
||||
} else {
|
||||
// This TX wasn't in the memory pool. It could be sending us coins and also spending our own coins if keys
|
||||
// are being shared between different wallets.
|
||||
if (sideChain) {
|
||||
if (unspent.containsKey(tx.getHash()) || spent.containsKey(tx.getHash())) {
|
||||
// This side chain block contains transactions that already appeared in the best chain. It's normal,
|
||||
// we don't need to consider this transaction inactive, we can just ignore it.
|
||||
} else {
|
||||
log.info(" ->inactive");
|
||||
addWalletTransaction(Pool.INACTIVE, tx);
|
||||
checkState(sideChain);
|
||||
// Transactions that appear in a side chain will have that appearance recorded below - we assume that
|
||||
// some miners are also trying to include the transaction into the current best chain too, so let's treat
|
||||
// it as pending, except we don't need to do any risk analysis on it.
|
||||
if (wasPending) {
|
||||
// Just put it back in without touching the connections.
|
||||
addWalletTransaction(Pool.PENDING, tx);
|
||||
} else {
|
||||
// Ignore the case where a tx appears on a side chain at the same time as the best chain (this is
|
||||
// quite normal and expected).
|
||||
Sha256Hash hash = tx.getHash();
|
||||
if (!unspent.containsKey(hash) && !spent.containsKey(hash)) {
|
||||
// Otherwise put it (possibly back) into pending.
|
||||
// Committing it updates the spent flags and inserts into the pool as well.
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING);
|
||||
commitTx(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.
|
||||
//
|
||||
// TODO: 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);
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: The code beyond this point can trigger event listeners on transaction confidence objects, which are
|
||||
// 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.
|
||||
|
||||
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.
|
||||
// TODO: This can trigger re-entrancy: delay running confidence listeners.
|
||||
tx.setBlockAppearance(block, bestChain);
|
||||
if (bestChain) {
|
||||
// Don't notify this tx of work done in notifyNewBestBlock which will be called immediately after
|
||||
|
@ -1156,9 +1063,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
}
|
||||
}
|
||||
|
||||
BigInteger newBalance = getBalance(); // This is slow.
|
||||
log.info("Balance is now: " + bitcoinValueToFriendlyString(newBalance));
|
||||
|
||||
// Inform anyone interested that we have received or sent coins but only if:
|
||||
// - This is not due to a re-org.
|
||||
// - The coins appeared on the best chain.
|
||||
|
@ -1166,10 +1070,9 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
// - We have not already informed the user about the coins when we received the tx broadcast, or for our
|
||||
// own spends. If users want to know when a broadcast tx becomes confirmed, they need to use tx confidence
|
||||
// listeners.
|
||||
//
|
||||
// TODO: Decide whether to run the event listeners, if a tx confidence listener already modified the wallet.
|
||||
boolean wasPending = wtx != null;
|
||||
if (!reorg && bestChain && !wasPending) {
|
||||
BigInteger newBalance = getBalance(); // This is slow.
|
||||
log.info("Balance is now: " + bitcoinValueToFriendlyString(newBalance));
|
||||
int diff = valueDifference.compareTo(BigInteger.ZERO);
|
||||
// We pick one callback based on the value difference, though a tx can of course both send and receive
|
||||
// coins from the wallet.
|
||||
|
@ -1213,7 +1116,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
// Notify all the BUILDING transactions of the new block.
|
||||
// This is so that they can update their work done and depth.
|
||||
onWalletChangedSuppressions++;
|
||||
Set<Transaction> transactions = getTransactions(true, false);
|
||||
Set<Transaction> transactions = getTransactions(true);
|
||||
for (Transaction tx : transactions) {
|
||||
if (ignoreNextNewBlock.contains(tx.getHash())) {
|
||||
// tx was already processed in receive() due to it appearing in this block, so we don't want to
|
||||
|
@ -1233,10 +1136,12 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
|
||||
/**
|
||||
* Handle when a transaction becomes newly active on the best chain, either due to receiving a new block or a
|
||||
* re-org making inactive transactions active.
|
||||
* re-org. Places the tx into the right pool, handles coinbase transactions, handles double-spends and so on.
|
||||
*/
|
||||
private void processTxFromBestChain(Transaction tx) throws VerificationException {
|
||||
checkState(lock.isLocked());
|
||||
checkState(!pending.containsKey(tx.getHash()));
|
||||
|
||||
// This TX may spend our existing outputs even though it was not pending. This can happen in unit
|
||||
// tests, if keys are moved between wallets, if we're catching up to the chain given only a set of keys,
|
||||
// or if a dead coinbase transaction has moved back onto the main chain.
|
||||
|
@ -1250,13 +1155,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
dead.remove(tx.getHash());
|
||||
}
|
||||
|
||||
if (inactive.containsKey(tx.getHash())) {
|
||||
// This transaction was seen first on a side chain, but now it's also been seen in the best chain.
|
||||
// So we don't need to track it as inactive anymore.
|
||||
log.info(" new tx {} <-inactive", tx.getHashAsString());
|
||||
inactive.remove(tx.getHash());
|
||||
}
|
||||
|
||||
// Update tx and other unspent/pending transactions by connecting inputs/outputs.
|
||||
updateForSpends(tx, true);
|
||||
|
||||
|
@ -1267,15 +1165,15 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
if (hasOutputsToMe) {
|
||||
// Needs to go into either unspent or spent (if the outputs were already spent by a pending tx).
|
||||
if (tx.isEveryOwnedOutputSpent(this)) {
|
||||
log.info(" new tx {} ->spent (by pending)", tx.getHashAsString());
|
||||
log.info(" tx {} ->spent (by pending)", tx.getHashAsString());
|
||||
addWalletTransaction(Pool.SPENT, tx);
|
||||
} else {
|
||||
log.info(" new tx {} ->unspent", tx.getHashAsString());
|
||||
log.info(" tx {} ->unspent", tx.getHashAsString());
|
||||
addWalletTransaction(Pool.UNSPENT, tx);
|
||||
}
|
||||
} else if (tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0) {
|
||||
// Didn't send us any money, but did spend some. Keep it around for record keeping purposes.
|
||||
log.info(" new tx {} ->spent", tx.getHashAsString());
|
||||
log.info(" tx {} ->spent", tx.getHashAsString());
|
||||
addWalletTransaction(Pool.SPENT, tx);
|
||||
}
|
||||
|
||||
|
@ -1302,6 +1200,8 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
*/
|
||||
private void updateForSpends(Transaction tx, boolean fromChain) throws VerificationException {
|
||||
checkState(lock.isLocked());
|
||||
if (fromChain)
|
||||
checkState(!pending.containsKey(tx.getHash()));
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
TransactionInput.ConnectionResult result = input.connect(unspent, TransactionInput.ConnectMode.ABORT_ON_CONFLICT);
|
||||
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
|
||||
|
@ -1319,13 +1219,12 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
|
||||
if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
|
||||
if (fromChain) {
|
||||
// This will be handled later by processTxFromBestChain.
|
||||
// Double spend from chain: this will be handled later by checkForDoubleSpendAgainstPending.
|
||||
} else {
|
||||
// We saw two pending transactions that double spend each other. We don't know which will win.
|
||||
// Either that, or we somehow allowed ourselves to create double spends ourselves!
|
||||
// TODO: Find some way to communicate to the user that both transactions in jeopardy.
|
||||
log.warn("Saw double spend from another pending transaction, ignoring tx {}",
|
||||
tx.getHashAsString());
|
||||
// This should not happen.
|
||||
log.warn("Saw two pending transactions double spend each other: {} vs {}",
|
||||
tx.getHash(), input.getConnectedOutput().getSpentBy().getParentTransaction().getHash());
|
||||
log.warn(" offending input is input {}", tx.getInputs().indexOf(input));
|
||||
}
|
||||
} else if (result == TransactionInput.ConnectionResult.SUCCESS) {
|
||||
|
@ -1361,13 +1260,24 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
|
||||
// Updates the wallet when a double spend occurs.
|
||||
private void killTx(Transaction overridingTx, TransactionInput overridingInput, Transaction killedTx) {
|
||||
final Sha256Hash killedTxHash = killedTx.getHash();
|
||||
if (overridingTx == null) {
|
||||
// killedTx depended on a transaction that died because it was double spent or a coinbase that got re-orgd.
|
||||
killedTx.getConfidence().setOverridingTransaction(null);
|
||||
pending.remove(killedTxHash);
|
||||
unspent.remove(killedTxHash);
|
||||
spent.remove(killedTxHash);
|
||||
addWalletTransaction(Pool.DEAD, killedTx);
|
||||
// TODO: Properly handle the recursive nature of killing transactions here.
|
||||
return;
|
||||
}
|
||||
TransactionOutPoint overriddenOutPoint = overridingInput.getOutpoint();
|
||||
// It is expected that we may not have the overridden/double-spent tx in our wallet ... in the (common?!) case
|
||||
// where somebody is stealing money from us, the overriden tx belongs to someone else.
|
||||
log.warn("Saw double spend of {} from chain override pending tx {}",
|
||||
overriddenOutPoint, killedTx.getHashAsString());
|
||||
log.warn(" <-pending ->dead killed by {}", overridingTx.getHashAsString());
|
||||
pending.remove(killedTx.getHash());
|
||||
pending.remove(killedTxHash);
|
||||
addWalletTransaction(Pool.DEAD, killedTx);
|
||||
log.info("Disconnecting inputs of the newly dead tx");
|
||||
for (TransactionInput deadInput : killedTx.getInputs()) {
|
||||
|
@ -1488,9 +1398,8 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
/**
|
||||
* Returns a set of all transactions in the wallet.
|
||||
* @param includeDead If true, transactions that were overridden by a double spend are included.
|
||||
* @param includeInactive If true, transactions that are on side chains (are unspendable) are included.
|
||||
*/
|
||||
public Set<Transaction> getTransactions(boolean includeDead, boolean includeInactive) {
|
||||
public Set<Transaction> getTransactions(boolean includeDead) {
|
||||
lock.lock();
|
||||
try {
|
||||
Set<Transaction> all = new HashSet<Transaction>();
|
||||
|
@ -1499,8 +1408,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
all.addAll(pending.values());
|
||||
if (includeDead)
|
||||
all.addAll(dead.values());
|
||||
if (includeInactive)
|
||||
all.addAll(inactive.values());
|
||||
return all;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
@ -1513,24 +1420,11 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
public Iterable<WalletTransaction> getWalletTransactions() {
|
||||
lock.lock();
|
||||
try {
|
||||
HashSet<Transaction> pendingInactive = new HashSet<Transaction>();
|
||||
pendingInactive.addAll(pending.values());
|
||||
pendingInactive.retainAll(inactive.values());
|
||||
HashSet<Transaction> onlyPending = new HashSet<Transaction>();
|
||||
HashSet<Transaction> onlyInactive = new HashSet<Transaction>();
|
||||
onlyPending.addAll(pending.values());
|
||||
onlyPending.removeAll(pendingInactive);
|
||||
onlyInactive.addAll(inactive.values());
|
||||
onlyInactive.removeAll(pendingInactive);
|
||||
|
||||
Set<WalletTransaction> all = new HashSet<WalletTransaction>();
|
||||
|
||||
addWalletTransactionsToSet(all, Pool.UNSPENT, unspent.values());
|
||||
addWalletTransactionsToSet(all, Pool.SPENT, spent.values());
|
||||
addWalletTransactionsToSet(all, Pool.DEAD, dead.values());
|
||||
addWalletTransactionsToSet(all, Pool.PENDING, onlyPending);
|
||||
addWalletTransactionsToSet(all, Pool.INACTIVE, onlyInactive);
|
||||
addWalletTransactionsToSet(all, Pool.PENDING_INACTIVE, pendingInactive);
|
||||
addWalletTransactionsToSet(all, Pool.PENDING, pending.values());
|
||||
return all;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
@ -1565,23 +1459,19 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
checkState(lock.isLocked());
|
||||
switch (pool) {
|
||||
case UNSPENT:
|
||||
Preconditions.checkState(unspent.put(tx.getHash(), tx) == null);
|
||||
checkState(unspent.put(tx.getHash(), tx) == null);
|
||||
break;
|
||||
case SPENT:
|
||||
Preconditions.checkState(spent.put(tx.getHash(), tx) == null);
|
||||
checkState(spent.put(tx.getHash(), tx) == null);
|
||||
break;
|
||||
case PENDING:
|
||||
Preconditions.checkState(pending.put(tx.getHash(), tx) == null);
|
||||
checkState(pending.put(tx.getHash(), tx) == null);
|
||||
break;
|
||||
case DEAD:
|
||||
Preconditions.checkState(dead.put(tx.getHash(), tx) == null);
|
||||
break;
|
||||
case INACTIVE:
|
||||
Preconditions.checkState(inactive.put(tx.getHash(), tx) == null);
|
||||
checkState(dead.put(tx.getHash(), tx) == null);
|
||||
break;
|
||||
case PENDING_INACTIVE:
|
||||
Preconditions.checkState(pending.put(tx.getHash(), tx) == null);
|
||||
Preconditions.checkState(inactive.put(tx.getHash(), tx) == null);
|
||||
checkState(pending.put(tx.getHash(), tx) == null);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown wallet transaction type " + pool);
|
||||
|
@ -1617,7 +1507,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
if (numTransactions > size || numTransactions == 0) {
|
||||
numTransactions = size;
|
||||
}
|
||||
ArrayList<Transaction> all = new ArrayList<Transaction>(getTransactions(includeDead, false));
|
||||
ArrayList<Transaction> all = new ArrayList<Transaction>(getTransactions(includeDead));
|
||||
// Order by date.
|
||||
Collections.sort(all, Collections.reverseOrder(new Comparator<Transaction>() {
|
||||
public int compare(Transaction t1, Transaction t2) {
|
||||
|
@ -1648,8 +1538,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
return tx;
|
||||
else if ((tx = spent.get(hash)) != null)
|
||||
return tx;
|
||||
else if ((tx = inactive.get(hash)) != null)
|
||||
return tx;
|
||||
else if ((tx = dead.get(hash)) != null)
|
||||
return tx;
|
||||
return null;
|
||||
|
@ -1670,7 +1558,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
unspent.clear();
|
||||
spent.clear();
|
||||
pending.clear();
|
||||
inactive.clear();
|
||||
dead.clear();
|
||||
queueAutoSave();
|
||||
} else {
|
||||
|
@ -1695,9 +1582,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
if (pending.containsKey(txHash)) {
|
||||
result.add(Pool.PENDING);
|
||||
}
|
||||
if (inactive.containsKey(txHash)) {
|
||||
result.add(Pool.INACTIVE);
|
||||
}
|
||||
if (dead.containsKey(txHash)) {
|
||||
result.add(Pool.DEAD);
|
||||
}
|
||||
|
@ -1717,12 +1601,10 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
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();
|
||||
return unspent.size() + spent.size() + pending.size() + dead.size();
|
||||
}
|
||||
throw new RuntimeException("Unreachable");
|
||||
} finally {
|
||||
|
@ -2246,7 +2128,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
builder.append(String.format(" %d unspent transactions%n", unspent.size()));
|
||||
builder.append(String.format(" %d spent transactions%n", spent.size()));
|
||||
builder.append(String.format(" %d pending transactions%n", pending.size()));
|
||||
builder.append(String.format(" %d inactive transactions%n", inactive.size()));
|
||||
builder.append(String.format(" %d dead transactions%n", dead.size()));
|
||||
builder.append(String.format("Last seen best block: (%d) %s%n",
|
||||
getLastBlockSeenHeight(), getLastBlockSeenHash()));
|
||||
|
@ -2275,10 +2156,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
builder.append("\nPENDING:\n");
|
||||
toStringHelper(builder, pending, chain);
|
||||
}
|
||||
if (inactive.size() > 0) {
|
||||
builder.append("\nINACTIVE:\n");
|
||||
toStringHelper(builder, inactive, chain);
|
||||
}
|
||||
if (dead.size() > 0) {
|
||||
builder.append("\nDEAD:\n");
|
||||
toStringHelper(builder, dead, chain);
|
||||
|
@ -2320,14 +2197,27 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
public void reorganize(StoredBlock splitPoint, List<StoredBlock> oldBlocks, List<StoredBlock> newBlocks) throws VerificationException {
|
||||
lock.lock();
|
||||
try {
|
||||
// This runs on any peer thread with the block chain synchronized.
|
||||
// This runs on any peer thread with the block chain locked.
|
||||
//
|
||||
// The reorganize functionality of the wallet is tested in ChainSplitTest.
|
||||
// The reorganize functionality of the wallet is tested in ChainSplitTest.java
|
||||
//
|
||||
// For each transaction we track which blocks they appeared in. Once a re-org takes place we have to find all
|
||||
// transactions in the old branch, all transactions in the new branch and find the difference of those sets.
|
||||
// receive() has been called on the block that is triggering the re-org before this is called, with type
|
||||
// of SIDE_CHAIN.
|
||||
//
|
||||
// receive() has been called on the block that is triggering the re-org before this is called.
|
||||
// Note that this code assumes blocks are not invalid - if blocks contain duplicated transactions,
|
||||
// transactions that double spend etc then we can calculate the incorrect result. This could open up
|
||||
// obscure DoS attacks if someone successfully mines a throwaway invalid block and feeds it to us, just
|
||||
// to try and corrupt the internal data structures. We should try harder to avoid this but it's tricky
|
||||
// because there are so many ways the block can be invalid.
|
||||
|
||||
// Map block hash to transactions that appear in it.
|
||||
Multimap<Sha256Hash, Transaction> mapBlockTx = ArrayListMultimap.create();
|
||||
for (Transaction tx : getTransactions(true)) {
|
||||
Collection<Sha256Hash> appearsIn = tx.getAppearsInHashes();
|
||||
if (appearsIn == null) continue; // Pending.
|
||||
for (Sha256Hash block : appearsIn)
|
||||
mapBlockTx.put(block, tx);
|
||||
}
|
||||
|
||||
List<Sha256Hash> oldBlockHashes = new ArrayList<Sha256Hash>(oldBlocks.size());
|
||||
List<Sha256Hash> newBlockHashes = new ArrayList<Sha256Hash>(newBlocks.size());
|
||||
|
@ -2342,64 +2232,13 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
newBlockHashes.add(b.getHeader().getHash());
|
||||
}
|
||||
|
||||
// Transactions that appear in the old chain segment.
|
||||
Map<Sha256Hash, Transaction> oldChainTransactions = new HashMap<Sha256Hash, Transaction>();
|
||||
// Transactions that appear in the old chain segment and NOT the new chain segment.
|
||||
Map<Sha256Hash, Transaction> onlyOldChainTransactions = new HashMap<Sha256Hash, Transaction>();
|
||||
// Transactions that appear in the new chain segment.
|
||||
Map<Sha256Hash, Transaction> newChainTransactions = new HashMap<Sha256Hash, Transaction>();
|
||||
// Transactions that don't appear in either the new or the old section, ie, the shared trunk.
|
||||
Map<Sha256Hash, Transaction> commonChainTransactions = new HashMap<Sha256Hash, Transaction>();
|
||||
|
||||
Map<Sha256Hash, Transaction> all = new HashMap<Sha256Hash, Transaction>();
|
||||
all.putAll(unspent);
|
||||
all.putAll(spent);
|
||||
all.putAll(inactive);
|
||||
|
||||
// Dead coinbase transactions are potentially resurrected so added to the list of tx to process.
|
||||
for (Transaction tx : dead.values()) {
|
||||
if (tx.isCoinBase()) {
|
||||
all.put(tx.getHash(), tx);
|
||||
boolean affectedUs = false;
|
||||
for (Sha256Hash hash : Iterables.concat(oldBlockHashes, newBlockHashes)) {
|
||||
if (mapBlockTx.get(hash) != null) {
|
||||
affectedUs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Map block hash to transactions that appear in it.
|
||||
Multimap<Sha256Hash, Transaction> blockTxMap = ArrayListMultimap.create();
|
||||
|
||||
for (Transaction tx : all.values()) {
|
||||
Collection<Sha256Hash> appearsIn = tx.getAppearsInHashes();
|
||||
checkNotNull(appearsIn);
|
||||
|
||||
for (Sha256Hash block : appearsIn)
|
||||
blockTxMap.put(block, tx);
|
||||
|
||||
// If the set of blocks this transaction appears in is disjoint with one of the chain segments it means
|
||||
// the transaction was never incorporated by a miner into that side of the chain.
|
||||
boolean inOldSection = !Collections.disjoint(appearsIn, oldBlockHashes);
|
||||
boolean inNewSection = !Collections.disjoint(appearsIn, newBlockHashes);
|
||||
boolean inCommonSection = !inNewSection && !inOldSection;
|
||||
|
||||
if (inCommonSection) {
|
||||
boolean alreadyPresent = commonChainTransactions.put(tx.getHash(), tx) != null;
|
||||
checkState(!alreadyPresent, "Transaction appears twice in common chain segment");
|
||||
} else {
|
||||
if (inOldSection) {
|
||||
boolean alreadyPresent = oldChainTransactions.put(tx.getHash(), tx) != null;
|
||||
checkState(!alreadyPresent, "Transaction appears twice in old chain segment");
|
||||
if (!inNewSection) {
|
||||
alreadyPresent = onlyOldChainTransactions.put(tx.getHash(), tx) != null;
|
||||
checkState(!alreadyPresent, "Transaction appears twice in only-old map");
|
||||
}
|
||||
}
|
||||
if (inNewSection) {
|
||||
boolean alreadyPresent = newChainTransactions.put(tx.getHash(), tx) != null;
|
||||
checkState(!alreadyPresent, "Transaction appears twice in new chain segment");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no difference it means we have nothing we need to do and the user does not care.
|
||||
boolean affectedUs = !oldChainTransactions.equals(newChainTransactions);
|
||||
log.info(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions");
|
||||
if (!affectedUs) return;
|
||||
|
||||
|
@ -2407,80 +2246,71 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
// user from modifying wallet contents (eg, trying to spend) whilst we're in the middle of the process.
|
||||
onWalletChangedSuppressions++;
|
||||
|
||||
// For simplicity we will reprocess every transaction to ensure it's in the right bucket and has the right
|
||||
// connections. Attempting to update each one with minimal work is possible but complex and was leading to
|
||||
// edge cases that were hard to fix. As re-orgs are rare the amount of work this implies should be manageable
|
||||
// unless the user has an enormous wallet. As an optimization fully spent transactions buried deeper than
|
||||
// 1000 blocks could be put into yet another bucket which we never touch and assume re-orgs cannot affect.
|
||||
Collections.reverse(newBlocks); // Need bottom-to-top but we get top-to-bottom.
|
||||
|
||||
for (Transaction tx : onlyOldChainTransactions.values()) log.info(" Only Old: {}", tx.getHashAsString());
|
||||
for (Transaction tx : oldChainTransactions.values()) log.info(" Old: {}", tx.getHashAsString());
|
||||
for (Transaction tx : newChainTransactions.values()) log.info(" New: {}", tx.getHashAsString());
|
||||
|
||||
// Break all the existing connections.
|
||||
for (Transaction tx : all.values())
|
||||
tx.disconnectInputs();
|
||||
for (Transaction tx : pending.values())
|
||||
tx.disconnectInputs();
|
||||
// Reconnect the transactions in the common part of the chain.
|
||||
for (Transaction tx : commonChainTransactions.values()) {
|
||||
TransactionInput badInput = tx.connectForReorganize(all);
|
||||
checkState(badInput == null, "Failed to connect %s, %s", tx.getHashAsString(),
|
||||
badInput == null ? "" : badInput.toString());
|
||||
}
|
||||
// Recalculate the unspent/spent buckets for the transactions the re-org did not affect.
|
||||
log.info("Moving transactions");
|
||||
unspent.clear();
|
||||
spent.clear();
|
||||
inactive.clear();
|
||||
for (Transaction tx : commonChainTransactions.values()) {
|
||||
if (tx.isEveryOwnedOutputSpent(this))
|
||||
spent.put(tx.getHash(), tx);
|
||||
else
|
||||
unspent.put(tx.getHash(), tx);
|
||||
}
|
||||
|
||||
// Inform all transactions that exist only in the old chain that they have moved, so they can update confidence
|
||||
// and timestamps. Transactions will be told they're on the new best chain when the blocks are replayed.
|
||||
for (Transaction tx : onlyOldChainTransactions.values()) {
|
||||
// Kill any coinbase transactions that are only in the old chain.
|
||||
// These transactions are no longer valid.
|
||||
if (tx.isCoinBase()) {
|
||||
// Move the transaction to the dead pool.
|
||||
if (unspent.containsKey(tx.getHash())) {
|
||||
log.info(" coinbase tx {} unspent->dead", tx.getHashAsString());
|
||||
unspent.remove(tx.getHash());
|
||||
} else if (spent.containsKey(tx.getHash())) {
|
||||
log.info(" coinbase tx {} spent->dead", tx.getHashAsString());
|
||||
// TODO Remove any dependent child transactions of the just removed coinbase transaction.
|
||||
spent.remove(tx.getHash());
|
||||
// For each block in the old chain, disconnect the transactions. It doesn't matter if
|
||||
// we don't do it in the exact ordering they appeared in the chain, all we're doing is ensuring all
|
||||
// the outputs are freed up so we can connect them back again in the next step.
|
||||
LinkedList<Transaction> oldChainTxns = Lists.newLinkedList();
|
||||
for (Sha256Hash blockHash : oldBlockHashes) {
|
||||
for (Transaction tx : mapBlockTx.get(blockHash)) {
|
||||
final Sha256Hash txHash = tx.getHash();
|
||||
if (tx.isCoinBase()) {
|
||||
log.warn("Coinbase tx {} -> dead", tx.getHash());
|
||||
// All the transactions that we have in our wallet which spent this coinbase are now invalid
|
||||
// and will never confirm. Hopefully this should never happen - that's the point of the maturity
|
||||
// rule that forbids spending of coinbase transactions for 100 blocks.
|
||||
//
|
||||
// This could be recursive, although of course because we don't have the full transaction
|
||||
// graph we can never reliably kill all transactions we might have that were rooted in
|
||||
// this coinbase tx. Some can just go pending forever, like the Satoshi client. However we
|
||||
// can do our best.
|
||||
//
|
||||
// TODO: Is it better to try and sometimes fail, or not try at all?
|
||||
killTx(null, null, tx);
|
||||
} else {
|
||||
for (TransactionOutput output : tx.getOutputs()) {
|
||||
TransactionInput input = output.getSpentBy();
|
||||
if (input != null) input.disconnect();
|
||||
}
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
input.disconnect();
|
||||
}
|
||||
oldChainTxns.add(tx);
|
||||
unspent.remove(txHash);
|
||||
spent.remove(txHash);
|
||||
checkState(!pending.containsKey(txHash));
|
||||
checkState(!dead.containsKey(txHash));
|
||||
}
|
||||
dead.put(tx.getHash(), tx);
|
||||
|
||||
// Set transaction confidence to dead and notify listeners.
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.DEAD);
|
||||
}
|
||||
}
|
||||
|
||||
// Now replay the act of receiving the blocks that were previously in a side chain. This will:
|
||||
// - Move any transactions that were pending and are now accepted into the right bucket.
|
||||
// - Connect the newly active transactions.
|
||||
// Put all the disconnected transactions back into the pending pool and re-connect them.
|
||||
for (Transaction tx : oldChainTxns) {
|
||||
// Coinbase transactions on the old part of the chain are dead for good and won't come back unless
|
||||
// there's another re-org.
|
||||
if (tx.isCoinBase()) continue;
|
||||
log.info(" ->pending {}", tx.getHash());
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); // Wipe height/depth/work data.
|
||||
addWalletTransaction(Pool.PENDING, tx);
|
||||
updateForSpends(tx, false);
|
||||
}
|
||||
|
||||
Collections.reverse(newBlocks); // Need bottom-to-top but we get top-to-bottom.
|
||||
// Note that dead transactions stay dead. Consider a chain that Finney attacks T1 and replaces it with
|
||||
// T2, so we move T1 into the dead pool. If there's now a re-org to a chain that doesn't include T2, it
|
||||
// doesn't matter - the miners deleted T1 from their mempool, will resurrect T2 and put that into the
|
||||
// mempool and so T1 is still seen as a losing double spend.
|
||||
|
||||
// The old blocks have contributed to the depth and work done for all the transactions in the
|
||||
// wallet that are in blocks up to and including the chain split block.
|
||||
// The total depth and work done is calculated here and then subtracted from the appropriate transactions.
|
||||
int depthToSubtract = oldBlocks.size();
|
||||
|
||||
BigInteger workDoneToSubtract = BigInteger.ZERO;
|
||||
for (StoredBlock b : oldBlocks) {
|
||||
workDoneToSubtract = workDoneToSubtract.add(b.getHeader().getWork());
|
||||
}
|
||||
log.info("DepthToSubtract = " + depthToSubtract + ", workDoneToSubtract = " + workDoneToSubtract);
|
||||
|
||||
// Remove depthToSubtract and workDoneToSubtract from all transactions in the wallet except for pending and inactive
|
||||
// (i.e. the transactions in the two chains of blocks we are reorganising).
|
||||
log.info("depthToSubtract = " + depthToSubtract + ", workDoneToSubtract = " + workDoneToSubtract);
|
||||
// Remove depthToSubtract and workDoneToSubtract from all transactions in the wallet except for pending.
|
||||
subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, spent.values());
|
||||
subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, unspent.values());
|
||||
subtractDepthAndWorkDone(depthToSubtract, workDoneToSubtract, dead.values());
|
||||
|
@ -2488,64 +2318,22 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
// The effective last seen block is now the split point so set the lastSeenBlockHash.
|
||||
setLastBlockSeenHash(splitPoint.getHeader().getHash());
|
||||
|
||||
for (StoredBlock b : newBlocks) {
|
||||
log.info("Replaying block {}", b.getHeader().getHashAsString());
|
||||
// Replay means: find the transactions that should be in that block, send them to the wallet, inform of
|
||||
// new best block, repeat.
|
||||
Set<Transaction> txns = new HashSet<Transaction>();
|
||||
Sha256Hash blockHash = b.getHeader().getHash();
|
||||
for (Transaction tx : newChainTransactions.values()) {
|
||||
if (tx.getAppearsInHashes().contains(blockHash)) {
|
||||
txns.add(tx);
|
||||
log.info(" containing tx {}", tx.getHashAsString());
|
||||
// For each block in the new chain, work forwards calling receive() and notifyNewBestBlock().
|
||||
// This will pull them back out of the pending pool, or if the tx didn't appear in the old chain and
|
||||
// does appear in the new chain, will treat it as such and possibly kill pending transactions that
|
||||
// conflict.
|
||||
for (StoredBlock block : newBlocks) {
|
||||
log.info("Replaying block {}", block.getHeader().getHashAsString());
|
||||
for (Transaction tx : mapBlockTx.get(block.getHeader().getHash())) {
|
||||
log.info(" tx {}", tx.getHash());
|
||||
try {
|
||||
receive(tx, block, BlockChain.NewBlockType.BEST_CHAIN, true);
|
||||
} catch (ScriptException e) {
|
||||
throw new RuntimeException(e); // Cannot happen as these blocks were already verified.
|
||||
}
|
||||
}
|
||||
|
||||
if (!txns.isEmpty()) {
|
||||
// Add the transactions to the new blocks.
|
||||
for (Transaction t : txns) {
|
||||
try {
|
||||
receive(t, b, BlockChain.NewBlockType.BEST_CHAIN, true);
|
||||
} catch (ScriptException e) {
|
||||
throw new RuntimeException(e); // Cannot happen as these blocks were already verified.
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyNewBestBlock(b);
|
||||
notifyNewBestBlock(block);
|
||||
}
|
||||
|
||||
// Find the transactions that didn't make it into the new chain yet. For each input, try to connect it to the
|
||||
// transactions that are in {spent,unspent,pending}. Check the status of each input. For inactive
|
||||
// transactions that only send us money, we put them into the inactive pool where they sit around waiting for
|
||||
// another re-org or re-inclusion into the main chain. For inactive transactions where we spent money we must
|
||||
// put them back into the pending pool if we can reconnect them, so we don't create a double spend whilst the
|
||||
// network heals itself.
|
||||
Map<Sha256Hash, Transaction> pool = new HashMap<Sha256Hash, Transaction>();
|
||||
pool.putAll(unspent);
|
||||
pool.putAll(spent);
|
||||
pool.putAll(pending);
|
||||
Map<Sha256Hash, Transaction> toReprocess = new HashMap<Sha256Hash, Transaction>();
|
||||
toReprocess.putAll(onlyOldChainTransactions);
|
||||
toReprocess.putAll(pending);
|
||||
log.info("Reprocessing transactions not in new best chain:");
|
||||
// Note, we must reprocess dead transactions first. The reason is that if there is a double spend across
|
||||
// chains from our own coins we get a complicated situation:
|
||||
//
|
||||
// 1) We switch to a new chain (B) that contains a double spend overriding a pending transaction. The
|
||||
// pending transaction goes dead.
|
||||
// 2) We switch BACK to the first chain (A). The dead transaction must go pending again.
|
||||
// 3) We resurrect the transactions that were in chain (B) and assume the miners will start work on putting them
|
||||
// in to the chain, but it's not possible because it's a double spend. So now that transaction must become
|
||||
// dead instead of pending.
|
||||
//
|
||||
// This only occurs when we are double spending our own coins.
|
||||
for (Transaction tx : dead.values()) {
|
||||
reprocessUnincludedTxAfterReorg(pool, tx);
|
||||
}
|
||||
for (Transaction tx : toReprocess.values()) {
|
||||
reprocessUnincludedTxAfterReorg(pool, tx);
|
||||
}
|
||||
|
||||
log.info("post-reorg balance is {}", Utils.bitcoinValueToFriendlyString(getBalance()));
|
||||
// Inform event listeners that a re-org took place. They should save the wallet at this point.
|
||||
invokeOnReorganize();
|
||||
|
@ -2570,70 +2358,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void reprocessUnincludedTxAfterReorg(Map<Sha256Hash, Transaction> pool, Transaction tx) {
|
||||
checkState(lock.isLocked());
|
||||
log.info("TX {}", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name());
|
||||
|
||||
boolean isDeadCoinbase = tx.isCoinBase() && ConfidenceType.DEAD == tx.getConfidence().getConfidenceType();
|
||||
|
||||
// Dead coinbase transactions on a side chain stay dead.
|
||||
if (isDeadCoinbase) {
|
||||
return;
|
||||
}
|
||||
|
||||
int numInputs = tx.getInputs().size();
|
||||
int noSuchTx = 0;
|
||||
int success = 0;
|
||||
boolean isDead = false;
|
||||
// The transactions that we connected inputs to, so we can go back later and move them into the right
|
||||
// bucket if all their outputs got spent.
|
||||
Set<Transaction> connectedTransactions = new HashSet<Transaction>();
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
TransactionInput.ConnectionResult result = input.connect(pool, TransactionInput.ConnectMode.ABORT_ON_CONFLICT);
|
||||
if (result == TransactionInput.ConnectionResult.SUCCESS) {
|
||||
success++;
|
||||
TransactionOutput connectedOutput = checkNotNull(input.getConnectedOutput(pool));
|
||||
connectedTransactions.add(checkNotNull(connectedOutput.parentTransaction));
|
||||
} else if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
|
||||
noSuchTx++;
|
||||
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
|
||||
isDead = true;
|
||||
// This transaction was replaced by a double spend on the new chain. Did you just reverse
|
||||
// your own transaction? I hope not!!
|
||||
log.info(" ->dead, will not confirm now unless there's another re-org", tx.getHashAsString());
|
||||
TransactionOutput doubleSpent = input.getConnectedOutput(pool);
|
||||
Transaction replacement = doubleSpent.getSpentBy().getParentTransaction();
|
||||
dead.put(tx.getHash(), tx);
|
||||
pending.remove(tx.getHash());
|
||||
// This updates the tx confidence type automatically.
|
||||
tx.getConfidence().setOverridingTransaction(replacement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isDead) return;
|
||||
|
||||
// If all inputs do not appear in this wallet move to inactive.
|
||||
if (noSuchTx == numInputs) {
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING);
|
||||
log.info(" ->inactive", tx.getHashAsString() + ", confidence = PENDING");
|
||||
inactive.put(tx.getHash(), tx);
|
||||
dead.remove(tx.getHash());
|
||||
|
||||
} else if (success == numInputs - noSuchTx) {
|
||||
// All inputs are either valid for spending or don't come from us. Miners are trying to reinclude it.
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING);
|
||||
log.info(" ->pending", tx.getHashAsString() + ", confidence = PENDING");
|
||||
pending.put(tx.getHash(), tx);
|
||||
dead.remove(tx.getHash());
|
||||
}
|
||||
|
||||
// The act of re-connecting this un-included transaction may have caused other transactions to become fully
|
||||
// spent so move them into the right bucket here to keep performance good.
|
||||
for (Transaction maybeSpent : connectedTransactions) {
|
||||
maybeMovePool(maybeSpent, "reorg");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an immutable view of the transactions currently waiting for network confirmations.
|
||||
*/
|
||||
|
@ -2992,7 +2716,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
*/
|
||||
public int getBloomFilterElementCount() {
|
||||
int size = getKeychainSize() * 2;
|
||||
for (Transaction tx : getTransactions(false, true)) {
|
||||
for (Transaction tx : getTransactions(false)) {
|
||||
for (TransactionOutput out : tx.getOutputs()) {
|
||||
try {
|
||||
if (out.isMine(this) && out.getScriptPubKey().isSentToRawPubKey())
|
||||
|
@ -3034,7 +2758,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
for (Transaction tx : getTransactions(false, true)) {
|
||||
for (Transaction tx : getTransactions(false)) {
|
||||
for (int i = 0; i < tx.getOutputs().size(); i++) {
|
||||
TransactionOutput out = tx.getOutputs().get(i);
|
||||
try {
|
||||
|
|
|
@ -351,7 +351,7 @@ public class WalletProtobufSerializer {
|
|||
|
||||
byte[] pubKey = keyProto.hasPublicKey() ? keyProto.getPublicKey().toByteArray() : null;
|
||||
|
||||
ECKey ecKey = null;
|
||||
ECKey ecKey;
|
||||
if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() != EncryptionType.UNENCRYPTED) {
|
||||
// If the key is encrypted construct an ECKey using the encrypted private key bytes.
|
||||
ecKey = new ECKey(encryptedPrivateKey, pubKey, keyCrypter);
|
||||
|
@ -455,6 +455,13 @@ public class WalletProtobufSerializer {
|
|||
protected WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) {
|
||||
Transaction tx = txMap.get(txProto.getHash());
|
||||
WalletTransaction.Pool pool = WalletTransaction.Pool.valueOf(txProto.getPool().getNumber());
|
||||
if (pool == WalletTransaction.Pool.INACTIVE || pool == WalletTransaction.Pool.PENDING_INACTIVE) {
|
||||
// Upgrade old wallets: inactive pool has been merged with the pending pool.
|
||||
// Remove this some time after 0.9 is old and everyone has upgraded.
|
||||
// There should not be any spent outputs in this tx as old wallets would not allow them to be spent
|
||||
// in this state.
|
||||
pool = WalletTransaction.Pool.PENDING;
|
||||
}
|
||||
for (int i = 0 ; i < tx.getOutputs().size() ; i++) {
|
||||
TransactionOutput output = tx.getOutputs().get(i);
|
||||
final Protos.TransactionOutput transactionOutput = txProto.getTransactionOutput(i);
|
||||
|
|
|
@ -94,6 +94,21 @@ public class ChainSplitTest {
|
|||
assertFalse(reorgHappened[0]); // No re-org took place.
|
||||
assertEquals(2, walletChanged[0]);
|
||||
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
// Check we can handle multi-way splits: this is almost certainly going to be extremely rare, but we have to
|
||||
// handle it anyway. The same transaction appears in b7/b8 (side chain) but not b2 or b3.
|
||||
// genesis -> b1--> b2
|
||||
// |-> b3
|
||||
// |-> b7 (x)
|
||||
// \-> b8 (x)
|
||||
Block b7 = b1.createNextBlock(coinsTo);
|
||||
assertTrue(chain.add(b7));
|
||||
Block b8 = b1.createNextBlock(coinsTo);
|
||||
b8.addTransaction(b7.getTransactions().get(1));
|
||||
b8.solve();
|
||||
assertTrue(chain.add(b8));
|
||||
assertFalse(reorgHappened[0]); // No re-org took place.
|
||||
assertEquals(2, walletChanged[0]);
|
||||
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
// Now we add another block to make the alternative chain longer.
|
||||
assertTrue(chain.add(b3.createNextBlock(someOtherGuy)));
|
||||
assertTrue(reorgHappened[0]); // Re-org took place.
|
||||
|
@ -102,8 +117,8 @@ public class ChainSplitTest {
|
|||
//
|
||||
// genesis -> b1 -> b2
|
||||
// \-> b3 -> b4
|
||||
//
|
||||
// We lost some coins! b2 is no longer a part of the best chain so our available balance should drop to 50.
|
||||
// It's now pending reconfirmation.
|
||||
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
// ... and back to the first chain.
|
||||
Block b5 = b2.createNextBlock(coinsTo);
|
||||
|
@ -191,12 +206,15 @@ public class ChainSplitTest {
|
|||
b3.addTransaction(spend);
|
||||
b3.solve();
|
||||
chain.add(b3);
|
||||
// The external spend is not active yet.
|
||||
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
|
||||
// The external spend is now pending.
|
||||
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
|
||||
Transaction tx = wallet.getTransaction(spend.getHash());
|
||||
assertEquals(ConfidenceType.PENDING, tx.getConfidence().getConfidenceType());
|
||||
Block b4 = b3.createNextBlock(someOtherGuy);
|
||||
chain.add(b4);
|
||||
// The external spend is now active.
|
||||
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
|
||||
assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -280,7 +298,7 @@ public class ChainSplitTest {
|
|||
|
||||
@Test
|
||||
public void testDoubleSpendOnForkPending() throws Exception {
|
||||
// Check what happens when a re-org happens and one of our UNconfirmed transactions becomes invalidated by a
|
||||
// Check what happens when a re-org happens and one of our unconfirmed transactions becomes invalidated by a
|
||||
// double spend on the new best chain.
|
||||
final Transaction[] eventDead = new Transaction[1];
|
||||
final Transaction[] eventReplacement = new Transaction[1];
|
||||
|
@ -321,6 +339,8 @@ public class ChainSplitTest {
|
|||
chain.add(b4); // New best chain.
|
||||
|
||||
// Should have seen a double spend against the pending pool.
|
||||
// genesis -> b1 -> b2 [t1 dead and exited the miners mempools]
|
||||
// \-> b3 (t2) -> b4
|
||||
assertEquals(t1, eventDead[0]);
|
||||
assertEquals(t2, eventReplacement[0]);
|
||||
assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance());
|
||||
|
@ -330,10 +350,13 @@ public class ChainSplitTest {
|
|||
chain.add(b5);
|
||||
Block b6 = b5.createNextBlock(new ECKey().toAddress(unitTestParams));
|
||||
chain.add(b6);
|
||||
// genesis -> b1 -> b2 -> b5 -> b6 [t1 pending]
|
||||
// \-> b3 [t2 inactive] -> b4
|
||||
// genesis -> b1 -> b2 -> b5 -> b6 [t1 still dead]
|
||||
// \-> b3 [t2 resurrected and now pending] -> b4
|
||||
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
|
||||
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
// t2 is pending - resurrected double spends take precedence over our dead transactions (which are in nobodies
|
||||
// mempool by this point).
|
||||
assertEquals(ConfidenceType.DEAD, t1.getConfidence().getConfidenceType());
|
||||
assertEquals(ConfidenceType.PENDING, t2.getConfidence().getConfidenceType());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -485,8 +508,9 @@ public class ChainSplitTest {
|
|||
|
||||
@Test
|
||||
public void coinbaseDeath() throws Exception {
|
||||
// Check that a coinbase tx is marked as dead after a reorg rather than inactive as normal non-double-spent transactions would be.
|
||||
// Also check that a dead coinbase on a sidechain is resurrected if the sidechain becomes the best chain once more.
|
||||
// Check that a coinbase tx is marked as dead after a reorg rather than pending as normal non-double-spent
|
||||
// transactions would be. Also check that a dead coinbase on a sidechain is resurrected if the sidechain
|
||||
// becomes the best chain once more.
|
||||
final ArrayList<Transaction> txns = new ArrayList<Transaction>(3);
|
||||
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||
@Override
|
||||
|
@ -522,7 +546,6 @@ public class ChainSplitTest {
|
|||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(wallet.unspent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.dead.containsKey(txns.get(1).getHash()));
|
||||
|
||||
// Fork like this:
|
||||
|
@ -547,7 +570,6 @@ public class ChainSplitTest {
|
|||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.unspent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(wallet.dead.containsKey(txns.get(1).getHash()));
|
||||
|
||||
// ... and back to the first chain.
|
||||
|
@ -569,7 +591,6 @@ public class ChainSplitTest {
|
|||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(wallet.unspent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.dead.containsKey(txns.get(1).getHash()));
|
||||
|
||||
// ... make the side chain dominant again.
|
||||
|
@ -590,7 +611,6 @@ public class ChainSplitTest {
|
|||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.unspent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash()));
|
||||
assertTrue(wallet.dead.containsKey(txns.get(1).getHash()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
|
|||
inbound(p1, tx3);
|
||||
inbound(p1, new Pong(((Ping)ping).getNonce()));
|
||||
|
||||
Set<Transaction> transactions = wallet.getTransactions(false, false);
|
||||
Set<Transaction> transactions = wallet.getTransactions(false);
|
||||
assertTrue(transactions.size() == 4);
|
||||
for (Transaction tx : transactions) {
|
||||
assertTrue(tx.getConfidence().getConfidenceType() == ConfidenceType.BUILDING);
|
||||
|
|
|
@ -274,7 +274,8 @@ public class WalletTest extends TestWithWallet {
|
|||
|
||||
@Test
|
||||
public void sideChain() throws Exception {
|
||||
// The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance.
|
||||
// The wallet receives a coin on the main chain, then on a side chain. Balance is equal to both added together
|
||||
// as we assume the side chain tx is pending and will be included shortly.
|
||||
BigInteger v1 = Utils.toNanoCoins(1, 0);
|
||||
sendMoneyToWallet(v1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
|
@ -283,10 +284,9 @@ public class WalletTest extends TestWithWallet {
|
|||
|
||||
BigInteger v2 = toNanoCoins(0, 50);
|
||||
sendMoneyToWallet(v2, AbstractBlockChain.NewBlockType.SIDE_CHAIN);
|
||||
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.INACTIVE));
|
||||
assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL));
|
||||
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
assertEquals(v1.add(v2), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -557,7 +557,7 @@ public class WalletTest extends TestWithWallet {
|
|||
// Receive 1 BTC.
|
||||
BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
sendMoneyToWallet(nanos, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
Transaction received = wallet.getTransactions(false, false).iterator().next();
|
||||
Transaction received = wallet.getTransactions(false).iterator().next();
|
||||
// Create a send to a merchant.
|
||||
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
|
||||
// Create a double spend.
|
||||
|
|
|
@ -45,7 +45,7 @@ public class WalletProtobufSerializerTest {
|
|||
public void empty() throws Exception {
|
||||
// Check the base case of a wallet with one key and no transactions.
|
||||
Wallet wallet1 = roundTrip(myWallet);
|
||||
assertEquals(0, wallet1.getTransactions(true, true).size());
|
||||
assertEquals(0, wallet1.getTransactions(true).size());
|
||||
assertEquals(BigInteger.ZERO, wallet1.getBalance());
|
||||
assertArrayEquals(myKey.getPubKey(),
|
||||
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPubKey());
|
||||
|
@ -66,7 +66,7 @@ public class WalletProtobufSerializerTest {
|
|||
t1.getConfidence().setSource(TransactionConfidence.Source.NETWORK);
|
||||
myWallet.receivePending(t1, new ArrayList<Transaction>());
|
||||
Wallet wallet1 = roundTrip(myWallet);
|
||||
assertEquals(1, wallet1.getTransactions(true, true).size());
|
||||
assertEquals(1, wallet1.getTransactions(true).size());
|
||||
assertEquals(v1, wallet1.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
Transaction t1copy = wallet1.getTransaction(t1.getHash());
|
||||
assertArrayEquals(t1.bitcoinSerialize(), t1copy.bitcoinSerialize());
|
||||
|
@ -100,7 +100,7 @@ public class WalletProtobufSerializerTest {
|
|||
// t2 rolls back t1 and spends somewhere else.
|
||||
myWallet.receiveFromBlock(doubleSpends.t2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
Wallet wallet1 = roundTrip(myWallet);
|
||||
assertEquals(1, wallet1.getTransactions(true, true).size());
|
||||
assertEquals(1, wallet1.getTransactions(true).size());
|
||||
Transaction t1 = wallet1.getTransaction(doubleSpends.t1.getHash());
|
||||
assertEquals(ConfidenceType.DEAD, t1.getConfidence().getConfidenceType());
|
||||
assertEquals(BigInteger.ZERO, wallet1.getBalance());
|
||||
|
@ -197,7 +197,7 @@ public class WalletProtobufSerializerTest {
|
|||
// Roundtrip the wallet and check it has stored the depth and workDone.
|
||||
Wallet rebornWallet = roundTrip(myWallet);
|
||||
|
||||
Set<Transaction> rebornTxns = rebornWallet.getTransactions(false, false);
|
||||
Set<Transaction> rebornTxns = rebornWallet.getTransactions(false);
|
||||
assertEquals(2, rebornTxns.size());
|
||||
|
||||
// The transactions are not guaranteed to be in the same order so sort them to be in chain height order if required.
|
||||
|
@ -239,7 +239,7 @@ public class WalletProtobufSerializerTest {
|
|||
@Test
|
||||
public void testSerializedExtensionNormalWallet() throws Exception {
|
||||
Wallet wallet1 = roundTrip(myWallet);
|
||||
assertEquals(0, wallet1.getTransactions(true, true).size());
|
||||
assertEquals(0, wallet1.getTransactions(true).size());
|
||||
assertEquals(BigInteger.ZERO, wallet1.getBalance());
|
||||
assertArrayEquals(myKey.getPubKey(),
|
||||
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPubKey());
|
||||
|
|
|
@ -562,7 +562,7 @@ public class WalletTool {
|
|||
private static void setup() throws BlockStoreException {
|
||||
if (store != null) return; // Already done.
|
||||
// Will create a fresh chain if one doesn't exist or there is an issue with this one.
|
||||
if (!chainFileName.exists() && wallet.getTransactions(true, true).size() > 0) {
|
||||
if (!chainFileName.exists() && wallet.getTransactions(true).size() > 0) {
|
||||
// No chain, so reset the wallet as we will be downloading from scratch.
|
||||
System.out.println("Chain file is missing so clearing transactions from the wallet.");
|
||||
reset();
|
||||
|
@ -599,7 +599,7 @@ public class WalletTool {
|
|||
private static void syncChain() {
|
||||
try {
|
||||
setup();
|
||||
int startTransactions = wallet.getTransactions(true, true).size();
|
||||
int startTransactions = wallet.getTransactions(true).size();
|
||||
DownloadListener listener = new DownloadListener();
|
||||
peers.startAndWait();
|
||||
peers.startBlockChainDownload(listener);
|
||||
|
@ -609,7 +609,7 @@ public class WalletTool {
|
|||
System.err.println("Chain download interrupted, quitting ...");
|
||||
System.exit(1);
|
||||
}
|
||||
int endTransactions = wallet.getTransactions(true, true).size();
|
||||
int endTransactions = wallet.getTransactions(true).size();
|
||||
if (endTransactions > startTransactions) {
|
||||
System.out.println("Synced " + (endTransactions - startTransactions) + " transactions.");
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue