From eacda0bdfcf3a51112362b92ab9306d309379856 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 14 Dec 2012 17:04:30 +0100 Subject: [PATCH] Decouple the wallet from the block chain by introducing a BlockChainListener interface, and making the Wallet implement it. Resolves issue 94. --- .../bitcoin/core/AbstractBlockChain.java | 87 ++++++++++++++----- .../com/google/bitcoin/core/BlockChain.java | 8 +- .../bitcoin/core/BlockChainListener.java | 64 ++++++++++++++ .../bitcoin/core/FullPrunedBlockChain.java | 20 ++--- .../java/com/google/bitcoin/core/Wallet.java | 13 +-- 5 files changed, 147 insertions(+), 45 deletions(-) create mode 100644 core/src/main/java/com/google/bitcoin/core/BlockChainListener.java diff --git a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java index d521dfb9..463f0c09 100644 --- a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java @@ -85,22 +85,22 @@ public abstract class AbstractBlockChain { private final Object chainHeadLock = new Object(); protected final NetworkParameters params; - private final List wallets; + private final List listeners; // Holds blocks that we have received but can't plug into the chain yet, eg because they were created whilst we // were downloading the block chain. private final LinkedHashMap orphanBlocks = new LinkedHashMap(); /** - * Constructs a BlockChain connected to the given list of wallets and a store. + * Constructs a BlockChain connected to the given list of listeners (eg, wallets) and a store. */ - public AbstractBlockChain(NetworkParameters params, List wallets, + public AbstractBlockChain(NetworkParameters params, List listeners, BlockStore blockStore) throws BlockStoreException { this.blockStore = blockStore; chainHead = blockStore.getChainHead(); log.info("chain head is at height {}:\n{}", chainHead.getHeight(), chainHead.getHeader()); this.params = params; - this.wallets = new ArrayList(wallets); + this.listeners = new ArrayList(listeners); } /** @@ -110,7 +110,21 @@ public abstract class AbstractBlockChain { * wallets is not well tested! */ public synchronized void addWallet(Wallet wallet) { - wallets.add(wallet); + listeners.add(wallet); + } + + /** + * Adds a generic {@link BlockChainListener} listener to the chain. + */ + public synchronized void addListener(BlockChainListener listener) { + listeners.add(listener); + } + + /** + * Removes the given {@link BlockChainListener} from the chain. + */ + public synchronized void removeListener(BlockChainListener listener) { + listeners.remove(listener); } /** @@ -311,19 +325,35 @@ public abstract class AbstractBlockChain { StoredBlock newStoredBlock = addToBlockStore(storedPrev, block.cloneAsHeader(), txOutChanges); setChainHead(newStoredBlock); log.debug("Chain is now {} blocks high", newStoredBlock.getHeight()); - // Notify the wallets of the new block, so the depth and workDone of stored transactions can be updated. - // The wallets need to know how deep each transaction is so coinbases aren't used before maturity. - for (int i = 0; i < wallets.size(); i++) { - Wallet wallet = wallets.get(i); + // Notify the listeners of the new block, so the depth and workDone of stored transactions can be updated + // (in the case of the listener being a wallet). Wallets need to know how deep each transaction is so + // coinbases aren't used before maturity. + final BlockChainListener first = listeners.size() > 0 ? listeners.get(0) : null; + for (int i = 0; i < listeners.size(); i++) { + BlockChainListener listener = listeners.get(i); if (block.transactions != null) { // If this is not the first wallet, ask for the transactions to be duplicated before being given // to the wallet when relevant. This ensures that if we have two connected wallets and a tx that // is relevant to both of them, they don't end up accidentally sharing the same object (which can // result in temporary in-memory corruption during re-orgs). See bug 257. We only duplicate in // the case of multiple wallets to avoid an unnecessary efficiency hit in the common case. - sendTransactionsToWallet(newStoredBlock, NewBlockType.BEST_CHAIN, wallet, block.transactions, i > 0); + sendTransactionsToListener(newStoredBlock, NewBlockType.BEST_CHAIN, listener, block.transactions, + i > 0); + } + // Allow the listener to have removed itself. + if (i == listeners.size()) { + break; // Listener removed itself and it was the last one. + } else if (listeners.get(i) != listener) { + i--; // Listener removed itself and it was not the last one. + break; + } + listener.notifyNewBestBlock(newStoredBlock.getHeader()); + if (i == listeners.size()) { + break; // Listener removed itself and it was the last one. + } else if (listeners.get(i) != listener) { + i--; // Listener removed itself and it was not the last one. + break; } - wallet.notifyNewBestBlock(newStoredBlock.getHeader()); } } else { // This block connects to somewhere other than the top of the best known chain. We treat these differently. @@ -363,14 +393,19 @@ public abstract class AbstractBlockChain { // If we do, send them to the wallet but state that they are on a side chain so it knows not to try and // spend them until they become activated. if (block.transactions != null) { - for (int i = 0; i < wallets.size(); i++) { - Wallet wallet = wallets.get(i); + for (int i = 0; i < listeners.size(); i++) { + BlockChainListener listener = listeners.get(i); // If this is not the first wallet, ask for the transactions to be duplicated before being given // to the wallet when relevant. This ensures that if we have two connected wallets and a tx that // is relevant to both of them, they don't end up accidentally sharing the same object (which can // result in temporary in-memory corruption during re-orgs). See bug 257. We only duplicate in // the case of multiple wallets to avoid an unnecessary efficiency hit in the common case. - sendTransactionsToWallet(newBlock, NewBlockType.SIDE_CHAIN, wallet, block.transactions, i > 0); + sendTransactionsToListener(newBlock, NewBlockType.SIDE_CHAIN, listener, block.transactions, i > 0); + if (i == listeners.size()) { + break; // Listener removed itself and it was the last one. + } else if (listeners.get(i) != listener) { + i--; // Listener removed itself and it was not the last one. + } } } @@ -453,11 +488,17 @@ public abstract class AbstractBlockChain { // (Finally) write block to block store storedNewHead = addToBlockStore(storedPrev, newChainHead.getHeader()); } - // Now inform the wallets. This is necessary so the set of currently active transactions (that we can spend) + // Now inform the listeners. 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. - for (Wallet wallet : wallets) { - wallet.reorganize(splitPoint, oldBlocks, newBlocks); + for (int i = 0; i < listeners.size(); i++) { + BlockChainListener listener = listeners.get(i); + listener.reorganize(splitPoint, oldBlocks, newBlocks); + if (i == listeners.size()) { + break; // Listener removed itself and it was the last one. + } else if (listeners.get(i) != listener) { + i--; // Listener removed itself and it was not the last one. + } } // Update the pointer to the best known block. setChainHead(storedNewHead); @@ -516,14 +557,14 @@ public abstract class AbstractBlockChain { SIDE_CHAIN } - private void sendTransactionsToWallet(StoredBlock block, NewBlockType blockType, Wallet wallet, - List transactions, boolean clone) throws VerificationException { + private void sendTransactionsToListener(StoredBlock block, NewBlockType blockType, BlockChainListener listener, + List transactions, boolean clone) throws VerificationException { for (Transaction tx : transactions) { try { - if (wallet.isTransactionRelevant(tx)) { + if (listener.isTransactionRelevant(tx)) { if (clone) tx = new Transaction(tx.params, tx.bitcoinSerialize()); - wallet.receiveFromBlock(tx, block, blockType); + listener.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 @@ -682,8 +723,8 @@ public abstract class AbstractBlockChain { private boolean containsRelevantTransactions(Block block) { for (Transaction tx : block.transactions) { try { - for (Wallet wallet : wallets) { - if (wallet.isTransactionRelevant(tx)) return true; + for (BlockChainListener listener : listeners) { + if (listener.isTransactionRelevant(tx)) return true; } } catch (ScriptException e) { // We don't want scripts we don't understand to break the block chain so just note that this tx was diff --git a/core/src/main/java/com/google/bitcoin/core/BlockChain.java b/core/src/main/java/com/google/bitcoin/core/BlockChain.java index 685979cd..3efbaaf7 100644 --- a/core/src/main/java/com/google/bitcoin/core/BlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/BlockChain.java @@ -38,7 +38,7 @@ public class BlockChain extends AbstractBlockChain { * {@link com.google.bitcoin.store.BoundedOverheadBlockStore} if you'd like to ensure fast startup the next time you run the program. */ public BlockChain(NetworkParameters params, Wallet wallet, BlockStore blockStore) throws BlockStoreException { - this(params, new ArrayList(), blockStore); + this(params, new ArrayList(), blockStore); if (wallet != null) addWallet(wallet); } @@ -48,13 +48,13 @@ public class BlockChain extends AbstractBlockChain { * and receiving coins but rather, just want to explore the network data structures. */ public BlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException { - this(params, new ArrayList(), blockStore); + this(params, new ArrayList(), blockStore); } /** - * Constructs a BlockChain connected to the given list of wallets and a store. + * Constructs a BlockChain connected to the given list of listeners and a store. */ - public BlockChain(NetworkParameters params, List wallets, + public BlockChain(NetworkParameters params, List wallets, BlockStore blockStore) throws BlockStoreException { super(params, wallets, blockStore); this.blockStore = blockStore; diff --git a/core/src/main/java/com/google/bitcoin/core/BlockChainListener.java b/core/src/main/java/com/google/bitcoin/core/BlockChainListener.java new file mode 100644 index 00000000..95356d18 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/core/BlockChainListener.java @@ -0,0 +1,64 @@ +/** + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import java.util.List; + +/** + * A block chain listener can be connected to a {@link BlockChain} and have its methods called when various things + * happen that modify the state of the chain. For example: new blocks being received, a re-org occurring, or the + * best chain head changing. + */ +public interface BlockChainListener { + /** + *

Called by the {@link BlockChain} when a new block on the best chain is seen, AFTER relevant + * transactions are extracted and sent to us UNLESS the new block caused a re-org, in which case this will + * not be called (the {@link Wallet#reorganize(StoredBlock, java.util.List, java.util.List)} method will + * call this one in that case).

+ */ + void notifyNewBestBlock(Block block) throws VerificationException; + + /** + * Called by the {@link BlockChain} when the best chain (representing total work done) has changed. In this case, + * we need to go through our transactions and find out if any have become invalid. It's possible for our balance + * to go down in this case: money we thought we had can suddenly vanish if the rest of the network agrees it + * should be so.

+ * + * The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last. + */ + void reorganize(StoredBlock splitPoint, List oldBlocks, + List newBlocks) throws VerificationException; + + /** + * Returns true if the given transaction is interesting to the listener. If yes, then the transaction will + * be provided via the receiveFromBlock method. This method is essentially an optimization that lets BlockChain + * bypass verification of a blocks merkle tree if no listeners are interested, which can save time when processing + * full blocks on mobile phones. It's likely the method will be removed in future and replaced with an alternative + * mechanism that involves listeners providing all keys that are interesting. + */ + boolean isTransactionRelevant(Transaction tx) throws ScriptException; + + /** + *

Called by the {@link BlockChain} when we receive a new block that contains a relevant transaction.

+ * + *

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.

+ */ + void receiveFromBlock(Transaction tx, StoredBlock block, + BlockChain.NewBlockType blockType) throws VerificationException; +} diff --git a/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java index 9040db00..d5779a2b 100644 --- a/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java @@ -18,20 +18,14 @@ package com.google.bitcoin.core; import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.store.FullPrunedBlockStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.*; /** *

A FullPrunedBlockChain works in conjunction with a {@link FullPrunedBlockStore} to provide a fully verifying * block chain. Fully verifying means all unspent transaction outputs are stored. Once a transaction output is spent @@ -50,7 +44,7 @@ public class FullPrunedBlockChain extends AbstractBlockChain { * one from scratch, or you can deserialize a saved wallet from disk using {@link Wallet#loadFromFile(java.io.File)} */ public FullPrunedBlockChain(NetworkParameters params, Wallet wallet, FullPrunedBlockStore blockStore) throws BlockStoreException { - this(params, new ArrayList(), blockStore); + this(params, new ArrayList(), blockStore); if (wallet != null) addWallet(wallet); } @@ -60,15 +54,15 @@ public class FullPrunedBlockChain extends AbstractBlockChain { * and receiving coins but rather, just want to explore the network data structures. */ public FullPrunedBlockChain(NetworkParameters params, FullPrunedBlockStore blockStore) throws BlockStoreException { - this(params, new ArrayList(), blockStore); + this(params, new ArrayList(), blockStore); } /** * Constructs a BlockChain connected to the given list of wallets and a store. */ - public FullPrunedBlockChain(NetworkParameters params, List wallets, + public FullPrunedBlockChain(NetworkParameters params, List listeners, FullPrunedBlockStore blockStore) throws BlockStoreException { - super(params, wallets, blockStore); + super(params, listeners, blockStore); this.blockStore = blockStore; } diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 5e8304ae..d09bca50 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -67,7 +67,7 @@ import static com.google.common.base.Preconditions.*; * {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.core.Wallet.AutosaveEventListener)} * for more information about this.

*/ -public class Wallet implements Serializable { +public class Wallet implements Serializable, BlockChainListener { private static final Logger log = LoggerFactory.getLogger(Wallet.class); private static final long serialVersionUID = 2L; @@ -1780,14 +1780,17 @@ public class Wallet implements Serializable { } /** - * Called by the {@link BlockChain} when the best chain (representing total work done) has changed. In this case, + *

Don't call this directly. It's not intended for API users.

+ * + *

Called by the {@link BlockChain} when the best chain (representing total work done) has changed. In this case, * we need to go through our transactions and find out if any have become invalid. It's possible for our balance * to go down in this case: money we thought we had can suddenly vanish if the rest of the network agrees it - * should be so.

+ * should be so.

* - * The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last. + *

The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.

*/ - synchronized void reorganize(StoredBlock splitPoint, List oldBlocks, List newBlocks) throws VerificationException { + public synchronized void reorganize(StoredBlock splitPoint, List oldBlocks, + List newBlocks) throws VerificationException { // This runs on any peer thread with the block chain synchronized. // // The reorganize functionality of the wallet is tested in ChainSplitTests.