/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt; import nxt.db.DbUtils; import nxt.util.Logger; import java.math.BigInteger; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; final class BlockDb { /** Block cache */ static final int BLOCK_CACHE_SIZE = 10; static final Map<Long, BlockImpl> blockCache = new HashMap<>(); static final SortedMap<Integer, BlockImpl> heightMap = new TreeMap<>(); static final Map<Long, TransactionImpl> transactionCache = new HashMap<>(); static final Blockchain blockchain = Nxt.getBlockchain(); static { Nxt.getBlockchainProcessor().addListener((block) -> { synchronized (blockCache) { int height = block.getHeight(); Iterator<BlockImpl> it = blockCache.values().iterator(); while (it.hasNext()) { Block cacheBlock = it.next(); int cacheHeight = cacheBlock.getHeight(); if (cacheHeight <= height - BLOCK_CACHE_SIZE || cacheHeight >= height) { cacheBlock.getTransactions().forEach((tx) -> transactionCache.remove(tx.getId())); heightMap.remove(cacheHeight); it.remove(); } } block.getTransactions().forEach((tx) -> transactionCache.put(tx.getId(), (TransactionImpl)tx)); heightMap.put(height, (BlockImpl)block); blockCache.put(block.getId(), (BlockImpl)block); } }, BlockchainProcessor.Event.BLOCK_PUSHED); } static private void clearBlockCache() { synchronized (blockCache) { blockCache.clear(); heightMap.clear(); transactionCache.clear(); } } static BlockImpl findBlock(long blockId) { // Check the block cache synchronized (blockCache) { BlockImpl block = blockCache.get(blockId); if (block != null) { return block; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block WHERE id = ?")) { pstmt.setLong(1, blockId); try (ResultSet rs = pstmt.executeQuery()) { BlockImpl block = null; if (rs.next()) { block = loadBlock(con, rs); } return block; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static boolean hasBlock(long blockId) { return hasBlock(blockId, Integer.MAX_VALUE); } static boolean hasBlock(long blockId, int height) { // Check the block cache synchronized(blockCache) { BlockImpl block = blockCache.get(blockId); if (block != null) { return block.getHeight() <= height; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT height FROM block WHERE id = ?")) { pstmt.setLong(1, blockId); try (ResultSet rs = pstmt.executeQuery()) { return rs.next() && rs.getInt("height") <= height; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static long findBlockIdAtHeight(int height) { // Check the cache synchronized(blockCache) { BlockImpl block = heightMap.get(height); if (block != null) { return block.getId(); } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT id FROM block WHERE height = ?")) { pstmt.setInt(1, height); try (ResultSet rs = pstmt.executeQuery()) { if (!rs.next()) { throw new RuntimeException("Block at height " + height + " not found in database!"); } return rs.getLong("id"); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static BlockImpl findBlockAtHeight(int height) { // Check the cache synchronized(blockCache) { BlockImpl block = heightMap.get(height); if (block != null) { return block; } } // Search the database try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block WHERE height = ?")) { pstmt.setInt(1, height); try (ResultSet rs = pstmt.executeQuery()) { BlockImpl block; if (rs.next()) { block = loadBlock(con, rs); } else { throw new RuntimeException("Block at height " + height + " not found in database!"); } return block; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static BlockImpl findLastBlock() { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block ORDER BY timestamp DESC LIMIT 1")) { BlockImpl block = null; try (ResultSet rs = pstmt.executeQuery()) { if (rs.next()) { block = loadBlock(con, rs); } } return block; } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static BlockImpl findLastBlock(int timestamp) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT * FROM block WHERE timestamp <= ? ORDER BY timestamp DESC LIMIT 1")) { pstmt.setInt(1, timestamp); BlockImpl block = null; try (ResultSet rs = pstmt.executeQuery()) { if (rs.next()) { block = loadBlock(con, rs); } } return block; } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static BlockImpl loadBlock(Connection con, ResultSet rs) { return loadBlock(con, rs, false); } static BlockImpl loadBlock(Connection con, ResultSet rs, boolean loadTransactions) { try { int version = rs.getInt("version"); int timestamp = rs.getInt("timestamp"); long previousBlockId = rs.getLong("previous_block_id"); long totalAmountNQT = rs.getLong("total_amount"); long totalFeeNQT = rs.getLong("total_fee"); int payloadLength = rs.getInt("payload_length"); long generatorId = rs.getLong("generator_id"); byte[] previousBlockHash = rs.getBytes("previous_block_hash"); BigInteger cumulativeDifficulty = new BigInteger(rs.getBytes("cumulative_difficulty")); long baseTarget = rs.getLong("base_target"); long nextBlockId = rs.getLong("next_block_id"); int height = rs.getInt("height"); byte[] generationSignature = rs.getBytes("generation_signature"); byte[] blockSignature = rs.getBytes("block_signature"); byte[] payloadHash = rs.getBytes("payload_hash"); long id = rs.getLong("id"); return new BlockImpl(version, timestamp, previousBlockId, totalAmountNQT, totalFeeNQT, payloadLength, payloadHash, generatorId, generationSignature, blockSignature, previousBlockHash, cumulativeDifficulty, baseTarget, nextBlockId, height, id, loadTransactions ? TransactionDb.findBlockTransactions(con, id) : null); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static void saveBlock(Connection con, BlockImpl block) { try { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO block (id, version, timestamp, previous_block_id, " + "total_amount, total_fee, payload_length, previous_block_hash, cumulative_difficulty, " + "base_target, height, generation_signature, block_signature, payload_hash, generator_id) " + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, block.getId()); pstmt.setInt(++i, block.getVersion()); pstmt.setInt(++i, block.getTimestamp()); DbUtils.setLongZeroToNull(pstmt, ++i, block.getPreviousBlockId()); pstmt.setLong(++i, block.getTotalAmountNQT()); pstmt.setLong(++i, block.getTotalFeeNQT()); pstmt.setInt(++i, block.getPayloadLength()); pstmt.setBytes(++i, block.getPreviousBlockHash()); pstmt.setBytes(++i, block.getCumulativeDifficulty().toByteArray()); pstmt.setLong(++i, block.getBaseTarget()); pstmt.setInt(++i, block.getHeight()); pstmt.setBytes(++i, block.getGenerationSignature()); pstmt.setBytes(++i, block.getBlockSignature()); pstmt.setBytes(++i, block.getPayloadHash()); pstmt.setLong(++i, block.getGeneratorId()); pstmt.executeUpdate(); TransactionDb.saveTransactions(con, block.getTransactions()); } if (block.getPreviousBlockId() != 0) { try (PreparedStatement pstmt = con.prepareStatement("UPDATE block SET next_block_id = ? WHERE id = ?")) { pstmt.setLong(1, block.getId()); pstmt.setLong(2, block.getPreviousBlockId()); pstmt.executeUpdate(); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } // relying on cascade triggers in the database to delete the transactions and public keys for all deleted blocks static void deleteBlocksFrom(long blockId) { if (!Db.db.isInTransaction()) { try { Db.db.beginTransaction(); deleteBlocksFrom(blockId); Db.db.commitTransaction(); } catch (Exception e) { Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } return; } try (Connection con = Db.db.getConnection(); PreparedStatement pstmtSelect = con.prepareStatement("SELECT db_id FROM block WHERE timestamp >= " + "(SELECT timestamp FROM block WHERE id = ?) ORDER BY timestamp DESC"); PreparedStatement pstmtDelete = con.prepareStatement("DELETE FROM block WHERE db_id = ?")) { try { pstmtSelect.setLong(1, blockId); try (ResultSet rs = pstmtSelect.executeQuery()) { Db.db.commitTransaction(); while (rs.next()) { pstmtDelete.setLong(1, rs.getLong("db_id")); pstmtDelete.executeUpdate(); Db.db.commitTransaction(); } } } catch (SQLException e) { Db.db.rollbackTransaction(); throw e; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } finally { clearBlockCache(); } } static void deleteAll() { if (!Db.db.isInTransaction()) { try { Db.db.beginTransaction(); deleteAll(); Db.db.commitTransaction(); } catch (Exception e) { Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } return; } Logger.logMessage("Deleting blockchain..."); try (Connection con = Db.db.getConnection(); Statement stmt = con.createStatement()) { try { stmt.executeUpdate("SET REFERENTIAL_INTEGRITY FALSE"); stmt.executeUpdate("TRUNCATE TABLE transaction"); stmt.executeUpdate("TRUNCATE TABLE block"); BlockchainProcessorImpl.getInstance().getDerivedTables().forEach(table -> { if (table.isPersistent()) { try { stmt.executeUpdate("TRUNCATE TABLE " + table.toString()); } catch (SQLException ignore) {} } }); stmt.executeUpdate("SET REFERENTIAL_INTEGRITY TRUE"); Db.db.commitTransaction(); } catch (SQLException e) { Db.db.rollbackTransaction(); throw e; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } finally { clearBlockCache(); } } }