3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 06:44:16 +00:00

Add a unit test for the case where the block chain overrides a currently pending transaction. This is an edge case that should only occur when the user executes a Finney attack against somebody else.

Fix the bug that Miron pointed out. Resolves comments on r76.
This commit is contained in:
Mike Hearn 2011-05-23 12:13:25 +00:00
parent 846412aa00
commit cb5025b987
7 changed files with 65 additions and 9 deletions

View File

@ -456,7 +456,7 @@ public class Block extends Message {
// counter in the scriptSig so every transaction has a different hash. The output is also different.
// Real coinbase transactions use <pubkey> OP_CHECKSIG rather than a send to an address though there's
// nothing in the system that enforces that and both are just as valid.
coinbase.inputs.add(new TransactionInput(params, new byte[] { (byte) coinbaseCounter++ } ));
coinbase.inputs.add(new TransactionInput(params, coinbase, new byte[] { (byte) coinbaseCounter++ } ));
coinbase.outputs.add(new TransactionOutput(params, coinbase, Utils.toNanoCoins(50, 0), to));
transactions.add(coinbase);
}

View File

@ -76,7 +76,7 @@ public class NetworkParameters implements Serializable {
// "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
byte[] bytes = Hex.decode
("04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73");
t.inputs.add(new TransactionInput(n, bytes));
t.inputs.add(new TransactionInput(n, t, bytes));
ByteArrayOutputStream scriptPubKeyBytes = new ByteArrayOutputStream();
Script.writeBytes(scriptPubKeyBytes, Hex.decode
("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"));

View File

@ -219,7 +219,7 @@ public class Transaction extends Message implements Serializable {
long numInputs = readVarInt();
inputs = new ArrayList<TransactionInput>((int)numInputs);
for (long i = 0; i < numInputs; i++) {
TransactionInput input = new TransactionInput(params, bytes, cursor);
TransactionInput input = new TransactionInput(params, this, bytes, cursor);
inputs.add(input);
cursor += input.getMessageSize();
}
@ -300,7 +300,7 @@ public class Transaction extends Message implements Serializable {
* accepted by the network.
*/
public void addInput(TransactionOutput from) {
inputs.add(new TransactionInput(params, from));
inputs.add(new TransactionInput(params, this, from));
}
/**

View File

@ -44,27 +44,33 @@ public class TransactionInput extends Message implements Serializable {
// The Script object obtained from parsing scriptBytes. Only filled in on demand and if the transaction is not
// coinbase.
transient private Script scriptSig;
// A pointer to the transaction that owns this input.
Transaction parentTransaction;
/** Used only in creation of the genesis block. */
TransactionInput(NetworkParameters params, byte[] scriptBytes) {
TransactionInput(NetworkParameters params, Transaction parentTransaction, byte[] scriptBytes) {
super(params);
this.scriptBytes = scriptBytes;
this.outpoint = new TransactionOutPoint(params, -1, null);
this.sequence = 0xFFFFFFFFL;
this.parentTransaction = parentTransaction;
}
/** Creates an UNSIGNED input that links to the given output */
TransactionInput(NetworkParameters params, TransactionOutput output) {
TransactionInput(NetworkParameters params, Transaction parentTransaction, TransactionOutput output) {
super(params);
long outputIndex = output.getIndex();
outpoint = new TransactionOutPoint(params, outputIndex, output.parentTransaction);
scriptBytes = EMPTY_ARRAY;
this.sequence = 0xFFFFFFFFL;
sequence = 0xFFFFFFFFL;
this.parentTransaction = parentTransaction;
}
/** Deserializes an input message. This is usually part of a transaction message. */
public TransactionInput(NetworkParameters params, byte[] payload, int offset) throws ProtocolException {
public TransactionInput(NetworkParameters params, Transaction parentTransaction,
byte[] payload, int offset) throws ProtocolException {
super(params, payload, offset);
this.parentTransaction = parentTransaction;
}
void parse() throws ProtocolException {

View File

@ -156,4 +156,9 @@ public class TransactionOutput extends Message implements Serializable {
throw new RuntimeException(e);
}
}
/** Returns the connected input. */
TransactionInput getSpentBy() {
return spentBy;
}
}

View File

@ -316,7 +316,13 @@ public class Wallet implements Serializable {
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
// Double spend! This must have overridden a pending tx, or the block is bad (contains transactions
// that illegally double spend: should never occur if we are connected to an honest node).
Transaction connected = input.outpoint.fromTx;
//
// Work backwards like so:
//
// A -> spent by B [pending]
// \-> spent by C [chain]
Transaction doubleSpent = input.outpoint.fromTx; // == A
Transaction connected = doubleSpent.outputs.get((int)input.outpoint.index).getSpentBy().parentTransaction;
if (pending.containsKey(connected.getHash())) {
log.info("Saw double spend from chain override pending tx {}", connected.getHashAsString());
log.info(" <-pending ->dead");

View File

@ -208,4 +208,43 @@ public class WalletTest {
Transaction send2 = new Transaction(params, send1.bitcoinSerialize());
assertEquals(nanos, send2.getValueSentFromMe(wallet));
}
@Test
public void testFinneyAttack() throws Exception {
// A Finney attack is where a miner includes a transaction spending coins to themselves but does not
// broadcast it. When they find a solved block, they hold it back temporarily whilst they buy something with
// those same coins. After purchasing, they broadcast the block thus reversing the transaction. It can be
// done by any miner for products that can be bought at a chosen time and very quickly (as every second you
// withold your block means somebody else might find it first, invalidating your work).
//
// Test that we handle ourselves performing the attack correctly: a double spend on the chain moves
// transactions from pending to dead.
//
// Note that the other way around, where a pending transaction sending us coins becomes dead,
// isn't tested because today BitCoinJ only learns about such transactions when they appear in the chain.
final Transaction[] eventDead = new Transaction[1];
final Transaction[] eventReplacement = new Transaction[1];
wallet.addEventListener(new WalletEventListener() {
@Override
public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) {
eventDead[0] = deadTx;
eventReplacement[0] = replacementTx;
}
});
// Receive 1 BTC.
BigInteger nanos = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(nanos, myAddress);
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
// Create a send to a merchant.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
// Create a double spend.
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
// Broadcast send1.
wallet.confirmSend(send1);
// Receive a block that overrides it.
wallet.receive(send2, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(send1, eventDead[0]);
assertEquals(send2, eventReplacement[0]);
}
}