mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 15:22:16 +00:00
Wallet: recursively kill transactions that depend on dead transactions.
Resolves issue 186.
This commit is contained in:
parent
51c0f283e2
commit
ce61bd211d
@ -244,10 +244,12 @@ public class TransactionConfidence implements Serializable {
|
|||||||
* transaction becomes available.
|
* transaction becomes available.
|
||||||
*/
|
*/
|
||||||
public synchronized void setConfidenceType(ConfidenceType confidenceType) {
|
public synchronized void setConfidenceType(ConfidenceType confidenceType) {
|
||||||
// Don't inform the event listeners if the confidence didn't really change.
|
|
||||||
if (confidenceType == this.confidenceType)
|
if (confidenceType == this.confidenceType)
|
||||||
return;
|
return;
|
||||||
this.confidenceType = confidenceType;
|
this.confidenceType = confidenceType;
|
||||||
|
if (confidenceType != ConfidenceType.DEAD) {
|
||||||
|
overridingTransaction = null;
|
||||||
|
}
|
||||||
if (confidenceType == ConfidenceType.PENDING) {
|
if (confidenceType == ConfidenceType.PENDING) {
|
||||||
depth = 0;
|
depth = 0;
|
||||||
appearedAtChainHeight = -1;
|
appearedAtChainHeight = -1;
|
||||||
@ -390,7 +392,8 @@ public class TransactionConfidence implements Serializable {
|
|||||||
/**
|
/**
|
||||||
* Called when the transaction becomes newly dead, that is, we learn that one of its inputs has already been spent
|
* Called when the transaction becomes newly dead, that is, we learn that one of its inputs has already been spent
|
||||||
* in such a way that the double-spending transaction takes precedence over this one. It will not become valid now
|
* in such a way that the double-spending transaction takes precedence over this one. It will not become valid now
|
||||||
* unless there is a re-org. Automatically sets the confidence type to DEAD.
|
* unless there is a re-org. Automatically sets the confidence type to DEAD. The overriding transaction may not
|
||||||
|
* directly double spend this one, but could also have double spent a dependency of this tx.
|
||||||
*/
|
*/
|
||||||
public synchronized void setOverridingTransaction(@Nullable Transaction overridingTransaction) {
|
public synchronized void setOverridingTransaction(@Nullable Transaction overridingTransaction) {
|
||||||
this.overridingTransaction = overridingTransaction;
|
this.overridingTransaction = overridingTransaction;
|
||||||
|
@ -52,7 +52,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
|||||||
// was owned by us and was sent to somebody else. If false and spentBy is set it means this output was owned by
|
// was owned by us and was sent to somebody else. If false and spentBy is set it means this output was owned by
|
||||||
// us and used in one of our own transactions (eg, because it is a change output).
|
// us and used in one of our own transactions (eg, because it is a change output).
|
||||||
private boolean availableForSpending;
|
private boolean availableForSpending;
|
||||||
private TransactionInput spentBy;
|
@Nullable private TransactionInput spentBy;
|
||||||
|
|
||||||
// A reference to the transaction which holds this output.
|
// A reference to the transaction which holds this output.
|
||||||
Transaction parentTransaction;
|
Transaction parentTransaction;
|
||||||
@ -311,6 +311,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
|||||||
/**
|
/**
|
||||||
* Returns the connected input.
|
* Returns the connected input.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public TransactionInput getSpentBy() {
|
public TransactionInput getSpentBy() {
|
||||||
return spentBy;
|
return spentBy;
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
transactions = new HashMap<Sha256Hash, Transaction>();
|
transactions = new HashMap<Sha256Hash, Transaction>();
|
||||||
eventListeners = new CopyOnWriteArrayList<ListenerRegistration<WalletEventListener>>();
|
eventListeners = new CopyOnWriteArrayList<ListenerRegistration<WalletEventListener>>();
|
||||||
extensions = new HashMap<String, WalletExtension>();
|
extensions = new HashMap<String, WalletExtension>();
|
||||||
confidenceChanged = new HashMap<Transaction, TransactionConfidence.Listener.ChangeReason>();
|
// Use a linked hash map to ensure ordering of event listeners is correct.
|
||||||
|
confidenceChanged = new LinkedHashMap<Transaction, TransactionConfidence.Listener.ChangeReason>();
|
||||||
createTransientState();
|
createTransientState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -987,9 +988,13 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
boolean isDeadCoinbase = tx.isCoinBase() && dead.containsKey(tx.getHash());
|
boolean isDeadCoinbase = tx.isCoinBase() && dead.containsKey(tx.getHash());
|
||||||
if (isDeadCoinbase) {
|
if (isDeadCoinbase) {
|
||||||
// There is a dead coinbase tx being received on the best chain. A coinbase tx is made dead when it moves
|
// There is a dead coinbase tx being received on the best chain. A coinbase tx is made dead when it moves
|
||||||
// to a side chain but it can be switched back on a reorg and 'resurrected' back to spent or unspent.
|
// to a side chain but it can be switched back on a reorg and resurrected back to spent or unspent.
|
||||||
// So take it out of the dead pool.
|
// So take it out of the dead pool. Note that we don't resurrect dependent transactions here, even though
|
||||||
log.info(" coinbase tx {} <-dead: confidence {}", tx.getHashAsString(),
|
// we could. Bitcoin Core nodes on the network have deleted the dependent transactions from their mempools
|
||||||
|
// entirely by this point. We could and maybe should rebroadcast them so the network remembers and tries
|
||||||
|
// to confirm them again. But this is a deeply unusual edge case that due to the maturity rule should never
|
||||||
|
// happen in practice, thus for simplicities sake we ignore it here.
|
||||||
|
log.info(" coinbase tx <-dead: confidence {}", tx.getHashAsString(),
|
||||||
tx.getConfidence().getConfidenceType().name());
|
tx.getConfidence().getConfidenceType().name());
|
||||||
dead.remove(tx.getHash());
|
dead.remove(tx.getHash());
|
||||||
}
|
}
|
||||||
@ -1103,25 +1108,18 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void killCoinbase(Transaction coinbase) {
|
// Updates the wallet when a double spend occurs. overridingTx can be null for the case of coinbases
|
||||||
log.warn("Coinbase killed by re-org: {}", coinbase.getHashAsString());
|
private void killTx(@Nullable Transaction overridingTx, List<Transaction> killedTx) {
|
||||||
coinbase.getConfidence().setOverridingTransaction(null);
|
LinkedList<Transaction> work = new LinkedList<Transaction>(killedTx);
|
||||||
confidenceChanged.put(coinbase, TransactionConfidence.Listener.ChangeReason.TYPE);
|
while (!work.isEmpty()) {
|
||||||
final Sha256Hash hash = coinbase.getHash();
|
final Transaction tx = work.poll();
|
||||||
pending.remove(hash);
|
log.warn("TX {} killed{}", tx.getHashAsString(),
|
||||||
unspent.remove(hash);
|
overridingTx != null ? "by " + overridingTx.getHashAsString() : "");
|
||||||
spent.remove(hash);
|
|
||||||
addWalletTransaction(Pool.DEAD, coinbase);
|
|
||||||
// TODO: Properly handle the recursive nature of killing transactions here.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the wallet when a double spend occurs. overridingTx/overridingInput can be null for the case of coinbases
|
|
||||||
private void killTx(Transaction overridingTx, List<Transaction> killedTx) {
|
|
||||||
for (Transaction tx : killedTx) {
|
|
||||||
log.warn("Saw double spend from chain override pending tx {}", tx.getHashAsString());
|
|
||||||
log.warn(" <-pending ->dead killed by {}", overridingTx.getHashAsString());
|
|
||||||
log.warn("Disconnecting each input and moving connected transactions.");
|
log.warn("Disconnecting each input and moving connected transactions.");
|
||||||
|
// TX could be pending (finney attack), or in unspent/spent (coinbase killed by reorg).
|
||||||
pending.remove(tx.getHash());
|
pending.remove(tx.getHash());
|
||||||
|
unspent.remove(tx.getHash());
|
||||||
|
spent.remove(tx.getHash());
|
||||||
addWalletTransaction(Pool.DEAD, tx);
|
addWalletTransaction(Pool.DEAD, tx);
|
||||||
for (TransactionInput deadInput : tx.getInputs()) {
|
for (TransactionInput deadInput : tx.getInputs()) {
|
||||||
Transaction connected = deadInput.getOutpoint().fromTx;
|
Transaction connected = deadInput.getOutpoint().fromTx;
|
||||||
@ -1131,7 +1129,17 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
}
|
}
|
||||||
tx.getConfidence().setOverridingTransaction(overridingTx);
|
tx.getConfidence().setOverridingTransaction(overridingTx);
|
||||||
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
|
confidenceChanged.put(tx, TransactionConfidence.Listener.ChangeReason.TYPE);
|
||||||
|
// Now kill any transactions we have that depended on this one.
|
||||||
|
for (TransactionOutput deadOutput : tx.getOutputs()) {
|
||||||
|
TransactionInput connected = deadOutput.getSpentBy();
|
||||||
|
if (connected == null) continue;
|
||||||
|
final Transaction parentTransaction = connected.getParentTransaction();
|
||||||
|
log.info("This death invalidated dependent tx {}", parentTransaction.getHash());
|
||||||
|
work.push(parentTransaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (overridingTx == null)
|
||||||
|
return;
|
||||||
log.warn("Now attempting to connect the inputs of the overriding transaction.");
|
log.warn("Now attempting to connect the inputs of the overriding transaction.");
|
||||||
for (TransactionInput input : overridingTx.getInputs()) {
|
for (TransactionInput input : overridingTx.getInputs()) {
|
||||||
TransactionInput.ConnectionResult result = input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
|
TransactionInput.ConnectionResult result = input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
|
||||||
@ -1144,7 +1152,6 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Recursively kill other transactions that were double spent.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2491,17 +2498,14 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
// graph we can never reliably kill all transactions we might have that were rooted in
|
// 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
|
// 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());
|
||||||
// TODO: Is it better to try and sometimes fail, or not try at all?
|
killTx(null, ImmutableList.of(tx));
|
||||||
killCoinbase(tx);
|
|
||||||
} else {
|
} else {
|
||||||
for (TransactionOutput output : tx.getOutputs()) {
|
for (TransactionOutput output : tx.getOutputs()) {
|
||||||
TransactionInput input = output.getSpentBy();
|
TransactionInput input = output.getSpentBy();
|
||||||
if (input != null) input.disconnect();
|
if (input != null) input.disconnect();
|
||||||
}
|
}
|
||||||
for (TransactionInput input : tx.getInputs()) {
|
tx.disconnectInputs();
|
||||||
input.disconnect();
|
|
||||||
}
|
|
||||||
oldChainTxns.add(tx);
|
oldChainTxns.add(tx);
|
||||||
unspent.remove(txHash);
|
unspent.remove(txHash);
|
||||||
spent.remove(txHash);
|
spent.remove(txHash);
|
||||||
|
@ -572,22 +572,19 @@ public class ChainSplitTest {
|
|||||||
public void coinbaseDeath() throws Exception {
|
public void coinbaseDeath() throws Exception {
|
||||||
// Check that a coinbase tx is marked as dead after a reorg rather than pending as normal non-double-spent
|
// 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
|
// transactions would be. Also check that a dead coinbase on a sidechain is resurrected if the sidechain
|
||||||
// becomes the best chain once more.
|
// becomes the best chain once more. Finally, check that dependent transactions are killed recursively.
|
||||||
final ArrayList<Transaction> txns = new ArrayList<Transaction>(3);
|
final ArrayList<Transaction> txns = new ArrayList<Transaction>(3);
|
||||||
wallet.addEventListener(new AbstractWalletEventListener() {
|
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||||
txns.add(tx);
|
txns.add(tx);
|
||||||
}
|
}
|
||||||
});
|
}, Threading.SAME_THREAD);
|
||||||
|
|
||||||
// Start by building three blocks on top of the genesis block.
|
Block b1 = unitTestParams.getGenesisBlock().createNextBlock(someOtherGuy);
|
||||||
// The first block contains a normal transaction that spends to coinTo.
|
final ECKey coinsTo2 = wallet.getKeys().get(1);
|
||||||
// The second block contains a coinbase transaction that spends to coinTo2.
|
Block b2 = b1.createNextBlockWithCoinbase(coinsTo2.getPubKey());
|
||||||
// The third block contains a normal transaction that spends to coinTo.
|
Block b3 = b2.createNextBlock(someOtherGuy);
|
||||||
Block b1 = unitTestParams.getGenesisBlock().createNextBlock(coinsTo);
|
|
||||||
Block b2 = b1.createNextBlockWithCoinbase(wallet.getKeys().get(1).getPubKey());
|
|
||||||
Block b3 = b2.createNextBlock(coinsTo);
|
|
||||||
|
|
||||||
log.debug("Adding block b1");
|
log.debug("Adding block b1");
|
||||||
assertTrue(chain.add(b1));
|
assertTrue(chain.add(b1));
|
||||||
@ -600,21 +597,38 @@ public class ChainSplitTest {
|
|||||||
// genesis -> b1 -> b2 -> b3
|
// genesis -> b1 -> b2 -> b3
|
||||||
//
|
//
|
||||||
|
|
||||||
// Check we have seen the three transactions.
|
// Check we have seen the coinbase.
|
||||||
Threading.waitForUserCode();
|
assertEquals(1, txns.size());
|
||||||
assertEquals(3, txns.size());
|
|
||||||
|
|
||||||
// Check the coinbase transaction is building and in the unspent pool only.
|
// Check the coinbase transaction is building and in the unspent pool only.
|
||||||
assertEquals(ConfidenceType.BUILDING, txns.get(1).getConfidence().getConfidenceType());
|
final Transaction coinbase = txns.get(0);
|
||||||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
assertEquals(ConfidenceType.BUILDING, coinbase.getConfidence().getConfidenceType());
|
||||||
assertTrue(wallet.unspent.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.pending.containsKey(coinbase.getHash()));
|
||||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
assertTrue(wallet.unspent.containsKey(coinbase.getHash()));
|
||||||
assertTrue(!wallet.dead.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.spent.containsKey(coinbase.getHash()));
|
||||||
|
assertTrue(!wallet.dead.containsKey(coinbase.getHash()));
|
||||||
|
|
||||||
|
// Add blocks to b3 until we can spend the coinbase.
|
||||||
|
Block firstTip = b3;
|
||||||
|
for (int i = 0; i < unitTestParams.getSpendableCoinbaseDepth() - 2; i++) {
|
||||||
|
firstTip = firstTip.createNextBlock(someOtherGuy);
|
||||||
|
chain.add(firstTip);
|
||||||
|
}
|
||||||
|
// ... and spend.
|
||||||
|
Transaction fodder = wallet.createSend(new ECKey().toAddress(unitTestParams), Utils.toNanoCoins(50, 0));
|
||||||
|
wallet.commitTx(fodder);
|
||||||
|
final AtomicBoolean fodderIsDead = new AtomicBoolean(false);
|
||||||
|
fodder.getConfidence().addEventListener(new TransactionConfidence.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onConfidenceChanged(Transaction tx, ChangeReason reason) {
|
||||||
|
fodderIsDead.set(tx.getConfidence().getConfidenceType() == ConfidenceType.DEAD);
|
||||||
|
}
|
||||||
|
}, Threading.SAME_THREAD);
|
||||||
|
|
||||||
// Fork like this:
|
// Fork like this:
|
||||||
//
|
//
|
||||||
// genesis -> b1 -> b2 -> b3
|
// genesis -> b1 -> b2 -> b3 -> [...]
|
||||||
// \-> b4 -> b5 -> b6
|
// \-> b4 -> b5 -> b6 -> [...]
|
||||||
//
|
//
|
||||||
// The b4/ b5/ b6 is now the best chain
|
// The b4/ b5/ b6 is now the best chain
|
||||||
Block b4 = b1.createNextBlock(someOtherGuy);
|
Block b4 = b1.createNextBlock(someOtherGuy);
|
||||||
@ -627,57 +641,64 @@ public class ChainSplitTest {
|
|||||||
assertTrue(chain.add(b5));
|
assertTrue(chain.add(b5));
|
||||||
log.debug("Adding block b6");
|
log.debug("Adding block b6");
|
||||||
assertTrue(chain.add(b6));
|
assertTrue(chain.add(b6));
|
||||||
Threading.waitForUserCode();
|
|
||||||
|
|
||||||
// Transaction 1 (in block b2) is now on a side chain and should have confidence type of dead and be in the dead pool only
|
Block secondTip = b6;
|
||||||
assertEquals(TransactionConfidence.ConfidenceType.DEAD, txns.get(1).getConfidence().getConfidenceType());
|
for (int i = 0; i < unitTestParams.getSpendableCoinbaseDepth() - 2; i++) {
|
||||||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
secondTip = secondTip.createNextBlock(someOtherGuy);
|
||||||
assertTrue(!wallet.unspent.containsKey(txns.get(1).getHash()));
|
chain.add(secondTip);
|
||||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
}
|
||||||
assertTrue(wallet.dead.containsKey(txns.get(1).getHash()));
|
|
||||||
|
// Transaction 1 (in block b2) is now on a side chain and should have confidence type of dead and be in
|
||||||
|
// the dead pool only.
|
||||||
|
assertEquals(TransactionConfidence.ConfidenceType.DEAD, coinbase.getConfidence().getConfidenceType());
|
||||||
|
assertTrue(!wallet.pending.containsKey(coinbase.getHash()));
|
||||||
|
assertTrue(!wallet.unspent.containsKey(coinbase.getHash()));
|
||||||
|
assertTrue(!wallet.spent.containsKey(coinbase.getHash()));
|
||||||
|
assertTrue(wallet.dead.containsKey(coinbase.getHash()));
|
||||||
|
assertTrue(fodderIsDead.get());
|
||||||
|
|
||||||
// ... and back to the first chain.
|
// ... and back to the first chain.
|
||||||
Block b7 = b3.createNextBlock(coinsTo);
|
Block b7 = firstTip.createNextBlock(someOtherGuy);
|
||||||
Block b8 = b7.createNextBlock(coinsTo);
|
Block b8 = b7.createNextBlock(someOtherGuy);
|
||||||
|
|
||||||
log.debug("Adding block b7");
|
log.debug("Adding block b7");
|
||||||
assertTrue(chain.add(b7));
|
assertTrue(chain.add(b7));
|
||||||
log.debug("Adding block b8");
|
log.debug("Adding block b8");
|
||||||
assertTrue(chain.add(b8));
|
assertTrue(chain.add(b8));
|
||||||
Threading.waitForUserCode();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// genesis -> b1 -> b2 -> b3 -> b7 -> b8
|
// genesis -> b1 -> b2 -> b3 -> [...] -> b7 -> b8
|
||||||
// \-> b4 -> b5 -> b6
|
// \-> b4 -> b5 -> b6 -> [...]
|
||||||
//
|
//
|
||||||
|
|
||||||
// The coinbase transaction should now have confidence type of building once more and in the unspent pool only.
|
// The coinbase transaction should now have confidence type of building once more and in the unspent pool only.
|
||||||
assertEquals(TransactionConfidence.ConfidenceType.BUILDING, txns.get(1).getConfidence().getConfidenceType());
|
assertEquals(TransactionConfidence.ConfidenceType.BUILDING, coinbase.getConfidence().getConfidenceType());
|
||||||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.pending.containsKey(coinbase.getHash()));
|
||||||
assertTrue(wallet.unspent.containsKey(txns.get(1).getHash()));
|
assertTrue(wallet.unspent.containsKey(coinbase.getHash()));
|
||||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.spent.containsKey(coinbase.getHash()));
|
||||||
assertTrue(!wallet.dead.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.dead.containsKey(coinbase.getHash()));
|
||||||
|
// However, fodder is still dead. Bitcoin Core doesn't keep killed transactions around in case they become
|
||||||
|
// valid again later. They are just deleted from the mempool for good.
|
||||||
|
|
||||||
// ... make the side chain dominant again.
|
// ... make the side chain dominant again.
|
||||||
Block b9 = b6.createNextBlock(coinsTo);
|
Block b9 = secondTip.createNextBlock(someOtherGuy);
|
||||||
Block b10 = b9.createNextBlock(coinsTo);
|
Block b10 = b9.createNextBlock(someOtherGuy);
|
||||||
|
|
||||||
log.debug("Adding block b9");
|
log.debug("Adding block b9");
|
||||||
assertTrue(chain.add(b9));
|
assertTrue(chain.add(b9));
|
||||||
log.debug("Adding block b10");
|
log.debug("Adding block b10");
|
||||||
assertTrue(chain.add(b10));
|
assertTrue(chain.add(b10));
|
||||||
Threading.waitForUserCode();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// genesis -> b1 -> b2 -> b3 -> b7 -> b8
|
// genesis -> b1 -> b2 -> b3 -> [...] -> b7 -> b8
|
||||||
// \-> b4 -> b5 -> b6 -> b9 -> b10
|
// \-> b4 -> b5 -> b6 -> [...] -> b9 -> b10
|
||||||
//
|
//
|
||||||
|
|
||||||
// The coinbase transaction should now have the confidence type of dead and be in the dead pool only.
|
// The coinbase transaction should now have the confidence type of dead and be in the dead pool only.
|
||||||
assertEquals(TransactionConfidence.ConfidenceType.DEAD, txns.get(1).getConfidence().getConfidenceType());
|
assertEquals(TransactionConfidence.ConfidenceType.DEAD, coinbase.getConfidence().getConfidenceType());
|
||||||
assertTrue(!wallet.pending.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.pending.containsKey(coinbase.getHash()));
|
||||||
assertTrue(!wallet.unspent.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.unspent.containsKey(coinbase.getHash()));
|
||||||
assertTrue(!wallet.spent.containsKey(txns.get(1).getHash()));
|
assertTrue(!wallet.spent.containsKey(coinbase.getHash()));
|
||||||
assertTrue(wallet.dead.containsKey(txns.get(1).getHash()));
|
assertTrue(wallet.dead.containsKey(coinbase.getHash()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,6 @@ import com.google.bitcoin.core.Transaction.SigHash;
|
|||||||
import com.google.bitcoin.core.Wallet.SendRequest;
|
import com.google.bitcoin.core.Wallet.SendRequest;
|
||||||
import com.google.bitcoin.wallet.DefaultCoinSelector;
|
import com.google.bitcoin.wallet.DefaultCoinSelector;
|
||||||
import com.google.bitcoin.wallet.RiskAnalysis;
|
import com.google.bitcoin.wallet.RiskAnalysis;
|
||||||
import com.google.bitcoin.wallet.WalletTransaction;
|
|
||||||
import com.google.bitcoin.wallet.WalletTransaction.Pool;
|
|
||||||
import com.google.bitcoin.crypto.KeyCrypter;
|
import com.google.bitcoin.crypto.KeyCrypter;
|
||||||
import com.google.bitcoin.crypto.KeyCrypterException;
|
import com.google.bitcoin.crypto.KeyCrypterException;
|
||||||
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
||||||
@ -33,10 +31,11 @@ import com.google.bitcoin.utils.TestWithWallet;
|
|||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.bitcoin.wallet.KeyTimeCoinSelector;
|
import com.google.bitcoin.wallet.KeyTimeCoinSelector;
|
||||||
import com.google.bitcoin.wallet.WalletFiles;
|
import com.google.bitcoin.wallet.WalletFiles;
|
||||||
|
import com.google.bitcoin.wallet.WalletTransaction;
|
||||||
|
import com.google.bitcoin.wallet.WalletTransaction.Pool;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.bitcoinj.wallet.Protos;
|
import org.bitcoinj.wallet.Protos;
|
||||||
import org.bitcoinj.wallet.Protos.ScryptParameters;
|
import org.bitcoinj.wallet.Protos.ScryptParameters;
|
||||||
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
|
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
|
||||||
@ -57,8 +56,8 @@ import java.util.concurrent.CountDownLatch;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static com.google.bitcoin.utils.TestUtils.*;
|
|
||||||
import static com.google.bitcoin.core.Utils.*;
|
import static com.google.bitcoin.core.Utils.*;
|
||||||
|
import static com.google.bitcoin.utils.TestUtils.*;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
@ -693,24 +692,47 @@ public class WalletTest extends TestWithWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void doubleSpendIdenticalTx() throws Exception {
|
public void doubleSpends() throws Exception {
|
||||||
// Test the case where two semantically identical but bitwise different transactions double spend each other.
|
// Test the case where two semantically identical but bitwise different transactions double spend each other.
|
||||||
|
// We call the second transaction a "mutant" of the first.
|
||||||
|
//
|
||||||
// This can (and has!) happened when a wallet is cloned between devices, and both devices decide to make the
|
// This can (and has!) happened when a wallet is cloned between devices, and both devices decide to make the
|
||||||
// same spend simultaneously - for example due a re-keying operation.
|
// same spend simultaneously - for example due a re-keying operation. It can also happen if there are malicious
|
||||||
|
// nodes in the P2P network that are mutating transactions on the fly as occurred during Feb 2014.
|
||||||
final BigInteger value = Utils.toNanoCoins(1, 0);
|
final BigInteger value = Utils.toNanoCoins(1, 0);
|
||||||
// Give us two outputs.
|
|
||||||
sendMoneyToWallet(value, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
|
||||||
sendMoneyToWallet(value, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
|
||||||
final BigInteger value2 = Utils.toNanoCoins(2, 0);
|
final BigInteger value2 = Utils.toNanoCoins(2, 0);
|
||||||
|
// Give us three coins and make sure we have some change.
|
||||||
|
sendMoneyToWallet(value.add(value2), AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||||
// The two transactions will have different hashes due to the lack of deterministic signing, but will be
|
// The two transactions will have different hashes due to the lack of deterministic signing, but will be
|
||||||
// otherwise identical. Once deterministic signatures are implemented, this test will have to be tweaked.
|
// otherwise identical. Once deterministic signatures are implemented, this test will have to be tweaked.
|
||||||
Transaction send1 = checkNotNull(wallet.createSend(new ECKey().toAddress(params), value2));
|
final Address address = new ECKey().toAddress(params);
|
||||||
Transaction send2 = checkNotNull(wallet.createSend(new ECKey().toAddress(params), value2));
|
Transaction send1 = checkNotNull(wallet.createSend(address, value2));
|
||||||
|
Transaction send2 = checkNotNull(wallet.createSend(address, value2));
|
||||||
send1 = roundTripTransaction(params, send1);
|
send1 = roundTripTransaction(params, send1);
|
||||||
wallet.commitTx(send2);
|
wallet.commitTx(send2);
|
||||||
|
wallet.allowSpendingUnconfirmedTransactions();
|
||||||
|
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
|
// Now spend the change. This transaction should die permanently when the mutant appears in the chain.
|
||||||
|
Transaction send3 = checkNotNull(wallet.createSend(address, value));
|
||||||
|
wallet.commitTx(send3);
|
||||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||||
|
final LinkedList<Transaction> dead = new LinkedList<Transaction>();
|
||||||
|
final TransactionConfidence.Listener listener = new TransactionConfidence.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onConfidenceChanged(Transaction tx, ChangeReason reason) {
|
||||||
|
final TransactionConfidence.ConfidenceType type = tx.getConfidence().getConfidenceType();
|
||||||
|
if (reason == ChangeReason.TYPE && type == TransactionConfidence.ConfidenceType.DEAD)
|
||||||
|
dead.add(tx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
send2.getConfidence().addEventListener(listener, Threading.SAME_THREAD);
|
||||||
|
send3.getConfidence().addEventListener(listener, Threading.SAME_THREAD);
|
||||||
|
// Double spend!
|
||||||
sendMoneyToWallet(send1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
sendMoneyToWallet(send1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
// Back to having one coin.
|
||||||
|
assertEquals(value, wallet.getBalance());
|
||||||
|
assertEquals(send2, dead.poll());
|
||||||
|
assertEquals(send3, dead.poll());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Loading…
Reference in New Issue
Block a user