3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 07:12:17 +00:00

Decouple the wallet from the block chain by introducing a BlockChainListener interface, and making the Wallet implement it. Resolves issue 94.

This commit is contained in:
Mike Hearn 2012-12-14 17:04:30 +01:00
parent 5abd546eb2
commit eacda0bdfc
5 changed files with 147 additions and 45 deletions

View File

@ -85,22 +85,22 @@ public abstract class AbstractBlockChain {
private final Object chainHeadLock = new Object();
protected final NetworkParameters params;
private final List<Wallet> wallets;
private final List<BlockChainListener> 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<Sha256Hash, Block> orphanBlocks = new LinkedHashMap<Sha256Hash, Block>();
/**
* 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<Wallet> wallets,
public AbstractBlockChain(NetworkParameters params, List<BlockChainListener> 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<Wallet>(wallets);
this.listeners = new ArrayList<BlockChainListener>(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<Transaction> transactions, boolean clone) throws VerificationException {
private void sendTransactionsToListener(StoredBlock block, NewBlockType blockType, BlockChainListener listener,
List<Transaction> 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

View File

@ -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<Wallet>(), blockStore);
this(params, new ArrayList<BlockChainListener>(), 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<Wallet>(), blockStore);
this(params, new ArrayList<BlockChainListener>(), 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<Wallet> wallets,
public BlockChain(NetworkParameters params, List<BlockChainListener> wallets,
BlockStore blockStore) throws BlockStoreException {
super(params, wallets, blockStore);
this.blockStore = blockStore;

View File

@ -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 {
/**
* <p>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).</p>
*/
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.<p>
*
* The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.
*/
void reorganize(StoredBlock splitPoint, List<StoredBlock> oldBlocks,
List<StoredBlock> 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;
/**
* <p>Called by the {@link BlockChain} when we receive a new block that contains a relevant transaction.</p>
*
* <p>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.</p>
*/
void receiveFromBlock(Transaction tx, StoredBlock block,
BlockChain.NewBlockType blockType) throws VerificationException;
}

View File

@ -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.*;
/**
* <p>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<Wallet>(), blockStore);
this(params, new ArrayList<BlockChainListener>(), 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<Wallet>(), blockStore);
this(params, new ArrayList<BlockChainListener>(), blockStore);
}
/**
* Constructs a BlockChain connected to the given list of wallets and a store.
*/
public FullPrunedBlockChain(NetworkParameters params, List<Wallet> wallets,
public FullPrunedBlockChain(NetworkParameters params, List<BlockChainListener> listeners,
FullPrunedBlockStore blockStore) throws BlockStoreException {
super(params, wallets, blockStore);
super(params, listeners, blockStore);
this.blockStore = blockStore;
}

View File

@ -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.</p>
*/
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,
* <p>Don't call this directly. It's not intended for API users.</p>
*
* <p>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.<p>
* should be so.</p>
*
* The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.
* <p>The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.</p>
*/
synchronized void reorganize(StoredBlock splitPoint, List<StoredBlock> oldBlocks, List<StoredBlock> newBlocks) throws VerificationException {
public synchronized void reorganize(StoredBlock splitPoint, List<StoredBlock> oldBlocks,
List<StoredBlock> newBlocks) throws VerificationException {
// This runs on any peer thread with the block chain synchronized.
//
// The reorganize functionality of the wallet is tested in ChainSplitTests.