From 03d8c71df3f02d9ca853005106e3237557e40467 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 9 Jul 2012 04:08:13 +0200 Subject: [PATCH] Add a FullPrunedBlockStore interface and in-memory implementation. --- .../bitcoin/store/FullPrunedBlockStore.java | 94 +++++ .../store/MemoryFullPrunedBlockStore.java | 339 ++++++++++++++++++ 2 files changed, 433 insertions(+) create mode 100644 core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java create mode 100644 core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java diff --git a/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java new file mode 100644 index 00000000..a60a2e34 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/store/FullPrunedBlockStore.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012 Matt Corallo. + * + * 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.store; + +import com.google.bitcoin.core.Sha256Hash; +import com.google.bitcoin.core.StoredBlock; +import com.google.bitcoin.core.StoredTransactionOutput; +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. + * 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.

+ * + *

It must store the {@link StoredBlock} of all blocks.

+ * + *

A FullPrunedBlockStore contains a map of hashes to [Full]StoredBlock. The hash is the double digest of the + * Bitcoin serialization of the block header, not the header with the extra data as well.

+ * + *

A FullPrunedBlockStore also contains a map of hash+index to StoredTransactionOutput. Again, the hash is + * a standard Bitcoin double-SHA256 hash of the transaction.

+ * + *

FullPrunedBlockStores are thread safe.

+ */ +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 + * @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 a {@link StoredUndoableBlock} who's block.getHash() method will be equal to the + * parameter. If no such block is found, returns null. + * Note that this may return null more often than get(Sha256Hash hash) as not all {@link StoredBlock}s have a + * {@link StoredUndoableBlock} copy stored as well. + */ + StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException; + + /** + * Gets a {@link StoredTransactionOutput} with the given hash and index, or null if none is found + */ + StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException; + + /** + * Adds a {@link StoredTransactionOutput} to the list of unspent TransactionOutputs + */ + void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException; + + /** + * Removes a {@link StoredTransactionOutput} from the list of unspent TransactionOutputs + * @throws BlockStoreException if there is an underlying storage issue, or out was not in the list. + */ + void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException; + + /** + * True if this store has any unspent outputs from a transaction with a hash equal to the first parameter + * @param numOutputs the number of outputs the given transaction has + */ + boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException; + + /** + *

Begins/Commits/Aborts a database transaction.

+ * + *

If abortDatabaseBatchWrite() is called by the same thread that called beginDatabaseBatchWrite(), + * any data writes between this call and abortDatabaseBatchWrite() made by the same thread + * should be discarded.

+ * + *

Furthermore, any data written after a call to beginDatabaseBatchWrite() should not be readable + * by any other threads until commitDatabaseBatchWrite() has been called by this thread. + * Multiple calls to beginDatabaseBatchWrite() in any given thread should be ignored and treated as one call.

