package org.ripple.power.txns.btc; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; public class DatabaseHandler implements Runnable { private final Map<Sha256Hash, Sha256Hash> txMap = new HashMap<>(50); List<BlockStoreListener> listeners = new LinkedList<BlockStoreListener>(); /** Database timer */ private Timer timer; /** Timer task to delete spent outputs */ private TimerTask timerTask; /** Database shutdown requested */ private boolean databaseShutdown = false; /** 'getblocks' chain height */ private int getblocksHeight = 0; /** 'getblocks' time */ private long getblocksTime = 0; /** * Creates the database listener */ public DatabaseHandler() { } /** * Shutdown the database handler */ public void shutdown() { try { databaseShutdown = true; BTCLoader.databaseQueue.put(new ShutdownDatabase()); } catch (InterruptedException exc) { BTCLoader.warn("Database handler shutdown interrupted", exc); } } private int rescanHeight = 0; public void addListener(BlockStoreListener listener) { listeners.add(listener); } public void rescanChain(long rescanTime) throws BlockStoreException { rescanHeight = BTCLoader.blockStore.getRescanHeight(rescanTime); if (rescanHeight > 0) { BTCLoader.info(String.format( "Block chain rescan started at height %d", rescanHeight)); Sha256Hash blockHash = BTCLoader.blockStore .getBlockHash(rescanHeight); PeerRequest request = new PeerRequest(blockHash, InventoryItem.INV_FILTERED_BLOCK); synchronized (BTCLoader.lock) { BTCLoader.pendingRequests.add(request); } BTCLoader.networkHandler.wakeup(); } } /** * Starts the database listener running */ @Override public void run() { BTCLoader.info("Database handler started"); // // Create a timer to delete spent transaction outputs // timer = new Timer(); timerTask = new DeleteOutputsTask(); timer.schedule(timerTask, 15 * 60 * 1000); try { // // Handle any pending blocks before accepting new blocks // processPendingBlocks(); // // Process blocks until the shutdown() method is called // for (;;) { // // Get the next block from the database queue, blocking if no // block is available // Object obj = BTCLoader.databaseQueue.take(); if (databaseShutdown) { break; } if (obj instanceof Block) { // // Process the block // processBlock((Block) obj); // // Get the next group of blocks if we are synchronizing with // the network // int chainHeight = BTCLoader.blockStore.getChainHeight(); if (chainHeight < BTCLoader.networkChainHeight - 100 && (getblocksHeight < chainHeight - 300 || getblocksTime < System .currentTimeMillis() - 60000) && BTCLoader.networkHandler != null) { getblocksHeight = chainHeight; getblocksTime = System.currentTimeMillis(); BTCLoader.networkHandler.getBlocks(); } else if (obj instanceof BlockHeader) { processBlock(new StoredHeader((BlockHeader) obj)); if (BTCLoader.databaseQueue.isEmpty()) { if (BTCLoader.blockStore.getChainHeight() >= BTCLoader.networkChainHeight) { BTCLoader.loadingChain = false; } else { BTCLoader.networkHandler.getBlocks(); } } } else if (obj instanceof Transaction) { processTransaction((Transaction) obj); } } } } catch (InterruptedException exc) { BTCLoader.warn("Database handler interrupted", exc); } catch (Throwable exc) { BTCLoader.error("Runtime exception while processing blocks", exc); } // // Stopping // timerTask.cancel(); timer.cancel(); BTCLoader.info("Database handler stopped"); } private void processBlock(StoredHeader blockHeader) { Sha256Hash blockHash = blockHeader.getHash(); try { synchronized (BTCLoader.lock) { List<Sha256Hash> matches = blockHeader.getMatches(); if (matches != null) { for (Sha256Hash txHash : matches) { if (BTCLoader.blockStore.isNewTransaction(txHash)) { txMap.put(txHash, blockHash); } } } } if (BTCLoader.blockStore.isNewBlock(blockHash)) { BTCLoader.blockStore.storeHeader(blockHeader); updateChain(blockHeader); if (blockHeader.isOnChain()) { Sha256Hash parentHash = blockHash; while (parentHash != null) parentHash = processChildBlock(parentHash); } } else { BTCLoader.blockStore.updateMatches(blockHeader); if (rescanHeight != 0) { rescanHeight++; if (rescanHeight > BTCLoader.blockStore.getChainHeight()) { rescanHeight = 0; BTCLoader.info("Block rescan completed"); for (BlockStoreListener listener : listeners) { listener.rescanCompleted(); } } else { if (rescanHeight % 1000 == 0) BTCLoader.debug(String.format( "Block rescan at block %d", rescanHeight)); Sha256Hash nextHash = BTCLoader.blockStore .getBlockHash(rescanHeight); PeerRequest request = new PeerRequest(nextHash, InventoryItem.INV_FILTERED_BLOCK); synchronized (BTCLoader.lock) { BTCLoader.pendingRequests.add(request); } BTCLoader.networkHandler.wakeup(); } } else { StoredHeader chkHeader = BTCLoader.blockStore .getHeader(blockHash); if (!chkHeader.isOnChain()) { updateChain(blockHeader); if (blockHeader.isOnChain()) { Sha256Hash parentHash = blockHash; while (parentHash != null) parentHash = processChildBlock(parentHash); } } } } } catch (BlockNotFoundException exc) { PeerRequest request = new PeerRequest(exc.getHash(), InventoryItem.INV_FILTERED_BLOCK); boolean wakeup = false; synchronized (BTCLoader.lock) { if (!BTCLoader.pendingRequests.contains(request) && !BTCLoader.processedRequests.contains(request)) { BTCLoader.pendingRequests.add(request); wakeup = true; } } if (wakeup) BTCLoader.networkHandler.wakeup(); } catch (VerificationException exc) { BTCLoader.error( String.format("Checkpoint verification failed\n %s", exc.getHash()), exc); } catch (BlockStoreException exc) { BTCLoader.error( String.format("Unable to process block\n %s", blockHash.toString()), exc); } } private void updateChain(StoredHeader blockHeader) throws VerificationException, BlockStoreException { List<StoredHeader> chainList = BTCLoader.blockStore.getJunctionHeader(blockHeader .getPrevHash()); chainList.add(blockHeader); StoredHeader chainHeader = chainList.get(0); BigInteger chainWork = chainHeader.getChainWork(); int blockHeight = chainHeader.getBlockHeight(); for (int i = 1; i < chainList.size(); i++) { chainHeader = chainList.get(i); chainWork = chainWork.add(chainHeader.getBlockWork()); chainHeader.setChainWork(chainWork); chainHeader.setBlockHeight(++blockHeight); } if (blockHeader.getChainWork().compareTo( BTCLoader.blockStore.getChainWork()) > 0) { BTCLoader.blockStore.setChainStoredHead(chainList); for (int i = 1; i < chainList.size(); i++) { chainHeader = chainList.get(i); chainHeader.setChain(true); for (BlockStoreListener listener : listeners){ listener.addChainBlock(chainHeader); } } BTCLoader.networkChainHeight = Math.max( BTCLoader.networkChainHeight, blockHeader.getBlockHeight()); } else { BTCLoader .debug(String .format("Block not added to chain: New chain work %d, Current chain work %d\n Block %s", blockHeader.getChainWork(), BTCLoader.blockStore.getChainWork(), blockHeader.getHash())); } } private Sha256Hash processChildBlock(Sha256Hash parentHash) throws VerificationException, BlockStoreException { Sha256Hash nextParent = null; StoredHeader childHeader = BTCLoader.blockStore.getChildHeader(parentHash); if (childHeader != null && !childHeader.isOnChain()) { updateChain(childHeader); if (childHeader.isOnChain()){ nextParent = childHeader.getHash(); } } return nextParent; } /** * Process a block * * @param block * Block to process */ private void processBlock(Block block) { try { // // Process the new block // List<StoredBlock> chainList = null; StoredBlock storedBlock = BTCLoader.blockStore.getStoredBlock(block .getHash()); if (storedBlock == null) { // // Add a new block to our database // chainList = BTCLoader.blockChain.storeBlock(block); } else if (!storedBlock.isOnChain()) { // // Attempt to connect an existing block to the current block // chain // chainList = BTCLoader.blockChain.updateBlockChain(storedBlock); } // // Notify our peers that we have added new blocks to the chain and // then // see if we have a child block which can now be processed. To avoid // flooding peers with blocks they have already seen, we won't send // an // 'inv' message if we are more than 3 blocks behind the best // network chain. // if (chainList != null) { for (StoredBlock chainStoredBlock : chainList) { Block chainBlock = chainStoredBlock.getBlock(); if (chainBlock != null) { updateTxPool(chainBlock); int chainHeight = chainStoredBlock.getHeight(); BTCLoader.networkChainHeight = Math.max(chainHeight, BTCLoader.networkChainHeight); if (chainHeight >= BTCLoader.networkChainHeight - 3) notifyPeers(chainStoredBlock); } } StoredBlock parentBlock = chainList.get(chainList.size() - 1); while (parentBlock != null && !databaseShutdown) parentBlock = processChildBlock(parentBlock); } // // Remove the request from the processedRequests list // synchronized (BTCLoader.pendingRequests) { Iterator<PeerRequest> it = BTCLoader.processedRequests .iterator(); while (it.hasNext()) { PeerRequest request = it.next(); if (request.getType() == InventoryItem.INV_BLOCK && request.getHash().equals(block.getHash())) { it.remove(); break; } } } } catch (BlockStoreException exc) { BTCLoader.error(String.format( "Unable to store block in database\n Block %s", block.getHashAsString()), exc); } } /** * Connect pending blocks to the current block chain * * @throws BlockStoreException * Database error occurred */ private void processPendingBlocks() throws BlockStoreException { StoredBlock parentBlock = BTCLoader.blockStore .getStoredBlock(BTCLoader.blockStore.getChainHead()); while (parentBlock != null && !databaseShutdown) parentBlock = processChildBlock(parentBlock); } /** * Process a child block and see if it can now be added to the chain * * @param storedBlock * The updated block * @return Next parent block or null * @throws BlockStoreException * Database error occurred */ private StoredBlock processChildBlock(StoredBlock storedBlock) throws BlockStoreException { StoredBlock parentBlock = null; StoredBlock childStoredBlock = BTCLoader.blockStore .getChildStoredBlock(storedBlock.getHash()); if (childStoredBlock != null && !childStoredBlock.isOnChain()) { // // Update the chain with the child block // BTCLoader.blockChain.updateBlockChain(childStoredBlock); if (childStoredBlock.isOnChain()) { updateTxPool(childStoredBlock.getBlock()); // // Notify our peers about this block. To avoid // flooding peers with blocks they have already seen, we won't // send an // 'inv' message if we are more than 3 blocks behind the best // network chain. // int chainHeight = childStoredBlock.getHeight(); BTCLoader.networkChainHeight = Math.max(chainHeight, BTCLoader.networkChainHeight); if (chainHeight >= BTCLoader.networkChainHeight - 3) notifyPeers(childStoredBlock); // // Continue working our way up the chain // parentBlock = childStoredBlock; } } return parentBlock; } /** * Remove the transactions in the current block from the memory pool, update * the spent outputs map, and retry orphan transactions * * @param block * The current block * @throws BlockStoreException * Database error occurred */ private void updateTxPool(Block block) throws BlockStoreException { List<Transaction> txList = block.getTransactions(); List<StoredTransaction> retryList = new ArrayList<>(); synchronized (BTCLoader.txMap) { for(Transaction tx:txList){ Sha256Hash txHash = tx.getHash(); // // Remove the transaction from the transaction maps // BTCLoader.txMap.remove(txHash); BTCLoader.recentTxMap.remove(txHash); // // Remove spent outputs from the map since they are now // updated in the database // List<TransactionInput> txInputs = tx.getInputs(); for(TransactionInput txInput:txInputs){ BTCLoader.spentOutputsMap .remove(txInput.getOutPoint()); } // // Get orphan transactions dependent on this transaction // List<StoredTransaction> orphanList = BTCLoader.orphanTxMap .remove(txHash); if (orphanList != null) retryList.addAll(orphanList); } } // // Retry orphan transactions that are not in the database // for (StoredTransaction orphan : retryList) { if (BTCLoader.blockStore.isNewTransaction(orphan.getHash())) BTCLoader.networkMessageListener.retryOrphanTransaction(orphan .getTransaction()); } } /** * Notify peers when a block has been added to the chain * * @param storedBlock * The stored block added to the chain */ private void notifyPeers(StoredBlock storedBlock) { List<InventoryItem> invList = new ArrayList<>(1); invList.add(new InventoryItem(InventoryItem.INV_BLOCK, storedBlock .getHash())); Message invMsg = InventoryMessage.buildInventoryMessage(null, invList); invMsg.setInventoryType(InventoryItem.INV_BLOCK); BTCLoader.networkHandler.broadcastMessage(invMsg); } public void processTransaction(Transaction tx) { Sha256Hash txHash = tx.getHash(); Sha256Hash blockHash; long txTime; boolean txUpdated = false; try { synchronized (BTCLoader.lock) { blockHash = txMap.get(txHash); if (blockHash != null) txMap.remove(txHash); } if (blockHash != null) { StoredHeader blockHeader = BTCLoader.blockStore .getHeader(blockHash); txTime = blockHeader.getBlockTime(); if (!blockHeader.isOnChain()) blockHash = null; } else { txTime = System.currentTimeMillis() / 1000; } if (BTCLoader.blockStore.isNewTransaction(txHash)) { List<TransactionOutput> txOutputs = tx.getOutputs(); BigInteger totalValue = BigInteger.ZERO; BigInteger totalChange = BigInteger.ZERO; for (int txIndex = 0; txIndex < txOutputs.size(); txIndex++) { TransactionOutput txOutput = txOutputs.get(txIndex); totalValue = totalValue.add(txOutput.getValue()); ECKey key = (ECKey) checkAddress(txOutput, true); if (key != null) { if (key.isChange()) totalChange = totalChange.add(txOutput.getValue()); ReceiveTransaction rcvTx = new ReceiveTransaction( tx.getNormalizedID(), txHash, txIndex, txTime, blockHash, key.toAddress(), txOutput.getValue(), txOutput.getScriptBytes(), key.isChange(), tx.isCoinBase()); BTCLoader.blockStore.storeReceiveTx(rcvTx); txUpdated = true; } } boolean isRelevant = false; List<ReceiveTransaction> rcvList = BTCLoader.blockStore .getReceiveTxList(); List<TransactionInput> txInputs = tx.getInputs(); BigInteger totalInput = BigInteger.ZERO; for (TransactionInput txInput : txInputs) { OutPoint txOutPoint = txInput.getOutPoint(); for (ReceiveTransaction rcv : rcvList) { if (rcv.getTxHash().equals(txOutPoint.getHash()) && rcv.getTxIndex() == txOutPoint.getIndex()) { totalInput = totalInput.add(rcv.getValue()); BTCLoader.blockStore.setTxSpent(rcv.getTxHash(), rcv.getTxIndex(), true); isRelevant = true; txUpdated = true; break; } } } if (isRelevant) { Address address = null; for (TransactionOutput txOutput : txOutputs) { address = (Address) checkAddress(txOutput, false); if (address != null) { break; } } if (address != null) { BigInteger fee = totalInput.subtract(totalValue); BigInteger sentValue = totalValue.subtract(totalChange); SendTransaction sendTx = new SendTransaction( tx.getNormalizedID(), txHash, txTime - 15, blockHash, address, sentValue, fee, tx.getBytes()); BTCLoader.blockStore.storeSendTx(sendTx); } } if (txUpdated) { for (BlockStoreListener listener : listeners) { listener.txUpdated(); } } } } catch (BlockStoreException exc) { BTCLoader.error(String.format( "Unable to process transaction\n %s", txHash), exc); } } /** * Timer task to delete spent transaction outputs */ private class DeleteOutputsTask extends TimerTask { /** Task is active */ private volatile boolean isSleeping = false; /** Execution thread */ private volatile Thread thread; /** * Create the timer task */ public DeleteOutputsTask() { super(); } /** * Delete spent outputs every hour. The task will run until all spent * outputs are deleted before scheduling the next execution. 1000 * outputs will be deleted in each batch with a 30-second interval * between each database request. */ @Override public void run() { // // Indicate task is active // thread = Thread.currentThread(); try { // // Delete spent transaction outputs at 30 second intervals // int count; do { isSleeping = true; Thread.sleep(30000); isSleeping = false; if (databaseShutdown) { break; } count = BTCLoader.blockStore.deleteSpentTxOutputs(); } while (count > 0 && !databaseShutdown); // // Schedule the next execution in one hour // timerTask = new DeleteOutputsTask(); timer.schedule(timerTask, 60 * 60 * 1000); } catch (BlockStoreException exc) { BTCLoader.error("Unable to delete spent transaction outputs", exc); } catch (InterruptedException exc) { BTCLoader.info("Database prune task terminated"); } catch (Throwable exc) { BTCLoader .error("Unexpected exception while deleting spent transaction outputs", exc); } // // Indicate task is no longer active // thread = null; } /** * Cancel task execution * * @return TRUE if a future execution was cancelled */ @Override public boolean cancel() { // // Cancel the current execution // try { while (thread != null) { if (isSleeping) { thread.interrupt(); } Thread.sleep(1000); } } catch (InterruptedException exc) { BTCLoader .error("Unable to wait for database prune task to complete"); } // // Cancel future execution // return super.cancel(); } } private Object checkAddress(TransactionOutput txOutput, boolean ourAddress) { Object result = null; byte[] scriptBytes = txOutput.getScriptBytes(); if (scriptBytes.length == 25 && scriptBytes[0] == (byte) ScriptOpCodes.OP_DUP && scriptBytes[1] == (byte) ScriptOpCodes.OP_HASH160 && scriptBytes[2] == 20 && scriptBytes[23] == (byte) ScriptOpCodes.OP_EQUALVERIFY && scriptBytes[24] == (byte) ScriptOpCodes.OP_CHECKSIG) { byte[] scriptAddress = Arrays.copyOfRange(scriptBytes, 3, 23); synchronized (BTCLoader.lock) { for (ECKey chkKey : BTCLoader.keys) { if (Arrays.equals(chkKey.getPubKeyHash(), scriptAddress)) { result = chkKey; break; } } } if (!ourAddress) { if (result == null) { result = new Address(scriptAddress); } else if (((ECKey) result).isChange()) { result = null; } else { result = ((ECKey) result).toAddress(); } } } return result; } }