/*
 * Copyright 2012 Matt Corallo.
 *
 * 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 com.google.bitcoin.store;

import com.google.bitcoin.core.*;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.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 {
    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;

    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";

    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),"
        + ")";

    /**
     * Creates a new H2FullPrunedBlockStore
     * @param params A copy of the NetworkParameters used
     * @param dbName The path to the database on disk
     * @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe)
     * @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);
        }
    }
    
    /**
     * Creates a new H2FullPrunedBlockStore with the given cache size
     * @param params A copy of the NetworkParameters used
     * @param dbName The path to the database on disk
     * @param fullStoreDepth The number of blocks of history stored in full (something like 1000 is pretty safe)
     * @param cacheSize The number of kilobytes to dedicate to H2 Cache (the default value of 16MB (16384) is a safe bet
     *                  to achieve good performance/cost when importing blocks from disk, past 32MB makes little sense,
     *                  and below 4MB sees a sharp drop in performance)
     * @throws BlockStoreException if the database fails to open for any reason
     */
    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);
            s.close();
        } catch (SQLException e) {
            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);
        }
    }
    
    public synchronized void close() {
        for (Connection conn : allConnections) {
            try {
                conn.rollback();
            } 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 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.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 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();
        }
    }

    public void put(StoredBlock storedBlock) throws BlockStoreException {
        maybeConnect();
        try {
            putUpdateStoredBlock(storedBlock, false);
        } catch (SQLException e) {
            throw new BlockStoreException(e);
        }
    }
    
    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("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);
        }
    }

    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 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"); }
        }
    }
    
    public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
        return get(hash, false);
    }
    
    public StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException {
        return get(hash, true);
    }
    
    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 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<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"); }
        }
    }

    public StoredBlock getChainHead() throws BlockStoreException {
        return chainHeadBlock;
    }

    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);
        }
    }
    
    public StoredBlock getVerifiedChainHead() throws BlockStoreException {
        return verifiedChainHeadBlock;
    }

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

    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);
            BigInteger value = new BigInteger(results.getBytes(2));
            // 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"); }
        }
    }

    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, out.getValue().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); }
        }
    }

    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 H2FullPrunedBlockStore 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);
        }
    }

    public void beginDatabaseBatchWrite() throws BlockStoreException {
        maybeConnect();
        try {
            conn.get().setAutoCommit(false);
        } catch (SQLException e) {
            throw new BlockStoreException(e);
        }
    }

    public void commitDatabaseBatchWrite() throws BlockStoreException {
        maybeConnect();
        try {
            conn.get().commit();
            conn.get().setAutoCommit(true);
        } catch (SQLException e) {
            throw new BlockStoreException(e);
        }
    }

    public void abortDatabaseBatchWrite() throws BlockStoreException {
        maybeConnect();
        try {
            conn.get().rollback();
            conn.get().setAutoCommit(true);
        } catch (SQLException e) {
            throw new BlockStoreException(e);
        }
    }

    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"); }
        }
    }
}