+ */ + void beginDatabaseBatchWrite() throws BlockStoreException; + void commitDatabaseBatchWrite() throws BlockStoreException; + void abortDatabaseBatchWrite() throws BlockStoreException; +} diff --git a/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java b/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java new file mode 100644 index 00000000..e3a3165c --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/store/MemoryFullPrunedBlockStore.java @@ -0,0 +1,339 @@ +/* + * Copyright 2012 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.store; + +import com.google.bitcoin.core.*; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; + +import java.io.Serializable; +import java.util.*; + + +/** + * Used as a key for memory map (to avoid having to think about NetworkParameters, + * which is required for {@link TransactionOutPoint} + */ +class StoredTransactionOutPoint implements Serializable { + private static final long serialVersionUID = -4064230006297064377L; + + /** Hash of the transaction to which we refer. */ + Sha256Hash hash; + /** Which output of that transaction we are talking about. */ + long index; + + StoredTransactionOutPoint(Sha256Hash hash, long index) { + this.hash = hash; + this.index = index; + } + + StoredTransactionOutPoint(StoredTransactionOutput out) { + this.hash = out.getHash(); + this.index = out.getIndex(); + } + + /** + * The hash of the transaction to which we refer + */ + Sha256Hash getHash() { + return hash; + } + + /** + * The index of the output in transaction to which we refer + */ + long getIndex() { + return index; + } + + public int hashCode() { + return this.hash.hashCode() + (int)index; + } + + public String toString() { + return "Stored transaction out point: " + hash.toString() + ":" + index; + } + + public boolean equals(Object o) { + if (!(o instanceof StoredTransactionOutPoint)) return false; + return ((StoredTransactionOutPoint)o).getIndex() == this.index && + Objects.equal(this.getHash(), ((StoredTransactionOutPoint)o).getHash()); + } +} + +/** + * A HashMap that is DB transaction-aware + * This class is not thread-safe. + */ +class TransactionalHashMap { + ThreadLocal> tempMap; + ThreadLocal> tempSetRemoved; + private ThreadLocal inTransaction; + + HashMap map; + + public TransactionalHashMap() { + tempMap = new ThreadLocal>(); + tempSetRemoved = new ThreadLocal>(); + inTransaction = new ThreadLocal(); + map = new HashMap(); + } + + public void beginDatabaseBatchWrite() { + inTransaction.set(true); + } + + public void commitDatabaseBatchWrite() { + if (tempSetRemoved.get() != null) + for(KeyType key : tempSetRemoved.get()) + map.remove(key); + if (tempMap.get() != null) + for (Map.Entry entry : tempMap.get().entrySet()) + map.put(entry.getKey(), entry.getValue()); + abortDatabaseBatchWrite(); + } + + public void abortDatabaseBatchWrite() { + inTransaction.set(false); + tempSetRemoved.remove(); + tempMap.remove(); + } + + public ValueType get(KeyType key) { + if (Boolean.TRUE.equals(inTransaction.get())) { + if (tempMap.get() != null) { + ValueType value = tempMap.get().get(key); + if (value != null) + return value; + } + if (tempSetRemoved.get() != null && tempSetRemoved.get().contains(key)) + return null; + } + return map.get(key); + } + + public void put(KeyType key, ValueType value) { + if (Boolean.TRUE.equals(inTransaction.get())) { + if (tempSetRemoved.get() != null) + tempSetRemoved.get().remove(key); + if (tempMap.get() == null) + tempMap.set(new HashMap()); + tempMap.get().put(key, value); + }else{ + map.put(key, value); + } + } + + public ValueType remove(KeyType key) { + if (Boolean.TRUE.equals(inTransaction.get())) { + ValueType retVal = map.get(key); + if (retVal != null) { + if (tempSetRemoved.get() == null) + tempSetRemoved.set(new HashSet()); + tempSetRemoved.get().add(key); + } + if (tempMap.get() != null) { + ValueType tempVal = tempMap.get().remove(key); + if (tempVal != null) + return tempVal; + } + return retVal; + }else{ + return map.remove(key); + } + } +} + +/** + * A Map with multiple key types that is DB per-thread-transaction-aware. + * However, this class is not thread-safe. + * @param UniqueKeyType is a key that must be unique per object + * @param MultiKeyType is a key that can have multiple values + */ +class TransactionalMultiKeyHashMap { + TransactionalHashMap mapValues; + HashMap> mapKeys; + + public TransactionalMultiKeyHashMap() { + mapValues = new TransactionalHashMap(); + mapKeys = new HashMap>(); + } + + public void BeginTransaction() { + mapValues.beginDatabaseBatchWrite(); + } + + public void CommitTransaction() { + mapValues.commitDatabaseBatchWrite(); + } + + public void AbortTransaction() { + mapValues.abortDatabaseBatchWrite(); + } + + public ValueType get(UniqueKeyType key) { + return mapValues.get(key); + } + + public void put(UniqueKeyType uniqueKey, MultiKeyType multiKey, ValueType value) { + mapValues.put(uniqueKey, value); + Set set = mapKeys.get(multiKey); + if (set == null) { + set = new HashSet(); + set.add(uniqueKey); + mapKeys.put(multiKey, set); + }else{ + set.add(uniqueKey); + } + } + + public ValueType removeByUniqueKey(UniqueKeyType key) { + return mapValues.remove(key); + } + + public void removeByMultiKey(MultiKeyType key) { + Set set = mapKeys.remove(key); + if (set != null) + for (UniqueKeyType uniqueKey : set) + removeByUniqueKey(uniqueKey); + } +} + +/** + * Keeps {@link StoredBlock}s, {@link StoredUndoableBlock}s and {@link StoredTransactionOutput}s in memory. + * Used primarily for unit testing. + */ +public class MemoryFullPrunedBlockStore implements FullPrunedBlockStore { + private TransactionalHashMap blockMap; + private TransactionalMultiKeyHashMap fullBlockMap; + //TODO: Use something more suited to remove-heavy use? + private TransactionalHashMap transactionOutputMap; + private StoredBlock chainHead; + private int fullStoreDepth; + + /** + * Set up the MemoryFullPrunedBlockStore + * @param params The network parameters of this block store - used to get genesis block + * @param fullStoreDepth The depth of blocks to keep FullStoredBlocks instead of StoredBlocks + */ + public MemoryFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth) { + blockMap = new TransactionalHashMap(); + fullBlockMap = new TransactionalMultiKeyHashMap(); + transactionOutputMap = new TransactionalHashMap(); + this.fullStoreDepth = fullStoreDepth > 0 ? fullStoreDepth : 1; + // Insert the genesis block. + try { + StoredBlock storedGenesisHeader = new StoredBlock(params.genesisBlock.cloneAsHeader(), params.genesisBlock.getWork(), 0); + + LinkedList genesisTransactions = new LinkedList(); + for (Transaction tx : params.genesisBlock.getTransactions()) + genesisTransactions.add(new StoredTransaction(tx, 0)); + StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.genesisBlock.getHash(), genesisTransactions); + + put(storedGenesisHeader, storedGenesis); + setChainHead(storedGenesisHeader); + } catch (BlockStoreException e) { + throw new RuntimeException(e); // Cannot happen. + } catch (VerificationException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + + 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); + } + + 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); + } + + public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException { + Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); + return blockMap.get(hash); + } + + public synchronized StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException { + Preconditions.checkNotNull(fullBlockMap, "MemoryFullPrunedBlockStore is closed"); + return fullBlockMap.get(hash); + } + + public StoredBlock getChainHead() throws BlockStoreException { + Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); + return chainHead; + } + + public synchronized void setChainHead(StoredBlock chainHead) throws BlockStoreException { + Preconditions.checkNotNull(blockMap, "MemoryFullPrunedBlockStore is closed"); + this.chainHead = 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); + } + + public void close() { + blockMap = null; + fullBlockMap = null; + transactionOutputMap = null; + } + + public synchronized StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { + Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed"); + return transactionOutputMap.get(new StoredTransactionOutPoint(hash, index)); + } + + public synchronized void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException { + Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed"); + transactionOutputMap.put(new StoredTransactionOutPoint(out), out); + } + + public synchronized void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException { + Preconditions.checkNotNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed"); + if (transactionOutputMap.remove(new StoredTransactionOutPoint(out)) == null) + throw new BlockStoreException("Tried to remove a StoredTransactionOutput from MemoryFullPrunedBlockStore that it didn't have!"); + } + + public synchronized void beginDatabaseBatchWrite() throws BlockStoreException { + blockMap.beginDatabaseBatchWrite(); + fullBlockMap.BeginTransaction(); + transactionOutputMap.beginDatabaseBatchWrite(); + } + + public synchronized void commitDatabaseBatchWrite() throws BlockStoreException { + blockMap.commitDatabaseBatchWrite(); + fullBlockMap.CommitTransaction(); + transactionOutputMap.commitDatabaseBatchWrite(); + } + + public synchronized void abortDatabaseBatchWrite() throws BlockStoreException { + blockMap.abortDatabaseBatchWrite(); + fullBlockMap.AbortTransaction(); + transactionOutputMap.abortDatabaseBatchWrite(); + } + + public boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException { + for (int i = 0; i < numOutputs; i++) + if (getTransactionOutput(hash, i) != null) + return true; + return false; + } +}