From fbf62614b46fddb19d5345882506425051c6f29f Mon Sep 17 00:00:00 2001
From: Kalpesh Parmar
Date: Sun, 26 Oct 2014 17:36:09 +0000
Subject: [PATCH] 1) Re-factored PostgresFullPrunedBlockStore and
H2FullPrunedBlockStore into a generic DatabaseFullPrunedBlockStore class. 2)
H2FullPrunedBlockStore now supports calculateBalanceForAddress() method. 3)
Created an additional store for MySQL (MySQLFullPrunedBlockStore) and unit
test. 4) Fixed unit test PostgresFullPrunedBlockStoreTest, second time
execution was failing due to not clearing down the tables correctly. 5) Added
methods getOpenTransactionOutputs() & deleteStore() to base class
DatabaseFullPrunedBlockStore.
---
core/pom.xml | 13 +-
.../store/DatabaseFullPrunedBlockStore.java | 1271 +++++++++++++++++
.../store/H2FullPrunedBlockStore.java | 765 +---------
.../store/MySQLFullPrunedBlockStore.java | 164 +++
.../store/PostgresFullPrunedBlockStore.java | 896 ++----------
.../AbstractFullPrunedBlockChainTest.java | 6 +-
.../core/MySQLFullPrunedBlockChainTest.java | 51 +
.../PostgresFullPrunedBlockChainTest.java | 6 +
8 files changed, 1680 insertions(+), 1492 deletions(-)
create mode 100644 core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java
create mode 100644 core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java
create mode 100644 core/src/test/java/org/bitcoinj/core/MySQLFullPrunedBlockChainTest.java
diff --git a/core/pom.xml b/core/pom.xml
index 504a2a16..acbc3e88 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -441,15 +441,20 @@
scrypt
1.4.0
-
-
-
postgresql
postgresql
9.1-901.jdbc4
+ true
+
+
+
+ mysql
+ mysql-connector-java
+ 5.1.33
+ true
- -->
org.bitcoinj
orchid
diff --git a/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java
new file mode 100644
index 00000000..8c03313f
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/store/DatabaseFullPrunedBlockStore.java
@@ -0,0 +1,1271 @@
+/*
+ * 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.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * A generic full pruned block store for a relational database. This generic class requires
+ * certain table structures for the block store.
+ *
+ * The following are the tables and field names/types that are assumed:-
+ *
+ *
+ * setting table
+ *
+ * Field Name | Type (generic) |
+ * name | string |
+ * value | binary |
+ *
+ *
+ *
+ *
+ * headers table
+ *
+ * Field Name | Type (generic) |
+ * hash | binary |
+ * chainwork | binary |
+ * height | integer |
+ * header | binary |
+ * wasundoable | boolean |
+ *
+ *
+ *
+ *
+ * undoableblocks table
+ *
+ * Field Name | Type (generic) |
+ * hash | binary |
+ * height | integer |
+ * txoutchanges | binary |
+ * transactions | binary |
+ *
+ *
+ *
+ *
+ * openoutputs table
+ *
+ * Field Name | Type (generic) |
+ * hash | binary |
+ * index | integer |
+ * height | integer |
+ * value | binary |
+ * scriptbytes | binary |
+ * toaddress | string |
+ * addresstargetable | integer |
+ *
+ *
+ *
+ */
+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";
+
+ // SQL Queries
+ 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 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) 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 value, scriptBytes FROM openOutputs where toaddress = ?";
+ private static final String SELECT_TRANSACTION_OUTPUTS_WITH_HEIGHT_SQL = "SELECT value, scriptBytes FROM openOutputs where toaddress = ? AND height <= ?";
+
+ protected Sha256Hash chainHeadHash;
+ protected StoredBlock chainHeadBlock;
+ protected Sha256Hash verifiedChainHeadHash;
+ protected StoredBlock verifiedChainHeadBlock;
+ protected NetworkParameters params;
+ protected ThreadLocal conn;
+ protected List allConnections;
+ protected String connectionURL;
+ protected int fullStoreDepth;
+ protected String username;
+ protected String password;
+ protected String schemaName;
+
+ /**
+ * Create a new DatabaseFullPrunedBlockStore, using the full connection URL instead of a hostname and password,
+ * and optionally allowing a schema to be specified.
+ *
+ * @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();
+ this.allConnections = new LinkedList();
+
+ 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 (!tableExists("settings")) {
+ createTables();
+ }
+ initFromDatabase();
+ } catch (SQLException e) {
+ throw new BlockStoreException(e);
+ }
+ }
+
+ /**
+ * Get the database driver class,
+ * i.e org.postgresql.Driver.
+ * @return The fully qualified database driver class.
+ */
+ protected abstract String getDatabaseDriverClass();
+
+ /**
+ * Get the SQL statement that checks is a particular table exists.
+ * @param tableName The name of the table.
+ * @return The SQL select statement.
+ */
+ protected abstract String getTableExistSQL(String tableName);
+
+ /**
+ * Get the SQL statements that create the schema (DDL).
+ * @return The list of SQL statements.
+ */
+ protected abstract List getCreateSchemeSQL();
+
+ /**
+ * Get the SQL statements that create the tables (DDL).
+ * @return The list of SQL statements.
+ */
+ protected abstract List getCreateTablesSQL();
+
+ /**
+ * Get the SQL statements that create the indexes (DDL).
+ * @return The list of SQL statements.
+ */
+ protected abstract List getCreateIndexesSQL();
+
+ /**
+ * Get the database specific error code that indicated a duplicate key error when inserting a record.
+ * This is the code returned by {@link java.sql.SQLException#getSQLState()}
+ * @return The database duplicate error code.
+ */
+ protected abstract String getDuplicateKeyErrorCode();
+
+ /**
+ * Get the SQL to select the total balance for a given address. This has been abstracted as
+ * difference databases will handle casting of return values differently (since values/balances
+ * are stored as bytes within the database for space reasons).
+ * @return The SQL prepared statement.
+ */
+ protected abstract String getBalanceSelectSQL();
+
+ /**
+ * Get the SQL to select the transaction outputs for a given address.
+ * @return The SQL prepared statement.
+ */
+ protected String getTrasactionOutputSelectSQL() {
+ return SELECT_TRANSACTION_OUTPUTS_SQL;
+ }
+
+ /**
+ * Get the SQL to select the transaction outputs for a given address and height seen.
+ * @return The SQL prepared statement.
+ */
+ protected String getTrasactionOutputWithHeightSelectSQL() {
+ return SELECT_TRANSACTION_OUTPUTS_WITH_HEIGHT_SQL;
+ }
+
+ /**
+ * Get the SQL to drop all the tables (DDL).
+ * @return The SQL drop statements.
+ */
+ protected List getDropTablesSQL() {
+ List sqlStatements = new ArrayList();
+ 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;
+ }
+
+ /**
+ * If there isn't a connection on the {@link ThreadLocal} then create and store it.
+ * This will also automatically set up the schema if it does not exist within the DB.
+ * @throws BlockStoreException if successful connection to the DB couldn't be made.
+ */
+ protected synchronized 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();
+ }
+
+ /**
+ * Check if a table exists within the database.
+ * @param table The name of the table.
+ * @return If the table exists.
+ * @throws java.sql.SQLException
+ */
+ private boolean tableExists(String table) throws SQLException {
+ Statement s = conn.get().createStatement();
+ try {
+ ResultSet results = s.executeQuery(getTableExistSQL(table));
+ results.close();
+ return true;
+ } catch (SQLException ex) {
+ return false;
+ } finally {
+ s.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 the reference client inits
+ // its database - the genesis transaction isn't actually in the db so its spent flags can never be updated.
+ List 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 = new Sha256Hash(rs.getBytes(1));
+ Sha256Hash hash = new Sha256Hash(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 = new Sha256Hash(rs.getBytes(1));
+ rs.close();
+ ps.close();
+ this.verifiedChainHeadBlock = get(hash);
+ this.verifiedChainHeadHash = hash;
+ if (this.verifiedChainHeadBlock == null) {
+ throw new BlockStoreException("corrupt databse 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 prodnet) the minimum target has 4 0-bytes
+ byte[] hashBytes = new byte[28];
+ System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
+ s.setBytes(1, hashBytes);
+ s.setBytes(2, storedBlock.getChainWork().toByteArray());
+ s.setInt(3, storedBlock.getHeight());
+ s.setBytes(4, storedBlock.getHeader().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 prodnet) the minimum target has 4 0-bytes
+ byte[] hashBytes = new byte[28];
+ System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, 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 prodnet) the minimum target has 4 0-bytes
+ byte[] hashBytes = new byte[28];
+ System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, 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((int) (0xFF & (numTxn >> 0)));
+ bos.write((int) (0xFF & (numTxn >> 8)));
+ bos.write((int) (0xFF & (numTxn >> 16)));
+ bos.write((int) (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 prodnet) the minimum target has 4 0-bytes
+ byte[] hashBytes = new byte[28];
+ System.arraycopy(hash.getBytes(), 3, 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 = new Block(params, 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 prodnet) the minimum target has 4 0-bytes
+
+ byte[] hashBytes = new byte[28];
+ System.arraycopy(hash.getBytes(), 3, 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) << 0) |
+ ((transactions[offset++] & 0xFF) << 8) |
+ ((transactions[offset++] & 0xFF) << 16) |
+ ((transactions[offset++] & 0xFF) << 24);
+ List transactionList = new LinkedList();
+ for (int i = 0; i < numTxn; i++) {
+ Transaction tx = new Transaction(params, 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 StoredTransactionOutput 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(new BigInteger(results.getBytes(2)).longValue());
+ // Tell the StoredTransactionOutput that we are a coinbase, as that is encoded in height
+ StoredTransactionOutput txout = new StoredTransactionOutput(hash, index, value, height, true, results.getBytes(3));
+ 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(StoredTransactionOutput out) throws BlockStoreException {
+ maybeConnect();
+ PreparedStatement s = null;
+
+ // Calculate the toAddress (if any)
+ String dbAddress = "";
+ int type = 0;
+ Script outputScript = null;
+ try {
+ outputScript = new Script(out.getScriptBytes());
+ }
+ catch (ScriptException e) {
+ // Unparseable, but this isn't an error - it's an output not containing an address
+ log.info("Could not parse script for output: " + out.getHash().toString());
+ }
+ if (outputScript != null && (outputScript.isSentToAddress()
+ || outputScript.isSentToRawPubKey()
+ || outputScript.isPayToScriptHash())) {
+ if (outputScript.isSentToAddress()) {
+ Address targetAddr = new Address(params, outputScript.getPubKeyHash());
+ dbAddress = targetAddr.toString();
+ type = 1;
+ }
+ else if (outputScript.isSentToRawPubKey()) {
+ /*
+ * Note we use the deprecated getFromAddress here. Coinbase outputs seem to have the target address
+ * in the pubkey of the script - perhaps we can rename this function?
+ */
+ dbAddress = outputScript.getFromAddress(params).toString();
+ type = 2;
+ } else {
+ dbAddress = Address.fromP2SHHash(params, outputScript.getPubKeyHash()).toString();
+ type = 3;
+ }
+ }
+
+ 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.setBytes(4, BigInteger.valueOf(out.getValue().value).toByteArray());
+ s.setBytes(5, out.getScriptBytes());
+ s.setString(6, dbAddress);
+ s.setInt(7, type);
+ 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(StoredTransactionOutput 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 StoredTransactionOutput 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");
+ }
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * The balance {@link org.bitcoinj.store.DatabaseFullPrunedBlockStore#getBalanceSelectSQL()} returns
+ * the balance (summed) as an number, then use calculateClientSide=false
+ *
+ * The balance {@link org.bitcoinj.store.DatabaseFullPrunedBlockStore#getBalanceSelectSQL()} returns
+ * the all the openoutputs as stored in the DB (binary), then use calculateClientSide=true
+ *
+ * @param address The address to calculate the balance of
+ * @param calculateClientSide Flag to indicate if the DB returns the raw value/s or the actual balance (summed)
+ * @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.
+ */
+ protected BigInteger calculateBalanceForAddress(Address address, boolean calculateClientSide) 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;
+ while (rs.next()) {
+ if(calculateClientSide) {
+ // The binary data is returned and we calculate the balance. This could be because the DB
+ // doesn't offer a convert function.
+ //balance = balance.add(BigInteger.valueOf(rs.getLong(1)));
+ balance = balance.add(new BigInteger(rs.getBytes(1)));
+ } else {
+ 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");
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculate the balance for a coinbase, to-address, or p2sh address.
+ *
+ * The balance {@link org.bitcoinj.store.DatabaseFullPrunedBlockStore#getBalanceSelectSQL()} returns
+ * the balance (summed) as an number.
+ *
+ * @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 {
+ return calculateBalanceForAddress(address, false);
+ }
+
+ /**
+ * Get the list of {@link org.bitcoinj.core.TransactionOutput}'s for a given address.
+ * @param address The address.
+ * @return The list of transaction outputs.
+ * @throws BlockStoreException If there is an error getting the list of outputs.
+ */
+ public List getOpenTransactionOutputs(Address address) throws BlockStoreException {
+ return getOpenTransactionOutputs(address, null);
+ }
+
+ /**
+ * Get the list of {@link org.bitcoinj.core.TransactionOutput}'s for a given address and a specified height.
+ * @param address The address.
+ * @param maxHeight The maximum block height of this tx output (inclusive).
+ * @return The list of transaction outputs.
+ * @throws BlockStoreException If there is an error getting the list of outputs.
+ */
+ public List getOpenTransactionOutputs(Address address, @Nullable Integer maxHeight) throws BlockStoreException {
+ maybeConnect();
+ PreparedStatement s = null;
+ List outputs = new ArrayList();
+ try {
+ if(maxHeight != null) {
+ s = conn.get().prepareStatement(getTrasactionOutputWithHeightSelectSQL());
+ s.setInt(2, maxHeight);
+ } else {
+ s = conn.get().prepareStatement(getTrasactionOutputSelectSQL());
+ }
+ s.setString(1, address.toString());
+ ResultSet rs = s.executeQuery();
+ while (rs.next()) {
+ Coin amount = Coin.valueOf((new BigInteger(rs.getBytes(1)).longValue()));
+ byte[] scriptBytes = rs.getBytes(2);
+ TransactionOutput output = new TransactionOutput(params, null, amount, scriptBytes);
+ outputs.add(output);
+ }
+ return outputs;
+ } catch (SQLException ex) {
+ throw new BlockStoreException(ex);
+ } finally {
+ if (s != null) {
+ try {
+ s.close();
+ } catch (SQLException e) {
+ throw new BlockStoreException("Could not close statement");
+ }
+ }
+ }
+ }
+
+ /**
+ * 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("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("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("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("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();
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/store/H2FullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/H2FullPrunedBlockStore.java
index 2795d150..92e26894 100644
--- a/core/src/main/java/org/bitcoinj/store/H2FullPrunedBlockStore.java
+++ b/core/src/main/java/org/bitcoinj/store/H2FullPrunedBlockStore.java
@@ -1,5 +1,6 @@
/*
* Copyright 2012 Matt Corallo.
+ * 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.
@@ -17,74 +18,71 @@
package org.bitcoinj.store;
import org.bitcoinj.core.*;
-import com.google.common.collect.Lists;
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.LinkedList;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
// Originally written for Apache Derby, but its DELETE (and general) performance was awful
/**
* A full pruned block store using the H2 pure-java embedded database.
- *
+ *
* Note that because of the heavy delete load on the database, during IBD,
* you may see the database files grow quite large (around 1.5G).
* H2 automatically frees some space at shutdown, so close()ing the database
* decreases the space usage somewhat (to only around 1.3G).
*/
-public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
+public class H2FullPrunedBlockStore extends DatabaseFullPrunedBlockStore {
private static final Logger log = LoggerFactory.getLogger(H2FullPrunedBlockStore.class);
- private Sha256Hash chainHeadHash;
- private StoredBlock chainHeadBlock;
- private Sha256Hash verifiedChainHeadHash;
- private StoredBlock verifiedChainHeadBlock;
- private NetworkParameters params;
- private ThreadLocal conn;
- private List allConnections;
- private String connectionURL;
- private int fullStoreDepth;
+ private static final String H2_DUPLICATE_KEY_ERROR_CODE = "23505";
+ private static final String DATABASE_DRIVER_CLASS = "org.h2.Driver";
+ private static final String DATABASE_CONNECTION_URL_PREFIX = "jdbc:h2:";
- static final String driver = "org.h2.Driver";
- static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings ( "
- + "name VARCHAR(32) NOT NULL CONSTRAINT settings_pk PRIMARY KEY,"
- + "value BLOB"
- + ")";
- static final String CHAIN_HEAD_SETTING = "chainhead";
- static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead";
- static final String VERSION_SETTING = "version";
+ // create table SQL
+ private static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings ( "
+ + "name VARCHAR(32) NOT NULL CONSTRAINT settings_pk PRIMARY KEY,"
+ + "value BLOB"
+ + ")";
- static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers ( "
- + "hash BINARY(28) NOT NULL CONSTRAINT headers_pk PRIMARY KEY,"
- + "chainWork BLOB NOT NULL,"
- + "height INT NOT NULL,"
- + "header BLOB NOT NULL,"
- + "wasUndoable BOOL NOT NULL"
- + ")";
-
- static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableBlocks ( "
- + "hash BINARY(28) NOT NULL CONSTRAINT undoableBlocks_pk PRIMARY KEY,"
- + "height INT NOT NULL,"
- + "txOutChanges BLOB,"
- + "transactions BLOB"
- + ")";
- static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX heightIndex ON undoableBlocks (height)";
-
- static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openOutputs ("
- + "hash BINARY(32) NOT NULL,"
- + "index INT NOT NULL,"
- + "height INT NOT NULL,"
- + "value BLOB NOT NULL,"
- + "scriptBytes BLOB NOT NULL,"
- + "PRIMARY KEY (hash, index),"
- + ")";
+ private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers ( "
+ + "hash BINARY(28) NOT NULL CONSTRAINT headers_pk PRIMARY KEY,"
+ + "chainWork BLOB NOT NULL,"
+ + "height INT NOT NULL,"
+ + "header BLOB NOT NULL,"
+ + "wasUndoable BOOL NOT NULL"
+ + ")";
+
+ private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableBlocks ( "
+ + "hash BINARY(28) NOT NULL CONSTRAINT undoableBlocks_pk PRIMARY KEY,"
+ + "height INT NOT NULL,"
+ + "txOutChanges BLOB,"
+ + "transactions BLOB"
+ + ")";
+
+ private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openOutputs ("
+ + "hash BINARY(32) NOT NULL,"
+ + "index INT NOT NULL,"
+ + "height INT NOT NULL,"
+ + "value BLOB NOT NULL,"
+ + "scriptBytes BLOB NOT NULL,"
+ + "toaddress VARCHAR(35),"
+ + "addresstargetable INT,"
+ + "PRIMARY KEY (hash, index),"
+ + ")";
+
+ // Some indexes to speed up inserts
+ private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_height_toaddress_idx ON openoutputs (hash, index, height, toaddress)";
+ private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs (toaddress)";
+ private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs (addresstargetable)";
+ private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs (hash)";
+ private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks (height)";
+
+ private static final String SELECT_BALANCE_SQL = "SELECT value from openoutputs where toaddress = ?";
/**
* Creates a new H2FullPrunedBlockStore
@@ -94,34 +92,9 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
* @throws BlockStoreException if the database fails to open for any reason
*/
public H2FullPrunedBlockStore(NetworkParameters params, String dbName, int fullStoreDepth) throws BlockStoreException {
- this.params = params;
- this.fullStoreDepth = fullStoreDepth;
- // We choose a very lax timeout to avoid the database throwing exceptions on complex operations, as time is not
- // a particularly precious resource when just keeping up with the chain.
- connectionURL = "jdbc:h2:" + dbName + ";create=true;LOCK_TIMEOUT=60000";
-
- conn = new ThreadLocal();
- allConnections = new LinkedList();
-
- try {
- Class.forName(driver);
- log.info(driver + " loaded. ");
- } catch (java.lang.ClassNotFoundException e) {
- log.error("check CLASSPATH for H2 jar ", e);
- }
-
- maybeConnect();
-
- try {
- // Create tables if needed
- if (!tableExists("settings"))
- createTables();
- initFromDatabase();
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
+ super(params, DATABASE_CONNECTION_URL_PREFIX + dbName + ";create=true;LOCK_TIMEOUT=60000;DB_CLOSE_ON_EXIT=FALSE", fullStoreDepth, null, null, null);
}
-
+
/**
* Creates a new H2FullPrunedBlockStore with the given cache size
* @param params A copy of the NetworkParameters used
@@ -134,7 +107,6 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
*/
public H2FullPrunedBlockStore(NetworkParameters params, String dbName, int fullStoreDepth, int cacheSize) throws BlockStoreException {
this(params, dbName, fullStoreDepth);
-
try {
Statement s = conn.get().createStatement();
s.executeUpdate("SET CACHE_SIZE " + cacheSize);
@@ -143,636 +115,57 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
throw new BlockStoreException(e);
}
}
-
- private synchronized void maybeConnect() throws BlockStoreException {
- try {
- if (conn.get() != null)
- return;
-
- conn.set(DriverManager.getConnection(connectionURL));
- allConnections.add(conn.get());
- 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 {
- conn.rollback();
- conn.close();
- } catch (SQLException ex) {
- throw new RuntimeException(ex);
- }
- }
- allConnections.clear();
- }
-
- public void resetStore() throws BlockStoreException {
- maybeConnect();
- try {
- Statement s = conn.get().createStatement();
- s.executeUpdate("DROP TABLE settings");
- s.executeUpdate("DROP TABLE headers");
- s.executeUpdate("DROP TABLE undoableBlocks");
- s.executeUpdate("DROP TABLE openOutputs");
- s.close();
- createTables();
- initFromDatabase();
- } catch (SQLException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- private void createTables() throws SQLException, BlockStoreException {
- Statement s = conn.get().createStatement();
- log.debug("H2FullPrunedBlockStore : CREATE headers table");
- s.executeUpdate(CREATE_HEADERS_TABLE);
-
- log.debug("H2FullPrunedBlockStore : CREATE settings table");
- s.executeUpdate(CREATE_SETTINGS_TABLE);
-
- log.debug("H2FullPrunedBlockStore : CREATE undoable block table");
- s.executeUpdate(CREATE_UNDOABLE_TABLE);
-
- log.debug("H2FullPrunedBlockStore : CREATE undoable block index");
- s.executeUpdate(CREATE_UNDOABLE_TABLE_INDEX);
-
- log.debug("H2FullPrunedBlockStore : CREATE open output table");
- s.executeUpdate(CREATE_OPEN_OUTPUT_TABLE);
-
- s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + CHAIN_HEAD_SETTING + "', NULL)");
- s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERIFIED_CHAIN_HEAD_SETTING + "', NULL)");
- s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERSION_SETTING + "', '03')");
- s.close();
- createNewStore(params);
- }
-
- private void initFromDatabase() throws SQLException, BlockStoreException {
- Statement s = conn.get().createStatement();
- ResultSet rs = s.executeQuery("SHOW TABLES");
- while (rs.next())
- if (rs.getString(1).equalsIgnoreCase("openOutputsIndex"))
- throw new BlockStoreException("Attempted to open a H2 database with an old schema, please reset database.");
-
- rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + CHAIN_HEAD_SETTING + "'");
- if (!rs.next()) {
- throw new BlockStoreException("corrupt H2 block store - no chain head pointer");
- }
- Sha256Hash hash = new Sha256Hash(rs.getBytes(1));
- rs.close();
- this.chainHeadBlock = get(hash);
- this.chainHeadHash = hash;
- if (this.chainHeadBlock == null)
- {
- throw new BlockStoreException("corrupt H2 block store - head block not found");
- }
-
- rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + VERIFIED_CHAIN_HEAD_SETTING + "'");
- if (!rs.next()) {
- throw new BlockStoreException("corrupt H2 block store - no verified chain head pointer");
- }
- hash = new Sha256Hash(rs.getBytes(1));
- rs.close();
- s.close();
- this.verifiedChainHeadBlock = get(hash);
- this.verifiedChainHeadHash = hash;
- if (this.verifiedChainHeadBlock == null)
- {
- throw new BlockStoreException("corrupt H2 block store - verified head block not found");
- }
- }
-
- 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 the reference client inits
- // its database - the genesis transaction isn't actually in the db so its spent flags can never be updated.
- List 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.
- }
- }
-
- private boolean tableExists(String table) throws SQLException {
- Statement s = conn.get().createStatement();
- try {
- ResultSet results = s.executeQuery("SELECT * FROM " + table + " WHERE 1 = 2");
- results.close();
- return true;
- } catch (SQLException ex) {
- return false;
- } finally {
- s.close();
- }
- }
-
- /**
- * 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("SELECT name, value FROM settings");
- while (rs.next()) {
- size += rs.getString(1).length();
- size += rs.getBytes(2).length;
- count++;
- }
- rs.close();
- System.out.printf("Settings size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
-
- totalSize += size; size = 0; count = 0;
- rs = s.executeQuery("SELECT chainWork, header FROM headers");
- 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("Headers size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
-
- totalSize += size; size = 0; count = 0;
- rs = s.executeQuery("SELECT txOutChanges, transactions FROM undoableBlocks");
- 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("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("SELECT value, scriptBytes FROM openOutputs");
- 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("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();
- }
-
-
- private void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) throws SQLException {
- try {
- PreparedStatement s =
- conn.get().prepareStatement("INSERT INTO headers(hash, chainWork, height, header, wasUndoable)"
- + " VALUES(?, ?, ?, ?, ?)");
- // 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(), 3, hashBytes, 0, 28);
- s.setBytes(1, hashBytes);
- s.setBytes(2, storedBlock.getChainWork().toByteArray());
- s.setInt(3, storedBlock.getHeight());
- s.setBytes(4, storedBlock.getHeader().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.getErrorCode() != 23505 || !wasUndoable)
- throw e;
-
- PreparedStatement s = conn.get().prepareStatement("UPDATE headers SET wasUndoable=? WHERE hash=?");
- 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(), 3, hashBytes, 0, 28);
- s.setBytes(2, hashBytes);
- s.executeUpdate();
- s.close();
- }
+ public BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException {
+ return calculateBalanceForAddress(address, true);
}
@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(), 3, 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((int) (0xFF & (numTxn >> 0)));
- bos.write((int) (0xFF & (numTxn >> 8)));
- bos.write((int) (0xFF & (numTxn >> 16)));
- bos.write((int) (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("INSERT INTO undoableBlocks(hash, height, txOutChanges, transactions)"
- + " VALUES(?, ?, ?, ?)");
- s.setBytes(1, hashBytes);
- s.setInt(2, height);
- if (transactions == null) {
- s.setBytes(3, txOutChanges);
- s.setNull(4, Types.BLOB);
- } else {
- s.setNull(3, Types.BLOB);
- s.setBytes(4, transactions);
- }
- s.executeUpdate();
- s.close();
- try {
- putUpdateStoredBlock(storedBlock, true);
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
- } catch (SQLException e) {
- if (e.getErrorCode() != 23505)
- 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("UPDATE undoableBlocks SET txOutChanges=?, transactions=?"
- + " WHERE hash = ?");
- s.setBytes(3, hashBytes);
- if (transactions == null) {
- s.setBytes(1, txOutChanges);
- s.setNull(2, Types.BLOB);
- } else {
- s.setNull(1, Types.BLOB);
- s.setBytes(2, transactions);
- }
- s.executeUpdate();
- s.close();
- }
- } catch (SQLException ex) {
- throw new BlockStoreException(ex);
- }
- }
-
- @Nullable
- 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("SELECT chainWork, height, header, wasUndoable FROM headers WHERE hash = ?");
- // 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(), 3, 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 = new Block(params, results.getBytes(3));
- b.verifyHeader();
- return new StoredBlock(b, chainWork, height);
- } 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
- @Nullable
- public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
- return get(hash, false);
- }
-
- @Override
- @Nullable
- public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException {
- return get(hash, true);
- }
-
- @Override
- @Nullable
- public StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException {
- maybeConnect();
- PreparedStatement s = null;
- try {
- s = conn.get()
- .prepareStatement("SELECT txOutChanges, transactions FROM undoableBlocks WHERE hash = ?");
- // 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(), 3, 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) << 0) |
- ((transactions[offset++] & 0xFF) << 8) |
- ((transactions[offset++] & 0xFF) << 16) |
- ((transactions[offset++] & 0xFF) << 24);
- List transactionList = new LinkedList();
- for (int i = 0; i < numTxn; i++) {
- Transaction tx = new Transaction(params, 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"); }
- }
+ protected String getDuplicateKeyErrorCode() {
+ return H2_DUPLICATE_KEY_ERROR_CODE;
}
@Override
- public StoredBlock getChainHead() throws BlockStoreException {
- return chainHeadBlock;
+ protected List getCreateTablesSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add(CREATE_SETTINGS_TABLE);
+ sqlStatements.add(CREATE_HEADERS_TABLE);
+ sqlStatements.add(CREATE_UNDOABLE_TABLE);
+ sqlStatements.add(CREATE_OPEN_OUTPUT_TABLE);
+ return sqlStatements;
}
@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("UPDATE settings SET value = ? WHERE name = ?");
- 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;
+ protected List getCreateIndexesSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add(CREATE_UNDOABLE_TABLE_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_ADDRESS_MULTI_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_HASH_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_TOADDRESS_INDEX);
+ return sqlStatements;
}
@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("UPDATE settings SET value = ? WHERE name = ?");
- 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("DELETE FROM undoableBlocks WHERE height <= ?");
- s.setInt(1, height);
- s.executeUpdate();
- s.close();
- } catch (SQLException ex) {
- throw new BlockStoreException(ex);
- }
+ protected List getCreateSchemeSQL() {
+ // do nothing
+ return Collections.emptyList();
}
@Override
- @Nullable
- public StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException {
- maybeConnect();
- PreparedStatement s = null;
- try {
- s = conn.get()
- .prepareStatement("SELECT height, value, scriptBytes FROM openOutputs " +
- "WHERE hash = ? AND index = ?");
- 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(new BigInteger(results.getBytes(2)).longValue());
- // Tell the StoredTransactionOutput that we are a coinbase, as that is encoded in height
- return new StoredTransactionOutput(hash, index, value, height, true, results.getBytes(3));
- } catch (SQLException ex) {
- throw new BlockStoreException(ex);
- } finally {
- if (s != null)
- try {
- s.close();
- } catch (SQLException e) { throw new BlockStoreException("Failed to close PreparedStatement"); }
- }
+ protected String getTableExistSQL(String tableName) {
+ return "SELECT * FROM " + tableName + " WHERE 1 = 2";
}
@Override
- public void addUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
- maybeConnect();
- PreparedStatement s = null;
- try {
- s = conn.get().prepareStatement("INSERT INTO openOutputs (hash, index, height, value, scriptBytes) " +
- "VALUES (?, ?, ?, ?, ?)");
- s.setBytes(1, out.getHash().getBytes());
- // index is actually an unsigned int
- s.setInt(2, (int)out.getIndex());
- s.setInt(3, out.getHeight());
- s.setBytes(4, BigInteger.valueOf(out.getValue().value).toByteArray());
- s.setBytes(5, out.getScriptBytes());
- s.executeUpdate();
- s.close();
- } catch (SQLException e) {
- if (e.getErrorCode() != 23505)
- throw new BlockStoreException(e);
- } finally {
- if (s != null)
- try {
- s.close();
- } catch (SQLException e) { throw new BlockStoreException(e); }
- }
+ protected String getDatabaseDriverClass() {
+ return DATABASE_DRIVER_CLASS;
}
@Override
- public void removeUnspentTransactionOutput(StoredTransactionOutput out) throws BlockStoreException {
- maybeConnect();
- try {
- PreparedStatement s = conn.get()
- .prepareStatement("DELETE FROM openOutputs WHERE hash = ? AND index = ?");
- s.setBytes(1, out.getHash().getBytes());
- // index is actually an unsigned int
- s.setInt(2, (int)out.getIndex());
- s.executeUpdate();
- int updateCount = s.getUpdateCount();
- s.close();
- if (updateCount == 0)
- throw new BlockStoreException("Tried to remove a StoredTransactionOutput from H2FullPrunedBlockStore that it didn't have!");
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
+ protected String getBalanceSelectSQL() {
+ return SELECT_BALANCE_SQL;
}
- @Override
- public void beginDatabaseBatchWrite() throws BlockStoreException {
- maybeConnect();
- try {
- conn.get().setAutoCommit(false);
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
- }
-
- @Override
- public void commitDatabaseBatchWrite() throws BlockStoreException {
- maybeConnect();
- try {
- conn.get().commit();
- conn.get().setAutoCommit(true);
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
- }
-
- @Override
- public void abortDatabaseBatchWrite() throws BlockStoreException {
- maybeConnect();
- try {
- conn.get().rollback();
- conn.get().setAutoCommit(true);
- } 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("SELECT COUNT(*) FROM openOutputs WHERE hash = ?");
- 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"); }
- }
- }
-}
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java
new file mode 100644
index 00000000..ad3ae308
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/store/MySQLFullPrunedBlockStore.java
@@ -0,0 +1,164 @@
+/*
+ * 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 org.bitcoinj.core.NetworkParameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A full pruned block store using the MySQL database engine. As an added bonus an address index is calculated,
+ * so you can use {@link #calculateBalanceForAddress(org.bitcoinj.core.Address)} to quickly look up
+ * the quantity of bitcoins controlled by that address.
+ */
+public class MySQLFullPrunedBlockStore extends DatabaseFullPrunedBlockStore {
+ private static final Logger log = LoggerFactory.getLogger(MySQLFullPrunedBlockStore.class);
+
+ private static final String MYSQL_DUPLICATE_KEY_ERROR_CODE = "23000";
+ private static final String DATABASE_DRIVER_CLASS = "com.mysql.jdbc.Driver";
+ private static final String DATABASE_CONNECTION_URL_PREFIX = "jdbc:mysql://";
+
+ // create table SQL
+ private static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings (\n" +
+ " name varchar(32) NOT NULL,\n" +
+ " value blob,\n" +
+ " CONSTRAINT `setting_pk` PRIMARY KEY (name)\n" +
+ ")\n";
+
+ private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers (\n" +
+ " hash blob NOT NULL,\n" +
+ " chainwork mediumblob NOT NULL,\n" +
+ " height integer NOT NULL,\n" +
+ " header blob NOT NULL,\n" +
+ " wasundoable tinyint(1) NOT NULL,\n" +
+ " CONSTRAINT `headers_pk` PRIMARY KEY (hash(28)) USING btree\n" +
+ ")";
+
+ private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableblocks (\n" +
+ " hash blob NOT NULL,\n" +
+ " height integer NOT NULL,\n" +
+ " txoutchanges mediumblob,\n" +
+ " transactions blob,\n" +
+ " CONSTRAINT `undoableblocks_pk` PRIMARY KEY (hash(28)) USING btree\n" +
+ ")\n";
+
+ private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openoutputs (\n" +
+ " hash blob NOT NULL,\n" +
+ " `index` integer NOT NULL,\n" +
+ " height integer NOT NULL,\n" +
+ " value blob NOT NULL,\n" +
+ " scriptbytes mediumblob NOT NULL,\n" +
+ " toaddress varchar(35),\n" +
+ " addresstargetable integer,\n" +
+ " CONSTRAINT `openoutputs_pk` PRIMARY KEY (hash(32),`index`) USING btree\n" +
+ ")\n";
+
+ // Some indexes to speed up inserts
+ private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_height_toaddress_idx ON openoutputs (hash(32), `index`, height, toaddress) USING btree";
+ private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs (toaddress) USING btree";
+ private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs (addresstargetable) USING btree";
+ private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs (hash(32)) USING btree";
+ private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks (height) USING btree";
+
+ // SQL involving index column (table openOutputs) overridden as it is a reserved word and must be back ticked in MySQL.
+ private static final String SELECT_OPENOUTPUTS_SQL = "SELECT height, value, scriptBytes FROM openOutputs WHERE hash = ? AND `index` = ?";
+ private static final String INSERT_OPENOUTPUTS_SQL = "INSERT INTO openOutputs (hash, `index`, height, value, scriptBytes, toAddress, addressTargetable) VALUES (?, ?, ?, ?, ?, ?, ?)";
+ private static final String DELETE_OPENOUTPUTS_SQL = "DELETE FROM openOutputs WHERE hash = ? AND `index` = ?";
+
+ private static final String SELECT_BALANCE_SQL = "SELECT sum(conv(hex(value),16,10)) from openoutputs where toaddress = ?";
+
+ /**
+ * Creates a new MySQLFullPrunedBlockStore.
+ *
+ * @param params A copy of the NetworkParameters used
+ * @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe)
+ * @param hostname The hostname of the database to connect to
+ * @param dbName The database to connect to
+ * @param username The database username
+ * @param password The password to the database
+ * @throws BlockStoreException if the database fails to open for any reason
+ */
+ public MySQLFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth, String hostname, String dbName,
+ String username, String password) throws BlockStoreException {
+ super(params, DATABASE_CONNECTION_URL_PREFIX + hostname + "/" + dbName, fullStoreDepth, username, password, null);
+ }
+
+ @Override
+ protected String getDuplicateKeyErrorCode() {
+ return MYSQL_DUPLICATE_KEY_ERROR_CODE;
+ }
+
+ @Override
+ protected String getSelectOpenoutputsSQL() {
+ return SELECT_OPENOUTPUTS_SQL;
+ }
+
+ @Override
+ protected String getInsertOpenoutputsSQL() {
+ return INSERT_OPENOUTPUTS_SQL;
+ }
+
+ @Override
+ protected String getDeleteOpenoutputsSQL() {
+ return DELETE_OPENOUTPUTS_SQL;
+ }
+
+ @Override
+ protected List getCreateTablesSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add(CREATE_SETTINGS_TABLE);
+ sqlStatements.add(CREATE_HEADERS_TABLE);
+ sqlStatements.add(CREATE_UNDOABLE_TABLE);
+ sqlStatements.add(CREATE_OPEN_OUTPUT_TABLE);
+ return sqlStatements;
+ }
+
+ @Override
+ protected List getCreateIndexesSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add(CREATE_UNDOABLE_TABLE_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_ADDRESS_MULTI_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_HASH_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_TOADDRESS_INDEX);
+ return sqlStatements;
+ }
+
+ @Override
+ protected List getCreateSchemeSQL() {
+ // do nothing
+ return Collections.emptyList();
+ }
+
+ @Override
+ protected String getTableExistSQL(String tableName) {
+ return "SELECT * FROM " + tableName + " WHERE 1 = 2";
+ }
+
+ @Override
+ protected String getDatabaseDriverClass() {
+ return DATABASE_DRIVER_CLASS;
+ }
+
+ @Override
+ protected String getBalanceSelectSQL() {
+ return SELECT_BALANCE_SQL;
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/store/PostgresFullPrunedBlockStore.java b/core/src/main/java/org/bitcoinj/store/PostgresFullPrunedBlockStore.java
index 41272782..a643663e 100644
--- a/core/src/main/java/org/bitcoinj/store/PostgresFullPrunedBlockStore.java
+++ b/core/src/main/java/org/bitcoinj/store/PostgresFullPrunedBlockStore.java
@@ -1,6 +1,7 @@
/*
* 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.
@@ -14,90 +15,79 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.bitcoinj.store;
import org.bitcoinj.core.*;
-import org.bitcoinj.script.Script;
-import com.google.common.collect.Lists;
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.LinkedList;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
import java.util.List;
-import java.util.Properties;
/**
* A full pruned block store using the Postgres database engine. As an added bonus an address index is calculated,
* so you can use {@link #calculateBalanceForAddress(org.bitcoinj.core.Address)} to quickly look up
* the quantity of bitcoins controlled by that address.
*/
-public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
+public class PostgresFullPrunedBlockStore extends DatabaseFullPrunedBlockStore {
private static final Logger log = LoggerFactory.getLogger(PostgresFullPrunedBlockStore.class);
+
private static final String POSTGRES_DUPLICATE_KEY_ERROR_CODE = "23505";
+ private static final String DATABASE_DRIVER_CLASS = "org.postgresql.Driver";
+ private static final String DATABASE_CONNECTION_URL_PREFIX = "jdbc:postgresql://";
- private Sha256Hash chainHeadHash;
- private StoredBlock chainHeadBlock;
- private Sha256Hash verifiedChainHeadHash;
- private StoredBlock verifiedChainHeadBlock;
- private NetworkParameters params;
- private ThreadLocal conn;
- private List allConnections;
- private String connectionURL;
- private int fullStoreDepth;
- private String username;
- private String password;
- private String schemaName;
-
- private static final String driver = "org.postgresql.Driver";
+ // create table SQL
private static final String CREATE_SETTINGS_TABLE = "CREATE TABLE settings (\n" +
" name character varying(32) NOT NULL,\n" +
- " value bytea\n" +
- ");";
- private static final String CHAIN_HEAD_SETTING = "chainhead";
- private static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead";
- private static final String VERSION_SETTING = "version";
+ " value bytea,\n" +
+ " CONSTRAINT setting_pk PRIMARY KEY (name)\n" +
+ ")\n";
- private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers (" +
- " hash bytea NOT NULL," +
- " chainwork bytea NOT NULL," +
- " height integer NOT NULL," +
- " header bytea NOT NULL," +
- " wasundoable boolean NOT NULL" +
- ");";
+ private static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers (\n" +
+ " hash bytea NOT NULL,\n" +
+ " chainwork bytea NOT NULL,\n" +
+ " height integer NOT NULL,\n" +
+ " header bytea NOT NULL,\n" +
+ " wasundoable boolean NOT NULL,\n" +
+ " CONSTRAINT headers_pk PRIMARY KEY (hash)\n" +
+ ")\n";
- private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableblocks (" +
- " hash bytea NOT NULL," +
- " height integer NOT NULL," +
- " txoutchanges bytea," +
- " transactions bytea" +
- ");";
- private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openoutputs (" +
- " hash bytea NOT NULL," +
- " index integer NOT NULL," +
- " height integer NOT NULL," +
- " value bytea NOT NULL," +
- " scriptbytes bytea NOT NULL," +
- " toaddress character varying(35)," +
- " addresstargetable integer" +
- ");";
+ private static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableblocks (\n" +
+ " hash bytea NOT NULL,\n" +
+ " height integer NOT NULL,\n" +
+ " txoutchanges bytea,\n" +
+ " transactions bytea,\n" +
+ " CONSTRAINT undoableblocks_pk PRIMARY KEY (hash)\n" +
+ ")\n";
- private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX heightIndex ON undoableBlocks (height)";
+ private static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openoutputs (\n" +
+ " hash bytea NOT NULL,\n" +
+ " index integer NOT NULL,\n" +
+ " height integer NOT NULL,\n" +
+ " value bytea NOT NULL,\n" +
+ " scriptbytes bytea NOT NULL,\n" +
+ " toaddress character varying(35),\n" +
+ " addresstargetable integer,\n" +
+ " CONSTRAINT openoutputs_pk PRIMARY KEY (hash,index)\n" +
+ ")\n";
// Some indexes to speed up inserts
- private static final String CREATE_HEADERS_HASH_INDEX = "CREATE INDEX headershashindex ON headers USING btree (hash);";
- private static final String CREATE_OUTPUTS_ADDRESS_INDEX = "CREATE INDEX idx_address ON openoutputs USING btree (hash, index, height, toaddress);";
- private static final String CREATE_OUTPUT_ADDRESS_TYPE_INDEX = "CREATE INDEX idx_addresstargetable ON openoutputs USING btree (addresstargetable);";
- private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputshash ON openoutputs USING btree (hash);";
- private static final String CREATE_OUTPUTS_HASH_INDEX_INDEX = "CREATE INDEX openoutputshashindex ON openoutputs USING btree (hash, index);";
- private static final String CREATE_UNDOABLE_HASH_INDEX = "CREATE INDEX undoableblockshashindex ON undoableblocks USING btree (hash);";
+ private static final String CREATE_OUTPUTS_ADDRESS_MULTI_INDEX = "CREATE INDEX openoutputs_hash_index_num_height_toaddress_idx ON openoutputs USING btree (hash, index, height, toaddress)";
+ private static final String CREATE_OUTPUTS_TOADDRESS_INDEX = "CREATE INDEX openoutputs_toaddress_idx ON openoutputs USING btree (toaddress)";
+ private static final String CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX = "CREATE INDEX openoutputs_addresstargetable_idx ON openoutputs USING btree (addresstargetable)";
+ private static final String CREATE_OUTPUTS_HASH_INDEX = "CREATE INDEX openoutputs_hash_idx ON openoutputs USING btree (hash)";
+ private static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX undoableblocks_height_idx ON undoableBlocks USING btree (height)";
+ private static final String SELECT_UNDOABLEBLOCKS_EXISTS_SQL = "select 1 from undoableBlocks where hash = ?";
+
+ private static final String SELECT_BALANCE_SQL = "select sum(('x'||lpad(substr(value::text, 3, 50),16,'0'))::bit(64)::bigint) from openoutputs where toaddress = ?";
/**
* Creates a new PostgresFullPrunedBlockStore.
@@ -112,7 +102,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
*/
public PostgresFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth, String hostname, String dbName,
String username, String password) throws BlockStoreException {
- this(params, "jdbc:postgresql://" + hostname + "/" + dbName, fullStoreDepth, username, password, null);
+ super(params, DATABASE_CONNECTION_URL_PREFIX + hostname + "/" + dbName, fullStoreDepth, username, password, null);
}
/**
@@ -133,340 +123,62 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
*/
public PostgresFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth, String hostname, String dbName,
String username, String password, @Nullable String schemaName) throws BlockStoreException {
- this(params, "jdbc:postgresql://" + hostname + "/" + dbName, fullStoreDepth, username, password, schemaName);
- }
-
- /**
- * Create a new PostgresFullPrunedBlockStore, using the full connection URL instead of a hostname and password,
- * and optionally allowing a schema to be specified.
- *
- * The connection URL will be passed to the database driver, and should look like
- * "jdbc:postrgresql://host[:port]/databasename". You can use this to change the port, or specify additional
- * parameters. See
- * the PostgreSQL JDBC documentation for more on the connection URL.
- *
- * This constructor also accepts a schema name to use, which can be used to avoid name collisions, or to keep the
- * database organized. If no schema is provided the default schema for the username will be used. See
- * the postgres schema docs for more on
- * schemas.
- *
- *
- * @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 the database fails to open for any reason.
- */
- public PostgresFullPrunedBlockStore(NetworkParameters params, String connectionURL, int fullStoreDepth,
- String username, 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;
-
- conn = new ThreadLocal();
- allConnections = new LinkedList();
-
- try {
- Class.forName(driver);
- log.info(driver + " loaded. ");
- } catch (java.lang.ClassNotFoundException e) {
- log.error("check CLASSPATH for Postgres jar ", e);
- }
-
- maybeConnect();
-
- try {
- // Create tables if needed
- if (!tableExists("settings"))
- createTables();
- initFromDatabase();
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
- }
-
-
-
- private synchronized void maybeConnect() throws BlockStoreException {
- try {
- if (conn.get() != null && !conn.get().isClosed())
- return;
-
- Properties props = new Properties();
- props.setProperty("user", this.username);
- props.setProperty("password", this.password);
-
- conn.set(DriverManager.getConnection(connectionURL, props));
-
- Connection connection = conn.get();
- // set the schema if one is needed
- if(schemaName != null) {
- Statement s = connection.createStatement();
- s.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName + ";");
- s.execute("set search_path to '" + schemaName +"';");
- }
- allConnections.add(conn.get());
- log.info("Made a new connection to database " + connectionURL);
- } catch (SQLException ex) {
- throw new BlockStoreException(ex);
- }
+ super(params, DATABASE_CONNECTION_URL_PREFIX + hostname + "/" + dbName, fullStoreDepth, username, password, schemaName);
}
@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();
- }
-
- public void resetStore() throws BlockStoreException {
- maybeConnect();
- try {
- Statement s = conn.get().createStatement();
- s.execute("DROP TABLE settings");
- s.execute("DROP TABLE headers");
- s.execute("DROP TABLE undoableBlocks");
- s.execute("DROP TABLE openOutputs");
- s.close();
- createTables();
- initFromDatabase();
- } catch (SQLException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- private void createTables() throws SQLException, BlockStoreException {
- Statement s = conn.get().createStatement();
- if (log.isDebugEnabled())
- log.debug("PostgresFullPrunedBlockStore : CREATE headers table");
- s.executeUpdate(CREATE_HEADERS_TABLE);
-
- if (log.isDebugEnabled())
- log.debug("PostgresFullPrunedBlockStore : CREATE settings table");
- s.executeUpdate(CREATE_SETTINGS_TABLE);
-
- if (log.isDebugEnabled())
- log.debug("PostgresFullPrunedBlockStore : CREATE undoable block table");
- s.executeUpdate(CREATE_UNDOABLE_TABLE);
-
- if (log.isDebugEnabled())
- log.debug("PostgresFullPrunedBlockStore : CREATE undoable block index");
- s.executeUpdate(CREATE_UNDOABLE_TABLE_INDEX);
- if (log.isDebugEnabled())
- log.debug("PostgresFullPrunedBlockStore : CREATE open output table");
- s.executeUpdate(CREATE_OPEN_OUTPUT_TABLE);
-
- // Create indexes..
- s.executeUpdate(CREATE_HEADERS_HASH_INDEX);
- s.executeUpdate(CREATE_OUTPUT_ADDRESS_TYPE_INDEX);
- s.executeUpdate(CREATE_OUTPUTS_ADDRESS_INDEX);
- s.executeUpdate(CREATE_OUTPUTS_HASH_INDEX);
- s.executeUpdate(CREATE_OUTPUTS_HASH_INDEX_INDEX);
- s.executeUpdate(CREATE_UNDOABLE_HASH_INDEX);
-
-
- s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + CHAIN_HEAD_SETTING + "', NULL)");
- s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERIFIED_CHAIN_HEAD_SETTING + "', NULL)");
- s.executeUpdate("INSERT INTO settings(name, value) VALUES('" + VERSION_SETTING + "', '03')");
- s.close();
- createNewStore(params);
- }
-
- private void initFromDatabase() throws SQLException, BlockStoreException {
- Statement s = conn.get().createStatement();
- ResultSet rs;
-
- rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + CHAIN_HEAD_SETTING + "'");
- if (!rs.next()) {
- throw new BlockStoreException("corrupt Postgres block store - no chain head pointer");
- }
- Sha256Hash hash = new Sha256Hash(rs.getBytes(1));
- rs.close();
- this.chainHeadBlock = get(hash);
- this.chainHeadHash = hash;
- if (this.chainHeadBlock == null) {
- throw new BlockStoreException("corrupt Postgres block store - head block not found");
- }
-
- rs = s.executeQuery("SELECT value FROM settings WHERE name = '" + VERIFIED_CHAIN_HEAD_SETTING + "'");
- if (!rs.next()) {
- throw new BlockStoreException("corrupt Postgres block store - no verified chain head pointer");
- }
- hash = new Sha256Hash(rs.getBytes(1));
- rs.close();
- s.close();
- this.verifiedChainHeadBlock = get(hash);
- this.verifiedChainHeadHash = hash;
- if (this.verifiedChainHeadBlock == null) {
- throw new BlockStoreException("corrupt Postgres block store - verified head block not found");
- }
- }
-
- 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 the reference client inits
- // its database - the genesis transaction isn't actually in the db so its spent flags can never be updated.
- List 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.
- }
- }
-
- private boolean tableExists(String table) throws SQLException {
- Statement s = conn.get().createStatement();
- try {
- ResultSet results = s.executeQuery("SELECT * FROM " + table + " WHERE 1 = 2");
- results.close();
- return true;
- } catch (SQLException ex) {
- return false;
- } finally {
- s.close();
- }
- }
-
- /**
- * 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("SELECT name, value FROM settings");
- while (rs.next()) {
- size += rs.getString(1).length();
- size += rs.getBytes(2).length;
- count++;
- }
- rs.close();
- System.out.printf("Settings size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
-
- totalSize += size; size = 0; count = 0;
- rs = s.executeQuery("SELECT chainWork, header FROM headers");
- 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("Headers size: %d, count: %d, average size: %f%n", size, count, (double)size/count);
-
- totalSize += size; size = 0; count = 0;
- rs = s.executeQuery("SELECT txOutChanges, transactions FROM undoableBlocks");
- 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("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("SELECT value, scriptBytes FROM openOutputs");
- 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("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();
- }
-
-
- private void putUpdateStoredBlock(StoredBlock storedBlock, boolean wasUndoable) throws SQLException {
- try {
- PreparedStatement s =
- conn.get().prepareStatement("INSERT INTO headers(hash, chainWork, height, header, wasUndoable)"
- + " VALUES(?, ?, ?, ?, ?)");
- // 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(), 3, hashBytes, 0, 28);
- s.setBytes(1, hashBytes);
- s.setBytes(2, storedBlock.getChainWork().toByteArray());
- s.setInt(3, storedBlock.getHeight());
- s.setBytes(4, storedBlock.getHeader().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(POSTGRES_DUPLICATE_KEY_ERROR_CODE)) || !wasUndoable)
- throw e;
-
- PreparedStatement s = conn.get().prepareStatement("UPDATE headers SET wasUndoable=? WHERE hash=?");
- 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(), 3, hashBytes, 0, 28);
- s.setBytes(2, hashBytes);
- s.executeUpdate();
- s.close();
- }
+ protected String getDuplicateKeyErrorCode() {
+ return POSTGRES_DUPLICATE_KEY_ERROR_CODE;
}
@Override
- public void put(StoredBlock storedBlock) throws BlockStoreException {
- maybeConnect();
- try {
- putUpdateStoredBlock(storedBlock, false);
- } catch (SQLException e) {
- throw new BlockStoreException(e);
- }
+ protected List getCreateTablesSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add(CREATE_SETTINGS_TABLE);
+ sqlStatements.add(CREATE_HEADERS_TABLE);
+ sqlStatements.add(CREATE_UNDOABLE_TABLE);
+ sqlStatements.add(CREATE_OPEN_OUTPUT_TABLE);
+ return sqlStatements;
+ }
+
+ @Override
+ protected List getCreateIndexesSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add(CREATE_UNDOABLE_TABLE_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_ADDRESS_MULTI_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_ADDRESSTARGETABLE_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_HASH_INDEX);
+ sqlStatements.add(CREATE_OUTPUTS_TOADDRESS_INDEX);
+ return sqlStatements;
+ }
+
+ @Override
+ protected List getCreateSchemeSQL() {
+ List sqlStatements = new ArrayList();
+ sqlStatements.add("CREATE SCHEMA IF NOT EXISTS " + schemaName);
+ sqlStatements.add("set search_path to '" + schemaName +"'");
+ return sqlStatements;
+ }
+
+ @Override
+ protected String getTableExistSQL(String tableName) {
+ return "SELECT * FROM " + tableName + " WHERE 1 = 2";
+ }
+
+ @Override
+ protected String getDatabaseDriverClass() {
+ return DATABASE_DRIVER_CLASS;
+ }
+
+ @Override
+ protected String getBalanceSelectSQL() {
+ return SELECT_BALANCE_SQL;
}
@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
+ // We skip the first 4 bytes because (on prodnet) the minimum target has 4 0-bytes
byte[] hashBytes = new byte[28];
System.arraycopy(storedBlock.getHeader().getHash().getBytes(), 3, hashBytes, 0, 28);
int height = storedBlock.getHeight();
@@ -497,7 +209,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
if (log.isDebugEnabled())
log.debug("Looking for undoable block with hash: " + Utils.HEX.encode(hashBytes));
- PreparedStatement findS = conn.get().prepareStatement("select 1 from undoableBlocks where hash = ?");
+ PreparedStatement findS = conn.get().prepareStatement(SELECT_UNDOABLEBLOCKS_EXISTS_SQL);
findS.setBytes(1, hashBytes);
ResultSet rs = findS.executeQuery();
@@ -509,8 +221,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
// Postgres insert-or-updates are very complex (and finnicky). This level of transaction isolation
// seems to work for bitcoinj
PreparedStatement s =
- conn.get().prepareStatement("UPDATE undoableBlocks SET txOutChanges=?, transactions=?"
- + " WHERE hash = ?");
+ conn.get().prepareStatement(getUpdateUndoableBlocksSQL());
s.setBytes(3, hashBytes);
if (log.isDebugEnabled())
@@ -530,8 +241,7 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
}
PreparedStatement s =
- conn.get().prepareStatement("INSERT INTO undoableBlocks(hash, height, txOutChanges, transactions)"
- + " VALUES(?, ?, ?, ?)");
+ conn.get().prepareStatement(getInsertUndoableBlocksSQL());
s.setBytes(1, hashBytes);
s.setInt(2, height);
@@ -558,416 +268,4 @@ public class PostgresFullPrunedBlockStore implements FullPrunedBlockStore {
}
}
-
- 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("SELECT chainWork, height, header, wasUndoable FROM headers WHERE hash = ?");
- // 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(), 3, 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 = new Block(params, 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("SELECT txOutChanges, transactions FROM undoableBlocks WHERE hash = ?");
- // 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(), 3, 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) << 0) |
- ((transactions[offset++] & 0xFF) << 8) |
- ((transactions[offset++] & 0xFF) << 16) |
- ((transactions[offset++] & 0xFF) << 24);
- List transactionList = new LinkedList();
- for (int i = 0; i < numTxn; i++) {
- Transaction tx = new Transaction(params, 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("UPDATE settings SET value = ? WHERE name = ?");
- 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("UPDATE settings SET value = ? WHERE name = ?");
- 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("DELETE FROM undoableBlocks WHERE height <= ?");
- 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 StoredTransactionOutput getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException {
- maybeConnect();
- PreparedStatement s = null;
- try {
- s = conn.get()
- .prepareStatement("SELECT height, value, scriptBytes FROM openOutputs " +
- "WHERE hash = ? AND index = ?");
- 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(new BigInteger(results.getBytes(2)).longValue());
- // Tell the StoredTransactionOutput that we are a coinbase, as that is encoded in height
- StoredTransactionOutput txout = new StoredTransactionOutput(hash, index, value, height, true, results.getBytes(3));
- 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(StoredTransactionOutput out) throws BlockStoreException {
- maybeConnect();
- PreparedStatement s = null;
-
- // Calculate the toAddress (if any)
- String dbAddress = "";
- int type = 0;
- Script outputScript = null;
- try
- {
- outputScript = new Script(out.getScriptBytes());
- }
- catch (ScriptException e)
- {
- // Unparseable, but this isn't an error - it's an output not containing an address
- log.info("Could not parse script for output: " + out.getHash().toString());
- }
- if (outputScript != null && (outputScript.isSentToAddress()
- || outputScript.isSentToRawPubKey()
- || outputScript.isPayToScriptHash()))
- {
- if (outputScript.isSentToAddress())
- {
- Address targetAddr = new Address(params, outputScript.getPubKeyHash());
- dbAddress = targetAddr.toString();
- type = 1;
- }
- else if (outputScript.isSentToRawPubKey())
- {
- /*
- * Note we use the deprecated getFromAddress here. Coinbase outputs seem to have the target address
- * in the pubkey of the script - perhaps we can rename this function?
- */
-
- dbAddress = outputScript.getFromAddress(params).toString();
- type = 2;
- } else {
- dbAddress = Address.fromP2SHHash(params, outputScript.getPubKeyHash()).toString();
- type = 3;
- }
- }
-
- try {
- s = conn.get().prepareStatement("INSERT INTO openOutputs (hash, index, height, value, scriptBytes, toAddress, addressTargetable) " +
- "VALUES (?, ?, ?, ?, ?, ?, ?)");
- s.setBytes(1, out.getHash().getBytes());
- // index is actually an unsigned int
- s.setInt(2, (int)out.getIndex());
- s.setInt(3, out.getHeight());
- s.setBytes(4, BigInteger.valueOf(out.getValue().value).toByteArray());
- s.setBytes(5, out.getScriptBytes());
- s.setString(6, dbAddress);
- s.setInt(7, type);
- s.executeUpdate();
- s.close();
- } catch (SQLException e) {
- if (!(e.getSQLState().equals(POSTGRES_DUPLICATE_KEY_ERROR_CODE)))
- throw new BlockStoreException(e);
- } finally {
- if (s != null)
- try {
- s.close();
- } catch (SQLException e) { throw new BlockStoreException(e); }
- }
- }
-
- @Override
- public void removeUnspentTransactionOutput(StoredTransactionOutput 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 StoredTransactionOutput from PostgresFullPrunedBlockStore that it didn't have!");
- try {
- PreparedStatement s = conn.get()
- .prepareStatement("DELETE FROM openOutputs WHERE hash = ? AND index = ?");
- 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("SELECT COUNT(*) FROM openOutputs WHERE hash = ?");
- 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"); }
- }
- }
-
- /**
- * Calculate the balance for a coinbase, to-address, or p2sh address.
- * @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
- */
- public BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException {
- maybeConnect();
- PreparedStatement s = null;
-
-
- try {
- s = conn.get().prepareStatement("select sum(('x'||lpad(substr(value::text, 3, 50),16,'0'))::bit(64)::bigint) "
- + "from openoutputs where toaddress = ?");
- s.setString(1, address.toString());
- ResultSet rs = s.executeQuery();
- if (rs.next()) {
- return BigInteger.valueOf(rs.getLong(1));
- } else {
- throw new BlockStoreException("Failed to execute balance lookup");
- }
-
- } catch (SQLException ex) {
- throw new BlockStoreException(ex);
- } finally {
- if (s != null)
- try {
- s.close();
- } catch (SQLException e) {
- throw new BlockStoreException("Could not close statement");
- }
- }
- }
-
-
-}
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java b/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java
index 68733821..0093a91c 100644
--- a/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java
+++ b/core/src/test/java/org/bitcoinj/core/AbstractFullPrunedBlockChainTest.java
@@ -44,9 +44,9 @@ public abstract class AbstractFullPrunedBlockChainTest
{
private static final Logger log = LoggerFactory.getLogger(AbstractFullPrunedBlockChainTest.class);
- private NetworkParameters params;
- private FullPrunedBlockChain chain;
- private FullPrunedBlockStore store;
+ protected NetworkParameters params;
+ protected FullPrunedBlockChain chain;
+ protected FullPrunedBlockStore store;
@Before
public void setUp() throws Exception {
diff --git a/core/src/test/java/org/bitcoinj/core/MySQLFullPrunedBlockChainTest.java b/core/src/test/java/org/bitcoinj/core/MySQLFullPrunedBlockChainTest.java
new file mode 100644
index 00000000..cf37a33d
--- /dev/null
+++ b/core/src/test/java/org/bitcoinj/core/MySQLFullPrunedBlockChainTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.core;
+
+import org.bitcoinj.store.BlockStoreException;
+import org.bitcoinj.store.FullPrunedBlockStore;
+import org.bitcoinj.store.MySQLFullPrunedBlockStore;
+import org.junit.After;
+import org.junit.Ignore;
+
+/**
+ * A MySQL implementation of the {@link AbstractFullPrunedBlockChainTest}
+ */
+@Ignore("enable the mysql driver dependency in the maven POM")
+public class MySQLFullPrunedBlockChainTest extends AbstractFullPrunedBlockChainTest {
+
+ @After
+ public void tearDown() throws Exception {
+ ((MySQLFullPrunedBlockStore)store).deleteStore();
+ }
+
+ // Replace these with your mysql location/credentials and remove @Ignore to test
+ private static final String DB_HOSTNAME = "localhost";
+ private static final String DB_NAME = "bitcoinj_test";
+ private static final String DB_USERNAME = "bitcoinj";
+ private static final String DB_PASSWORD = "password";
+
+ @Override
+ public FullPrunedBlockStore createStore(NetworkParameters params, int blockCount)
+ throws BlockStoreException {
+ return new MySQLFullPrunedBlockStore(params, blockCount, DB_HOSTNAME, DB_NAME, DB_USERNAME, DB_PASSWORD);
+ }
+
+ @Override
+ public void resetStore(FullPrunedBlockStore store) throws BlockStoreException {
+ ((MySQLFullPrunedBlockStore)store).resetStore();
+ }
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/bitcoinj/core/PostgresFullPrunedBlockChainTest.java b/core/src/test/java/org/bitcoinj/core/PostgresFullPrunedBlockChainTest.java
index 5caa15e8..d9aaef0a 100644
--- a/core/src/test/java/org/bitcoinj/core/PostgresFullPrunedBlockChainTest.java
+++ b/core/src/test/java/org/bitcoinj/core/PostgresFullPrunedBlockChainTest.java
@@ -3,6 +3,7 @@ package org.bitcoinj.core;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.FullPrunedBlockStore;
import org.bitcoinj.store.PostgresFullPrunedBlockStore;
+import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
@@ -22,6 +23,11 @@ public class PostgresFullPrunedBlockChainTest extends AbstractFullPrunedBlockCha
// whether to run the test with a schema name
private boolean useSchema = false;
+ @After
+ public void tearDown() throws Exception {
+ ((PostgresFullPrunedBlockStore)store).deleteStore();
+ }
+
@Override
public FullPrunedBlockStore createStore(NetworkParameters params, int blockCount)
throws BlockStoreException {