/** * 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.io.*; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import static com.google.bitcoin.core.Utils.LOG; /** * Stores the block chain to disk but still holds it in memory. This is intended for desktop apps and tests. * Constrained environments like mobile phones probably won't want to or be able to store all the block headers in RAM. */ public class DiskBlockStore implements BlockStore { private FileOutputStream stream; private Map<Sha256Hash, StoredBlock> blockMap; private Sha256Hash chainHead; private NetworkParameters params; public DiskBlockStore(NetworkParameters params, File file) throws BlockStoreException { this.params = params; blockMap = new HashMap<Sha256Hash, StoredBlock>(); try { load(file); stream = new FileOutputStream(file, true); // Do append. } catch (IOException e) { LOG(e.toString()); createNewStore(params, file); } } private void createNewStore(NetworkParameters params, File file) throws BlockStoreException { // Create a new block store if the file wasn't found or anything went wrong whilst reading. blockMap.clear(); try { stream = new FileOutputStream(file, false); // Do not append, create fresh. stream.write(1); // Version. } catch (IOException e1) { // We could not load a block store nor could we create a new one! throw new BlockStoreException(e1); } try { // Set up the genesis block. When we start out fresh, it is by definition the top of the chain. Block genesis = params.genesisBlock.cloneAsHeader(); StoredBlock storedGenesis = new StoredBlock(genesis, genesis.getWork(), 0); this.chainHead = new Sha256Hash(storedGenesis.getHeader().getHash()); stream.write(this.chainHead.hash); put(storedGenesis); } catch (VerificationException e1) { throw new RuntimeException(e1); // Cannot happen. } catch (IOException e) { throw new BlockStoreException(e); } } private void load(File file) throws IOException, BlockStoreException { LOG("Reading block store from " + file.getAbsolutePath()); InputStream input = new BufferedInputStream(new FileInputStream(file)); // Read a version byte. int version = input.read(); if (version == -1) { // No such file or the file was empty. throw new FileNotFoundException(file.getName() + " does not exist or is empty"); } if (version != 1) { throw new BlockStoreException("Bad version number: " + version); } // Chain head pointer is the first thing in the file. byte[] chainHeadHash = new byte[32]; input.read(chainHeadHash); this.chainHead = new Sha256Hash(chainHeadHash); LOG("Read chain head from disk: " + this.chainHead); long now = System.currentTimeMillis(); // Rest of file is raw block headers. byte[] headerBytes = new byte[Block.HEADER_SIZE]; try { while (true) { // Read a block from disk. if (input.read(headerBytes) < 80) { // End of file. break; } // Parse it. Block b = new Block(params, headerBytes); // Look up the previous block it connects to. StoredBlock prev = get(b.getPrevBlockHash()); StoredBlock s; if (prev == null) { // First block in the stored chain has to be treated specially. if (b.equals(params.genesisBlock)) { s = new StoredBlock(params.genesisBlock.cloneAsHeader(), params.genesisBlock.getWork(), 0); } else { throw new BlockStoreException("Could not connect " + Utils.bytesToHexString(b.getHash()) + " to " + Utils.bytesToHexString(b.getPrevBlockHash())); } } else { // Don't try to verify the genesis block to avoid upsetting the unit tests. b.verify(); // Calculate its height and total chain work. s = prev.build(b); } // Save in memory. blockMap.put(new Sha256Hash(b.getHash()), s); } } catch (ProtocolException e) { // Corrupted file. throw new BlockStoreException(e); } catch (VerificationException e) { // Should not be able to happen unless the file contains bad blocks. throw new BlockStoreException(e); } long elapsed = System.currentTimeMillis() - now; LOG("Block chain read complete in " + elapsed + "ms"); } public synchronized void put(StoredBlock block) throws BlockStoreException { try { Sha256Hash hash = new Sha256Hash(block.getHeader().getHash()); assert blockMap.get(hash) == null : "Attempt to insert duplicate"; // Append to the end of the file. The other fields in StoredBlock will be recalculated when it's reloaded. byte[] bytes = block.getHeader().bitcoinSerialize(); stream.write(bytes); stream.flush(); blockMap.put(hash, block); } catch (IOException e) { throw new BlockStoreException(e); } } public synchronized StoredBlock get(byte[] hash) throws BlockStoreException { return blockMap.get(new Sha256Hash(hash)); } public synchronized StoredBlock getChainHead() throws BlockStoreException { return blockMap.get(chainHead); } public synchronized void setChainHead(StoredBlock chainHead) throws BlockStoreException { try { byte[] hash = chainHead.getHeader().getHash(); this.chainHead = new Sha256Hash(hash); // Write out new hash to the first 32 bytes of the file past one (first byte is version number). stream.getChannel().write(ByteBuffer.wrap(hash), 1); } catch (IOException e) { throw new BlockStoreException(e); } } }