3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-15 11:45:51 +00:00

Support double spend forwarding

This adds a new IN_CONFLICT transaction confidence type, meaning there is another transaction
(or several other transactions) spending one (or several) of its inputs but nor this transaction
nor the other/s transaction/s are included in the best chain.
This commit is contained in:
Oscar Guindzberg 2015-03-11 19:57:58 -03:00 committed by Andreas Schildbach
parent 61377297ae
commit 293591bf24
6 changed files with 609 additions and 44 deletions

View File

@ -96,6 +96,15 @@ public class TransactionConfidence {
*/ */
DEAD(4), DEAD(4),
/**
* If IN_CONFLICT, then it means there is another transaction (or several other transactions) spending one
* (or several) of its inputs but nor this transaction nor the other/s transaction/s are included in the best chain.
* The other/s transaction/s should be IN_CONFLICT too.
* IN_CONFLICT can be thought as an intermediary state between a) PENDING and BUILDING or b) PENDING and DEAD.
* Another common name for this situation is "double spend".
*/
IN_CONFLICT(5),
/** /**
* If a transaction hasn't been broadcast yet, or there's no record of it, its confidence is UNKNOWN. * If a transaction hasn't been broadcast yet, or there's no record of it, its confidence is UNKNOWN.
*/ */
@ -260,7 +269,7 @@ public class TransactionConfidence {
if (confidenceType != ConfidenceType.DEAD) { if (confidenceType != ConfidenceType.DEAD) {
overridingTransaction = null; overridingTransaction = null;
} }
if (confidenceType == ConfidenceType.PENDING) { if (confidenceType == ConfidenceType.PENDING || confidenceType == ConfidenceType.IN_CONFLICT) {
depth = 0; depth = 0;
appearedAtChainHeight = -1; appearedAtChainHeight = -1;
} }
@ -323,6 +332,9 @@ public class TransactionConfidence {
case PENDING: case PENDING:
builder.append("Pending/unconfirmed."); builder.append("Pending/unconfirmed.");
break; break;
case IN_CONFLICT:
builder.append("In conflict.");
break;
case BUILDING: case BUILDING:
builder.append(String.format("Appeared in best chain at height %d, depth %d.", builder.append(String.format("Appeared in best chain at height %d, depth %d.",
getAppearedAtChainHeight(), getDepthInBlocks())); getAppearedAtChainHeight(), getDepthInBlocks()));
@ -377,12 +389,12 @@ public class TransactionConfidence {
* store this information. * store this information.
* *
* @return the transaction that double spent this one * @return the transaction that double spent this one
* @throws IllegalStateException if confidence type is not OVERRIDDEN_BY_DOUBLE_SPEND. * @throws IllegalStateException if confidence type is not DEAD.
*/ */
public synchronized Transaction getOverridingTransaction() { public synchronized Transaction getOverridingTransaction() {
if (getConfidenceType() != ConfidenceType.DEAD) if (getConfidenceType() != ConfidenceType.DEAD)
throw new IllegalStateException("Confidence type is " + getConfidenceType() + throw new IllegalStateException("Confidence type is " + getConfidenceType() +
", not OVERRIDDEN_BY_DOUBLE_SPEND"); ", not DEAD");
return overridingTransaction; return overridingTransaction;
} }

View File

@ -105,9 +105,9 @@ public class Wallet extends BaseTaggableObject
// The various pools below give quick access to wallet-relevant transactions by the state they're in: // The various pools below give quick access to wallet-relevant transactions by the state they're in:
// //
// Pending: Transactions that didn't make it into the best chain yet. Pending transactions can be killed if a // 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. // 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 // If a double spend appears in the pending state as well, we update the confidence type
// and wait for the miners to resolve the race. // of all txns in conflict to IN_CONFLICT 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 // 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 // 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. // reason being that this simplifies handling of re-orgs. It would be worth fixing this in future.
@ -1692,6 +1692,7 @@ public class Wallet extends BaseTaggableObject
// We only care about transactions that: // We only care about transactions that:
// - Send us coins // - Send us coins
// - Spend our coins // - Spend our coins
// - Double spend a tx in our wallet
if (!isTransactionRelevant(tx)) { if (!isTransactionRelevant(tx)) {
log.debug("Received tx that isn't relevant to this wallet, discarding."); log.debug("Received tx that isn't relevant to this wallet, discarding.");
return false; return false;
@ -1715,41 +1716,64 @@ public class Wallet extends BaseTaggableObject
try { try {
return tx.getValueSentFromMe(this).signum() > 0 || return tx.getValueSentFromMe(this).signum() > 0 ||
tx.getValueSentToMe(this).signum() > 0 || tx.getValueSentToMe(this).signum() > 0 ||
checkForDoubleSpendAgainstPending(tx, false); !findDoubleSpendsAgainst(tx, transactions).isEmpty();
} finally { } finally {
lock.unlock(); lock.unlock();
} }
} }
/** /**
* Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if * Finds transactions in the specified candidates that double spend "tx". Not a general check, but it can work even if
* the double spent inputs are not ours. * the double spent inputs are not ours.
* @return The set of transactions that double spend "tx".
*/ */
private boolean checkForDoubleSpendAgainstPending(Transaction tx, boolean takeAction) { private Set<Transaction> findDoubleSpendsAgainst(Transaction tx, Map<Sha256Hash, Transaction> candidates) {
checkState(lock.isHeldByCurrentThread()); checkState(lock.isHeldByCurrentThread());
if (tx.isCoinBase()) return Sets.newHashSet();
// Compile a set of outpoints that are spent by tx. // Compile a set of outpoints that are spent by tx.
HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>(); HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>();
for (TransactionInput input : tx.getInputs()) { for (TransactionInput input : tx.getInputs()) {
outpoints.add(input.getOutpoint()); outpoints.add(input.getOutpoint());
} }
// Now for each pending transaction, see if it shares any outpoints with this tx. // Now for each pending transaction, see if it shares any outpoints with this tx.
LinkedList<Transaction> doubleSpentTxns = Lists.newLinkedList(); Set<Transaction> doubleSpendTxns = Sets.newHashSet();
for (Transaction p : pending.values()) { for (Transaction p : candidates.values()) {
for (TransactionInput input : p.getInputs()) { for (TransactionInput input : p.getInputs()) {
// This relies on the fact that TransactionOutPoint equality is defined at the protocol not object // This relies on the fact that TransactionOutPoint equality is defined at the protocol not object
// level - outpoints from two different inputs that point to the same output compare the same. // level - outpoints from two different inputs that point to the same output compare the same.
TransactionOutPoint outpoint = input.getOutpoint(); TransactionOutPoint outpoint = input.getOutpoint();
if (outpoints.contains(outpoint)) { if (outpoints.contains(outpoint)) {
// It does, it's a double spend against the pending pool, which makes it relevant. // It does, it's a double spend against the candidates, which makes it relevant.
if (!doubleSpentTxns.isEmpty() && doubleSpentTxns.getLast() == p) continue; doubleSpendTxns.add(p);
doubleSpentTxns.add(p);
} }
} }
} }
if (takeAction && !doubleSpentTxns.isEmpty()) { return doubleSpendTxns;
killTx(tx, doubleSpentTxns); }
/**
* Adds to txSet all the txns in txPool spending outputs of txns in txSet,
* and all txns spending the outputs of those txns, recursively.
*/
void addTransactionsDependingOn(Set<Transaction> txSet, Set<Transaction> txPool) {
Map<Sha256Hash, Transaction> txQueue = new LinkedHashMap<Sha256Hash, Transaction>();
for (Transaction tx : txSet) {
txQueue.put(tx.getHash(), tx);
}
while(!txQueue.isEmpty()) {
Transaction tx = txQueue.remove(txQueue.keySet().iterator().next());
for (Transaction anotherTx : txPool) {
if (anotherTx.equals(tx)) continue;
for (TransactionInput input : anotherTx.getInputs()) {
if (input.getOutpoint().getHash().equals(tx.getHash())) {
if (txQueue.get(anotherTx.getHash()) == null) {
txQueue.put(anotherTx.getHash(), anotherTx);
txSet.add(anotherTx);
}
}
}
}
} }
return !doubleSpentTxns.isEmpty();
} }
/** /**
@ -1824,6 +1848,9 @@ public class Wallet extends BaseTaggableObject
log.info(" <-pending"); log.info(" <-pending");
if (bestChain) { if (bestChain) {
boolean wasDead = dead.remove(txHash) != null;
if (wasDead)
log.info(" <-dead");
if (wasPending) { if (wasPending) {
// Was pending and is now confirmed. Disconnect the outputs in case we spent any already: they will be // Was pending and is now confirmed. Disconnect the outputs in case we spent any already: they will be
// re-connected by processTxFromBestChain below. // re-connected by processTxFromBestChain below.
@ -1835,7 +1862,7 @@ public class Wallet extends BaseTaggableObject
} }
} }
} }
processTxFromBestChain(tx, wasPending); processTxFromBestChain(tx, wasPending || wasDead);
} else { } else {
checkState(sideChain); checkState(sideChain);
// Transactions that appear in a side chain will have that appearance recorded below - we assume that // Transactions that appear in a side chain will have that appearance recorded below - we assume that
@ -1849,7 +1876,7 @@ public class Wallet extends BaseTaggableObject
// Ignore the case where a tx appears on a side chain at the same time as the best chain (this is // 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). // quite normal and expected).
Sha256Hash hash = tx.getHash(); Sha256Hash hash = tx.getHash();
if (!unspent.containsKey(hash) && !spent.containsKey(hash)) { if (!unspent.containsKey(hash) && !spent.containsKey(hash) && !dead.containsKey(hash)) {
// Otherwise put it (possibly back) into pending. // Otherwise put it (possibly back) into pending.
// Committing it updates the spent flags and inserts into the pool as well. // Committing it updates the spent flags and inserts into the pool as well.
commitTx(tx); commitTx(tx);
@ -1866,6 +1893,22 @@ public class Wallet extends BaseTaggableObject
// this method has been called by BlockChain for all relevant transactions. Otherwise we'd double // this method has been called by BlockChain for all relevant transactions. Otherwise we'd double
// count. // count.
ignoreNextNewBlock.add(txHash); ignoreNextNewBlock.add(txHash);
// When a tx is received from the best chain, if other txns that spend this tx are IN_CONFLICT,
// change its confidence to PENDING (Unless they are also spending other txns IN_CONFLICT).
// Consider dependency chains.
Set<Transaction> currentTxDependencies = Sets.newHashSet(tx);
addTransactionsDependingOn(currentTxDependencies, getTransactions(true));
currentTxDependencies.remove(tx);
List<Transaction> currentTxDependenciesSorted = sortTxnsByDependency(currentTxDependencies);
for (Transaction txDependency : currentTxDependenciesSorted) {
if (txDependency.getConfidence().getConfidenceType().equals(ConfidenceType.IN_CONFLICT)) {
if (isNotSpendingTxnsInConfidenceType(txDependency, ConfidenceType.IN_CONFLICT)) {
txDependency.getConfidence().setConfidenceType(ConfidenceType.PENDING);
confidenceChanged.put(txDependency, TransactionConfidence.Listener.ChangeReason.TYPE);
}
}
}
} }
} }
@ -1909,6 +1952,53 @@ public class Wallet extends BaseTaggableObject
hardSaveOnNextBlock = true; hardSaveOnNextBlock = true;
} }
/** Finds if tx is NOT spending other txns which are in the specified confidence type */
private boolean isNotSpendingTxnsInConfidenceType(Transaction tx, ConfidenceType confidenceType) {
for (TransactionInput txInput : tx.getInputs()) {
Transaction connectedTx = this.getTransaction(txInput.getOutpoint().getHash());
if (connectedTx != null && connectedTx.getConfidence().getConfidenceType().equals(confidenceType)) {
return false;
}
}
return true;
}
/**
* Creates and returns a new List with the same txns as inputSet
* but txns are sorted by depencency (a topological sort).
* If tx B spends tx A, then tx A should be before tx B on the returned List.
* Several invocations to this method with the same inputSet could result in lists with txns in different order,
* as there is no guarantee on the order of the returned txns besides what was already stated.
*/
List<Transaction> sortTxnsByDependency(Set<Transaction> inputSet) {
ArrayList<Transaction> result = new ArrayList<Transaction>(inputSet);
for (int i = 0; i < result.size()-1; i++) {
boolean txAtISpendsOtherTxInTheList;
do {
txAtISpendsOtherTxInTheList = false;
for (int j = i+1; j < result.size(); j++) {
if (spends(result.get(i), result.get(j))) {
Transaction transactionAtI = result.remove(i);
result.add(j, transactionAtI);
txAtISpendsOtherTxInTheList = true;
break;
}
}
} while (txAtISpendsOtherTxInTheList);
}
return result;
}
/** Finds whether txA spends txB */
boolean spends(Transaction txA, Transaction txB) {
for (TransactionInput txInput : txA.getInputs()) {
if (txInput.getOutpoint().getHash().equals(txB.getHash())) {
return true;
}
}
return false;
}
private void informConfidenceListenersIfNotReorganizing() { private void informConfidenceListenersIfNotReorganizing() {
if (insideReorg) if (insideReorg)
return; return;
@ -2032,7 +2122,12 @@ public class Wallet extends BaseTaggableObject
addWalletTransaction(Pool.SPENT, tx); addWalletTransaction(Pool.SPENT, tx);
} }
checkForDoubleSpendAgainstPending(tx, true); // Kill txns in conflict with this tx
Set<Transaction> doubleSpendTxns = findDoubleSpendsAgainst(tx, pending);
if (!doubleSpendTxns.isEmpty()) {
// no need to addTransactionsDependingOn(doubleSpendTxns) because killTxns() already kills dependencies;
killTxns(doubleSpendTxns, tx);
}
} }
/** /**
@ -2078,7 +2173,7 @@ public class Wallet extends BaseTaggableObject
// Can be: // Can be:
// (1) We already marked this output as spent when we saw the pending transaction (most likely). // (1) We already marked this output as spent when we saw the pending transaction (most likely).
// Now it's being confirmed of course, we cannot mark it as spent again. // Now it's being confirmed of course, we cannot mark it as spent again.
// (2) A double spend from chain: this will be handled later by checkForDoubleSpendAgainstPending. // (2) A double spend from chain: this will be handled later by findDoubleSpendsAgainst()/killTxns().
// //
// In any case, nothing to do here. // In any case, nothing to do here.
} else { } else {
@ -2137,8 +2232,8 @@ public class Wallet extends BaseTaggableObject
} }
// Updates the wallet when a double spend occurs. overridingTx can be null for the case of coinbases // Updates the wallet when a double spend occurs. overridingTx can be null for the case of coinbases
private void killTx(@Nullable Transaction overridingTx, List<Transaction> killedTx) { private void killTxns(Set<Transaction> txnsToKill, @Nullable Transaction overridingTx) {
LinkedList<Transaction> work = new LinkedList<Transaction>(killedTx); LinkedList<Transaction> work = new LinkedList<Transaction>(txnsToKill);
while (!work.isEmpty()) { while (!work.isEmpty()) {
final Transaction tx = work.poll(); final Transaction tx = work.poll();
log.warn("TX {} killed{}", tx.getHashAsString(), log.warn("TX {} killed{}", tx.getHashAsString(),
@ -2152,7 +2247,7 @@ public class Wallet extends BaseTaggableObject
for (TransactionInput deadInput : tx.getInputs()) { for (TransactionInput deadInput : tx.getInputs()) {
Transaction connected = deadInput.getOutpoint().fromTx; Transaction connected = deadInput.getOutpoint().fromTx;
if (connected == null) continue; if (connected == null) continue;
if (connected.getConfidence().getConfidenceType() != ConfidenceType.DEAD) { if (connected.getConfidence().getConfidenceType() != ConfidenceType.DEAD && deadInput.getConnectedOutput().getSpentBy() != null && deadInput.getConnectedOutput().getSpentBy().equals(deadInput)) {
checkState(myUnspents.add(deadInput.getConnectedOutput())); checkState(myUnspents.add(deadInput.getConnectedOutput()));
log.info("Added to UNSPENTS: {} in {}", deadInput.getConnectedOutput(), deadInput.getConnectedOutput().getParentTransaction().getHash()); log.info("Added to UNSPENTS: {} in {}", deadInput.getConnectedOutput(), deadInput.getConnectedOutput().getParentTransaction().getHash());
} }
@ -2240,12 +2335,42 @@ public class Wallet extends BaseTaggableObject
// move any transactions that are now fully spent to the spent map so we can skip them when creating future // move any transactions that are now fully spent to the spent map so we can skip them when creating future
// spends. // spends.
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.
// This also registers txConfidenceListener so wallet listeners get informed. Set<Transaction> doubleSpendPendingTxns = findDoubleSpendsAgainst(tx, pending);
log.info("->pending: {}", tx.getHashAsString()); Set<Transaction> doubleSpendUnspentTxns = findDoubleSpendsAgainst(tx, unspent);
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); Set<Transaction> doubleSpendSpentTxns = findDoubleSpendsAgainst(tx, spent);
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
addWalletTransaction(Pool.PENDING, tx); if (!doubleSpendUnspentTxns.isEmpty() ||
!doubleSpendSpentTxns.isEmpty() ||
!isNotSpendingTxnsInConfidenceType(tx, ConfidenceType.DEAD)) {
// tx is a double spend against a tx already in the best chain or spends outputs of a DEAD tx.
// Add tx to the dead pool and schedule confidence listener notifications.
log.info("->dead: {}", tx.getHashAsString());
tx.getConfidence().setConfidenceType(ConfidenceType.DEAD);
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
addWalletTransaction(Pool.DEAD, tx);
} else if (!doubleSpendPendingTxns.isEmpty() ||
!isNotSpendingTxnsInConfidenceType(tx, ConfidenceType.IN_CONFLICT)) {
// tx is a double spend against a pending tx or spends outputs of a tx already IN_CONFLICT.
// Add tx to the pending pool. Update the confidence type of tx, the txns in conflict with tx and all
// their dependencies to IN_CONFLICT and schedule confidence listener notifications.
log.info("->pending (IN_CONFLICT): {}", tx.getHashAsString());
addWalletTransaction(Pool.PENDING, tx);
doubleSpendPendingTxns.add(tx);
addTransactionsDependingOn(doubleSpendPendingTxns, getTransactions(true));
for (Transaction doubleSpendTx : doubleSpendPendingTxns) {
doubleSpendTx.getConfidence().setConfidenceType(ConfidenceType.IN_CONFLICT);
confidenceChanged.put(doubleSpendTx, TransactionConfidence.Listener.ChangeReason.TYPE);
}
} else {
// No conflict detected.
// Add to the pending pool and schedule confidence listener notifications.
log.info("->pending: {}", tx.getHashAsString());
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING);
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
addWalletTransaction(Pool.PENDING, tx);
}
// Mark any keys used in the outputs as "used", this allows wallet UI's to auto-advance the current key // Mark any keys used in the outputs as "used", this allows wallet UI's to auto-advance the current key
// they are showing to the user in qr codes etc. // they are showing to the user in qr codes etc.
markKeysAsUsed(tx); markKeysAsUsed(tx);
@ -2494,10 +2619,10 @@ public class Wallet extends BaseTaggableObject
} }
} }
private static void addWalletTransactionsToSet(Set<WalletTransaction> txs, private static void addWalletTransactionsToSet(Set<WalletTransaction> txns,
Pool poolType, Collection<Transaction> pool) { Pool poolType, Collection<Transaction> pool) {
for (Transaction tx : pool) { for (Transaction tx : pool) {
txs.add(new WalletTransaction(poolType, tx)); txns.add(new WalletTransaction(poolType, tx));
} }
} }
@ -4274,7 +4399,7 @@ public class Wallet extends BaseTaggableObject
// this coinbase tx. Some can just go pending forever, like the Satoshi client. However we // this coinbase tx. Some can just go pending forever, like the Satoshi client. However we
// can do our best. // can do our best.
log.warn("Coinbase killed by re-org: {}", tx.getHashAsString()); log.warn("Coinbase killed by re-org: {}", tx.getHashAsString());
killTx(null, ImmutableList.of(tx)); killTxns(ImmutableSet.of(tx), null);
} else { } else {
for (TransactionOutput output : tx.getOutputs()) { for (TransactionOutput output : tx.getOutputs()) {
TransactionInput input = output.getSpentBy(); TransactionInput input = output.getSpentBy();
@ -4299,6 +4424,7 @@ public class Wallet extends BaseTaggableObject
// there's another re-org. // there's another re-org.
if (tx.isCoinBase()) continue; if (tx.isCoinBase()) continue;
log.info(" ->pending {}", tx.getHash()); log.info(" ->pending {}", tx.getHash());
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); // Wipe height/depth/work data. tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); // Wipe height/depth/work data.
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE); confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
addWalletTransaction(Pool.PENDING, tx); addWalletTransaction(Pool.PENDING, tx);

View File

@ -733,6 +733,7 @@ public class WalletProtobufSerializer {
// These two are equivalent (must be able to read old wallets). // These two are equivalent (must be able to read old wallets).
case NOT_IN_BEST_CHAIN: confidenceType = ConfidenceType.PENDING; break; case NOT_IN_BEST_CHAIN: confidenceType = ConfidenceType.PENDING; break;
case PENDING: confidenceType = ConfidenceType.PENDING; break; case PENDING: confidenceType = ConfidenceType.PENDING; break;
case IN_CONFLICT: confidenceType = ConfidenceType.IN_CONFLICT; break;
case UNKNOWN: case UNKNOWN:
// Fall through. // Fall through.
default: default:

View File

@ -175,13 +175,13 @@ public class FakeTxBuilder {
Coin value = COIN; Coin value = COIN;
Address someBadGuy = new ECKey().toAddress(params); Address someBadGuy = new ECKey().toAddress(params);
doubleSpends.t1 = new Transaction(params);
TransactionOutput o1 = new TransactionOutput(params, doubleSpends.t1, value, to);
doubleSpends.t1.addOutput(o1);
doubleSpends.prevTx = new Transaction(params); doubleSpends.prevTx = new Transaction(params);
TransactionOutput prevOut = new TransactionOutput(params, doubleSpends.prevTx, value, someBadGuy); TransactionOutput prevOut = new TransactionOutput(params, doubleSpends.prevTx, value, someBadGuy);
doubleSpends.prevTx.addOutput(prevOut); doubleSpends.prevTx.addOutput(prevOut);
doubleSpends.t1 = new Transaction(params);
TransactionOutput o1 = new TransactionOutput(params, doubleSpends.t1, value, to);
doubleSpends.t1.addOutput(o1);
doubleSpends.t1.addInput(prevOut); doubleSpends.t1.addInput(prevOut);
doubleSpends.t2 = new Transaction(params); doubleSpends.t2 = new Transaction(params);
@ -209,14 +209,14 @@ public class FakeTxBuilder {
return createFakeBlock(blockStore, version, timeSeconds, 0, transactions); return createFakeBlock(blockStore, version, timeSeconds, 0, transactions);
} }
/** Emulates receiving a valid block that builds on top of the chain. */ /** Emulates receiving a valid block */
public static BlockPair createFakeBlock(BlockStore blockStore, long version, public static BlockPair createFakeBlock(BlockStore blockStore, StoredBlock previousStoredBlock, long version,
long timeSeconds, int height, long timeSeconds, int height,
Transaction... transactions) { Transaction... transactions) {
try { try {
Block chainHead = blockStore.getChainHead().getHeader(); Block previousBlock = previousStoredBlock.getHeader();
Address to = new ECKey().toAddress(chainHead.getParams()); Address to = new ECKey().toAddress(previousBlock.getParams());
Block b = chainHead.createNextBlock(to, version, timeSeconds, height); Block b = previousBlock.createNextBlock(to, version, timeSeconds, height);
// Coinbase tx was already added. // Coinbase tx was already added.
for (Transaction tx : transactions) { for (Transaction tx : transactions) {
tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK); tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK);
@ -225,7 +225,7 @@ public class FakeTxBuilder {
b.solve(); b.solve();
BlockPair pair = new BlockPair(); BlockPair pair = new BlockPair();
pair.block = b; pair.block = b;
pair.storedBlock = blockStore.getChainHead().build(b); pair.storedBlock = previousStoredBlock.build(b);
blockStore.put(pair.storedBlock); blockStore.put(pair.storedBlock);
blockStore.setChainHead(pair.storedBlock); blockStore.setChainHead(pair.storedBlock);
return pair; return pair;
@ -236,6 +236,19 @@ public class FakeTxBuilder {
} }
} }
public static BlockPair createFakeBlock(BlockStore blockStore, StoredBlock previousStoredBlock, int height, Transaction... transactions) {
return createFakeBlock(blockStore, previousStoredBlock, Block.BLOCK_VERSION_BIP66, Utils.currentTimeSeconds(), height, transactions);
}
/** Emulates receiving a valid block that builds on top of the chain. */
public static BlockPair createFakeBlock(BlockStore blockStore, long version, long timeSeconds, int height, Transaction... transactions) {
try {
return createFakeBlock(blockStore, blockStore.getChainHead(), version, timeSeconds, height, transactions);
} catch (BlockStoreException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
/** Emulates receiving a valid block that builds on top of the chain. */ /** Emulates receiving a valid block that builds on top of the chain. */
public static BlockPair createFakeBlock(BlockStore blockStore, int height, public static BlockPair createFakeBlock(BlockStore blockStore, int height,
Transaction... transactions) { Transaction... transactions) {

View File

@ -6766,6 +6766,14 @@ public final class Protos {
* </pre> * </pre>
*/ */
DEAD(4, 4), DEAD(4, 4),
/**
* <code>IN_CONFLICT = 5;</code>
*
* <pre>
* There is another transaction spending one of this transaction inputs.
* </pre>
*/
IN_CONFLICT(5, 5),
; ;
/** /**
@ -6815,6 +6823,7 @@ public final class Protos {
case 2: return PENDING; case 2: return PENDING;
case 3: return NOT_IN_BEST_CHAIN; case 3: return NOT_IN_BEST_CHAIN;
case 4: return DEAD; case 4: return DEAD;
case 5: return IN_CONFLICT;
default: return null; default: return null;
} }
} }

View File

@ -19,6 +19,7 @@ package org.bitcoinj.core;
import org.bitcoinj.core.listeners.AbstractWalletEventListener; import org.bitcoinj.core.listeners.AbstractWalletEventListener;
import org.bitcoinj.core.listeners.WalletCoinEventListener; import org.bitcoinj.core.listeners.WalletCoinEventListener;
import org.bitcoinj.core.TransactionConfidence.ConfidenceType;
import org.bitcoinj.core.Wallet.SendRequest; import org.bitcoinj.core.Wallet.SendRequest;
import org.bitcoinj.crypto.*; import org.bitcoinj.crypto.*;
import org.bitcoinj.script.Script; import org.bitcoinj.script.Script;
@ -747,7 +748,7 @@ public class WalletTest extends TestWithWallet {
send2 = params.getDefaultSerializer().makeTransaction(send2.bitcoinSerialize()); send2 = params.getDefaultSerializer().makeTransaction(send2.bitcoinSerialize());
// Broadcast send1, it's now pending. // Broadcast send1, it's now pending.
wallet.commitTx(send1); wallet.commitTx(send1);
assertEquals(ZERO, wallet.getBalance()); assertEquals(ZERO, wallet.getBalance()); // change of 10 cents is not yet mined so not included in the balance.
// Receive a block that overrides the send1 using send2. // Receive a block that overrides the send1 using send2.
sendMoneyToWallet(send2, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(send2, AbstractBlockChain.NewBlockType.BEST_CHAIN);
// send1 got rolled back and replaced with a smaller send that only used one of our received coins, thus ... // send1 got rolled back and replaced with a smaller send that only used one of our received coins, thus ...
@ -863,6 +864,409 @@ public class WalletTest extends TestWithWallet {
assertEquals(5, eventWalletChanged[0]); assertEquals(5, eventWalletChanged[0]);
} }
@Test
public void doubleSpendWeCreate() throws Exception {
// Test we keep pending double spends in IN_CONFLICT until one of them is included in a block
// and we handle reorgs and dependency chains properly.
// The following graph shows the txns we use in this test and how they are related
// (Eg txA1 spends txARoot outputs, txC1 spends txA1 and txB1 outputs, etc).
// txARoot (10) -> txA1 (1) -+
// |--> txC1 (0.10) -> txD1 (0.01)
// txBRoot (100) -> txB1 (11) -+
//
// txARoot (10) -> txA2 (2) -+
// |--> txC2 (0.20) -> txD2 (0.02)
// txBRoot (100) -> txB2 (22) -+
//
// txARoot (10) -> txA3 (3)
//
// txA1 is in conflict with txA2 and txA3. txB1 is in conflict with txB2.
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
final Address address = new ECKey().toAddress(params);
Transaction txARoot = sendMoneyToWallet(valueOf(10, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
SendRequest a1Req = SendRequest.to(address, valueOf(1, 0));
a1Req.tx.addInput(txARoot.getOutput(0));
a1Req.shuffleOutputs = false;
wallet.completeTx(a1Req);
Transaction txA1 = a1Req.tx;
SendRequest a2Req = SendRequest.to(address, valueOf(2, 0));
a2Req.tx.addInput(txARoot.getOutput(0));
a2Req.shuffleOutputs = false;
wallet.completeTx(a2Req);
Transaction txA2 = a2Req.tx;
SendRequest a3Req = SendRequest.to(address, valueOf(3, 0));
a3Req.tx.addInput(txARoot.getOutput(0));
a3Req.shuffleOutputs = false;
wallet.completeTx(a3Req);
Transaction txA3 = a3Req.tx;
wallet.commitTx(txA1);
wallet.commitTx(txA2);
wallet.commitTx(txA3);
Transaction txBRoot = sendMoneyToWallet(valueOf(100, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
SendRequest b1Req = SendRequest.to(address, valueOf(11, 0));
b1Req.tx.addInput(txBRoot.getOutput(0));
b1Req.shuffleOutputs = false;
wallet.completeTx(b1Req);
Transaction txB1 = b1Req.tx;
SendRequest b2Req = SendRequest.to(address, valueOf(22, 0));
b2Req.tx.addInput(txBRoot.getOutput(0));
b2Req.shuffleOutputs = false;
wallet.completeTx(b2Req);
Transaction txB2 = b2Req.tx;
wallet.commitTx(txB1);
wallet.commitTx(txB2);
SendRequest c1Req = SendRequest.to(address, valueOf(0, 10));
c1Req.tx.addInput(txA1.getOutput(1));
c1Req.tx.addInput(txB1.getOutput(1));
c1Req.shuffleOutputs = false;
wallet.completeTx(c1Req);
Transaction txC1 = c1Req.tx;
SendRequest c2Req = SendRequest.to(address, valueOf(0, 20));
c2Req.tx.addInput(txA2.getOutput(1));
c2Req.tx.addInput(txB2.getOutput(1));
c2Req.shuffleOutputs = false;
wallet.completeTx(c2Req);
Transaction txC2 = c2Req.tx;
wallet.commitTx(txC1);
wallet.commitTx(txC2);
SendRequest d1Req = SendRequest.to(address, valueOf(0, 1));
d1Req.tx.addInput(txC1.getOutput(1));
d1Req.shuffleOutputs = false;
wallet.completeTx(d1Req);
Transaction txD1 = d1Req.tx;
SendRequest d2Req = SendRequest.to(address, valueOf(0, 2));
d2Req.tx.addInput(txC2.getOutput(1));
d2Req.shuffleOutputs = false;
wallet.completeTx(d2Req);
Transaction txD2 = d2Req.tx;
wallet.commitTx(txD1);
wallet.commitTx(txD2);
assertInConflict(txA1);
assertInConflict(txA2);
assertInConflict(txA3);
assertInConflict(txB1);
assertInConflict(txB2);
assertInConflict(txC1);
assertInConflict(txC2);
assertInConflict(txD1);
assertInConflict(txD2);
// Add a block to the block store. The rest of the blocks in this test will be on top of this one.
FakeTxBuilder.BlockPair blockPair0 = createFakeBlock(blockStore, 1);
// A block was mined including txA1
FakeTxBuilder.BlockPair blockPair1 = createFakeBlock(blockStore, 2, txA1);
wallet.receiveFromBlock(txA1, blockPair1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.notifyNewBestBlock(blockPair1.storedBlock);
assertSpent(txA1);
assertDead(txA2);
assertDead(txA3);
assertInConflict(txB1);
assertInConflict(txB2);
assertInConflict(txC1);
assertDead(txC2);
assertInConflict(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txA1 and txB1
FakeTxBuilder.BlockPair blockPair2 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA1, txB1);
wallet.receiveFromBlock(txA1, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.receiveFromBlock(txB1, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 1);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair1.storedBlock),
Lists.newArrayList(blockPair2.storedBlock));
assertSpent(txA1);
assertDead(txA2);
assertDead(txA3);
assertSpent(txB1);
assertDead(txB2);
assertPending(txC1);
assertDead(txC2);
assertPending(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txA1, txB1 and txC1
FakeTxBuilder.BlockPair blockPair3 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA1, txB1,
txC1);
wallet.receiveFromBlock(txA1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.receiveFromBlock(txB1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 1);
wallet.receiveFromBlock(txC1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 2);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair2.storedBlock),
Lists.newArrayList(blockPair3.storedBlock));
assertSpent(txA1);
assertDead(txA2);
assertDead(txA3);
assertSpent(txB1);
assertDead(txB2);
assertSpent(txC1);
assertDead(txC2);
assertPending(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txB1
FakeTxBuilder.BlockPair blockPair4 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txB1);
wallet.receiveFromBlock(txB1, blockPair4.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair3.storedBlock),
Lists.newArrayList(blockPair4.storedBlock));
assertPending(txA1);
assertDead(txA2);
assertDead(txA3);
assertSpent(txB1);
assertDead(txB2);
assertPending(txC1);
assertDead(txC2);
assertPending(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txA2
FakeTxBuilder.BlockPair blockPair5 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA2);
wallet.receiveFromBlock(txA2, blockPair5.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair4.storedBlock),
Lists.newArrayList(blockPair5.storedBlock));
assertDead(txA1);
assertUnspent(txA2);
assertDead(txA3);
assertPending(txB1);
assertDead(txB2);
assertDead(txC1);
assertDead(txC2);
assertDead(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new empty block
FakeTxBuilder.BlockPair blockPair6 = createFakeBlock(blockStore, blockPair0.storedBlock, 2);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair5.storedBlock),
Lists.newArrayList(blockPair6.storedBlock));
assertDead(txA1);
assertPending(txA2);
assertDead(txA3);
assertPending(txB1);
assertDead(txB2);
assertDead(txC1);
assertDead(txC2);
assertDead(txD1);
assertDead(txD2);
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void doubleSpendWeReceive() throws Exception {
FakeTxBuilder.DoubleSpends doubleSpends = FakeTxBuilder.createFakeDoubleSpendTxns(params, myAddress);
// doubleSpends.t1 spends to our wallet. doubleSpends.t2 double spends somewhere else.
final Address notMyaddress = new ECKey().toAddress(params);
Transaction t1b = new Transaction(params);
TransactionOutput t1bo = new TransactionOutput(params, t1b, valueOf(0, 50), notMyaddress);
t1b.addOutput(t1bo);
t1b.addInput(doubleSpends.t1.getOutput(0));
wallet.receivePending(doubleSpends.t1, null);
wallet.receivePending(doubleSpends.t2, null);
wallet.receivePending(t1b, null);
assertInConflict(doubleSpends.t1);
assertInConflict(doubleSpends.t1);
assertInConflict(t1b);
// Add a block to the block store. The rest of the blocks in this test will be on top of this one.
FakeTxBuilder.BlockPair blockPair0 = createFakeBlock(blockStore, 1);
// A block was mined including doubleSpends.t1
FakeTxBuilder.BlockPair blockPair1 = createFakeBlock(blockStore, 2, doubleSpends.t1);
wallet.receiveFromBlock(doubleSpends.t1, blockPair1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.notifyNewBestBlock(blockPair1.storedBlock);
assertSpent(doubleSpends.t1);
assertDead(doubleSpends.t2);
assertPending(t1b);
// A reorg: previous block "replaced" by new block containing doubleSpends.t2
FakeTxBuilder.BlockPair blockPair2 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, doubleSpends.t2);
wallet.receiveFromBlock(doubleSpends.t2, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair1.storedBlock),
Lists.newArrayList(blockPair2.storedBlock));
assertDead(doubleSpends.t1);
assertSpent(doubleSpends.t2);
assertDead(t1b);
}
@Test
public void doubleSpendForBuildingTx() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
final Address address = new ECKey().toAddress(params);
Transaction send1 = checkNotNull(wallet.createSend(address, valueOf(1, 0)));
Transaction send2 = checkNotNull(wallet.createSend(address, valueOf(1, 20)));
FakeTxBuilder.BlockPair bp1 = createFakeBlock(blockStore, 1, send1);
wallet.receiveFromBlock(send1, bp1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.notifyNewBestBlock(bp1.storedBlock);
assertUnspent(send1);
wallet.receivePending(send2, null);
assertUnspent(send1);
assertDead(send2);
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void txSpendingDeadTx() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
final Address address = new ECKey().toAddress(params);
Transaction send1 = checkNotNull(wallet.createSend(address, valueOf(1, 0)));
Transaction send2 = checkNotNull(wallet.createSend(address, valueOf(1, 20)));
wallet.commitTx(send1);
assertPending(send1);
Transaction send1b = checkNotNull(wallet.createSend(address, valueOf(0, 50)));
FakeTxBuilder.BlockPair bp1 = createFakeBlock(blockStore, 1, send2);
wallet.receiveFromBlock(send2, bp1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.notifyNewBestBlock(bp1.storedBlock);
assertDead(send1);
assertUnspent(send2);
wallet.receivePending(send1b, null);
assertDead(send1);
assertUnspent(send2);
assertDead(send1b);
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
private void assertInConflict(Transaction tx) {
assertEquals(ConfidenceType.IN_CONFLICT, tx.getConfidence().getConfidenceType());
assertTrue(wallet.pending.containsKey(tx.getHash()));
}
private void assertPending(Transaction tx) {
assertEquals(ConfidenceType.PENDING, tx.getConfidence().getConfidenceType());
assertTrue(wallet.pending.containsKey(tx.getHash()));
}
private void assertSpent(Transaction tx) {
assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType());
assertTrue(wallet.spent.containsKey(tx.getHash()));
}
private void assertUnspent(Transaction tx) {
assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType());
assertTrue(wallet.unspent.containsKey(tx.getHash()));
}
private void assertDead(Transaction tx) {
assertEquals(ConfidenceType.DEAD, tx.getConfidence().getConfidenceType());
assertTrue(wallet.dead.containsKey(tx.getHash()));
}
@Test
public void testAddTransactionsDependingOn() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
final Address address = new ECKey().toAddress(params);
Transaction send1 = checkNotNull(wallet.createSend(address, valueOf(1, 0)));
Transaction send2 = checkNotNull(wallet.createSend(address, valueOf(1, 20)));
wallet.commitTx(send1);
Transaction send1b = checkNotNull(wallet.createSend(address, valueOf(0, 50)));
wallet.commitTx(send1b);
Transaction send1c = checkNotNull(wallet.createSend(address, valueOf(0, 25)));
wallet.commitTx(send1c);
wallet.commitTx(send2);
Set<Transaction> txns = new HashSet<Transaction>();
txns.add(send1);
wallet.addTransactionsDependingOn(txns, wallet.getTransactions(true));
assertEquals(3, txns.size());
assertTrue(txns.contains(send1));
assertTrue(txns.contains(send1b));
assertTrue(txns.contains(send1c));
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void sortTxnsByDependency() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
Transaction send1 = sendMoneyToWallet(valueOf(2, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
final Address address = new ECKey().toAddress(params);
Transaction send1a = checkNotNull(wallet.createSend(address, valueOf(1, 0)));
wallet.commitTx(send1a);
Transaction send1b = checkNotNull(wallet.createSend(address, valueOf(0, 50)));
wallet.commitTx(send1b);
Transaction send1c = checkNotNull(wallet.createSend(address, valueOf(0, 25)));
wallet.commitTx(send1c);
Transaction send1d = checkNotNull(wallet.createSend(address, valueOf(0, 12)));
wallet.commitTx(send1d);
Transaction send1e = checkNotNull(wallet.createSend(address, valueOf(0, 06)));
wallet.commitTx(send1e);
Transaction send2 = sendMoneyToWallet(valueOf(200, 0), AbstractBlockChain.NewBlockType.BEST_CHAIN);
SendRequest req2a = SendRequest.to(address, valueOf(100, 0));
req2a.tx.addInput(send2.getOutput(0));
req2a.shuffleOutputs = false;
wallet.completeTx(req2a);
Transaction send2a = req2a.tx;
SendRequest req2b = SendRequest.to(address, valueOf(50, 0));
req2b.tx.addInput(send2a.getOutput(1));
req2b.shuffleOutputs = false;
wallet.completeTx(req2b);
Transaction send2b = req2b.tx;
SendRequest req2c = SendRequest.to(address, valueOf(25, 0));
req2c.tx.addInput(send2b.getOutput(1));
req2c.shuffleOutputs = false;
wallet.completeTx(req2c);
Transaction send2c = req2c.tx;
Set<Transaction> unsortedTxns = new HashSet<Transaction>();
unsortedTxns.add(send1a);
unsortedTxns.add(send1b);
unsortedTxns.add(send1c);
unsortedTxns.add(send1d);
unsortedTxns.add(send1e);
unsortedTxns.add(send2a);
unsortedTxns.add(send2b);
unsortedTxns.add(send2c);
List<Transaction> sortedTxns = wallet.sortTxnsByDependency(unsortedTxns);
assertEquals(8, sortedTxns.size());
assertTrue(sortedTxns.indexOf(send1a) < sortedTxns.indexOf(send1b));
assertTrue(sortedTxns.indexOf(send1b) < sortedTxns.indexOf(send1c));
assertTrue(sortedTxns.indexOf(send1c) < sortedTxns.indexOf(send1d));
assertTrue(sortedTxns.indexOf(send1d) < sortedTxns.indexOf(send1e));
assertTrue(sortedTxns.indexOf(send2a) < sortedTxns.indexOf(send2b));
assertTrue(sortedTxns.indexOf(send2b) < sortedTxns.indexOf(send2c));
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test @Test
public void pending1() throws Exception { public void pending1() throws Exception {
// Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate. // Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate.