mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-15 03:35:52 +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:
parent
61377297ae
commit
293591bf24
@ -96,6 +96,15 @@ public class TransactionConfidence {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -260,7 +269,7 @@ public class TransactionConfidence {
|
||||
if (confidenceType != ConfidenceType.DEAD) {
|
||||
overridingTransaction = null;
|
||||
}
|
||||
if (confidenceType == ConfidenceType.PENDING) {
|
||||
if (confidenceType == ConfidenceType.PENDING || confidenceType == ConfidenceType.IN_CONFLICT) {
|
||||
depth = 0;
|
||||
appearedAtChainHeight = -1;
|
||||
}
|
||||
@ -323,6 +332,9 @@ public class TransactionConfidence {
|
||||
case PENDING:
|
||||
builder.append("Pending/unconfirmed.");
|
||||
break;
|
||||
case IN_CONFLICT:
|
||||
builder.append("In conflict.");
|
||||
break;
|
||||
case BUILDING:
|
||||
builder.append(String.format("Appeared in best chain at height %d, depth %d.",
|
||||
getAppearedAtChainHeight(), getDepthInBlocks()));
|
||||
@ -377,12 +389,12 @@ public class TransactionConfidence {
|
||||
* store this information.
|
||||
*
|
||||
* @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() {
|
||||
if (getConfidenceType() != ConfidenceType.DEAD)
|
||||
throw new IllegalStateException("Confidence type is " + getConfidenceType() +
|
||||
", not OVERRIDDEN_BY_DOUBLE_SPEND");
|
||||
", not DEAD");
|
||||
return overridingTransaction;
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
//
|
||||
// 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.
|
||||
// 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, we update the confidence type
|
||||
// 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
|
||||
// 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.
|
||||
@ -1692,6 +1692,7 @@ public class Wallet extends BaseTaggableObject
|
||||
// We only care about transactions that:
|
||||
// - Send us coins
|
||||
// - Spend our coins
|
||||
// - Double spend a tx in our wallet
|
||||
if (!isTransactionRelevant(tx)) {
|
||||
log.debug("Received tx that isn't relevant to this wallet, discarding.");
|
||||
return false;
|
||||
@ -1715,41 +1716,64 @@ public class Wallet extends BaseTaggableObject
|
||||
try {
|
||||
return tx.getValueSentFromMe(this).signum() > 0 ||
|
||||
tx.getValueSentToMe(this).signum() > 0 ||
|
||||
checkForDoubleSpendAgainstPending(tx, false);
|
||||
!findDoubleSpendsAgainst(tx, transactions).isEmpty();
|
||||
} finally {
|
||||
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.
|
||||
* @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());
|
||||
if (tx.isCoinBase()) return Sets.newHashSet();
|
||||
// Compile a set of outpoints that are spent by tx.
|
||||
HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>();
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
outpoints.add(input.getOutpoint());
|
||||
}
|
||||
// Now for each pending transaction, see if it shares any outpoints with this tx.
|
||||
LinkedList<Transaction> doubleSpentTxns = Lists.newLinkedList();
|
||||
for (Transaction p : pending.values()) {
|
||||
Set<Transaction> doubleSpendTxns = Sets.newHashSet();
|
||||
for (Transaction p : candidates.values()) {
|
||||
for (TransactionInput input : p.getInputs()) {
|
||||
// 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.
|
||||
TransactionOutPoint outpoint = input.getOutpoint();
|
||||
if (outpoints.contains(outpoint)) {
|
||||
// It does, it's a double spend against the pending pool, which makes it relevant.
|
||||
if (!doubleSpentTxns.isEmpty() && doubleSpentTxns.getLast() == p) continue;
|
||||
doubleSpentTxns.add(p);
|
||||
// It does, it's a double spend against the candidates, which makes it relevant.
|
||||
doubleSpendTxns.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (takeAction && !doubleSpentTxns.isEmpty()) {
|
||||
killTx(tx, doubleSpentTxns);
|
||||
return doubleSpendTxns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
|
||||
if (bestChain) {
|
||||
boolean wasDead = dead.remove(txHash) != null;
|
||||
if (wasDead)
|
||||
log.info(" <-dead");
|
||||
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.
|
||||
@ -1835,7 +1862,7 @@ public class Wallet extends BaseTaggableObject
|
||||
}
|
||||
}
|
||||
}
|
||||
processTxFromBestChain(tx, wasPending);
|
||||
processTxFromBestChain(tx, wasPending || wasDead);
|
||||
} else {
|
||||
checkState(sideChain);
|
||||
// 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
|
||||
// quite normal and expected).
|
||||
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.
|
||||
// Committing it updates the spent flags and inserts into the pool as well.
|
||||
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
|
||||
// count.
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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() {
|
||||
if (insideReorg)
|
||||
return;
|
||||
@ -2032,7 +2122,12 @@ public class Wallet extends BaseTaggableObject
|
||||
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:
|
||||
// (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.
|
||||
// (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.
|
||||
} 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
|
||||
private void killTx(@Nullable Transaction overridingTx, List<Transaction> killedTx) {
|
||||
LinkedList<Transaction> work = new LinkedList<Transaction>(killedTx);
|
||||
private void killTxns(Set<Transaction> txnsToKill, @Nullable Transaction overridingTx) {
|
||||
LinkedList<Transaction> work = new LinkedList<Transaction>(txnsToKill);
|
||||
while (!work.isEmpty()) {
|
||||
final Transaction tx = work.poll();
|
||||
log.warn("TX {} killed{}", tx.getHashAsString(),
|
||||
@ -2152,7 +2247,7 @@ public class Wallet extends BaseTaggableObject
|
||||
for (TransactionInput deadInput : tx.getInputs()) {
|
||||
Transaction connected = deadInput.getOutpoint().fromTx;
|
||||
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()));
|
||||
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
|
||||
// spends.
|
||||
updateForSpends(tx, false);
|
||||
// Add to the pending pool. It'll be moved out once we receive this transaction on the best chain.
|
||||
// This also registers txConfidenceListener so wallet listeners get informed.
|
||||
log.info("->pending: {}", tx.getHashAsString());
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING);
|
||||
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
|
||||
addWalletTransaction(Pool.PENDING, tx);
|
||||
|
||||
Set<Transaction> doubleSpendPendingTxns = findDoubleSpendsAgainst(tx, pending);
|
||||
Set<Transaction> doubleSpendUnspentTxns = findDoubleSpendsAgainst(tx, unspent);
|
||||
Set<Transaction> doubleSpendSpentTxns = findDoubleSpendsAgainst(tx, spent);
|
||||
|
||||
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
|
||||
// they are showing to the user in qr codes etc.
|
||||
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) {
|
||||
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
|
||||
// can do our best.
|
||||
log.warn("Coinbase killed by re-org: {}", tx.getHashAsString());
|
||||
killTx(null, ImmutableList.of(tx));
|
||||
killTxns(ImmutableSet.of(tx), null);
|
||||
} else {
|
||||
for (TransactionOutput output : tx.getOutputs()) {
|
||||
TransactionInput input = output.getSpentBy();
|
||||
@ -4299,6 +4424,7 @@ public class Wallet extends BaseTaggableObject
|
||||
// there's another re-org.
|
||||
if (tx.isCoinBase()) continue;
|
||||
log.info(" ->pending {}", tx.getHash());
|
||||
|
||||
tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); // Wipe height/depth/work data.
|
||||
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
|
||||
addWalletTransaction(Pool.PENDING, tx);
|
||||
|
@ -733,6 +733,7 @@ public class WalletProtobufSerializer {
|
||||
// These two are equivalent (must be able to read old wallets).
|
||||
case NOT_IN_BEST_CHAIN: confidenceType = ConfidenceType.PENDING; break;
|
||||
case PENDING: confidenceType = ConfidenceType.PENDING; break;
|
||||
case IN_CONFLICT: confidenceType = ConfidenceType.IN_CONFLICT; break;
|
||||
case UNKNOWN:
|
||||
// Fall through.
|
||||
default:
|
||||
|
@ -175,13 +175,13 @@ public class FakeTxBuilder {
|
||||
Coin value = COIN;
|
||||
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);
|
||||
TransactionOutput prevOut = new TransactionOutput(params, doubleSpends.prevTx, value, someBadGuy);
|
||||
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.t2 = new Transaction(params);
|
||||
@ -209,14 +209,14 @@ public class FakeTxBuilder {
|
||||
return createFakeBlock(blockStore, version, timeSeconds, 0, transactions);
|
||||
}
|
||||
|
||||
/** Emulates receiving a valid block that builds on top of the chain. */
|
||||
public static BlockPair createFakeBlock(BlockStore blockStore, long version,
|
||||
/** Emulates receiving a valid block */
|
||||
public static BlockPair createFakeBlock(BlockStore blockStore, StoredBlock previousStoredBlock, long version,
|
||||
long timeSeconds, int height,
|
||||
Transaction... transactions) {
|
||||
try {
|
||||
Block chainHead = blockStore.getChainHead().getHeader();
|
||||
Address to = new ECKey().toAddress(chainHead.getParams());
|
||||
Block b = chainHead.createNextBlock(to, version, timeSeconds, height);
|
||||
Block previousBlock = previousStoredBlock.getHeader();
|
||||
Address to = new ECKey().toAddress(previousBlock.getParams());
|
||||
Block b = previousBlock.createNextBlock(to, version, timeSeconds, height);
|
||||
// Coinbase tx was already added.
|
||||
for (Transaction tx : transactions) {
|
||||
tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK);
|
||||
@ -225,7 +225,7 @@ public class FakeTxBuilder {
|
||||
b.solve();
|
||||
BlockPair pair = new BlockPair();
|
||||
pair.block = b;
|
||||
pair.storedBlock = blockStore.getChainHead().build(b);
|
||||
pair.storedBlock = previousStoredBlock.build(b);
|
||||
blockStore.put(pair.storedBlock);
|
||||
blockStore.setChainHead(pair.storedBlock);
|
||||
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. */
|
||||
public static BlockPair createFakeBlock(BlockStore blockStore, int height,
|
||||
Transaction... transactions) {
|
||||
|
@ -6766,6 +6766,14 @@ public final class Protos {
|
||||
* </pre>
|
||||
*/
|
||||
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 3: return NOT_IN_BEST_CHAIN;
|
||||
case 4: return DEAD;
|
||||
case 5: return IN_CONFLICT;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package org.bitcoinj.core;
|
||||
|
||||
import org.bitcoinj.core.listeners.AbstractWalletEventListener;
|
||||
import org.bitcoinj.core.listeners.WalletCoinEventListener;
|
||||
import org.bitcoinj.core.TransactionConfidence.ConfidenceType;
|
||||
import org.bitcoinj.core.Wallet.SendRequest;
|
||||
import org.bitcoinj.crypto.*;
|
||||
import org.bitcoinj.script.Script;
|
||||
@ -747,7 +748,7 @@ public class WalletTest extends TestWithWallet {
|
||||
send2 = params.getDefaultSerializer().makeTransaction(send2.bitcoinSerialize());
|
||||
// Broadcast send1, it's now pending.
|
||||
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.
|
||||
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 ...
|
||||
@ -863,6 +864,409 @@ public class WalletTest extends TestWithWallet {
|
||||
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
|
||||
public void pending1() throws Exception {
|
||||
// Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate.
|
||||
|
Loading…
x
Reference in New Issue
Block a user