3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 23:32:16 +00:00

Simplify the handling of double spends somewhat.

This commit is contained in:
Mike Hearn 2013-03-21 15:25:52 +01:00
parent 608810cfc1
commit 76e539e8e7
3 changed files with 37 additions and 49 deletions

View File

@ -309,7 +309,7 @@ public class TransactionInput extends ChildMessage implements Serializable {
* @return NO_SUCH_TX if transaction is not the prevtx, ALREADY_SPENT if there was a conflict, SUCCESS if not. * @return NO_SUCH_TX if transaction is not the prevtx, ALREADY_SPENT if there was a conflict, SUCCESS if not.
*/ */
public ConnectionResult connect(Transaction transaction, ConnectMode mode) { public ConnectionResult connect(Transaction transaction, ConnectMode mode) {
if (!transaction.getHash().equals(outpoint.getHash())) if (!transaction.getHash().equals(outpoint.getHash()) && mode != ConnectMode.DISCONNECT_ON_CONFLICT)
return ConnectionResult.NO_SUCH_TX; return ConnectionResult.NO_SUCH_TX;
checkElementIndex((int) outpoint.getIndex(), transaction.getOutputs().size(), "Corrupt transaction"); checkElementIndex((int) outpoint.getIndex(), transaction.getOutputs().size(), "Corrupt transaction");
TransactionOutput out = transaction.getOutput((int) outpoint.getIndex()); TransactionOutput out = transaction.getOutput((int) outpoint.getIndex());
@ -332,15 +332,21 @@ public class TransactionInput extends ChildMessage implements Serializable {
} }
/** /**
* Release the connected output, making it spendable once again. * If this input is connected, check the output is connected back to this input and release it if so, making
* it spendable once again.
* *
* @return true if the disconnection took place, false if it was not connected. * @return true if the disconnection took place, false if it was not connected.
*/ */
boolean disconnect() { boolean disconnect() {
if (outpoint.fromTx == null) return false; if (outpoint.fromTx == null) return false;
outpoint.fromTx.getOutputs().get((int) outpoint.getIndex()).markAsUnspent(); TransactionOutput output = outpoint.fromTx.getOutput((int) outpoint.getIndex());
outpoint.fromTx = null; if (output.getSpentBy() == this) {
return true; output.markAsUnspent();
outpoint.fromTx = null;
return true;
} else {
return false;
}
} }
/** /**

View File

@ -153,7 +153,7 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
@Override @Override
public String toString() { public String toString() {
return "outpoint " + hash.toString() + ":" + index; return hash.toString() + ":" + index;
} }

View File

@ -971,7 +971,7 @@ public class Wallet implements Serializable, BlockChainListener {
* Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if * Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if
* the double spent inputs are not ours. Returns the pending tx that was double spent or null if none found. * the double spent inputs are not ours. Returns the pending tx that was double spent or null if none found.
*/ */
private Transaction findDoubleSpendAgainstPending(Transaction tx) { private TransactionInput findDoubleSpendAgainstPending(Transaction tx) {
checkState(lock.isLocked()); checkState(lock.isLocked());
// 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>();
@ -983,7 +983,7 @@ public class Wallet implements Serializable, BlockChainListener {
for (TransactionInput input : p.getInputs()) { for (TransactionInput input : p.getInputs()) {
if (outpoints.contains(input.getOutpoint())) { if (outpoints.contains(input.getOutpoint())) {
// It does, it's a double spend against the pending pool, which makes it relevant. // It does, it's a double spend against the pending pool, which makes it relevant.
return p; return input;
} }
} }
} }
@ -1236,17 +1236,11 @@ public class Wallet implements Serializable, BlockChainListener {
addWalletTransaction(Pool.SPENT, tx); addWalletTransaction(Pool.SPENT, tx);
} }
Transaction doubleSpend = findDoubleSpendAgainstPending(tx); TransactionInput doubleSpent = findDoubleSpendAgainstPending(tx);
if (doubleSpend != null) { if (doubleSpent != null) {
// This is mostly the same as the codepath in updateForSpends, but that one is only triggered when // This is mostly the same as the codepath in updateForSpends, but that one is only triggered when
// the transaction being double spent is actually in our wallet (ie, maybe we're double spending). // the transaction being double spent is actually in our wallet (ie, maybe we're double spending).
log.warn(" saw double spend from chain override pending tx {}", doubleSpend.getHashAsString()); killTx(tx, doubleSpent, doubleSpent.getParentTransaction());
log.warn(" <-pending ->dead killed by {}", tx.getHashAsString());
pending.remove(doubleSpend.getHash());
addWalletTransaction(Pool.DEAD, doubleSpend);
// Inform the event listeners of the newly dead tx.
doubleSpend.getConfidence().setOverridingTransaction(tx);
// TODO: Disconnect the inputs of doubleSpend here.
} }
} }
@ -1286,42 +1280,15 @@ public class Wallet implements Serializable, BlockChainListener {
} }
if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
// Double spend! Work backwards like so:
//
// A -> spent by B [pending]
// \-> spent by C [this tx]
//
// fromTx here was set by the connect call above.
Transaction doubleSpent = input.getOutpoint().fromTx; // == A
checkNotNull(doubleSpent);
int index = (int) input.getOutpoint().getIndex();
TransactionOutput output = doubleSpent.getOutput(index);
TransactionInput spentBy = checkNotNull(output.getSpentBy());
Transaction connected = checkNotNull(spentBy.getParentTransaction());
if (fromChain) { if (fromChain) {
// This must have overridden a pending tx, or the block is bad (contains transactions // This will be handled later by processTxFromBestChain.
// that illegally double spend: should never occur if we are connected to an honest node).
if (pending.containsKey(connected.getHash())) {
log.warn("Saw double spend from chain override pending tx {}", connected.getHashAsString());
log.warn(" <-pending ->dead killed by {}", tx.getHashAsString());
pending.remove(connected.getHash());
dead.put(connected.getHash(), connected);
// Now forcibly change the connection.
input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
// Inform the [tx] event listeners of the newly dead tx. This sets confidence type also.
connected.getConfidence().setOverridingTransaction(tx);
} else {
throw new VerificationException("Transaction from chain double spent in unspent/spent maps: " + tx.getHashAsString()) ;
}
} else { } else {
// A pending transaction that tried to double spend our coins - we log and ignore it, because either // We saw two pending transactions that double spend each other. We don't know which will win.
// 1) The double-spent tx is confirmed and thus this tx has no effect .... or // Either that, or we somehow allowed ourselves to create double spends ourselves!
// 2) Both txns are pending, neither has priority. Miners will decide in a few minutes which won. // TODO: Find some way to communicate to the user that both transactions in jeopardy.
log.warn("Saw double spend from another pending transaction, ignoring tx {}", log.warn("Saw double spend from another pending transaction, ignoring tx {}",
tx.getHashAsString()); tx.getHashAsString());
log.warn(" offending input is input {}", tx.getInputs().indexOf(input)); log.warn(" offending input is input {}", tx.getInputs().indexOf(input));
// TODO: We should report this state via tx confidence somehow ("in jeopardy"?)
// Fall through now to checking the pending inputs.
} }
} else if (result == TransactionInput.ConnectionResult.SUCCESS) { } else if (result == TransactionInput.ConnectionResult.SUCCESS) {
// Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet. // Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet.
@ -1354,6 +1321,21 @@ public class Wallet implements Serializable, BlockChainListener {
} }
} }
private void killTx(Transaction overridingTx, TransactionInput overridingInput, Transaction killedTx) {
TransactionOutPoint overriddenOutPoint = overridingInput.getOutpoint();
log.warn("Saw double spend of {} from chain override pending tx {}",
overriddenOutPoint, killedTx.getHashAsString());
log.warn(" <-pending ->dead killed by {}", overridingTx.getHashAsString());
checkState(overridingInput.connect(overridingTx, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT) == TransactionInput.ConnectionResult.SUCCESS);
pending.remove(killedTx.getHash());
addWalletTransaction(Pool.DEAD, killedTx);
// Inform the [tx] event listeners of the newly dead tx and disconnect the other inputs.
for (TransactionInput deadInput : killedTx.getInputs()) deadInput.disconnect();
killedTx.getConfidence().setOverridingTransaction(overridingTx);
// TODO: Move newly unspent transactions (if any) back into the unspent pool to avoid inconsistency.
// TODO: Recursively kill other transactions that were double spent.
}
/** /**
* If the transactions outputs are all marked as spent, and it's in the unspent map, move it. * If the transactions outputs are all marked as spent, and it's in the unspent map, move it.
*/ */