/******************************************************************************
* 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();
}
}
}