diff --git a/src/com/google/bitcoin/core/Block.java b/src/com/google/bitcoin/core/Block.java index 6ac534ce..d517f015 100644 --- a/src/com/google/bitcoin/core/Block.java +++ b/src/com/google/bitcoin/core/Block.java @@ -457,7 +457,7 @@ public class Block extends Message { // 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.outputs.add(new TransactionOutput(params, Utils.toNanoCoins(50, 0), to, coinbase)); + coinbase.outputs.add(new TransactionOutput(params, coinbase, Utils.toNanoCoins(50, 0), to)); transactions.add(coinbase); } diff --git a/src/com/google/bitcoin/core/BlockChain.java b/src/com/google/bitcoin/core/BlockChain.java index 201c65a5..303df0dd 100644 --- a/src/com/google/bitcoin/core/BlockChain.java +++ b/src/com/google/bitcoin/core/BlockChain.java @@ -195,8 +195,8 @@ public class BlockChain { log.info("New chain head: {}", newChainHead.getHeader().getHashAsString()); log.info("Split at block: {}", splitPoint.getHeader().getHashAsString()); // Then build a list of all blocks in the old part of the chain and the new part. - Set oldBlocks = getPartialChain(chainHead, splitPoint); - Set newBlocks = getPartialChain(newChainHead, splitPoint); + List oldBlocks = getPartialChain(chainHead, splitPoint); + List newBlocks = getPartialChain(newChainHead, splitPoint); // Now inform the wallet. This is necessary so the set of currently active transactions (that we can spend) // can be updated to take into account the re-organize. We might also have received new coins we didn't have // before and our previous spends might have been undone. @@ -208,9 +208,9 @@ public class BlockChain { /** * Returns the set of contiguous blocks between 'higher' and 'lower'. Higher is included, lower is not. */ - private Set getPartialChain(StoredBlock higher, StoredBlock lower) throws BlockStoreException { + private List getPartialChain(StoredBlock higher, StoredBlock lower) throws BlockStoreException { assert higher.getHeight() > lower.getHeight(); - Set results = new HashSet(); + LinkedList results = new LinkedList(); StoredBlock cursor = higher; while (true) { results.add(cursor); diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 86dc7157..cd8e9413 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -117,7 +117,7 @@ public class Transaction extends Message implements Serializable { BigInteger v = BigInteger.ZERO; for (TransactionOutput o : outputs) { if (!o.isMine(wallet)) continue; - if (!includeSpent && o.isSpent) continue; + if (!includeSpent && !o.isAvailableForSpending()) continue; v = v.add(o.getValue()); } return v; @@ -161,17 +161,44 @@ public class Transaction extends Message implements Serializable { // This is tested in WalletTest. BigInteger v = BigInteger.ZERO; for (TransactionInput input : inputs) { - boolean connected = input.outpoint.connect(wallet.unspent.values()) || - input.outpoint.connect(wallet.spent.values()); - if (connected) { - // This input is taking value from an transaction in our wallet. To discover the value, - // we must find the connected transaction. - v = v.add(input.outpoint.getConnectedOutput().getValue()); - } + // This input is taking value from an transaction in our wallet. To discover the value, + // we must find the connected transaction. + TransactionOutput connected = input.getConnectedOutput(wallet.unspent); + if (connected == null) + connected = input.getConnectedOutput(wallet.spent); + if (connected == null) + connected = input.getConnectedOutput(wallet.pending); + if (connected == null) + continue; + v = v.add(connected.getValue()); } return v; } + boolean disconnectInputs() { + boolean disconnected = false; + for (TransactionInput input : inputs) { + disconnected |= input.disconnect(); + } + return disconnected; + } + + /** + * Connects all inputs using the provided transactions. If any input cannot be connected returns that input or + * null on success. + */ + TransactionInput connectInputs(Map transactions, boolean disconnect) { + for (TransactionInput input : inputs) { + // Coinbase transactions, by definition, do not have connectable inputs. + if (input.isCoinBase()) continue; + if (input.connect(transactions, disconnect) != TransactionInput.ConnectionResult.SUCCESS) { + // Could not connect this input, so return it and abort. + return input; + } + } + return null; + } + /** * These constants are a part of a scriptSig signature on the inputs. They define the details of how a * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated. @@ -225,6 +252,9 @@ public class Transaction extends Message implements Serializable { */ public String toString() { StringBuffer s = new StringBuffer(); + s.append(" "); + s.append(getHashAsString()); + s.append("\n"); if (isCoinBase()) { String script = "???"; String script2 = "???"; @@ -323,7 +353,6 @@ public class Transaction extends Message implements Serializable { // The anyoneCanPay feature isn't used at the moment. boolean anyoneCanPay = false; byte[] hash = hashTransactionForSignature(hashType, anyoneCanPay); - log.info(" signInputs hash={}", Utils.bytesToHexString(hash)); // Set the script to empty again for the next input. input.scriptBytes = TransactionInput.EMPTY_ARRAY; diff --git a/src/com/google/bitcoin/core/TransactionInput.java b/src/com/google/bitcoin/core/TransactionInput.java index ad080479..04be2d71 100644 --- a/src/com/google/bitcoin/core/TransactionInput.java +++ b/src/com/google/bitcoin/core/TransactionInput.java @@ -19,6 +19,7 @@ package com.google.bitcoin.core; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; +import java.util.Map; /** * A transfer of coins from one address to another creates a transaction in which the outputs @@ -27,11 +28,14 @@ import java.io.Serializable; * to the outputs of another. The exceptions are coinbase transactions, which create new coins. */ public class TransactionInput extends Message implements Serializable { - private static final long serialVersionUID = -7687665228438202968L; - // An apparently unused field intended for altering transactions after they were broadcast. - long sequence; - // The output of the transaction we're gathering coins from. + private static final long serialVersionUID = 2; + public static final byte[] EMPTY_ARRAY = new byte[0]; + // Allows for altering transactions after they were broadcast. Tx replacement is currently disabled in the C++ + // client so this is always the UINT_MAX. + // TODO: Document this in more detail and build features that use it. + long sequence; + // Data needed to connect to the output of the transaction we're gathering coins from. TransactionOutPoint outpoint; // The "script bytes" might not actually be a script. In coinbase transactions where new coins are minted there // is no input transaction, so instead the scriptBytes contains some extra stuff (like a rollover nonce) that we @@ -41,8 +45,6 @@ public class TransactionInput extends Message implements Serializable { // coinbase. transient private Script scriptSig; - static public final byte[] EMPTY_ARRAY = new byte[0]; - /** Used only in creation of the genesis block. */ TransactionInput(NetworkParameters params, byte[] scriptBytes) { super(params); @@ -52,7 +54,7 @@ public class TransactionInput extends Message implements Serializable { } /** Creates an UNSIGNED input that links to the given output */ - TransactionInput(NetworkParameters params, TransactionOutput output) { + TransactionInput(NetworkParameters params, TransactionOutput output) { super(params); long outputIndex = output.getIndex(); outpoint = new TransactionOutPoint(params, outputIndex, output.parentTransaction); @@ -124,4 +126,62 @@ public class TransactionInput extends Message implements Serializable { throw new RuntimeException(e); } } + + enum ConnectionResult { + NO_SUCH_TX, + ALREADY_SPENT, + SUCCESS + } + + // TODO: Clean all this up once TransactionOutPoint disappears. + + /** + * Locates the referenced output from the given pool of transactions. + * @return The TransactionOutput or null if the transactions map doesn't contain the referenced tx. + */ + TransactionOutput getConnectedOutput(Map transactions) { + Sha256Hash h = new Sha256Hash(outpoint.hash); + Transaction tx = transactions.get(h); + if (tx == null) + return null; + TransactionOutput out = tx.outputs.get((int)outpoint.index); + return out; + } + + /** + * Connects this input to the relevant output of the referenced transaction if it's in the given map. + * Connecting means updating the internal pointers and spent flags. + * + * @param transactions Map of txhash->transaction. + * @param disconnect Whether to abort if there's a pre-existing connection or not. + * @return true if connection took place, false if the referenced transaction was not in the list. + */ + ConnectionResult connect(Map transactions, boolean disconnect) { + Sha256Hash h = new Sha256Hash(outpoint.hash); + Transaction tx = transactions.get(h); + if (tx == null) + return TransactionInput.ConnectionResult.NO_SUCH_TX; + TransactionOutput out = tx.outputs.get((int)outpoint.index); + if (!out.isAvailableForSpending()) { + if (disconnect) + out.markAsUnspent(); + else + return TransactionInput.ConnectionResult.ALREADY_SPENT; + } + outpoint.fromTx = tx; + out.markAsSpent(this); + return TransactionInput.ConnectionResult.SUCCESS; + } + + /** + * Release the connected output, making it spendable once again. + * + * @return true if the disconnection took place, false if it was not connected. + */ + boolean disconnect() { + if (outpoint.fromTx == null) return false; + outpoint.fromTx.outputs.get((int)outpoint.index).markAsUnspent(); + outpoint.fromTx = null; + return true; + } } diff --git a/src/com/google/bitcoin/core/TransactionOutPoint.java b/src/com/google/bitcoin/core/TransactionOutPoint.java index 3cd476f1..ac745179 100644 --- a/src/com/google/bitcoin/core/TransactionOutPoint.java +++ b/src/com/google/bitcoin/core/TransactionOutPoint.java @@ -19,8 +19,8 @@ package com.google.bitcoin.core; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; -import java.util.Arrays; -import java.util.Collection; + +// TODO: Fold this class into the TransactionInput class. It's not necessary. /** * This message is a reference or pointer to an output of a different transaction. @@ -33,7 +33,8 @@ public class TransactionOutPoint extends Message implements Serializable { /** Which output of that transaction we are talking about. */ long index; - // This is not part of bitcoin serialization. + // This is not part of Bitcoin serialization. It's included in Java serialization. + // It points to the connected transaction. Transaction fromTx; TransactionOutPoint(NetworkParameters params, long index, Transaction fromTx) { @@ -66,27 +67,12 @@ public class TransactionOutPoint extends Message implements Serializable { Utils.uint32ToByteStreamLE(index, stream); } - /** - * Scans the list for the transaction this outpoint refers to, and sets up the internal reference used by - * getConnectedOutput(). - * @return true if connection took place, false if the referenced transaction was not in the list. - */ - boolean connect(Collection transactions) { - for (Transaction tx : transactions) { - if (Arrays.equals(tx.getHash().hash, hash)) { - fromTx = tx; - return true; - } - } - return false; - } - /** * If this transaction was created using the explicit constructor rather than deserialized, * retrieves the connected output transaction. Asserts if there is no connected transaction. */ TransactionOutput getConnectedOutput() { - assert fromTx != null; + if (fromTx == null) return null; return fromTx.outputs.get((int)index); } diff --git a/src/com/google/bitcoin/core/TransactionOutput.java b/src/com/google/bitcoin/core/TransactionOutput.java index daf001b9..dbfec384 100644 --- a/src/com/google/bitcoin/core/TransactionOutput.java +++ b/src/com/google/bitcoin/core/TransactionOutput.java @@ -40,10 +40,12 @@ public class TransactionOutput extends Message implements Serializable { // The script bytes are parsed and turned into a Script on demand. private transient Script scriptPubKey; - // This field is Java serialized but not BitCoin serialized. It's used for tracking purposes in our wallet only. - // If this flag is set to true, it means we have spent this outputs value and it shouldn't be used again or - // counted towards our balance. - boolean isSpent; + // These fields are Java serialized but not BitCoin serialized. They are used for tracking purposes in our wallet + // only. If set to true, this output is counted towards our balance. If false and spentBy is null the tx output + // was owned by us and was sent to somebody else. If false and spentBy is true it means this output was owned by + // us and used in one of our own transactions (eg, because it is a change output). + private boolean availableForSpending; + private TransactionInput spentBy; // A reference to the transaction which holds this output. Transaction parentTransaction; @@ -53,13 +55,15 @@ public class TransactionOutput extends Message implements Serializable { int offset) throws ProtocolException { super(params, payload, offset); parentTransaction = parent; + availableForSpending = true; } - TransactionOutput(NetworkParameters params, BigInteger value, Address to, Transaction parent) { + TransactionOutput(NetworkParameters params, Transaction parent, BigInteger value, Address to) { super(params); this.value = value; this.scriptBytes = Script.createOutputScript(to); parentTransaction = parent; + availableForSpending = true; } /** Used only in creation of the genesis blocks and in unit tests. */ @@ -67,6 +71,7 @@ public class TransactionOutput extends Message implements Serializable { super(params); this.scriptBytes = scriptBytes; this.value = Utils.toNanoCoins(50, 0); + availableForSpending = true; } public Script getScriptPubKey() throws ScriptException { @@ -108,6 +113,25 @@ public class TransactionOutput extends Message implements Serializable { throw new RuntimeException("Output linked to wrong parent transaction?"); } + /** + * Sets this objects availableToSpend flag to false and the spentBy pointer to the given input. + * If the input is null, it means this output was signed over to somebody else rather than one of our own keys. + */ + void markAsSpent(TransactionInput input) { + assert availableForSpending; + availableForSpending = false; + spentBy = input; + } + + void markAsUnspent() { + availableForSpending = true; + spentBy = null; + } + + boolean isAvailableForSpending() { + return availableForSpending; + } + public byte[] getScriptBytes() { return scriptBytes; } diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index b025a6c8..8270ffe3 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -27,12 +27,11 @@ import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString; /** * A Wallet stores keys and a record of transactions that have not yet been spent. Thus, it is capable of - * providing transactions on demand that meet a given combined value. Once a transaction - * output is used, it is removed from the wallet as it is no longer available for spending.

+ * providing transactions on demand that meet a given combined value.

* - * The Wallet is read and written from disk, so be sure to follow the Java serialization - * versioning rules here. We use the built in Java serialization to avoid the need to - * pull in a potentially large (code-size) third party serialization library.

+ * The Wallet is read and written from disk, so be sure to follow the Java serialization versioning rules here. We + * use the built in Java serialization to avoid the need to pull in a potentially large (code-size) third party + * serialization library.

*/ public class Wallet implements Serializable { private static final Logger log = LoggerFactory.getLogger(Wallet.class); @@ -54,15 +53,15 @@ public class Wallet implements Serializable { // 5. Inbound tx is accepted into a side chain: // ->inactive // + // Whilst it's also 'pending' in some sense, in that miners will probably try and incorporate it into the + // best chain, we don't mark it as such here. It'll eventually show up after a re-org. + // // Re-orgs: // 1. Tx is present in old chain and not present in new chain - // <-unspent/spent ->inactive + // <-unspent/spent ->pending // // These newly inactive transactions will (if they are relevant to us) eventually come back via receive() - // as miners resurrect them and re-include into the new best chain. Until then we do NOT consider them - // pending as it's possible some of the transactions have become invalid (eg because the new chain contains - // a double spend). This could cause some confusing UI changes for the user but these events should be very - // rare. + // as miners resurrect them and re-include into the new best chain. // // 2. Tx is not present in old chain and is present in new chain // <-inactive and ->unspent/spent @@ -74,7 +73,8 @@ public class Wallet implements Serializable { // change outputs would not be considered spendable. /** - * Map of txhash->Transactions that have not made it into the best chain yet. These transactions inputs count as + * Map of txhash->Transactions that have not made it into the best chain yet. They are eligible to move there but + * are waiting for a miner to send a block on the best chain including them. These transactions inputs count as * spent for the purposes of calculating our balance but their outputs are not available for spending yet. This * means after a spend, our balance can actually go down temporarily before going up again! */ @@ -114,6 +114,14 @@ public class Wallet implements Serializable { */ private Map inactive; + /** + * A dead transaction is one that's been overridden by a double spend. Such a transaction is pending except it + * will never confirm and so should be presented to the user in some unique way - flashing red for example. This + * should nearly never happen in normal usage. Dead transactions can be "resurrected" by re-orgs just like any + * other. Dead transactions are not in the pending pool. + */ + private Map dead; + /** A list of public/private EC keys owned by this user. */ public final ArrayList keychain; @@ -132,6 +140,7 @@ public class Wallet implements Serializable { spent = new HashMap(); inactive = new HashMap(); pending = new HashMap(); + dead = new HashMap(); eventListeners = new ArrayList(); } @@ -194,6 +203,11 @@ public class Wallet implements Serializable { * block might change which chain is best causing a reorganize. A re-org can totally change our balance! */ synchronized void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException { + receive(tx, block, blockType, false); + } + + private synchronized void receive(Transaction tx, StoredBlock block, + BlockChain.NewBlockType blockType, boolean reorg) throws VerificationException, ScriptException { // Runs in a peer thread. BigInteger prevBalance = getBalance(); @@ -206,8 +220,10 @@ public class Wallet implements Serializable { BigInteger valueSentToMe = tx.getValueSentToMe(this); BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe); - log.info("Wallet: Received tx" + (sideChain ? " on a side chain" :"") + " for " + - bitcoinValueToFriendlyString(valueDifference) + " BTC"); + if (!reorg) { + log.info("Received tx{} for {} BTC: {}", new Object[] { sideChain ? " on a side chain" : "", + bitcoinValueToFriendlyString(valueDifference), tx.getHashAsString()}); + } // If this transaction is already in the wallet we may need to move it into a different pool. At the very // least we need to ensure we're manipulating the canonical object rather than a duplicate. @@ -248,8 +264,10 @@ public class Wallet implements Serializable { pending.put(wtx.getHash(), wtx); } } else { - // Mark the tx as appearing in this block so we can find it later after a re-org. - tx.addBlockAppearance(block); + if (!reorg) { + // Mark the tx as appearing in this block so we can find it later after a re-org. + tx.addBlockAppearance(block); + } // This TX didn't originate with us. It could be sending us coins and also spending our own coins if keys // are being shared between different wallets. if (sideChain) { @@ -265,7 +283,7 @@ public class Wallet implements Serializable { // Inform anyone interested that we have new coins. Note: we may be re-entered by the event listener, // so we must not make assumptions about our state after this loop returns! For example, // the balance we just received might already be spent! - if (bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) { + if (!reorg && bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) { for (WalletEventListener l : eventListeners) { synchronized (l) { l.onCoinsReceived(this, tx, prevBalance, getBalance()); @@ -284,37 +302,58 @@ public class Wallet implements Serializable { updateForSpends(tx); if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) { // It's sending us coins. - log.info(" ->unspent"); + log.info(" new tx ->unspent"); boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null; assert !alreadyPresent : "TX was received twice"; } else { // It spent some of our coins and did not send us any. - log.info(" ->spent"); + log.info(" new tx ->spent"); boolean alreadyPresent = spent.put(tx.getHash(), tx) != null; assert !alreadyPresent : "TX was received twice"; } } /** - * Updates the wallet by checking if this TX spends any of our unspent outputs. This is not used normally because + * Updates the wallet by checking if this TX spends any of our outputs. This is not used normally because * when we receive our own spends, we've already marked the outputs as spent previously (during tx creation) so * there's no need to go through and do it again. */ private void updateForSpends(Transaction tx) throws VerificationException { for (TransactionInput input : tx.inputs) { - if (input.outpoint.connect(unspent.values())) { - TransactionOutput output = input.outpoint.getConnectedOutput(); - assert !output.isSpent : "Double spend accepted by the network?"; - log.info(" Saw some of my unspent outputs be spent by someone else who has my keys."); - log.info(" Total spent value is " + bitcoinValueToFriendlyString(output.getValue())); - output.isSpent = true; - Transaction connectedTx = input.outpoint.fromTx; - if (connectedTx.getValueSentToMe(this, false).equals(BigInteger.ZERO)) { + TransactionInput.ConnectionResult result = input.connect(unspent, false); + if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { + // Doesn't spend any of our outputs or is coinbase. + continue; + } 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; + if (pending.containsKey(connected.getHash())) { + log.info("Saw double spend from chain override pending tx {}", connected.getHashAsString()); + log.info(" <-pending ->dead"); + pending.remove(connected.getHash()); + dead.put(connected.getHash(), connected); + // Now forcibly change the connection. + input.connect(unspent, true); + // Inform the event listeners of the newly dead tx. + for (WalletEventListener listener : eventListeners) { + synchronized (listener) { + listener.onDeadTransaction(connected, tx); + } + } + } + } else if (result == TransactionInput.ConnectionResult.SUCCESS) { + // Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet. + // The outputs are already marked as spent by the connect call above, so check if there are any more for + // us to use. Move if not. + Transaction connected = input.outpoint.fromTx; + if (connected.getValueSentToMe(this, false).equals(BigInteger.ZERO)) { // There's nothing left I can spend in this transaction. - if (unspent.remove(connectedTx.getHash()) != null); + if (unspent.remove(connected.getHash()) != null) { log.info(" prevtx <-unspent"); - spent.put(connectedTx.getHash(), connectedTx); - log.info(" prevtx ->spent"); + log.info(" prevtx ->spent"); + spent.put(connected.getHash(), connected); + } } } } @@ -337,11 +376,11 @@ public class Wallet implements Serializable { */ synchronized void confirmSend(Transaction tx) { assert !pending.containsKey(tx) : "confirmSend called on the same transaction twice"; - // Mark each connected output of the tx as spent, so we don't try and spend it again. + log.info("confirmSend of {}", tx.getHashAsString()); + // Mark the outputs of the used transcations as spent, so we don't try and spend it again. for (TransactionInput input : tx.inputs) { TransactionOutput connectedOutput = input.outpoint.getConnectedOutput(); - assert !connectedOutput.isSpent : "createSend called before corresponding confirmSend"; - connectedOutput.isSpent = true; + connectedOutput.markAsSpent(input); } // Some of the outputs probably send coins back to us, eg for change or because this transaction is just // consolidating the wallet. Mark any output that is NOT back to us as spent. Then add this TX to the @@ -349,8 +388,7 @@ public class Wallet implements Serializable { for (TransactionOutput output : tx.outputs) { if (!output.isMine(this)) { // This output didn't go to us, so by definition it is now spent. - assert !output.isSpent; - output.isSpent = true; + output.markAsSpent(null); } } pending.put(tx.getHash(), tx); @@ -412,7 +450,7 @@ public class Wallet implements Serializable { List gathered = new LinkedList(); for (Transaction tx : unspent.values()) { for (TransactionOutput output : tx.outputs) { - if (output.isSpent) continue; + if (!output.isAvailableForSpending()) continue; if (!output.isMine(this)) continue; gathered.add(output); valueGathered = valueGathered.add(output.getValue()); @@ -428,14 +466,14 @@ public class Wallet implements Serializable { } assert gathered.size() > 0; Transaction sendTx = new Transaction(params); - sendTx.addOutput(new TransactionOutput(params, nanocoins, address, sendTx)); + sendTx.addOutput(new TransactionOutput(params, sendTx, nanocoins, address)); BigInteger change = valueGathered.subtract(nanocoins); if (change.compareTo(BigInteger.ZERO) > 0) { // The value of the inputs is greater than what we want to send. Just like in real life then, // we need to take back some coins ... this is called "change". Add another output that sends the change // back to us. log.info(" with " + bitcoinValueToFriendlyString(change) + " coins change"); - sendTx.addOutput(new TransactionOutput(params, change, changeAddress, sendTx)); + sendTx.addOutput(new TransactionOutput(params, sendTx, change, changeAddress)); } for (TransactionOutput output : gathered) { sendTx.addInput(output); @@ -449,6 +487,7 @@ public class Wallet implements Serializable { // happen, if it does it means the wallet has got into an inconsistent state. throw new RuntimeException(e); } + log.info(" created {}", sendTx.getHashAsString()); return sendTx; } @@ -494,18 +533,64 @@ public class Wallet implements Serializable { } /** - * Returns the balance of this wallet by summing up all unspent outputs that were sent to us. + * It's possible to calculate a wallets balance from multiple points of view. This enum selects which + * getBalance() should use.

+ * + * Consider a real-world example: you buy a snack costing $5 but you only have a $10 bill. At the start you have + * $10 viewed from every possible angle. After you order the snack you hand over your $10 bill. From the + * perspective of your wallet you have zero dollars (AVAILABLE). But you know in a few seconds the shopkeeper + * will give you back $5 change so most people in practice would say they have $5 (ESTIMATED).

+ */ + public enum BalanceType { + /** + * Balance calculated assuming all pending transactions are in fact included into the best chain by miners. + * This is the right balance to show in user interfaces. + */ + ESTIMATED, + + /** + * Balance that can be safely used to create new spends. This is all confirmed unspent outputs minus the ones + * spent by pending transactions, but not including the outputs of those pending transactions. + */ + AVAILABLE + }; + + /** + * Returns the AVAILABLE balance of this wallet. See {@link BalanceType#AVAILABLE} for details on what this + * means.

+ * + * Note: the estimated balance is usually the one you want to show to the end user - however attempting to + * actually spend these coins may result in temporary failure. This method returns how much you can safely + * provide to {@link Wallet#createSend(Address, java.math.BigInteger)}. */ public synchronized BigInteger getBalance() { - BigInteger balance = BigInteger.ZERO; + return getBalance(BalanceType.AVAILABLE); + } + + /** + * Returns the balance of this wallet as calculated by the provided balanceType. + */ + public synchronized BigInteger getBalance(BalanceType balanceType) { + BigInteger available = BigInteger.ZERO; for (Transaction tx : unspent.values()) { for (TransactionOutput output : tx.outputs) { - if (output.isSpent) continue; if (!output.isMine(this)) continue; - balance = balance.add(output.getValue()); + if (!output.isAvailableForSpending()) continue; + available = available.add(output.getValue()); } } - return balance; + if (balanceType == BalanceType.AVAILABLE) + return available; + assert balanceType == BalanceType.ESTIMATED; + // Now add back all the pending outputs to assume the transaction goes through. + BigInteger estimated = available; + for (Transaction tx : pending.values()) { + for (TransactionOutput output : tx.outputs) { + if (!output.isMine(this)) continue; + estimated = estimated.add(output.getValue()); + } + } + return estimated; } @Override @@ -534,67 +619,162 @@ public class Wallet implements Serializable { * Called by the {@link BlockChain} when the best chain (representing total work done) has changed. In this case, * we need to go through our transactions and find out if any have become invalid. It's possible for our balance * to go down in this case: money we thought we had can suddenly vanish if the rest of the network agrees it - * should be so. + * should be so.

+ * + * The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last. */ - synchronized void reorganize(Set oldBlocks, Set newBlocks) throws VerificationException { + synchronized void reorganize(List oldBlocks, List newBlocks) throws VerificationException { // This runs on any peer thread with the block chain synchronized. // - // The reorganize functionality of the wallet is tested in the BlockChainTest.testForking* methods. + // The reorganize functionality of the wallet is tested in ChainSplitTests. // // For each transaction we track which blocks they appeared in. Once a re-org takes place we have to find all // transactions in the old branch, all transactions in the new branch and find the difference of those sets. // // receive() has been called on the block that is triggering the re-org before this is called. - Set oldChainTransactions = new HashSet(); - Set newChainTransactions = new HashSet(); - Set all = new HashSet(); - all.addAll(unspent.values()); - all.addAll(spent.values()); - all.addAll(inactive.values()); - for (Transaction tx : all) { + log.info(" Old part of chain (top to bottom):"); + for (StoredBlock b : oldBlocks) log.info(" {}", b.getHeader().getHashAsString()); + log.info(" New part of chain (top to bottom):"); + for (StoredBlock b : newBlocks) log.info(" {}", b.getHeader().getHashAsString()); + + // Transactions that appear in the old chain segment. + Map oldChainTransactions = new HashMap(); + // Transactions that appear in the old chain segment and NOT the new chain segment. + Map onlyOldChainTransactions = new HashMap(); + // Transactions that appear in the new chain segment. + Map newChainTransactions = new HashMap(); + // Transactions that don't appear in either the new or the old section, ie, the shared trunk. + Map commonChainTransactions = new HashMap(); + + Map all = new HashMap(); + all.putAll(unspent); + all.putAll(spent); + all.putAll(inactive); + for (Transaction tx : all.values()) { Set appearsIn = tx.getAppearsIn(); assert appearsIn != null; // If the set of blocks this transaction appears in is disjoint with one of the chain segments it means // the transaction was never incorporated by a miner into that side of the chain. - if (!Collections.disjoint(appearsIn, oldBlocks)) { - boolean alreadyPresent = !oldChainTransactions.add(tx); - assert !alreadyPresent : "Transaction appears twice in chain segment"; - } - if (!Collections.disjoint(appearsIn, newBlocks)) { - boolean alreadyPresent = !newChainTransactions.add(tx); - assert !alreadyPresent : "Transaction appears twice in chain segment"; + boolean inOldSection = !Collections.disjoint(appearsIn, oldBlocks); + boolean inNewSection = !Collections.disjoint(appearsIn, newBlocks); + boolean inCommonSection = !inNewSection && !inOldSection; + + if (inCommonSection) { + boolean alreadyPresent = commonChainTransactions.put(tx.getHash(), tx) != null; + assert !alreadyPresent : "Transaction appears twice in common chain segment"; + } else { + if (inOldSection) { + boolean alreadyPresent = oldChainTransactions.put(tx.getHash(), tx) != null; + assert !alreadyPresent : "Transaction appears twice in old chain segment"; + if (!inNewSection) { + alreadyPresent = onlyOldChainTransactions.put(tx.getHash(), tx) != null; + assert !alreadyPresent : "Transaction appears twice in only-old map"; + } + } + if (inNewSection) { + boolean alreadyPresent = newChainTransactions.put(tx.getHash(), tx) != null; + assert !alreadyPresent : "Transaction appears twice in new chain segment"; + } } } - // If there is no difference it means we the user doesn't really care about this re-org but we still need to - // update the transaction block pointers for next time. + // If there is no difference it means we have nothing we need to do and the user does not care. boolean affectedUs = !oldChainTransactions.equals(newChainTransactions); log.info(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions"); if (!affectedUs) return; - // Transactions that were in the old chain but aren't in the new chain. These will become inactive. - Set gone = new HashSet(oldChainTransactions); - gone.removeAll(newChainTransactions); - // Transactions that are in the new chain but aren't in the old chain. These will be re-processed. - Set fresh = new HashSet(newChainTransactions); - fresh.removeAll(oldChainTransactions); - assert !(gone.isEmpty() && fresh.isEmpty()) : "There must have been some changes to get here"; + // For simplicity we will reprocess every transaction to ensure it's in the right bucket and has the right + // connections. Attempting to update each one with minimal work is possible but complex and was leading to + // edge cases that were hard to fix. As re-orgs are rare the amount of work this implies should be manageable + // unless the user has an enormous wallet. As an optimization fully spent transactions buried deeper than + // 1000 blocks could be put into yet another bucket which we never touch and assume re-orgs cannot affect. - for (Transaction tx : gone) { - log.info("tx not in new chain: <-unspent/spent ->inactive\n" + tx.toString()); - unspent.remove(tx.getHash()); - spent.remove(tx.getHash()); - inactive.put(tx.getHash(), tx); - // We do not put it into the pending pool. Pending is for transactions we know are valid. After a re-org - // some transactions may become permanently invalid if the new chain contains a double spend. We don't - // want transactions sitting in the pending pool forever. This means shortly after a re-org the balance - // might change rapidly as newly transactions are resurrected and included into the new chain by miners. + for (Transaction tx : onlyOldChainTransactions.values()) log.info(" Only Old: {}", tx.getHashAsString()); + for (Transaction tx : oldChainTransactions.values()) log.info(" Old: {}", tx.getHashAsString()); + for (Transaction tx : newChainTransactions.values()) log.info(" New: {}", tx.getHashAsString()); + + // Break all the existing connections. + for (Transaction tx : all.values()) + tx.disconnectInputs(); + for (Transaction tx : pending.values()) + tx.disconnectInputs(); + // Reconnect the transactions in the common part of the chain. + for (Transaction tx : commonChainTransactions.values()) { + TransactionInput badInput = tx.connectInputs(all, false); + assert badInput == null : "Failed to connect " + tx.getHashAsString() + ", " + badInput.toString(); } - for (Transaction tx : fresh) { - inactive.remove(tx.getHash()); - processTxFromBestChain(tx); + // Recalculate the unspent/spent buckets for the transactions the re-org did not affect. + unspent.clear(); + spent.clear(); + inactive.clear(); + for (Transaction tx : commonChainTransactions.values()) { + int unspentOutputs = 0; + for (TransactionOutput output : tx.outputs) { + if (output.isAvailableForSpending()) unspentOutputs++; + } + if (unspentOutputs > 0) { + log.info(" TX {}: ->unspent", tx.getHashAsString()); + unspent.put(tx.getHash(), tx); + } else { + log.info(" TX {}: ->spent", tx.getHashAsString()); + spent.put(tx.getHash(), tx); + } } + // Now replay the act of receiving the blocks that were previously in a side chain. This will: + // - Move any transactions that were pending and are now accepted into the right bucket. + // - Connect the newly active transactions. + Collections.reverse(newBlocks); // Need bottom-to-top but we get top-to-bottom. + for (StoredBlock b : newBlocks) { + log.info("Replaying block {}", b.getHeader().getHashAsString()); + Set txns = new HashSet(); + for (Transaction tx : newChainTransactions.values()) { + if (tx.appearsIn.contains(b)) { + txns.add(tx); + log.info(" containing tx {}", tx.getHashAsString()); + } + } + for (Transaction t : txns) { + try { + receive(t, b, BlockChain.NewBlockType.BEST_CHAIN, true); + } catch (ScriptException e) { + throw new RuntimeException(e); // Cannot happen as these blocks were already verified. + } + } + } + + // Find the transactions that didn't make it into the new chain yet. For each input, try to connect it to the + // transactions that are in {spent,unspent,pending}. Check the status of each input. For inactive + // transactions that only send us money, we put them into the inactive pool where they sit around waiting for + // another re-org or re-inclusion into the main chain. For inactive transactions where we spent money we must + // put them back into the pending pool if we can reconnect them, so we don't create a double spend whilst the + // network heals itself. + Map pool = new HashMap(); + pool.putAll(unspent); + pool.putAll(spent); + pool.putAll(pending); + Map toReprocess = new HashMap(); + toReprocess.putAll(onlyOldChainTransactions); + toReprocess.putAll(pending); + log.info("Reprocessing:"); + // Note, we must reprocess dead transactions first. The reason is that if there is a double spend across + // chains from our own coins we get a complicated situation: + // + // 1) We switch to a new chain (B) that contains a double spend overriding a pending transaction. It goes dead. + // 2) We switch BACK to the first chain (A). The dead transaction must go pending again. + // 3) We resurrect the transactions that were in chain (B) and assume the miners will start work on putting them + // in to the chain, but it's not possible because it's a double spend. So now that transaction must become + // dead instead of pending. + // + // This only occurs when we are double spending our own coins. + for (Transaction tx : dead.values()) { + reprocessTxAfterReorg(pool, tx); + } + for (Transaction tx : toReprocess.values()) { + reprocessTxAfterReorg(pool, tx); + } + + log.info("post-reorg balance is {}", Utils.bitcoinValueToFriendlyString(getBalance())); // Inform event listeners that a re-org took place. for (WalletEventListener l : eventListeners) { @@ -606,6 +786,51 @@ public class Wallet implements Serializable { } } + private void reprocessTxAfterReorg(Map pool, Transaction tx) { + log.info(" TX {}", tx.getHashAsString()); + int numInputs = tx.inputs.size(); + int noSuchTx = 0; + int success = 0; + boolean isDead = false; + for (TransactionInput input : tx.inputs) { + if (input.isCoinBase()) { + // Input is not in our wallet so there is "no such input tx", bit of an abuse. + noSuchTx++; + continue; + } + TransactionInput.ConnectionResult result = input.connect(pool, false); + if (result == TransactionInput.ConnectionResult.SUCCESS) { + success++; + } else if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { + noSuchTx++; + } else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { + isDead = true; + // This transaction was replaced by a double spend on the new chain. Did you just reverse + // your own transaction? I hope not!! + log.info(" ->dead, will not confirm now unless there's another re-org", + tx.getHashAsString()); + dead.put(tx.getHash(), tx); + // Inform the event listeners of the newly dead tx. + for (WalletEventListener listener : eventListeners) { + synchronized (listener) { + listener.onDeadTransaction(input.outpoint.fromTx, tx); + } + } + break; + } + } + if (isDead) return; + + if (noSuchTx == numInputs) { + log.info(" ->inactive", tx.getHashAsString()); + inactive.put(tx.getHash(), tx); + } else if (success == numInputs - noSuchTx) { + // All inputs are either valid for spending or don't come from us. Miners are trying to reinclude it. + log.info(" ->pending", tx.getHashAsString()); + pending.put(tx.getHash(), tx); + } + } + /** * Returns an immutable view of the transactions currently waiting for network confirmations. */ diff --git a/src/com/google/bitcoin/core/WalletEventListener.java b/src/com/google/bitcoin/core/WalletEventListener.java index c790a956..668eb57c 100644 --- a/src/com/google/bitcoin/core/WalletEventListener.java +++ b/src/com/google/bitcoin/core/WalletEventListener.java @@ -1,7 +1,25 @@ +/** + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.bitcoin.core; import java.math.BigInteger; +// TODO: Make this be an interface with a convenience abstract impl. + /** * Implementing a subclass WalletEventListener allows you to learn when the contents of the wallet changes due to * receiving money or a block chain re-organize. Methods are called with the event listener object locked so your @@ -36,4 +54,20 @@ public abstract class WalletEventListener { */ public void onReorganize() { } + + + /** + * This is called on a Peer thread when a transaction becomes dead. A dead transaction is one that has + * been overridden by a double spend from the network and so will never confirm no matter how long you wait.

+ * + * A dead transaction can occur if somebody is attacking the network, or by accident if keys are being shared. + * You can use this event handler to inform the user of the situation. A dead spend will show up in the BitCoin + * C++ client of the recipient as 0/unconfirmed forever, so if it was used to purchase something, + * the user needs to know their goods will never arrive. + * + * @param deadTx The transaction that is newly dead. + * @param replacementTx The transaction that killed it. + */ + public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) { + } } diff --git a/tests/com/google/bitcoin/core/BlockChainTest.java b/tests/com/google/bitcoin/core/BlockChainTest.java index 878f3bdf..70c8ba1a 100644 --- a/tests/com/google/bitcoin/core/BlockChainTest.java +++ b/tests/com/google/bitcoin/core/BlockChainTest.java @@ -24,10 +24,8 @@ import java.math.BigInteger; import static org.junit.Assert.*; -// Tests still to write: -// - Fragmented chains can be joined together. -// - Longest testNetChain is selected based on total difficulty not length. -// - Many more ... +// NOTE: Handling of chain splits/reorgs are in ChainSplitTests. + public class BlockChainTest { private static final NetworkParameters testNet = NetworkParameters.testNet(); private BlockChain testNetChain; @@ -88,140 +86,6 @@ public class BlockChainTest { assertEquals(chain.getChainHead().getHeader(), b3.cloneAsHeader()); } - @Test - public void testForking1() throws Exception { - // Check that if the block chain forks, we end up using the right chain. Only tests inbound transactions - // (receiving coins). Checking that we understand reversed spends is in testForking2. - - // TODO: Change this test to not use coinbase transactions as they are special (maturity rules). - final boolean[] reorgHappened = new boolean[1]; - reorgHappened[0] = false; - wallet.addEventListener(new WalletEventListener() { - @Override - public void onReorganize() { - reorgHappened[0] = true; - } - }); - - // Start by building a couple of blocks on top of the genesis block. - Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); - Block b2 = b1.createNextBlock(coinbaseTo); - assertTrue(chain.add(b1)); - assertTrue(chain.add(b2)); - assertFalse(reorgHappened[0]); - // We got two blocks which generated 50 coins each, to us. - assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - // We now have the following chain: - // genesis -> b1 -> b2 - // - // so fork like this: - // - // genesis -> b1 -> b2 - // \-> b3 - // - // Nothing should happen at this point. We saw b2 first so it takes priority. - Block b3 = b1.createNextBlock(someOtherGuy); - assertTrue(chain.add(b3)); - assertFalse(reorgHappened[0]); // No re-org took place. - assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - // Now we add another block to make the alternative chain longer. - assertTrue(chain.add(b3.createNextBlock(someOtherGuy))); - assertTrue(reorgHappened[0]); // Re-org took place. - reorgHappened[0] = false; - // - // genesis -> b1 -> b2 - // \-> b3 -> b4 - // - // We lost some coins! b2 is no longer a part of the best chain so our balance should drop to 50 again. - assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - // ... and back to the first chain. - Block b5 = b2.createNextBlock(coinbaseTo); - Block b6 = b5.createNextBlock(coinbaseTo); - assertTrue(chain.add(b5)); - assertTrue(chain.add(b6)); - // - // genesis -> b1 -> b2 -> b5 -> b6 - // \-> b3 -> b4 - // - assertTrue(reorgHappened[0]); - assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - } - - @Test - public void testForking2() throws Exception { - // Check that if the chain forks and new coins are received in the alternate chain our balance goes up. - Block b1 = unitTestParams.genesisBlock.createNextBlock(someOtherGuy); - Block b2 = b1.createNextBlock(someOtherGuy); - assertTrue(chain.add(b1)); - assertTrue(chain.add(b2)); - // genesis -> b1 -> b2 - // \-> b3 -> b4 - assertEquals(BigInteger.ZERO, wallet.getBalance()); - Block b3 = b1.createNextBlock(coinbaseTo); - Block b4 = b3.createNextBlock(someOtherGuy); - assertTrue(chain.add(b3)); - assertTrue(chain.add(b4)); - assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - } - - @Test - public void testForking3() throws Exception { - // Check that we can handle our own spends being rolled back by a fork. - Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); - chain.add(b1); - assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - Address dest = new ECKey().toAddress(unitTestParams); - Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(10, 0)); - wallet.confirmSend(spend); - // Waiting for confirmation ... - assertEquals(BigInteger.ZERO, wallet.getBalance()); - Block b2 = b1.createNextBlock(someOtherGuy); - b2.addTransaction(spend); - b2.solve(); - chain.add(b2); - assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance()); - // genesis -> b1 (receive coins) -> b2 (spend coins) - // \-> b3 -> b4 - Block b3 = b1.createNextBlock(someOtherGuy); - Block b4 = b3.createNextBlock(someOtherGuy); - chain.add(b3); - chain.add(b4); - // b4 causes a re-org that should make our spend go inactive. Because the inputs are already spent our balance - // drops to zero again. - assertEquals(BigInteger.ZERO, wallet.getBalance()); - // Not pending .... we don't know if our spend will EVER become active again (if there's an attack it may not). - assertEquals(0, wallet.getPendingTransactions().size()); - } - - @Test - public void testForking4() throws Exception { - // Check that we can handle external spends on an inactive chain becoming active. An external spend is where - // we see a transaction that spends our own coins but we did not broadcast it ourselves. This happens when - // keys are being shared between wallets. - Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); - chain.add(b1); - assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - Address dest = new ECKey().toAddress(unitTestParams); - Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(50, 0)); - // We do NOT confirm the spend here. That means it's not considered to be pending because createSend is - // stateless. For our purposes it is as if some other program with our keys created the tx. - // - // genesis -> b1 (receive 50) --> b2 - // \-> b3 (external spend) -> b4 - Block b2 = b1.createNextBlock(someOtherGuy); - chain.add(b2); - Block b3 = b1.createNextBlock(someOtherGuy); - b3.addTransaction(spend); - b3.solve(); - chain.add(b3); - // The external spend is not active yet. - assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance()); - Block b4 = b3.createNextBlock(someOtherGuy); - chain.add(b4); - // The external spend is now active. - assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance()); - } - @Test public void testDifficultyTransitions() throws Exception { // Add a bunch of blocks in a loop until we reach a difficulty transition point. The unit test params have an diff --git a/tests/com/google/bitcoin/core/ChainSplitTests.java b/tests/com/google/bitcoin/core/ChainSplitTests.java new file mode 100644 index 00000000..8c01c431 --- /dev/null +++ b/tests/com/google/bitcoin/core/ChainSplitTests.java @@ -0,0 +1,271 @@ +/** + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ChainSplitTests { + private NetworkParameters unitTestParams; + private Wallet wallet; + private BlockChain chain; + private Address coinbaseTo; + private Address someOtherGuy; + + @Before + public void setUp() { + unitTestParams = NetworkParameters.unitTests(); + wallet = new Wallet(unitTestParams); + wallet.addKey(new ECKey()); + chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams)); + coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams); + someOtherGuy = new ECKey().toAddress(unitTestParams); + } + + @Test + public void testForking1() throws Exception { + // Check that if the block chain forks, we end up using the right chain. Only tests inbound transactions + // (receiving coins). Checking that we understand reversed spends is in testForking2. + + // TODO: Change this test to not use coinbase transactions as they are special (maturity rules). + final boolean[] reorgHappened = new boolean[1]; + reorgHappened[0] = false; + wallet.addEventListener(new WalletEventListener() { + @Override + public void onReorganize() { + reorgHappened[0] = true; + } + }); + + // Start by building a couple of blocks on top of the genesis block. + Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); + Block b2 = b1.createNextBlock(coinbaseTo); + assertTrue(chain.add(b1)); + assertTrue(chain.add(b2)); + assertFalse(reorgHappened[0]); + // We got two blocks which generated 50 coins each, to us. + assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + // We now have the following chain: + // genesis -> b1 -> b2 + // + // so fork like this: + // + // genesis -> b1 -> b2 + // \-> b3 + // + // Nothing should happen at this point. We saw b2 first so it takes priority. + Block b3 = b1.createNextBlock(someOtherGuy); + assertTrue(chain.add(b3)); + assertFalse(reorgHappened[0]); // No re-org took place. + assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + // Now we add another block to make the alternative chain longer. + assertTrue(chain.add(b3.createNextBlock(someOtherGuy))); + assertTrue(reorgHappened[0]); // Re-org took place. + reorgHappened[0] = false; + // + // genesis -> b1 -> b2 + // \-> b3 -> b4 + // + // We lost some coins! b2 is no longer a part of the best chain so our available balance should drop to 50. + assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + // ... and back to the first chain. + Block b5 = b2.createNextBlock(coinbaseTo); + Block b6 = b5.createNextBlock(coinbaseTo); + assertTrue(chain.add(b5)); + assertTrue(chain.add(b6)); + // + // genesis -> b1 -> b2 -> b5 -> b6 + // \-> b3 -> b4 + // + assertTrue(reorgHappened[0]); + assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + } + + @Test + public void testForking2() throws Exception { + // Check that if the chain forks and new coins are received in the alternate chain our balance goes up + // after the re-org takes place. + Block b1 = unitTestParams.genesisBlock.createNextBlock(someOtherGuy); + Block b2 = b1.createNextBlock(someOtherGuy); + assertTrue(chain.add(b1)); + assertTrue(chain.add(b2)); + // genesis -> b1 -> b2 + // \-> b3 -> b4 + assertEquals(BigInteger.ZERO, wallet.getBalance()); + Block b3 = b1.createNextBlock(coinbaseTo); + Block b4 = b3.createNextBlock(someOtherGuy); + assertTrue(chain.add(b3)); + assertEquals(BigInteger.ZERO, wallet.getBalance()); + assertTrue(chain.add(b4)); + assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + } + + @Test + public void testForking3() throws Exception { + // Check that we can handle our own spends being rolled back by a fork. + Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); + chain.add(b1); + assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + Address dest = new ECKey().toAddress(unitTestParams); + Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(10, 0)); + wallet.confirmSend(spend); + // Waiting for confirmation ... + assertEquals(BigInteger.ZERO, wallet.getBalance()); + Block b2 = b1.createNextBlock(someOtherGuy); + b2.addTransaction(spend); + b2.solve(); + chain.add(b2); + assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance()); + // genesis -> b1 (receive coins) -> b2 (spend coins) + // \-> b3 -> b4 + Block b3 = b1.createNextBlock(someOtherGuy); + Block b4 = b3.createNextBlock(someOtherGuy); + chain.add(b3); + chain.add(b4); + // b4 causes a re-org that should make our spend go inactive. Because the inputs are already spent our + // available balance drops to zero again. + assertEquals(BigInteger.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); + // We estimate that it'll make it back into the block chain (we know we won't double spend). + // assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + } + + @Test + public void testForking4() throws Exception { + // Check that we can handle external spends on an inactive chain becoming active. An external spend is where + // we see a transaction that spends our own coins but we did not broadcast it ourselves. This happens when + // keys are being shared between wallets. + Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); + chain.add(b1); + assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); + Address dest = new ECKey().toAddress(unitTestParams); + Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(50, 0)); + // We do NOT confirm the spend here. That means it's not considered to be pending because createSend is + // stateless. For our purposes it is as if some other program with our keys created the tx. + // + // genesis -> b1 (receive 50) --> b2 + // \-> b3 (external spend) -> b4 + Block b2 = b1.createNextBlock(someOtherGuy); + chain.add(b2); + Block b3 = b1.createNextBlock(someOtherGuy); + b3.addTransaction(spend); + b3.solve(); + chain.add(b3); + // The external spend is not active yet. + assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance()); + Block b4 = b3.createNextBlock(someOtherGuy); + chain.add(b4); + // The external spend is now active. + assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance()); + } + + @Test + public void testDoubleSpendOnFork() throws Exception { + // Check what happens when a re-org happens and one of our confirmed transactions becomes invalidated by a + // double spend on the new best chain. + + final boolean[] eventCalled = new boolean[1]; + wallet.addEventListener(new WalletEventListener() { + @Override + public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) { + eventCalled[0] = true; + } + }); + + Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); + chain.add(b1); + + Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0)); + Address yetAnotherGuy = new ECKey().toAddress(unitTestParams); + Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0)); + wallet.confirmSend(t1); + // Receive t1 as confirmed by the network. + Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams)); + b2.addTransaction(t1); + b2.solve(); + chain.add(b2); + + // Now we make a double spend become active after a re-org. + Block b3 = b1.createNextBlock(new ECKey().toAddress(unitTestParams)); + b3.addTransaction(t2); + b3.solve(); + chain.add(b3); // Side chain. + Block b4 = b3.createNextBlock(new ECKey().toAddress(unitTestParams)); + chain.add(b4); // New best chain. + + // Should have seen a double spend. + assertTrue(eventCalled[0]); + assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance()); + } + + @Test + public void testDoubleSpendOnForkPending() throws Exception { + // Check what happens when a re-org happens and one of our UNconfirmed transactions becomes invalidated by a + // double spend on the new best chain. + + final boolean[] eventCalled = new boolean[1]; + wallet.addEventListener(new WalletEventListener() { + @Override + public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) { + eventCalled[0] = true; + } + }); + + // Start with 50 coins. + Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo); + chain.add(b1); + + Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0)); + Address yetAnotherGuy = new ECKey().toAddress(unitTestParams); + Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0)); + wallet.confirmSend(t1); + // t1 is still pending ... + Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams)); + chain.add(b2); + assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance()); + assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + + // Now we make a double spend become active after a re-org. + // genesis -> b1 -> b2 [t1 pending] + // \-> b3 (t2) -> b4 + Block b3 = b1.createNextBlock(new ECKey().toAddress(unitTestParams)); + b3.addTransaction(t2); + b3.solve(); + chain.add(b3); // Side chain. + Block b4 = b3.createNextBlock(new ECKey().toAddress(unitTestParams)); + chain.add(b4); // New best chain. + + // Should have seen a double spend against the pending pool. + assertTrue(eventCalled[0]); + assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance()); + + // ... and back to our own parallel universe. + Block b5 = b2.createNextBlock(new ECKey().toAddress(unitTestParams)); + chain.add(b5); + Block b6 = b5.createNextBlock(new ECKey().toAddress(unitTestParams)); + chain.add(b6); + // genesis -> b1 -> b2 -> b5 -> b6 [t1 pending] + // \-> b3 [t2 inactive] -> b4 + assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance()); + assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + } +} diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 21e5e07b..8d4ca994 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -23,6 +23,7 @@ import java.math.BigInteger; import static com.google.bitcoin.core.Utils.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class WalletTest { @@ -41,16 +42,17 @@ public class WalletTest { blockStore = new MemoryBlockStore(params); } - private static byte fakeHashCounter = 0; private Transaction createFakeTx(BigInteger nanocoins, Address to) { Transaction t = new Transaction(params); - TransactionOutput o1 = new TransactionOutput(params, nanocoins, to, t); + TransactionOutput o1 = new TransactionOutput(params, t, nanocoins, to); t.addOutput(o1); - // t1 is not a valid transaction - it has no inputs. Nonetheless, if we set it up with a fake hash it'll be - // valid enough for these tests. - byte[] hash = new byte[32]; - hash[0] = fakeHashCounter++; - t.setFakeHashForTesting(new Sha256Hash(hash)); + // Make a previous tx simply to send us sufficient coins. This prev tx is not really valid but it doesn't + // matter for our purposes. + Transaction prevTx = new Transaction(params); + TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to); + prevTx.addOutput(prevOut); + // Connect it. + t.addInput(prevOut); return t; } @@ -152,8 +154,11 @@ public class WalletTest { Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3); wallet.confirmSend(spend); - // Balance should be 0.50 because the change output is pending confirmation by the network. - assertEquals(toNanoCoins(0, 50), wallet.getBalance()); + // Available and estimated balances should not be the same. We don't check the exact available balance here + // because it depends on the coin selection algorithm. + assertEquals(toNanoCoins(4, 50), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + assertFalse(wallet.getBalance(Wallet.BalanceType.AVAILABLE).equals( + wallet.getBalance(Wallet.BalanceType.ESTIMATED))); // Now confirm the transaction by including it into a block. StoredBlock b3 = createFakeBlock(spend).storedBlock; @@ -161,8 +166,7 @@ public class WalletTest { // Change is confirmed. We started with 5.50 so we should have 4.50 left. BigInteger v4 = toNanoCoins(4, 50); - assertEquals(bitcoinValueToFriendlyString(v4), - bitcoinValueToFriendlyString(wallet.getBalance())); + assertEquals(v4, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); } // Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an