diff --git a/src/com/google/bitcoin/core/Block.java b/src/com/google/bitcoin/core/Block.java index d517f015..cda2b335 100644 --- a/src/com/google/bitcoin/core/Block.java +++ b/src/com/google/bitcoin/core/Block.java @@ -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 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); } diff --git a/src/com/google/bitcoin/core/NetworkParameters.java b/src/com/google/bitcoin/core/NetworkParameters.java index 42d47e0a..4a0ac72b 100644 --- a/src/com/google/bitcoin/core/NetworkParameters.java +++ b/src/com/google/bitcoin/core/NetworkParameters.java @@ -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")); diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index cd8e9413..a5093157 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -219,7 +219,7 @@ public class Transaction extends Message implements Serializable { long numInputs = readVarInt(); inputs = new ArrayList((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)); } /** diff --git a/src/com/google/bitcoin/core/TransactionInput.java b/src/com/google/bitcoin/core/TransactionInput.java index 04be2d71..046a3a09 100644 --- a/src/com/google/bitcoin/core/TransactionInput.java +++ b/src/com/google/bitcoin/core/TransactionInput.java @@ -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 { diff --git a/src/com/google/bitcoin/core/TransactionOutput.java b/src/com/google/bitcoin/core/TransactionOutput.java index dbfec384..f0a38587 100644 --- a/src/com/google/bitcoin/core/TransactionOutput.java +++ b/src/com/google/bitcoin/core/TransactionOutput.java @@ -156,4 +156,9 @@ public class TransactionOutput extends Message implements Serializable { throw new RuntimeException(e); } } + + /** Returns the connected input. */ + TransactionInput getSpentBy() { + return spentBy; + } } \ No newline at end of file diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index 86f0dab2..8df2c82a 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -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"); diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 8d4ca994..dd925392 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -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]); + } }