diff --git a/src/com/google/bitcoin/core/Block.java b/src/com/google/bitcoin/core/Block.java index 2d6a20a9..4dbc734d 100644 --- a/src/com/google/bitcoin/core/Block.java +++ b/src/com/google/bitcoin/core/Block.java @@ -294,7 +294,7 @@ public class Block extends Message { ArrayList tree = new ArrayList(); // Start by adding all the hashes of the transactions as leaves of the tree. for (Transaction t : transactions) { - tree.add(t.getHash()); + tree.add(t.getHash().hash); } int j = 0; // Now step through each level ... @@ -453,7 +453,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.outputs.add(new TransactionOutput(params, Utils.toNanoCoins(50, 0), to, coinbase)); transactions.add(coinbase); } diff --git a/src/com/google/bitcoin/core/BlockChain.java b/src/com/google/bitcoin/core/BlockChain.java index b6299a69..539ad0b9 100644 --- a/src/com/google/bitcoin/core/BlockChain.java +++ b/src/com/google/bitcoin/core/BlockChain.java @@ -17,9 +17,7 @@ package com.google.bitcoin.core; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; +import java.util.*; import static com.google.bitcoin.core.Utils.LOG; @@ -105,23 +103,24 @@ public class BlockChain { private synchronized boolean add(Block block, boolean tryConnecting) throws BlockStoreException, VerificationException, ScriptException { + LOG("Adding block " + block.getHashAsString() + " to the chain"); + if (blockStore.get(block.getHash()) != null) { + LOG("Already have block"); + return true; + } + + // Prove the block is internally valid: hash is lower than target, merkle root is correct and so on. try { - // Prove the block is internally valid: hash is lower than target, merkle root is correct and so on. block.verify(); } catch (VerificationException e) { LOG("Failed to verify block: " + e.toString()); LOG(block.toString()); throw e; } - // Inform the wallet about transactions relevant to our keys, then throw away the transaction data. - extractRelevantTransactions(block); - assert block.transactions == null; - if (blockStore.get(block.getHash()) != null) { - LOG("Already have block"); - return true; - } + // Try linking it to a place in the currently known blocks. StoredBlock storedPrev = blockStore.get(block.getPrevBlockHash()); + if (storedPrev == null) { // We can't find the previous block. Probably we are still in the process of downloading the chain and a // block was solved whilst we were doing it. We put it to one side and try to connect it later when we @@ -131,8 +130,15 @@ public class BlockChain { return false; } else { // It connects to somewhere on the chain. Not necessarily the top of the best known chain. + // + // Create a new StoredBlock from this block. It will throw away the transaction data so when block goes + // out of scope we will reclaim the used memory. checkDifficultyTransitions(storedPrev, block); - connectAndStoreBlock(block, storedPrev); + StoredBlock newStoredBlock = storedPrev.build(block); + blockStore.put(newStoredBlock); + // block.transactions may be null here if we received only a header and not a full block. This does not + // happen currently but might in future if getheaders is implemented. + connectBlock(newStoredBlock, storedPrev, block.transactions); } if (tryConnecting) @@ -141,46 +147,129 @@ public class BlockChain { return true; } - private void connectAndStoreBlock(Block block, StoredBlock storedPrev) throws BlockStoreException, VerificationException { - StoredBlock newStoredBlock = storedPrev.build(block); - blockStore.put(newStoredBlock); + private void connectBlock(StoredBlock newStoredBlock, StoredBlock storedPrev, List newTransactions) + throws BlockStoreException, VerificationException { if (storedPrev.equals(chainHead)) { // This block connects to the best known block, it is a normal continuation of the system. setChainHead(newStoredBlock); - LOG("Received block " + block.getHashAsString() + ", chain is now " + chainHead.getHeight() + - " blocks high"); + LOG("Chain is now " + chainHead.getHeight() + " blocks high"); + if (newTransactions != null) + sendTransactionsToWallet(newStoredBlock, NewBlockType.BEST_CHAIN, newTransactions); } else { - // This block connects to somewhere other than the top of the chain. - if (newStoredBlock.moreWorkThan(chainHead)) { - // This chain has overtaken the one we currently believe is best. Reorganize is required. - wallet.reorganize(chainHead, newStoredBlock); - // Update the pointer to the best known block. - setChainHead(newStoredBlock); + // This block connects to somewhere other than the top of the best known chain. We treat these differently. + // + // Note that we send the transactions to the wallet FIRST, even if we're about to re-organize this block + // to become the new best chain head. This simplifies handling of the re-org in the Wallet class. + boolean causedSplit = newStoredBlock.moreWorkThan(chainHead); + if (causedSplit) { + LOG("Block is causing a re-organize"); } else { - LOG("Received a block which forks the chain, but it did not cause a reorganize."); + LOG("Block forks the chain, but it did not cause a reorganize."); } + + // We may not have any transactions if we received only a header. That never happens today but will in + // future when getheaders is used as an optimization. + if (newTransactions != null) { + sendTransactionsToWallet(newStoredBlock, NewBlockType.SIDE_CHAIN, newTransactions); + } + + if (causedSplit) + handleChainSplit(newStoredBlock); } } - private void extractRelevantTransactions(Block block) throws VerificationException { - // If this block is a full block, scan, otherwise it's just headers (eg from getheaders or a unit test). - if (block.transactions != null) { - // Scan the transactions to find out if any sent money to us. We don't care about the rest. - // TODO: We should also scan to see if any of our own keys sent money to somebody else and became spent. - for (Transaction tx : block.transactions) { - try { - scanTransaction(tx); - } catch (ScriptException e) { - // We don't want scripts we don't understand to break the block chain, - // so just note that this tx was not scanned here and continue. - LOG("Failed to parse a script: " + e.toString()); - } + /** + * Called as part of connecting a block when the new block results in a different chain having higher total work. + */ + private void handleChainSplit(StoredBlock newChainHead) throws BlockStoreException, VerificationException { + // This chain has overtaken the one we currently believe is best. Reorganize is required. + // + // Firstly, calculate the block at which the chain diverged. We only need to examine the + // chain from beyond this block to find differences. + StoredBlock splitPoint = findSplit(newChainHead, chainHead); + LOG("Re-organize after split at height " + splitPoint.getHeight()); + LOG("Old chain head: " + chainHead.getHeader().getHashAsString()); + LOG("New chain head: " + newChainHead.getHeader().getHashAsString()); + LOG("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); + // 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. + wallet.reorganize(oldBlocks, newBlocks); + // Update the pointer to the best known block. + setChainHead(newChainHead); + } + + /** + * 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 { + assert higher.getHeight() > lower.getHeight(); + Set results = new HashSet(); + StoredBlock cursor = higher; + while (true) { + results.add(cursor); + cursor = cursor.getPrev(blockStore); + assert cursor != null : "Ran off the end of the chain"; + if (cursor.equals(lower)) break; + } + return results; + } + + /** + * Locates the point in the chain at which newStoredBlock and chainHead diverge. Returns null if no split point was + * found (ie they are part of the same chain). + */ + private StoredBlock findSplit(StoredBlock newChainHead, StoredBlock chainHead) throws BlockStoreException { + StoredBlock currentChainCursor = chainHead; + StoredBlock newChainCursor = newChainHead; + // Loop until we find the block both chains have in common. Example: + // + // A -> B -> C -> D + // \--> E -> F -> G + // + // findSplit will return block B. chainHead = D and newChainHead = G. + while (!currentChainCursor.equals(newChainCursor)) { + // Move the new chain cursor backwards until it is at the same height as the current chain head. + while (newChainCursor.getHeight() > currentChainCursor.getHeight()) { + newChainCursor = blockStore.get(newChainCursor.getHeader().getPrevBlockHash()); + // Stores contain the genesis block which has a height of zero. Thus we should always be able to find + // a block of equal height in this loop. If we fall off the end of the chain it means there is a bug + // in the library. + assert newChainCursor != null : "Attempt to follow an orphan chain"; + } + // We found a place where the chains have equal height. In the first iteration with the above example + // newChainCursor will be F. Did we find the fork yet? + if (newChainCursor.equals(currentChainCursor)) + break; + // No, we did not. First iteration D != F so move currentChainCursor backwards one and try again. + currentChainCursor = blockStore.get(currentChainCursor.getHeader().getPrevBlockHash()); + assert currentChainCursor != null : "Attempt to follow an orphan chain"; + // Eventually currentChainCursor will move from C to B, the inner while loop will move newChainCursor + // from E to B and we will exit. + } + return currentChainCursor; + } + + enum NewBlockType { + BEST_CHAIN, + SIDE_CHAIN + } + + private void sendTransactionsToWallet(StoredBlock block, NewBlockType blockType, + List newTransactions) throws VerificationException { + // Scan the transactions to find out if any mention addresses we own. + for (Transaction tx : newTransactions) { + try { + scanTransaction(block, tx, blockType); + } catch (ScriptException e) { + // We don't want scripts we don't understand to break the block chain, + // so just note that this tx was not scanned here and continue. + LOG("Failed to parse a script: " + e.toString()); } } - // Throw away the transactions. We have to do this because we can't hold all the transaction data for the - // production chain in memory at once. Because BitCoinJ implements client mode/simplified payment - // verification we don't store the transactions to disk or use them later anyway. - block.transactions = null; } private void setChainHead(StoredBlock chainHead) { @@ -280,41 +369,31 @@ public class BlockChain { receivedDifficulty.toString(16) + " vs " + newDifficulty.toString(16)); } - private void scanTransaction(Transaction tx) throws ScriptException, VerificationException { - for (TransactionOutput i : tx.outputs) { + private void scanTransaction(StoredBlock block, Transaction tx, NewBlockType blockType) + throws ScriptException, VerificationException { + boolean shouldReceive = false; + for (TransactionOutput output : tx.outputs) { // TODO: Handle more types of outputs, not just regular to address outputs. - if (i.getScriptPubKey().isSentToIP()) return; - byte[] pubKeyHash; - pubKeyHash = i.getScriptPubKey().getPubKeyHash(); - synchronized (wallet) { - for (ECKey key : wallet.keychain) { - if (Arrays.equals(pubKeyHash, key.getPubKeyHash())) { - // We found a transaction that sends us money. - if (!wallet.isTransactionPresent(tx)) { - wallet.receive(tx); - } - } + if (output.getScriptPubKey().isSentToIP()) return; + // This is not thread safe as a key could be removed between the call to isMine and receive. + if (output.isMine(wallet)) { + shouldReceive = true; + } + } + + // Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air). + if (!tx.isCoinBase()) { + for (TransactionInput i : tx.inputs) { + byte[] pubkey = i.getScriptSig().getPubKey(); + // This is not thread safe as a key could be removed between the call to isPubKeyMine and receive. + if (wallet.isPubKeyMine(pubkey)) { + shouldReceive = true; } } } - // Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air), - // so we can stop scanning at this point. - if (tx.isCoinBase()) return; - - for (TransactionInput i : tx.inputs) { - byte[] pubkey = i.getScriptSig().getPubKey(); - synchronized (wallet) { - for (ECKey key : wallet.keychain) { - if (Arrays.equals(pubkey, key.getPubKey())) { - // We found a transaction where we spent money. - if (wallet.isTransactionPresent(tx)) { - // TODO: Implement catching up with a set of pre-generated keys using the blockchain. - } - } - } - } - } + if (shouldReceive) + wallet.receive(tx, block, blockType); } /** diff --git a/src/com/google/bitcoin/core/StoredBlock.java b/src/com/google/bitcoin/core/StoredBlock.java index dbd23df5..8cb0af10 100644 --- a/src/com/google/bitcoin/core/StoredBlock.java +++ b/src/com/google/bitcoin/core/StoredBlock.java @@ -36,7 +36,6 @@ public class StoredBlock implements Serializable { private int height; public StoredBlock(Block header, BigInteger chainWork, int height) { - assert header.transactions == null : "Should not have transactions in a block header object"; this.header = header; this.chainWork = chainWork; this.height = height; @@ -93,6 +92,21 @@ public class StoredBlock implements Serializable { // the largest amount of work done not the tallest. BigInteger chainWork = this.chainWork.add(block.getWork()); int height = this.height + 1; - return new StoredBlock(block, chainWork, height); + return new StoredBlock(block.cloneAsHeader(), chainWork, height); + } + + /** + * Given a block store, looks up the previous block in this chain. Convenience method for doing + * store.get(this.getHeader().getPrevBlockHash()). + * + * @return the previous block in the chain or null if it was not found in the store. + */ + public StoredBlock getPrev(BlockStore store) throws BlockStoreException { + return store.get(getHeader().getPrevBlockHash()); + } + + @Override + public String toString() { + return "Block at height " + getHeight() + ": " + getHeader().toString(); } } diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 65bb678b..16e95d6e 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -21,10 +21,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import static com.google.bitcoin.core.Utils.*; @@ -46,14 +43,24 @@ public class Transaction extends Message implements Serializable { ArrayList outputs; long lockTime; + // This is only stored in Java serialization. It records which blocks (and their height + work) the transaction + // has been included in. For most transactions this set will have a single member. In the case of a chain split a + // transaction may appear in multiple blocks but only one of them is part of the best chain. It's not valid to + // have an identical transaction appear in two blocks in the same chain but this invariant is expensive to check, + // so it's not directly enforced anywhere. + // + // If this transaction is not stored in the wallet, appearsIn is null. + Set appearsIn; + // This is an in memory helper only. - transient byte[] hash; + transient Sha256Hash hash; Transaction(NetworkParameters params) { super(params); version = 1; inputs = new ArrayList(); outputs = new ArrayList(); + // We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet. } /** @@ -81,37 +88,64 @@ public class Transaction extends Message implements Serializable { /** * Returns the transaction hash as you see them in the block explorer. */ - public byte[] getHash() { + public Sha256Hash getHash() { if (hash == null) { byte[] bits = bitcoinSerialize(); - hash = reverseBytes(doubleDigest(bits)); + hash = new Sha256Hash(reverseBytes(doubleDigest(bits))); } return hash; } public String getHashAsString() { - return Utils.bytesToHexString(getHash()); + return getHash().toString(); } - void setFakeHashForTesting(byte[] hash) { + void setFakeHashForTesting(Sha256Hash hash) { this.hash = hash; } /** - * Calculates the sum of the outputs that are sending coins to a key in the wallet. - * @return sum in nanocoins + * Calculates the sum of the outputs that are sending coins to a key in the wallet. The flag controls whether to + * include spent outputs or not. */ - public BigInteger getValueSentToMe(Wallet wallet) { + BigInteger getValueSentToMe(Wallet wallet, boolean includeSpent) { // This is tested in WalletTest. BigInteger v = BigInteger.ZERO; for (TransactionOutput o : outputs) { - if (o.isMine(wallet)) { - v = v.add(o.getValue()); - } + if (!o.isMine(wallet)) continue; + if (!includeSpent && o.isSpent) continue; + v = v.add(o.getValue()); } return v; } + /** + * Calculates the sum of the outputs that are sending coins to a key in the wallet. + */ + public BigInteger getValueSentToMe(Wallet wallet) { + return getValueSentToMe(wallet, true); + } + + /** + * Returns a set of blocks which contain the transaction, or null if this transaction doesn't have that data + * because it's not stored in the wallet or because it has never appeared in a block. + */ + Set getAppearsIn() { + return appearsIn; + } + + /** + * Adds the given block to the internal serializable set of blocks in which this transaction appears. This is + * used by the wallet to ensure transactions that appear on side chains are recorded properly even though the + * block stores do not save the transaction data at all. + */ + void addBlockAppearance(StoredBlock block) { + if (appearsIn == null) { + appearsIn = new HashSet(); + } + appearsIn.add(block); + } + /** * Calculates the sum of the inputs that are spending coins with keys in the wallet. This requires the * transactions sending coins to those keys to be in the wallet. This method will not attempt to download the @@ -123,8 +157,8 @@ 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) || - input.outpoint.connect(wallet.fullySpent); + 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. @@ -138,10 +172,9 @@ public class Transaction extends Message implements Serializable { * 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. * - * Note: in the official client, this enum also has another flag, SIGHASH_ANYONECANPAY. In this implementation, - * that's kept separate. - * - * Also note: only SIGHASH_ALL is actually used in the official client today. + * In the official client, this enum also has another flag, SIGHASH_ANYONECANPAY. In this implementation, + * that's kept separate. Only SIGHASH_ALL is actually used in the official client today. The other flags + * exist to allow for distributed contracts. */ public enum SigHash { ALL, // 1 @@ -170,7 +203,7 @@ public class Transaction extends Message implements Serializable { lockTime = readUint32(); // Store a hash, it may come in useful later (want to avoid reserialization costs). - hash = reverseBytes(doubleDigest(bytes, offset, cursor - offset)); + hash = new Sha256Hash(reverseBytes(doubleDigest(bytes, offset, cursor - offset))); } /** @@ -315,7 +348,7 @@ public class Transaction extends Message implements Serializable { // Every input is now complete. } - private byte[] hashTransactionForSignature( SigHash type, boolean anyoneCanPay) { + private byte[] hashTransactionForSignature(SigHash type, boolean anyoneCanPay) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); bitcoinSerializeToStream(bos); @@ -377,13 +410,11 @@ public class Transaction extends Message implements Serializable { if (!(other instanceof Transaction)) return false; Transaction t = (Transaction) other; - byte[] hash1 = t.getHash(); - byte[] hash2 = getHash(); - return Arrays.equals(hash2, hash1); + return t.getHash().equals(getHash()); } @Override public int hashCode() { - return Arrays.hashCode(hash); + return Arrays.hashCode(getHash().hash); } } diff --git a/src/com/google/bitcoin/core/TransactionOutPoint.java b/src/com/google/bitcoin/core/TransactionOutPoint.java index a824beed..3cd476f1 100644 --- a/src/com/google/bitcoin/core/TransactionOutPoint.java +++ b/src/com/google/bitcoin/core/TransactionOutPoint.java @@ -20,7 +20,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.util.Arrays; -import java.util.List; +import java.util.Collection; /** * This message is a reference or pointer to an output of a different transaction. @@ -40,7 +40,7 @@ public class TransactionOutPoint extends Message implements Serializable { super(params); this.index = index; if (fromTx != null) { - this.hash = fromTx.getHash(); + this.hash = fromTx.getHash().hash; this.fromTx = fromTx; } else { // This happens when constructing the genesis block. @@ -71,9 +71,9 @@ public class TransactionOutPoint extends Message implements Serializable { * getConnectedOutput(). * @return true if connection took place, false if the referenced transaction was not in the list. */ - boolean connect(List transactions) { + boolean connect(Collection transactions) { for (Transaction tx : transactions) { - if (Arrays.equals(tx.getHash(), hash)) { + if (Arrays.equals(tx.getHash().hash, hash)) { fromTx = tx; return true; } @@ -84,7 +84,6 @@ public class TransactionOutPoint extends Message implements Serializable { /** * If this transaction was created using the explicit constructor rather than deserialized, * retrieves the connected output transaction. Asserts if there is no connected transaction. - * @return */ TransactionOutput getConnectedOutput() { assert fromTx != null; diff --git a/src/com/google/bitcoin/core/TransactionOutput.java b/src/com/google/bitcoin/core/TransactionOutput.java index 6c8ff42e..72455b94 100644 --- a/src/com/google/bitcoin/core/TransactionOutput.java +++ b/src/com/google/bitcoin/core/TransactionOutput.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.math.BigInteger; -import java.util.Arrays; /** * A TransactionOutput message contains a scriptPubKey that controls who is able to spend its value. It is a sub-part @@ -52,10 +51,11 @@ public class TransactionOutput extends Message implements Serializable { parentTransaction = parent; } - TransactionOutput(NetworkParameters params, BigInteger value, Address to) { + TransactionOutput(NetworkParameters params, BigInteger value, Address to, Transaction parent) { super(params); this.value = value; this.scriptBytes = Script.createOutputScript(to); + parentTransaction = parent; } /** Used only in creation of the genesis blocks and in unit tests. */ @@ -112,11 +112,7 @@ public class TransactionOutput extends Message implements Serializable { public boolean isMine(Wallet wallet) { try { byte[] pubkeyHash = getScriptPubKey().getPubKeyHash(); - for (ECKey key : wallet.keychain) { - if (Arrays.equals(key.getPubKeyHash(), pubkeyHash)) - return true; - } - return false; + return wallet.isPubKeyHashMine(pubkeyHash); } catch (ScriptException e) { throw new RuntimeException(e); } diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index 0a648bdb..c0140cc5 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -18,12 +18,10 @@ package com.google.bitcoin.core; import java.io.*; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import static com.google.bitcoin.core.Utils.LOG; +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 @@ -35,21 +33,83 @@ import static com.google.bitcoin.core.Utils.LOG; * pull in a potentially large (code-size) third party serialization library.

*/ public class Wallet implements Serializable { - private static final long serialVersionUID = -4501424466753895784L; + private static final long serialVersionUID = 2L; + + // Algorithm for movement of transactions between pools. Outbound tx = us spending coins. Inbound tx = us + // receiving coins. If a tx is both inbound and outbound (spend with change) it is considered outbound for the + // purposes of the explanation below. + // + // 1. Outbound tx is created by us: ->pending + // 2. Outbound tx that was broadcast is accepted into the main chain: + // <-pending and + // If there is a change output ->unspent + // If there is no change output ->spent + // 3. Outbound tx that was broadcast is accepted into a side chain: + // ->inactive (remains in pending). + // 4. Inbound tx is accepted into the best chain: + // ->unspent/spent + // 5. Inbound tx is accepted into a side chain: + // ->inactive + // + // Re-orgs: + // 1. Tx is present in old chain and not present in new chain + // <-unspent/spent ->inactive + // + // 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. + // + // 2. Tx is not present in old chain and is present in new chain + // <-inactive and ->unspent/spent + // + // Balance: + // 1. Sum up all unspent outputs of the transactions in unspent. + // 2. Subtract the inputs of transactions in pending. + // 3. In future: re-add the outputs of pending transactions that are mine. Don't do this today because those + // change outputs would not be considered spendable. /** - * A list of transactions with outputs we can spend. Note that some of these transactions may be partially spent, - * that is, they have outputs some of which are redeemed and others which aren't already. The spentness of each - * output is tracked in the TransactionOutput object. The value of all unspent outputs is the balance of the - * wallet. + * Map of txhash->Transactions that have not made it into the best chain yet. 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! */ - public final ArrayList unspent; + final Map pending; /** - * When all the outputs of a transaction are spent, it gets put here. These transactions aren't useful for - * anything except record keeping and presentation to the user. + * Map of txhash->Transactions where the Transaction has unspent outputs. These are transactions we can use + * to pay other people and so count towards our balance. Transactions only appear in this map if they are part + * of the best chain. Transactions we have broacast that are not confirmed yet appear in pending even though they + * may have unspent "change" outputs.

+ * + * Note: for now we will not allow spends of transactions that did not make it into the block chain. The code + * that handles this in BitCoin C++ is complicated. Satoshis code will not allow you to spend unconfirmed coins, + * however, it does seem to support dependency resolution entirely within the context of the memory pool so + * theoretically you could spend zero-conf coins and all of them would be included together. To simplify we'll + * make people wait but it would be a good improvement to resolve this in future. */ - final LinkedList fullySpent; + final Map unspent; + + /** + * Map of txhash->Transactions where the Transactions outputs are all fully spent. They are kept separately so + * the time to create a spend does not grow infinitely as wallets become more used. Some of these transactions + * may not have appeared in a block yet if they were created by us to spend coins and that spend is still being + * worked on by miners.

+ * + * Transactions only appear in this map if they are part of the best chain. + */ + final Map spent; + + /** + * An inactive transaction is one that is seen only in a block that is not a part of the best chain. We keep it + * around in case a re-org promotes a different chain to be the best. In this case some (not necessarily all) + * inactive transactions will be moved out to unspent and spent, and some might be moved in.

+ * + * Note that in the case where a transaction appears in both the best chain and a side chain as well, it is not + * placed in this map. It's an error for a transaction to be in both the inactive pool and unspent/spent. + */ + private Map inactive; /** A list of public/private EC keys owned by this user. */ public final ArrayList keychain; @@ -65,8 +125,10 @@ public class Wallet implements Serializable { public Wallet(NetworkParameters params) { this.params = params; keychain = new ArrayList(); - unspent = new ArrayList(); - fullySpent = new LinkedList(); + unspent = new HashMap(); + spent = new HashMap(); + inactive = new HashMap(); + pending = new HashMap(); eventListeners = new ArrayList(); } @@ -105,70 +167,152 @@ public class Wallet implements Serializable { * transaction is logically equal. */ public synchronized boolean isTransactionPresent(Transaction transaction) { - for (Transaction tx : unspent) { - if (Arrays.equals(tx.getHash(), transaction.getHash())) return true; - } - for (Transaction tx : fullySpent) { - if (Arrays.equals(tx.getHash(), transaction.getHash())) return true; - } - return false; + // TODO: Redefine or delete this method. + Sha256Hash hash = transaction.getHash(); + return unspent.containsKey(hash) || spent.containsKey(hash); } /** - * Called by the {@link BlockChain} when we receive a new block that sends coins to one of our addresses, - * stores the transaction in the wallet so we can spend it in future. Don't call this on transactions we already - * have, for instance because we created them ourselves! + * Called by the {@link BlockChain} when we receive a new block that sends coins to one of our addresses or + * spends coins from one of our addresses (note that a single transaction can do both).

+ * + * This is necessary for the internal book-keeping Wallet does. When a transaction is received that sends us + * coins it is added to a pool so we can use it later to create spends. When a transaction is received that + * consumes outputs they are marked as spent so they won't be used in future.

+ * + * A transaction that spends our own coins can be received either because a spend we created was accepted by the + * network and thus made it into a block, or because our keys are being shared between multiple instances and + * some other node spent the coins instead. We still have to know about that to avoid accidentally trying to + * double spend.

+ * + * A transaction may be received multiple times if is included into blocks in parallel chains. The blockType + * parameter describes whether the containing block is on the main/best chain or whether it's on a presently + * inactive side chain. We must still record these transactions and the blocks they appear in because a future + * block might change which chain is best causing a reorganize. A re-org can totally change our balance! */ - synchronized void receive(Transaction tx) throws VerificationException { + synchronized void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException { // Runs in a peer thread. BigInteger prevBalance = getBalance(); - // We need to check if this transaction is spending one of our own previous transactions. This allows us to - // build up a record of our balance by reading the block chain from scratch. Other than making testing easier - // this will be useful if one day we want to support importing keypairs from a wallet.db - for (TransactionInput input : tx.inputs) { - for (int i = 0; i < unspent.size(); i++) { - Transaction t = unspent.get(i); - if (!Arrays.equals(input.outpoint.hash, t.getHash())) continue; - if (input.outpoint.index > t.outputs.size()) { - throw new VerificationException("Invalid tx connection for " + - Utils.bytesToHexString(tx.getHash())); - } - TransactionOutput linkedOutput = t.outputs.get((int) input.outpoint.index); - assert !linkedOutput.isSpent : "Double spend was accepted by network?"; - LOG("Saw a record of me spending " + Utils.bitcoinValueToFriendlyString(linkedOutput.getValue()) - + " BTC"); - linkedOutput.isSpent = true; - // Are all the outputs on this TX that are mine now spent? Note that some of the outputs may not - // be mine and thus we don't care about them. - int myOutputs = 0; - int mySpentOutputs = 0; - for (TransactionOutput output : t.outputs) { - if (!output.isMine(this)) continue; - myOutputs++; - if (output.isSpent) - mySpentOutputs++; - } - if (myOutputs == mySpentOutputs) { - // All the outputs we can claim on this TX are gone now. So remove it from the unspent list - // so future transaction processing is faster. - unspent.remove(i); - i--; // Adjust the counter so we are still in the right place after removal. - // Keep around a record of the now useless TX in case we need it in future. - fullySpent.add(t); + Sha256Hash txHash = tx.getHash(); + + boolean bestChain = blockType == BlockChain.NewBlockType.BEST_CHAIN; + boolean sideChain = blockType == BlockChain.NewBlockType.SIDE_CHAIN; + + BigInteger valueSentFromMe = tx.getValueSentFromMe(this); + BigInteger valueSentToMe = tx.getValueSentToMe(this); + BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe); + + LOG("Wallet: Received tx" + (sideChain ? " on a side chain" :"") + " for " + + bitcoinValueToFriendlyString(valueDifference) + " BTC"); + + // 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. + Transaction wtx = null; + if ((wtx = pending.remove(txHash)) != null) { + LOG(" <-pending"); + // A transaction we created appeared in a block. Probably this is a spend we broadcast that has been + // accepted by the network. + // + // Mark the tx as appearing in this block so we can find it later after a re-org. + wtx.addBlockAppearance(block); + if (bestChain) { + if (valueSentToMe.equals(BigInteger.ZERO)) { + // There were no change transactions so this tx is fully spent. + LOG(" ->spent"); + boolean alreadyPresent = spent.put(wtx.getHash(), wtx) != null; + assert !alreadyPresent : "TX in both pending and spent pools"; + } else { + // There was change back to us, or this tx was purely a spend back to ourselves (perhaps for + // anonymization purposes). + LOG(" ->unspent"); + boolean alreadyPresent = unspent.put(wtx.getHash(), wtx) != null; + assert !alreadyPresent : "TX in both pending and unspent pools"; } + } else if (sideChain) { + // The transaction was accepted on an inactive side chain, but not yet by the best chain. + LOG(" ->inactive"); + // It's OK for this to already be in the inactive pool because there can be multiple independent side + // chains in which it appears: + // + // b1 --> b2 + // \-> b3 + // \-> b4 (at this point it's already present in 'inactive' + boolean alreadyPresent = inactive.put(wtx.getHash(), wtx) != null; + if (alreadyPresent) + LOG("Saw a transaction be incorporated into multiple independent side chains"); + // Put it back into the pending pool, because 'pending' means 'waiting to be included in best chain'. + 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); + // 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) { + LOG(" ->inactive"); + inactive.put(tx.getHash(), tx); + } else if (bestChain) { + processTxFromBestChain(tx); } } - LOG("Received " + Utils.bitcoinValueToFriendlyString(tx.getValueSentToMe(this))); - unspent.add(tx); - LOG("Balance is now: " + Utils.bitcoinValueToFriendlyString(getBalance())); + + LOG("Balance is now: " + bitcoinValueToFriendlyString(getBalance())); // 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! - for (WalletEventListener l : eventListeners) { - synchronized (l) { - l.onCoinsReceived(this, tx, prevBalance, getBalance()); + if (bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) { + for (WalletEventListener l : eventListeners) { + synchronized (l) { + l.onCoinsReceived(this, tx, prevBalance, getBalance()); + } + } + } + } + + /** + * Handle when a transaction becomes newly active on the best chain, either due to receiving a new block or a + * re-org making inactive transactions active. + */ + private void processTxFromBestChain(Transaction tx) throws VerificationException { + // This TX may spend our existing outputs even though it was not pending. This can happen in unit + // tests and if keys are moved between wallets. + updateForSpends(tx); + if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) { + // It's sending us coins. + LOG(" ->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(" ->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 + * 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(" Saw some of my unspent outputs be spent by someone else who has my keys."); + LOG(" Total spent value is " + bitcoinValueToFriendlyString(output.getValue())); + output.isSpent = true; + Transaction connectedTx = input.outpoint.fromTx; + if (connectedTx.getValueSentToMe(this, false).equals(BigInteger.ZERO)) { + // There's nothing left I can spend in this transaction. + if (unspent.remove(connectedTx.getHash()) != null); + LOG(" prevtx <-unspent"); + spent.put(connectedTx.getHash(), connectedTx); + LOG(" prevtx ->spent"); + } } } } @@ -189,9 +333,7 @@ public class Wallet implements Serializable { * Call this when we have successfully transmitted the send tx to the network, to update the wallet. */ synchronized void confirmSend(Transaction tx) { - // This tx is supposed to be fresh, it's an error to confirmSend on a transaction that was already sent. - assert !unspent.contains(tx); - assert !fullySpent.contains(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. for (TransactionInput input : tx.inputs) { TransactionOutput connectedOutput = input.outpoint.getConnectedOutput(); @@ -199,35 +341,25 @@ public class Wallet implements Serializable { connectedOutput.isSpent = true; } // 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 wallet so we can show it in the UI later and use it for further spending. - try { - int numSpentOutputs = 0; - for (TransactionOutput output : tx.outputs) { - if (findKeyFromPubHash(output.getScriptPubKey().getToAddress().getHash160()) == null) { - // This output didn't go to us, so by definition it is now spent. - assert !output.isSpent; - output.isSpent = true; - numSpentOutputs++; - } + // consolidating the wallet. Mark any output that is NOT back to us as spent. Then add this TX to the + // pending pool. + 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; } - if (numSpentOutputs == tx.outputs.size()) { - // All of the outputs are to other people, so this transaction isn't useful anymore for further - // spending. Stick it in a different section of the wallet so it doesn't slow down creating future - // spend transactions. - fullySpent.add(tx); - } else { - unspent.add(tx); - } - } catch (ScriptException e) { - // This cannot happen - we made this script so we should be able to parse it. - throw new RuntimeException(e); } + pending.put(tx.getHash(), tx); } /** - * Creates a transaction that sends the given number of nanocoins to address. The change is sent to the first - * address in the wallet, so you must have added at least one key. + * Statelessly creates a transaction that sends the given number of nanocoins to address. The change is sent to + * the first address in the wallet, so you must have added at least one key.

+ * + * This method is stateless in the sense that calling it twice with the same inputs will result in two + * Transaction objects which are equal. The wallet is not updated to track its pending status or to mark the + * coins as spent until confirmSend is called on the result. */ synchronized Transaction createSend(Address address, BigInteger nanocoins) { // For now let's just pick the first key in our keychain. In future we might want to do something else to @@ -267,15 +399,15 @@ public class Wallet implements Serializable { * our coins. This should be an address we own (is in the keychain). * @return a new {@link Transaction} or null if we cannot afford this send. */ - synchronized Transaction createSend(Address address, BigInteger nanocoins, Address changeAddress) { + synchronized Transaction createSend(Address address, BigInteger nanocoins, Address changeAddress) { LOG("Creating send tx to " + address.toString() + " for " + - Utils.bitcoinValueToFriendlyString(nanocoins)); - // To send money to somebody else, we need to do the following: - // - Gather up transactions with unspent outputs until we have sufficient value. - // TODO: Sort coins so we use the smallest first, to combat wallet fragmentation. + bitcoinValueToFriendlyString(nanocoins)); + // To send money to somebody else, we need to do gather up transactions with unspent outputs until we have + // sufficient value. Many coin selection algorithms are possible, we use a simple but suboptimal one. + // TODO: Sort coins so we use the smallest first, to combat wallet fragmentation and reduce fees. BigInteger valueGathered = BigInteger.ZERO; List gathered = new LinkedList(); - for (Transaction tx : unspent) { + for (Transaction tx : unspent.values()) { for (TransactionOutput output : tx.outputs) { if (output.isSpent) continue; if (!output.isMine(this)) continue; @@ -287,19 +419,20 @@ public class Wallet implements Serializable { // Can we afford this? if (valueGathered.compareTo(nanocoins) < 0) { LOG("Insufficient value in wallet for send, missing " + - Utils.bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered))); + bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered))); // TODO: Should throw an exception here. return null; } + assert gathered.size() > 0; Transaction sendTx = new Transaction(params); - sendTx.addOutput(new TransactionOutput(params, nanocoins, address)); + sendTx.addOutput(new TransactionOutput(params, nanocoins, address, sendTx)); 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(" with " + Utils.bitcoinValueToFriendlyString(change) + " coins change"); - sendTx.addOutput(new TransactionOutput(params, change, changeAddress)); + LOG(" with " + bitcoinValueToFriendlyString(change) + " coins change"); + sendTx.addOutput(new TransactionOutput(params, change, changeAddress, sendTx)); } for (TransactionOutput output : gathered) { sendTx.addInput(output); @@ -336,12 +469,33 @@ public class Wallet implements Serializable { return null; } + /** Returns true if this wallet contains a public key which hashes to the given hash. */ + public synchronized boolean isPubKeyHashMine(byte[] pubkeyHash) { + return findKeyFromPubHash(pubkeyHash) != null; + } + /** - * Returns the balance of this wallet in nanocoins by summing up all unspent outputs that were sent to us. + * Locates a keypair from the keychain given the raw public key bytes. + * @return ECKey or null if no such key was found. + */ + public synchronized ECKey findKeyFromPubKey(byte[] pubkey) { + for (ECKey key : keychain) { + if (Arrays.equals(key.getPubKey(), pubkey)) return key; + } + return null; + } + + /** Returns true if this wallet contains a keypair with the given public key. */ + public synchronized boolean isPubKeyMine(byte[] pubkey) { + return findKeyFromPubKey(pubkey) != null; + } + + /** + * Returns the balance of this wallet by summing up all unspent outputs that were sent to us. */ public synchronized BigInteger getBalance() { BigInteger balance = BigInteger.ZERO; - for (Transaction tx : unspent) { + for (Transaction tx : unspent.values()) { for (TransactionOutput output : tx.outputs) { if (output.isSpent) continue; if (!output.isMine(this)) continue; @@ -355,11 +509,11 @@ public class Wallet implements Serializable { public synchronized String toString() { StringBuilder builder = new StringBuilder(); builder.append("Wallet containing "); - builder.append(Utils.bitcoinValueToFriendlyString(getBalance())); + builder.append(bitcoinValueToFriendlyString(getBalance())); builder.append("BTC in "); builder.append(unspent.size()); builder.append(" unspent transactions/"); - builder.append(fullySpent.size()); + builder.append(spent.size()); builder.append(" spent transactions"); // Do the keys. builder.append("\nKeys:\n"); @@ -379,29 +533,80 @@ public class Wallet implements Serializable { * 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. */ - synchronized void reorganize(StoredBlock chainHead, StoredBlock newStoredBlock) { - // This runs on any peer thread with the block chain synchronized. Thus we do not have to worry about it - // being called simultaneously or repeatedly. - LOG("Re-organize!"); - LOG("Old chain head: " + chainHead.getHeader().toString()); - LOG("New chain head: " + newStoredBlock.getHeader().toString()); + synchronized void reorganize(Set oldBlocks, Set 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. + // + // 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(); - // TODO: Implement me! - // For each transaction we have to track which blocks they appeared in. Once a re-org takes place, - // we will have to find all transactions in the old branch, all transactions in the new branch and find the - // difference of those sets. 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. - boolean affectedUs = true; + Set all = new HashSet(); + all.addAll(unspent.values()); + all.addAll(spent.values()); + all.addAll(inactive.values()); + for (Transaction tx : all) { + 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"; + } + } - // We should only trigger this event if the re-org actually impacted our wallet. Otherwise the user is - // unlikely to care. - if (affectedUs) { - // Inform event listeners that a re-org took place. - for (WalletEventListener l : eventListeners) { - synchronized (l) { - l.onReorganize(); - } + // 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. + boolean affectedUs = !oldChainTransactions.equals(newChainTransactions); + LOG(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 (Transaction tx : gone) { + LOG("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 : fresh) { + inactive.remove(tx.getHash()); + processTxFromBestChain(tx); + } + + // Inform event listeners that a re-org took place. + for (WalletEventListener l : eventListeners) { + // Synchronize on the event listener as well. This allows a single listener to handle events from + // multiple wallets without needing to worry about being thread safe. + synchronized (l) { + l.onReorganize(); } } } + + /** + * Returns an immutable view of the transactions currently waiting for network confirmations. + */ + public Collection getPendingTransactions() { + return Collections.unmodifiableCollection(pending.values()); + } } diff --git a/tests/com/google/bitcoin/core/BlockChainTest.java b/tests/com/google/bitcoin/core/BlockChainTest.java index 32c3bde9..878f3bdf 100644 --- a/tests/com/google/bitcoin/core/BlockChainTest.java +++ b/tests/com/google/bitcoin/core/BlockChainTest.java @@ -36,6 +36,7 @@ public class BlockChainTest { private BlockChain chain; private Address coinbaseTo; private NetworkParameters unitTestParams; + private Address someOtherGuy; @Before public void setUp() { @@ -44,8 +45,10 @@ public class BlockChainTest { unitTestParams = NetworkParameters.unitTests(); wallet = new Wallet(unitTestParams); wallet.addKey(new ECKey()); - coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams); chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams)); + + coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams); + someOtherGuy = new ECKey().toAddress(unitTestParams); } @Test @@ -86,23 +89,26 @@ public class BlockChainTest { } @Test - public void testForking() throws Exception { - // Check that if the block chain forks, we end up using the right one. - // Start by building a couple of blocks on top of the genesis block. - final boolean[] flags = new boolean[1]; - flags[0] = false; + 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() { - flags[0] = true; + 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(flags[0]); + 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: @@ -114,34 +120,106 @@ public class BlockChainTest { // \-> b3 // // Nothing should happen at this point. We saw b2 first so it takes priority. - Address someOtherGuy = new ECKey().toAddress(unitTestParams); Block b3 = b1.createNextBlock(someOtherGuy); assertTrue(chain.add(b3)); - assertFalse(flags[0]); // No re-org took place. + 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(flags[0]); // Re-org took place. - flags[0] = false; + 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. - if (false) { - // These tests do not pass currently, as wallet handling of re-orgs isn't implemented. - assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - // ... and back to the first testNetChain - 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 - // - assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance())); - } + 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 diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index f78ef4ed..21e5e07b 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -26,38 +26,66 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class WalletTest { - static final NetworkParameters params = NetworkParameters.testNet(); + static final NetworkParameters params = NetworkParameters.unitTests(); private Address myAddress; private Wallet wallet; + private BlockStore blockStore; @Before - public void setUp() { + public void setUp() throws Exception { ECKey myKey = new ECKey(); myAddress = myKey.toAddress(params); wallet = new Wallet(params); wallet.addKey(myKey); + 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); + TransactionOutput o1 = new TransactionOutput(params, nanocoins, to, t); 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]; - for (byte i = 0; i < 32; i++) hash[i] = i; - t.setFakeHashForTesting(hash); + hash[0] = fakeHashCounter++; + t.setFakeHashForTesting(new Sha256Hash(hash)); return t; } + class BlockPair { + StoredBlock storedBlock; + Block block; + } + + // Emulates receiving a valid block that builds on top of the chain. + private BlockPair createFakeBlock(Transaction... transactions) { + try { + Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params)); + for (Transaction tx : transactions) + b.addTransaction(tx); + b.solve(); + BlockPair pair = new BlockPair(); + pair.block = b; + pair.storedBlock = blockStore.getChainHead().build(b); + blockStore.put(pair.storedBlock); + blockStore.setChainHead(pair.storedBlock); + return pair; + } catch (VerificationException e) { + throw new RuntimeException(e); // Cannot happen. + } catch (BlockStoreException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + @Test public void testBasicSpending() throws Exception { // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. BigInteger v1 = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(v1, myAddress); - wallet.receive(t1); + wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); ECKey k2 = new ECKey(); @@ -66,12 +94,27 @@ public class WalletTest { // Do some basic sanity checks. assertEquals(1, t2.inputs.size()); - LOG(t2.inputs.get(0).getScriptSig().toString()); assertEquals(myAddress, t2.inputs.get(0).getScriptSig().getFromAddress()); // We have NOT proven that the signature is correct! } + @Test + public void testSideChain() throws Exception { + // The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance. + BigInteger v1 = Utils.toNanoCoins(1, 0); + Transaction t1 = createFakeTx(v1, myAddress); + + wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(v1, wallet.getBalance()); + + BigInteger v2 = toNanoCoins(0, 50); + Transaction t2 = createFakeTx(v2, myAddress); + wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); + + assertEquals(v1, wallet.getBalance()); + } + @Test public void testListener() throws Exception { final Transaction fakeTx = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); @@ -86,7 +129,7 @@ public class WalletTest { } }; wallet.addEventListener(listener); - wallet.receive(fakeTx); + wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN); assertTrue(didRun[0]); } @@ -97,24 +140,29 @@ public class WalletTest { BigInteger v2 = toNanoCoins(0, 50); Transaction t1 = createFakeTx(v1, myAddress); Transaction t2 = createFakeTx(v2, myAddress); + StoredBlock b1 = createFakeBlock(t1).storedBlock; + StoredBlock b2 = createFakeBlock(t2).storedBlock; BigInteger expected = toNanoCoins(5, 50); - wallet.receive(t1); - wallet.receive(t2); + wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(expected, wallet.getBalance()); // Now spend one coin. BigInteger v3 = toNanoCoins(1, 0); Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3); wallet.confirmSend(spend); - // We started with 5.50 so we should have 4.50 left. + + // Balance should be 0.50 because the change output is pending confirmation by the network. + assertEquals(toNanoCoins(0, 50), wallet.getBalance()); + + // Now confirm the transaction by including it into a block. + StoredBlock b3 = createFakeBlock(spend).storedBlock; + wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN); + + // 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())); - // And spend another coin ... - wallet.confirmSend(wallet.createSend(new ECKey().toAddress(params), v3)); - BigInteger v5 = toNanoCoins(3, 50); - assertEquals(bitcoinValueToFriendlyString(v5), - bitcoinValueToFriendlyString(wallet.getBalance())); } // Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an @@ -125,17 +173,22 @@ public class WalletTest { @Test public void testBlockChainCatchup() throws Exception { Transaction tx1 = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); - wallet.receive(tx1); + StoredBlock b1 = createFakeBlock(tx1).storedBlock; + wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN); // Send 0.10 to somebody else. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we - // want to get back to our previous state. - wallet.receive(send1); + // want to get back to our previous state. We can do this by just not confirming the transaction as + // createSend is stateless. + StoredBlock b2 = createFakeBlock(send1).storedBlock; + wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90"); // And we do it again after the catchup. Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // What we'd really like to do is prove the official client would accept it .... no such luck unfortunately. wallet.confirmSend(send2); + StoredBlock b3 = createFakeBlock(send2).storedBlock; + wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80"); } @@ -143,8 +196,8 @@ public class WalletTest { public void testBalances() throws Exception { BigInteger nanos = Utils.toNanoCoins(1, 0); Transaction tx1 = createFakeTx(nanos, myAddress); - wallet.receive(tx1); - assertEquals(nanos, tx1.getValueSentToMe(wallet)); + wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(nanos, tx1.getValueSentToMe(wallet, true)); // Send 0.10 to somebody else. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // Reserialize.