package org.ripple.power.txns.btc;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class BlockChain {
/** Verify blocks */
private final boolean verifyBlocks;
/** Chain listeners */
private final List<ChainListener> listeners = new ArrayList<ChainListener>();
/**
* Creates a new block chain
*
* @param verifyBlocks TRUE if new blocks should be verified
*/
public BlockChain(boolean verifyBlocks) {
this.verifyBlocks = verifyBlocks;
}
/**
* Registers a chain listener
*
* @param chainListener The chain listener
*/
public void addListener(ChainListener chainListener) {
listeners.add(chainListener);
}
/**
* Adds a block to the block store and updates the block chain
*
* @param block The block to add
* @return List of blocks that have been added to the chain.
* The first element in the list is the junction block
* and will not contain any block data. The list will
* be null if no blocks have been added to the chain.
* @throws BlockStoreException Unable to store the block in the database
*/
public List<StoredBlock> storeBlock(Block block) throws BlockStoreException {
//
// Store the block in the database with hold status until we have verified the block
//
StoredBlock storedBlock = new StoredBlock(block, BigInteger.ZERO, 0);
storedBlock.setHold(true);
BTCLoader.blockStore.storeBlock(storedBlock);
for(ChainListener listener:listeners){
listener.blockStored(storedBlock);
}
//
// Update the block chain and return the chain list to our caller
//
return updateBlockChain(storedBlock);
}
/**
* Updates the block chain to reflect a new or updated block.
* The block must be in the block store and must be on hold if this
* is a new block that hasn't been verified yet.
*
* @param storedBlock The new or updated stored block
* @return List of blocks that have been added to the chain.
* The first element in the list is the junction block
* and will not contain any block data. The list will
* be null if no blocks have been added to the chain.
* @throws BlockStoreException Unable to update the block chain in the database
*/
public List<StoredBlock> updateBlockChain(StoredBlock storedBlock) throws BlockStoreException {
List<StoredBlock> chainList = null;
Map<Sha256Hash, Transaction> txMap = null;
Map<Sha256Hash, List<StoredOutput>> outputMap = null;
boolean onHold = false;
Block block = storedBlock.getBlock();
//
// Locate the chain containing this block and map the transactions in the chain.
// We will need this information when validating transactions since these transactions
// are not in the database yet.
//
// A BlockNotFoundException is thrown if a block in the chain is not in the database.
// This can happen if we receive blocks out-of-order. In this case, we need to place
// the new block on hold until we receive another block in the chain. We will add
// the missing block to the list of blocks to be fetched from a peer.
//
// A ChainTooLongException is thrown if the block chain exceeds 144 blocks. This is
// done to avoid running out of storage as the unresolved chain increases in size.
// The exception contains the hash for the restart block. We will recursively call
// ourself to work our way down to the junction block.
//
boolean buildChain = true;
while (buildChain && !onHold) {
try {
chainList = BTCLoader.blockStore.getJunction(block.getPrevBlockHash());
txMap = new HashMap<>(chainList.size());
outputMap = new HashMap<>(chainList.size()*250);
for (StoredBlock chainStoredBlock : chainList) {
Block chainBlock = chainStoredBlock.getBlock();
if (chainBlock != null) {
List<Transaction> txList = chainBlock.getTransactions();
for (Transaction tx : txList)
txMap.put(tx.getHash(), tx);
}
}
List<Transaction> txList = block.getTransactions();
for (Transaction tx : txList)
txMap.put(tx.getHash(), tx);
buildChain = false;
} catch (ChainTooLongException exc) {
Sha256Hash chainHash = exc.getHash();
StoredBlock chainStoredBlock = BTCLoader.blockStore.getStoredBlock(chainHash);
chainList = updateBlockChain(chainStoredBlock);
if (chainList == null) {
buildChain = false;
onHold = true;
}
} catch (BlockNotFoundException exc) {
onHold = true;
if (BTCLoader.networkHandler != null) {
PeerRequest request = new PeerRequest(exc.getHash(), InventoryItem.INV_BLOCK);
synchronized(BTCLoader.pendingRequests) {
if (!BTCLoader.pendingRequests.contains(request) &&
!BTCLoader.processedRequests.contains(request))
BTCLoader.pendingRequests.add(request);
}
BTCLoader.networkHandler.wakeup();
}
}
}
if (onHold)
return null;
//
// The block version must be 2 (or greater) if the chain height is 250,000 or greater
//
long version = block.getVersion();
if (BTCLoader.blockStore.getChainHeight() >= 250000 && version < 2) {
BTCLoader.error(String.format("Block version %d is no longer acceptable", version));
return null;
}
//
// Check for any held blocks in the chain. If we find one, attempt to verify it.
// If the verification fails, we will need to wait until another block is received
// before we can try to verify the chain again.
//
BigInteger chainWork = chainList.get(0).getChainWork();
int chainHeight = chainList.get(0).getHeight();
for (StoredBlock chainStoredBlock : chainList) {
Block chainBlock = chainStoredBlock.getBlock();
if (chainBlock != null) {
chainWork = chainWork.add(chainBlock.getWork());
chainStoredBlock.setChainWork(chainWork);
chainStoredBlock.setHeight(++chainHeight);
if (chainStoredBlock.isOnHold()) {
if (verifyBlocks) {
if (!verifyBlock(chainStoredBlock, chainList.get(0).getHeight(), txMap, outputMap)) {
BTCLoader.info(String.format("Failed to verify held block\n Block %s",
chainBlock.getHashAsString()));
onHold = true;
break;
}
}
chainStoredBlock.setHold(false);
BTCLoader.blockStore.releaseBlock(chainStoredBlock.getHash());
BTCLoader.info(String.format(String.format("Held block released\n Block %s",
chainBlock.getHashAsString())));
for(ChainListener listener:listeners){
listener.blockUpdated(chainStoredBlock);
}
}
}
}
//
// Update the new block
//
if (!onHold) {
chainWork = chainWork.add(block.getWork());
storedBlock.setChainWork(chainWork);
storedBlock.setHeight(++chainHeight);
}
//
// Verify the transactions for the new block
//
if (!onHold && verifyBlocks) {
if (!verifyBlock(storedBlock, chainList.get(0).getHeight(), txMap, outputMap)) {
BTCLoader.info(String.format("Block verification failed\n Block %s", storedBlock.getHash()));
onHold = true;
}
}
//
// Stop now if the block is not ready for processing
//
if (onHold)
return null;
//
// Add this block to the end of the chain
//
chainList.add(storedBlock);
//
// Release the block and update the chain work and block height values in the database
//
storedBlock.setHold(false);
BTCLoader.blockStore.releaseBlock(storedBlock.getHash());
for(ChainListener listener:listeners){
listener.blockUpdated(storedBlock);
}
//
// Make this block the new chain head if it is a better chain than the current chain.
// This means the cumulative chain work is greater.
//
if (storedBlock.getChainWork().compareTo(BTCLoader.blockStore.getChainWork()) > 0) {
try {
BTCLoader.blockStore.setChainHead(chainList);
for (StoredBlock updatedStoredBlock : chainList) {
Block updatedBlock = updatedStoredBlock.getBlock();
if (updatedBlock == null)
continue;
//
// Notify listeners that we updated the block
//
updatedStoredBlock.setChain(true);
for(ChainListener listener:listeners){
listener.blockUpdated(updatedStoredBlock);
}
}
for(ChainListener listener:listeners){
listener.chainUpdated();
}
//
// Delete spent transaction outputs if we are caught up with the network
//
if (BTCLoader.blockStore.getChainHeight() >= BTCLoader.networkChainHeight)
BTCLoader.blockStore.deleteSpentTxOutputs();
} catch (VerificationException exc) {
chainList = null;
BTCLoader.info(String.format("Block being held due to verification failure\n Block %s", exc.getHash()));
}
}
return chainList;
}
/**
* Verify a block
*
* @param block Block to be verified
* @param junctionHeight Height of the junction block
* @param txMap Transaction map
* @param outputMap Transaction output map
* @return TRUE if the block is verified, FALSE otherwise
* @throws BlockStoreException Unable to read from database
*/
private boolean verifyBlock(StoredBlock storedBlock, int junctionHeight,
Map<Sha256Hash, Transaction> txMap,
Map<Sha256Hash, List<StoredOutput>> outputMap)
throws BlockStoreException {
Block block = storedBlock.getBlock();
boolean txValid = true;
BigInteger totalFees = BigInteger.ZERO;
//
// Check each transaction in the block
//
List<Transaction> txList = block.getTransactions();
for (Transaction tx : txList) {
//
// The input script for the coinbase transaction must contain the chain height
// as the first data element if the block version is 2 (BIP0034)
//
if (tx.isCoinBase()) {
if (block.getVersion() >= 2 && junctionHeight >= 250000) {
TransactionInput input = tx.getInputs().get(0);
byte[] scriptBytes = input.getScriptBytes();
if (scriptBytes.length < 1) {
BTCLoader.error(String.format("Coinbase input script is not valid\n Tx %s", tx.getHash()));
txValid = false;
break;
}
int length = (int)scriptBytes[0]&0xff;
if (length+1 > scriptBytes.length) {
BTCLoader.error(String.format("Coinbase script is too short\n Tx %s", tx.getHash()));
txValid = false;
break;
}
int chainHeight = (int)scriptBytes[1]&0xff;
for (int i=1; i<length; i++)
chainHeight = chainHeight | (((int)scriptBytes[i+1]&0xff)<<(i*8));
if (chainHeight != storedBlock.getHeight()) {
BTCLoader.error(String.format("Coinbase height %d does not match block height %d\n Tx %s",
chainHeight, storedBlock.getHeight(), tx.getHash()));
BTCLoader.dumpData("Coinbase Script", scriptBytes);
txValid = false;
break;
}
}
continue;
}
//
// Check each input in the transaction
//
BigInteger txAmount = BigInteger.ZERO;
List<TransactionInput> inputs = tx.getInputs();
for (TransactionInput input : inputs) {
OutPoint op = input.getOutPoint();
Sha256Hash opHash = op.getHash();
int opIndex = op.getIndex();
//
// Locate the connected transaction output
//
List<StoredOutput> outputs = outputMap.get(opHash);
if (outputs == null) {
Transaction outTx = txMap.get(opHash);
if (outTx == null) {
outputs = BTCLoader.blockStore.getTxOutputs(opHash);
if (outputs == null) {
BTCLoader.error(String.format("Transaction input specifies unavailable transaction\n"+
" Transaction %s\n Transaction input %d\n Connected output %s",
tx.getHash(), input.getIndex(), opHash));
txValid = false;
} else {
outputMap.put(opHash, outputs);
}
} else {
List<TransactionOutput> txOutputList = outTx.getOutputs();
outputs = new ArrayList<>(txOutputList.size());
for (TransactionOutput txOutput : txOutputList)
outputs.add(new StoredOutput(txOutput.getIndex(), txOutput.getValue(),
txOutput.getScriptBytes(), outTx.isCoinBase()));
outputMap.put(opHash, outputs);
}
}
//
// Add the input amount to the running total for the transaction.
// Verify the input signature against the connected output. We allow a double-spend
// if the spending block is above the junction block since that spending block will
// be removed if the chain ends up being reorganized.
//
if (txValid) {
StoredOutput output = null;
boolean foundOutput = false;
for (StoredOutput output1 : outputs) {
output = output1;
if (output.getIndex() == opIndex) {
foundOutput = true;
break;
}
}
if (!foundOutput) {
// Connected output not found
BTCLoader.error(String.format("Transaction input specifies non-existent output\n"+
" Transaction %s\n Transaction input %d\n"+
" Connected output %s\n Connected output index %d",
tx.getHash(), input.getIndex(), opHash, opIndex));
BTCLoader.dumpData("Failing Transaction", tx.getBytes());
txValid = false;
} else {
if (output.isSpent() && output.getHeight()!=0 && output.getHeight()<=junctionHeight) {
// Connected output has been spent
BTCLoader.error(String.format("Transaction input specifies spent output\n"+
" Transaction %s\n Transaction intput %d\n"+
" Connected output %s\n Connected output index %d",
tx.getHash(), input.getIndex(), opHash, opIndex));
txValid = false;
} else {
if (output.isCoinBase()) {
// Check for immature coinbase transaction output
int txDepth = BTCLoader.blockStore.getTxDepth(opHash);
txDepth += storedBlock.getHeight() - BTCLoader.blockStore.getChainHeight();
if (txDepth < BTCLoader.COINBASE_MATURITY) {
BTCLoader.error(String.format("Transaction input specifies immature coinbase output\n"+
" Transaction %s\n Transaction input %d\n"+
" Connected output %s\n Connected output index %d",
tx.getHash(), input.getIndex(), opHash, opIndex));
txValid = false;
}
}
if (txValid) {
// Update amounts
txAmount = txAmount.add(output.getValue());
output.setSpent(true);
output.setHeight(storedBlock.getHeight());
}
}
}
//
// Verify the transaction signature
//
if (txValid) {
try {
txValid = BitcoinConsensus.verifyScript(input, output);
if (!txValid) {
BTCLoader.error(String.format("Transaction failed signature verification\n"+
" Transaction %s\n Transaction input %d\n"+
" Outpoint %s\n Outpoint index %d",
tx.getHash(), input.getIndex(),
op.getHash(), op.getIndex()));
}
} catch (ScriptException exc) {
BTCLoader.warn(String.format("Unable to verify transaction input\n Tx %s",
tx.getHash()), exc);
txValid = false;
}
if (!txValid) {
BTCLoader.dumpData("Input Script", input.getScriptBytes());
BTCLoader.dumpData("output Script", output.getScriptBytes());
}
}
}
//
// Stop processing transaction inputs if this input failed to verify
//
if (!txValid){
break;
}
}
//
// Get the amount for each output and subtract it from the transaction total
//
if (txValid) {
List<TransactionOutput> outputs = tx.getOutputs();
for (TransactionOutput output : outputs)
txAmount = txAmount.subtract(output.getValue());
if (txAmount.compareTo(BigInteger.ZERO) < 0) {
BTCLoader.error(String.format("Transaction inputs less than transaction outputs\n Tx %s",
tx.getHash()));
txValid = false;
} else {
totalFees = totalFees.add(txAmount);
}
}
//
// Stop processing the block transactions if we already have a failed transaction
//
if (!txValid)
break;
}
//
// The coinbase amount must not exceed the block reward plus the transaction fees for the block.
// The block reward starts at 50 BTC and is cut in half every 210,000 blocks.
//
if (txValid) {
long divisor = 1<<((storedBlock.getHeight())/210000);
BigInteger blockReward = BigInteger.valueOf(5000000000L).divide(BigInteger.valueOf(divisor));
Transaction tx = block.getTransactions().get(0);
List<TransactionOutput> outputs = tx.getOutputs();
BigInteger txAmount = blockReward.add(totalFees);
for (TransactionOutput output : outputs)
txAmount = txAmount.subtract(output.getValue());
if (txAmount.compareTo(BigInteger.ZERO) < 0) {
BTCLoader.error(String.format("Coinbase transaction outputs exceed block reward plus fees\n Block %s",
block.getHashAsString()));
txValid = false;
}
}
return txValid;
}
}