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 65013b9a..b79d77a5 100644 --- a/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/AbstractBlockChain.java @@ -76,7 +76,7 @@ public abstract class AbstractBlockChain { * greater work than the one obtained by following this one down. In that case a reorganize is triggered, * potentially invalidating transactions in our wallet. */ - private StoredBlock chainHead; + protected StoredBlock chainHead; // The chainHead field is read/written synchronized with this object rather than BlockChain. However writing is // also guaranteed to happen whilst BlockChain is synchronized (see setChainHead). The goal of this is to let @@ -157,11 +157,11 @@ public abstract class AbstractBlockChain { throws BlockStoreException, VerificationException; /** - * Called before setting chain head in memory, but after writing the blockstore to disk. - * Can be used to commit database transactions that were started by - * disconnectTransactions/connectTransactions. + * Called before setting chain head in memory. + * Should write the new head to block store and then commit any database transactions + * that were started by disconnectTransactions/connectTransactions. */ - protected abstract void preSetChainHead() throws BlockStoreException; + protected abstract void doSetChainHead(StoredBlock chainHead) throws BlockStoreException; /** * Called if we (possibly) previously called disconnectTransaction/connectTransactions, @@ -170,6 +170,12 @@ public abstract class AbstractBlockChain { * disconnectTransactions/connectTransactions. */ protected abstract void notSettingChainHead() throws BlockStoreException; + + /** + * For a standard BlockChain, this should return blockStore.get(hash), + * for a FullPrunedBlockChain blockStore.getOnceUndoableStoredBlock(hash) + */ + protected abstract StoredBlock getStoredBlockInCurrentScope(Sha256Hash hash) throws BlockStoreException; /** * Processes a received block and tries to add it to the chain. If there's something wrong with the block an @@ -275,7 +281,7 @@ public abstract class AbstractBlockChain { } // Try linking it to a place in the currently known blocks. - StoredBlock storedPrev = blockStore.get(block.getPrevBlockHash()); + StoredBlock storedPrev = getStoredBlockInCurrentScope(block.getPrevBlockHash()); if (storedPrev == null) { // We can't find the previous block. Probably we are still in the process of downloading the chain and a @@ -579,8 +585,7 @@ public abstract class AbstractBlockChain { } private void setChainHead(StoredBlock chainHead) throws BlockStoreException { - blockStore.setChainHead(chainHead); - preSetChainHead(); + doSetChainHead(chainHead); synchronized (chainHeadLock) { this.chainHead = chainHead; } @@ -604,8 +609,8 @@ public abstract class AbstractBlockChain { Block block = iter.next(); log.debug("Trying to connect {}", block.getHash()); // Look up the blocks previous. - StoredBlock prev = blockStore.get(block.getPrevBlockHash()); - if (prev == null) { + StoredBlock prev = getStoredBlockInCurrentScope(block.getPrevBlockHash()); + if (prev == null || prev.getHeight() > getChainHead().getHeight()) { // This is still an unconnected/orphan block. log.debug(" but it is not connectable right now"); continue; 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 3efbaaf7..8106b642 100644 --- a/core/src/main/java/com/google/bitcoin/core/BlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/BlockChain.java @@ -99,12 +99,17 @@ public class BlockChain extends AbstractBlockChain { } @Override - protected void preSetChainHead() { - // We don't use DB transactions here, so we don't need to do anything + protected void doSetChainHead(StoredBlock chainHead) throws BlockStoreException { + blockStore.setChainHead(chainHead); } @Override protected void notSettingChainHead() throws BlockStoreException { // We don't use DB transactions here, so we don't need to do anything } + + @Override + protected StoredBlock getStoredBlockInCurrentScope(Sha256Hash hash) throws BlockStoreException { + return blockStore.get(hash); + } } 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 d5779a2b..933d7242 100644 --- a/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java @@ -64,6 +64,8 @@ public class FullPrunedBlockChain extends AbstractBlockChain { FullPrunedBlockStore blockStore) throws BlockStoreException { super(params, listeners, blockStore); this.blockStore = blockStore; + // Ignore upgrading for now + this.chainHead = blockStore.getVerifiedChainHead(); } @Override @@ -409,7 +411,8 @@ public class FullPrunedBlockChain extends AbstractBlockChain { } @Override - protected void preSetChainHead() throws BlockStoreException { + protected void doSetChainHead(StoredBlock chainHead) throws BlockStoreException { + blockStore.setVerifiedChainHead(chainHead); blockStore.commitDatabaseBatchWrite(); } @@ -417,4 +420,9 @@ public class FullPrunedBlockChain extends AbstractBlockChain { protected void notSettingChainHead() throws BlockStoreException { blockStore.abortDatabaseBatchWrite(); } + + @Override + protected StoredBlock getStoredBlockInCurrentScope(Sha256Hash hash) throws BlockStoreException { + return blockStore.getOnceUndoableStoredBlock(hash); + } } diff --git a/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java index c613b6af..5211abc5 100644 --- a/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java +++ b/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java @@ -24,8 +24,17 @@ import com.google.bitcoin.core.StoredUndoableBlock; /** *

An implementor of FullPrunedBlockStore saves StoredBlock objects to some storage mechanism.

* - *

It should store the {@link StoredUndoableBlock}s of a number of recent blocks. - * It is advisable to store any {@link StoredUndoableBlock} which has a height > head.height - N. + *

In addition to keeping tack of a chain using {@link StoredBlock}s, it should also keep track of a second + * copy of the chain which holds {@link StoredUndoableBlock}s. In this way, an application can perform a + * headers-only initial sync and then use that information to more efficiently download a locally verified + * full copy of the block chain.

+ * + *

A FullPrunedBlockStore should function well as a standard {@link BlockStore} and then be able to + * trivially switch to being used as a FullPrunedBlockStore.

+ * + *

It should store the {@link StoredUndoableBlock}s of a number of recent blocks before verifiedHead.height and + * all those after verifiedHead.height. + * It is advisable to store any {@link StoredUndoableBlock} which has a height > verifiedHead.height - N. * Because N determines the memory usage, it is recommended that N be customizable. N should be chosen such that * re-orgs beyond that point are vanishingly unlikely, for example, a few thousand blocks is a reasonable choice.

* @@ -41,11 +50,21 @@ import com.google.bitcoin.core.StoredUndoableBlock; */ public interface FullPrunedBlockStore extends BlockStore { /** - * Saves the given {@link StoredUndoableBlock} and {@link StoredBlock}. Calculates keys from the {@link StoredBlock} - * Note that a call to put(StoredBlock) will throw a BlockStoreException if its height is > head.height - N + *

Saves the given {@link StoredUndoableBlock} and {@link StoredBlock}. Calculates keys from the {@link StoredBlock}

+ * + *

Though not required for proper function of a FullPrunedBlockStore, any user of a FullPrunedBlockStore should ensure + * that a StoredUndoableBlock for each block up to the fully verified chain head has been added to this block store using + * this function (not put(StoredBlock)), so that the ability to perform reorgs is maintained.

+ * * @throws BlockStoreException if there is a problem with the underlying storage layer, such as running out of disk space. */ void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException; + + /** + * Returns the StoredBlock that was added as a StoredUndoableBlock given a hash. The returned values block.getHash() method will be equal to the + * parameter. If no such block is found, returns null. + */ + StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException; /** * Returns a {@link StoredUndoableBlock} who's block.getHash() method will be equal to the @@ -78,6 +97,25 @@ public interface FullPrunedBlockStore extends BlockStore { */ boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException; + /** + * Returns the {@link StoredBlock} that represents the top of the chain of greatest total work that has + * been fully verified and the point in the chain at which the unspent transaction output set in this + * store represents. + */ + StoredBlock getVerifiedChainHead() throws BlockStoreException; + + /** + * Sets the {@link StoredBlock} that represents the top of the chain of greatest total work that has been + * fully verified. It should generally be set after a batch of updates to the transaction unspent output set, + * before a call to commitDatabaseBatchWrite. + * + * If chainHead has a greater height than the non-verified chain head (ie that set with + * {@link BlockStore.setChainHead}) the non-verified chain head should be set to the one set here. + * In this way a class using a FullPrunedBlockStore only in full-verification mode can ignore the regular + * {@link BlockStore} functions implemented as a part of a FullPrunedBlockStore. + */ + void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException; + /** *

Begins/Commits/Aborts a database transaction.

* diff --git a/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java index f1aba97c..d9474e26 100644 --- a/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java +++ b/core/src/main/java/com/google/bitcoin/store/H2FullPrunedBlockStore.java @@ -42,8 +42,10 @@ import java.util.List; public class H2FullPrunedBlockStore implements FullPrunedBlockStore { private static final Logger log = LoggerFactory.getLogger(H2FullPrunedBlockStore.class); - private StoredBlock chainHeadBlock; private Sha256Hash chainHeadHash; + private StoredBlock chainHeadBlock; + private Sha256Hash verifiedChainHeadHash; + private StoredBlock verifiedChainHeadBlock; private NetworkParameters params; private ThreadLocal conn; private List allConnections; @@ -56,12 +58,14 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { + "value BLOB" + ")"; static final String CHAIN_HEAD_SETTING = "chainhead"; + static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead"; static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers ( " + "hash BINARY(28) NOT NULL CONSTRAINT headers_pk PRIMARY KEY," + "chainWork BLOB NOT NULL," + "height INT NOT NULL," - + "header BLOB NOT NULL" + + "header BLOB NOT NULL," + + "wasUndoable BOOL NOT NULL" + ")"; static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableBlocks ( " @@ -203,25 +207,40 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { log.debug("H2FullPrunedBlockStore : CREATE open output table"); s.executeUpdate(CREATE_OPEN_OUTPUT_TABLE); - s.executeUpdate("INSERT INTO settings(name, value) VALUES('chainhead', NULL)"); + s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + CHAIN_HEAD_SETTING + "', NULL)"); + s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERIFIED_CHAIN_HEAD_SETTING + "', NULL)"); s.close(); createNewStore(params); } private void initFromDatabase() throws SQLException, BlockStoreException { Statement s = conn.get().createStatement(); - ResultSet rs = s.executeQuery("SELECT value FROM settings WHERE name = 'chainhead'"); + ResultSet rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + CHAIN_HEAD_SETTING + "'"); if (!rs.next()) { throw new BlockStoreException("corrupt H2 block store - no chain head pointer"); } Sha256Hash hash = new Sha256Hash(rs.getBytes(1)); - s.close(); + rs.close(); this.chainHeadBlock = get(hash); + this.chainHeadHash = hash; if (this.chainHeadBlock == null) { throw new BlockStoreException("corrupt H2 block store - head block not found"); } - this.chainHeadHash = hash; + + rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + VERIFIED_CHAIN_HEAD_SETTING + "'"); + if (!rs.next()) { + throw new BlockStoreException("corrupt H2 block store - no verified chain head pointer"); + } + hash = new Sha256Hash(rs.getBytes(1)); + rs.close(); + s.close(); + this.verifiedChainHeadBlock = get(hash); + this.verifiedChainHeadHash = hash; + if (this.verifiedChainHeadBlock == null) + { + throw new BlockStoreException("corrupt H2 block store - verified head block not found"); + } } private void createNewStore(NetworkParameters params) throws BlockStoreException { @@ -235,6 +254,7 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.genesisBlock.getHash(), genesisTransactions); put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader); + setVerifiedChainHead(storedGenesisHeader); } catch (VerificationException e) { throw new RuntimeException(e); // Cannot happen. } @@ -335,11 +355,11 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } - private void putStoredBlock(StoredBlock storedBlock) throws BlockStoreException { + private void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) throws SQLException { try { PreparedStatement s = - conn.get().prepareStatement("INSERT INTO headers(hash, chainWork, height, header)" - + " VALUES(?, ?, ?, ?)"); + conn.get().prepareStatement("INSERT INTO headers(hash, chainWork, height, header, wasUndoable)" + + " VALUES(?, ?, ?, ?, ?)"); // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28); @@ -347,18 +367,33 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { s.setBytes(2, storedBlock.getChainWork().toByteArray()); s.setInt(3, storedBlock.getHeight()); s.setBytes(4, storedBlock.getHeader().unsafeBitcoinSerialize()); + s.setBoolean(5, wasUndoable); + s.executeUpdate(); + s.close(); + } catch (SQLException e) { + // It is possible we try to add a duplicate StoredBlock if we upgraded + // In that case, we just update the entry to mark it wasUndoable + if (e.getErrorCode() != 23505 || !wasUndoable) + throw e; + + PreparedStatement s = conn.get().prepareStatement("UPDATE headers SET wasUndoable=? WHERE hash=?"); + s.setBoolean(1, true); + // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes + byte[] hashBytes = new byte[28]; + System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28); + s.setBytes(2, hashBytes); s.executeUpdate(); s.close(); - } catch (SQLException ex) { - throw new BlockStoreException(ex); } } public void put(StoredBlock storedBlock) throws BlockStoreException { maybeConnect(); - if (storedBlock.getHeight() > chainHeadBlock.getHeight() - fullStoreDepth) - throw new BlockStoreException("Putting a StoredBlock in H2FullPrunedBlockStore at height higher than head - fullStoredDepth"); - putStoredBlock(storedBlock); + try { + putUpdateStoredBlock(storedBlock, false); + } catch (SQLException e) { + throw new BlockStoreException(e); + } } public void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException { @@ -405,7 +440,11 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } s.executeUpdate(); s.close(); - putStoredBlock(storedBlock); + try { + putUpdateStoredBlock(storedBlock, true); + } catch (SQLException e) { + throw new BlockStoreException(e); + } } catch (SQLException e) { if (e.getErrorCode() != 23505) throw new BlockStoreException(e); @@ -430,15 +469,17 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } } - public StoredBlock get(Sha256Hash hash) throws BlockStoreException { + public StoredBlock get(Sha256Hash hash, boolean wasUndoableOnly) throws BlockStoreException { // Optimize for chain head if (chainHeadHash != null && chainHeadHash.equals(hash)) return chainHeadBlock; + if (verifiedChainHeadHash != null && verifiedChainHeadHash.equals(hash)) + return verifiedChainHeadBlock; maybeConnect(); PreparedStatement s = null; try { s = conn.get() - .prepareStatement("SELECT chainWork, height, header FROM headers WHERE hash = ?"); + .prepareStatement("SELECT chainWork, height, header, wasUndoable FROM headers WHERE hash = ?"); // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(hash.getBytes(), 3, hashBytes, 0, 28); @@ -448,7 +489,10 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { return null; } // Parse it. - + + if (wasUndoableOnly && !results.getBoolean(4)) + return null; + BigInteger chainWork = new BigInteger(results.getBytes(1)); int height = results.getInt(2); Block b = new Block(params, results.getBytes(3)); @@ -472,6 +516,14 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } } + public StoredBlock get(Sha256Hash hash) throws BlockStoreException { + return get(hash, false); + } + + public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException { + return get(hash, true); + } + public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; @@ -550,6 +602,29 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore { } catch (SQLException ex) { throw new BlockStoreException(ex); } + } + + public StoredBlock getVerifiedChainHead() throws BlockStoreException { + return verifiedChainHeadBlock; + } + + public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException { + Sha256Hash hash = chainHead.getHeader().getHash(); + this.verifiedChainHeadHash = hash; + this.verifiedChainHeadBlock = chainHead; + maybeConnect(); + try { + PreparedStatement s = conn.get() + .prepareStatement("UPDATE settings SET value = ? WHERE name = ?"); + s.setString(2, VERIFIED_CHAIN_HEAD_SETTING); + s.setBytes(1, hash.getBytes()); + s.executeUpdate(); + s.close(); + } catch (SQLException ex) { + throw new BlockStoreException(ex); + } + if (this.chainHeadBlock.getHeight() < chainHead.getHeight()) + setChainHead(chainHead); removeUndoableBlocksWhereHeightIsLessThan(chainHead.getHeight() - fullStoreDepth); } diff --git a/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java index d33fc237..6a46a03a 100644 --- a/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java +++ b/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java @@ -308,11 +308,17 @@ class TransactionalMultiKeyHashMap { * Used primarily for unit testing. */ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { - private TransactionalHashMap blockMap; + class StoredBlockAndWasUndoableFlag { + public StoredBlock block; + public boolean wasUndoable; + public StoredBlockAndWasUndoableFlag(StoredBlock block, boolean wasUndoable) { this.block = block; this.wasUndoable = wasUndoable; } + } + private TransactionalHashMap blockMap; private TransactionalMultiKeyHashMap fullBlockMap; //TODO: Use something more suited to remove-heavy use? private TransactionalHashMap transactionOutputMap; private StoredBlock chainHead; + private StoredBlock verifiedChainHead; private int fullStoreDepth; /** @@ -321,7 +327,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { * @param fullStoreDepth The depth of blocks to keep FullStoredBlocks instead of StoredBlocks */ public MemoryFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth) { - blockMap = new TransactionalHashMap(); + blockMap = new TransactionalHashMap(); fullBlockMap = new TransactionalMultiKeyHashMap(); transactionOutputMap = new TransactionalHashMap(); this.fullStoreDepth = fullStoreDepth > 0 ? fullStoreDepth : 1; @@ -333,6 +339,7 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.genesisBlock.getHash(), genesisTransactions); put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader); + setVerifiedChainHead(storedGenesisHeader); } catch (BlockStoreException e) { throw new RuntimeException(e); // Cannot happen. } catch (VerificationException e) { @@ -342,21 +349,27 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { public synchronized void put(StoredBlock block) throws BlockStoreException { Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); - if (block.getHeight() > chainHead.getHeight() - fullStoreDepth) - throw new BlockStoreException("Putting a StoredBlock in MemoryFullPrunedBlockStore at height higher than head - fullStoredDepth"); Sha256Hash hash = block.getHeader().getHash(); - blockMap.put(hash, block); + blockMap.put(hash, new StoredBlockAndWasUndoableFlag(block, false)); } public synchronized void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException { Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); - fullBlockMap.put(storedBlock.getHeader().getHash(), storedBlock.getHeight(), undoableBlock); - blockMap.put(storedBlock.getHeader().getHash(), storedBlock); + Sha256Hash hash = storedBlock.getHeader().getHash(); + fullBlockMap.put(hash, storedBlock.getHeight(), undoableBlock); + blockMap.put(hash, new StoredBlockAndWasUndoableFlag(storedBlock, true)); } public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException { Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); - return blockMap.get(hash); + StoredBlockAndWasUndoableFlag storedBlock = blockMap.get(hash); + return storedBlock == null ? null : storedBlock.block; + } + + public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException { + Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); + StoredBlockAndWasUndoableFlag storedBlock = blockMap.get(hash); + return (storedBlock != null && storedBlock.wasUndoable) ? storedBlock.block : null; } public synchronized StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException { @@ -372,6 +385,18 @@ public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { public synchronized void setChainHead(StoredBlock chainHead) throws BlockStoreException { Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); this.chainHead = chainHead; + } + + public StoredBlock getVerifiedChainHead() throws BlockStoreException { + Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); + return verifiedChainHead; + } + + public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException { + Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); + this.verifiedChainHead = chainHead; + if (this.chainHead.getHeight() < chainHead.getHeight()) + setChainHead(chainHead); // Potential leak here if not all blocks get setChainHead'd // Though the FullPrunedBlockStore allows for this, the current AbstractBlockChain will not do it. fullBlockMap.removeByMultiKey(chainHead.getHeight() - fullStoreDepth);