diff --git a/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java
new file mode 100644
index 00000000..ef7b88dc
--- /dev/null
+++ b/core/src/main/java/com/google/bitcoin/core/FullPrunedBlockChain.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * 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.core;
+
+import com.google.bitcoin.store.BlockStoreException;
+import com.google.bitcoin.store.FullPrunedBlockStore;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ *
A FullPrunedBlockChain works in conjunction with a {@link FullPrunedBlockStore} to provide a fully verifying
+ * block chain. Fully verifying means all unspent transaction outputs are stored. Once a transaction output is spent
+ * and that spend is buried deep enough, the data related to is deleted to ensure disk space usage doesn't grow
+ * forever. For this reason a pruning node cannot serve the full block chain to other clients, but it nevertheless
+ * provides the same security guarantees as a regular Satoshi client does.
+ */
+public class FullPrunedBlockChain extends AbstractBlockChain {
+ /** Keeps a map of block hashes to StoredBlocks. */
+ protected final FullPrunedBlockStore blockStore;
+
+ /**
+ * Constructs a BlockChain connected to the given wallet and store. To obtain a {@link Wallet} you can construct
+ * one from scratch, or you can deserialize a saved wallet from disk using {@link Wallet#loadFromFile(java.io.File)}
+ */
+ public FullPrunedBlockChain(NetworkParameters params, Wallet wallet, FullPrunedBlockStore blockStore) throws BlockStoreException {
+ this(params, new ArrayList(), blockStore);
+ if (wallet != null)
+ addWallet(wallet);
+ }
+
+ /**
+ * Constructs a BlockChain that has no wallet at all. This is helpful when you don't actually care about sending
+ * and receiving coins but rather, just want to explore the network data structures.
+ */
+ public FullPrunedBlockChain(NetworkParameters params, FullPrunedBlockStore blockStore) throws BlockStoreException {
+ this(params, new ArrayList(), blockStore);
+ }
+
+ /**
+ * Constructs a BlockChain connected to the given list of wallets and a store.
+ */
+ public FullPrunedBlockChain(NetworkParameters params, List wallets,
+ FullPrunedBlockStore blockStore) throws BlockStoreException {
+ super(params, wallets, blockStore);
+ this.blockStore = blockStore;
+ }
+
+ @Override
+ protected StoredBlock addToBlockStore(StoredBlock storedPrev, Block header, TransactionOutputChanges txOutChanges)
+ throws BlockStoreException, VerificationException {
+ StoredBlock newBlock = storedPrev.build(header);
+ blockStore.put(newBlock, new StoredUndoableBlock(newBlock.getHeader().getHash(), txOutChanges));
+ return newBlock;
+ }
+
+ @Override
+ protected StoredBlock addToBlockStore(StoredBlock storedPrev, Block block)
+ throws BlockStoreException, VerificationException {
+ StoredBlock newBlock = storedPrev.build(block);
+ LinkedList transactions = new LinkedList();
+ for (Transaction tx : block.transactions)
+ transactions.add(new StoredTransaction(tx, newBlock.getHeight()));
+ blockStore.put(newBlock, new StoredUndoableBlock(newBlock.getHeader().getHash(), transactions));
+ return newBlock;
+ }
+
+ @Override
+ protected boolean shouldVerifyTransactions() {
+ return true;
+ }
+
+ //TODO: Remove lots of duplicated code in the two connectTransactions
+ //TODO: More checking can be done here (eg spent-coinbase depth check)
+
+ @Override
+ protected TransactionOutputChanges connectTransactions(int height, Block block)
+ throws VerificationException, BlockStoreException {
+ if (block.transactions == null)
+ throw new RuntimeException("connectTransactions called with Block that didn't have transactions!");
+ if (!params.passesCheckpoint(height, block.getHash()))
+ throw new VerificationException("Block failed checkpoint lockin at " + height);
+
+ blockStore.beginDatabaseBatchWrite();
+
+ LinkedList txOutsSpent = new LinkedList();
+ LinkedList txOutsCreated = new LinkedList();
+ try {
+ if (!params.isCheckpoint(height)) {
+ // BIP30 violator blocks are ones that contain a duplicated transaction. They are all in the
+ // checkpoints list and we therefore only check non-checkpoints for duplicated transactions here. See the
+ // BIP30 document for more details on this: https://en.bitcoin.it/wiki/BIP_0030
+ for (Transaction tx : block.transactions) {
+ Sha256Hash hash = tx.getHash();
+ // If we already have unspent outputs for this hash, we saw the tx already. Either the block is
+ // being added twice (bug) or the block is a BIP30 violator.
+ if (blockStore.hasUnspentOutputs(hash, tx.getOutputs().size()))
+ throw new VerificationException("Block failed BIP30 test!");
+ }
+ }
+ for (Transaction tx : block.transactions) {
+ boolean isCoinBase = tx.isCoinBase();
+ if (!isCoinBase) {
+ // For each input of the transaction remove the corresponding output from the set of unspent
+ // outputs.
+ for (TransactionInput in : tx.getInputs()) {
+ StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
+ in.getOutpoint().getIndex());
+ if (prevOut == null)
+ throw new VerificationException("Attempted to spend a non-existent or already spent output!");
+ //TODO: check script here
+ blockStore.removeUnspentTransactionOutput(prevOut);
+ txOutsSpent.add(prevOut);
+ }
+ }
+ Sha256Hash hash = tx.getHash();
+ for (TransactionOutput out : tx.getOutputs()) {
+ // For each output, add it to the set of unspent outputs so it can be consumed in future.
+ StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(),
+ height, isCoinBase, out.getScriptBytes());
+ blockStore.addUnspentTransactionOutput(newOut);
+ txOutsCreated.add(newOut);
+ }
+ }
+ } catch (VerificationException e) {
+ blockStore.abortDatabaseBatchWrite();
+ throw e;
+ } catch (BlockStoreException e) {
+ blockStore.abortDatabaseBatchWrite();
+ throw e;
+ }
+ return new TransactionOutputChanges(txOutsCreated, txOutsSpent);
+ }
+
+ @Override
+ /**
+ * Used during reorgs to connect a block previously on a fork
+ */
+ protected TransactionOutputChanges connectTransactions(StoredBlock newBlock)
+ throws VerificationException, BlockStoreException, PrunedException {
+ if (!params.passesCheckpoint(newBlock.getHeight(), newBlock.getHeader().getHash()))
+ throw new VerificationException("Block failed checkpoint lockin at " + newBlock.getHeight());
+
+ blockStore.beginDatabaseBatchWrite();
+ StoredUndoableBlock block = blockStore.getUndoBlock(newBlock.getHeader().getHash());
+ if (block == null) {
+ // We're trying to re-org too deep and the data needed has been deleted.
+ blockStore.abortDatabaseBatchWrite();
+ throw new PrunedException(newBlock.getHeader().getHash());
+ }
+ TransactionOutputChanges txOutChanges;
+ try {
+ List transactions = block.getTransactions();
+ if (transactions != null) {
+ LinkedList txOutsSpent = new LinkedList();
+ LinkedList txOutsCreated = new LinkedList();
+ if (!params.isCheckpoint(newBlock.getHeight())) {
+ // See explanation above.
+ for(StoredTransaction tx : transactions) {
+ Sha256Hash hash = tx.getHash();
+ if (blockStore.hasUnspentOutputs(hash, tx.getOutputs().size()))
+ throw new VerificationException("Block failed BIP30 test!");
+ }
+ }
+ for (StoredTransaction tx : transactions) {
+ boolean isCoinBase = tx.isCoinBase();
+ if (!isCoinBase)
+ for(TransactionInput in : tx.getInputs()) {
+ StoredTransactionOutput prevOut = blockStore.getTransactionOutput(in.getOutpoint().getHash(),
+ in.getOutpoint().getIndex());
+ if (prevOut == null)
+ throw new VerificationException("Attempted spend of a non-existent or already spent output!");
+ //TODO: check script here
+ blockStore.removeUnspentTransactionOutput(prevOut);
+ txOutsSpent.add(prevOut);
+ }
+ Sha256Hash hash = tx.getHash();
+ for (StoredTransactionOutput out : tx.getOutputs()) {
+ StoredTransactionOutput newOut = new StoredTransactionOutput(hash, out.getIndex(), out.getValue(),
+ newBlock.getHeight(), isCoinBase,
+ out.getScriptBytes());
+ blockStore.addUnspentTransactionOutput(newOut);
+ txOutsCreated.add(newOut);
+ }
+ }
+ txOutChanges = new TransactionOutputChanges(txOutsCreated, txOutsSpent);
+ } else {
+ // Use the undo data.
+ txOutChanges = block.getTxOutChanges();
+ if (!params.isCheckpoint(newBlock.getHeight()))
+ for(StoredTransactionOutput out : txOutChanges.txOutsCreated) {
+ Sha256Hash hash = out.getHash();
+ if (blockStore.getTransactionOutput(hash, out.getIndex()) != null)
+ throw new VerificationException("Block failed BIP30 test!");
+ }
+ for (StoredTransactionOutput out : txOutChanges.txOutsCreated)
+ blockStore.addUnspentTransactionOutput(out);
+ for (StoredTransactionOutput out : txOutChanges.txOutsSpent)
+ blockStore.removeUnspentTransactionOutput(out);
+ }
+ } catch (VerificationException e) {
+ blockStore.abortDatabaseBatchWrite();
+ throw e;
+ } catch (BlockStoreException e) {
+ blockStore.abortDatabaseBatchWrite();
+ throw e;
+ }
+ return txOutChanges;
+ }
+
+ /**
+ * This is broken for blocks that do not pass BIP30, so all BIP30-failing blocks which are allowed to fail BIP30
+ * must be checkpointed.
+ */
+ @Override
+ protected void disconnectTransactions(StoredBlock oldBlock) throws PrunedException, BlockStoreException {
+ blockStore.beginDatabaseBatchWrite();
+ try {
+ StoredUndoableBlock undoBlock = blockStore.getUndoBlock(oldBlock.getHeader().getHash());
+ if (undoBlock == null) throw new PrunedException(oldBlock.getHeader().getHash());
+ TransactionOutputChanges txOutChanges = undoBlock.getTxOutChanges();
+ for(StoredTransactionOutput out : txOutChanges.txOutsSpent)
+ blockStore.addUnspentTransactionOutput(out);
+ for(StoredTransactionOutput out : txOutChanges.txOutsCreated)
+ blockStore.removeUnspentTransactionOutput(out);
+ } catch (PrunedException e) {
+ blockStore.abortDatabaseBatchWrite();
+ throw e;
+ } catch (BlockStoreException e) {
+ blockStore.abortDatabaseBatchWrite();
+ throw e;
+ }
+ }
+
+ @Override
+ protected void preSetChainHead() throws BlockStoreException {
+ blockStore.commitDatabaseBatchWrite();
+ }
+
+ @Override
+ protected void notSettingChainHead() throws BlockStoreException {
+ blockStore.abortDatabaseBatchWrite();
+ }
+}