/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt; import nxt.crypto.Crypto; import nxt.db.DbIterator; import nxt.db.DerivedDbTable; import nxt.db.FilteringIterator; import nxt.db.FullTextTrigger; import nxt.peer.Peer; import nxt.peer.Peers; import nxt.util.Convert; import nxt.util.JSON; import nxt.util.Listener; import nxt.util.Listeners; import nxt.util.Logger; import nxt.util.ThreadPool; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONStreamAware; import org.json.simple.JSONValue; import java.math.BigInteger; import java.security.MessageDigest; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; final class BlockchainProcessorImpl implements BlockchainProcessor { private static final byte[] CHECKSUM_TRANSPARENT_FORGING = new byte[] { -122, -111, -35, 76, 59, 79, -75, 117, 34, 2, -70, -65, -38, 59, 0, 57, 120, 0, -107, 11, 97, -48, 21, 36, 48, -94, 88, 54, -14, 60, -101, -80 }; private static final byte[] CHECKSUM_NQT_BLOCK = Constants.isTestnet ? new byte[] { 110, -1, -56, -56, -58, 48, 43, 12, -41, -37, 90, -93, 80, 20, 3, -76, -84, -15, -113, -34, 30, 32, 57, 85, -30, 16, -10, 127, -101, 17, 121, 124 } : new byte[] { -90, -42, -57, -76, 88, -49, 127, 6, -47, -72, -39, -56, 51, 90, -90, -105, 121, 71, -94, -97, 49, -24, -12, 86, 7, -48, 90, -91, -24, -105, -17, -104 }; private static final byte[] CHECKSUM_MONETARY_SYSTEM_BLOCK = Constants.isTestnet ? new byte[] { 119, 51, 105, -101, -74, -49, -49, 19, 11, 103, -84, 80, -46, -5, 51, 42, 84, 88, 87, -115, -19, 104, 49, -93, -41, 84, -34, -92, 103, -48, 29, 44 } : new byte[] { -117, -101, 74, 111, -114, 39, 80, -67, 48, 86, 68, 106, -105, 2, 84, -109, 1, 4, -20, -82, -112, -112, 25, 119, 23, -113, 126, -121, -36, 15, -32, -24 }; private static final byte[] CHECKSUM_PHASING_BLOCK = Constants.isTestnet ? new byte[] { 4, -100, -26, 47, 93, 1, -114, 86, -42, 46, -103, 13, 120, 0, 2, 100, -52, -67, 109, -90, 87, 13, 30, -110, -58, -70, -94, 21, 105, -58, 20, 0 } : new byte[] { -88, -128, 68, -118, 10, -62, 110, 19, -73, 61, 34, -76, 35, 73, -101, 9, 33, -111, 40, 114, 27, 105, 54, 0, 16, -97, 115, -12, -110, -88, 1, -15 }; private static final byte[] CHECKSUM_16 = Constants.isTestnet ? new byte[] { -12, 21, 56, 106, -58, -126, 123, 33, 117, 11, -79, 28, -79, -45, 7, 69, 120, 71, -3, 27, 67, -85, 30, -25, -12, 127, 76, -60, -114, 41, -46, 55 } : new byte[] { 4, -96, 70, -17, 32, 17, 76, -92, 127, -127, 76, -77, 38, 7, 36, -113, 69, 26, -91, -94, -81, -70, 62, 30, 114, 63, -102, -55, -75, 25, -17, -12 }; private static final byte[] CHECKSUM_17 = Constants.isTestnet ? new byte[] { -19, -44, -49, 101, 5, -57, 51, 119, 16, 36, -3, 123, 90, -83, 89, 55, 72, 116, 4, 27, -14, 114, 28, 79, -104, 100, -74, 61, -64, -6, -53, 103 } : null; private static final BlockchainProcessorImpl instance = new BlockchainProcessorImpl(); static BlockchainProcessorImpl getInstance() { return instance; } private final BlockchainImpl blockchain = BlockchainImpl.getInstance(); private final ExecutorService networkService = Executors.newCachedThreadPool(); private final List<DerivedDbTable> derivedTables = new CopyOnWriteArrayList<>(); private final boolean trimDerivedTables = Nxt.getBooleanProperty("nxt.trimDerivedTables"); private final int defaultNumberOfForkConfirmations = Nxt.getIntProperty(Constants.isTestnet ? "nxt.testnetNumberOfForkConfirmations" : "nxt.numberOfForkConfirmations"); private int initialScanHeight; private volatile int lastTrimHeight; private volatile int lastRestoreTime = 0; private final Set<Long> prunableTransactions = new HashSet<>(); private final Listeners<Block, Event> blockListeners = new Listeners<>(); private volatile Peer lastBlockchainFeeder; private volatile int lastBlockchainFeederHeight; private volatile boolean getMoreBlocks = true; private volatile boolean isTrimming; private volatile boolean isScanning; private volatile boolean isDownloading; private volatile boolean isProcessingBlock; private volatile boolean isRestoring; private volatile boolean alreadyInitialized = false; private final Runnable getMoreBlocksThread = new Runnable() { private final JSONStreamAware getCumulativeDifficultyRequest; { JSONObject request = new JSONObject(); request.put("requestType", "getCumulativeDifficulty"); getCumulativeDifficultyRequest = JSON.prepareRequest(request); } private boolean peerHasMore; private List<Peer> connectedPublicPeers; private List<Long> chainBlockIds; private long totalTime = 1; private int totalBlocks; @Override public void run() { try { // // Download blocks until we are up-to-date // while (true) { if (!getMoreBlocks) { return; } int chainHeight = blockchain.getHeight(); downloadPeer(); if (blockchain.getHeight() == chainHeight) { if (isDownloading) { Logger.logMessage("Finished blockchain download"); isDownloading = false; } break; } } // // Restore prunable data // int now = Nxt.getEpochTime(); if (!isRestoring && !prunableTransactions.isEmpty() && now - lastRestoreTime > 60 * 60) { isRestoring = true; lastRestoreTime = now; networkService.submit(new RestorePrunableDataTask()); } } catch (InterruptedException e) { Logger.logDebugMessage("Blockchain download thread interrupted"); } catch (Throwable t) { Logger.logErrorMessage("CRITICAL ERROR. PLEASE REPORT TO THE DEVELOPERS.\n" + t.toString(), t); System.exit(1); } } private void downloadPeer() throws InterruptedException { try { long startTime = System.currentTimeMillis(); int numberOfForkConfirmations = blockchain.getHeight() > Constants.LAST_CHECKSUM_BLOCK - 720 ? defaultNumberOfForkConfirmations : Math.min(1, defaultNumberOfForkConfirmations); connectedPublicPeers = Peers.getPublicPeers(Peer.State.CONNECTED, true); if (connectedPublicPeers.size() <= numberOfForkConfirmations) { return; } peerHasMore = true; final Peer peer = Peers.getWeightedPeer(connectedPublicPeers); if (peer == null) { return; } JSONObject response = peer.send(getCumulativeDifficultyRequest); if (response == null) { return; } BigInteger curCumulativeDifficulty = blockchain.getLastBlock().getCumulativeDifficulty(); String peerCumulativeDifficulty = (String) response.get("cumulativeDifficulty"); if (peerCumulativeDifficulty == null) { return; } BigInteger betterCumulativeDifficulty = new BigInteger(peerCumulativeDifficulty); if (betterCumulativeDifficulty.compareTo(curCumulativeDifficulty) < 0) { return; } if (response.get("blockchainHeight") != null) { lastBlockchainFeeder = peer; lastBlockchainFeederHeight = ((Long) response.get("blockchainHeight")).intValue(); } if (betterCumulativeDifficulty.equals(curCumulativeDifficulty)) { return; } long commonMilestoneBlockId = Genesis.GENESIS_BLOCK_ID; if (blockchain.getLastBlock().getId() != Genesis.GENESIS_BLOCK_ID) { commonMilestoneBlockId = getCommonMilestoneBlockId(peer); } if (commonMilestoneBlockId == 0 || !peerHasMore) { return; } chainBlockIds = getBlockIdsAfterCommon(peer, commonMilestoneBlockId, false); if (chainBlockIds.size() < 2 || !peerHasMore) { return; } final long commonBlockId = chainBlockIds.get(0); final Block commonBlock = blockchain.getBlock(commonBlockId); if (commonBlock == null || blockchain.getHeight() - commonBlock.getHeight() >= 720) { return; } blockchain.updateLock(); try { if (betterCumulativeDifficulty.compareTo(blockchain.getLastBlock().getCumulativeDifficulty()) <= 0) { return; } long lastBlockId = blockchain.getLastBlock().getId(); downloadBlockchain(peer, commonBlock, commonBlock.getHeight()); if (blockchain.getHeight() - commonBlock.getHeight() <= 10) { return; } if (!isDownloading) { Logger.logMessage("Blockchain download in progress"); isDownloading = true; } int confirmations = 0; for (Peer otherPeer : connectedPublicPeers) { if (confirmations >= numberOfForkConfirmations) { break; } if (peer.getHost().equals(otherPeer.getHost())) { continue; } chainBlockIds = getBlockIdsAfterCommon(otherPeer, commonBlockId, true); if (chainBlockIds.isEmpty()) { continue; } long otherPeerCommonBlockId = chainBlockIds.get(0); if (otherPeerCommonBlockId == blockchain.getLastBlock().getId()) { confirmations++; continue; } Block otherPeerCommonBlock = blockchain.getBlock(otherPeerCommonBlockId); if (blockchain.getHeight() - otherPeerCommonBlock.getHeight() >= 720) { continue; } String otherPeerCumulativeDifficulty; JSONObject otherPeerResponse = peer.send(getCumulativeDifficultyRequest); if (otherPeerResponse == null || (otherPeerCumulativeDifficulty = (String) response.get("cumulativeDifficulty")) == null) { continue; } if (new BigInteger(otherPeerCumulativeDifficulty).compareTo(blockchain.getLastBlock().getCumulativeDifficulty()) <= 0) { continue; } Logger.logDebugMessage("Found a peer with better difficulty"); downloadBlockchain(otherPeer, otherPeerCommonBlock, commonBlock.getHeight()); } Logger.logDebugMessage("Got " + confirmations + " confirmations"); if (blockchain.getLastBlock().getId() != lastBlockId) { long time = System.currentTimeMillis() - startTime; totalTime += time; int numBlocks = blockchain.getHeight() - commonBlock.getHeight(); totalBlocks += numBlocks; Logger.logMessage("Downloaded " + numBlocks + " blocks in " + time / 1000 + " s, " + (totalBlocks * 1000) / totalTime + " per s, " + totalTime * (lastBlockchainFeederHeight - blockchain.getHeight()) / ((long) totalBlocks * 1000 * 60) + " min left"); } else { Logger.logDebugMessage("Did not accept peer's blocks, back to our own fork"); } } finally { blockchain.updateUnlock(); } } catch (NxtException.StopException e) { Logger.logMessage("Blockchain download stopped: " + e.getMessage()); throw new InterruptedException("Blockchain download stopped"); } catch (Exception e) { Logger.logMessage("Error in blockchain download thread", e); } } private long getCommonMilestoneBlockId(Peer peer) { String lastMilestoneBlockId = null; while (true) { JSONObject milestoneBlockIdsRequest = new JSONObject(); milestoneBlockIdsRequest.put("requestType", "getMilestoneBlockIds"); if (lastMilestoneBlockId == null) { milestoneBlockIdsRequest.put("lastBlockId", blockchain.getLastBlock().getStringId()); } else { milestoneBlockIdsRequest.put("lastMilestoneBlockId", lastMilestoneBlockId); } JSONObject response = peer.send(JSON.prepareRequest(milestoneBlockIdsRequest)); if (response == null) { return 0; } JSONArray milestoneBlockIds = (JSONArray) response.get("milestoneBlockIds"); if (milestoneBlockIds == null) { return 0; } if (milestoneBlockIds.isEmpty()) { return Genesis.GENESIS_BLOCK_ID; } // prevent overloading with blockIds if (milestoneBlockIds.size() > 20) { Logger.logDebugMessage("Obsolete or rogue peer " + peer.getHost() + " sends too many milestoneBlockIds, blacklisting"); peer.blacklist("Too many milestoneBlockIds"); return 0; } if (Boolean.TRUE.equals(response.get("last"))) { peerHasMore = false; } for (Object milestoneBlockId : milestoneBlockIds) { long blockId = Convert.parseUnsignedLong((String) milestoneBlockId); if (BlockDb.hasBlock(blockId)) { if (lastMilestoneBlockId == null && milestoneBlockIds.size() > 1) { peerHasMore = false; } return blockId; } lastMilestoneBlockId = (String) milestoneBlockId; } } } private List<Long> getBlockIdsAfterCommon(final Peer peer, final long startBlockId, final boolean countFromStart) { long matchId = startBlockId; List<Long> blockList = new ArrayList<>(720); boolean matched = false; int limit = countFromStart ? 720 : 1440; while (true) { JSONObject request = new JSONObject(); request.put("requestType", "getNextBlockIds"); request.put("blockId", Long.toUnsignedString(matchId)); request.put("limit", limit); JSONObject response = peer.send(JSON.prepareRequest(request)); if (response == null) { return Collections.emptyList(); } JSONArray nextBlockIds = (JSONArray) response.get("nextBlockIds"); if (nextBlockIds == null || nextBlockIds.size() == 0) { break; } // prevent overloading with blockIds if (nextBlockIds.size() > limit) { Logger.logDebugMessage("Obsolete or rogue peer " + peer.getHost() + " sends too many nextBlockIds, blacklisting"); peer.blacklist("Too many nextBlockIds"); return Collections.emptyList(); } boolean matching = true; int count = 0; for (Object nextBlockId : nextBlockIds) { long blockId = Convert.parseUnsignedLong((String)nextBlockId); if (matching) { if (BlockDb.hasBlock(blockId)) { matchId = blockId; matched = true; } else { blockList.add(matchId); blockList.add(blockId); matching = false; } } else { blockList.add(blockId); if (blockList.size() >= 720) { break; } } if (countFromStart && ++count >= 720) { break; } } if (!matching || countFromStart) { break; } } if (blockList.isEmpty() && matched) { blockList.add(matchId); } return blockList; } /** * Download the block chain * * @param feederPeer Peer supplying the blocks list * @param commonBlock Common block * @throws InterruptedException Download interrupted */ private void downloadBlockchain(final Peer feederPeer, final Block commonBlock, final int startHeight) throws InterruptedException { Map<Long, PeerBlock> blockMap = new HashMap<>(); // // Break the download into multiple segments. The first block in each segment // is the common block for that segment. // List<GetNextBlocks> getList = new ArrayList<>(); int segSize = 36; int stop = chainBlockIds.size() - 1; for (int start = 0; start < stop; start += segSize) { getList.add(new GetNextBlocks(chainBlockIds, start, Math.min(start + segSize, stop))); } int nextPeerIndex = ThreadLocalRandom.current().nextInt(connectedPublicPeers.size()); long maxResponseTime = 0; Peer slowestPeer = null; // // Issue the getNextBlocks requests and get the results. We will repeat // a request if the peer didn't respond or returned a partial block list. // The download will be aborted if we are unable to get a segment after // retrying with different peers. // download: while (!getList.isEmpty()) { // // Submit threads to issue 'getNextBlocks' requests. The first segment // will always be sent to the feeder peer. Subsequent segments will // be sent to the feeder peer if we failed trying to download the blocks // from another peer. We will stop the download and process any pending // blocks if we are unable to download a segment from the feeder peer. // for (GetNextBlocks nextBlocks : getList) { Peer peer; if (nextBlocks.getRequestCount() > 1) { break download; } if (nextBlocks.getStart() == 0 || nextBlocks.getRequestCount() != 0) { peer = feederPeer; } else { if (nextPeerIndex >= connectedPublicPeers.size()) { nextPeerIndex = 0; } peer = connectedPublicPeers.get(nextPeerIndex++); } if (nextBlocks.getPeer() == peer) { break download; } nextBlocks.setPeer(peer); Future<List<BlockImpl>> future = networkService.submit(nextBlocks); nextBlocks.setFuture(future); } // // Get the results. A peer is on a different fork if a returned // block is not in the block identifier list. // Iterator<GetNextBlocks> it = getList.iterator(); while (it.hasNext()) { GetNextBlocks nextBlocks = it.next(); List<BlockImpl> blockList; try { blockList = nextBlocks.getFuture().get(); } catch (ExecutionException exc) { throw new RuntimeException(exc.getMessage(), exc); } if (blockList == null) { nextBlocks.getPeer().deactivate(); continue; } Peer peer = nextBlocks.getPeer(); int index = nextBlocks.getStart() + 1; for (BlockImpl block : blockList) { if (block.getId() != chainBlockIds.get(index)) { break; } blockMap.put(block.getId(), new PeerBlock(peer, block)); index++; } if (index > nextBlocks.getStop()) { it.remove(); } else { nextBlocks.setStart(index - 1); } if (nextBlocks.getResponseTime() > maxResponseTime) { maxResponseTime = nextBlocks.getResponseTime(); slowestPeer = nextBlocks.getPeer(); } } } if (slowestPeer != null && connectedPublicPeers.size() >= Peers.maxNumberOfConnectedPublicPeers && chainBlockIds.size() > 360) { Logger.logDebugMessage(slowestPeer.getHost() + " took " + maxResponseTime + " ms, disconnecting"); slowestPeer.deactivate(); } // // Add the new blocks to the blockchain. We will stop if we encounter // a missing block (this will happen if an invalid block is encountered // when downloading the blocks) // blockchain.writeLock(); try { List<BlockImpl> forkBlocks = new ArrayList<>(); for (int index = 1; index < chainBlockIds.size() && blockchain.getHeight() - startHeight < 720; index++) { PeerBlock peerBlock = blockMap.get(chainBlockIds.get(index)); if (peerBlock == null) { break; } BlockImpl block = peerBlock.getBlock(); if (blockchain.getLastBlock().getId() == block.getPreviousBlockId()) { try { pushBlock(block); } catch (BlockNotAcceptedException e) { peerBlock.getPeer().blacklist(e); } } else { forkBlocks.add(block); } } // // Process a fork // if (!forkBlocks.isEmpty() && blockchain.getHeight() - startHeight < 720) { Logger.logDebugMessage("Will process a fork of " + forkBlocks.size() + " blocks"); processFork(feederPeer, forkBlocks, commonBlock); } } finally { blockchain.writeUnlock(); } } private void processFork(final Peer peer, final List<BlockImpl> forkBlocks, final Block commonBlock) { BigInteger curCumulativeDifficulty = blockchain.getLastBlock().getCumulativeDifficulty(); List<BlockImpl> myPoppedOffBlocks = popOffTo(commonBlock); int pushedForkBlocks = 0; if (blockchain.getLastBlock().getId() == commonBlock.getId()) { for (BlockImpl block : forkBlocks) { if (blockchain.getLastBlock().getId() == block.getPreviousBlockId()) { try { pushBlock(block); pushedForkBlocks += 1; } catch (BlockNotAcceptedException e) { peer.blacklist(e); break; } } } } if (pushedForkBlocks > 0 && blockchain.getLastBlock().getCumulativeDifficulty().compareTo(curCumulativeDifficulty) < 0) { Logger.logDebugMessage("Pop off caused by peer " + peer.getHost() + ", blacklisting"); peer.blacklist("Pop off"); List<BlockImpl> peerPoppedOffBlocks = popOffTo(commonBlock); pushedForkBlocks = 0; for (BlockImpl block : peerPoppedOffBlocks) { TransactionProcessorImpl.getInstance().processLater(block.getTransactions()); } } if (pushedForkBlocks == 0) { Logger.logDebugMessage("Didn't accept any blocks, pushing back my previous blocks"); for (int i = myPoppedOffBlocks.size() - 1; i >= 0; i--) { BlockImpl block = myPoppedOffBlocks.remove(i); try { pushBlock(block); } catch (BlockNotAcceptedException e) { Logger.logErrorMessage("Popped off block no longer acceptable: " + block.getJSONObject().toJSONString(), e); break; } } } else { Logger.logDebugMessage("Switched to peer's fork"); for (BlockImpl block : myPoppedOffBlocks) { TransactionProcessorImpl.getInstance().processLater(block.getTransactions()); } } } }; /** * Callable method to get the next block segment from the selected peer */ private static class GetNextBlocks implements Callable<List<BlockImpl>> { /** Callable future */ private Future<List<BlockImpl>> future; /** Peer */ private Peer peer; /** Block identifier list */ private final List<Long> blockIds; /** Start index */ private int start; /** Stop index */ private int stop; /** Request count */ private int requestCount; /** Time it took to return getNextBlocks */ private long responseTime; /** * Create the callable future * * @param blockIds Block identifier list * @param start Start index within the list * @param stop Stop index within the list */ public GetNextBlocks(List<Long> blockIds, int start, int stop) { this.blockIds = blockIds; this.start = start; this.stop = stop; this.requestCount = 0; } /** * Return the result * * @return List of blocks or null if an error occurred */ @Override public List<BlockImpl> call() { requestCount++; // // Build the block request list // JSONArray idList = new JSONArray(); for (int i = start + 1; i <= stop; i++) { idList.add(Long.toUnsignedString(blockIds.get(i))); } JSONObject request = new JSONObject(); request.put("requestType", "getNextBlocks"); request.put("blockIds", idList); request.put("blockId", Long.toUnsignedString(blockIds.get(start))); long startTime = System.currentTimeMillis(); JSONObject response = peer.send(JSON.prepareRequest(request), 10 * 1024 * 1024); responseTime = System.currentTimeMillis() - startTime; if (response == null) { return null; } // // Get the list of blocks. We will stop parsing blocks if we encounter // an invalid block. We will return the valid blocks and reset the stop // index so no more blocks will be processed. // List<JSONObject> nextBlocks = (List<JSONObject>)response.get("nextBlocks"); if (nextBlocks == null) return null; if (nextBlocks.size() > 36) { Logger.logDebugMessage("Obsolete or rogue peer " + peer.getHost() + " sends too many nextBlocks, blacklisting"); peer.blacklist("Too many nextBlocks"); return null; } List<BlockImpl> blockList = new ArrayList<>(nextBlocks.size()); try { int count = stop - start; for (JSONObject blockData : nextBlocks) { blockList.add(BlockImpl.parseBlock(blockData)); if (--count <= 0) break; } } catch (RuntimeException | NxtException.NotValidException e) { Logger.logDebugMessage("Failed to parse block: " + e.toString(), e); peer.blacklist(e); stop = start + blockList.size(); } return blockList; } /** * Return the callable future * * @return Callable future */ public Future<List<BlockImpl>> getFuture() { return future; } /** * Set the callable future * * @param future Callable future */ public void setFuture(Future<List<BlockImpl>> future) { this.future = future; } /** * Return the peer * * @return Peer */ public Peer getPeer() { return peer; } /** * Set the peer * * @param peer Peer */ public void setPeer(Peer peer) { this.peer = peer; } /** * Return the start index * * @return Start index */ public int getStart() { return start; } /** * Set the start index * * @param start Start index */ public void setStart(int start) { this.start = start; } /** * Return the stop index * * @return Stop index */ public int getStop() { return stop; } /** * Return the request count * * @return Request count */ public int getRequestCount() { return requestCount; } /** * Return the response time * * @return Response time */ public long getResponseTime() { return responseTime; } } /** * Block returned by a peer */ private static class PeerBlock { /** Peer */ private final Peer peer; /** Block */ private final BlockImpl block; /** * Create the peer block * * @param peer Peer * @param block Block */ public PeerBlock(Peer peer, BlockImpl block) { this.peer = peer; this.block = block; } /** * Return the peer * * @return Peer */ public Peer getPeer() { return peer; } /** * Return the block * * @return Block */ public BlockImpl getBlock() { return block; } } /** * Task to restore prunable data for downloaded blocks */ private class RestorePrunableDataTask implements Runnable { @Override public void run() { Peer peer = null; try { // // Locate an archive peer // List<Peer> peers = Peers.getPeers(chkPeer -> chkPeer.providesService(Peer.Service.PRUNABLE) && !chkPeer.isBlacklisted() && chkPeer.getAnnouncedAddress() != null); while (!peers.isEmpty()) { Peer chkPeer = peers.get(ThreadLocalRandom.current().nextInt(peers.size())); if (chkPeer.getState() != Peer.State.CONNECTED) { Peers.connectPeer(chkPeer); } if (chkPeer.getState() == Peer.State.CONNECTED) { peer = chkPeer; break; } } if (peer == null) { Logger.logDebugMessage("Cannot find any archive peers"); return; } Logger.logDebugMessage("Connected to archive peer " + peer.getHost()); // // Make a copy of the prunable transaction list so we can remove entries // as we process them while still retaining the entry if we need to // retry later using a different archive peer // Set<Long> processing; synchronized (prunableTransactions) { processing = new HashSet<>(prunableTransactions.size()); processing.addAll(prunableTransactions); } Logger.logDebugMessage("Need to restore " + processing.size() + " pruned data"); // // Request transactions in batches of 100 until all transactions have been processed // while (!processing.isEmpty()) { // // Get the pruned transactions from the archive peer // JSONObject request = new JSONObject(); JSONArray requestList = new JSONArray(); synchronized (prunableTransactions) { Iterator<Long> it = processing.iterator(); while (it.hasNext()) { long id = it.next(); requestList.add(Long.toUnsignedString(id)); it.remove(); if (requestList.size() == 100) break; } } request.put("requestType", "getTransactions"); request.put("transactionIds", requestList); JSONObject response = peer.send(JSON.prepareRequest(request)); if (response == null) { return; } // // Restore the prunable data // JSONArray transactions = (JSONArray)response.get("transactions"); if (transactions == null || transactions.isEmpty()) { return; } List<Transaction> processed = Nxt.getTransactionProcessor().restorePrunableData(transactions); // // Remove transactions that have been successfully processed // synchronized (prunableTransactions) { processed.forEach(transaction -> prunableTransactions.remove(transaction.getId())); } } Logger.logDebugMessage("Done retrieving prunable transactions from " + peer.getHost()); } catch (NxtException.ValidationException e) { Logger.logErrorMessage("Peer " + peer.getHost() + " returned invalid prunable transaction", e); peer.blacklist(e); } catch (RuntimeException e) { Logger.logErrorMessage("Unable to restore prunable data", e); } finally { isRestoring = false; Logger.logDebugMessage("Remaining " + prunableTransactions.size() + " pruned transactions"); } } } private final Listener<Block> checksumListener = block -> { if (block.getHeight() == Constants.TRANSPARENT_FORGING_BLOCK && ! verifyChecksum(CHECKSUM_TRANSPARENT_FORGING, 0, Constants.TRANSPARENT_FORGING_BLOCK)) { popOffTo(0); } if (block.getHeight() == Constants.NQT_BLOCK && ! verifyChecksum(CHECKSUM_NQT_BLOCK, Constants.TRANSPARENT_FORGING_BLOCK, Constants.NQT_BLOCK)) { popOffTo(Constants.TRANSPARENT_FORGING_BLOCK); } if (block.getHeight() == Constants.MONETARY_SYSTEM_BLOCK && ! verifyChecksum(CHECKSUM_MONETARY_SYSTEM_BLOCK, Constants.NQT_BLOCK, Constants.MONETARY_SYSTEM_BLOCK)) { popOffTo(Constants.NQT_BLOCK); } if (block.getHeight() == Constants.PHASING_BLOCK && ! verifyChecksum(CHECKSUM_PHASING_BLOCK, Constants.MONETARY_SYSTEM_BLOCK, Constants.PHASING_BLOCK)) { popOffTo(Constants.MONETARY_SYSTEM_BLOCK); } if (block.getHeight() == Constants.CHECKSUM_BLOCK_16 && ! verifyChecksum(CHECKSUM_16, Constants.PHASING_BLOCK, Constants.CHECKSUM_BLOCK_16)) { popOffTo(Constants.PHASING_BLOCK); } if (block.getHeight() == Constants.CHECKSUM_BLOCK_17 && ! verifyChecksum(CHECKSUM_17, Constants.CHECKSUM_BLOCK_16, Constants.CHECKSUM_BLOCK_17)) { popOffTo(Constants.CHECKSUM_BLOCK_16); } }; private BlockchainProcessorImpl() { final int trimFrequency = Nxt.getIntProperty("nxt.trimFrequency"); blockListeners.addListener(block -> { if (block.getHeight() % 5000 == 0) { Logger.logMessage("processed block " + block.getHeight()); } if (trimDerivedTables && block.getHeight() % trimFrequency == 0) { doTrimDerivedTables(); } }, Event.BLOCK_SCANNED); blockListeners.addListener(block -> { if (trimDerivedTables && block.getHeight() % trimFrequency == 0 && !isTrimming) { isTrimming = true; networkService.submit(() -> { trimDerivedTables(); isTrimming = false; }); } if (block.getHeight() % 5000 == 0) { Logger.logMessage("received block " + block.getHeight()); if (!isDownloading || block.getHeight() % 50000 == 0) { networkService.submit(Db.db::analyzeTables); } } }, Event.BLOCK_PUSHED); blockListeners.addListener(checksumListener, Event.BLOCK_PUSHED); blockListeners.addListener(block -> Db.db.analyzeTables(), Event.RESCAN_END); ThreadPool.runBeforeStart(() -> { alreadyInitialized = true; if (addGenesisBlock()) { scan(0, false); } else if (Nxt.getBooleanProperty("nxt.forceScan")) { scan(0, Nxt.getBooleanProperty("nxt.forceValidate")); } else { boolean rescan; boolean validate; int height; try (Connection con = Db.db.getConnection(); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM scan")) { rs.next(); rescan = rs.getBoolean("rescan"); validate = rs.getBoolean("validate"); height = rs.getInt("height"); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } if (rescan) { scan(height, validate); } } }, false); ThreadPool.scheduleThread("GetMoreBlocks", getMoreBlocksThread, 1); } @Override public boolean addListener(Listener<Block> listener, BlockchainProcessor.Event eventType) { return blockListeners.addListener(listener, eventType); } @Override public boolean removeListener(Listener<Block> listener, Event eventType) { return blockListeners.removeListener(listener, eventType); } @Override public void registerDerivedTable(DerivedDbTable table) { if (alreadyInitialized) { throw new IllegalStateException("Too late to register table " + table + ", must have done it in Nxt.Init"); } derivedTables.add(table); } @Override public void trimDerivedTables() { try { Db.db.beginTransaction(); doTrimDerivedTables(); Db.db.commitTransaction(); } catch (Exception e) { Logger.logMessage(e.toString(), e); Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); } } private void doTrimDerivedTables() { lastTrimHeight = Math.max(blockchain.getHeight() - Constants.MAX_ROLLBACK, 0); if (lastTrimHeight > 0) { for (DerivedDbTable table : derivedTables) { blockchain.readLock(); try { table.trim(lastTrimHeight); Db.db.commitTransaction(); } finally { blockchain.readUnlock(); } } } } List<DerivedDbTable> getDerivedTables() { return derivedTables; } @Override public Peer getLastBlockchainFeeder() { return lastBlockchainFeeder; } @Override public int getLastBlockchainFeederHeight() { return lastBlockchainFeederHeight; } @Override public boolean isScanning() { return isScanning; } @Override public int getInitialScanHeight() { return initialScanHeight; } @Override public boolean isDownloading() { return isDownloading; } @Override public boolean isProcessingBlock() { return isProcessingBlock; } @Override public int getMinRollbackHeight() { return trimDerivedTables ? (lastTrimHeight > 0 ? lastTrimHeight : Math.max(blockchain.getHeight() - Constants.MAX_ROLLBACK, 0)) : 0; } @Override public void processPeerBlock(JSONObject request) throws NxtException { BlockImpl block = BlockImpl.parseBlock(request); BlockImpl lastBlock = blockchain.getLastBlock(); if (block.getPreviousBlockId() == lastBlock.getId()) { pushBlock(block); } else if (block.getPreviousBlockId() == lastBlock.getPreviousBlockId() && block.getTimestamp() < lastBlock.getTimestamp()) { blockchain.writeLock(); try { if (lastBlock.getId() != blockchain.getLastBlock().getId()) { return; // blockchain changed, ignore the block } BlockImpl previousBlock = blockchain.getBlock(lastBlock.getPreviousBlockId()); lastBlock = popOffTo(previousBlock).get(0); try { pushBlock(block); TransactionProcessorImpl.getInstance().processLater(lastBlock.getTransactions()); Logger.logDebugMessage("Last block " + lastBlock.getStringId() + " was replaced by " + block.getStringId()); } catch (BlockNotAcceptedException e) { Logger.logDebugMessage("Replacement block failed to be accepted, pushing back our last block"); pushBlock(lastBlock); TransactionProcessorImpl.getInstance().processLater(block.getTransactions()); } } finally { blockchain.writeUnlock(); } } // else ignore the block } @Override public List<BlockImpl> popOffTo(int height) { if (height <= 0) { fullReset(); } else if (height < blockchain.getHeight()) { return popOffTo(blockchain.getBlockAtHeight(height)); } return Collections.emptyList(); } @Override public void fullReset() { blockchain.writeLock(); try { try { setGetMoreBlocks(false); scheduleScan(0, false); //BlockDb.deleteBlock(Genesis.GENESIS_BLOCK_ID); // fails with stack overflow in H2 BlockDb.deleteAll(); if (addGenesisBlock()) { scan(0, false); } } finally { setGetMoreBlocks(true); } } finally { blockchain.writeUnlock(); } } @Override public void setGetMoreBlocks(boolean getMoreBlocks) { this.getMoreBlocks = getMoreBlocks; } @Override public int restorePrunedData() { Db.db.beginTransaction(); try (Connection con = Db.db.getConnection()) { int now = Nxt.getEpochTime(); int minTimestamp = Math.max(1, now - Constants.MAX_PRUNABLE_LIFETIME); int maxTimestamp = Math.max(minTimestamp, now - Constants.MIN_PRUNABLE_LIFETIME) - 1; List<TransactionDb.PrunableTransaction> transactionList = TransactionDb.findPrunableTransactions(con, minTimestamp, maxTimestamp); transactionList.forEach(prunableTransaction -> { long id = prunableTransaction.getId(); if ((prunableTransaction.hasPrunableAttachment() && prunableTransaction.getTransactionType().isPruned(id)) || PrunableMessage.isPruned(id, prunableTransaction.hasPrunablePlainMessage(), prunableTransaction.hasPrunableEncryptedMessage())) { synchronized (prunableTransactions) { prunableTransactions.add(id); } } }); if (!prunableTransactions.isEmpty()) { lastRestoreTime = 0; } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } finally { Db.db.endTransaction(); } synchronized (prunableTransactions) { return prunableTransactions.size(); } } @Override public Transaction restorePrunedTransaction(long transactionId) { TransactionImpl transaction = TransactionDb.findTransaction(transactionId); boolean isPruned = false; for (Appendix.AbstractAppendix appendage : transaction.getAppendages(true)) { if ((appendage instanceof Appendix.Prunable) && !((Appendix.Prunable)appendage).hasPrunableData()) { isPruned = true; break; } } if (!isPruned) { return transaction; } List<Peer> peers = Peers.getPeers(chkPeer -> chkPeer.providesService(Peer.Service.PRUNABLE) && !chkPeer.isBlacklisted() && chkPeer.getAnnouncedAddress() != null); if (peers.isEmpty()) { Logger.logDebugMessage("Cannot find any archive peers"); return null; } JSONObject json = new JSONObject(); JSONArray requestList = new JSONArray(); requestList.add(Long.toUnsignedString(transactionId)); json.put("requestType", "getTransactions"); json.put("transactionIds", requestList); JSONStreamAware request = JSON.prepareRequest(json); for (Peer peer : peers) { if (peer.getState() != Peer.State.CONNECTED) { Peers.connectPeer(peer); } if (peer.getState() != Peer.State.CONNECTED) { continue; } Logger.logDebugMessage("Connected to archive peer " + peer.getHost()); JSONObject response = peer.send(request); if (response == null) { continue; } JSONArray transactions = (JSONArray)response.get("transactions"); if (transactions == null || transactions.isEmpty()) { continue; } try { List<Transaction> processed = Nxt.getTransactionProcessor().restorePrunableData(transactions); if (processed.isEmpty()) { continue; } synchronized (prunableTransactions) { prunableTransactions.remove(transactionId); } return processed.get(0); } catch (NxtException.NotValidException e) { Logger.logErrorMessage("Peer " + peer.getHost() + " returned invalid prunable transaction", e); peer.blacklist(e); } } return null; } private void addBlock(BlockImpl block) { try (Connection con = Db.db.getConnection()) { BlockDb.saveBlock(con, block); blockchain.setLastBlock(block); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } private boolean addGenesisBlock() { if (BlockDb.hasBlock(Genesis.GENESIS_BLOCK_ID, 0)) { Logger.logMessage("Genesis block already in database"); BlockImpl lastBlock = BlockDb.findLastBlock(); blockchain.setLastBlock(lastBlock); popOffTo(lastBlock); Logger.logMessage("Last block height: " + lastBlock.getHeight()); return false; } Logger.logMessage("Genesis block not in database, starting from scratch"); try { List<TransactionImpl> transactions = new ArrayList<>(); for (int i = 0; i < Genesis.GENESIS_RECIPIENTS.length; i++) { TransactionImpl transaction = new TransactionImpl.BuilderImpl((byte) 0, Genesis.CREATOR_PUBLIC_KEY, Genesis.GENESIS_AMOUNTS[i] * Constants.ONE_NXT, 0, (short) 0, Attachment.ORDINARY_PAYMENT) .timestamp(0) .recipientId(Genesis.GENESIS_RECIPIENTS[i]) .signature(Genesis.GENESIS_SIGNATURES[i]) .height(0) .ecBlockHeight(0) .ecBlockId(0) .build(); transactions.add(transaction); } Collections.sort(transactions, Comparator.comparingLong(Transaction::getId)); MessageDigest digest = Crypto.sha256(); for (TransactionImpl transaction : transactions) { digest.update(transaction.bytes()); } BlockImpl genesisBlock = new BlockImpl(-1, 0, 0, Constants.MAX_BALANCE_NQT, 0, transactions.size() * 128, digest.digest(), Genesis.CREATOR_PUBLIC_KEY, new byte[64], Genesis.GENESIS_BLOCK_SIGNATURE, null, transactions); genesisBlock.setPrevious(null); addBlock(genesisBlock); return true; } catch (NxtException.ValidationException e) { Logger.logMessage(e.getMessage()); throw new RuntimeException(e.toString(), e); } } private void pushBlock(final BlockImpl block) throws BlockNotAcceptedException { int curTime = Nxt.getEpochTime(); blockchain.writeLock(); try { BlockImpl previousLastBlock = null; try { Db.db.beginTransaction(); previousLastBlock = blockchain.getLastBlock(); validate(block, previousLastBlock, curTime); long nextHitTime = Generator.getNextHitTime(previousLastBlock.getId(), curTime); if (nextHitTime > 0 && block.getTimestamp() > nextHitTime + 1) { String msg = "Rejecting block " + block.getStringId() + " at height " + previousLastBlock.getHeight() + " block timestamp " + block.getTimestamp() + " next hit time " + nextHitTime + " current time " + curTime; Logger.logDebugMessage(msg); Generator.setDelay(-Constants.FORGING_SPEEDUP); throw new BlockOutOfOrderException(msg, block); } Map<TransactionType, Map<String, Integer>> duplicates = new HashMap<>(); List<TransactionImpl> validPhasedTransactions = new ArrayList<>(); List<TransactionImpl> invalidPhasedTransactions = new ArrayList<>(); validatePhasedTransactions(previousLastBlock.getHeight(), validPhasedTransactions, invalidPhasedTransactions, duplicates); validateTransactions(block, previousLastBlock, curTime, duplicates, previousLastBlock.getHeight() >= Constants.LAST_CHECKSUM_BLOCK); block.setPrevious(previousLastBlock); blockListeners.notify(block, Event.BEFORE_BLOCK_ACCEPT); TransactionProcessorImpl.getInstance().requeueAllUnconfirmedTransactions(); addBlock(block); accept(block, validPhasedTransactions, invalidPhasedTransactions, duplicates); Db.db.commitTransaction(); } catch (Exception e) { Db.db.rollbackTransaction(); blockchain.setLastBlock(previousLastBlock); throw e; } finally { Db.db.endTransaction(); } blockListeners.notify(block, Event.AFTER_BLOCK_ACCEPT); } finally { blockchain.writeUnlock(); } if (block.getTimestamp() >= curTime - (Constants.MAX_TIMEDRIFT + Constants.FORGING_DELAY)) { Peers.sendToSomePeers(block); } blockListeners.notify(block, Event.BLOCK_PUSHED); } private void validatePhasedTransactions(int height, List<TransactionImpl> validPhasedTransactions, List<TransactionImpl> invalidPhasedTransactions, Map<TransactionType, Map<String, Integer>> duplicates) { if (height >= Constants.PHASING_BLOCK) { try (DbIterator<TransactionImpl> phasedTransactions = PhasingPoll.getFinishingTransactions(height + 1)) { for (TransactionImpl phasedTransaction : phasedTransactions) { if (height > Constants.SHUFFLING_BLOCK && PhasingPoll.getResult(phasedTransaction.getId()) != null) { continue; } try { phasedTransaction.validate(); if (!phasedTransaction.attachmentIsDuplicate(duplicates, false)) { validPhasedTransactions.add(phasedTransaction); } else { Logger.logDebugMessage("At height " + height + " phased transaction " + phasedTransaction.getStringId() + " is duplicate, will not apply"); invalidPhasedTransactions.add(phasedTransaction); } } catch (NxtException.ValidationException e) { Logger.logDebugMessage("At height " + height + " phased transaction " + phasedTransaction.getStringId() + " no longer passes validation: " + e.getMessage() + ", will not apply"); invalidPhasedTransactions.add(phasedTransaction); } } } } } private void validate(BlockImpl block, BlockImpl previousLastBlock, int curTime) throws BlockNotAcceptedException { if (previousLastBlock.getId() != block.getPreviousBlockId()) { throw new BlockOutOfOrderException("Previous block id doesn't match", block); } if (block.getVersion() != getBlockVersion(previousLastBlock.getHeight())) { throw new BlockNotAcceptedException("Invalid version " + block.getVersion(), block); } if (block.getTimestamp() > curTime + Constants.MAX_TIMEDRIFT) { Logger.logWarningMessage("Received block " + block.getStringId() + " from the future, block timestamp is " + block.getTimestamp() + ", current time is " + curTime + ", system clock may be off"); throw new BlockOutOfOrderException("Invalid timestamp: " + block.getTimestamp() + " current time is " + curTime, block); } if (block.getTimestamp() <= previousLastBlock.getTimestamp()) { throw new BlockNotAcceptedException("Block timestamp " + block.getTimestamp() + " is before previous block timestamp " + previousLastBlock.getTimestamp(), block); } if (block.getVersion() != 1 && !Arrays.equals(Crypto.sha256().digest(previousLastBlock.bytes()), block.getPreviousBlockHash())) { throw new BlockNotAcceptedException("Previous block hash doesn't match", block); } if (block.getId() == 0L || BlockDb.hasBlock(block.getId(), previousLastBlock.getHeight())) { throw new BlockNotAcceptedException("Duplicate block or invalid id", block); } if (!block.verifyGenerationSignature() && !Generator.allowsFakeForging(block.getGeneratorPublicKey())) { throw new BlockNotAcceptedException("Generation signature verification failed", block); } if (!block.verifyBlockSignature()) { throw new BlockNotAcceptedException("Block signature verification failed", block); } if (block.getTransactions().size() > Constants.MAX_NUMBER_OF_TRANSACTIONS) { throw new BlockNotAcceptedException("Invalid block transaction count " + block.getTransactions().size(), block); } if (block.getPayloadLength() > Constants.MAX_PAYLOAD_LENGTH || block.getPayloadLength() < 0) { throw new BlockNotAcceptedException("Invalid block payload length " + block.getPayloadLength(), block); } } private void validateTransactions(BlockImpl block, BlockImpl previousLastBlock, int curTime, Map<TransactionType, Map<String, Integer>> duplicates, boolean fullValidation) throws BlockNotAcceptedException { long payloadLength = 0; long calculatedTotalAmount = 0; long calculatedTotalFee = 0; MessageDigest digest = Crypto.sha256(); boolean hasPrunedTransactions = false; for (TransactionImpl transaction : block.getTransactions()) { if (transaction.getTimestamp() > curTime + Constants.MAX_TIMEDRIFT) { throw new BlockOutOfOrderException("Invalid transaction timestamp: " + transaction.getTimestamp() + ", current time is " + curTime, block); } if (!transaction.verifySignature()) { throw new TransactionNotAcceptedException("Transaction signature verification failed at height " + previousLastBlock.getHeight(), transaction); } if (fullValidation) { // cfb: Block 303 contains a transaction which expired before the block timestamp if (transaction.getTimestamp() > block.getTimestamp() + Constants.MAX_TIMEDRIFT || (transaction.getExpiration() < block.getTimestamp() && previousLastBlock.getHeight() != 303)) { throw new TransactionNotAcceptedException("Invalid transaction timestamp " + transaction.getTimestamp() + ", current time is " + curTime + ", block timestamp is " + block.getTimestamp(), transaction); } if (TransactionDb.hasTransaction(transaction.getId(), previousLastBlock.getHeight())) { throw new TransactionNotAcceptedException("Transaction is already in the blockchain", transaction); } if (transaction.referencedTransactionFullHash() != null) { if ((previousLastBlock.getHeight() < Constants.REFERENCED_TRANSACTION_FULL_HASH_BLOCK && !TransactionDb.hasTransaction(Convert.fullHashToId(transaction.referencedTransactionFullHash()), previousLastBlock.getHeight())) || (previousLastBlock.getHeight() >= Constants.REFERENCED_TRANSACTION_FULL_HASH_BLOCK && !hasAllReferencedTransactions(transaction, transaction.getTimestamp(), 0))) { throw new TransactionNotAcceptedException("Missing or invalid referenced transaction " + transaction.getReferencedTransactionFullHash(), transaction); } } if (transaction.getVersion() != getTransactionVersion(previousLastBlock.getHeight())) { throw new TransactionNotAcceptedException("Invalid transaction version " + transaction.getVersion() + " at height " + previousLastBlock.getHeight(), transaction); } /* if (!EconomicClustering.verifyFork(transaction)) { Logger.logDebugMessage("Block " + block.getStringId() + " height " + (previousLastBlock.getHeight() + 1) + " contains transaction that was generated on a fork: " + transaction.getStringId() + " ecBlockHeight " + transaction.getECBlockHeight() + " ecBlockId " + Convert.toUnsignedLong(transaction.getECBlockId())); //throw new TransactionNotAcceptedException("Transaction belongs to a different fork", transaction); } */ if (transaction.getId() == 0L) { throw new TransactionNotAcceptedException("Invalid transaction id 0", transaction); } try { transaction.validate(); } catch (NxtException.ValidationException e) { throw new TransactionNotAcceptedException(e.getMessage(), transaction); } } if (transaction.attachmentIsDuplicate(duplicates, true)) { throw new TransactionNotAcceptedException("Transaction is a duplicate", transaction); } if (!hasPrunedTransactions) { for (Appendix.AbstractAppendix appendage : transaction.getAppendages()) { if ((appendage instanceof Appendix.Prunable) && !((Appendix.Prunable)appendage).hasPrunableData()) { hasPrunedTransactions = true; break; } } } calculatedTotalAmount += transaction.getAmountNQT(); calculatedTotalFee += transaction.getFeeNQT(); payloadLength += transaction.getFullSize(); digest.update(transaction.bytes()); } if (calculatedTotalAmount != block.getTotalAmountNQT() || calculatedTotalFee != block.getTotalFeeNQT()) { throw new BlockNotAcceptedException("Total amount or fee don't match transaction totals", block); } if (!Arrays.equals(digest.digest(), block.getPayloadHash())) { throw new BlockNotAcceptedException("Payload hash doesn't match", block); } if (hasPrunedTransactions ? payloadLength > block.getPayloadLength() : payloadLength != block.getPayloadLength()) { throw new BlockNotAcceptedException("Transaction payload length " + payloadLength + " does not match block payload length " + block.getPayloadLength(), block); } } private void accept(BlockImpl block, List<TransactionImpl> validPhasedTransactions, List<TransactionImpl> invalidPhasedTransactions, Map<TransactionType, Map<String, Integer>> duplicates) throws TransactionNotAcceptedException { try { isProcessingBlock = true; for (TransactionImpl transaction : block.getTransactions()) { if (! transaction.applyUnconfirmed()) { throw new TransactionNotAcceptedException("Double spending", transaction); } } blockListeners.notify(block, Event.BEFORE_BLOCK_APPLY); block.apply(); validPhasedTransactions.forEach(transaction -> transaction.getPhasing().countVotes(transaction)); invalidPhasedTransactions.forEach(transaction -> transaction.getPhasing().reject(transaction)); int fromTimestamp = Nxt.getEpochTime() - Constants.MAX_PRUNABLE_LIFETIME; for (TransactionImpl transaction : block.getTransactions()) { try { transaction.apply(); if (transaction.getTimestamp() > fromTimestamp) { for (Appendix.AbstractAppendix appendage : transaction.getAppendages(true)) { if ((appendage instanceof Appendix.Prunable) && !((Appendix.Prunable)appendage).hasPrunableData()) { synchronized (prunableTransactions) { prunableTransactions.add(transaction.getId()); } lastRestoreTime = 0; break; } } } } catch (RuntimeException e) { Logger.logErrorMessage(e.toString(), e); throw new BlockchainProcessor.TransactionNotAcceptedException(e, transaction); } } if (block.getHeight() > Constants.SHUFFLING_BLOCK) { SortedSet<TransactionImpl> possiblyApprovedTransactions = new TreeSet<>(finishingTransactionsComparator); block.getTransactions().forEach(transaction -> { PhasingPoll.getLinkedPhasedTransactions(transaction.fullHash()).forEach(phasedTransaction -> { if (phasedTransaction.getPhasing().getFinishHeight() > block.getHeight()) { possiblyApprovedTransactions.add((TransactionImpl)phasedTransaction); } }); if (transaction.getType() == TransactionType.Messaging.PHASING_VOTE_CASTING && !transaction.attachmentIsPhased()) { Attachment.MessagingPhasingVoteCasting voteCasting = (Attachment.MessagingPhasingVoteCasting)transaction.getAttachment(); voteCasting.getTransactionFullHashes().forEach(hash -> { PhasingPoll phasingPoll = PhasingPoll.getPoll(Convert.fullHashToId(hash)); if (phasingPoll.allowEarlyFinish() && phasingPoll.getFinishHeight() > block.getHeight()) { possiblyApprovedTransactions.add(TransactionDb.findTransaction(phasingPoll.getId())); } }); } }); validPhasedTransactions.forEach(phasedTransaction -> { if (phasedTransaction.getType() == TransactionType.Messaging.PHASING_VOTE_CASTING) { PhasingPoll.PhasingPollResult result = PhasingPoll.getResult(phasedTransaction.getId()); if (result != null && result.isApproved()) { Attachment.MessagingPhasingVoteCasting phasingVoteCasting = (Attachment.MessagingPhasingVoteCasting) phasedTransaction.getAttachment(); phasingVoteCasting.getTransactionFullHashes().forEach(hash -> { PhasingPoll phasingPoll = PhasingPoll.getPoll(Convert.fullHashToId(hash)); if (phasingPoll.allowEarlyFinish() && phasingPoll.getFinishHeight() > block.getHeight()) { possiblyApprovedTransactions.add(TransactionDb.findTransaction(phasingPoll.getId())); } }); } } }); possiblyApprovedTransactions.forEach(transaction -> { if (PhasingPoll.getResult(transaction.getId()) == null) { try { transaction.validate(); transaction.getPhasing().tryCountVotes(transaction, duplicates); } catch (NxtException.ValidationException e) { Logger.logDebugMessage("At height " + block.getHeight() + " phased transaction " + transaction.getStringId() + " no longer passes validation: " + e.getMessage() + ", cannot finish early"); } } }); } if (!Constants.isTestnet && block.getHeight() == Constants.SHUFFLING_BLOCK) { //TODO: temporary bugfix for transaction 11815651636695037775, remove after hardfork Account.getAccount(Convert.parseUnsignedLong("4345946899368325355")).addToUnconfirmedBalanceNQT(AccountLedger.LedgerEvent.ASSET_DIVIDEND_PAYMENT, Convert.parseUnsignedLong("11815651636695037775"), 100 * Constants.ONE_NXT); } blockListeners.notify(block, Event.AFTER_BLOCK_APPLY); if (block.getTransactions().size() > 0) { TransactionProcessorImpl.getInstance().notifyListeners(block.getTransactions(), TransactionProcessor.Event.ADDED_CONFIRMED_TRANSACTIONS); } AccountLedger.commitEntries(); } finally { isProcessingBlock = false; AccountLedger.clearEntries(); } } private static final Comparator<Transaction> finishingTransactionsComparator = Comparator .comparingInt(Transaction::getHeight) .thenComparingInt(Transaction::getIndex) .thenComparingLong(Transaction::getId); private List<BlockImpl> popOffTo(Block commonBlock) { blockchain.writeLock(); try { if (!Db.db.isInTransaction()) { try { Db.db.beginTransaction(); return popOffTo(commonBlock); } finally { Db.db.endTransaction(); } } if (commonBlock.getHeight() < getMinRollbackHeight()) { Logger.logMessage("Rollback to height " + commonBlock.getHeight() + " not supported, will do a full rescan"); popOffWithRescan(commonBlock.getHeight() + 1); return Collections.emptyList(); } if (! blockchain.hasBlock(commonBlock.getId())) { Logger.logDebugMessage("Block " + commonBlock.getStringId() + " not found in blockchain, nothing to pop off"); return Collections.emptyList(); } List<BlockImpl> poppedOffBlocks = new ArrayList<>(); try { BlockImpl block = blockchain.getLastBlock(); block.loadTransactions(); Logger.logDebugMessage("Rollback from block " + block.getStringId() + " at height " + block.getHeight() + " to " + commonBlock.getStringId() + " at " + commonBlock.getHeight()); while (block.getId() != commonBlock.getId() && block.getId() != Genesis.GENESIS_BLOCK_ID) { poppedOffBlocks.add(block); block = popLastBlock(); } for (DerivedDbTable table : derivedTables) { table.rollback(commonBlock.getHeight()); } Db.db.clearCache(); Db.db.commitTransaction(); } catch (RuntimeException e) { Logger.logErrorMessage("Error popping off to " + commonBlock.getHeight() + ", " + e.toString()); Db.db.rollbackTransaction(); BlockImpl lastBlock = BlockDb.findLastBlock(); blockchain.setLastBlock(lastBlock); popOffTo(lastBlock); throw e; } return poppedOffBlocks; } finally { blockchain.writeUnlock(); } } private BlockImpl popLastBlock() { BlockImpl block = blockchain.getLastBlock(); if (block.getId() == Genesis.GENESIS_BLOCK_ID) { throw new RuntimeException("Cannot pop off genesis block"); } BlockImpl previousBlock = blockchain.getBlock(block.getPreviousBlockId()); previousBlock.loadTransactions(); blockchain.setLastBlock(block, previousBlock); BlockDb.deleteBlocksFrom(block.getId()); blockListeners.notify(block, Event.BLOCK_POPPED); return previousBlock; } private void popOffWithRescan(int height) { blockchain.writeLock(); try { try { scheduleScan(0, false); BlockDb.deleteBlocksFrom(BlockDb.findBlockIdAtHeight(height)); Logger.logDebugMessage("Deleted blocks starting from height %s", height); } finally { scan(0, false); } } finally { blockchain.writeUnlock(); } } private int getBlockVersion(int previousBlockHeight) { return previousBlockHeight < Constants.TRANSPARENT_FORGING_BLOCK ? 1 : previousBlockHeight < Constants.NQT_BLOCK ? 2 : 3; } private int getTransactionVersion(int previousBlockHeight) { return previousBlockHeight < Constants.DIGITAL_GOODS_STORE_BLOCK ? 0 : 1; } private boolean verifyChecksum(byte[] validChecksum, int fromHeight, int toHeight) { MessageDigest digest = Crypto.sha256(); try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement( "SELECT * FROM transaction WHERE height > ? AND height <= ? ORDER BY id ASC, timestamp ASC")) { pstmt.setInt(1, fromHeight); pstmt.setInt(2, toHeight); try (DbIterator<TransactionImpl> iterator = blockchain.getTransactions(con, pstmt)) { while (iterator.hasNext()) { digest.update(iterator.next().bytes()); } } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } byte[] checksum = digest.digest(); if (validChecksum == null) { Logger.logMessage("Checksum calculated:\n" + Arrays.toString(checksum)); return true; } else if (!Arrays.equals(checksum, validChecksum)) { Logger.logErrorMessage("Checksum failed at block " + blockchain.getHeight() + ": " + Arrays.toString(checksum)); return false; } else { Logger.logMessage("Checksum passed at block " + blockchain.getHeight()); return true; } } SortedSet<UnconfirmedTransaction> selectUnconfirmedTransactions(Map<TransactionType, Map<String, Integer>> duplicates, Block previousBlock, int blockTimestamp) { List<UnconfirmedTransaction> orderedUnconfirmedTransactions = new ArrayList<>(); try (FilteringIterator<UnconfirmedTransaction> unconfirmedTransactions = new FilteringIterator<>( TransactionProcessorImpl.getInstance().getAllUnconfirmedTransactions(), transaction -> hasAllReferencedTransactions(transaction.getTransaction(), transaction.getTimestamp(), 0))) { for (UnconfirmedTransaction unconfirmedTransaction : unconfirmedTransactions) { orderedUnconfirmedTransactions.add(unconfirmedTransaction); } } SortedSet<UnconfirmedTransaction> sortedTransactions = new TreeSet<>(transactionArrivalComparator); int payloadLength = 0; while (payloadLength <= Constants.MAX_PAYLOAD_LENGTH && sortedTransactions.size() <= Constants.MAX_NUMBER_OF_TRANSACTIONS) { int prevNumberOfNewTransactions = sortedTransactions.size(); for (UnconfirmedTransaction unconfirmedTransaction : orderedUnconfirmedTransactions) { int transactionLength = unconfirmedTransaction.getTransaction().getFullSize(); if (sortedTransactions.contains(unconfirmedTransaction) || payloadLength + transactionLength > Constants.MAX_PAYLOAD_LENGTH) { continue; } if (unconfirmedTransaction.getVersion() != getTransactionVersion(previousBlock.getHeight())) { continue; } if (blockTimestamp > 0 && (unconfirmedTransaction.getTimestamp() > blockTimestamp + Constants.MAX_TIMEDRIFT || unconfirmedTransaction.getExpiration() < blockTimestamp)) { continue; } try { unconfirmedTransaction.getTransaction().validate(); } catch (NxtException.ValidationException e) { continue; } if (unconfirmedTransaction.getTransaction().attachmentIsDuplicate(duplicates, true)) { continue; } /* if (!EconomicClustering.verifyFork(transaction)) { Logger.logDebugMessage("Including transaction that was generated on a fork: " + transaction.getStringId() + " ecBlockHeight " + transaction.getECBlockHeight() + " ecBlockId " + Convert.toUnsignedLong(transaction.getECBlockId())); //continue; } */ sortedTransactions.add(unconfirmedTransaction); payloadLength += transactionLength; } if (sortedTransactions.size() == prevNumberOfNewTransactions) { break; } } return sortedTransactions; } private static final Comparator<UnconfirmedTransaction> transactionArrivalComparator = Comparator .comparingLong(UnconfirmedTransaction::getArrivalTimestamp) .thenComparingInt(UnconfirmedTransaction::getHeight) .thenComparingLong(UnconfirmedTransaction::getId); void generateBlock(String secretPhrase, int blockTimestamp) throws BlockNotAcceptedException { Map<TransactionType, Map<String, Integer>> duplicates = new HashMap<>(); if (blockchain.getHeight() >= Constants.PHASING_BLOCK) { try (DbIterator<TransactionImpl> phasedTransactions = PhasingPoll.getFinishingTransactions(blockchain.getHeight() + 1)) { for (TransactionImpl phasedTransaction : phasedTransactions) { try { phasedTransaction.validate(); phasedTransaction.attachmentIsDuplicate(duplicates, false); // pre-populate duplicates map } catch (NxtException.ValidationException ignore) { } } } } BlockImpl previousBlock = blockchain.getLastBlock(); SortedSet<UnconfirmedTransaction> sortedTransactions = selectUnconfirmedTransactions(duplicates, previousBlock, blockTimestamp); List<TransactionImpl> blockTransactions = new ArrayList<>(); MessageDigest digest = Crypto.sha256(); long totalAmountNQT = 0; long totalFeeNQT = 0; int payloadLength = 0; for (UnconfirmedTransaction unconfirmedTransaction : sortedTransactions) { TransactionImpl transaction = unconfirmedTransaction.getTransaction(); blockTransactions.add(transaction); digest.update(transaction.bytes()); totalAmountNQT += transaction.getAmountNQT(); totalFeeNQT += transaction.getFeeNQT(); payloadLength += transaction.getFullSize(); } byte[] payloadHash = digest.digest(); digest.update(previousBlock.getGenerationSignature()); final byte[] publicKey = Crypto.getPublicKey(secretPhrase); byte[] generationSignature = digest.digest(publicKey); byte[] previousBlockHash = Crypto.sha256().digest(previousBlock.bytes()); BlockImpl block = new BlockImpl(getBlockVersion(previousBlock.getHeight()), blockTimestamp, previousBlock.getId(), totalAmountNQT, totalFeeNQT, payloadLength, payloadHash, publicKey, generationSignature, previousBlockHash, blockTransactions, secretPhrase); try { pushBlock(block); blockListeners.notify(block, Event.BLOCK_GENERATED); Logger.logDebugMessage("Account " + Long.toUnsignedString(block.getGeneratorId()) + " generated block " + block.getStringId() + " at height " + block.getHeight() + " timestamp " + block.getTimestamp() + " fee " + ((float)block.getTotalFeeNQT())/Constants.ONE_NXT); } catch (TransactionNotAcceptedException e) { Logger.logDebugMessage("Generate block failed: " + e.getMessage()); TransactionProcessorImpl.getInstance().processWaitingTransactions(); TransactionImpl transaction = e.getTransaction(); Logger.logDebugMessage("Removing invalid transaction: " + transaction.getStringId()); blockchain.writeLock(); try { TransactionProcessorImpl.getInstance().removeUnconfirmedTransaction(transaction); } finally { blockchain.writeUnlock(); } throw e; } catch (BlockNotAcceptedException e) { Logger.logDebugMessage("Generate block failed: " + e.getMessage()); throw e; } } boolean hasAllReferencedTransactions(TransactionImpl transaction, int timestamp, int count) { if (transaction.referencedTransactionFullHash() == null) { return timestamp - transaction.getTimestamp() < Constants.MAX_REFERENCED_TRANSACTION_TIMESPAN && count < 10; } TransactionImpl referencedTransaction = TransactionDb.findTransactionByFullHash(transaction.referencedTransactionFullHash()); return referencedTransaction != null && referencedTransaction.getHeight() < transaction.getHeight() && hasAllReferencedTransactions(referencedTransaction, timestamp, count + 1); } void scheduleScan(int height, boolean validate) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("UPDATE scan SET rescan = TRUE, height = ?, validate = ?")) { pstmt.setInt(1, height); pstmt.setBoolean(2, validate); pstmt.executeUpdate(); Logger.logDebugMessage("Scheduled scan starting from height " + height + (validate ? ", with validation" : "")); } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } @Override public void scan(int height, boolean validate) { scan(height, validate, false); } @Override public void fullScanWithShutdown() { scan(0, true, true); } private void scan(int height, boolean validate, boolean shutdown) { blockchain.writeLock(); try { if (!Db.db.isInTransaction()) { try { Db.db.beginTransaction(); if (validate) { blockListeners.addListener(checksumListener, Event.BLOCK_SCANNED); } scan(height, validate, shutdown); Db.db.commitTransaction(); } catch (Exception e) { Db.db.rollbackTransaction(); throw e; } finally { Db.db.endTransaction(); blockListeners.removeListener(checksumListener, Event.BLOCK_SCANNED); } return; } scheduleScan(height, validate); if (height > 0 && height < getMinRollbackHeight()) { Logger.logMessage("Rollback to height less than " + getMinRollbackHeight() + " not supported, will do a full scan"); height = 0; } if (height < 0) { height = 0; } Logger.logMessage("Scanning blockchain starting from height " + height + "..."); if (validate) { Logger.logDebugMessage("Also verifying signatures and validating transactions..."); } try (Connection con = Db.db.getConnection(); PreparedStatement pstmtSelect = con.prepareStatement("SELECT * FROM block " + (height > 0 ? "WHERE height >= ? " : "") + "ORDER BY db_id ASC"); PreparedStatement pstmtDone = con.prepareStatement("UPDATE scan SET rescan = FALSE, height = 0, validate = FALSE")) { isScanning = true; initialScanHeight = blockchain.getHeight(); if (height > blockchain.getHeight() + 1) { Logger.logMessage("Rollback height " + (height - 1) + " exceeds current blockchain height of " + blockchain.getHeight() + ", no scan needed"); pstmtDone.executeUpdate(); Db.db.commitTransaction(); return; } if (height == 0) { Logger.logDebugMessage("Dropping all full text search indexes"); FullTextTrigger.dropAll(con); } for (DerivedDbTable table : derivedTables) { if (height == 0) { table.truncate(); } else { table.rollback(height - 1); } } Db.db.clearCache(); Db.db.commitTransaction(); Logger.logDebugMessage("Rolled back derived tables"); BlockImpl currentBlock = BlockDb.findBlockAtHeight(height); blockListeners.notify(currentBlock, Event.RESCAN_BEGIN); long currentBlockId = currentBlock.getId(); if (height == 0) { blockchain.setLastBlock(currentBlock); // special case to avoid no last block Account.addOrGetAccount(Genesis.CREATOR_ID).apply(Genesis.CREATOR_PUBLIC_KEY); } else { blockchain.setLastBlock(BlockDb.findBlockAtHeight(height - 1)); } if (shutdown) { Logger.logMessage("Scan will be performed at next start"); new Thread(() -> { System.exit(0); }).start(); return; } if (height > 0) { pstmtSelect.setInt(1, height); } try (ResultSet rs = pstmtSelect.executeQuery()) { while (rs.next()) { try { currentBlock = BlockDb.loadBlock(con, rs, true); currentBlock.loadTransactions(); if (currentBlock.getId() != currentBlockId || currentBlock.getHeight() > blockchain.getHeight() + 1) { throw new NxtException.NotValidException("Database blocks in the wrong order!"); } Map<TransactionType, Map<String, Integer>> duplicates = new HashMap<>(); List<TransactionImpl> validPhasedTransactions = new ArrayList<>(); List<TransactionImpl> invalidPhasedTransactions = new ArrayList<>(); validatePhasedTransactions(blockchain.getHeight(), validPhasedTransactions, invalidPhasedTransactions, duplicates); if (validate && currentBlockId != Genesis.GENESIS_BLOCK_ID) { int curTime = Nxt.getEpochTime(); validate(currentBlock, blockchain.getLastBlock(), curTime); byte[] blockBytes = currentBlock.bytes(); JSONObject blockJSON = (JSONObject) JSONValue.parse(currentBlock.getJSONObject().toJSONString()); if (!Arrays.equals(blockBytes, BlockImpl.parseBlock(blockJSON).bytes())) { throw new NxtException.NotValidException("Block JSON cannot be parsed back to the same block"); } validateTransactions(currentBlock, blockchain.getLastBlock(), curTime, duplicates, true); for (TransactionImpl transaction : currentBlock.getTransactions()) { byte[] transactionBytes = transaction.bytes(); if (currentBlock.getHeight() > Constants.NQT_BLOCK && !Arrays.equals(transactionBytes, TransactionImpl.newTransactionBuilder(transactionBytes).build().bytes())) { throw new NxtException.NotValidException("Transaction bytes cannot be parsed back to the same transaction: " + transaction.getJSONObject().toJSONString()); } JSONObject transactionJSON = (JSONObject) JSONValue.parse(transaction.getJSONObject().toJSONString()); if (!Arrays.equals(transactionBytes, TransactionImpl.newTransactionBuilder(transactionJSON).build().bytes())) { throw new NxtException.NotValidException("Transaction JSON cannot be parsed back to the same transaction: " + transaction.getJSONObject().toJSONString()); } } } blockListeners.notify(currentBlock, Event.BEFORE_BLOCK_ACCEPT); blockchain.setLastBlock(currentBlock); accept(currentBlock, validPhasedTransactions, invalidPhasedTransactions, duplicates); currentBlockId = currentBlock.getNextBlockId(); Db.db.clearCache(); Db.db.commitTransaction(); blockListeners.notify(currentBlock, Event.AFTER_BLOCK_ACCEPT); } catch (NxtException | RuntimeException e) { Db.db.rollbackTransaction(); Logger.logDebugMessage(e.toString(), e); Logger.logDebugMessage("Applying block " + Long.toUnsignedString(currentBlockId) + " at height " + (currentBlock == null ? 0 : currentBlock.getHeight()) + " failed, deleting from database"); if (currentBlock != null) { currentBlock.loadTransactions(); TransactionProcessorImpl.getInstance().processLater(currentBlock.getTransactions()); } while (rs.next()) { try { currentBlock = BlockDb.loadBlock(con, rs, true); currentBlock.loadTransactions(); TransactionProcessorImpl.getInstance().processLater(currentBlock.getTransactions()); } catch (RuntimeException e2) { Logger.logErrorMessage(e2.toString(), e); break; } } BlockDb.deleteBlocksFrom(currentBlockId); BlockImpl lastBlock = BlockDb.findLastBlock(); blockchain.setLastBlock(lastBlock); popOffTo(lastBlock); break; } blockListeners.notify(currentBlock, Event.BLOCK_SCANNED); } } if (height == 0) { for (DerivedDbTable table : derivedTables) { table.createSearchIndex(con); } } pstmtDone.executeUpdate(); Db.db.commitTransaction(); blockListeners.notify(currentBlock, Event.RESCAN_END); Logger.logMessage("...done at height " + blockchain.getHeight()); if (height == 0 && validate) { Logger.logMessage("SUCCESSFULLY PERFORMED FULL RESCAN WITH VALIDATION"); } lastRestoreTime = 0; } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } finally { isScanning = false; } } finally { blockchain.writeUnlock(); } } }