From 7aa485110af81b25222720a42d2ce61588e6e49b Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 29 Nov 2011 15:11:15 +0100 Subject: [PATCH] First cut at a pending transactions patch. This isn't the final API, which will involve some changes to the wallet event listener/tx to have a concept of confidence levels. --- .../core/AbstractPeerEventListener.java | 5 + src/com/google/bitcoin/core/BlockChain.java | 27 +- src/com/google/bitcoin/core/Peer.java | 60 ++++- .../bitcoin/core/PeerEventListener.java | 8 + src/com/google/bitcoin/core/PeerGroup.java | 48 +++- src/com/google/bitcoin/core/Transaction.java | 12 +- .../bitcoin/core/TransactionOutPoint.java | 27 +- src/com/google/bitcoin/core/Wallet.java | 250 ++++++++++++++---- .../bitcoin/examples/DerbyPingService.java | 3 +- .../google/bitcoin/examples/PingService.java | 14 +- .../google/bitcoin/examples/PrivateKeys.java | 2 +- .../bitcoin/examples/RefreshWallet.java | 2 +- .../google/bitcoin/core/ChainSplitTests.java | 6 +- .../google/bitcoin/core/PeerGroupTest.java | 33 ++- tests/com/google/bitcoin/core/PeerTest.java | 25 +- .../core/TestWithNetworkConnections.java | 12 +- tests/com/google/bitcoin/core/WalletTest.java | 182 ++++++++++--- 17 files changed, 559 insertions(+), 157 deletions(-) diff --git a/src/com/google/bitcoin/core/AbstractPeerEventListener.java b/src/com/google/bitcoin/core/AbstractPeerEventListener.java index 6235476f..f3fae62e 100644 --- a/src/com/google/bitcoin/core/AbstractPeerEventListener.java +++ b/src/com/google/bitcoin/core/AbstractPeerEventListener.java @@ -35,4 +35,9 @@ public class AbstractPeerEventListener extends Object implements PeerEventListen public void onPeerDisconnected(Peer peer, int peerCount) { } + + public Message onPreMessageReceived(Peer peer, Message m) { + // Just pass the message right through for further processing. + return m; + } } diff --git a/src/com/google/bitcoin/core/BlockChain.java b/src/com/google/bitcoin/core/BlockChain.java index 38a53a1f..f7c744c1 100644 --- a/src/com/google/bitcoin/core/BlockChain.java +++ b/src/com/google/bitcoin/core/BlockChain.java @@ -312,7 +312,7 @@ public class BlockChain { try { List txns = entry.getValue(); for (Transaction tx : txns) { - entry.getKey().receive(tx, block, blockType); + entry.getKey().receiveFromBlock(tx, block, 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 @@ -427,28 +427,9 @@ public class BlockChain { for (Transaction tx : block.transactions) { try { for (Wallet wallet : wallets) { - boolean shouldReceive = false; - for (TransactionOutput output : tx.getOutputs()) { - // TODO: Handle more types of outputs, not just regular to address outputs. - if (output.getScriptPubKey().isSentToIP()) continue; - // This is not thread safe as a key could be removed between the call to isMine and receive. - if (output.isMine(wallet)) { - shouldReceive = true; - break; - } - } - - // Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air). - if (!shouldReceive && !tx.isCoinBase()) { - for (TransactionInput i : tx.getInputs()) { - 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; - } - } - } - + if (tx.isCoinBase()) + continue; + boolean shouldReceive = wallet.isTransactionRelevant(tx, true); if (!shouldReceive) continue; List txList = walletToTxMap.get(wallet); if (txList == null) { diff --git a/src/com/google/bitcoin/core/Peer.java b/src/com/google/bitcoin/core/Peer.java index fc7cd249..fa872fd1 100644 --- a/src/com/google/bitcoin/core/Peer.java +++ b/src/com/google/bitcoin/core/Peer.java @@ -41,6 +41,7 @@ public class Peer { private final BlockChain blockChain; // When an API user explicitly requests a block or transaction from a peer, the InventoryItem is put here // whilst waiting for the response. Synchronized on itself. Is not used for downloads Peer generates itself. + // TODO: Make this work for transactions as well. private final List> pendingGetBlockFutures; // Height of the chain advertised in the peers version message. private int bestHeight; @@ -64,6 +65,9 @@ public class Peer { // received at which point it gets set to true again. This isn't relevant unless downloadData is true. private boolean downloadBlockBodies = true; + // Wallets that will be notified of pending transactions. + private ArrayList wallets = new ArrayList(); + /** * Construct a peer that reads/writes from the given block chain. Note that communication won't occur until * you call connect(), which will set up a new NetworkConnection. @@ -151,10 +155,23 @@ public class Peer { try { while (true) { Message m = conn.readMessage(); + + // Allow event listeners to filter the message stream. Listeners are allowed to drop messages by + // returning null. + for (PeerEventListener listener : eventListeners) { + synchronized (listener) { + m = listener.onPreMessageReceived(this, m); + if (m == null) break; + } + } + if (m == null) continue; + if (m instanceof InventoryMessage) { processInv((InventoryMessage) m); } else if (m instanceof Block) { processBlock((Block) m); + } else if (m instanceof Transaction) { + processTransaction((Transaction) m); } else if (m instanceof AddressMessage) { // We don't care about addresses of the network right now. But in future, // we should save them in the wallet so we don't put too much load on the seed nodes and can @@ -225,8 +242,21 @@ public class Peer { } } + private void processTransaction(Transaction m) { + log.info("Received broadcast tx {}", m.getHashAsString()); + for (Wallet wallet : wallets) { + try { + wallet.receivePending(m); + } catch (VerificationException e) { + log.warn("Received invalid transaction, ignoring", e); + } catch (ScriptException e) { + log.warn("Received invalid transaction, ignoring", e); + } + } + } + private void processBlock(Block m) throws IOException { - // This should called in the network loop thread for this peer + log.info("Received broadcast block {}", m.getHashAsString()); try { // Was this block requested by getBlock()? synchronized (pendingGetBlockFutures) { @@ -293,18 +323,13 @@ public class Peer { blockChainDownload(topHash); return; } + // Just copy the message contents across - request whatever we're told about. + // TODO: Don't re-request items that were already fetched. GetDataMessage getdata = new GetDataMessage(params); - boolean dirty = false; for (InventoryItem item : items) { - if (item.type != InventoryItem.Type.Block) continue; getdata.addItem(item); - dirty = true; } - // No blocks to download. This probably contained transactions instead, but right now we can't prove they are - // valid so we don't bother downloading transactions that aren't in blocks yet. - if (!dirty) - return; - // This will cause us to receive a bunch of block messages. + // This will cause us to receive a bunch of block or tx messages. conn.writeMessage(getdata); } @@ -361,6 +386,21 @@ public class Peer { } } } + + /** + * Add a wallet that will receive notifications of broadcast transactions. You need to connect the peer to each + * wallet this way if you care about handling un-confirmed transactions. If you don't, + * it's unnecessary as the wallet will be informed of transactions in new blocks via the block chain object. + */ + public void addWallet(Wallet wallet) { + if (wallet == null) + throw new IllegalArgumentException("Wallet must not be null"); + wallets.add(wallet); + } + + public void removeWallet(Wallet wallet) { + wallets.remove(wallet); + } // A GetDataFuture wraps the result of a getBlock or (in future) getTransaction so the owner of the object can // decide whether to wait forever, wait for a short while or check later after doing other work. @@ -418,7 +458,7 @@ public class Peer { /** * Send the given Transaction, ie, make a payment with BitCoins. To create a transaction you can broadcast, use - * a {@link Wallet}. After the broadcast completes, confirm the send using the wallet confirmSend() method. + * a {@link Wallet}. After the broadcast completes, confirm the send using the wallet commitTx() method. * @throws IOException */ void broadcastTransaction(Transaction tx) throws IOException { diff --git a/src/com/google/bitcoin/core/PeerEventListener.java b/src/com/google/bitcoin/core/PeerEventListener.java index 76368de6..79db99ce 100644 --- a/src/com/google/bitcoin/core/PeerEventListener.java +++ b/src/com/google/bitcoin/core/PeerEventListener.java @@ -60,4 +60,12 @@ public interface PeerEventListener { * @param peerCount the total number of connected peers */ public void onPeerDisconnected(Peer peer, int peerCount); + + /** + * Called when a message is received by a peer, before the message is processed. The returned message is + * processed instead. Returning null will cause the message to be ignored by the Peer returning the same message + * object allows you to see the messages received but not change them. The result from one event listeners + * callback is passed as "m" to the next, forming a chain. + */ + public Message onPreMessageReceived(Peer peer, Message m); } diff --git a/src/com/google/bitcoin/core/PeerGroup.java b/src/com/google/bitcoin/core/PeerGroup.java index 8461ede8..3eda8346 100644 --- a/src/com/google/bitcoin/core/PeerGroup.java +++ b/src/com/google/bitcoin/core/PeerGroup.java @@ -19,8 +19,6 @@ package com.google.bitcoin.core; import com.google.bitcoin.discovery.PeerDiscovery; import com.google.bitcoin.discovery.PeerDiscoveryException; -import com.google.bitcoin.store.BlockStore; -import com.google.bitcoin.store.BlockStoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,28 +73,33 @@ public class PeerGroup { private Set peerDiscoverers; private NetworkParameters params; - private BlockStore blockStore; private BlockChain chain; private int connectionDelayMillis; private long fastCatchupTimeSecs; + private Set wallets; /** - * Creates a PeerGroup with the given parameters and a default 5 second connection timeout. + * Creates a PeerGroup with the given parameters and a default 5 second connection timeout. If you don't care + * about blocks or pending transactions, you can just provide a MemoryBlockStore and a newly created Wallet. + * + * @param params Network parameters + * @param chain a BlockChain object that will receive and handle block messages. + * @param wallet a Wallet object that will receive pending transactions. */ - public PeerGroup(BlockStore blockStore, NetworkParameters params, BlockChain chain) { - this(blockStore, params, chain, DEFAULT_CONNECTION_DELAY_MILLIS); + public PeerGroup(NetworkParameters params, BlockChain chain) { + this(params, chain, DEFAULT_CONNECTION_DELAY_MILLIS); } /** * Creates a PeerGroup with the given parameters. The connectionDelayMillis parameter controls how long the * PeerGroup will wait between attempts to connect to nodes or read from any added peer discovery sources. */ - public PeerGroup(BlockStore blockStore, NetworkParameters params, BlockChain chain, int connectionDelayMillis) { - this.blockStore = blockStore; + public PeerGroup(NetworkParameters params, BlockChain chain, int connectionDelayMillis) { this.params = params; this.chain = chain; this.connectionDelayMillis = connectionDelayMillis; this.fastCatchupTimeSecs = params.genesisBlock.getTimeSeconds(); + this.wallets = new HashSet(); inactives = new LinkedBlockingQueue(); peers = Collections.synchronizedSet(new HashSet()); @@ -122,6 +125,20 @@ public class PeerGroup { return peerEventListeners.remove(listener); } + /** + * The given wallet will receive broadcast/pending transactions that did not appear in a block yet. + * @return Whether the wallet was added successfully. + */ + public boolean addWallet(Wallet wallet) { + if (wallet == null) + throw new IllegalArgumentException("Wallet must not be null"); + return wallets.add(wallet); + } + + public boolean removeWallet(Wallet wallet) { + return wallets.remove(wallet); + } + /** * Use this to directly add an already initialized and connected {@link Peer} object. Normally, you would prefer * to use {@link PeerGroup#addAddress(PeerAddress)} and let this object handle construction of the peer for you. @@ -291,7 +308,7 @@ public class PeerGroup { final PeerAddress address = inactives.take(); while (true) { try { - Peer peer = new Peer(params, address, blockStore.getChainHead().getHeight(), chain); + Peer peer = new Peer(params, address, chain.getChainHead().getHeight(), chain); executePeer(address, peer, true, ExecuteBlockMode.RETURN_IMMEDIATELY); break; } catch (RejectedExecutionException e) { @@ -301,11 +318,6 @@ public class PeerGroup { // if we reached maxConnections or if peer queue is empty. Also consider // exponential backoff on peers and adjusting the sleep time according to the // lowest backoff value in queue. - } catch (BlockStoreException e) { - // Fatal error - log.error("Block store corrupt?", e); - running = false; - throw new RuntimeException(e); } // If we got here, we should retry this address because an error unrelated @@ -442,10 +454,15 @@ public class PeerGroup { private synchronized void setDownloadPeer(Peer peer) { if (downloadPeer != null) { + log.info("Unsetting download peer: {}", downloadPeer); downloadPeer.setDownloadData(false); + for (Wallet w : wallets) { + downloadPeer.removeWallet(w); + } } downloadPeer = peer; if (downloadPeer != null) { + log.info("Setting download peer: {}", downloadPeer); downloadPeer.setDownloadData(true); downloadPeer.setFastCatchupTime(fastCatchupTimeSecs); } @@ -459,6 +476,9 @@ public class PeerGroup { fastCatchupTimeSecs = secondsSinceEpoch; if (downloadPeer != null) { downloadPeer.setFastCatchupTime(secondsSinceEpoch); + for (Wallet w : wallets) { + downloadPeer.addWallet(w); + } } } diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index d7d2a3c3..e16def67 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -75,7 +75,7 @@ public class Transaction extends ChildMessage implements Serializable { inputs = new ArrayList(); outputs = new ArrayList(); // We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet. - length = 10; //8 for std fields + 1 for each 0 varint + length = 10; // 8 for std fields + 1 for each 0 varint } /** @@ -176,6 +176,15 @@ public class Transaction extends ChildMessage implements Serializable { return appearsIn; } + /** Returns true if this transaction hasn't been seen in any block yet. */ + public boolean isPending() { + if (appearsIn == null) + return true; + if (appearsIn.size() == 0) + return true; + return false; + } + /** * 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 @@ -451,7 +460,6 @@ public class Transaction extends ChildMessage implements Serializable { s.append(in.getScriptSig().getFromAddress().toString()); } catch (Exception e) { s.append("[exception: ").append(e.getMessage()).append("]"); - throw new RuntimeException(e); } s.append("\n"); } diff --git a/src/com/google/bitcoin/core/TransactionOutPoint.java b/src/com/google/bitcoin/core/TransactionOutPoint.java index 84a919ad..f83e6233 100644 --- a/src/com/google/bitcoin/core/TransactionOutPoint.java +++ b/src/com/google/bitcoin/core/TransactionOutPoint.java @@ -21,8 +21,6 @@ import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; -// 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. */ @@ -103,8 +101,9 @@ public class TransactionOutPoint extends ChildMessage 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. + * An outpoint is a part of a transaction input that points to the output of another transaction. If we have both + * sides in memory, and they have been linked together, this returns a pointer to the connected output, or null + * if there is no such connection. */ TransactionOutput getConnectedOutput() { if (fromTx == null) return null; @@ -157,14 +156,6 @@ public class TransactionOutPoint extends ChildMessage implements Serializable { return index; } -// /** -// * @param index the index to set -// */ -// public void setIndex(long index) { -// unCache(); -// this.index = index; -// } - /** * Ensure object is fully parsed before invoking java serialization. The backing byte array * is transient so if the object has parseLazy = true and hasn't invoked checkParse yet @@ -174,4 +165,16 @@ public class TransactionOutPoint extends ChildMessage implements Serializable { maybeParse(); out.defaultWriteObject(); } + + @Override + public boolean equals(Object other) { + if (!(other instanceof TransactionOutPoint)) return false; + TransactionOutPoint o = (TransactionOutPoint) other; + return o.getIndex() == getIndex() && o.getHash().equals(getHash()); + } + + @Override + public int hashCode() { + return getHash().hashCode(); + } } diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index 94e30a35..f56e5c97 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -178,7 +178,7 @@ public class Wallet implements Serializable { } /** - * Returns a wallet deserialied from the given file input stream. + * Returns a wallet deserialized from the given file input stream. */ public static Wallet loadFromFileStream(FileInputStream f) throws IOException { ObjectInputStream ois = null; @@ -200,25 +200,113 @@ public class Wallet implements Serializable { /** * 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, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException { + synchronized void receiveFromBlock(Transaction tx, StoredBlock block, + BlockChain.NewBlockType blockType) throws VerificationException, ScriptException { receive(tx, block, blockType, false); } + /** + * Called when we have found a tranasction (via network broadcast or otherwise) that is relevant to this wallet + * and want to record it. Note that we cannot verify these transactions at all, they may spend fictional + * coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon, + * and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not + * be double spent as an optimization. + * + * @param tx + * @throws VerificationException + * @throws ScriptException + */ + synchronized void receivePending(Transaction tx) throws VerificationException, ScriptException { + // TODO: Add a notion of confidence levels to this API. + // Runs in a peer thread. + log.info(tx.getHashAsString()); + + // Ignore it if we already know about this transaction. Receiving a pending transaction never moves it + // between pools. + EnumSet containingPools = getContainingPools(tx); + if (!containingPools.equals(EnumSet.noneOf(Pool.class))) { + log.info("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString()); + return; + } + + // We only care about transactions that: + // - Send us coins + // - Spend our coins + if (!isTransactionRelevant(tx, true)) { + log.info("Received tx that isn't relevant to this wallet, discarding."); + return; + } + + BigInteger value = tx.getValueSentToMe(this); + log.info("Received a pending transaction {} that sends us {} BTC", tx.getHashAsString(), + Utils.bitcoinValueToFriendlyString(value)); + + // If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This + // ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the + // timestamp on the transaction. + commitTx(tx); + + // Event listeners may re-enter so we cannot make assumptions about wallet state after this loop completes. + BigInteger balance = getBalance(); + BigInteger newBalance = balance.add(value); + for (WalletEventListener l : eventListeners) { + synchronized (l) { + l.onCoinsReceived(this, tx, balance, newBalance); + } + } + } + + /** + * Returns true if the given transaction sends coins to any of our keys, or has inputs spending any of our outputs, + * and if includeDoubleSpending is true, also returns true if tx has inputs that are spending outputs which are + * not ours but which are spent by pending transactions.

