/* * Copyright 2016 Robin Owens * * 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 org.bitcoinj.store; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.io.*; import java.nio.ByteBuffer; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.ScriptException; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.StoredUndoableBlock; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutputChanges; import org.bitcoinj.core.UTXO; import org.bitcoinj.core.UTXOProviderException; import org.bitcoinj.core.VerificationException; import org.bitcoinj.script.Script; import org.iq80.leveldb.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.fusesource.leveldbjni.JniDBFactory.*; import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; /** * <p> * An implementation of a Fully Pruned Block Store using a leveldb implementation as the backing data store. * <p> * * <p> * Includes number of caches to optimise the initial blockchain download. * </p> */ public class LevelDBFullPrunedBlockStore implements FullPrunedBlockStore { private static final Logger log = LoggerFactory.getLogger(LevelDBFullPrunedBlockStore.class); NetworkParameters params; // LevelDB reference. DB db = null; // Standard blockstore properties protected Sha256Hash chainHeadHash; protected StoredBlock chainHeadBlock; protected Sha256Hash verifiedChainHeadHash; protected StoredBlock verifiedChainHeadBlock; protected int fullStoreDepth; // Indicates if we track and report runtime for each method // this is very useful to focus performance tuning on correct areas. protected boolean instrument = false; // instrumentation stats Stopwatch totalStopwatch; protected long hit; protected long miss; Map<String, Stopwatch> methodStartTime; Map<String, Long> methodCalls; Map<String, Long> methodTotalTime; int exitBlock; // Must be multiple of 1000 and causes code to exit at this // block! // ONLY used for performance benchmarking. // LRU Cache for getTransactionOutput protected Map<ByteBuffer, UTXO> utxoCache; // Additional cache to cope with case when transactions are rolled back // e.g. when block fails to verify. protected Map<ByteBuffer, UTXO> utxoUncommittedCache; protected Set<ByteBuffer> utxoUncommittedDeletedCache; // Database folder protected String filename; // Do we auto commit transactions. protected boolean autoCommit = true; // Datastructures to allow us to search for uncommited inserts/deletes. // leveldb does not support dirty reads so we have to // do it ourselves. Map<ByteBuffer, byte[]> uncommited; Set<ByteBuffer> uncommitedDeletes; // Sizes of leveldb caches. protected long leveldbReadCache; protected int leveldbWriteCache; // Size of cache for getTransactionOutput protected int openOutCache; // Bloomfilter for caching calls to hasUnspentOutputs protected BloomFilter bloom; // Defaults for cache sizes static final long LEVELDB_READ_CACHE_DEFAULT = 100 * 1048576; // 100 meg static final int LEVELDB_WRITE_CACHE_DEFAULT = 10 * 1048576; // 10 meg static final int OPENOUT_CACHE_DEFAULT = 100000; // LRUCache public class LRUCache extends LinkedHashMap<ByteBuffer, UTXO> { private static final long serialVersionUID = 1L; private int capacity; public LRUCache(int capacity, float loadFactor) { super(capacity, loadFactor, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<ByteBuffer, UTXO> eldest) { return size() > this.capacity; } } // Simple bloomfilter. We take advantage of fact that a Transaction Hash // can be split into 3 30bit numbers that are all random and uncorrelated // so ideal to use as the input to a 3 function bloomfilter. No has function // needed. private class BloomFilter { private byte[] cache; public long returnedTrue; public long returnedFalse; public long added; public BloomFilter() { // 2^27 so since 8 bits in a byte this is // 1,073,741,824 bits cache = new byte[134217728]; // This size chosen as with 3 functions we should only get 4% errors // with 150m entries. } // Called to prime cache. // Might be idea to call periodically to flush out removed keys. // Would need to benchmark 1st though. public void reloadCache(DB db) { // LevelDB is great at scanning consecutive keys. // This take seconds even with 20m keys to add. log.info("Loading Bloom Filter"); DBIterator iterator = db.iterator(); byte[] key = getKey(KeyType.OPENOUT_ALL); for (iterator.seek(key); iterator.hasNext(); iterator.next()) { ByteBuffer bbKey = ByteBuffer.wrap(iterator.peekNext().getKey()); byte firstByte = bbKey.get(); // remove the KeyType.OPENOUT_ALL // byte. if (key[0] != firstByte) { printStat(); return; } byte[] hash = new byte[32]; bbKey.get(hash); add(hash); } try { iterator.close(); } catch (IOException e) { log.error("Error closing iterator", e); } printStat(); } public void printStat() { log.info("Bloom Added: " + added + " T: " + returnedTrue + " F: " + returnedFalse); } // Add a txhash to the filter. public void add(byte[] hash) { byte[] firstHash = new byte[4]; added++; for (int i = 0; i < 3; i++) { System.arraycopy(hash, i * 4, firstHash, 0, 4); setBit(firstHash); } } public void add(Sha256Hash hash) { add(hash.getBytes()); } // check if hash was added. // if returns false then 100% sure never added // if returns true need to check what state is in DB as can // not be 100% sure. public boolean wasAdded(Sha256Hash hash) { byte[] firstHash = new byte[4]; for (int i = 0; i < 3; i++) { System.arraycopy(hash.getBytes(), i * 4, firstHash, 0, 4); boolean result = getBit(firstHash); if (!result) { returnedFalse++; return false; } } returnedTrue++; return true; } private void setBit(byte[] entry) { int arrayIndex = (entry[0] & 0x3F) << 21 | (entry[1] & 0xFF) << 13 | (entry[2] & 0xFF) << 5 | (entry[3] & 0xFF) >> 3; int bit = (entry[3] & 0x07); int orBit = (0x1 << bit); byte newEntry = (byte) ((int) cache[arrayIndex] | orBit); cache[arrayIndex] = newEntry; } private boolean getBit(byte[] entry) { int arrayIndex = (entry[0] & 0x3F) << 21 | (entry[1] & 0xFF) << 13 | (entry[2] & 0xFF) << 5 | (entry[3] & 0xFF) >> 3; int bit = (entry[3] & 0x07); int orBit = (0x1 << bit); byte arrayEntry = cache[arrayIndex]; int result = arrayEntry & orBit; if (result == 0) { return false; } else { return true; } } } public LevelDBFullPrunedBlockStore(NetworkParameters params, String filename, int blockCount) { this(params, filename, blockCount, LEVELDB_READ_CACHE_DEFAULT, LEVELDB_WRITE_CACHE_DEFAULT, OPENOUT_CACHE_DEFAULT, false, Integer.MAX_VALUE); } public LevelDBFullPrunedBlockStore(NetworkParameters params, String filename, int blockCount, long leveldbReadCache, int leveldbWriteCache, int openOutCache, boolean instrument, int exitBlock) { this.params = params; fullStoreDepth = blockCount; this.instrument = instrument; this.exitBlock = exitBlock; methodStartTime = new HashMap<String, Stopwatch>(); methodCalls = new HashMap<String, Long>(); methodTotalTime = new HashMap<String, Long>(); this.filename = filename; this.leveldbReadCache = leveldbReadCache; this.leveldbWriteCache = leveldbWriteCache; this.openOutCache = openOutCache; bloom = new BloomFilter(); totalStopwatch = Stopwatch.createStarted(); openDB(); bloom.reloadCache(db); // Reset after bloom filter loaded totalStopwatch = Stopwatch.createStarted(); } private void openDB() { Options options = new Options(); options.createIfMissing(true); // options.compressionType(CompressionType.NONE); options.cacheSize(leveldbReadCache); options.writeBufferSize(leveldbWriteCache); options.maxOpenFiles(10000); // options.blockSize(1024*1024*50); try { db = factory.open(new File(filename), options); } catch (IOException e) { throw new RuntimeException("Can not open DB", e); } utxoCache = new LRUCache(openOutCache, 0.75f); try { if (batchGet(getKey(KeyType.CREATED)) == null) { createNewStore(params); } else { initFromDb(); } } catch (BlockStoreException e) { throw new RuntimeException("Can not init/load db", e); } } private void initFromDb() throws BlockStoreException { Sha256Hash hash = Sha256Hash.wrap(batchGet(getKey(KeyType.CHAIN_HEAD_SETTING))); this.chainHeadBlock = get(hash); this.chainHeadHash = hash; if (this.chainHeadBlock == null) { throw new BlockStoreException("corrupt database block store - head block not found"); } hash = Sha256Hash.wrap(batchGet(getKey(KeyType.VERIFIED_CHAIN_HEAD_SETTING))); this.verifiedChainHeadBlock = get(hash); this.verifiedChainHeadHash = hash; if (this.verifiedChainHeadBlock == null) { throw new BlockStoreException("corrupt databse block store - verified head block not found"); } } private void createNewStore(NetworkParameters params) throws BlockStoreException { try { // Set up the genesis block. When we start out fresh, it is by // definition the top of the chain. StoredBlock storedGenesisHeader = new StoredBlock(params.getGenesisBlock().cloneAsHeader(), params.getGenesisBlock().getWork(), 0); // The coinbase in the genesis block is not spendable. This is // because of how the reference client inits // its database - the genesis transaction isn't actually in the db // so its spent flags can never be updated. List<Transaction> genesisTransactions = Lists.newLinkedList(); StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions); beginDatabaseBatchWrite(); put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader); setVerifiedChainHead(storedGenesisHeader); batchPut(getKey(KeyType.CREATED), bytes("done")); commitDatabaseBatchWrite(); } catch (VerificationException e) { throw new RuntimeException(e); // Cannot happen. } } void beginMethod(String name) { methodStartTime.put(name, Stopwatch.createStarted()); } void endMethod(String name) { if (methodCalls.containsKey(name)) { methodCalls.put(name, methodCalls.get(name) + 1); methodTotalTime.put(name, methodTotalTime.get(name) + methodStartTime.get(name).elapsed(TimeUnit.NANOSECONDS)); } else { methodCalls.put(name, 1l); methodTotalTime.put(name, methodStartTime.get(name).elapsed(TimeUnit.NANOSECONDS)); } } // Debug method to display stats on runtime of each method // and cache hit rates etc.. void dumpStats() { long wallTimeNanos = totalStopwatch.elapsed(TimeUnit.NANOSECONDS); long dbtime = 0; for (String name : methodCalls.keySet()) { long calls = methodCalls.get(name); long time = methodTotalTime.get(name); dbtime += time; long average = time / calls; double proportion = (time + 0.0) / (wallTimeNanos + 0.0); log.info(name + " c:" + calls + " r:" + time + " a:" + average + " p:" + String.format("%.2f", proportion)); } double dbproportion = (dbtime + 0.0) / (wallTimeNanos + 0.0); double hitrate = (hit + 0.0) / (hit + miss + 0.0); log.info("Cache size:" + utxoCache.size() + " hit:" + hit + " miss:" + miss + " rate:" + String.format("%.2f", hitrate)); bloom.printStat(); log.info("hasTxOut call:" + hasCall + " True:" + hasTrue + " False:" + hasFalse); log.info("Wall:" + totalStopwatch + " percent:" + String.format("%.2f", dbproportion)); String stats = db.getProperty("leveldb.stats"); System.out.println(stats); } @Override public void put(StoredBlock block) throws BlockStoreException { putUpdateStoredBlock(block, false); } @Override public StoredBlock getChainHead() throws BlockStoreException { return chainHeadBlock; } @Override public void setChainHead(StoredBlock chainHead) throws BlockStoreException { if (instrument) beginMethod("setChainHead"); Sha256Hash hash = chainHead.getHeader().getHash(); this.chainHeadHash = hash; this.chainHeadBlock = chainHead; batchPut(getKey(KeyType.CHAIN_HEAD_SETTING), hash.getBytes()); if (instrument) endMethod("setChainHead"); } @Override public void close() throws BlockStoreException { try { db.close(); } catch (IOException e) { throw new BlockStoreException("Could not close db", e); } } @Override public NetworkParameters getParams() { return params; } @Override public List<UTXO> getOpenTransactionOutputs(List<Address> addresses) throws UTXOProviderException { // Run this on a snapshot of database so internally consistent result // This is critical or if one address paid another could get incorrect // results List<UTXO> results = new LinkedList<UTXO>(); for (Address a : addresses) { ByteBuffer bb = ByteBuffer.allocate(21); bb.put((byte) KeyType.ADDRESS_HASHINDEX.ordinal()); bb.put(a.getHash160()); ReadOptions ro = new ReadOptions(); Snapshot sn = db.getSnapshot(); ro.snapshot(sn); // Scanning over iterator very fast DBIterator iterator = db.iterator(ro); for (iterator.seek(bb.array()); iterator.hasNext(); iterator.next()) { ByteBuffer bbKey = ByteBuffer.wrap(iterator.peekNext().getKey()); bbKey.get(); // remove the address_hashindex byte. byte[] addressKey = new byte[20]; bbKey.get(addressKey); if (!Arrays.equals(addressKey, a.getHash160())) { break; } byte[] hashBytes = new byte[32]; bbKey.get(hashBytes); int index = bbKey.getInt(); Sha256Hash hash = Sha256Hash.wrap(hashBytes); UTXO txout; try { // TODO this should be on the SNAPSHOT too...... // this is really a BUG. txout = getTransactionOutput(hash, index); } catch (BlockStoreException e) { throw new UTXOProviderException("block store execption", e); } if (txout != null) { Script sc = txout.getScript(); Address address = sc.getToAddress(params, true); UTXO output = new UTXO(txout.getHash(), txout.getIndex(), txout.getValue(), txout.getHeight(), txout.isCoinbase(), txout.getScript(), address.toString()); results.add(output); } } try { iterator.close(); ro = null; sn.close(); sn = null; } catch (IOException e) { log.error("Error closing snapshot/iterator?", e); } } return results; } @Override public int getChainHeadHeight() throws UTXOProviderException { try { return getVerifiedChainHead().getHeight(); } catch (BlockStoreException e) { throw new UTXOProviderException(e); } } protected void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) { // We put as one record as then the get is much faster. if (instrument) beginMethod("putUpdateStoredBlock"); Sha256Hash hash = storedBlock.getHeader().getHash(); ByteBuffer bb = ByteBuffer.allocate(97); storedBlock.serializeCompact(bb); bb.put((byte) (wasUndoable ? 1 : 0)); batchPut(getKey(KeyType.HEADERS_ALL, hash), bb.array()); if (instrument) endMethod("putUpdateStoredBlock"); } @Override public void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException { if (instrument) beginMethod("put"); int height = storedBlock.getHeight(); byte[] transactions = null; byte[] txOutChanges = null; try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); if (undoableBlock.getTxOutChanges() != null) { undoableBlock.getTxOutChanges().serializeToStream(bos); txOutChanges = bos.toByteArray(); } else { int numTxn = undoableBlock.getTransactions().size(); bos.write((int) (0xFF & (numTxn >> 0))); bos.write((int) (0xFF & (numTxn >> 8))); bos.write((int) (0xFF & (numTxn >> 16))); bos.write((int) (0xFF & (numTxn >> 24))); for (Transaction tx : undoableBlock.getTransactions()) tx.bitcoinSerialize(bos); transactions = bos.toByteArray(); } bos.close(); } catch (IOException e) { throw new BlockStoreException(e); } Sha256Hash hash = storedBlock.getHeader().getHash(); ByteBuffer keyBuf = ByteBuffer.allocate(33); keyBuf.put((byte) KeyType.HEIGHT_UNDOABLEBLOCKS.ordinal()); keyBuf.putInt(height); keyBuf.put(hash.getBytes(), 4, 28); batchPut(keyBuf.array(), new byte[1]); if (transactions == null) { ByteBuffer undoBuf = ByteBuffer.allocate(4 + 4 + txOutChanges.length + 4 + 0); undoBuf.putInt(height); undoBuf.putInt(txOutChanges.length); undoBuf.put(txOutChanges); undoBuf.putInt(0); batchPut(getKey(KeyType.UNDOABLEBLOCKS_ALL, hash), undoBuf.array()); } else { ByteBuffer undoBuf = ByteBuffer.allocate(4 + 4 + 0 + 4 + transactions.length); undoBuf.putInt(height); undoBuf.putInt(0); undoBuf.putInt(transactions.length); undoBuf.put(transactions); batchPut(getKey(KeyType.UNDOABLEBLOCKS_ALL, hash), undoBuf.array()); } if (instrument) endMethod("put"); putUpdateStoredBlock(storedBlock, true); } // Since LevelDB is a key value store we do not have "tables". // So these keys are the 1st byte of each key to indicate the "table" it is // in. // Do wonder if grouping each "table" like this is efficient or not... enum KeyType { CREATED, CHAIN_HEAD_SETTING, VERIFIED_CHAIN_HEAD_SETTING, VERSION_SETTING, HEADERS_ALL, UNDOABLEBLOCKS_ALL, HEIGHT_UNDOABLEBLOCKS, OPENOUT_ALL, ADDRESS_HASHINDEX } // These helpers just get the key for an input private byte[] getKey(KeyType keytype) { byte[] key = new byte[1]; key[0] = (byte) keytype.ordinal(); return key; } private byte[] getTxKey(KeyType keytype, Sha256Hash hash) { byte[] key = new byte[33]; key[0] = (byte) keytype.ordinal(); System.arraycopy(hash.getBytes(), 0, key, 1, 32); return key; } private byte[] getTxKey(KeyType keytype, Sha256Hash hash, int index) { byte[] key = new byte[37]; key[0] = (byte) keytype.ordinal(); System.arraycopy(hash.getBytes(), 0, key, 1, 32); byte[] heightBytes = ByteBuffer.allocate(4).putInt(index).array(); System.arraycopy(heightBytes, 0, key, 33, 4); return key; } private byte[] getKey(KeyType keytype, Sha256Hash hash) { byte[] key = new byte[29]; key[0] = (byte) keytype.ordinal(); System.arraycopy(hash.getBytes(), 4, key, 1, 28); return key; } private byte[] getKey(KeyType keytype, byte[] hash) { byte[] key = new byte[29]; key[0] = (byte) keytype.ordinal(); System.arraycopy(hash, 4, key, 1, 28); return key; } @Override public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException { return get(hash, true); } @Override public StoredBlock get(Sha256Hash hash) throws BlockStoreException { return get(hash, false); } 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; if (instrument) beginMethod("get");// ignore optimised case as not interesting for // tuning. boolean undoableResult; byte[] result = batchGet(getKey(KeyType.HEADERS_ALL, hash)); if (result == null) { if (instrument) endMethod("get"); return null; } undoableResult = (result[96] == 1 ? true : false); if (wasUndoableOnly && !undoableResult) { if (instrument) endMethod("get"); return null; } // TODO Should I chop the last byte off? Seems to work with it left // there... StoredBlock stored = StoredBlock.deserializeCompact(params, ByteBuffer.wrap(result)); stored.getHeader().verifyHeader(); if (instrument) endMethod("get"); return stored; } @Override public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException { try { if (instrument) beginMethod("getUndoBlock"); byte[] result = batchGet(getKey(KeyType.UNDOABLEBLOCKS_ALL, hash)); if (result == null) { if (instrument) endMethod("getUndoBlock"); return null; } ByteBuffer bb = ByteBuffer.wrap(result); bb.getInt();// TODO Read height - but seems to be unused - maybe can // skip storing it but only 4 bytes! int txOutSize = bb.getInt(); StoredUndoableBlock block; if (txOutSize == 0) { int txSize = bb.getInt(); byte[] transactions = new byte[txSize]; bb.get(transactions); int offset = 0; int numTxn = ((transactions[offset++] & 0xFF) << 0) | ((transactions[offset++] & 0xFF) << 8) | ((transactions[offset++] & 0xFF) << 16) | ((transactions[offset++] & 0xFF) << 24); List<Transaction> transactionList = new LinkedList<Transaction>(); for (int i = 0; i < numTxn; i++) { Transaction tx = new Transaction(params, transactions, offset); transactionList.add(tx); offset += tx.getMessageSize(); } block = new StoredUndoableBlock(hash, transactionList); } else { byte[] txOutChanges = new byte[txOutSize]; bb.get(txOutChanges); TransactionOutputChanges outChangesObject = new TransactionOutputChanges( new ByteArrayInputStream(txOutChanges)); block = new StoredUndoableBlock(hash, outChangesObject); } if (instrument) endMethod("getUndoBlock"); return block; } catch (IOException e) { // Corrupted database. if (instrument) endMethod("getUndoBlock"); throw new BlockStoreException(e); } } @Override public UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { if (instrument) beginMethod("getTransactionOutput"); try { UTXO result = null; byte[] key = getTxKey(KeyType.OPENOUT_ALL, hash, (int) index); // Use cache if (autoCommit) { // Simple case of auto commit on so cache is consistent. result = utxoCache.get(ByteBuffer.wrap(key)); } else { // Check if we have an uncommitted delete. if (utxoUncommittedDeletedCache.contains(ByteBuffer.wrap(key))) { // has been deleted so return null; hit++; if (instrument) endMethod("getTransactionOutput"); return result; } // Check if we have an uncommitted entry result = utxoUncommittedCache.get(ByteBuffer.wrap(key)); if (result == null) result = utxoCache.get(ByteBuffer.wrap(key)); // And lastly above check if we have a committed cached entry } if (result != null) { hit++; if (instrument) endMethod("getTransactionOutput"); return result; } miss++; // If we get here have to hit the database. byte[] inbytes = batchGet(key); if (inbytes == null) { if (instrument) endMethod("getTransactionOutput"); return null; } ByteArrayInputStream bis = new ByteArrayInputStream(inbytes); UTXO txout = new UTXO(bis); if (instrument) endMethod("getTransactionOutput"); return txout; } catch (DBException e) { log.error("Exception in getTransactionOutput.", e); if (instrument) endMethod("getTransactionOutput"); } catch (IOException e) { log.error("Exception in getTransactionOutput.", e); if (instrument) endMethod("getTransactionOutput"); } throw new BlockStoreException("problem"); } @Override public void addUnspentTransactionOutput(UTXO out) throws BlockStoreException { if (instrument) beginMethod("addUnspentTransactionOutput"); // Add to bloom filter - is very fast to add. bloom.add(out.getHash()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { out.serializeToStream(bos); } catch (IOException e) { throw new BlockStoreException("problem serialising utxo", e); } byte[] key = getTxKey(KeyType.OPENOUT_ALL, out.getHash(), (int) out.getIndex()); batchPut(key, bos.toByteArray()); if (autoCommit) { utxoCache.put(ByteBuffer.wrap(key), out); } else { utxoUncommittedCache.put(ByteBuffer.wrap(key), out); // leveldb just stores the last key/value added. // So if we do an add must remove any previous deletes. utxoUncommittedDeletedCache.remove(ByteBuffer.wrap(key)); } // Could run this in parallel with above too. // Should update instrumentation to see if worth while. Address a; if (out.getAddress() == null || out.getAddress().equals("")) { if (instrument) endMethod("addUnspentTransactionOutput"); return; } else { try { a = Address.fromBase58(params, out.getAddress()); } catch (AddressFormatException e) { if (instrument) endMethod("addUnspentTransactionOutput"); return; } } ByteBuffer bb = ByteBuffer.allocate(57); bb.put((byte) KeyType.ADDRESS_HASHINDEX.ordinal()); bb.put(a.getHash160()); bb.put(out.getHash().getBytes()); bb.putInt((int) out.getIndex()); byte[] value = new byte[0]; batchPut(bb.array(), value); if (instrument) endMethod("addUnspentTransactionOutput"); } private void batchPut(byte[] key, byte[] value) { if (autoCommit) { db.put(key, value); } else { // Add this so we can get at uncommitted inserts which // leveldb does not support uncommited.put(ByteBuffer.wrap(key), value); batch.put(key, value); } } private byte[] batchGet(byte[] key) { ByteBuffer bbKey = ByteBuffer.wrap(key); // This is needed to cope with deletes that are not yet committed to db. if (!autoCommit && uncommitedDeletes != null && uncommitedDeletes.contains(bbKey)) return null; byte[] value = null; // And this to handle uncommitted inserts (dirty reads) if (!autoCommit && uncommited != null) { value = uncommited.get(bbKey); if (value != null) return value; } try { value = db.get(key); } catch (DBException e) { log.error("Caught error opening file", e); try { Thread.sleep(1000); } catch (InterruptedException e1) { } value = db.get(key); } return value; } private void batchDelete(byte[] key) { if (!autoCommit) { batch.delete(key); uncommited.remove(ByteBuffer.wrap(key)); uncommitedDeletes.add(ByteBuffer.wrap(key)); } else { db.delete(key); } } @Override public void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException { if (instrument) beginMethod("removeUnspentTransactionOutput"); byte[] key = getTxKey(KeyType.OPENOUT_ALL, out.getHash(), (int) out.getIndex()); if (autoCommit) { utxoCache.remove(ByteBuffer.wrap(key)); } else { utxoUncommittedDeletedCache.add(ByteBuffer.wrap(key)); utxoUncommittedCache.remove(ByteBuffer.wrap(key)); } batchDelete(key); // could run this and the above in parallel // Need to update instrumentation to check if worth the effort // TODO storing as byte[] hash to save space. But think should just // store as String of address. Might be faster. Need to test. ByteBuffer bb = ByteBuffer.allocate(57); Address a; byte[] hashBytes = null; try { String address = out.getAddress(); if (address == null || address.equals("")) { Script sc = out.getScript(); a = sc.getToAddress(params); hashBytes = a.getHash160(); } else { a = Address.fromBase58(params, out.getAddress()); hashBytes = a.getHash160(); } } catch (AddressFormatException e) { if (instrument) endMethod("removeUnspentTransactionOutput"); return; } catch (ScriptException e) { if (instrument) endMethod("removeUnspentTransactionOutput"); return; } bb.put((byte) KeyType.ADDRESS_HASHINDEX.ordinal()); bb.put(hashBytes); bb.put(out.getHash().getBytes()); bb.putInt((int) out.getIndex()); batchDelete(bb.array()); if (instrument) endMethod("removeUnspentTransactionOutput"); } // Instrumentation of bloom filter to check theory // matches reality. Without this initial chain sync takes // 50-75% longer. long hasCall; long hasTrue; long hasFalse; @Override public boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException { if (instrument) beginMethod("hasUnspentOutputs"); hasCall++; if (!bloom.wasAdded(hash)) { if (instrument) endMethod("hasUnspentOutputs"); hasFalse++; return false; } // no index is fine as will find any entry with any index... // TODO should I be checking uncommitted inserts/deletes??? byte[] key = getTxKey(KeyType.OPENOUT_ALL, hash); byte[] subResult = new byte[key.length]; DBIterator iterator = db.iterator(); for (iterator.seek(key); iterator.hasNext();) { byte[] result = iterator.peekNext().getKey(); System.arraycopy(result, 0, subResult, 0, subResult.length); if (Arrays.equals(key, subResult)) { hasTrue++; try { iterator.close(); } catch (IOException e) { log.error("Error closing iterator", e); } if (instrument) endMethod("hasUnspentOutputs"); return true; } else { hasFalse++; try { iterator.close(); } catch (IOException e) { log.error("Error closing iterator", e); } if (instrument) endMethod("hasUnspentOutputs"); return false; } } try { iterator.close(); } catch (IOException e) { log.error("Error closing iterator", e); } hasFalse++; if (instrument) endMethod("hasUnspentOutputs"); return false; } @Override public StoredBlock getVerifiedChainHead() throws BlockStoreException { return verifiedChainHeadBlock; } @Override public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException { if (instrument) beginMethod("setVerifiedChainHead"); Sha256Hash hash = chainHead.getHeader().getHash(); this.verifiedChainHeadHash = hash; this.verifiedChainHeadBlock = chainHead; batchPut(getKey(KeyType.VERIFIED_CHAIN_HEAD_SETTING), hash.getBytes()); if (this.chainHeadBlock.getHeight() < chainHead.getHeight()) setChainHead(chainHead); removeUndoableBlocksWhereHeightIsLessThan(chainHead.getHeight() - fullStoreDepth); if (instrument) endMethod("setVerifiedChainHead"); } void removeUndoableBlocksWhereHeightIsLessThan(int height) { if (height < 0) return; DBIterator iterator = db.iterator(); ByteBuffer keyBuf = ByteBuffer.allocate(5); keyBuf.put((byte) KeyType.HEIGHT_UNDOABLEBLOCKS.ordinal()); keyBuf.putInt(height); for (iterator.seek(keyBuf.array()); iterator.hasNext(); iterator.next()) { byte[] bytekey = iterator.peekNext().getKey(); ByteBuffer buff = ByteBuffer.wrap(bytekey); buff.get(); // Just remove byte from buffer. int keyHeight = buff.getInt(); byte[] hashbytes = new byte[32]; buff.get(hashbytes, 4, 28); if (keyHeight > height) break; batchDelete(getKey(KeyType.UNDOABLEBLOCKS_ALL, hashbytes)); batchDelete(bytekey); } try { iterator.close(); } catch (IOException e) { log.error("Error closing iterator", e); } } WriteBatch batch; @Override public void beginDatabaseBatchWrite() throws BlockStoreException { // This is often called twice in row! But they are not nested // transactions! // We just ignore the second call. if (!autoCommit) { return; } if (instrument) beginMethod("beginDatabaseBatchWrite"); batch = db.createWriteBatch(); uncommited = new HashMap<ByteBuffer, byte[]>(); uncommitedDeletes = new HashSet<ByteBuffer>(); utxoUncommittedCache = new HashMap<ByteBuffer, UTXO>(); utxoUncommittedDeletedCache = new HashSet<ByteBuffer>(); autoCommit = false; if (instrument) endMethod("beginDatabaseBatchWrite"); } @Override public void commitDatabaseBatchWrite() throws BlockStoreException { uncommited = null; uncommitedDeletes = null; if (instrument) beginMethod("commitDatabaseBatchWrite"); db.write(batch); // order of these is not important as we only allow entry to be in one // or the other. // must update cache with uncommitted adds/deletes. for (Map.Entry<ByteBuffer, UTXO> entry : utxoUncommittedCache.entrySet()) { utxoCache.put(entry.getKey(), entry.getValue()); } utxoUncommittedCache = null; for (ByteBuffer entry : utxoUncommittedDeletedCache) { utxoCache.remove(entry); } utxoUncommittedDeletedCache = null; autoCommit = true; try { batch.close(); batch = null; } catch (IOException e) { log.error("Error in db commit.", e); throw new BlockStoreException("could not close batch."); } if (instrument) endMethod("commitDatabaseBatchWrite"); if (instrument && verifiedChainHeadBlock.getHeight() % 1000 == 0) { log.info("Height: " + verifiedChainHeadBlock.getHeight()); dumpStats(); if (verifiedChainHeadBlock.getHeight() == exitBlock) { System.err.println("Exit due to exitBlock set"); System.exit(1); } } } @Override public void abortDatabaseBatchWrite() throws BlockStoreException { try { uncommited = null; uncommitedDeletes = null; utxoUncommittedCache = null; utxoUncommittedDeletedCache = null; autoCommit = true; if (batch != null) { batch.close(); batch = null; } } catch (IOException e) { throw new BlockStoreException("could not close batch in abort.", e); } } public void resetStore() { // only used in unit tests. // bit dangerous and deletes files! try { db.close(); uncommited = null; uncommitedDeletes = null; autoCommit = true; bloom = new BloomFilter(); utxoCache = new LRUCache(openOutCache, 0.75f); } catch (IOException e) { log.error("Exception in resetStore.", e); } File f = new File(filename); if (f.isDirectory()) { for (File c : f.listFiles()) c.delete(); } openDB(); } }