/**
* 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.digitalcoin.store;
import com.google.digitalcoin.core.*;
import com.google.digitalcoin.utils.NamedSemaphores;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.HashMap;
import java.util.Map;
import static com.google.common.base.Preconditions.checkState;
/**
* 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 static final Logger log = LoggerFactory.getLogger(DiskBlockStore.class);
private RandomAccessFile file;
private Map<Sha256Hash, StoredBlock> blockMap;
private Sha256Hash chainHead;
private NetworkParameters params;
private FileLock lock;
private String fileName;
private static NamedSemaphores semaphores = new NamedSemaphores();
public DiskBlockStore(NetworkParameters params, File theFile) throws BlockStoreException {
this.params = params;
try {
this.fileName = theFile.getCanonicalPath();
} catch (IOException e) {
throw new RuntimeException(e);
}
blockMap = new HashMap<Sha256Hash, StoredBlock>();
try {
file = new RandomAccessFile(theFile, "rwd");
// Lock the file from other processes.
lock();
// The file position is at BOF
load(theFile);
// The file position is at EOF
} catch (IOException e) {
log.error("failed to load block store from file", e);
createNewStore(params);
// The file position is at EOF
}
}
public void close() throws BlockStoreException {
ensureOpen();
try {
file.close();
} catch (IOException e) {
throw new BlockStoreException(e);
} finally {
semaphores.release(this.fileName);
file = null;
}
}
private void createNewStore(NetworkParameters params) throws BlockStoreException {
// Create a new block store if the file wasn't found or anything went wrong whilst reading.
blockMap.clear();
try {
file.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 = storedGenesis.getHeader().getHash();
file.write(this.chainHead.getBytes());
put(storedGenesis);
} catch (VerificationException e1) {
throw new RuntimeException(e1); // Cannot happen.
} catch (IOException e) {
throw new BlockStoreException(e);
}
}
private void load(File theFile) throws IOException, BlockStoreException {
log.info("Reading block store from {}", theFile);
// Read a version byte.
int version = file.read();
if (version == -1) {
// No such file or the file was empty.
throw new FileNotFoundException(theFile.getName() + " 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];
if (file.read(chainHeadHash) < chainHeadHash.length)
throw new BlockStoreException("Truncated block store: cannot read chain head hash");
this.chainHead = new Sha256Hash(chainHeadHash);
log.info("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.
int read = file.read(headerBytes);
if (read == -1) {
// End of file.
break;
}
if (read < headerBytes.length) {
throw new BlockStoreException("Truncated block store: partial block read");
}
// 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 " + b.getHash().toString() + " to "
+ b.getPrevBlockHash().toString());
}
} else {
// Don't try to verify the genesis block to avoid upsetting the unit tests.
b.verifyHeader();
// Calculate its height and total chain work.
s = prev.build(b);
}
// Save in memory.
blockMap.put(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.info("Block chain read complete in {}ms", elapsed);
}
private void ensureOpen() throws BlockStoreException {
if (file == null) {
throw new BlockStoreException("BlockStore was closed");
}
}
public synchronized void put(StoredBlock block) throws BlockStoreException {
ensureOpen();
try {
Sha256Hash hash = block.getHeader().getHash();
checkState(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().digitalcoinSerialize();
file.write(bytes);
blockMap.put(hash, block);
} catch (IOException e) {
throw new BlockStoreException(e);
}
}
public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException {
ensureOpen();
return blockMap.get(hash);
}
public synchronized StoredBlock getChainHead() throws BlockStoreException {
ensureOpen();
return blockMap.get(chainHead);
}
public synchronized void setChainHead(StoredBlock chainHead) throws BlockStoreException {
ensureOpen();
try {
this.chainHead = chainHead.getHeader().getHash();
// Write out new hash to the first 32 bytes of the file past one (first byte is version number).
file.getChannel().write(ByteBuffer.wrap(this.chainHead.getBytes()), 1);
} catch (IOException e) {
throw new BlockStoreException(e);
}
}
private void lock() throws IOException, BlockStoreException {
if (!semaphores.tryAcquire(fileName)) {
throw new BlockStoreException("File in use");
}
try {
lock = file.getChannel().tryLock();
} catch (OverlappingFileLockException e) {
semaphores.release(fileName);
lock = null;
}
if (lock == null) {
try {
this.file.close();
} finally {
this.file = null;
}
throw new BlockStoreException("Could not lock file");
}
}
}