+ * + * Note that if the tx has inputs containing one of our keys, but the connected transaction is not in the wallet, + * it will not be considered relevant. + */ + public boolean isTransactionRelevant(Transaction tx, boolean includeDoubleSpending) throws ScriptException { + return tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0 || + tx.getValueSentToMe(this).compareTo(BigInteger.ZERO) > 0 || + (includeDoubleSpending && (findDoubleSpendAgainstPending(tx) != null)); + } + + /** + * Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if + * the double spent inputs are not ours. Returns the pending tx that was double spent or null if none found. + */ + private Transaction findDoubleSpendAgainstPending(Transaction tx) { + // Compile a set of outpoints that are spent by tx. + HashSet outpoints = new HashSet(); + for (TransactionInput input : tx.getInputs()) { + outpoints.add(input.getOutpoint()); + } + // Now for each pending transaction, see if it shares any outpoints with this tx. + for (Transaction p : pending.values()) { + for (TransactionInput input : p.getInputs()) { + if (outpoints.contains(input.getOutpoint())) { + // It does, it's a double spend against the pending pool, which makes it relevant. + return p; + } + } + } + return null; + } + private synchronized void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType, boolean reorg) throws VerificationException, ScriptException { // Runs in a peer thread. @@ -311,31 +399,61 @@ public class Wallet implements Serializable { * 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 { + private void processTxFromBestChain(Transaction tx) throws VerificationException, ScriptException { // 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); + // tests, if keys are moved between wallets, and if we're catching up to the chain given only a set of keys. + updateForSpends(tx, true); if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) { // It's sending us coins. log.info(" new tx ->unspent"); boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null; assert !alreadyPresent : "TX was received twice"; - } else { + } else if (!tx.getValueSentFromMe(this).equals(BigInteger.ZERO)) { // It spent some of our coins and did not send us any. log.info(" new tx ->spent"); boolean alreadyPresent = spent.put(tx.getHash(), tx) != null; assert !alreadyPresent : "TX was received twice"; + } else { + // It didn't send us coins nor spend any of our coins. If we're processing it, that must be because it + // spends outpoints that are also spent by some pending transactions - maybe a double spend of somebody + // elses coins that were originally sent to us? ie, this might be a Finney attack where we think we + // received some money and then the sender co-operated with a miner to take back the coins, using a tx + // that isn't involving our keys at all. + Transaction doubleSpend = findDoubleSpendAgainstPending(tx); + if (doubleSpend != null) { + // This is mostly the same as the codepath in updateForSpends, but that one is only triggered when + // the transaction being double spent is actually in our wallet (ie, maybe we're double spending). + log.warn("Saw double spend from chain override pending tx {}", doubleSpend.getHashAsString()); + log.warn(" <-pending ->dead"); + pending.remove(doubleSpend.getHash()); + dead.put(doubleSpend.getHash(), doubleSpend); + // Inform the event listeners of the newly dead tx. + for (WalletEventListener listener : eventListeners) { + synchronized (listener) { + listener.onDeadTransaction(this, doubleSpend, tx); + } + } + } } } /** - * 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. + * Updates the wallet by checking if this TX spends any of our outputs, and marking them as spent if so. It can + * be called in two contexts. One is when we receive a transaction on the best chain but it wasn't pending, this + * most commonly happens when we have a set of keys but the wallet transactions were wiped and we are catching up + * with the block chain. It can also happen if a block includes a transaction we never saw at broadcast time. + * If this tx double spends, it takes precedence over our pending transactions and the pending tx goes dead. + * + * The other context it can be called is from {@link Wallet#receivePending(Transaction)} ie we saw a tx be + * broadcast or one was submitted directly that spends our own coins. If this tx double spends it does NOT take + * precedence because the winner will be resolved by the miners - we assume that our version will win, + * if we are wrong then when a block appears the tx will go dead. */ - private void updateForSpends(Transaction tx) throws VerificationException { + private void updateForSpends(Transaction tx, boolean fromChain) throws VerificationException { // tx is on the best chain by this point. - for (TransactionInput input : tx.getInputs()) { + List inputs = tx.getInputs(); + for (int i = 0; i < inputs.size(); i++) { + TransactionInput input = inputs.get(i); TransactionInput.ConnectionResult result = input.connect(unspent, false); if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) { // Not found in the unspent map. Try again with the spent map. @@ -347,10 +465,7 @@ public class Wallet implements Serializable { } 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). - // - // Work backwards like so: + // Double spend! Work backwards like so: // // A -> spent by B [pending] // \-> spent by C [chain] @@ -362,19 +477,31 @@ public class Wallet implements Serializable { assert spentBy != null; Transaction connected = spentBy.getParentTransaction(); assert connected != null; - 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(this, connected, tx); + if (fromChain) { + // 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). + if (pending.containsKey(connected.getHash())) { + log.warn("Saw double spend from chain override pending tx {}", connected.getHashAsString()); + log.warn(" <-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(this, connected, tx); + } } } + } else { + // A pending transaction that tried to double spend our coins - we log and ignore it, because either + // 1) The double-spent tx is confirmed and thus this tx has no effect .... or + // 2) Both txns are pending, neither has priority. Miners will decide in a few minutes which won. + log.warn("Saw double spend from another pending transaction, ignoring tx {}", + tx.getHashAsString()); + log.warn(" offending input is input {}", i); + return; } } else if (result == TransactionInput.ConnectionResult.SUCCESS) { // Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet. @@ -423,19 +550,23 @@ public class Wallet implements Serializable { } /** - * Call this when we have successfully transmitted the send tx to the network, to update the wallet. + * Updates the wallet with the given transaction: puts it into the pending pool and sets the spent flags. Used in + * two situations:

+ * + *

    + *
  1. When we have just successfully transmitted the tx we created to the network.
  2. + *
  3. When we receive a pending transaction that didn't appear in the chain yet, + * and we did not create it, and it spends some of our outputs.
  4. + *
*/ - synchronized void confirmSend(Transaction tx) { - assert !pending.containsKey(tx.getHash()) : "confirmSend called on the same transaction twice"; - 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.getInputs()) { - TransactionOutput connectedOutput = input.getOutpoint().getConnectedOutput(); - Transaction connectedTx = connectedOutput.parentTransaction; - connectedOutput.markAsSpent(input); - maybeMoveTxToSpent(connectedTx, "spent tx"); - } + synchronized void commitTx(Transaction tx) throws VerificationException { + assert !pending.containsKey(tx.getHash()) : "commitTx called on the same transaction twice"; + log.info("commitTx of {}", tx.getHashAsString()); tx.updatedAt = Utils.now(); + // Mark the outputs we're spending as spent so we won't try and use them in future creations. This will also + // move any transactions that are now fully spent to the spent map so we can skip them when creating future + // spends. + updateForSpends(tx, false); // Add to the pending pool. It'll be moved out once we receive this transaction on the best chain. pending.put(tx.getHash(), tx); } @@ -505,6 +636,27 @@ public class Wallet implements Serializable { ALL, } + EnumSet getContainingPools(Transaction tx) { + EnumSet result = EnumSet.noneOf(Pool.class); + Sha256Hash txHash = tx.getHash(); + if (unspent.containsKey(txHash)) { + result.add(Pool.UNSPENT); + } + if (spent.containsKey(txHash)) { + result.add(Pool.SPENT); + } + if (pending.containsKey(txHash)) { + result.add(Pool.PENDING); + } + if (inactive.containsKey(txHash)) { + result.add(Pool.INACTIVE); + } + if (dead.containsKey(txHash)) { + result.add(Pool.DEAD); + } + return result; + } + int getPoolSize(Pool pool) { switch (pool) { case UNSPENT: @@ -529,7 +681,7 @@ public class Wallet implements Serializable { *

* 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. + * coins as spent until commitTx 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 @@ -557,7 +709,11 @@ public class Wallet implements Serializable { } // TODO - retry logic - confirmSend(tx); + try { + commitTx(tx); + } catch (VerificationException e) { + throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves. + } return tx; } @@ -575,7 +731,11 @@ public class Wallet implements Serializable { if (tx == null) // Not enough money! :-( return null; peer.broadcastTransaction(tx); - confirmSend(tx); + try { + commitTx(tx); + } catch (VerificationException e) { + throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves. + } return tx; } @@ -583,7 +743,7 @@ public class Wallet implements Serializable { * Creates a transaction that sends $coins.$cents BTC to the given address.

*

* IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions - * that spend the same coins. You have to call confirmSend on the created transaction to prevent this, + * that spend the same coins. You have to call commitTx on the created transaction to prevent this, * but that should only occur once the transaction has been accepted by the network. This implies you cannot have * more than one outstanding sending tx at once. * @@ -714,8 +874,6 @@ public class Wallet implements Serializable { AVAILABLE } - ; - /** * Returns the AVAILABLE balance of this wallet. See {@link BalanceType#AVAILABLE} for details on what this * means.

diff --git a/src/com/google/bitcoin/examples/DerbyPingService.java b/src/com/google/bitcoin/examples/DerbyPingService.java index 8808b38f..ecca841f 100644 --- a/src/com/google/bitcoin/examples/DerbyPingService.java +++ b/src/com/google/bitcoin/examples/DerbyPingService.java @@ -74,8 +74,9 @@ public class DerbyPingService { // Connect to the localhost node. One minute timeout since we won't try any other peers System.out.println("Connecting ..."); BlockChain chain = new BlockChain(params, wallet, blockStore); - final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain); + final PeerGroup peerGroup = new PeerGroup(params, chain); peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost())); + peerGroup.addWallet(wallet); peerGroup.start(); // We want to know when the balance changes. diff --git a/src/com/google/bitcoin/examples/PingService.java b/src/com/google/bitcoin/examples/PingService.java index 9a131ccf..1a0ef32c 100644 --- a/src/com/google/bitcoin/examples/PingService.java +++ b/src/com/google/bitcoin/examples/PingService.java @@ -81,10 +81,11 @@ public class PingService { System.out.println("Connecting ..."); BlockChain chain = new BlockChain(params, wallet, blockStore); - final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain); + final PeerGroup peerGroup = new PeerGroup(params, chain); peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost())); // Download headers only until a day ago. peerGroup.setFastCatchupTimeSecs((new Date().getTime() / 1000) - (60 * 60 * 24)); + peerGroup.addWallet(wallet); peerGroup.start(); // We want to know when the balance changes. @@ -93,13 +94,22 @@ public class PingService { public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { // Running on a peer thread. assert !newBalance.equals(BigInteger.ZERO); + + BigInteger value = tx.getValueSentToMe(w); + + if (tx.isPending()) { + System.out.println("Received pending tx for " + Utils.bitcoinValueToFriendlyString(value)); + // Ignore for now, as we won't be allowed to spend until the tx is no longer pending. We'll get + // another callback here when the tx is confirmed. + return; + } + // It's impossible to pick one specific identity that you receive coins from in BitCoin as there // could be inputs from many addresses. So instead we just pick the first and assume they were all // owned by the same person. try { TransactionInput input = tx.getInputs().get(0); Address from = input.getFromAddress(); - BigInteger value = tx.getValueSentToMe(w); System.out.println("Received " + Utils.bitcoinValueToFriendlyString(value) + " from " + from.toString()); // Now send the coins back! Transaction sendTx = w.sendCoins(peerGroup, from, value); diff --git a/src/com/google/bitcoin/examples/PrivateKeys.java b/src/com/google/bitcoin/examples/PrivateKeys.java index 56ab0c36..58fe84a3 100644 --- a/src/com/google/bitcoin/examples/PrivateKeys.java +++ b/src/com/google/bitcoin/examples/PrivateKeys.java @@ -58,7 +58,7 @@ public class PrivateKeys { final MemoryBlockStore blockStore = new MemoryBlockStore(params); BlockChain chain = new BlockChain(params, wallet, blockStore); - final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain); + final PeerGroup peerGroup = new PeerGroup(params, chain); peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost())); peerGroup.start(); peerGroup.downloadBlockChain(); diff --git a/src/com/google/bitcoin/examples/RefreshWallet.java b/src/com/google/bitcoin/examples/RefreshWallet.java index fb4ed08e..48d3c4da 100644 --- a/src/com/google/bitcoin/examples/RefreshWallet.java +++ b/src/com/google/bitcoin/examples/RefreshWallet.java @@ -38,7 +38,7 @@ public class RefreshWallet { BlockStore blockStore = new MemoryBlockStore(params); BlockChain chain = new BlockChain(params, wallet, blockStore); - final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain); + final PeerGroup peerGroup = new PeerGroup(params, chain); peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost())); peerGroup.start(); diff --git a/tests/com/google/bitcoin/core/ChainSplitTests.java b/tests/com/google/bitcoin/core/ChainSplitTests.java index 195fce8b..03cb0da6 100644 --- a/tests/com/google/bitcoin/core/ChainSplitTests.java +++ b/tests/com/google/bitcoin/core/ChainSplitTests.java @@ -129,7 +129,7 @@ public class ChainSplitTests { 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); + wallet.commitTx(spend); // Waiting for confirmation ... assertEquals(BigInteger.ZERO, wallet.getBalance()); Block b2 = b1.createNextBlock(someOtherGuy); @@ -198,7 +198,7 @@ public class ChainSplitTests { 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); + wallet.commitTx(t1); // Receive t1 as confirmed by the network. Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams)); b2.addTransaction(t1); @@ -240,7 +240,7 @@ public class ChainSplitTests { 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); + wallet.commitTx(t1); // t1 is still pending ... Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams)); chain.add(b2); diff --git a/tests/com/google/bitcoin/core/PeerGroupTest.java b/tests/com/google/bitcoin/core/PeerGroupTest.java index 882d03c6..28c4bfc2 100644 --- a/tests/com/google/bitcoin/core/PeerGroupTest.java +++ b/tests/com/google/bitcoin/core/PeerGroupTest.java @@ -23,6 +23,7 @@ import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.math.BigInteger; import java.net.InetSocketAddress; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -33,7 +34,6 @@ import static org.junit.Assert.*; public class PeerGroupTest extends TestWithNetworkConnections { static final NetworkParameters params = NetworkParameters.unitTests(); - private Wallet wallet; private PeerGroup peerGroup; private final BlockingQueue disconnectedPeers = new LinkedBlockingQueue(); @@ -41,10 +41,10 @@ public class PeerGroupTest extends TestWithNetworkConnections { @Before public void setUp() throws Exception { super.setUp(); - wallet = new Wallet(params); + blockStore = new MemoryBlockStore(params); BlockChain chain = new BlockChain(params, wallet, blockStore); - peerGroup = new PeerGroup(blockStore, params, chain, 1000); + peerGroup = new PeerGroup(params, chain, 1000); // Support for testing disconnect events in a non-racy manner. peerGroup.addEventListener(new AbstractPeerEventListener() { @@ -95,6 +95,33 @@ public class PeerGroupTest extends TestWithNetworkConnections { peerGroup.stop(); } + @Test + public void receiveTxBroadcast() throws Exception { + // Check that when we receive transactions on all our peers, we do the right thing. + + // Create a couple of peers. + peerGroup.addWallet(wallet); + MockNetworkConnection n1 = createMockNetworkConnection(); + Peer p1 = new Peer(params, blockChain, n1); + MockNetworkConnection n2 = createMockNetworkConnection(); + Peer p2 = new Peer(params, blockChain, n2); + peerGroup.start(); + peerGroup.addPeer(p1); + peerGroup.addPeer(p2); + + BigInteger value = Utils.toNanoCoins(1, 0); + Transaction t1 = TestUtils.createFakeTx(unitTestParams, value, address); + InventoryMessage inv = new InventoryMessage(unitTestParams); + inv.addItem(new InventoryItem(InventoryItem.Type.Transaction, t1.getHash())); + n1.inbound(inv); + n2.inbound(inv); + GetDataMessage getdata = (GetDataMessage) n1.outbound(); + assertNull(n2.outbound()); // Only one peer is used to download. + n1.inbound(t1); + n1.outbound(); // Wait for processing to complete. + assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + } + @Test public void singleDownloadPeer1() throws Exception { // Check that we don't attempt to retrieve blocks on multiple peers. diff --git a/tests/com/google/bitcoin/core/PeerTest.java b/tests/com/google/bitcoin/core/PeerTest.java index 8953ab41..2675cf74 100644 --- a/tests/com/google/bitcoin/core/PeerTest.java +++ b/tests/com/google/bitcoin/core/PeerTest.java @@ -20,6 +20,7 @@ import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; @@ -38,6 +39,7 @@ public class PeerTest extends TestWithNetworkConnections { conn = createMockNetworkConnection(); peer = new Peer(unitTestParams, blockChain, conn); + peer.addWallet(wallet); } @Test @@ -139,6 +141,26 @@ public class PeerTest extends TestWithNetworkConnections { assertNull(message != null ? message.toString() : "", message); } + @Test + public void invDownloadTx() throws Exception { + peer.setDownloadData(true); + // Make a transaction and tell the peer we have it.; + BigInteger value = Utils.toNanoCoins(1, 0); + Transaction tx = TestUtils.createFakeTx(unitTestParams, value, address); + InventoryMessage inv = new InventoryMessage(unitTestParams); + InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash()); + inv.addItem(item); + conn.inbound(inv); + // Peer hasn't seen it before, so will ask for it. + runPeer(peer, conn); + GetDataMessage message = (GetDataMessage) conn.popOutbound(); + assertEquals(1, message.getItems().size()); + assertEquals(tx.getHash(), message.getItems().get(0).hash); + conn.inbound(tx); + runPeer(peer, conn); + assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + } + // Check that inventory message containing blocks we want is processed correctly. @Test public void newBlock() throws Exception { @@ -148,7 +170,6 @@ public class PeerTest extends TestWithNetworkConnections { Block b1 = TestUtils.createFakeBlock(unitTestParams, blockStore).block; blockChain.add(b1); Block b2 = TestUtils.makeSolvedTestBlock(unitTestParams, b1); - Block b3 = TestUtils.makeSolvedTestBlock(unitTestParams, b2); conn.setVersionMessageForHeight(unitTestParams, 100); // Receive notification of a new block. InventoryMessage inv = new InventoryMessage(unitTestParams); @@ -158,6 +179,8 @@ public class PeerTest extends TestWithNetworkConnections { // Response to the getdata message. conn.inbound(b2); + expect(listener.onPreMessageReceived(eq(peer), eq(inv))).andReturn(inv); + expect(listener.onPreMessageReceived(eq(peer), eq(b2))).andReturn(b2); listener.onBlocksDownloaded(eq(peer), anyObject(Block.class), eq(98)); expectLastCall(); diff --git a/tests/com/google/bitcoin/core/TestWithNetworkConnections.java b/tests/com/google/bitcoin/core/TestWithNetworkConnections.java index 0e24e745..d19d7886 100644 --- a/tests/com/google/bitcoin/core/TestWithNetworkConnections.java +++ b/tests/com/google/bitcoin/core/TestWithNetworkConnections.java @@ -17,6 +17,7 @@ package com.google.bitcoin.core; import com.google.bitcoin.store.MemoryBlockStore; +import com.google.bitcoin.utils.BriefLogFormatter; import org.easymock.IMocksControl; import java.io.IOException; @@ -31,14 +32,23 @@ public class TestWithNetworkConnections { protected NetworkParameters unitTestParams; protected MemoryBlockStore blockStore; protected BlockChain blockChain; + protected Wallet wallet; + protected ECKey key; + protected Address address; public void setUp() throws Exception { + BriefLogFormatter.init(); + control = createStrictControl(); control.checkOrder(true); unitTestParams = NetworkParameters.unitTests(); blockStore = new MemoryBlockStore(unitTestParams); - blockChain = new BlockChain(unitTestParams, new Wallet(unitTestParams), blockStore); + wallet = new Wallet(unitTestParams); + key = new ECKey(); + address = key.toAddress(unitTestParams); + wallet.addKey(key); + blockChain = new BlockChain(unitTestParams, wallet, blockStore); } protected MockNetworkConnection createMockNetworkConnection() { diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 777c08b3..b35d8186 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -16,19 +16,23 @@ package com.google.bitcoin.core; -import com.google.bitcoin.store.BlockStore; -import com.google.bitcoin.store.MemoryBlockStore; -import org.junit.Before; -import org.junit.Test; - -import java.math.BigInteger; -import java.util.List; - import static com.google.bitcoin.core.TestUtils.createFakeBlock; import static com.google.bitcoin.core.TestUtils.createFakeTx; import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString; import static com.google.bitcoin.core.Utils.toNanoCoins; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.math.BigInteger; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.google.bitcoin.store.BlockStore; +import com.google.bitcoin.store.MemoryBlockStore; +import com.google.bitcoin.utils.BriefLogFormatter; public class WalletTest { static final NetworkParameters params = NetworkParameters.unitTests(); @@ -45,6 +49,8 @@ public class WalletTest { wallet = new Wallet(params); wallet.addKey(myKey); blockStore = new MemoryBlockStore(params); + + BriefLogFormatter.init(); } @Test @@ -53,7 +59,7 @@ public class WalletTest { BigInteger v1 = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(params, v1, myAddress); - wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); @@ -70,7 +76,7 @@ public class WalletTest { // We have NOT proven that the signature is correct! - wallet.confirmSend(t2); + wallet.commitTx(t2); assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING)); assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT)); assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL)); @@ -82,14 +88,14 @@ public class WalletTest { BigInteger v1 = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(params, v1, myAddress); - wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = createFakeTx(params, v2, myAddress); - wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); + wallet.receiveFromBlock(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); assertEquals(1, wallet.getPoolSize(Wallet.Pool.INACTIVE)); assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL)); @@ -110,7 +116,7 @@ public class WalletTest { } }; wallet.addEventListener(listener); - wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN); assertTrue(didRun[0]); } @@ -125,19 +131,16 @@ public class WalletTest { StoredBlock b2 = createFakeBlock(params, blockStore, t2).storedBlock; BigInteger expected = toNanoCoins(5, 50); assertEquals(0, wallet.getPoolSize(Wallet.Pool.ALL)); - wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); - wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(t2, b2, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(2, wallet.getPoolSize(Wallet.Pool.UNSPENT)); 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); - // TODO: This test depends on the coin selection which is non-deterministic. FIX! - // assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); - // assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT)); + wallet.commitTx(spend); assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING)); // Available and estimated balances should not be the same. We don't check the exact available balance here @@ -148,7 +151,7 @@ public class WalletTest { // Now confirm the transaction by including it into a block. StoredBlock b3 = createFakeBlock(params, blockStore, spend).storedBlock; - wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(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); @@ -164,21 +167,21 @@ public class WalletTest { public void blockChainCatchup() throws Exception { Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress); StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock; - wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(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. We can do this by just not confirming the transaction as // createSend is stateless. StoredBlock b2 = createFakeBlock(params, blockStore, send1).storedBlock; - wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(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); + wallet.commitTx(send2); StoredBlock b3 = createFakeBlock(params, blockStore, send2).storedBlock; - wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(send2, b3, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80"); } @@ -186,7 +189,7 @@ public class WalletTest { public void balances() throws Exception { BigInteger nanos = Utils.toNanoCoins(1, 0); Transaction tx1 = createFakeTx(params, nanos, myAddress); - wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(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); @@ -206,7 +209,7 @@ public class WalletTest { // Note that tx is no longer valid: it spends more than it imports. However checking transactions balance // correctly isn't possible in SPV mode because value is a property of outputs not inputs. Without all // transactions you can't check they add up. - wallet.receive(tx, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN); // Now the other guy creates a transaction which spends that change. Transaction tx2 = new Transaction(params); tx2.addInput(output); @@ -223,20 +226,20 @@ public class WalletTest { BigInteger coinHalf = Utils.toNanoCoins(0, 50); // Start by giving us 1 coin. Transaction inbound1 = createFakeTx(params, coin1, myAddress); - wallet.receive(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN); // Send half to some other guy. Sending only half then waiting for a confirm is important to ensure the tx is // in the unspent pool, not pending or spent. assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT)); assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL)); Address someOtherGuy = new ECKey().toAddress(params); Transaction outbound1 = wallet.createSend(someOtherGuy, coinHalf); - wallet.confirmSend(outbound1); - wallet.receive(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.commitTx(outbound1); + wallet.receiveFromBlock(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN); // That other guy gives us the coins right back. Transaction inbound2 = new Transaction(params); inbound2.addOutput(new TransactionOutput(params, inbound2, coinHalf, myAddress)); inbound2.addInput(outbound1.getOutputs().get(0)); - wallet.receive(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(coin1, wallet.getBalance()); } @@ -266,19 +269,124 @@ public class WalletTest { // Receive 1 BTC. BigInteger nanos = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(params, nanos, myAddress); - wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN); // Create a send to a merchant. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50)); // Create a double spend. Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50)); // Broadcast send1. - wallet.confirmSend(send1); + wallet.commitTx(send1); // Receive a block that overrides it. - wallet.receive(send2, null, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(send2, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(send1, eventDead[0]); assertEquals(send2, eventReplacement[0]); } + @Test + public void pending1() throws Exception { + // Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate. + final BigInteger nanos = Utils.toNanoCoins(1, 0); + final Transaction t1 = createFakeTx(params, nanos, myAddress); + + // First one is "called" second is "pending". + final boolean[] flags = new boolean[2]; + wallet.addEventListener(new AbstractWalletEventListener() { + @Override + public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { + // Check we got the expected transaction. + assertEquals(tx, t1); + // Check that it's considered to be pending inclusion in the block chain. + assertEquals(prevBalance, BigInteger.ZERO); + assertEquals(newBalance, nanos); + flags[0] = true; + flags[1] = tx.isPending(); + } + }); + + wallet.receivePending(t1); + assertTrue(flags[0]); + assertTrue(flags[1]); // is pending + flags[0] = false; + // Check we don't get notified if we receive it again. + wallet.receivePending(t1); + assertFalse(flags[0]); + // Now check again when we receive it via a block. + flags[1] = true; + wallet.receiveFromBlock(t1, createFakeBlock(params, blockStore, t1).storedBlock, + BlockChain.NewBlockType.BEST_CHAIN); + assertTrue(flags[0]); + assertFalse(flags[1]); // is not pending + // Check we don't get notified about an irrelevant transaction. + flags[0] = false; + flags[1] = false; + Transaction irrelevant = createFakeTx(params, nanos, new ECKey().toAddress(params)); + wallet.receivePending(irrelevant); + assertFalse(flags[0]); + } + + @Test + public void pending2() throws Exception { + // Check that if we receive a pending tx we did not send, it updates our spent flags correctly. + + // Receive some coins. + BigInteger nanos = Utils.toNanoCoins(1, 0); + Transaction t1 = createFakeTx(params, nanos, myAddress); + StoredBlock b1 = createFakeBlock(params, blockStore, t1).storedBlock; + wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(nanos, wallet.getBalance()); + // Create a spend with them, but don't commit it (ie it's from somewhere else but using our keys). + Transaction t2 = wallet.createSend(new ECKey().toAddress(params), nanos); + // Now receive it as pending. + wallet.receivePending(t2); + // Our balance is now zero. + assertEquals(BigInteger.ZERO, wallet.getBalance()); + } + + @Test + public void pending3() throws Exception { + // Check that if we receive a pending tx, and it's overridden by a double spend from the main chain, we + // are notified that it's dead. This should work even if the pending tx inputs are NOT ours, ie, they don't + // connect to anything. + BigInteger nanos = Utils.toNanoCoins(1, 0); + + // Create two transactions that share the same input tx. + Address badGuy = new ECKey().toAddress(params); + Transaction doubleSpentTx = new Transaction(params); + TransactionOutput doubleSpentOut = new TransactionOutput(params, doubleSpentTx, nanos, badGuy); + doubleSpentTx.addOutput(doubleSpentOut); + Transaction t1 = new Transaction(params); + TransactionOutput o1 = new TransactionOutput(params, t1, nanos, myAddress); + t1.addOutput(o1); + t1.addInput(doubleSpentOut); + Transaction t2 = new Transaction(params); + TransactionOutput o2 = new TransactionOutput(params, t2, nanos, badGuy); + t2.addOutput(o2); + t2.addInput(doubleSpentOut); + + final Transaction[] called = new Transaction[2]; + wallet.addEventListener(new AbstractWalletEventListener() { + public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { + called[0] = tx; + } + + public void onDeadTransaction(Wallet wallet, Transaction deadTx, Transaction replacementTx) { + called[0] = deadTx; + called[1] = replacementTx; + } + }); + + assertEquals(BigInteger.ZERO, wallet.getBalance()); + wallet.receivePending(t1); + assertEquals(t1, called[0]); + assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED)); + // Now receive a double spend on the main chain. + called[0] = called[1] = null; + wallet.receiveFromBlock(t2, createFakeBlock(params, blockStore, t2).storedBlock, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(BigInteger.ZERO, wallet.getBalance()); + assertEquals(t1, called[0]); // dead + assertEquals(t2, called[1]); // replacement + } + @Test public void transactionsList() throws Exception { // Check the wallet can give us an ordered list of all received transactions. @@ -286,12 +394,12 @@ public class WalletTest { // Receive a coin. Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress); StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock; - wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN); // Receive half a coin 10 minutes later. Utils.rollMockClock(60 * 10); Transaction tx2 = createFakeTx(params, Utils.toNanoCoins(0, 5), myAddress); StoredBlock b2 = createFakeBlock(params, blockStore, tx1).storedBlock; - wallet.receive(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN); + wallet.receiveFromBlock(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN); // Check we got them back in order. List transactions = wallet.getTransactionsByTime(); assertEquals(tx2, transactions.get(0)); @@ -307,7 +415,7 @@ public class WalletTest { Transaction tx3 = wallet.createSend(new ECKey().toAddress(params), Utils.toNanoCoins(0, 5)); // Does not appear in list yet. assertEquals(2, wallet.getTransactionsByTime().size()); - wallet.confirmSend(tx3); + wallet.commitTx(tx3); // Now it does. transactions = wallet.getTransactionsByTime(); assertEquals(3, transactions.size());