package org.ripple.power.txns.btc; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.math.BigInteger; import java.security.KeyException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; import org.ripple.power.Helper; import org.ripple.power.config.LSystem; import org.ripple.power.utils.FileUtils; public abstract class BlockStore { /** Maximum block file size */ protected static final long MAX_BLOCK_FILE_SIZE = 256 * 1024 * 1024; /** * Maximum age (seconds) of spent transactions in the transaction outputs * table */ protected static final long MAX_TX_AGE = 2 * 24 * 60 * 60; /** Block chain checkpoints */ protected static final Map<Integer, Sha256Hash> checkpoints = new HashMap<Integer, Sha256Hash>(); static { checkpoints .put(50000, new Sha256Hash( "000000001aeae195809d120b5d66a39c83eb48792e068f8ea1fea19d84a4278a")); checkpoints .put(75000, new Sha256Hash( "00000000000ace2adaabf1baf9dc0ec54434db11e9fd63c1819d8d77df40afda")); checkpoints .put(100000, new Sha256Hash( "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506")); checkpoints .put(125000, new Sha256Hash( "00000000000042391c3620056af66ca9ad7cb962424a9b34611915cebb9e1a2a")); checkpoints .put(150000, new Sha256Hash( "0000000000000a3290f20e75860d505ce0e948a1d1d846bec7e39015d242884b")); checkpoints .put(175000, new Sha256Hash( "00000000000006b975c097e9a5235de03d9024ddb205fd24dfcd508403fa907c")); checkpoints .put(200000, new Sha256Hash( "000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf")); checkpoints .put(225000, new Sha256Hash( "000000000000013d8781110987bf0e9f230e3cc85127d1ee752d5dd014f8a8e1")); checkpoints .put(250000, new Sha256Hash( "000000000000003887df1f29024b06fc2200b55f8af8f35453d7be294df2d214")); checkpoints .put(275000, new Sha256Hash( "00000000000000044750d80a0d3f3e307e54e8802397ae840d91adc28068f5bc")); checkpoints .put(300000, new Sha256Hash( "000000000000000082ccf8f1557c5d40b21edabb18d2d691cfbf87118bac7254")); checkpoints .put(325000, new Sha256Hash( "00000000000000000409695bce21828b31a7143fa35fcab64670dd337a71425d")); } /** Database update lock */ protected final Object lock = new Object(); /** Application data path */ protected String dataPath; /** Chain update time */ protected long chainTime; /** Chain head */ protected Sha256Hash chainHead; /** Block preceding the chain head */ protected Sha256Hash prevChainHead; /** Target difficulty */ protected long targetDifficulty; /** Current chain height */ protected int chainHeight; /** Current chain work */ protected BigInteger chainWork; /** Current block file number */ protected int blockFileNumber; /** Compressed block inflater */ protected final Inflater inflater = new Inflater(); /** Compressed block deflater */ protected final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); /** * Creates a BlockStore * * @param dataPath * Application data path */ public BlockStore(String dataPath) { this.dataPath = dataPath; // // Create the Blocks subdirectory if it doesn't exist // File blocksDir = new File(dataPath + LSystem.FS + "Blocks"); if (!blocksDir.exists()) { try { FileUtils.makedirs(blocksDir); } catch (IOException e) { e.printStackTrace(); } } } /** * Closes the database */ public void close() { } /** * Compacts the database tables * * @throws BlockStoreException * Unable to compact database */ public void compactDatabase() throws BlockStoreException { } /** * Returns the block hash for the current chain head * * @return Chain head block hash */ public Sha256Hash getChainHead() { return chainHead; } /** * Returns the current chain height * * @return Current chain height */ public int getChainHeight() { return chainHeight; } /** * Returns the current target difficulty as a BigInteger * * @return Target difficulty */ public BigInteger getTargetDifficulty() { return Helper.decodeCompactBits(targetDifficulty); } /** * Returns the current chain work * * @return Current chain work */ public BigInteger getChainWork() { return chainWork; } /** * Checks if the block is already in our database * * @param blockHash * The block to check * @return TRUE if this is a new block * @throws BlockStoreException * Unable to check the block status */ public abstract boolean isNewBlock(Sha256Hash blockHash) throws BlockStoreException; /** * Checks if the alert is already in our database * * @param alertID * Alert identifier * @return TRUE if this is a new alert * @throws BlockStoreException * Unable to get the alert status */ public abstract boolean isNewAlert(int alertID) throws BlockStoreException; /** * Returns a list of all alerts in the database * * @return List of all alerts * @throws BlockStoreException * Unable to get alerts from database */ public abstract List<Alert> getAlerts() throws BlockStoreException; /** * Stores an alert in the database * * @param alert * The alert * @throws BlockStoreException * Unable to store the alert */ public abstract void storeAlert(Alert alert) throws BlockStoreException; /** * Cancels an alert * * @param alertID * The alert identifier * @throws BlockStoreException * Unable to update the alert */ public abstract void cancelAlert(int alertID) throws BlockStoreException; /** * Checks if the block is on the main chain * * @param blockHash * The block to check * @return TRUE if the block is on the main chain * @throws BlockStoreException * Unable to get the block status */ public abstract boolean isOnChain(Sha256Hash blockHash) throws BlockStoreException; /** * Returns a block that was stored in the database. The returned block * represents the block data sent over the wire and does not include any * information about the block location within the block chain. * * @param blockHash * Block hash * @return The block or null if the block is not found * @throws BlockStoreException * Unable to get block from database */ public abstract Block getBlock(Sha256Hash blockHash) throws BlockStoreException; /** * Returns the block hash for the block stored at the specified height. * * @param height * Chain height * @return The block hash or null if the block is not found * @throws BlockStoreException * Unable to get block from database */ public abstract Sha256Hash getBlockId(int height) throws BlockStoreException; /** * Returns a block that was stored in the database. The returned block * contains the basic block plus information about its current location * within the block chain. * * @param blockHash * The block hash * @return The stored block or null if the block is not found * @throws BlockStoreException * Unable to get block from database */ public abstract StoredBlock getStoredBlock(Sha256Hash blockHash) throws BlockStoreException; /** * Returns the child block for the specified block * * @param blockHash * The block hash * @return The stored block or null if the block is not found * @throws BlockStoreException * Unable to get block */ public abstract StoredBlock getChildStoredBlock(Sha256Hash blockHash) throws BlockStoreException; /** * Returns the block status for recent blocks * * @param maxCount * The maximum number of blocks to be returned * @return A list of BlockStatus objects * @throws BlockStoreException * Unable to get block status */ public abstract List<BlockStatus> getBlockStatus(int maxCount) throws BlockStoreException; /** * Check if this is a new transaction * * @param txHash * Transaction hash * @return TRUE if the transaction is not in the database * @throws BlockStoreException * Unable to check transaction status */ public abstract boolean isNewTransaction(Sha256Hash txHash) throws BlockStoreException; /** * Returns the transaction depth. A depth of 0 indicates the transaction is * not in a block on the current chain. * * @param txHash * Transaction hash * @return Transaction depth * @throws BlockStoreException * Unable to get transaction depth */ public abstract int getTxDepth(Sha256Hash txHash) throws BlockStoreException; /** * Returns the requested transaction output * * @param outPoint * Transaction outpoint * @return Transaction output or null if the transaction is not found * @throws BlockStoreException * Unable to get transaction output status */ public abstract StoredOutput getTxOutput(OutPoint outPoint) throws BlockStoreException; /** * Returns the outputs for the specified transaction * * @param txHash * Transaction hash * @return Stored output list * @throws BlockStoreException * Unable to get transaction outputs */ public abstract List<StoredOutput> getTxOutputs(Sha256Hash txHash) throws BlockStoreException; /** * Deletes spent transaction outputs that are older than the maximum * transaction age * * @return The number of deleted outputs * @throws BlockStoreException * Unable to delete spent transaction outputs */ public abstract int deleteSpentTxOutputs() throws BlockStoreException; /** * Returns the chain list from the block following the start block up to the * stop block. A maximum of 500 blocks will be returned. The list will start * with the genesis block if the start block is not found. * * @param startBlock * The start block * @param stopBlock * The stop block * @return Block inventory list * @throws BlockStoreException * Unable to get blocks from database */ public abstract List<InventoryItem> getChainList(Sha256Hash startBlock, Sha256Hash stopBlock) throws BlockStoreException; /** * Returns the chain list from the block following the start block up to the * stop block. A maximum of 500 blocks will be returned. * * @param startHeight * Start block height * @param stopBlock * Stop block * @return Block inventory list * @throws BlockStoreException * Unable to get blocks from database */ public abstract List<InventoryItem> getChainList(int startHeight, Sha256Hash stopBlock) throws BlockStoreException; /** * Returns the header list from the block following the start block up to * the stop block. A maximum of 2000 blocks will be returned. The list will * start with the genesis block if the start block is not found. * * @param startBlock * The start block * @param stopBlock * The stop block * @return Block header list (empty list if one or more blocks not found) * @throws BlockStoreException * Unable to get data from the database */ public abstract List<BlockHeader> getHeaderList(Sha256Hash startBlock, Sha256Hash stopBlock) throws BlockStoreException; /** * Releases a held block for processing * * @param blockHash * Block hash * @throws BlockStoreException * Unable to release the block */ public abstract void releaseBlock(Sha256Hash blockHash) throws BlockStoreException; /** * Stores a block in the database * * @param storedBlock * Block to be stored * @throws BlockStoreException * Unable to store the block */ public abstract void storeBlock(StoredBlock storedBlock) throws BlockStoreException; /** * Locates the junction where the chain represented by the specified block * joins the current block chain. The returned list starts with the junction * block and contains all blocks in the chain leading to the specified * block. The StoredBlock object for the junction block will not contain a * Block object while the StoredBlock objects for the blocks in the new * chain will contain Block objects. * * A BlockNotFoundException will be thrown if the chain cannot be resolved * because a block is missing. The caller should get the block from a peer, * store it in the database and then retry. * * A ChainTooLongException will be thrown if the block chain is getting too * big. The caller should restart the chain resolution closer to the * junction block and then work backwards toward the original block. * * @param chainHash * The block hash of the chain head * @return List of blocks in the chain leading to the new head * @throws BlockNotFoundException * A block in the chain was not found * @throws BlockStoreException * Unable to get blocks from the database * @throws ChainTooLongException * The block chain is too long */ public abstract List<StoredBlock> getJunction(Sha256Hash chainHash) throws BlockNotFoundException, BlockStoreException, ChainTooLongException; /** * Changes the chain head and updates all blocks from the junction block up * to the new chain head. The junction block is the point where the current * chain and the new chain intersect. A VerificationException will be thrown * if a block in the new chain is for a checkpoint block and the block hash * doesn't match the checkpoint hash. * * @param chainList * List of all chain blocks starting with the junction block up * to and including the new chain head * @throws BlockStoreException * Unable to update the database * @throws VerificationException * Chain verification failed */ public abstract void setChainHead(List<StoredBlock> chainList) throws BlockStoreException, VerificationException; /** * Returns a block that was stored in one of the block files * * An uncompressed block has the following format: Bytes 0-3: Magic number * Bytes 4-7: Block length Bytes 8-n: Block data * * A compressed block has the following format: Bytes 0-3: Magic number * Bytes 4-7: Compressed data length with the high-order bit set to 1 Bytes * 8-11: Uncompressed data length Bytes 12-n: Compressed block data * * @param fileNumber * The block file number * @param fileOffset * The block offset within the file * @return The requested block or null if the block is not found * @throws BlockStoreException * Unable to read the block data */ protected Block getBlock(int fileNumber, int fileOffset) throws BlockStoreException { if (fileNumber < 0) { throw new BlockStoreException(String.format( "Invalid file number %d", fileNumber)); } Block block = null; File blockFile = new File(String.format("%s%sBlocks%sblk%05d.dat", dataPath, LSystem.FS, LSystem.FS, fileNumber)); if (!blockFile.exists()) { BTCLoader.info(String.format("Block file %d does not exist", fileNumber)); return null; } try { try (RandomAccessFile inFile = new RandomAccessFile(blockFile, "r")) { // // Read the block prefix // inFile.seek(fileOffset); byte[] bytes = new byte[8]; int count = inFile.read(bytes); if (count != 8) { BTCLoader.error(String .format("End-of-data reading from block file %d, offset %d", fileNumber, fileOffset)); throw new BlockStoreException("Unable to read block file"); } long magic = Helper.readUint32LE(bytes, 0); long length = Helper.readUint32LE(bytes, 4); if (magic != NetParams.MAGIC_NUMBER) { BTCLoader.error(String .format("Magic number %X is incorrect in block file %d, offset %d", magic, fileNumber, fileOffset)); throw new BlockStoreException("Incorrect block file format"); } if ((length & 0x80000000L) != 0) { // // Read the compressed block data // length &= 0x7fffffffL; byte[] compressedData = new byte[(int) length + 4]; count = inFile.read(compressedData); if (count != length + 4) { BTCLoader.error(String .format("End-of-data reading compressed block from file %d, offset %d", fileNumber, fileOffset)); throw new BlockStoreException( "Unable to read block file"); } length = Helper.readUint32LE(compressedData, 0); byte[] blockData = new byte[(int) length]; synchronized (inflater) { inflater.reset(); inflater.setInput(compressedData, 4, compressedData.length - 4); count = inflater.inflate(blockData); if (count != length || !inflater.finished()) { BTCLoader.error(String .format("Incomplete compressed block read from file %d, offset %d", fileNumber, fileOffset)); throw new BlockStoreException( "Unable to read block file"); } } block = new Block(blockData, 0, (int) length, false); } else { byte[] blockData = new byte[(int) length]; count = inFile.read(blockData); if (count != length) { BTCLoader.error(String .format("End-of-data reading uncompressed block from file %d, offset %d", fileNumber, fileOffset)); throw new BlockStoreException( "Unable to read block file"); } block = new Block(blockData, 0, (int) length, false); } } } catch (DataFormatException | IOException | VerificationException exc) { BTCLoader.error(String.format("Unable to read block file %d, offset %d", fileNumber, fileOffset), exc); throw new BlockStoreException("Unable to read block file"); } return block; } protected int[] storeBlock(Block block) throws BlockStoreException { int[] blockLocation = new int[2]; try { byte[] blockData = block.getBytes(); byte[] compressedData = new byte[blockData.length + 128]; int offset = 0; synchronized (deflater) { deflater.reset(); deflater.setInput(blockData); deflater.finish(); while (true) { int count = deflater.deflate(compressedData, offset, compressedData.length - offset); offset += count; if (deflater.finished()){ break; } compressedData = Arrays .copyOf(compressedData, offset + (blockData.length - (int) deflater .getBytesRead() + 256)); } } String fileName = String.format("%s%sBlocks%sblk%05d.dat", dataPath, LSystem.FS, LSystem.FS, blockFileNumber); File blockFile = new File(fileName); if(!blockFile.exists()){ FileUtils.makedirs(blockFile); } long filePosition = blockFile.length(); if (filePosition >= MAX_BLOCK_FILE_SIZE) { blockFileNumber++; filePosition = 0; blockFile = new File(fileName); if (blockFile.exists()){ blockFile.delete(); } } try (RandomAccessFile outFile = new RandomAccessFile(blockFile, "rws")) { outFile.seek(filePosition); byte[] bytes = new byte[12]; Helper.uint32ToByteArrayLE(NetParams.MAGIC_NUMBER, bytes, 0); Helper.uint32ToByteArrayLE((long) offset | 0x80000000L, bytes, 4); Helper.uint32ToByteArrayLE(blockData.length, bytes, 8); outFile.write(bytes); outFile.write(compressedData, 0, offset); blockLocation[0] = blockFileNumber; blockLocation[1] = (int) filePosition; } } catch (IOException exc) { BTCLoader.error(String.format("Unable to write to block file %d", blockFileNumber), exc); throw new BlockStoreException("Unable to write to block file"); } return blockLocation; } /** * Truncate a block file to recover from a database error * * @param fileLocation * The file location returned by storeBlock() */ protected void truncateBlockFile(int[] fileLocation) { File blockFile = new File(String.format("%s%sBlocks%sblk%05d.dat", dataPath, LSystem.FS, LSystem.FS, fileLocation[0])); try { // // If the block is stored at the beginning of the file, just delete // the file // and decrement the block number. Otherwise, truncate the file. if (fileLocation[1] == 0) { blockFile.delete(); blockFileNumber--; } else { try (RandomAccessFile outFile = new RandomAccessFile(blockFile, "rws")) { outFile.getChannel().truncate(fileLocation[1]); } } } catch (IOException exc) { BTCLoader.error(String.format("Unable to truncate block file %d", fileLocation[0]), exc); } } public abstract void setAddressLabel(Address address) throws BlockStoreException; public abstract void storeAddress(Address address) throws BlockStoreException; public abstract void deleteAddress(Address address) throws BlockStoreException ; public abstract List<Address> getAddressList() throws BlockStoreException ; public abstract void storeKey(ECKey key) throws BlockStoreException; public abstract void setKeyLabel(ECKey key) throws BlockStoreException; public abstract List<ECKey> getKeyList() throws KeyException, BlockStoreException; public abstract void storeReceiveTx(ReceiveTransaction receiveTx) throws BlockStoreException; public abstract void setTxSpent(Sha256Hash txHash, int txIndex, boolean isSpent) throws BlockStoreException; public abstract void setTxSafe(Sha256Hash txHash, int txIndex, boolean inSafe) throws BlockStoreException; public abstract void setReceiveTxDelete(Sha256Hash txHash, int txIndex, boolean isDeleted) throws BlockStoreException ; public abstract List<ReceiveTransaction> getReceiveTxList() throws BlockStoreException; public abstract void storeSendTx(SendTransaction sendTx) throws BlockStoreException; public abstract void setSendTxDelete(Sha256Hash txHash, boolean isDeleted) throws BlockStoreException; public abstract SendTransaction getSendTx(Sha256Hash txHash) throws BlockStoreException; public abstract List<SendTransaction> getSendTxList() throws BlockStoreException; public abstract void deleteTransactions(long rescanTime) throws BlockStoreException; public abstract StoredHeader getHeader(Sha256Hash blockHash) throws BlockStoreException ; public abstract StoredHeader getChildHeader(Sha256Hash parentHash) throws BlockStoreException ; public abstract void updateMatches(BlockHeader header) throws BlockStoreException; public abstract int getRescanHeight(long rescanTime) throws BlockStoreException; public abstract Sha256Hash getBlockHash(int blockHeight) throws BlockStoreException; public abstract List<StoredHeader> getJunctionHeader(Sha256Hash chainHash) throws BlockNotFoundException, BlockStoreException ; public abstract void setChainStoredHead(List<StoredHeader> chainList) throws BlockStoreException, VerificationException; public abstract void storeHeader(StoredHeader storedHeader) throws BlockStoreException ; }