/* * Copyright 2014 BitPOS Pty Ltd. * Copyright 2014 Andreas Schildbach. * Copyright 2014 Kalpesh Parmar. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bitcoinj.store; import com.google.common.collect.Lists; import org.bitcoinj.core.*; import org.bitcoinj.script.Script; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigInteger; import java.sql.*; import java.util.*; /** * <p>A generic full pruned block store for a relational database. This generic class requires * certain table structures for the block store.</p> * * <p>The following are the tables and field names/types that are assumed:-</p> * * <p> * <b>setting</b> table * <table> * <tr><th>Field Name</th><th>Type (generic)</th></tr> * <tr><td>name</td><td>string</td></tr> * <tr><td>value</td><td>binary</td></tr> * </table> * </p> * * <p><br/> * <b>headers</b> table * <table> * <tr><th>Field Name</th><th>Type (generic)</th></tr> * <tr><td>hash</td><td>binary</td></tr> * <tr><td>chainwork</td><td>binary</td></tr> * <tr><td>height</td><td>integer</td></tr> * <tr><td>header</td><td>binary</td></tr> * <tr><td>wasundoable</td><td>boolean</td></tr> * </table> * </p> * * <p><br/> * <b>undoableblocks</b> table * <table> * <tr><th>Field Name</th><th>Type (generic)</th></tr> * <tr><td>hash</td><td>binary</td></tr> * <tr><td>height</td><td>integer</td></tr> * <tr><td>txoutchanges</td><td>binary</td></tr> * <tr><td>transactions</td><td>binary</td></tr> * </table> * </p> * * <p><br/> * <b>openoutputs</b> table * <table> * <tr><th>Field Name</th><th>Type (generic)</th></tr> * <tr><td>hash</td><td>binary</td></tr> * <tr><td>index</td><td>integer</td></tr> * <tr><td>height</td><td>integer</td></tr> * <tr><td>value</td><td>integer</td></tr> * <tr><td>scriptbytes</td><td>binary</td></tr> * <tr><td>toaddress</td><td>string</td></tr> * <tr><td>addresstargetable</td><td>integer</td></tr> * <tr><td>coinbase</td><td>boolean</td></tr> * </table> * </p> * */ public abstract class DatabaseFullPrunedBlockStore implements FullPrunedBlockStore { private static final Logger log = LoggerFactory.getLogger(DatabaseFullPrunedBlockStore.class); private static final String CHAIN_HEAD_SETTING = "chainhead"; private static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead"; private static final String VERSION_SETTING = "version"; // Drop table SQL. private static final String DROP_SETTINGS_TABLE = "DROP TABLE settings"; private static final String DROP_HEADERS_TABLE = "DROP TABLE headers"; private static final String DROP_UNDOABLE_TABLE = "DROP TABLE undoableblocks"; private static final String DROP_OPEN_OUTPUT_TABLE = "DROP TABLE openoutputs"; // Queries SQL. private static final String SELECT_SETTINGS_SQL = "SELECT value FROM settings WHERE name = ?"; private static final String INSERT_SETTINGS_SQL = "INSERT INTO settings(name, value) VALUES(?, ?)"; private static final String UPDATE_SETTINGS_SQL = "UPDATE settings SET value = ? WHERE name = ?"; private static final String SELECT_HEADERS_SQL = "SELECT chainwork, height, header, wasundoable FROM headers WHERE hash = ?"; private static final String INSERT_HEADERS_SQL = "INSERT INTO headers(hash, chainwork, height, header, wasundoable) VALUES(?, ?, ?, ?, ?)"; private static final String UPDATE_HEADERS_SQL = "UPDATE headers SET wasundoable=? WHERE hash=?"; private static final String SELECT_UNDOABLEBLOCKS_SQL = "SELECT txoutchanges, transactions FROM undoableblocks WHERE hash = ?"; private static final String INSERT_UNDOABLEBLOCKS_SQL = "INSERT INTO undoableblocks(hash, height, txoutchanges, transactions) VALUES(?, ?, ?, ?)"; private static final String UPDATE_UNDOABLEBLOCKS_SQL = "UPDATE undoableblocks SET txoutchanges=?, transactions=? WHERE hash = ?"; private static final String DELETE_UNDOABLEBLOCKS_SQL = "DELETE FROM undoableblocks WHERE height <= ?"; private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptbytes, coinbase, toaddress, addresstargetable FROM openoutputs WHERE hash = ? AND index = ?"; private static final String SELECT_OPENOUTPUTS_COUNT_SQL = "SELECT COUNT(*) FROM openoutputs WHERE hash = ?"; private static final String INSERT_OPENOUTPUTS_SQL = "INSERT INTO openoutputs (hash, index, height, value, scriptbytes, toaddress, addresstargetable, coinbase) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; private static final String DELETE_OPENOUTPUTS_SQL = "DELETE FROM openoutputs WHERE hash = ? AND index = ?"; // Dump table SQL (this is just for data sizing statistics). private static final String SELECT_DUMP_SETTINGS_SQL = "SELECT name, value FROM settings"; private static final String SELECT_DUMP_HEADERS_SQL = "SELECT chainwork, header FROM headers"; private static final String SELECT_DUMP_UNDOABLEBLOCKS_SQL = "SELECT txoutchanges, transactions FROM undoableblocks"; private static final String SELECT_DUMP_OPENOUTPUTS_SQL = "SELECT value, scriptbytes FROM openoutputs"; private static final String SELECT_TRANSACTION_OUTPUTS_SQL = "SELECT hash, value, scriptbytes, height, index, coinbase, toaddress, addresstargetable FROM openoutputs where toaddress = ?"; // Select the balance of an address SQL. private static final String SELECT_BALANCE_SQL = "select sum(value) from openoutputs where toaddress = ?"; // Tables exist SQL. private static final String SELECT_CHECK_TABLES_EXIST_SQL = "SELECT * FROM settings WHERE 1 = 2"; // Compatibility SQL. private static final String SELECT_COMPATIBILITY_COINBASE_SQL = "SELECT coinbase FROM openoutputs WHERE 1 = 2"; protected Sha256Hash chainHeadHash; protected StoredBlock chainHeadBlock; protected Sha256Hash verifiedChainHeadHash; protected StoredBlock verifiedChainHeadBlock; protected NetworkParameters params; protected ThreadLocal<Connection> conn; protected List<Connection> allConnections; protected String connectionURL; protected int fullStoreDepth; protected String username; protected String password; protected String schemaName; /** * <p>Create a new DatabaseFullPrunedBlockStore, using the full connection URL instead of a hostname and password, * and optionally allowing a schema to be specified.</p> * * @param params A copy of the NetworkParameters used. * @param connectionURL The jdbc url to connect to the database. * @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe). * @param username The database username. * @param password The password to the database. * @param schemaName The name of the schema to put the tables in. May be null if no schema is being used. * @throws BlockStoreException If there is a failure to connect and/or initialise the database. */ public DatabaseFullPrunedBlockStore(NetworkParameters params, String connectionURL, int fullStoreDepth, @Nullable String username, @Nullable String password, @Nullable String schemaName) throws BlockStoreException { this.params = params; this.fullStoreDepth = fullStoreDepth; this.connectionURL = connectionURL; this.schemaName = schemaName; this.username = username; this.password = password; this.conn = new ThreadLocal<Connection>(); this.allConnections = new LinkedList<Connection>(); try { Class.forName(getDatabaseDriverClass()); log.info(getDatabaseDriverClass() + " loaded. "); } catch (ClassNotFoundException e) { log.error("check CLASSPATH for database driver jar ", e); } maybeConnect(); try { // Create tables if needed if (!tablesExists()) { createTables(); } else { checkCompatibility(); } initFromDatabase(); } catch (SQLException e) { throw new BlockStoreException(e); } } /** * Get the database driver class, * <p>i.e org.postgresql.Driver.</p> * @return The fully qualified database driver class. */ protected abstract String getDatabaseDriverClass(); /** * Get the SQL statements that create the schema (DDL). * @return The list of SQL statements. */ protected abstract List<String> getCreateSchemeSQL(); /** * Get the SQL statements that create the tables (DDL). * @return The list of SQL statements. */ protected abstract List<String> getCreateTablesSQL(); /** * Get the SQL statements that create the indexes (DDL). * @return The list of SQL statements. */ protected abstract List<String> getCreateIndexesSQL(); /** * Get the database specific error code that indicated a duplicate key error when inserting a record. * <p>This is the code returned by {@link java.sql.SQLException#getSQLState()}</p> * @return The database duplicate error code. */ protected abstract String getDuplicateKeyErrorCode(); /** * Get the SQL to select the total balance for a given address. * @return The SQL prepared statement. */ protected String getBalanceSelectSQL() { return SELECT_BALANCE_SQL; } /** * Get the SQL statement that checks if tables exist. * @return The SQL prepared statement. */ protected String getTablesExistSQL() { return SELECT_CHECK_TABLES_EXIST_SQL; } /** * Get the SQL statements to check if the database is compatible. * @return The SQL prepared statements. */ protected List<String> getCompatibilitySQL() { List<String> sqlStatements = new ArrayList<String>(); sqlStatements.add(SELECT_COMPATIBILITY_COINBASE_SQL); return sqlStatements; } /** * Get the SQL to select the transaction outputs for a given address. * @return The SQL prepared statement. */ protected String getTransactionOutputSelectSQL() { return SELECT_TRANSACTION_OUTPUTS_SQL; } /** * Get the SQL to drop all the tables (DDL). * @return The SQL drop statements. */ protected List<String> getDropTablesSQL() { List<String> sqlStatements = new ArrayList<String>(); sqlStatements.add(DROP_SETTINGS_TABLE); sqlStatements.add(DROP_HEADERS_TABLE); sqlStatements.add(DROP_UNDOABLE_TABLE); sqlStatements.add(DROP_OPEN_OUTPUT_TABLE); return sqlStatements; } /** * Get the SQL to select a setting value. * @return The SQL select statement. */ protected String getSelectSettingsSQL() { return SELECT_SETTINGS_SQL; } /** * Get the SQL to insert a settings record. * @return The SQL insert statement. */ protected String getInsertSettingsSQL() { return INSERT_SETTINGS_SQL; } /** * Get the SQL to update a setting value. * @return The SQL update statement. */ protected String getUpdateSettingsSLQ() { return UPDATE_SETTINGS_SQL; } /** * Get the SQL to select a headers record. * @return The SQL select statement. */ protected String getSelectHeadersSQL() { return SELECT_HEADERS_SQL; } /** * Get the SQL to insert a headers record. * @return The SQL insert statement. */ protected String getInsertHeadersSQL() { return INSERT_HEADERS_SQL; } /** * Get the SQL to update a headers record. * @return The SQL update statement. */ protected String getUpdateHeadersSQL() { return UPDATE_HEADERS_SQL; } /** * Get the SQL to select an undoableblocks record. * @return The SQL select statement. */ protected String getSelectUndoableBlocksSQL() { return SELECT_UNDOABLEBLOCKS_SQL; } /** * Get the SQL to insert a undoableblocks record. * @return The SQL insert statement. */ protected String getInsertUndoableBlocksSQL() { return INSERT_UNDOABLEBLOCKS_SQL; } /** * Get the SQL to update a undoableblocks record. * @return The SQL update statement. */ protected String getUpdateUndoableBlocksSQL() { return UPDATE_UNDOABLEBLOCKS_SQL; } /** * Get the SQL to delete a undoableblocks record. * @return The SQL delete statement. */ protected String getDeleteUndoableBlocksSQL() { return DELETE_UNDOABLEBLOCKS_SQL; } /** * Get the SQL to select a openoutputs record. * @return The SQL select statement. */ protected String getSelectOpenoutputsSQL() { return SELECT_OPENOUTPUTS_SQL; } /** * Get the SQL to select count of openoutputs. * @return The SQL select statement. */ protected String getSelectOpenoutputsCountSQL() { return SELECT_OPENOUTPUTS_COUNT_SQL; } /** * Get the SQL to insert a openoutputs record. * @return The SQL insert statement. */ protected String getInsertOpenoutputsSQL() { return INSERT_OPENOUTPUTS_SQL; } /** * Get the SQL to delete a openoutputs record. * @return The SQL delete statement. */ protected String getDeleteOpenoutputsSQL() { return DELETE_OPENOUTPUTS_SQL; } /** * Get the SQL to select the setting dump fields for sizing/statistics. * @return The SQL select statement. */ protected String getSelectSettingsDumpSQL() { return SELECT_DUMP_SETTINGS_SQL; } /** * Get the SQL to select the headers dump fields for sizing/statistics. * @return The SQL select statement. */ protected String getSelectHeadersDumpSQL() { return SELECT_DUMP_HEADERS_SQL; } /** * Get the SQL to select the undoableblocks dump fields for sizing/statistics. * @return The SQL select statement. */ protected String getSelectUndoableblocksDumpSQL() { return SELECT_DUMP_UNDOABLEBLOCKS_SQL; } /** * Get the SQL to select the openoutouts dump fields for sizing/statistics. * @return The SQL select statement. */ protected String getSelectopenoutputsDumpSQL() { return SELECT_DUMP_OPENOUTPUTS_SQL; } /** * <p>If there isn't a connection on the {@link ThreadLocal} then create and store it.</p> * <p>This will also automatically set up the schema if it does not exist within the DB.</p> * @throws BlockStoreException if successful connection to the DB couldn't be made. */ protected synchronized final void maybeConnect() throws BlockStoreException { try { if (conn.get() != null && !conn.get().isClosed()) return; if (username == null || password == null) { conn.set(DriverManager.getConnection(connectionURL)); } else { Properties props = new Properties(); props.setProperty("user", this.username); props.setProperty("password", this.password); conn.set(DriverManager.getConnection(connectionURL, props)); } allConnections.add(conn.get()); Connection connection = conn.get(); // set the schema if one is needed if (schemaName != null) { Statement s = connection.createStatement(); for (String sql : getCreateSchemeSQL()) { s.execute(sql); } } log.info("Made a new connection to database " + connectionURL); } catch (SQLException ex) { throw new BlockStoreException(ex); } } @Override public synchronized void close() { for (Connection conn : allConnections) { try { if (!conn.getAutoCommit()) { conn.rollback(); } conn.close(); if (conn == this.conn.get()) { this.conn.set(null); } } catch (SQLException ex) { throw new RuntimeException(ex); } } allConnections.clear(); } /** * <p>Check if a tables exists within the database.</p> * * <p>This specifically checks for the 'settings' table and * if it exists makes an assumption that the rest of the data * structures are present.</p> * * @return If the tables exists. * @throws java.sql.SQLException */ private boolean tablesExists() throws SQLException { PreparedStatement ps = null; try { ps = conn.get().prepareStatement(getTablesExistSQL()); ResultSet results = ps.executeQuery(); results.close(); return true; } catch (SQLException ex) { return false; } finally { if(ps != null && !ps.isClosed()) { ps.close(); } } } /** * Check that the database is compatible with this version of the {@link DatabaseFullPrunedBlockStore}. * @throws BlockStoreException If the database is not compatible. */ private void checkCompatibility() throws SQLException, BlockStoreException { for(String sql : getCompatibilitySQL()) { PreparedStatement ps = null; try { ps = conn.get().prepareStatement(sql); ResultSet results = ps.executeQuery(); results.close(); } catch (SQLException ex) { throw new BlockStoreException("Database block store is not compatible with the current release. " + "See bitcoinj release notes for further information: " + ex.getMessage()); } finally { if (ps != null && !ps.isClosed()) { ps.close(); } } } } /** * Create the tables/block store in the database and * @throws java.sql.SQLException If there is a database error. * @throws BlockStoreException If the block store could not be created. */ private void createTables() throws SQLException, BlockStoreException { Statement s = conn.get().createStatement(); // create all the database tables for (String sql : getCreateTablesSQL()) { if (log.isDebugEnabled()) { log.debug("DatabaseFullPrunedBlockStore : CREATE table [SQL= {0}]", sql); } s.executeUpdate(sql); } // create all the database indexes for (String sql : getCreateIndexesSQL()) { if (log.isDebugEnabled()) { log.debug("DatabaseFullPrunedBlockStore : CREATE index [SQL= {0}]", sql); } s.executeUpdate(sql); } s.close(); // insert the initial settings for this store PreparedStatement ps = conn.get().prepareStatement(getInsertSettingsSQL()); ps.setString(1, CHAIN_HEAD_SETTING); ps.setNull(2, Types.BINARY); ps.execute(); ps.setString(1, VERIFIED_CHAIN_HEAD_SETTING); ps.setNull(2, Types.BINARY); ps.execute(); ps.setString(1, VERSION_SETTING); ps.setBytes(2, "03".getBytes()); ps.execute(); ps.close(); createNewStore(params); } /** * Create a new store for the given {@link org.bitcoinj.core.NetworkParameters}. * @param params The network. * @throws BlockStoreException If the store couldn't be created. */ private void createNewStore(NetworkParameters params) throws BlockStoreException { try { // Set up the genesis block. When we start out fresh, it is by // definition the top of the chain. StoredBlock storedGenesisHeader = new StoredBlock(params.getGenesisBlock().cloneAsHeader(), params.getGenesisBlock().getWork(), 0); // The coinbase in the genesis block is not spendable. This is because of how Bitcoin Core inits // its database - the genesis transaction isn't actually in the db so its spent flags can never be updated. List<Transaction> genesisTransactions = Lists.newLinkedList(); StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions); put(storedGenesisHeader, storedGenesis); setChainHead(storedGenesisHeader); setVerifiedChainHead(storedGenesisHeader); } catch (VerificationException e) { throw new RuntimeException(e); // Cannot happen. } } /** * Initialise the store state from the database. * @throws java.sql.SQLException If there is a database error. * @throws BlockStoreException If there is a block store error. */ private void initFromDatabase() throws SQLException, BlockStoreException { PreparedStatement ps = conn.get().prepareStatement(getSelectSettingsSQL()); ResultSet rs; ps.setString(1, CHAIN_HEAD_SETTING); rs = ps.executeQuery(); if (!rs.next()) { throw new BlockStoreException("corrupt database block store - no chain head pointer"); } Sha256Hash hash = Sha256Hash.wrap(rs.getBytes(1)); rs.close(); this.chainHeadBlock = get(hash); this.chainHeadHash = hash; if (this.chainHeadBlock == null) { throw new BlockStoreException("corrupt database block store - head block not found"); } ps.setString(1, VERIFIED_CHAIN_HEAD_SETTING); rs = ps.executeQuery(); if (!rs.next()) { throw new BlockStoreException("corrupt database block store - no verified chain head pointer"); } hash = Sha256Hash.wrap(rs.getBytes(1)); rs.close(); ps.close(); this.verifiedChainHeadBlock = get(hash); this.verifiedChainHeadHash = hash; if (this.verifiedChainHeadBlock == null) { throw new BlockStoreException("corrupt database block store - verified head block not found"); } } protected void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) throws SQLException { try { PreparedStatement s = conn.get().prepareStatement(getInsertHeadersSQL()); // We skip the first 4 bytes because (on mainnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 4, hashBytes, 0, 28); s.setBytes(1, hashBytes); s.setBytes(2, storedBlock.getChainWork().toByteArray()); s.setInt(3, storedBlock.getHeight()); s.setBytes(4, storedBlock.getHeader().cloneAsHeader().unsafeBitcoinSerialize()); s.setBoolean(5, wasUndoable); s.executeUpdate(); s.close(); } catch (SQLException e) { // It is possible we try to add a duplicate StoredBlock if we upgraded // In that case, we just update the entry to mark it wasUndoable if (!(e.getSQLState().equals(getDuplicateKeyErrorCode())) || !wasUndoable) throw e; PreparedStatement s = conn.get().prepareStatement(getUpdateHeadersSQL()); s.setBoolean(1, true); // We skip the first 4 bytes because (on mainnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 4, hashBytes, 0, 28); s.setBytes(2, hashBytes); s.executeUpdate(); s.close(); } } @Override public void put(StoredBlock storedBlock) throws BlockStoreException { maybeConnect(); try { putUpdateStoredBlock(storedBlock, false); } catch (SQLException e) { throw new BlockStoreException(e); } } @Override public void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException { maybeConnect(); // We skip the first 4 bytes because (on mainnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 4, hashBytes, 0, 28); int height = storedBlock.getHeight(); byte[] transactions = null; byte[] txOutChanges = null; try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); if (undoableBlock.getTxOutChanges() != null) { undoableBlock.getTxOutChanges().serializeToStream(bos); txOutChanges = bos.toByteArray(); } else { int numTxn = undoableBlock.getTransactions().size(); bos.write(0xFF & numTxn); bos.write(0xFF & (numTxn >> 8)); bos.write(0xFF & (numTxn >> 16)); bos.write(0xFF & (numTxn >> 24)); for (Transaction tx : undoableBlock.getTransactions()) tx.bitcoinSerialize(bos); transactions = bos.toByteArray(); } bos.close(); } catch (IOException e) { throw new BlockStoreException(e); } try { try { PreparedStatement s = conn.get().prepareStatement(getInsertUndoableBlocksSQL()); s.setBytes(1, hashBytes); s.setInt(2, height); if (transactions == null) { s.setBytes(3, txOutChanges); s.setNull(4, Types.BINARY); } else { s.setNull(3, Types.BINARY); s.setBytes(4, transactions); } s.executeUpdate(); s.close(); try { putUpdateStoredBlock(storedBlock, true); } catch (SQLException e) { throw new BlockStoreException(e); } } catch (SQLException e) { if (!e.getSQLState().equals(getDuplicateKeyErrorCode())) throw new BlockStoreException(e); // There is probably an update-or-insert statement, but it wasn't obvious from the docs PreparedStatement s = conn.get().prepareStatement(getUpdateUndoableBlocksSQL()); s.setBytes(3, hashBytes); if (transactions == null) { s.setBytes(1, txOutChanges); s.setNull(2, Types.BINARY); } else { s.setNull(1, Types.BINARY); s.setBytes(2, transactions); } s.executeUpdate(); s.close(); } } catch (SQLException ex) { throw new BlockStoreException(ex); } } public StoredBlock get(Sha256Hash hash, boolean wasUndoableOnly) throws BlockStoreException { // Optimize for chain head if (chainHeadHash != null && chainHeadHash.equals(hash)) return chainHeadBlock; if (verifiedChainHeadHash != null && verifiedChainHeadHash.equals(hash)) return verifiedChainHeadBlock; maybeConnect(); PreparedStatement s = null; try { s = conn.get() .prepareStatement(getSelectHeadersSQL()); // We skip the first 4 bytes because (on mainnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(hash.getBytes(), 4, hashBytes, 0, 28); s.setBytes(1, hashBytes); ResultSet results = s.executeQuery(); if (!results.next()) { return null; } // Parse it. if (wasUndoableOnly && !results.getBoolean(4)) return null; BigInteger chainWork = new BigInteger(results.getBytes(1)); int height = results.getInt(2); Block b = params.getDefaultSerializer().makeBlock(results.getBytes(3)); b.verifyHeader(); StoredBlock stored = new StoredBlock(b, chainWork, height); return stored; } catch (SQLException ex) { throw new BlockStoreException(ex); } catch (ProtocolException e) { // Corrupted database. throw new BlockStoreException(e); } catch (VerificationException e) { // Should not be able to happen unless the database contains bad // blocks. throw new BlockStoreException(e); } finally { if (s != null) { try { s.close(); } catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); } } } } @Override public StoredBlock get(Sha256Hash hash) throws BlockStoreException { return get(hash, false); } @Override public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException { return get(hash, true); } @Override public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { s = conn.get() .prepareStatement(getSelectUndoableBlocksSQL()); // We skip the first 4 bytes because (on mainnet) the minimum target has 4 0-bytes byte[] hashBytes = new byte[28]; System.arraycopy(hash.getBytes(), 4, hashBytes, 0, 28); s.setBytes(1, hashBytes); ResultSet results = s.executeQuery(); if (!results.next()) { return null; } // Parse it. byte[] txOutChanges = results.getBytes(1); byte[] transactions = results.getBytes(2); StoredUndoableBlock block; if (txOutChanges == null) { int offset = 0; int numTxn = ((transactions[offset++] & 0xFF)) | ((transactions[offset++] & 0xFF) << 8) | ((transactions[offset++] & 0xFF) << 16) | ((transactions[offset++] & 0xFF) << 24); List<Transaction> transactionList = new LinkedList<Transaction>(); for (int i = 0; i < numTxn; i++) { Transaction tx = params.getDefaultSerializer().makeTransaction(transactions, offset); transactionList.add(tx); offset += tx.getMessageSize(); } block = new StoredUndoableBlock(hash, transactionList); } else { TransactionOutputChanges outChangesObject = new TransactionOutputChanges(new ByteArrayInputStream(txOutChanges)); block = new StoredUndoableBlock(hash, outChangesObject); } return block; } catch (SQLException ex) { throw new BlockStoreException(ex); } catch (NullPointerException e) { // Corrupted database. throw new BlockStoreException(e); } catch (ClassCastException e) { // Corrupted database. throw new BlockStoreException(e); } catch (ProtocolException e) { // Corrupted database. throw new BlockStoreException(e); } catch (IOException e) { // Corrupted database. throw new BlockStoreException(e); } finally { if (s != null) { try { s.close(); } catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); } } } } @Override public StoredBlock getChainHead() throws BlockStoreException { return chainHeadBlock; } @Override public void setChainHead(StoredBlock chainHead) throws BlockStoreException { Sha256Hash hash = chainHead.getHeader().getHash(); this.chainHeadHash = hash; this.chainHeadBlock = chainHead; maybeConnect(); try { PreparedStatement s = conn.get() .prepareStatement(getUpdateSettingsSLQ()); s.setString(2, CHAIN_HEAD_SETTING); s.setBytes(1, hash.getBytes()); s.executeUpdate(); s.close(); } catch (SQLException ex) { throw new BlockStoreException(ex); } } @Override public StoredBlock getVerifiedChainHead() throws BlockStoreException { return verifiedChainHeadBlock; } @Override public void setVerifiedChainHead(StoredBlock chainHead) throws BlockStoreException { Sha256Hash hash = chainHead.getHeader().getHash(); this.verifiedChainHeadHash = hash; this.verifiedChainHeadBlock = chainHead; maybeConnect(); try { PreparedStatement s = conn.get() .prepareStatement(getUpdateSettingsSLQ()); s.setString(2, VERIFIED_CHAIN_HEAD_SETTING); s.setBytes(1, hash.getBytes()); s.executeUpdate(); s.close(); } catch (SQLException ex) { throw new BlockStoreException(ex); } if (this.chainHeadBlock.getHeight() < chainHead.getHeight()) setChainHead(chainHead); removeUndoableBlocksWhereHeightIsLessThan(chainHead.getHeight() - fullStoreDepth); } private void removeUndoableBlocksWhereHeightIsLessThan(int height) throws BlockStoreException { try { PreparedStatement s = conn.get() .prepareStatement(getDeleteUndoableBlocksSQL()); s.setInt(1, height); if (log.isDebugEnabled()) log.debug("Deleting undoable undoable block with height <= " + height); s.executeUpdate(); s.close(); } catch (SQLException ex) { throw new BlockStoreException(ex); } } @Override public UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { s = conn.get() .prepareStatement(getSelectOpenoutputsSQL()); s.setBytes(1, hash.getBytes()); // index is actually an unsigned int s.setInt(2, (int) index); ResultSet results = s.executeQuery(); if (!results.next()) { return null; } // Parse it. int height = results.getInt(1); Coin value = Coin.valueOf(results.getLong(2)); byte[] scriptBytes = results.getBytes(3); boolean coinbase = results.getBoolean(4); String address = results.getString(5); UTXO txout = new UTXO(hash, index, value, height, coinbase, new Script(scriptBytes), address); return txout; } catch (SQLException ex) { throw new BlockStoreException(ex); } finally { if (s != null) { try { s.close(); } catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); } } } } @Override public void addUnspentTransactionOutput(UTXO out) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { s = conn.get().prepareStatement(getInsertOpenoutputsSQL()); s.setBytes(1, out.getHash().getBytes()); // index is actually an unsigned int s.setInt(2, (int) out.getIndex()); s.setInt(3, out.getHeight()); s.setLong(4, out.getValue().value); s.setBytes(5, out.getScript().getProgram()); s.setString(6, out.getAddress()); s.setInt(7, out.getScript().getScriptType().ordinal()); s.setBoolean(8, out.isCoinbase()); s.executeUpdate(); s.close(); } catch (SQLException e) { if (!(e.getSQLState().equals(getDuplicateKeyErrorCode()))) throw new BlockStoreException(e); } finally { if (s != null) { try { s.close(); } catch (SQLException e) { throw new BlockStoreException(e); } } } } @Override public void removeUnspentTransactionOutput(UTXO out) throws BlockStoreException { maybeConnect(); // TODO: This should only need one query (maybe a stored procedure) if (getTransactionOutput(out.getHash(), out.getIndex()) == null) throw new BlockStoreException("Tried to remove a UTXO from DatabaseFullPrunedBlockStore that it didn't have!"); try { PreparedStatement s = conn.get() .prepareStatement(getDeleteOpenoutputsSQL()); s.setBytes(1, out.getHash().getBytes()); // index is actually an unsigned int s.setInt(2, (int)out.getIndex()); s.executeUpdate(); s.close(); } catch (SQLException e) { throw new BlockStoreException(e); } } @Override public void beginDatabaseBatchWrite() throws BlockStoreException { maybeConnect(); if (log.isDebugEnabled()) log.debug("Starting database batch write with connection: " + conn.get().toString()); try { conn.get().setAutoCommit(false); } catch (SQLException e) { throw new BlockStoreException(e); } } @Override public void commitDatabaseBatchWrite() throws BlockStoreException { maybeConnect(); if (log.isDebugEnabled()) log.debug("Committing database batch write with connection: " + conn.get().toString()); try { conn.get().commit(); conn.get().setAutoCommit(true); } catch (SQLException e) { throw new BlockStoreException(e); } } @Override public void abortDatabaseBatchWrite() throws BlockStoreException { maybeConnect(); if (log.isDebugEnabled()) log.debug("Rollback database batch write with connection: " + conn.get().toString()); try { if (!conn.get().getAutoCommit()) { conn.get().rollback(); conn.get().setAutoCommit(true); } else { log.warn("Warning: Rollback attempt without transaction"); } } catch (SQLException e) { throw new BlockStoreException(e); } } @Override public boolean hasUnspentOutputs(Sha256Hash hash, int numOutputs) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { s = conn.get().prepareStatement(getSelectOpenoutputsCountSQL()); s.setBytes(1, hash.getBytes()); ResultSet results = s.executeQuery(); if (!results.next()) { throw new BlockStoreException("Got no results from a COUNT(*) query"); } int count = results.getInt(1); return count != 0; } catch (SQLException ex) { throw new BlockStoreException(ex); } finally { if (s != null) { try { s.close(); } catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); } } } } @Override public NetworkParameters getParams() { return params; } @Override public int getChainHeadHeight() throws UTXOProviderException { try { return getVerifiedChainHead().getHeight(); } catch (BlockStoreException e) { throw new UTXOProviderException(e); } } /** * Resets the store by deleting the contents of the tables and reinitialising them. * @throws BlockStoreException If the tables couldn't be cleared and initialised. */ public void resetStore() throws BlockStoreException { maybeConnect(); try { deleteStore(); createTables(); initFromDatabase(); } catch (SQLException ex) { throw new RuntimeException(ex); } } /** * Deletes the store by deleting the tables within the database. * @throws BlockStoreException If tables couldn't be deleted. */ public void deleteStore() throws BlockStoreException { maybeConnect(); try { Statement s = conn.get().createStatement(); for(String sql : getDropTablesSQL()) { s.execute(sql); } s.close(); } catch (SQLException ex) { throw new RuntimeException(ex); } } /** * Calculate the balance for a coinbase, to-address, or p2sh address. * * <p>The balance {@link org.bitcoinj.store.DatabaseFullPrunedBlockStore#getBalanceSelectSQL()} returns * the balance (summed) as an number, then use calculateClientSide=false</p> * * <p>The balance {@link org.bitcoinj.store.DatabaseFullPrunedBlockStore#getBalanceSelectSQL()} returns * the all the openoutputs as stored in the DB (binary), then use calculateClientSide=true</p> * * @param address The address to calculate the balance of * @return The balance of the address supplied. If the address has not been seen, or there are no outputs open for this * address, the return value is 0. * @throws BlockStoreException If there is an error getting the balance. */ public BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException { maybeConnect(); PreparedStatement s = null; try { s = conn.get().prepareStatement(getBalanceSelectSQL()); s.setString(1, address.toString()); ResultSet rs = s.executeQuery(); BigInteger balance = BigInteger.ZERO; if (rs.next()) { return BigInteger.valueOf(rs.getLong(1)); } return balance; } catch (SQLException ex) { throw new BlockStoreException(ex); } finally { if (s != null) { try { s.close(); } catch (SQLException e) { throw new BlockStoreException("Could not close statement"); } } } } @Override public List<UTXO> getOpenTransactionOutputs(List<Address> addresses) throws UTXOProviderException { PreparedStatement s = null; List<UTXO> outputs = new ArrayList<UTXO>(); try { maybeConnect(); s = conn.get().prepareStatement(getTransactionOutputSelectSQL()); for (Address address : addresses) { s.setString(1, address.toString()); ResultSet rs = s.executeQuery(); while (rs.next()) { Sha256Hash hash = Sha256Hash.wrap(rs.getBytes(1)); Coin amount = Coin.valueOf(rs.getLong(2)); byte[] scriptBytes = rs.getBytes(3); int height = rs.getInt(4); int index = rs.getInt(5); boolean coinbase = rs.getBoolean(6); String toAddress = rs.getString(7); UTXO output = new UTXO(hash, index, amount, height, coinbase, new Script(scriptBytes), toAddress); outputs.add(output); } } return outputs; } catch (SQLException ex) { throw new UTXOProviderException(ex); } catch (BlockStoreException bse) { throw new UTXOProviderException(bse); } finally { if (s != null) try { s.close(); } catch (SQLException e) { throw new UTXOProviderException("Could not close statement", e); } } } /** * Dumps information about the size of actual data in the database to standard output * The only truly useless data counted is printed in the form "N in id indexes" * This does not take database indexes into account. */ public void dumpSizes() throws SQLException, BlockStoreException { maybeConnect(); Statement s = conn.get().createStatement(); long size = 0; long totalSize = 0; int count = 0; ResultSet rs = s.executeQuery(getSelectSettingsDumpSQL()); while (rs.next()) { size += rs.getString(1).length(); size += rs.getBytes(2).length; count++; } rs.close(); System.out.printf(Locale.US, "Settings size: %d, count: %d, average size: %f%n", size, count, (double)size/count); totalSize += size; size = 0; count = 0; rs = s.executeQuery(getSelectHeadersDumpSQL()); while (rs.next()) { size += 28; // hash size += rs.getBytes(1).length; size += 4; // height size += rs.getBytes(2).length; count++; } rs.close(); System.out.printf(Locale.US, "Headers size: %d, count: %d, average size: %f%n", size, count, (double)size/count); totalSize += size; size = 0; count = 0; rs = s.executeQuery(getSelectUndoableblocksDumpSQL()); while (rs.next()) { size += 28; // hash size += 4; // height byte[] txOutChanges = rs.getBytes(1); byte[] transactions = rs.getBytes(2); if (txOutChanges == null) size += transactions.length; else size += txOutChanges.length; // size += the space to represent NULL count++; } rs.close(); System.out.printf(Locale.US, "Undoable Blocks size: %d, count: %d, average size: %f%n", size, count, (double)size/count); totalSize += size; size = 0; count = 0; long scriptSize = 0; rs = s.executeQuery(getSelectopenoutputsDumpSQL()); while (rs.next()) { size += 32; // hash size += 4; // index size += 4; // height size += rs.getBytes(1).length; size += rs.getBytes(2).length; scriptSize += rs.getBytes(2).length; count++; } rs.close(); System.out.printf(Locale.US, "Open Outputs size: %d, count: %d, average size: %f, average script size: %f (%d in id indexes)%n", size, count, (double)size/count, (double)scriptSize/count, count * 8); totalSize += size; System.out.println("Total Size: " + totalSize); s.close(); } }