package co.nyzo.verifier; import co.nyzo.verifier.util.*; import java.io.*; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class HistoricalBlockManager { public static final String startManagerKey = "start_historical_block_manager"; private static final AtomicBoolean alive = new AtomicBoolean(false); public static void start() { // Start the manager if the preference indicates. Resource usage is not trivial, so the default is false. if (PreferencesUtil.getBoolean(startManagerKey, false) && !alive.getAndSet(true)) { new Thread(new Runnable() { @Override public void run() { while (!UpdateUtil.shouldTerminate()) { try { // Sleep for 5 minutes (300 seconds) in 3-second increments. This is a process that provides // access to 1000-block sections of the Nyzo blockchain history, so each unit of work for // this process covers 7000 seconds of the blockchain. Sleeping for five minutes between // iterations keeps resource usage at a reasonable rate while leaving plenty of room for // eventually building and indexing a full history on this node. for (int i = 0; i < 100; i++) { ThreadUtil.sleep(3000); } // Build an offset file. buildOffsetFile(); } catch (Exception e) { LogUtil.println("HistoricalBlockManager: exception in outer thread" + PrintUtil.printException(e)); } } alive.set(false); } }).start(); } else { LogUtil.println("HistoricalBlockManager: not starting"); } } private static void buildOffsetFile() { // This is a brute-force process for finding which offset file to build. Just before a consolidated file is // written by the block-file consolidator, its corresponding offset file is deleted to ensure that stale offset // files do not exist. This process checks all consolidated files backward from the frozen edge. When a // consolidated file without an offset file is found, the offset file is built. long offsetFileHeight = -1L; for (long height = BlockManager.getFrozenEdgeHeight(); height >= 0 && offsetFileHeight < 0; height -= BlockManager.blocksPerFile) { if (BlockManager.consolidatedFileForBlockHeight(height).exists() && !offsetFileForHeight(height).exists()) { offsetFileHeight = height; } } if (offsetFileHeight >= 0) { // Calculate the offsets. File consolidatedFile = BlockManager.consolidatedFileForBlockHeight(offsetFileHeight); int[] offsets = blockOffsetsForConsolidatedFile(consolidatedFile); // Write the offsets to the file. byte[] offsetBytes = new byte[offsets.length * 4]; ByteBuffer offsetBuffer = ByteBuffer.wrap(offsetBytes); for (int offset : offsets) { offsetBuffer.putInt(offset); } try { Files.write(Paths.get(offsetFileForHeight(offsetFileHeight).getAbsolutePath()), offsetBytes); } catch (Exception ignored) { } } } private static int[] blockOffsetsForConsolidatedFile(File file) { // The result contains a start offset and an end offset for each of the 1000 blocks that might be in the file. // The block heights are implicit, relative to the start height of the file. The offsets are 32-bit integers. int blocksPerFile = (int) BlockManager.blocksPerFile; int[] offsets = new int[blocksPerFile * 2]; for (int i = 0; i < blocksPerFile; i++) { offsets[i] = -1; } // Generate the offsets. if (file.exists()) { Path path = Paths.get(file.getAbsolutePath()); try { byte[] fileBytes = Files.readAllBytes(path); ByteBuffer buffer = ByteBuffer.wrap(fileBytes); int numberOfBlocks = buffer.getShort(); Block previousBlock = null; for (int i = 0; i < numberOfBlocks; i++) { // Record the offset before and after reading the block. int blockStartOffset = buffer.position(); Block block = Block.fromByteBuffer(buffer, false); int blockEndOffset = buffer.position(); // Read and discard the balance list, if present. if (previousBlock == null || (previousBlock.getBlockHeight() != block.getBlockHeight() - 1)) { BalanceList.fromByteBuffer(buffer); } // Store the offsets in the array. int offsetArrayIndex = (int) (block.getBlockHeight() % BlockManager.blocksPerFile); if (offsetArrayIndex >= 0 && offsetArrayIndex < blocksPerFile) { offsets[offsetArrayIndex * 2] = blockStartOffset; offsets[offsetArrayIndex * 2 + 1] = blockEndOffset; } // Store the block for use in the next iteration. previousBlock = block; } } catch (Exception ignored) { } } return offsets; } public static Block blockForHeight(long height) { // First, look to individual files that may not have been consolidated yet. File file = BlockManager.individualFileForBlockHeight(height); Block block = null; if (file.exists()) { List<Block> blocksInFile = BlockManager.loadBlocksInFile(file, height, height); if (blocksInFile.size() > 0 && blocksInFile.get(0).getBlockHeight() == height) { block = blocksInFile.get(0); } } // Next, look to indexed consolidated files. File offsetFile = offsetFileForHeight(height); if (block == null && offsetFile.exists()) { try { // Read the offsets from the file into a byte array. RandomAccessFile offsetFileReader = new RandomAccessFile(offsetFile, "r"); offsetFileReader.seek((height % BlockManager.blocksPerFile) * 8); byte[] offsetBytes = new byte[8]; offsetFileReader.read(offsetBytes); offsetFileReader.close(); // Get the offsets as integers. ByteBuffer offsetBuffer = ByteBuffer.wrap(offsetBytes); int startOffset = offsetBuffer.getInt(); int endOffset = offsetBuffer.getInt(); // If the block is in the file, read it. if (startOffset >= 0 && endOffset >= 0) { // Read the block bytes from the block file. File consolidatedFile = BlockManager.consolidatedFileForBlockHeight(height); RandomAccessFile blockFileReader = new RandomAccessFile(consolidatedFile, "r"); blockFileReader.seek(startOffset); byte[] blockBytes = new byte[endOffset - startOffset]; blockFileReader.read(blockBytes); blockFileReader.close(); // Make the block from the bytes. ByteBuffer blockBuffer = ByteBuffer.wrap(blockBytes); block = Block.fromByteBuffer(blockBuffer); } } catch (Exception ignored) { } } return block; } public static File offsetFileForHeight(long height) { File blockFile = BlockManager.consolidatedFileForBlockHeight(height); return new File(blockFile.getAbsolutePath() + "_offsets"); } }