package org.ripple.power.txns.btc;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.ripple.power.Helper;
import org.ripple.power.config.LSystem;
import org.ripple.power.utils.FileUtils;
public class BlockStoreDataBase extends BlockStore {
/** Settings table definition */
public static final String Settings_Table = "CREATE TABLE IF NOT EXISTS Settings ("
+ "schema_name VARCHAR(32) NOT NULL," + // Schema
// name
"schema_version INTEGER NOT NULL)"; // Schema
// version
/** Blocks table definition */
public static final String Blocks_Table = "CREATE TABLE IF NOT EXISTS Blocks ("
+ "db_id IDENTITY," + // Database identity
"block_hash_index BIGINT NOT NULL," + // Block hash index
"block_hash BINARY NOT NULL," + // Block hash
"prev_hash_index BIGINT NOT NULL," + // Previous hash
// index
"prev_hash BINARY NOT NULL," + // Previous hash
"timestamp BIGINT NOT NULL," + // Block timestamp
"block_height INTEGER NOT NULL," + // Block height or
// -1
"chain_work BINARY NOT NULL," + // Cumulative chain
// work
"on_hold BOOLEAN NOT NULL," + // Block is held
"file_number INTEGER NOT NULL," + // Block file
// number
"file_offset INTEGER NOT NULL," + // Block offset
// within block
// file
"header BINARY NOT NULL)"; // Block header
public static final String Blocks_IX1 = "CREATE INDEX IF NOT EXISTS Blocks_IX1 on Blocks(block_hash_index)";
public static final String Blocks_IX2 = "CREATE INDEX IF NOT EXISTS Blocks_IX2 ON Blocks(prev_hash_index)";
public static final String Blocks_IX3 = "CREATE INDEX IF NOT EXISTS Blocks_IX3 ON Blocks(block_height)";
/** TxOutputs table definition */
public static final String TxOutputs_Table = "CREATE TABLE IF NOT EXISTS TxOutputs ("
+ "db_id IDENTITY," + // Database identity
"tx_hash_index BIGINT NOT NULL," + // Transaction hash
// index
"tx_hash BINARY NOT NULL," + // Transaction hash
"tx_index SMALLINT NOT NULL," + // Output index
"block_hash BINARY NOT NULL," + // Block hash
"block_height INTEGER NOT NULL," + // Block height
// when output
// spent
"time_spent BIGINT NOT NULL," + // Time when output
// spent
"is_coinbase BOOLEAN NOT NULL," + // Coinbase
// transaction
"value BIGINT NOT NULL," + // Value
"script_bytes BINARY NOT NULL)"; // Script bytes
public static final String TxOutputs_IX1 = "CREATE INDEX IF NOT EXISTS TxOutputs_IX1 ON TxOutputs(tx_hash_index)";
/** TxSpentOutputs table definition */
public static final String TxSpentOutputs_Table = "CREATE TABLE IF NOT EXISTS TxSpentOutputs ("
+ "time_spent BIGINT NOT NULL," + // Time when
// output spent
"db_id BIGINT NOT NULL " + // Referenced spent
// output
"REFERENCES TxOutputs(db_id) ON DELETE CASCADE)";
public static final String TxSpentOutputs_IX1 = "CREATE INDEX IF NOT EXISTS TxSpentOutputs_IX1 ON TxSpentOutputs(time_spent)";
/** Alerts table definition */
public static final String Alerts_Table = "CREATE TABLE IF NOT EXISTS Alerts ("
+ "alert_id INTEGER NOT NULL " + // Alert
// identifier
"PRIMARY KEY," + "is_cancelled BOOLEAN NOT NULL," + // Alert
// cancelled
"payload BINARY NOT NULL," + // Payload
"signature BINARY NOT NULL)"; // Signature
/** Addresses table definitions */
private static final String Addresses_Table = "CREATE TABLE IF NOT EXISTS Addresses ("
+ "db_id IDENTITY," // Row identity
+ "address BINARY NOT NULL," // Bitcoin address
+ "label VARCHAR)"; // Associated label or null
/** Keys table definitions */
private static final String Keys_Table = "CREATE TABLE IF NOT EXISTS Keys ("
+ "db_id IDENTITY," // Row identity
+ "public_key BINARY NOT NULL," // Public key
+ "private_key BINARY NOT NULL," // Encrypted private
// key
+ "timestamp BIGINT NOT NULL," // Time key created
+ "label VARCHAR," // Associated label or null
+ "is_change BOOLEAN NOT NULL)"; // Is a change key
/** Received table definitions */
private static final String Received_Table = "CREATE TABLE IF NOT EXISTS Received ("
+ "db_id IDENTITY," // Row identity
+ "tx_hash_index BIGINT NOT NULL," // Transaction hash
// index
+ "tx_hash BINARY NOT NULL," // Transaction hash
+ "tx_index SMALLINT NOT NULL," // Transaction
// output index
+ "norm_hash BINARY NOT NULL," // Normalized
// transaction hash
+ "timestamp BIGINT NOT NULL," // Transaction
// timestamp
+ "block_hash BINARY," // Block containing the
// transaction or null
+ "address BINARY NOT NULL," // Recipient address
+ "value BIGINT NOT NULL," // Transaction value
+ "script_bytes BINARY NOT NULL," // Transaction output
// script bytes
+ "is_spent BOOLEAN NOT NULL," // Transaction output
// is spent
+ "is_change BOOLEAN NOT NULL," // Address is a
// change address
+ "in_safe BOOLEAN NOT NULL," // Transaction output
// is in the safe
+ "is_coinbase BOOLEAN NOT NULL," // Transaction is
// coinbase
// transaction
+ "is_deleted BOOLEAN NOT NULL)"; // Transaction
// output is deleted
private static final String Received_IX1 = "CREATE INDEX IF NOT EXISTS Received_IX1 ON Received(tx_hash_index)";
/** Sent table definitions */
private static final String Sent_Table = "CREATE TABLE IF NOT EXISTS Sent ("
+ "db_id IDENTITY," // Row identity
+ "tx_hash_index BIGINT NOT NULL," // Transaction hash
// index
+ "tx_hash BINARY NOT NULL," // Transaction hash
+ "norm_hash BINARY NOT NULL," // Normalized
// transaction hash
+ "timestamp BIGINT NOT NULL," // Transaction
// timestamp
+ "block_hash BINARY," // Block containing the
// transaction or null
+ "address BINARY NOT NULL," // Recipient address
+ "value BIGINT NOT NULL," // Transaction value
+ "fee BIGINT NOT NULL," // Transaction fee
+ "is_deleted BOOLEAN NOT NULL," // Transaction is
// deleted
+ "tx_data BINARY NOT NULL)"; // Transaction data
private static final String Sent_IX1 = "CREATE UNIQUE INDEX IF NOT EXISTS Sent_IX1 ON Sent(tx_hash_index)";
/** Database schema name */
public static final String schemaName = LSystem.applicationName
+ " Block Store";
/** Database schema version */
public static final int schemaVersion = 102;
/** Per-thread database connection */
private final ThreadLocal<Connection> threadConnection = new ThreadLocal<>();
/** List of all database connections */
private final List<Connection> allConnections = Collections
.synchronizedList(new ArrayList<Connection>());
/** Database connection URL */
private final String connectionURL;
/**
* Create a BlockStore
*
* @param dataPath
* Application data path
* @throws BlockStoreException
* Unable to initialize the database
*/
public BlockStoreDataBase(String dataPath) throws BlockStoreException {
super(dataPath);
long maxMemory = Runtime.getRuntime().maxMemory() / (1024 * 1024);
long dbCacheSize;
if (maxMemory < 256)
dbCacheSize = 64;
else if (maxMemory < 384)
dbCacheSize = 128;
else
dbCacheSize = 256;
String databasePath = dataPath.replace('\\', '/');
connectionURL = String.format("jdbc:h2:%s/Database/bitcoin;"
+ "DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE;CACHE_SIZE=%d",
databasePath, dbCacheSize * 1024);
BTCLoader.info("Database connection URL: " + connectionURL);
//
// Load the JDBC driver
//
try {
Class.forName("org.h2.Driver");
} catch (ClassNotFoundException exc) {
BTCLoader.error("Unable to load the JDBC driver", exc);
throw new BlockStoreException("Unable to load the JDBC driver", exc);
}
//
// Initialize the database
//
if (tableExists("Settings")) {
getSettings();
} else {
createTables();
initTables();
}
}
/**
* Close the database
*/
@Override
public void close() {
int index = 0;
for (Connection conn : allConnections) {
index++;
try {
conn.close();
BTCLoader.info(String.format("Database connection %d closed",
index));
} catch (SQLException exc) {
BTCLoader.error(String.format(
"SQL error while closing connection %d", index), exc);
}
}
allConnections.clear();
}
/**
* Get the database connection for the current thread
*
* @return Connection for the current thread
* @throws BlockStoreException
* Unable to obtain a database connection
*/
private Connection getConnection() throws BlockStoreException {
Connection conn;
synchronized (lock) {
try {
conn = threadConnection.get();
if (conn == null || conn.isClosed()) {
threadConnection.set(DriverManager.getConnection(
connectionURL, "SCRIPTERRON", "Bitcoin"));
conn = threadConnection.get();
allConnections.add(conn);
BTCLoader.info(String.format(
"Database connection %d created",
allConnections.size()));
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to connect to SQL database %s", connectionURL),
exc);
throw new BlockStoreException(
"Unable to connect to SQL database");
}
}
return conn;
}
/**
* Rollback the current transaction and turn auto commit back on
*
* @param stmt
* Statement to be closed or null
*/
private void rollback(AutoCloseable... stmts) {
try {
Connection conn = getConnection();
for (AutoCloseable stmt : stmts)
if (stmt != null)
stmt.close();
conn.rollback();
conn.setAutoCommit(true);
} catch (Exception exc) {
BTCLoader.error("Unable to rollback transaction", exc);
}
}
/**
* Get the hash index for a SHA-256 hash
*
* @param hash
* SHA-256 hash
* @return Hash index
*/
private long getHashIndex(Sha256Hash hash) {
byte[] bytes = hash.getBytes();
return (((long) bytes[24] & 0xffL) << 56)
| (((long) bytes[25] & 0xffL) << 48)
| (((long) bytes[26] & 0xffL) << 40)
| (((long) bytes[27] & 0xffl) << 32)
| (((long) bytes[28] & 0xffL) << 24)
| (((long) bytes[29] & 0xffL) << 16)
| (((long) bytes[30] & 0xffL) << 8)
| ((long) bytes[31] & 0xffL);
}
/**
* Compacts the database tables
*
* All database connections will be closed and the database will be deleted
* and recreated. This means the database should be compacted only in
* single-thread maintenance mode.
*
* @throws BlockStoreException
* Unable to compact database
*/
@Override
public void compactDatabase() throws BlockStoreException {
File backupFile = new File(String.format("%s%sDatabase%sbackup.sql.gz",
dataPath, LSystem.FS, LSystem.FS));
File databaseFile = new File(
String.format("%s%sDatabase%sbitcoin.mv.db", dataPath,
LSystem.FS, LSystem.FS));
File lockFile = new File(String.format("%s%sDatabase%sbitcoin.lock.db",
dataPath, LSystem.FS, LSystem.FS));
Connection conn = getConnection();
ResultSet r;
//
// Delete spent transaction outputs before creating the backup script
//
long ageLimit = Math.max(chainTime - MAX_TX_AGE, 0);
int deletedCount = 0;
BTCLoader.info("Deleting spent transaction outputs");
try (PreparedStatement s = conn
.prepareStatement("DELETE FROM TxOutputs WHERE db_id IN "
+ "(SELECT db_id FROM TxSpentOutputs WHERE time_spent<? LIMIT 2000)")) {
int count;
do {
s.setLong(1, ageLimit);
count = s.executeUpdate();
deletedCount += count;
if (deletedCount % 100000 == 0)
BTCLoader.info(String.format(
"Deleted %,d spent transaction outputs",
deletedCount));
} while (count > 0);
} catch (SQLException exc) {
BTCLoader.error("Unable to delete spent transaction outputs", exc);
throw new BlockStoreException(
"Unable to delete spent transaction outputs");
}
BTCLoader.info(String.format("%,d spent transaction outputs deleted",
deletedCount));
//
// Create the SQL backup script
//
try {
BTCLoader.info("Creating the SQL backup script");
if (backupFile.exists())
backupFile.delete();
try (Statement s = conn.createStatement()) {
r = s.executeQuery(String.format(
"SCRIPT TO '%s' COMPRESSION GZIP CHARSET 'UTF-8'",
backupFile.getPath()));
while (r.next())
BTCLoader.debug(r.getString(1));
r.close();
}
BTCLoader.info("SQL backup script created");
} catch (SQLException exc) {
BTCLoader.error("Unable to create the SQL backup script", exc);
throw new BlockStoreException(
"Unable to create the SQL backup script");
}
//
// Close and delete the database
//
close();
threadConnection.set(null);
BTCLoader.info("Database closed");
if (lockFile.exists()) {
BTCLoader.info("Waiting for database lock to be released");
try {
while (lockFile.exists())
Thread.sleep(1000);
} catch (InterruptedException exc) {
BTCLoader.error("Interrupted while waiting on database lock");
throw new BlockStoreException(
"Interrupted while waiting on database lock");
}
BTCLoader.info("Database lock released");
}
databaseFile.delete();
BTCLoader.info("Database deleted");
//
// Create the new database
//
BTCLoader.info("Creating database from SQL backup script");
conn = getConnection();
try (Statement s = conn.createStatement()) {
int count = s.executeUpdate(String.format(
"RUNSCRIPT FROM '%s' COMPRESSION GZIP CHARSET 'UTF-8'",
backupFile.getPath()));
BTCLoader.debug(String.format("%d database updates completed",
count));
} catch (SQLException exc) {
BTCLoader.error("Unable to create database from SQL backup script",
exc);
throw new BlockStoreException(
"Unable to create database from SQL backup script");
}
BTCLoader.info("Database created");
}
/**
* Check if the block is already in the database
*
* @param blockHash
* The block to check
* @return TRUE if this is a new block
* @throws BlockStoreException
* Unable to check the block status
*/
@Override
public boolean isNewBlock(Sha256Hash blockHash) throws BlockStoreException {
boolean isNewBlock;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT 1 FROM Blocks "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
ResultSet r = s.executeQuery();
isNewBlock = !r.next();
} catch (SQLException exc) {
BTCLoader
.error(String.format(
"Unable to check block status\n Block %s",
blockHash), exc);
throw new BlockStoreException("Unable to check block status",
blockHash);
}
return isNewBlock;
}
/**
* Check 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
*/
@Override
public boolean isNewAlert(int alertID) throws BlockStoreException {
boolean isNewAlert;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT 1 FROM Alerts WHERE alert_id=?")) {
s.setInt(1, alertID);
ResultSet r = s.executeQuery();
isNewAlert = !r.next();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to check alert status for %d", alertID), exc);
throw new BlockStoreException("Unable to check alert status");
}
return isNewAlert;
}
/**
* Return a list of all alerts in the database
*
* @return List of all alerts
* @throws BlockStoreException
* Unable to get alerts from database
*/
@Override
public List<Alert> getAlerts() throws BlockStoreException {
List<Alert> alertList = new LinkedList<>();
Connection conn = getConnection();
try (Statement s = conn.createStatement()) {
ResultSet r = s
.executeQuery("SELECT is_cancelled,payload,signature FROM Alerts ORDER BY alert_id ASC");
while (r.next()) {
boolean isCancelled = r.getBoolean(1);
byte[] payload = r.getBytes(2);
byte[] signature = r.getBytes(3);
Alert alert = new Alert(payload, signature);
alert.setCancel(isCancelled);
alertList.add(alert);
}
} catch (IOException | SQLException exc) {
BTCLoader.error("Unable to build alert list", exc);
throw new BlockStoreException("Unable to build alert list");
}
return alertList;
}
/**
* Store an alert in the database
*
* @param alert
* The alert
* @throws BlockStoreException
* Unable to store the alert
*/
@Override
public void storeAlert(Alert alert) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Alerts "
+ "(alert_id,is_cancelled,payload,signature) VALUES(?,false,?,?)")) {
s.setInt(1, alert.getID());
s.setBytes(2, alert.getPayload());
s.setBytes(3, alert.getSignature());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(
String.format("Unable to store alert %d", alert.getID()),
exc);
throw new BlockStoreException("Unable to store alert");
}
}
/**
* Cancel an alert
*
* @param alertID
* The alert identifier
* @throws BlockStoreException
* Unable to update the alert
*/
@Override
public void cancelAlert(int alertID) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Alerts SET is_cancelled=true WHERE alert_id=?")) {
s.setInt(1, alertID);
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(
String.format("Unable to cancel alert %d", alertID), exc);
throw new BlockStoreException("Unable to cancel alert");
}
}
/**
* Check if the block is on the block chain
*
* @param blockHash
* The block to check
* @return TRUE if the block is on the block chain
* @throws BlockStoreException
* Unable to get the block status
*/
@Override
public boolean isOnChain(Sha256Hash blockHash) throws BlockStoreException {
boolean onChain;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_height from Blocks "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
ResultSet r = s.executeQuery();
onChain = (r.next() && r.getInt(1) >= 0);
} catch (SQLException exc) {
BTCLoader
.error(String.format(
"Unable to check block status\n Block %s",
blockHash), exc);
throw new BlockStoreException("Unable to check block status",
blockHash);
}
return onChain;
}
/**
* Return a block 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
*/
@Override
public Block getBlock(Sha256Hash blockHash) throws BlockStoreException {
Block block = null;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT file_number,file_offset FROM Blocks "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
ResultSet r = s.executeQuery();
if (r.next()) {
int fileNumber = r.getInt(1);
int fileOffset = r.getInt(2);
block = getBlock(fileNumber, fileOffset);
}
} catch (SQLException exc) {
BTCLoader
.error(String.format("Unable to get block\n Block %s",
blockHash), exc);
throw new BlockStoreException("Unable to get block", blockHash);
}
return block;
}
/**
* 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
*/
@Override
public Sha256Hash getBlockId(int height) throws BlockStoreException {
Sha256Hash blockHash = null;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_hash FROM Blocks WHERE block_height=?")) {
s.setInt(1, height);
ResultSet r = s.executeQuery();
if (r.next())
blockHash = new Sha256Hash(r.getBytes(1));
} catch (SQLException exc) {
BTCLoader
.error(String
.format("Unable to get block hash from database: Height %d",
height), exc);
throw new BlockStoreException(
"Unable to get block hash from database");
}
return blockHash;
}
/**
* Return a block 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
*/
@Override
public StoredBlock getStoredBlock(Sha256Hash blockHash)
throws BlockStoreException {
StoredBlock storedBlock = null;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_height,chain_work,on_hold,"
+ "file_number,file_offset FROM Blocks "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
ResultSet r = s.executeQuery();
if (r.next()) {
int blockHeight = r.getInt(1);
BigInteger blockWork = new BigInteger(r.getBytes(2));
boolean onHold = r.getBoolean(3);
int fileNumber = r.getInt(4);
int fileOffset = r.getInt(5);
Block block = getBlock(fileNumber, fileOffset);
if (block != null)
storedBlock = new StoredBlock(block, blockWork,
blockHeight, (blockHeight >= 0), onHold);
}
} catch (SQLException exc) {
BTCLoader
.error(String.format("Unable to get block\n Block %s",
blockHash), exc);
throw new BlockStoreException("Unable to get block", blockHash);
}
return storedBlock;
}
/**
* Return the child block for the specified block. If the block has multiple
* children, the child block that is on the chain will be returned.
*
* @param blockHash
* The block hash
* @return The stored block or null if the block is not found
* @throws BlockStoreException
* Unable to get block
*/
@Override
public StoredBlock getChildStoredBlock(Sha256Hash blockHash)
throws BlockStoreException {
StoredBlock childStoredBlock = null;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_height,chain_work,on_hold,file_number,file_offset "
+ "FROM Blocks WHERE prev_hash_index=? AND prev_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
ResultSet r = s.executeQuery();
while (r.next()) {
int blockHeight = r.getInt(1);
if (blockHeight < 0 && childStoredBlock != null)
continue;
BigInteger blockWork = new BigInteger(r.getBytes(2));
boolean onHold = r.getBoolean(3);
int fileNumber = r.getInt(4);
int fileOffset = r.getInt(5);
Block block = getBlock(fileNumber, fileOffset);
if (block != null)
childStoredBlock = new StoredBlock(block, blockWork,
blockHeight, (blockHeight >= 0), onHold);
if (blockHeight >= 0)
break;
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get child block\n Block %s", blockHash), exc);
throw new BlockStoreException("Unable to get child block",
blockHash);
}
return childStoredBlock;
}
/**
* Return 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
*/
@Override
public List<BlockStatus> getBlockStatus(int maxCount)
throws BlockStoreException {
List<BlockStatus> blockList = new LinkedList<>();
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_hash,timestamp,block_height,on_hold,header FROM Blocks "
+ "WHERE block_height>=? ORDER BY timestamp DESC")) {
s.setInt(1, Math.max(chainHeight - maxCount - 1, 0));
ResultSet r = s.executeQuery();
while (r.next()) {
Sha256Hash blockHash = new Sha256Hash(r.getBytes(1));
long timeStamp = r.getLong(2);
int blockHeight = r.getInt(3);
boolean onHold = r.getBoolean(4);
byte[] header = r.getBytes(5);
int version = ((int) header[0] & 255)
| (((int) header[1] & 255) << 8)
| (((int) header[2] & 255) << 16)
| (((int) header[3] & 255) << 24);
BlockStatus status = new BlockStatus(blockHash, timeStamp,
blockHeight, version, (blockHeight >= 0), onHold);
blockList.add(status);
}
} catch (SQLException exc) {
BTCLoader.error("Unable to get block status", exc);
throw new BlockStoreException("Unable to get block status");
}
return blockList;
}
/**
* 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
*/
@Override
public boolean isNewTransaction(Sha256Hash txHash)
throws BlockStoreException {
boolean isNew;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT 1 FROM TxOutputs "
+ "WHERE tx_hash_index=? AND tx_hash=? LIMIT 1")) {
s.setLong(1, getHashIndex(txHash));
s.setBytes(2, txHash.getBytes());
ResultSet r = s.executeQuery();
isNew = !r.next();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get transaction status\n Tx %s",
txHash.toString()), exc);
throw new BlockStoreException("Unable to get transaction status");
}
return isNew;
}
/**
* Return 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
*/
@Override
public int getTxDepth(Sha256Hash txHash) throws BlockStoreException {
int txDepth = 0;
Connection conn = getConnection();
try (PreparedStatement s1 = conn
.prepareStatement("SELECT block_height from Blocks "
+ "WHERE block_hash_index=? AND block_hash=?");
PreparedStatement s2 = conn
.prepareStatement("SELECT block_hash FROM TxOutputs "
+ "WHERE tx_hash_index=? AND tx_hash=?")) {
s2.setLong(1, getHashIndex(txHash));
s2.setBytes(2, txHash.getBytes());
ResultSet r = s2.executeQuery();
if (r.next()) {
Sha256Hash blockHash = new Sha256Hash(r.getBytes(1));
r.close();
s1.setLong(1, getHashIndex(blockHash));
s1.setBytes(2, blockHash.getBytes());
r = s1.executeQuery();
if (r.next()) {
int height = r.getInt(1);
if (height >= 0)
txDepth = chainHeight - height + 1;
}
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get transaction depth\n Tx %s", txHash), exc);
throw new BlockStoreException("Unable to get transaction depth");
}
return txDepth;
}
/**
* Return 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
*/
@Override
public StoredOutput getTxOutput(OutPoint outPoint)
throws BlockStoreException {
StoredOutput output = null;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT time_spent,value,script_bytes,block_height,is_coinbase "
+ "FROM TxOutputs WHERE tx_hash_index=? AND tx_hash=? AND tx_index=?")) {
s.setLong(1, getHashIndex(outPoint.getHash()));
s.setBytes(2, outPoint.getHash().getBytes());
s.setShort(3, (short) outPoint.getIndex());
ResultSet r = s.executeQuery();
if (r.next()) {
long timeSpent = r.getLong(1);
BigInteger value = BigInteger.valueOf(r.getLong(2));
byte[] scriptBytes = r.getBytes(3);
int blockHeight = r.getInt(4);
boolean isCoinbase = r.getBoolean(5);
output = new StoredOutput(outPoint.getIndex(), value,
scriptBytes, isCoinbase, (timeSpent != 0), blockHeight);
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get transaction output\n Tx %s : Index %d",
outPoint.getHash(), outPoint.getIndex()), exc);
throw new BlockStoreException("Unable to get transaction output");
}
return output;
}
/**
* Returns the outputs for the specified transaction
*
* @param txHash
* Transaction hash
* @return Stored output list
* @throws BlockStoreException
* Unable to get transaction outputs
*/
@Override
public List<StoredOutput> getTxOutputs(Sha256Hash txHash)
throws BlockStoreException {
List<StoredOutput> outputList = new LinkedList<>();
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT tx_index,time_spent,value,script_bytes,block_height,is_coinbase "
+ "FROM TxOutputs WHERE tx_hash_index=? AND tx_hash=? ORDER BY tx_index ASC")) {
s.setLong(1, getHashIndex(txHash));
s.setBytes(2, txHash.getBytes());
ResultSet r = s.executeQuery();
while (r.next()) {
int txIndex = r.getShort(1);
long timeSpent = r.getLong(2);
BigInteger value = BigInteger.valueOf(r.getLong(3));
byte[] scriptBytes = r.getBytes(4);
int blockHeight = r.getInt(5);
boolean isCoinbase = r.getBoolean(6);
StoredOutput output = new StoredOutput(txIndex, value,
scriptBytes, isCoinbase, (timeSpent != 0), blockHeight);
outputList.add(output);
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get transaction outputs\n Tx %s", txHash), exc);
throw new BlockStoreException("Unable to get transaction outputs",
txHash);
}
return outputList;
}
/**
* 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
*/
@Override
public int deleteSpentTxOutputs() throws BlockStoreException {
Connection conn = getConnection();
long ageLimit = Math.max(chainTime - MAX_TX_AGE, 0);
int deletedCount = 0;
//
// Delete spent outputs in increments of 1000 to reduce the time that
// other
// transactions are locked out of the database
//
synchronized (lock) {
BTCLoader.info("Deleting spent transaction outputs");
try (PreparedStatement s = conn
.prepareStatement("DELETE FROM TxOutputs WHERE db_id IN "
+ "(SELECT db_id FROM TxSpentOutputs WHERE time_spent<? LIMIT 1000)")) {
s.setLong(1, ageLimit);
deletedCount = s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to delete spent transaction outputs", exc));
throw new BlockStoreException(
"Unable to delete spent transaction outputs");
}
BTCLoader.info(String.format(
"Deleted %d spent transaction outputs", deletedCount));
}
return deletedCount;
}
/**
* 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
*/
@Override
public List<InventoryItem> getChainList(Sha256Hash startBlock,
Sha256Hash stopBlock) throws BlockStoreException {
//
// Get the block height for the start block
//
int blockHeight = 0;
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_height FROM Blocks "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(startBlock));
s.setBytes(2, startBlock.getBytes());
ResultSet r = s.executeQuery();
if (r.next())
blockHeight = Math.max(r.getInt(1), 0);
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get start block\n Block %s", startBlock), exc);
throw new BlockStoreException("Unable to get start block",
startBlock);
}
//
// If we found the start block, we will start at the block following it.
// Otherwise,
// we will start with the block following the genesis block.
//
return getChainList(blockHeight, stopBlock);
}
/**
* 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
*/
@Override
public List<InventoryItem> getChainList(int startHeight,
Sha256Hash stopBlock) throws BlockStoreException {
List<InventoryItem> chainList = new LinkedList<>();
//
// Get the chain list starting at the block following the start block
// and continuing
// for a maximum of 500 blocks.
//
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("SELECT block_hash FROM Blocks "
+ "WHERE block_height>? AND block_height<=? ORDER BY block_height ASC")) {
s.setInt(1, startHeight);
s.setInt(2, startHeight + 500);
ResultSet r = s.executeQuery();
while (r.next()) {
Sha256Hash blockHash = new Sha256Hash(r.getBytes(1));
chainList.add(new InventoryItem(InventoryItem.INV_BLOCK,
blockHash));
if (blockHash.equals(stopBlock))
break;
}
} catch (SQLException exc) {
BTCLoader.error("Unable to get the chain list", exc);
throw new BlockStoreException("Unable to get the chain list");
}
return chainList;
}
/**
* 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
*/
@Override
public List<BlockHeader> getHeaderList(Sha256Hash startBlock,
Sha256Hash stopBlock) throws BlockStoreException {
List<BlockHeader> headerList = new LinkedList<>();
//
// Get the start block
//
int blockHeight = 0;
try {
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT block_height FROM Blocks "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(startBlock));
s.setBytes(2, startBlock.getBytes());
r = s.executeQuery();
if (r.next())
blockHeight = Math.max(r.getInt(1), 0);
}
//
// If we found the start block, we will start at the block following
// it. Otherwise,
// we will start at the block following the genesis block.
//
try (PreparedStatement s = conn
.prepareStatement("SELECT header,block_height FROM Blocks "
+ "WHERE block_height>? AND block_height<=? ORDER BY block_height ASC")) {
s.setInt(1, blockHeight);
s.setInt(2, blockHeight + 2000);
r = s.executeQuery();
while (r.next())
headerList.add(new BlockHeader(r.getBytes(1), false));
}
} catch (EOFException | SQLException | VerificationException exc) {
BTCLoader.error("Unable to get header list", exc);
throw new BlockStoreException("Unable to get header list");
}
return headerList;
}
/**
* Releases a held block for processing
*
* @param blockHash
* Block hash
* @throws BlockStoreException
* Unable to release the block
*/
@Override
public void releaseBlock(Sha256Hash blockHash) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Blocks SET on_hold=false "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader
.error(String.format(
"Unable to release held block\n Block %s",
blockHash), exc);
throw new BlockStoreException("Unable to release held block");
}
}
/**
* Stores a block in the database
*
* @param storedBlock
* Block to be stored
* @throws BlockStoreException
* Unable to store the block
*/
@Override
public void storeBlock(StoredBlock storedBlock) throws BlockStoreException {
Block block = storedBlock.getBlock();
synchronized (lock) {
//
// Add the block to the current block file
//
int[] fileLocation = storeBlock(block);
Connection conn = getConnection();
try (PreparedStatement s1 = conn
.prepareStatement("INSERT INTO Blocks "
+ "(block_hash_index,block_hash,prev_hash_index,prev_hash,"
+ "block_height,timestamp,chain_work,on_hold,file_number,file_offset,header) "
+ "VALUES(?,?,?,?,?,?,?,?,?,?,?)")) {
//
// Store the block in the Blocks table
//
s1.setLong(1, getHashIndex(block.getHash()));
s1.setBytes(2, block.getHash().getBytes());
s1.setLong(3, getHashIndex(block.getPrevBlockHash()));
s1.setBytes(4, block.getPrevBlockHash().getBytes());
s1.setInt(5, storedBlock.isOnChain() ? storedBlock.getHeight()
: -1);
s1.setLong(6, block.getTimeStamp());
s1.setBytes(7, storedBlock.getChainWork().toByteArray());
s1.setBoolean(8, storedBlock.isOnHold());
s1.setInt(9, fileLocation[0]);
s1.setInt(10, fileLocation[1]);
s1.setBytes(11, block.getHeaderBytes());
s1.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to store block in database\n Block %s",
storedBlock.getHash()), exc);
rollback();
truncateBlockFile(fileLocation);
throw new BlockStoreException(
"Unable to store block in database");
}
}
}
/**
* 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
*/
@Override
public List<StoredBlock> getJunction(Sha256Hash chainHash)
throws BlockNotFoundException, BlockStoreException,
ChainTooLongException {
List<StoredBlock> chainList = new LinkedList<>();
boolean onChain = false;
Sha256Hash blockHash = chainHash;
Block block;
StoredBlock chainStoredBlock;
synchronized (lock) {
//
// If this block immediately follows the current chain head, we
// don't need
// to search the database. Just create a StoredBlock and add it to
// the beginning
// of the chain list.
//
if (chainHead.equals(blockHash)) {
chainStoredBlock = new StoredBlock(chainHead, prevChainHead,
chainWork, chainHeight);
chainList.add(0, chainStoredBlock);
} else {
//
// Starting with the supplied block, follow the previous hash
// values until
// we reach a block which is on the block chain. This block is
// the junction
// block. We will throw a ChainTooLongException if the chain
// exceeds 144 blocks
// (1 days worth). The caller should call this method again
// starting with the
// last block found to build a sub-segment of the chain.
//
PreparedStatement s1;
try {
Sha256Hash prevHash;
boolean onHold;
int fileNumber;
int fileOffset;
int blockHeight;
BigInteger blockWork;
Connection conn = getConnection();
ResultSet r;
s1 = conn
.prepareStatement("SELECT prev_hash,on_hold,chain_work,block_height,file_number,file_offset "
+ "FROM Blocks WHERE block_hash_index=? AND block_hash=?");
while (!onChain) {
s1.setLong(1, getHashIndex(blockHash));
s1.setBytes(2, blockHash.getBytes());
r = s1.executeQuery();
if (r.next()) {
prevHash = new Sha256Hash(r.getBytes(1));
onHold = r.getBoolean(2);
blockWork = new BigInteger(r.getBytes(3));
blockHeight = r.getInt(4);
fileNumber = r.getInt(5);
fileOffset = r.getInt(6);
onChain = (blockHeight >= 0);
r.close();
if (!onChain) {
if (chainList.size() >= 144)
throw new ChainTooLongException(
"Chain length too long", blockHash);
block = getBlock(fileNumber, fileOffset);
if (block == null) {
BTCLoader
.error(String
.format("Chain block file %d is not available\n Block %s",
fileNumber,
blockHash));
throw new BlockNotFoundException(
"Unable to resolve block chain",
blockHash);
}
chainStoredBlock = new StoredBlock(block,
BigInteger.ZERO, -1, false, onHold);
blockHash = block.getPrevBlockHash();
} else {
chainStoredBlock = new StoredBlock(blockHash,
prevHash, blockWork, blockHeight);
}
chainList.add(0, chainStoredBlock);
} else {
BTCLoader.debug(String.format(
"Chain block is not available\n Block %s",
blockHash));
throw new BlockNotFoundException(
"Unable to resolve block chain", blockHash);
}
}
} catch (SQLException exc) {
BTCLoader.error("Unable to locate junction block", exc);
throw new BlockStoreException(
"Unable to locate junction block", blockHash);
}
}
}
return chainList;
}
/**
* 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
*/
@Override
public void setChainHead(List<StoredBlock> chainList)
throws BlockStoreException, VerificationException {
//
// See if we have reached a checkpoint. If we have, the new block at
// that height
// must match the checkpoint block.
//
for (StoredBlock storedBlock : chainList) {
if (storedBlock.getBlock() == null)
continue;
Sha256Hash checkHash = checkpoints.get(Integer.valueOf(storedBlock
.getHeight()));
if (checkHash != null) {
if (checkHash.equals(storedBlock.getHash())) {
BTCLoader.info(String.format(
"New chain head at height %d matches checkpoint",
storedBlock.getHeight()));
} else {
BTCLoader
.error(String
.format("New chain head at height %d does not match checkpoint",
storedBlock.getHeight()));
throw new VerificationException(
"Checkpoint verification failed",
RejectMessage.REJECT_CHECKPOINT,
storedBlock.getHash());
}
}
}
//
// Make the new block the chain head
//
StoredBlock storedBlock = chainList.get(chainList.size() - 1);
synchronized (lock) {
Sha256Hash blockHash = null;
Block block;
Sha256Hash txHash;
PreparedStatement s1 = null;
PreparedStatement s2 = null;
PreparedStatement s3 = null;
PreparedStatement s4 = null;
PreparedStatement s5 = null;
PreparedStatement s6 = null;
try {
Connection conn = getConnection();
conn.setAutoCommit(false);
ResultSet r;
//
// The ideal case is where the new block links to the current
// chain head.
// If this is not the case, we need to remove all blocks from
// the block
// chain following the junction block.
//
if (!chainHead.equals(storedBlock.getPrevBlockHash())) {
s1 = conn
.prepareStatement("SELECT file_number,file_offset FROM Blocks "
+ "WHERE block_hash_index=? AND block_hash=?");
s2 = conn
.prepareStatement("DELETE FROM TxOutputs WHERE tx_hash_index=? AND tx_hash=?");
s3 = conn
.prepareStatement("UPDATE TxOutputs SET time_spent=0 "
+ "WHERE tx_hash_index=? AND tx_hash=? AND tx_index=?");
s4 = conn
.prepareStatement("UPDATE Blocks SET block_height=-1 "
+ "WHERE block_hash_index=? AND block_hash=?");
Sha256Hash junctionHash = chainList.get(0).getHash();
blockHash = chainHead;
//
// Process each block starting at the current chain head and
// working backwards
// until we reach the junction block
//
while (!blockHash.equals(junctionHash)) {
//
// Get the block from the Blocks database
//
s1.setLong(1, getHashIndex(blockHash));
s1.setBytes(2, blockHash.getBytes());
r = s1.executeQuery();
if (!r.next()) {
BTCLoader
.error(String
.format("Chain block not found in Blocks database\n Block %s",
blockHash));
throw new BlockStoreException(
"Chain block not found in Blocks database");
}
int fileNumber = r.getInt(1);
int fileOffset = r.getInt(2);
block = getBlock(fileNumber, fileOffset);
if (block == null) {
BTCLoader
.error(String
.format("Chain block file %d is not available\n Block %s",
fileNumber, blockHash));
throw new BlockStoreException(
"Chain block is not available");
}
//
// Process each transaction in the block
//
List<Transaction> txList = block.getTransactions();
for (Transaction tx : txList) {
txHash = tx.getHash();
//
// Delete the transaction from the TxOutputs table
//
s2.setLong(1, getHashIndex(txHash));
s2.setBytes(2, txHash.getBytes());
s2.executeUpdate();
//
// Update spent outputs to indicate they have not
// been spent. We
// need to ignore inputs for coinbase transactions
// since they are
// not used for spending coins. It is also possible
// that a transaction
// in the block spends an output from another
// transaction in the block,
// in which case the output will not be found since
// we have already
// deleted all of the block transactions.
//
if (tx.isCoinBase()) {
continue;
}
List<TransactionInput> txInputs = tx.getInputs();
for (TransactionInput txInput : txInputs) {
OutPoint op = txInput.getOutPoint();
Sha256Hash outHash = op.getHash();
int outIndex = op.getIndex();
s3.setLong(1, getHashIndex(outHash));
s3.setBytes(2, outHash.getBytes());
s3.setShort(3, (short) outIndex);
s3.executeUpdate();
}
}
//
// Update the block status in the Blocks table
//
s4.setLong(1, getHashIndex(blockHash));
s4.setBytes(2, blockHash.getBytes());
s4.executeUpdate();
BTCLoader.networkChainHeight--;
BTCLoader.info(String.format(
"Block removed from block chain\n Block %s",
blockHash));
//
// Advance to the block before this block
//
blockHash = block.getPrevBlockHash();
}
}
//
// Now add the new blocks to the block chain starting with the
// block following the junction block
//
s1 = conn.prepareStatement("SELECT tx_index FROM TxOutputs "
+ "WHERE tx_hash_index=? AND tx_hash=? LIMIT 1");
s2 = conn
.prepareStatement("INSERT INTO TxOutputs (tx_hash_index,tx_hash,tx_index,block_hash,"
+ "block_height,time_spent,value,script_bytes,is_coinbase) VALUES(?,?,?,?,0,0,?,?,?)");
s3 = conn
.prepareStatement("UPDATE TxOutputs SET time_spent=?,block_height=? WHERE db_id=?");
s4 = conn
.prepareStatement("UPDATE Blocks SET block_height=?,chain_work=? "
+ "WHERE block_hash_index=? AND block_hash=?");
s5 = conn
.prepareStatement("INSERT INTO TxSpentOutputs (time_spent,db_id) VALUES(?,?)");
s6 = conn.prepareStatement("SELECT db_id FROM TxOutputs "
+ "WHERE tx_hash_index=? AND tx_hash=? AND tx_index=?");
for (int i = 1; i < chainList.size(); i++) {
storedBlock = chainList.get(i);
block = storedBlock.getBlock();
blockHash = block.getHash();
int blockHeight = storedBlock.getHeight();
BigInteger blockWork = storedBlock.getChainWork();
List<Transaction> txList = block.getTransactions();
//
// Add the block transactions to the TxOutputs table and
// update the
// spent status for transaction outputs referenced by the
// transactions
// in this block.
//
// Unfortunately, before BIP 30 was implemented, there were
// several
// cases where a block contained the same coinbase
// transaction. So
// we need to check the TxOutputs table first to make sure
// the transaction
// output is not already in the table for a coinbase
// transaction. We will
// allow a duplicate coinbase transaction if it is in a
// block before 250,000.
//
// Some transactions contain a text message as one of the
// outputs with the
// associated script set to OP_RETURN (which means the
// output can never be spent).
// We will check for this case and set the transaction
// output as spent.
//
for (Transaction tx : txList) {
txHash = tx.getHash();
boolean processOutputs = true;
s1.setLong(1, getHashIndex(txHash));
s1.setBytes(2, txHash.getBytes());
r = s1.executeQuery();
if (r.next()) {
r.close();
if (!tx.isCoinBase()
|| storedBlock.getHeight() >= 250000) {
BTCLoader.error(String.format(
"Height %d: Transaction outputs already in TxOutputs\n"
+ " Block %s\n Tx %s",
storedBlock.getHeight(),
block.getHashAsString(), txHash));
throw new VerificationException(
"Transaction outputs already in TxOutputs",
RejectMessage.REJECT_DUPLICATE, txHash);
}
processOutputs = false;
} else {
r.close();
}
if (processOutputs) {
List<TransactionOutput> txOutputs = tx.getOutputs();
for (TransactionOutput txOutput : txOutputs) {
if (txOutput.isSpendable()) {
s2.setLong(1, getHashIndex(txHash));
s2.setBytes(2, txHash.getBytes());
s2.setShort(3, (short) txOutput.getIndex());
s2.setBytes(4, blockHash.getBytes());
s2.setLong(5, txOutput.getValue()
.longValue());
s2.setBytes(6, txOutput.getScriptBytes());
s2.setBoolean(7, tx.isCoinBase());
s2.executeUpdate();
}
}
}
//
// Connect transaction inputs to transaction outputs and
// mark them spent.
//
// We need to ignore inputs for coinbase transactions
// since they are not
// used for spending coins.
//
if (tx.isCoinBase())
continue;
List<TransactionInput> txInputs = tx.getInputs();
for (TransactionInput txInput : txInputs) {
OutPoint op = txInput.getOutPoint();
Sha256Hash outHash = op.getHash();
int outIndex = op.getIndex();
s6.setLong(1, getHashIndex(outHash));
s6.setBytes(2, outHash.getBytes());
s6.setShort(3, (short) outIndex);
r = s6.executeQuery();
if (!r.next()) {
BTCLoader
.error(String
.format("Transaction output not found\n Tx %s",
tx.getHashAsString()));
throw new BlockStoreException(
"Transaction output not found");
}
long dbId = r.getLong(1);
s3.setLong(1, block.getTimeStamp());
s3.setInt(2, blockHeight);
s3.setLong(3, dbId);
s3.executeUpdate();
s5.setLong(1, block.getTimeStamp());
s5.setLong(2, dbId);
s5.executeUpdate();
}
}
//
// Update the block status in the Blocks database
//
s4.setInt(1, blockHeight);
s4.setBytes(2, blockWork.toByteArray());
s4.setLong(3, getHashIndex(blockHash));
s4.setBytes(4, blockHash.getBytes());
s4.executeUpdate();
BTCLoader
.info(String
.format("Block added to block chain at height %d\n Block %s",
blockHeight,
block.getHashAsString()));
}
//
// Commit the changes
//
conn.commit();
conn.setAutoCommit(true);
//
// Update chain values for the new chain
//
storedBlock = chainList.get(chainList.size() - 1);
chainTime = storedBlock.getBlock().getTimeStamp();
chainHead = storedBlock.getHash();
prevChainHead = storedBlock.getPrevBlockHash();
chainHeight = storedBlock.getHeight();
chainWork = storedBlock.getChainWork();
targetDifficulty = storedBlock.getBlock().getTargetDifficulty();
} catch (SQLException exc) {
BTCLoader.error("Unable to update block chain", exc);
rollback(s1, s2, s3, s4);
throw new BlockStoreException("Unable to update block chain",
blockHash);
}
}
}
/**
* Checks if a table exists
*
* @param table
* Table name
* @return TRUE if the table exists
* @throws BlockStoreException
* Unable to access the database server
*/
private boolean tableExists(String table) throws BlockStoreException {
boolean tableExists;
Connection conn = getConnection();
try (Statement s = conn.createStatement()) {
s.executeQuery("SELECT 1 FROM " + table + " WHERE 1 = 2");
tableExists = true;
} catch (SQLException exc) {
tableExists = false;
}
return tableExists;
}
/**
* Create the database tables
*
* @throws BlockStoreException
* Unable to create database tables
*/
private void createTables() throws BlockStoreException {
Connection conn = getConnection();
try (Statement s = conn.createStatement()) {
//
// Create the tables
//
s.executeUpdate(Settings_Table);
s.executeUpdate(TxOutputs_Table);
s.executeUpdate(TxOutputs_IX1);
s.executeUpdate(TxSpentOutputs_Table);
s.executeUpdate(TxSpentOutputs_IX1);
s.executeUpdate(Blocks_Table);
s.executeUpdate(Blocks_IX1);
s.executeUpdate(Blocks_IX2);
s.executeUpdate(Blocks_IX3);
s.executeUpdate(Alerts_Table);
s.executeUpdate(Received_Table);
s.executeUpdate(Received_IX1);
s.executeUpdate(Sent_Table);
s.executeUpdate(Sent_IX1);
s.executeUpdate(Addresses_Table);
s.executeUpdate(Keys_Table);
BTCLoader.info("SQL database tables created");
//
// We are creating a new database, so delete any existing block
// files
//
File dirFile = new File(String.format("%s%sBlocks", dataPath,
LSystem.FS));
if (dirFile.exists()) {
File[] fileList = dirFile.listFiles();
for (File file : fileList){
file.delete();
}
}
} catch (SQLException exc) {
BTCLoader.error("Unable to create SQL database tables", exc);
throw new BlockStoreException(
"Unable to create SQL database tables");
}
}
private boolean existsBlock = false;
/**
* Initialize the tables
*
* @throws BlockStoreException
* Unable to initialize the database tables
*/
private void initTables() throws BlockStoreException {
Connection conn = getConnection();
try {
conn.setAutoCommit(false);
//
// Initialize the block chain with the genesis block
//
Block genesisBlock = new Block(BTCLoader.GENESIS_BLOCK_BYTES, 0,
BTCLoader.GENESIS_BLOCK_BYTES.length, false);
chainHead = genesisBlock.getHash();
prevChainHead = Sha256Hash.ZERO_HASH;
chainHeight = 0;
chainWork = BigInteger.ONE;
targetDifficulty = NetParams.MAX_TARGET_DIFFICULTY;
blockFileNumber = 0;
chainTime = genesisBlock.getTimeStamp();
//
// Initialize the Settings table
//
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Settings (schema_name,schema_version) VALUES(?,?)")) {
s.setString(1, schemaName);
s.setInt(2, schemaVersion);
s.executeUpdate();
}
//
// Add the genesis block to the Blocks table
//
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Blocks(block_hash_index,block_hash,prev_hash_index,prev_hash,"
+ "block_height,timestamp,chain_work,on_hold,"
+ "file_number,file_offset,header) VALUES(?,?,?,?,0,?,?,false,0,0,?)")) {
s.setLong(1, getHashIndex(chainHead));
s.setBytes(2, chainHead.getBytes());
s.setLong(3, getHashIndex(prevChainHead));
s.setBytes(4, prevChainHead.getBytes());
s.setLong(5, chainTime);
s.setBytes(6, chainWork.toByteArray());
s.setBytes(7, genesisBlock.getHeaderBytes());
s.executeUpdate();
}
//
// Copy the genesis block as the initial block file
//
File blockFile = new File(String.format("%s%sBlocks%sblk00000.dat",
dataPath, LSystem.FS, LSystem.FS));
if (!existsBlock) {
existsBlock = blockFile.exists();
if (!existsBlock) {
FileUtils.makedirs(blockFile);
}
}
try (FileOutputStream outFile = new FileOutputStream(blockFile)) {
byte[] prefixBytes = new byte[8];
Helper.uint32ToByteArrayLE(NetParams.MAGIC_NUMBER, prefixBytes,
0);
Helper.uint32ToByteArrayLE(
BTCLoader.GENESIS_BLOCK_BYTES.length, prefixBytes, 4);
outFile.write(prefixBytes);
outFile.write(BTCLoader.GENESIS_BLOCK_BYTES);
}
//
// All done - commit the updates
//
conn.commit();
conn.setAutoCommit(true);
BTCLoader.info(String.format(
"Database initialized with schema version %d.%d",
schemaVersion / 100, schemaVersion % 100));
} catch (IOException | SQLException | VerificationException exc) {
BTCLoader.error("Unable to initialize the database tables", exc);
rollback();
throw new BlockStoreException(
"Unable to initialize the database tables");
}
}
/**
* Get the initial database settings
*
* @throws BlockStoreException
* Unable to get the initial values
*/
private void getSettings() throws BlockStoreException {
Connection conn = getConnection();
ResultSet r;
int version = 0;
try {
//
// Get the initial values from the Settings table
//
try (PreparedStatement s = conn
.prepareStatement("SELECT schema_version FROM Settings "
+ "WHERE schema_name=?")) {
s.setString(1, schemaName);
r = s.executeQuery();
if (!r.next())
throw new BlockStoreException("Incorrect database schema");
version = r.getInt(1);
if (version > schemaVersion)
throw new BlockStoreException(String.format(
"Schema version %d.%d is not supported",
version / 100, version % 100));
switch (version) {
case 100:
dbUpgrade100(conn); // Upgrade from Version 1.00 to 1.01
case 101:
dbUpgrade101(conn); // Upgrade from Version 1.01 to 1.02
break;
}
version = schemaVersion;
}
//
// Get the current chain values from the chain head block
//
try (Statement s = conn.createStatement()) {
r = s.executeQuery("SELECT block_hash,prev_hash,block_height,chain_work,timestamp,"
+ "file_number,file_offset "
+ "FROM Blocks WHERE block_height=(SELECT MAX(block_height) FROM Blocks)");
if (!r.next())
throw new BlockStoreException(
"Unable to get chain head block");
chainHead = new Sha256Hash(r.getBytes(1));
prevChainHead = new Sha256Hash(r.getBytes(2));
chainHeight = r.getInt(3);
chainWork = new BigInteger(r.getBytes(4));
chainTime = r.getLong(5);
int fileNumber = r.getInt(6);
int fileOffset = r.getInt(7);
Block block = getBlock(fileNumber, fileOffset);
if (block == null) {
BTCLoader
.error(String
.format("Unable to get block from block file %d, offset %d\n %s",
fileNumber, fileOffset, chainHead));
throw new BlockStoreException(
"Unable to get block from block file", chainHead);
}
targetDifficulty = block.getTargetDifficulty();
}
//
// Get the cuurrent block file number
//
File blockDir = new File(String.format("%s%sBlocks", dataPath,
LSystem.FS));
String[] fileList = blockDir.list();
for (String fileName : fileList) {
int sep = fileName.lastIndexOf('.');
if (sep >= 0 && fileName.substring(0, 3).equals("blk")
&& fileName.substring(sep).equals(".dat"))
blockFileNumber = Math.max(blockFileNumber,
Integer.parseInt(fileName.substring(3, sep)));
}
BigInteger networkDifficulty = NetParams.PROOF_OF_WORK_LIMIT
.divide(Helper.decodeCompactBits(targetDifficulty));
BTCLoader
.info(String
.format("Database opened with schema version %d.%d\n"
+ " Chain height %,d, Target difficulty %s, Block File number %d\n"
+ " Chain head %s",
version / 100,
version % 100,
chainHeight,
Helper.numberToShortString(networkDifficulty),
blockFileNumber, chainHead));
} catch (SQLException exc) {
BTCLoader.error("Unable to get initial table settings", exc);
throw new BlockStoreException(
"Unable to get initial table settings");
}
}
/**
* Upgrade the database from Version 1.00 to 1.01
*
* Due to the size of the Blocks table, we will commit each update. If an
* error occurs, the upgrade can be repeated without restoring the table to
* its original state.
*
* @param conn
* Database connection
* @throws BlockStoreException
* Unable to upgrade the database
*/
private void dbUpgrade100(Connection conn) throws BlockStoreException {
try {
BTCLoader
.info("Upgrading the SQL database from Version 1.00 to Version 1.01");
ResultSet r;
//
// Add the 'header' column to the Blocks table
//
Statement s1 = conn.createStatement();
s1.executeUpdate("ALTER TABLE Blocks ADD COLUMN IF NOT EXIST header BINARY");
//
// Build the block list
//
List<Sha256Hash> blockList = new LinkedList<>();
r = s1.executeQuery("SELECT block_hash FROM Blocks WHERE header IS NULL");
while (r.next())
blockList.add(new Sha256Hash(r.getBytes(1)));
r.close();
//
// Add the block headers to the database
//
PreparedStatement s2 = conn
.prepareStatement("SELECT file_number,file_offset FROM Blocks WHERE block_hash=?");
PreparedStatement s3 = conn
.prepareStatement("UPDATE Blocks SET header=? WHERE block_hash=?");
for (Sha256Hash blockHash : blockList) {
s2.setBytes(1, blockHash.getBytes());
r = s2.executeQuery();
if (!r.next()) {
BTCLoader
.warn(String
.format("Block file pointer not found in database\n Block %s",
blockHash));
throw new BlockStoreException(
"Block file pointer not found in database");
}
int fileNumber = r.getInt(1);
int fileOffset = r.getInt(2);
r.close();
Block block = getBlock(fileNumber, fileOffset);
if (block == null) {
BTCLoader.error(String.format(
"Block in file %d at position %d is unavailable",
fileNumber, fileOffset));
throw new BlockStoreException(
"Unable to upgrade database due to unavailable block");
}
s3.setBytes(1, block.getHeaderBytes());
s3.setBytes(2, blockHash.getBytes());
s3.executeUpdate();
}
//
// Set the header column to NOT NULL and update the schema version
//
s1.executeUpdate("ALTER TABLE Blocks ALTER COLUMN header BINARY NOT NULL");
s1.executeUpdate("UPDATE Settings SET schema_version=101");
//
// Upgrade completed
//
BTCLoader.info("Database upgrade to Version 1.01 completed");
} catch (SQLException exc) {
BTCLoader.error("Unable to upgrade database version", exc);
throw new BlockStoreException("Unable to upgrade database version");
}
}
/**
* Upgrade the database from Version 1.01 to 1.02
*
* Due to the size of the Blocks and TxOutputs tables, we will commit each
* update. If an error occurs, the upgrade can be repeated without restoring
* the tables to their original state.
*
* @param conn
* Database connection
* @throws BlockStoreException
* Unable to upgrade the database
*/
private void dbUpgrade101(Connection conn) throws BlockStoreException {
ResultSet r;
try {
BTCLoader
.info("Upgrading the SQL database from Version 1.01 to Version 1.02");
Statement s1 = conn.createStatement();
PreparedStatement s2;
PreparedStatement s3;
//
// Delete the existing hash indexes
//
BTCLoader.info("Deleting existing hash indexes");
s1.executeUpdate("DROP INDEX IF EXISTS Blocks_IX1");
s1.executeUpdate("DROP INDEX IF EXISTS Blocks_IX2");
s1.executeUpdate("DROP INDEX IF EXISTS TxOutputs_IX1");
//
// Add the new index columns
//
BTCLoader.info("Adding new hash index columns");
s1.executeUpdate("ALTER TABLE Blocks ADD COLUMN IF NOT EXISTS block_hash_index BIGINT");
s1.executeUpdate("ALTER TABLE Blocks ADD COLUMN IF NOT EXISTS prev_hash_index BIGINT");
s1.executeUpdate("ALTER TABLE TxOutputs ADD COLUMN IF NOT EXISTS tx_hash_index BIGINT");
//
// Fill in the index columns for the Blocks table
//
BTCLoader.info("Upgrading Blocks table");
List<Long> idList = new LinkedList<>();
s2 = conn
.prepareStatement("SELECT block_hash,prev_hash FROM Blocks WHERE db_id=?");
s3 = conn
.prepareStatement("UPDATE Blocks SET block_hash_index=?,prev_hash_index=? WHERE db_id=?");
r = s1.executeQuery("SELECT db_id FROM Blocks WHERE block_hash_index IS NULL");
while (r.next())
idList.add(r.getLong(1));
r.close();
for (Long dbId : idList) {
s2.setLong(1, dbId);
r = s2.executeQuery();
if (r.next()) {
Sha256Hash blockHash = new Sha256Hash(r.getBytes(1));
Sha256Hash prevHash = new Sha256Hash(r.getBytes(2));
r.close();
s3.setLong(1, getHashIndex(blockHash));
s3.setLong(2, getHashIndex(prevHash));
s3.setLong(3, dbId);
s3.executeUpdate();
} else {
r.close();
}
}
s2.close();
s3.close();
idList.clear();
System.gc();
//
// Fill in the index column for the TxOutputs table
//
BTCLoader.info("Upgrading TxOutputs table");
s2 = conn
.prepareStatement("SELECT tx_hash FROM TxOutputs WHERE db_id=?");
s3 = conn
.prepareStatement("UPDATE TxOutputs SET tx_hash_index=? WHERE db_id=?");
r = s1.executeQuery("SELECT db_id FROM TxOutputs WHERE tx_hash_index IS NULL");
while (r.next())
idList.add(r.getLong(1));
r.close();
for (Long dbId : idList) {
s2.setLong(1, dbId);
r = s2.executeQuery();
if (r.next()) {
Sha256Hash txHash = new Sha256Hash(r.getBytes(1));
r.close();
s3.setLong(1, getHashIndex(txHash));
s3.setLong(2, dbId);
s3.executeUpdate();
} else {
r.close();
}
}
s2.close();
s3.close();
idList.clear();
System.gc();
//
// Set the index columns to NOT NULL and update the schema version
//
BTCLoader.info("Updating the table version");
s1.executeUpdate("ALTER TABLE Blocks ALTER COLUMN block_hash_index BIGINT NOT NULL");
s1.executeUpdate("ALTER TABLE Blocks ALTER COLUMN prev_hash_index BIGINT NOT NULL");
s1.executeUpdate("ALTER TABLE TxOutputs ALTER COLUMN tx_hash_index BIGINT NOT NULL");
s1.executeUpdate("UPDATE Settings SET schema_version=102");
//
// Create the new hash indexes
//
BTCLoader.info("Creating the new hash indexes");
s1.executeUpdate("CREATE INDEX IF NOT EXISTS Blocks_IX1 ON Blocks(block_hash_index)");
s1.executeUpdate("CREATE INDEX IF NOT EXISTS Blocks_IX2 ON Blocks(prev_hash_index)");
s1.executeUpdate("CREATE INDEX IF NOT EXISTS TxOutputs_IX1 ON TxOutputs(tx_hash_index)");
//
// Upgrade completed
//
BTCLoader.info("Database upgrade to Version 1.02 completed");
} catch (SQLException exc) {
BTCLoader.error("Unable to upgrade database version", exc);
throw new BlockStoreException("Unable to upgrade database version");
}
}
public void setAddressLabel(Address address) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Addresses SET label=? WHERE address=?")) {
if (address.getLabel().isEmpty()) {
s.setNull(1, Types.VARCHAR);
} else {
s.setString(1, address.getLabel());
}
s.setBytes(2, address.getHash());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error("Unable to update address label", exc);
throw new BlockStoreException("Unable to update address label");
}
}
public void storeAddress(Address address) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Addresses (address,label) VALUES(?,?)")) {
s.setBytes(1, address.getHash());
if (address.getLabel().isEmpty())
s.setNull(2, Types.VARCHAR);
else
s.setString(2, address.getLabel());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error("Unable to store address", exc);
throw new BlockStoreException("Unable to store address");
}
}
public void deleteAddress(Address address) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("DELETE FROM Addresses WHERE address=?")) {
s.setBytes(1, address.getHash());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error("Unable to delete address", exc);
throw new BlockStoreException("Unable to delete address");
}
}
public List<Address> getAddressList() throws BlockStoreException {
List<Address> addressList = new ArrayList<>();
Connection conn = getConnection();
ResultSet r;
try (Statement s = conn.createStatement()) {
r = s.executeQuery("SELECT address,label FROM Addresses ORDER BY label ASC NULLS FIRST");
while (r.next()) {
String label = r.getString(2);
addressList.add(new Address(r.getBytes(1),
label != null ? label : ""));
}
} catch (SQLException exc) {
BTCLoader.error("Unable to get address list", exc);
throw new BlockStoreException("Unable to get address list");
}
return addressList;
}
public void storeKey(ECKey key) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Keys "
+ "(public_key,private_key,timestamp,is_change,label) VALUES(?,?,?,?,?)")) {
EncryptedPrivateKey encPrivKey = new EncryptedPrivateKey(
key.getPrivKey(), LSystem.getAppPassword());
s.setBytes(1, key.getPubKey());
s.setBytes(2, encPrivKey.getBytes());
s.setLong(3, key.getCreationTime());
s.setBoolean(4, key.isChange());
if (key.getLabel().isEmpty()) {
s.setNull(5, Types.VARCHAR);
} else {
s.setString(5, key.getLabel());
}
s.executeUpdate();
} catch (ECException | SQLException exc) {
BTCLoader.error("Unable to store key", exc);
throw new BlockStoreException("Unable to store key");
}
}
public void setKeyLabel(ECKey key) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Keys SET label=? WHERE public_key=?")) {
if (key.getLabel().isEmpty()) {
s.setNull(1, Types.VARCHAR);
} else {
s.setString(1, key.getLabel());
}
s.setBytes(2, key.getPubKey());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error("Unable to update key label", exc);
throw new BlockStoreException("Unable to update key label");
}
}
public List<ECKey> getKeyList() throws KeyException, BlockStoreException {
List<ECKey> keyList = new ArrayList<>();
Connection conn = getConnection();
ResultSet r;
try (Statement s = conn.createStatement()) {
r = s.executeQuery("SELECT public_key,private_key,timestamp,is_change,label FROM Keys "
+ "ORDER BY label ASC NULLS FIRST");
while (r.next()) {
byte[] pubKey = r.getBytes(1);
EncryptedPrivateKey encPrivKey = new EncryptedPrivateKey(
r.getBytes(2));
ECKey key = new ECKey(encPrivKey.getPrivKey(LSystem
.getAppPassword()), (pubKey.length == 33));
if (!Arrays.equals(key.getPubKey(), pubKey)) {
throw new KeyException(
"Private key does not match public key");
}
key.setCreationTime(r.getLong(3));
key.setChange(r.getBoolean(4));
String label = r.getString(5);
key.setLabel(label != null ? label : "");
keyList.add(key);
}
} catch (EOFException | ECException | SQLException exc) {
BTCLoader.error("Unable to get key list", exc);
throw new BlockStoreException("Unable to get key list");
}
return keyList;
}
public void storeReceiveTx(ReceiveTransaction receiveTx)
throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Received "
+ "(tx_hash_index,tx_hash,tx_index,norm_hash,timestamp,block_hash,address,"
+ "value,script_bytes,is_spent,is_change,in_safe,is_coinbase,is_deleted) "
+ "VALUES(?,?,?,?,?,?,?,?,?,false,?,false,?,false)")) {
s.setLong(1, getHashIndex(receiveTx.getTxHash()));
s.setBytes(2, receiveTx.getTxHash().getBytes());
s.setShort(3, (short) receiveTx.getTxIndex());
s.setBytes(4, receiveTx.getNormalizedID().getBytes());
s.setLong(5, receiveTx.getTxTime());
if (receiveTx.getBlockHash() == null)
s.setNull(6, Types.BINARY);
else
s.setBytes(6, receiveTx.getBlockHash().getBytes());
s.setBytes(7, receiveTx.getAddress().getHash());
s.setLong(8, receiveTx.getValue().longValue());
s.setBytes(9, receiveTx.getScriptBytes());
s.setBoolean(10, receiveTx.isChange());
s.setBoolean(11, receiveTx.isCoinBase());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to store receive transaction output\n Tx %s[%d]",
receiveTx.getTxHash(), receiveTx.getTxIndex()), exc);
throw new BlockStoreException(
"Unable to store receive transaction output");
}
}
public void setTxSpent(Sha256Hash txHash, int txIndex, boolean isSpent)
throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Received SET is_spent=? "
+ "WHERE tx_hash_index=? AND tx_hash=? and tx_index=?")) {
s.setBoolean(1, isSpent);
s.setLong(2, getHashIndex(txHash));
s.setBytes(3, txHash.getBytes());
s.setShort(4, (short) txIndex);
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to update receive transaction output\n Tx %s[%d]",
txHash, txIndex), exc);
throw new BlockStoreException(
"Unable to update receive transaction outputs");
}
}
public void setTxSafe(Sha256Hash txHash, int txIndex, boolean inSafe)
throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Received SET in_safe=? "
+ "WHERE tx_hash_index=? AND tx_hash=? and tx_index=?")) {
s.setBoolean(1, inSafe);
s.setLong(2, getHashIndex(txHash));
s.setBytes(3, txHash.getBytes());
s.setShort(4, (short) txIndex);
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to update receive transaction output\n Tx %s[%d]",
txHash, txIndex), exc);
throw new BlockStoreException(
"Unable to update receive transaction outputs");
}
}
public void setReceiveTxDelete(Sha256Hash txHash, int txIndex,
boolean isDeleted) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Received SET is_deleted=? "
+ "WHERE tx_hash_index=? AND tx_hash=? and tx_index=?")) {
s.setBoolean(1, isDeleted);
s.setLong(2, getHashIndex(txHash));
s.setBytes(3, txHash.getBytes());
s.setShort(4, (short) txIndex);
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to update receive transaction output\n Tx %s[%d]",
txHash, txIndex), exc);
throw new BlockStoreException(
"Unable to update receive transaction outputs");
}
}
public List<ReceiveTransaction> getReceiveTxList()
throws BlockStoreException {
List<ReceiveTransaction> txList = new LinkedList<>();
Map<TransactionID, ReceiveTransaction> txMap = new HashMap<TransactionID, ReceiveTransaction>();
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT tx_hash,tx_index,norm_hash,timestamp,"
+ "block_hash,address,value,script_bytes,is_spent,is_change,in_safe,is_coinbase "
+ "FROM Received WHERE is_deleted=false")) {
r = s.executeQuery();
while (r.next()) {
Sha256Hash txHash = new Sha256Hash(r.getBytes(1));
int txIndex = r.getShort(2);
Sha256Hash normID = new Sha256Hash(r.getBytes(3));
long txTime = r.getLong(4);
byte[] bytes = r.getBytes(5);
Sha256Hash blockHash = (bytes != null ? new Sha256Hash(bytes)
: null);
Address address = new Address(r.getBytes(6));
BigInteger value = BigInteger.valueOf(r.getLong(7));
byte[] scriptBytes = r.getBytes(8);
boolean isSpent = r.getBoolean(9);
boolean isChange = r.getBoolean(10);
boolean inSafe = r.getBoolean(11);
boolean isCoinbase = r.getBoolean(12);
TransactionID txID = new TransactionID(txHash, txIndex);
ReceiveTransaction tx = new ReceiveTransaction(normID, txHash,
txIndex, txTime, blockHash, address, value,
scriptBytes, isSpent, isChange, isCoinbase, inSafe);
ReceiveTransaction prevTx = txMap.get(txID);
if (blockHash != null) {
if (prevTx != null) {
txList.remove(prevTx);
}
txList.add(tx);
txMap.put(txID, tx);
} else if (prevTx == null) {
txList.add(tx);
txMap.put(txID, tx);
}
}
} catch (SQLException exc) {
BTCLoader.error("Unable to get receive transaction list", exc);
throw new BlockStoreException(
"Unable to get receive transaction list");
}
return txList;
}
public void storeSendTx(SendTransaction sendTx) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Sent "
+ "(tx_hash_index,tx_hash,norm_hash,timestamp,block_hash,address,value,fee,"
+ "is_deleted,tx_data) VALUES(?,?,?,?,?,?,?,?,false,?)")) {
s.setLong(1, getHashIndex(sendTx.getTxHash()));
s.setBytes(2, sendTx.getTxHash().getBytes());
s.setBytes(3, sendTx.getNormalizedID().getBytes());
s.setLong(4, sendTx.getTxTime());
if (sendTx.getBlockHash() == null)
s.setNull(5, Types.BINARY);
else
s.setBytes(5, sendTx.getBlockHash().getBytes());
s.setBytes(6, sendTx.getAddress().getHash());
s.setLong(7, sendTx.getValue().longValue());
s.setLong(8, sendTx.getFee().longValue());
s.setBytes(9, sendTx.getTxData());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to store send transaction\n Tx %s",
sendTx.getTxHash()), exc);
throw new BlockStoreException("Unable to store send transaction");
}
}
public void setSendTxDelete(Sha256Hash txHash, boolean isDeleted)
throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Sent SET is_deleted=? "
+ "WHERE tx_hash_index=? AND tx_hash=?")) {
s.setBoolean(1, isDeleted);
s.setLong(2, getHashIndex(txHash));
s.setBytes(3, txHash.getBytes());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to update send transaction\n Tx %s", txHash), exc);
throw new BlockStoreException("Unable to update send transaction");
}
}
public SendTransaction getSendTx(Sha256Hash txHash)
throws BlockStoreException {
SendTransaction tx = null;
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT norm_hash,timestamp,block_hash,address,value,fee,"
+ "tx_data FROM Sent WHERE tx_hash_index=? AND tx_hash=? AND is_deleted=false")) {
s.setLong(1, getHashIndex(txHash));
s.setBytes(2, txHash.getBytes());
r = s.executeQuery();
if (r.next()) {
Sha256Hash normID = new Sha256Hash(r.getBytes(1));
long txTime = r.getLong(2);
byte[] bytes = r.getBytes(3);
Sha256Hash blockHash = (bytes != null ? new Sha256Hash(bytes)
: null);
Address address = new Address(r.getBytes(4));
BigInteger value = BigInteger.valueOf(r.getLong(5));
BigInteger fee = BigInteger.valueOf(r.getLong(6));
byte[] txData = r.getBytes(7);
tx = new SendTransaction(normID, txHash, txTime, blockHash,
address, value, fee, txData);
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get send transaction\n Tx %s", txHash), exc);
throw new BlockStoreException("Unable to get send transaction");
}
return tx;
}
public List<SendTransaction> getSendTxList() throws BlockStoreException {
List<SendTransaction> txList = new LinkedList<>();
Map<Sha256Hash, SendTransaction> txMap = new HashMap<Sha256Hash, SendTransaction>();
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT tx_hash,norm_hash,timestamp,"
+ "block_hash,address,value,fee,tx_data FROM Sent WHERE is_deleted=false")) {
r = s.executeQuery();
while (r.next()) {
Sha256Hash txHash = new Sha256Hash(r.getBytes(1));
Sha256Hash normID = new Sha256Hash(r.getBytes(2));
long txTime = r.getLong(3);
byte[] bytes = r.getBytes(4);
Sha256Hash blockHash = (bytes != null ? new Sha256Hash(bytes)
: null);
Address address = new Address(r.getBytes(5));
BigInteger value = BigInteger.valueOf(r.getLong(6));
BigInteger fee = BigInteger.valueOf(r.getLong(7));
byte[] txData = r.getBytes(8);
SendTransaction tx = new SendTransaction(normID, txHash,
txTime, blockHash, address, value, fee, txData);
SendTransaction prevTx = txMap.get(normID);
if (blockHash != null) {
if (prevTx != null) {
txList.remove(prevTx);
}
txList.add(tx);
txMap.put(normID, tx);
} else if (prevTx == null) {
txList.add(tx);
txMap.put(normID, tx);
}
}
} catch (SQLException exc) {
BTCLoader.error("Unable to get send transaction list", exc);
throw new BlockStoreException("Unable to get send transaction list");
}
return txList;
}
public void deleteTransactions(long rescanTime) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s1 = conn
.prepareStatement("DELETE FROM Received "
+ "WHERE timestamp>=? AND block_hash IS NOT NULL");
PreparedStatement s2 = conn
.prepareStatement("DELETE FROM Sent "
+ "WHERE timestamp>=? AND block_hash IS NOT NULL")) {
s1.setLong(1, rescanTime);
s1.executeUpdate();
s2.setLong(1, rescanTime);
s2.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error("Unable to delete rescan transactions", exc);
throw new BlockStoreException(
"Unable to delete rescan transactions");
}
}
public StoredHeader getHeader(Sha256Hash blockHash)
throws BlockStoreException {
StoredHeader header = null;
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT prev_hash,version,timestamp,target_difficulty,"
+ "merkle_root,block_height,chain_work,matches FROM Headers "
+ "WHERE block_hash_index=? AND block_hash=?")) {
s.setLong(1, getHashIndex(blockHash));
s.setBytes(2, blockHash.getBytes());
r = s.executeQuery();
if (r.next()) {
Sha256Hash prevHash = new Sha256Hash(r.getBytes(1));
int version = r.getInt(2);
long timestamp = r.getLong(3);
long targetDifficulty = r.getLong(4);
Sha256Hash merkleRoot = new Sha256Hash(r.getBytes(5));
int blockHeight = r.getInt(6);
BigInteger blockWork = new BigInteger(r.getBytes(7));
List<Sha256Hash> matches = getMatches(r.getBytes(8));
header = new StoredHeader(version, blockHash, prevHash,
timestamp, targetDifficulty, merkleRoot,
blockHeight >= 0, blockHeight, blockWork, matches);
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get block header\n Block %s", blockHash), exc);
throw new BlockStoreException("Unable to get block header");
}
return header;
}
public StoredHeader getChildHeader(Sha256Hash parentHash)
throws BlockStoreException {
StoredHeader header = null;
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT block_hash,version,timestamp,target_difficulty,"
+ "merkle_root,block_height,chain_work,matches FROM Headers "
+ "WHERE prev_hash_index=? AND prev_hash=?")) {
s.setLong(1, getHashIndex(parentHash));
s.setBytes(2, parentHash.getBytes());
r = s.executeQuery();
if (r.next()) {
Sha256Hash blockHash = new Sha256Hash(r.getBytes(1));
int version = r.getInt(2);
long timestamp = r.getLong(3);
long targetDifficulty = r.getLong(4);
Sha256Hash merkleRoot = new Sha256Hash(r.getBytes(5));
int blockHeight = r.getInt(6);
BigInteger blockWork = new BigInteger(r.getBytes(7));
List<Sha256Hash> matches = getMatches(r.getBytes(8));
header = new StoredHeader(version, blockHash, parentHash,
timestamp, targetDifficulty, merkleRoot,
blockHeight >= 0, blockHeight, blockWork, matches);
}
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to get child header\n Block %s", parentHash), exc);
throw new BlockStoreException("Unable to get child header");
}
return header;
}
public void updateMatches(BlockHeader header) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("UPDATE Headers SET matches=? "
+ "WHERE block_hash_index=? AND block_hash=?")) {
if (header.getMatches() == null || header.getMatches().isEmpty()) {
s.setNull(1, Types.BINARY);
} else {
s.setBytes(1, getMatches(header.getMatches()));
}
s.setLong(2, getHashIndex(header.getHash()));
s.setBytes(3, header.getHash().getBytes());
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to update matched transactions\n Block %s",
header.getHash()), exc);
throw new BlockStoreException(
"Unable to update matched transactions");
}
}
public int getRescanHeight(long rescanTime) throws BlockStoreException {
int height = 0;
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT block_height,timestamp FROM Headers "
+ "WHERE timestamp<? ORDER BY timestamp DESC LIMIT 1")) {
s.setLong(1, rescanTime);
r = s.executeQuery();
if (r.next())
height = r.getInt(1);
} catch (SQLException exc) {
BTCLoader.error("Unable to get rescan height", exc);
throw new BlockStoreException("Unable to get rescan height");
}
return height;
}
public Sha256Hash getBlockHash(int blockHeight) throws BlockStoreException {
Sha256Hash blockHash = null;
Connection conn = getConnection();
ResultSet r;
try (PreparedStatement s = conn
.prepareStatement("SELECT block_hash FROM Headers WHERE block_height=?")) {
s.setInt(1, blockHeight);
r = s.executeQuery();
if (r.next()) {
blockHash = new Sha256Hash(r.getBytes(1));
}
} catch (SQLException exc) {
BTCLoader.error(String.format("Unable to get block at height %d",
exc));
throw new BlockStoreException("Unable to get block");
}
return blockHash;
}
public List<StoredHeader> getJunctionHeader(Sha256Hash chainHash)
throws BlockNotFoundException, BlockStoreException {
List<StoredHeader> chainList = new LinkedList<>();
boolean onChain = false;
Sha256Hash blockHash = chainHash;
synchronized (lock) {
while (!onChain) {
StoredHeader header = getHeader(blockHash);
if (header == null) {
BTCLoader.debug(String.format(
"Chain block is not available\n Block %s",
blockHash));
throw new BlockNotFoundException(
"Unable to resolve block chain", blockHash);
}
chainList.add(0, header);
blockHash = header.getPrevHash();
onChain = header.isOnChain();
}
}
return chainList;
}
public void setChainStoredHead(List<StoredHeader> chainList)
throws BlockStoreException, VerificationException {
if (!BTCLoader.testNetwork) {
for (StoredHeader header : chainList) {
Sha256Hash checkHash = checkpoints.get(Integer.valueOf(header
.getBlockHeight()));
if (checkHash != null) {
if (checkHash.equals(header.getHash())) {
BTCLoader
.info(String
.format("New chain head at height %d matches checkpoint",
header.getBlockHeight()));
} else {
BTCLoader
.error(String
.format("New chain head at height %d does not match checkpoint",
header.getBlockHeight()));
throw new VerificationException(
"Checkpoint verification failed",
RejectMessage.REJECT_CHECKPOINT,
header.getHash());
}
}
}
}
StoredHeader chainHeader = chainList.get(chainList.size() - 1);
Connection conn = getConnection();
ResultSet r;
synchronized (lock) {
StoredHeader header;
Sha256Hash blockHash;
Sha256Hash prevHash;
List<Sha256Hash> txList;
try (PreparedStatement s1 = conn
.prepareStatement("SELECT prev_hash,matches FROM Headers "
+ "WHERE block_hash_index=? AND block_hash=? FOR UPDATE");
PreparedStatement s2 = conn
.prepareStatement("UPDATE Received SET block_hash=? "
+ "WHERE tx_hash_index=? AND tx_hash=?");
PreparedStatement s3 = conn
.prepareStatement("UPDATE Sent SET block_hash=? "
+ "WHERE tx_hash_index=? AND tx_hash=?");
PreparedStatement s4 = conn
.prepareStatement("UPDATE Headers SET block_height=-1 "
+ "WHERE block_hash_index=? AND block_hash=?");
PreparedStatement s5 = conn
.prepareStatement("UPDATE Headers SET block_height=?,chain_work=? "
+ "WHERE block_hash_index=? AND block_hash=?")) {
conn.setAutoCommit(false);
if (!chainHead.equals(chainHeader.getPrevHash())) {
Sha256Hash junctionHash = chainList.get(0).getHash();
blockHash = chainHead;
while (!blockHash.equals(junctionHash)) {
s1.setLong(1, getHashIndex(blockHash));
s1.setBytes(2, blockHash.getBytes());
r = s1.executeQuery();
if (!r.next()) {
BTCLoader.error(String.format(
"Chain block not found\n Block %s",
blockHash));
throw new BlockStoreException("Chain block not found",
blockHash);
}
prevHash = new Sha256Hash(r.getBytes(1));
byte[] bytes = r.getBytes(2);
r.close();
if (bytes != null) {
txList = getMatches(bytes);
for (Sha256Hash txHash : txList) {
s2.setNull(1, Types.BINARY);
s2.setLong(2, getHashIndex(txHash));
s2.setBytes(3, txHash.getBytes());
s2.executeUpdate();
s3.setNull(1, Types.BINARY);
s3.setLong(2, getHashIndex(txHash));
s3.setBytes(3, txHash.getBytes());
s3.executeUpdate();
}
}
s4.setLong(1, getHashIndex(blockHash));
s4.setBytes(2, blockHash.getBytes());
s4.executeUpdate();
BTCLoader.info(String.format(
"Block removed from block chain\n Block %s",
blockHash));
blockHash = prevHash;
}
}
for (int i = 1; i < chainList.size(); i++) {
header = chainList.get(i);
blockHash = header.getHash();
int blockHeight = header.getBlockHeight();
txList = header.getMatches();
if (txList != null) {
for (Sha256Hash txHash : txList) {
s2.setBytes(1, blockHash.getBytes());
s2.setLong(2, getHashIndex(txHash));
s2.setBytes(3, txHash.getBytes());
s2.executeUpdate();
s3.setBytes(1, blockHash.getBytes());
s3.setLong(2, getHashIndex(txHash));
s3.setBytes(3, txHash.getBytes());
s3.executeUpdate();
}
}
s5.setInt(1, blockHeight);
s5.setBytes(2, header.getChainWork().toByteArray());
s5.setLong(3, getHashIndex(blockHash));
s5.setBytes(4, blockHash.getBytes());
s5.executeUpdate();
BTCLoader
.info(String
.format("Block added to block chain at height %d, Difficulty %d\n Block %s",
blockHeight, header.getChainWork(),
blockHash));
}
conn.commit();
conn.setAutoCommit(true);
chainHead = chainHeader.getHash();
chainHeight = chainHeader.getBlockHeight();
chainWork = chainHeader.getChainWork();
} catch (SQLException exc) {
BTCLoader.error("Unable to update block chain", exc);
rollback();
throw new BlockStoreException("Unable to update block chain");
}
}
}
public void storeHeader(StoredHeader storedHeader) throws BlockStoreException {
Connection conn = getConnection();
try (PreparedStatement s = conn
.prepareStatement("INSERT INTO Headers "
+ "(block_hash_index,block_hash,prev_hash_index,prev_hash,version,timestamp,"
+ "target_difficulty,merkle_root,block_height,chain_work,matches) "
+ "VALUES(?,?,?,?,?,?,?,?,?,?,?)")) {
s.setLong(1, getHashIndex(storedHeader.getHash()));
s.setBytes(2, storedHeader.getHash().getBytes());
s.setLong(3, getHashIndex(storedHeader.getPrevHash()));
s.setBytes(4, storedHeader.getPrevHash().getBytes());
s.setInt(5, storedHeader.getVersion());
s.setLong(6, storedHeader.getBlockTime());
s.setLong(7, storedHeader.getTargetDifficulty());
s.setBytes(8, storedHeader.getMerkleRoot().getBytes());
s.setInt(9,
storedHeader.isOnChain() ? storedHeader.getBlockHeight()
: -1);
s.setBytes(10, storedHeader.getChainWork().toByteArray());
if (storedHeader.getMatches() == null
|| storedHeader.getMatches().isEmpty()){
s.setNull(11, Types.BINARY);
}
else{
s.setBytes(11, getMatches(storedHeader.getMatches()));
}
s.executeUpdate();
} catch (SQLException exc) {
BTCLoader.error(String.format(
"Unable to store block header\n Block %s",
storedHeader.getHash()), exc);
throw new BlockStoreException("Unable to store block header");
}
}
private byte[] getMatches(List<Sha256Hash> matches) {
if (matches == null || matches.isEmpty())
return null;
byte[] bytes = new byte[matches.size() * 32];
int offset = 0;
for (Sha256Hash txHash : matches) {
System.arraycopy(txHash.getBytes(), 0, bytes, offset, 32);
offset += 32;
}
return bytes;
}
private List<Sha256Hash> getMatches(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
List<Sha256Hash> matches = new ArrayList<>(bytes.length / 32);
for (int offset = 0; offset < bytes.length; offset += 32) {
matches.add(new Sha256Hash(bytes, offset, 32));
}
return matches;
}
}