3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 14:54:15 +00:00

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.
This commit is contained in:
Kalpesh Parmar 2014-10-26 17:36:09 +00:00 committed by Mike Hearn
parent 580a67c0b7
commit fbf62614b4
8 changed files with 1680 additions and 1492 deletions

View File

@ -441,15 +441,20 @@
<artifactId>scrypt</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Add in to test/use Postgres blockstore -->
<!--
<!-- Note this is an optional dependency: Postgres blockstore -->
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version>
<optional>true</optional>
</dependency>
<!-- Note this is an optional dependency: MySQL blockstore -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.33</version>
<optional>true</optional>
</dependency>
-->
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>orchid</artifactId>

File diff suppressed because it is too large Load Diff

View File

@ -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,17 +18,13 @@
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
@ -39,29 +36,20 @@ import java.util.List;
* 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<Connection> conn;
private List<Connection> 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 ( "
// 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 CHAIN_HEAD_SETTING = "chainhead";
static final String VERIFIED_CHAIN_HEAD_SETTING = "verifiedchainhead";
static final String VERSION_SETTING = "version";
static final String CREATE_HEADERS_TABLE = "CREATE TABLE headers ( "
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,"
@ -69,23 +57,33 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
+ "wasUndoable BOOL NOT NULL"
+ ")";
static final String CREATE_UNDOABLE_TABLE = "CREATE TABLE undoableBlocks ( "
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"
+ ")";
static final String CREATE_UNDOABLE_TABLE_INDEX = "CREATE INDEX heightIndex ON undoableBlocks (height)";
static final String CREATE_OPEN_OUTPUT_TABLE = "CREATE TABLE openOutputs ("
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
* @param params A copy of the NetworkParameters used
@ -94,32 +92,7 @@ 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<Connection>();
allConnections = new LinkedList<Connection>();
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);
}
/**
@ -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);
@ -144,635 +116,56 @@ public class H2FullPrunedBlockStore implements FullPrunedBlockStore {
}
}
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 BigInteger calculateBalanceForAddress(Address address) throws BlockStoreException {
return calculateBalanceForAddress(address, true);
}
@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<Transaction> genesisTransactions = Lists.newLinkedList();
StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions);
put(storedGenesisHeader, storedGenesis);
setChainHead(storedGenesisHeader);
setVerifiedChainHead(storedGenesisHeader);
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
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();
}
protected String getDuplicateKeyErrorCode() {
return H2_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<String> getCreateTablesSQL() {
List<String> sqlStatements = new ArrayList<String>();
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 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");
}
}
}
protected List<String> getCreateIndexesSQL() {
List<String> sqlStatements = new ArrayList<String>();
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
@Nullable
public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
return get(hash, false);
protected List<String> getCreateSchemeSQL() {
// do nothing
return Collections.emptyList();
}
@Override
@Nullable
public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException {
return get(hash, true);
protected String getTableExistSQL(String tableName) {
return "SELECT * FROM " + tableName + " WHERE 1 = 2";
}
@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<Transaction> transactionList = new LinkedList<Transaction>();
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 getDatabaseDriverClass() {
return DATABASE_DRIVER_CLASS;
}
@Override
public StoredBlock getChainHead() throws BlockStoreException {
return chainHeadBlock;
protected String getBalanceSelectSQL() {
return SELECT_BALANCE_SQL;
}
@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);
s.executeUpdate();
s.close();
} catch (SQLException ex) {
throw new BlockStoreException(ex);
}
}
@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"); }
}
}
@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); }
}
}
@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);
}
}
@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"); }
}
}
}

View File

@ -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;
/**
* <p>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.</p>
*/
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<String> getCreateTablesSQL() {
List<String> sqlStatements = new ArrayList<String>();
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<String> getCreateIndexesSQL() {
List<String> sqlStatements = new ArrayList<String>();
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<String> 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;
}
}

View File

@ -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;
/**
* <p>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.</p>
*/
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<Connection> conn;
private List<Connection> 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);
}
/**
* <p>Create a new PostgresFullPrunedBlockStore, using the full connection URL instead of a hostname and password,
* and optionally allowing a schema to be specified.</p>
*
* <p>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 <a href="http://jdbc.postgresql.org/documentation/head/connect.html#connection-parameters">
* the PostgreSQL JDBC documentation</a> for more on the connection URL.</p>
*
* <p>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
* <a href="http://www.postgres.org/docs/9.3/static/ddl-schemas.html">the postgres schema docs</a> for more on
* schemas.</p>
*
*
* @param params A copy of the NetworkParameters used.
* @param connectionURL The jdbc url to connect to the database.
* @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe).
* @param username The database username.
* @param password The password to the database.
* @param schemaName The name of the schema to put the tables in. May be null if no schema is being used.
* @throws BlockStoreException If 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<Connection>();
allConnections = new LinkedList<Connection>();
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<Transaction> genesisTransactions = Lists.newLinkedList();
StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions);
put(storedGenesisHeader, storedGenesis);
setChainHead(storedGenesisHeader);
setVerifiedChainHead(storedGenesisHeader);
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
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<String> getCreateTablesSQL() {
List<String> sqlStatements = new ArrayList<String>();
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<String> getCreateIndexesSQL() {
List<String> sqlStatements = new ArrayList<String>();
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<String> getCreateSchemeSQL() {
List<String> sqlStatements = new ArrayList<String>();
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<Transaction> transactionList = new LinkedList<Transaction>();
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");
}
}
}
}

View File

@ -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 {

View File

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

View File

@ -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 {