/* * Copyright (c) [2016] [ <ether.camp> ] * This file is part of the ethereumJ library. * * The ethereumJ library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The ethereumJ library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>. */ package org.ethereum.sync; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.lang3.tuple.Pair; import org.ethereum.config.SystemProperties; import org.ethereum.core.*; import org.ethereum.crypto.HashUtil; import org.ethereum.datasource.BloomFilter; import org.ethereum.datasource.DbSource; import org.ethereum.db.DbFlushManager; import org.ethereum.db.IndexedBlockStore; import org.ethereum.db.StateSource; import org.ethereum.facade.SyncStatus; import org.ethereum.listener.CompositeEthereumListener; import org.ethereum.listener.EthereumListener; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.net.client.Capability; import org.ethereum.net.eth.handler.Eth63; import org.ethereum.net.message.ReasonCode; import org.ethereum.net.rlpx.discover.NodeHandler; import org.ethereum.net.server.Channel; import org.ethereum.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import java.math.BigInteger; import java.util.*; import java.util.concurrent.*; import static org.ethereum.listener.EthereumListener.SyncState.COMPLETE; import static org.ethereum.listener.EthereumListener.SyncState.SECURE; import static org.ethereum.listener.EthereumListener.SyncState.UNSECURE; import static org.ethereum.util.CompactEncoder.hasTerminator; /** * Created by Anton Nashatyrev on 24.10.2016. */ @Component public class FastSyncManager { private final static Logger logger = LoggerFactory.getLogger("sync"); private final static long REQUEST_TIMEOUT = 5 * 1000; private final static int REQUEST_MAX_NODES = 384; private final static int NODE_QUEUE_BEST_SIZE = 100_000; private final static int MIN_PEERS_FOR_PIVOT_SELECTION = 5; private final static int FORCE_SYNC_TIMEOUT = 60 * 1000; private final static int PIVOT_DISTANCE_FROM_HEAD = 1024; private final static int MSX_DB_QUEUE_SIZE = 20000; private static final Capability ETH63_CAPABILITY = new Capability(Capability.ETH, (byte) 63); public static final byte[] FASTSYNC_DB_KEY_SYNC_STAGE = HashUtil.sha3("Key in state DB indicating fastsync stage in progress".getBytes()); public static final byte[] FASTSYNC_DB_KEY_PIVOT = HashUtil.sha3("Key in state DB with encoded selected pivot block".getBytes()); @Autowired private SystemProperties config; @Autowired private SyncPool pool; @Autowired private BlockchainImpl blockchain; @Autowired private IndexedBlockStore blockStore; @Autowired private SyncManager syncManager; @Autowired @Qualifier("blockchainDB") DbSource<byte[]> blockchainDB; @Autowired private StateSource stateSource; @Autowired DbFlushManager dbFlushManager; @Autowired FastSyncDownloader downloader; @Autowired CompositeEthereumListener listener; @Autowired ApplicationContext applicationContext; int nodesInserted = 0; private boolean fastSyncInProgress = false; private BlockingQueue<TrieNodeRequest> dbWriteQueue = new LinkedBlockingQueue<>(); private Thread dbWriterThread; private Thread fastSyncThread; private int dbQueueSizeMonitor = -1; private BlockHeader pivot; private HeadersDownloader headersDownloader; private BlockBodiesDownloader blockBodiesDownloader; private ReceiptsDownloader receiptsDownloader; private long forceSyncRemains; private void waitDbQueueSizeBelow(int size) { synchronized (this) { try { dbQueueSizeMonitor = size; while (dbWriteQueue.size() > size) wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { dbQueueSizeMonitor = -1; } } } void init() { dbWriterThread = new Thread("FastSyncDBWriter") { @Override public void run() { try { while (!Thread.currentThread().isInterrupted()) { synchronized (FastSyncManager.this) { if (dbQueueSizeMonitor >= 0 && dbWriteQueue.size() <= dbQueueSizeMonitor) { FastSyncManager.this.notifyAll(); } } TrieNodeRequest request = dbWriteQueue.take(); nodesInserted++; stateSource.getNoJournalSource().put(request.nodeHash, request.response); if (nodesInserted % 1000 == 0) { dbFlushManager.commit(); logger.debug("FastSyncDBWriter: commit: dbWriteQueue.size = " + dbWriteQueue.size()); } } } catch (InterruptedException e) { } catch (Exception e) { logger.error("Fatal FastSync error while writing data", e); } } }; dbWriterThread.start(); fastSyncThread = new Thread("FastSyncLoop") { @Override public void run() { try { main(); } catch (Exception e) { logger.error("Fatal FastSync loop error", e); } } }; fastSyncThread.start(); } public SyncStatus getSyncState() { if (!isFastSyncInProgress()) return new SyncStatus(SyncStatus.SyncStage.Complete, 0, 0); if (pivot == null) { return new SyncStatus(SyncStatus.SyncStage.PivotBlock, (FORCE_SYNC_TIMEOUT - forceSyncRemains) / 1000, FORCE_SYNC_TIMEOUT / 1000); } EthereumListener.SyncState syncStage = getSyncStage(); switch (syncStage) { case UNSECURE: return new SyncStatus(SyncStatus.SyncStage.StateNodes, nodesInserted, nodesQueue.size() + pendingNodes.size() + nodesInserted); case SECURE: return new SyncStatus(SyncStatus.SyncStage.Headers, headersDownloader.getHeadersLoaded(), pivot.getNumber()); case COMPLETE: if (receiptsDownloader != null) { return new SyncStatus(SyncStatus.SyncStage.Receipts, receiptsDownloader.getDownloadedBlocksCount(), pivot.getNumber()); } else if (blockBodiesDownloader!= null) { return new SyncStatus(SyncStatus.SyncStage.BlockBodies, blockBodiesDownloader.getDownloadedCount(), pivot.getNumber()); } else { return new SyncStatus(SyncStatus.SyncStage.BlockBodies, 0, pivot.getNumber()); } } return new SyncStatus(SyncStatus.SyncStage.Complete, 0, 0); } enum TrieNodeType { STATE, STORAGE, CODE } int stateNodesCnt = 0; int codeNodesCnt = 0; int storageNodesCnt = 0; private class TrieNodeRequest { TrieNodeType type; byte[] nodeHash; byte[] response; final Map<Long, Long> requestSent = new HashMap<>(); TrieNodeRequest(TrieNodeType type, byte[] nodeHash) { this.type = type; this.nodeHash = nodeHash; switch (type) { case STATE: stateNodesCnt++; break; case CODE: codeNodesCnt++; break; case STORAGE: storageNodesCnt++; break; } } List<TrieNodeRequest> createChildRequests() { if (type == TrieNodeType.CODE) { return Collections.emptyList(); } List<Object> node = Value.fromRlpEncoded(response).asList(); List<TrieNodeRequest> ret = new ArrayList<>(); if (type == TrieNodeType.STATE) { if (node.size() == 2 && hasTerminator((byte[]) node.get(0))) { byte[] nodeValue = (byte[]) node.get(1); AccountState state = new AccountState(nodeValue); if (!FastByteComparisons.equal(HashUtil.EMPTY_DATA_HASH, state.getCodeHash())) { ret.add(new TrieNodeRequest(TrieNodeType.CODE, state.getCodeHash())); } if (!FastByteComparisons.equal(HashUtil.EMPTY_TRIE_HASH, state.getStateRoot())) { ret.add(new TrieNodeRequest(TrieNodeType.STORAGE, state.getStateRoot())); } return ret; } } List<byte[]> childHashes = getChildHashes(node); for (byte[] childHash : childHashes) { ret.add(new TrieNodeRequest(type, childHash)); } return ret; } public void reqSent(Long requestId) { synchronized (FastSyncManager.this) { Long timestamp = System.currentTimeMillis(); requestSent.put(requestId, timestamp); } } public Set<Long> requestIdsSnapshot() { synchronized (FastSyncManager.this) { return new HashSet<Long>(requestSent.keySet()); } } @Override public String toString() { return "TrieNodeRequest{" + "type=" + type + ", nodeHash=" + Hex.toHexString(nodeHash) + '}'; } } private static List<byte[]> getChildHashes(List<Object> siblings) { List<byte[]> ret = new ArrayList<>(); if (siblings.size() == 2) { Value val = new Value(siblings.get(1)); if (val.isHashCode() && !hasTerminator((byte[]) siblings.get(0))) ret.add(val.asBytes()); } else { for (int j = 0; j < 16; ++j) { Value val = new Value(siblings.get(j)); if (val.isHashCode()) ret.add(val.asBytes()); } } return ret; } Deque<TrieNodeRequest> nodesQueue = new LinkedBlockingDeque<>(); ByteArrayMap<TrieNodeRequest> pendingNodes = new ByteArrayMap<>(); Long requestId = 0L; private synchronized void purgePending(byte[] hash) { TrieNodeRequest request = pendingNodes.get(hash); if (request.requestSent.isEmpty()) pendingNodes.remove(hash); } synchronized void processTimeouts() { long cur = System.currentTimeMillis(); for (TrieNodeRequest request : new ArrayList<>(pendingNodes.values())) { Iterator<Map.Entry<Long, Long>> reqIterator = request.requestSent.entrySet().iterator(); while (reqIterator.hasNext()) { Map.Entry<Long, Long> requestEntry = reqIterator.next(); if (cur - requestEntry.getValue() > REQUEST_TIMEOUT) { reqIterator.remove(); purgePending(request.nodeHash); nodesQueue.addFirst(request); } } } } synchronized void processResponse(TrieNodeRequest req) { dbWriteQueue.add(req); for (TrieNodeRequest childRequest : req.createChildRequests()) { if (nodesQueue.size() > NODE_QUEUE_BEST_SIZE) { // reducing queue by traversing tree depth-first nodesQueue.addFirst(childRequest); } else { // enlarging queue by traversing tree breadth-first nodesQueue.add(childRequest); } } } boolean requestNextNodes(int cnt) { final Channel idle = pool.getAnyIdle(); if (idle != null) { final List<byte[]> hashes = new ArrayList<>(); final List<TrieNodeRequest> requestsSent = new ArrayList<>(); final Set<Long> sentRequestIds = new HashSet<>(); synchronized (this) { for (int i = 0; i < cnt && !nodesQueue.isEmpty(); i++) { TrieNodeRequest req = nodesQueue.poll(); hashes.add(req.nodeHash); TrieNodeRequest request = pendingNodes.get(req.nodeHash); if (request == null) { pendingNodes.put(req.nodeHash, req); request = req; } sentRequestIds.add(requestId); request.reqSent(requestId); requestId++; requestsSent.add(request); } } if (hashes.size() > 0) { logger.trace("Requesting " + hashes.size() + " nodes from peer: " + idle); ListenableFuture<List<Pair<byte[], byte[]>>> nodes = ((Eth63) idle.getEthHandler()).requestTrieNodes(hashes); final long reqTime = System.currentTimeMillis(); Futures.addCallback(nodes, new FutureCallback<List<Pair<byte[], byte[]>>>() { @Override public void onSuccess(List<Pair<byte[], byte[]>> result) { try { synchronized (FastSyncManager.this) { logger.trace("Received " + result.size() + " nodes (of " + hashes.size() + ") from peer: " + idle); for (Pair<byte[], byte[]> pair : result) { TrieNodeRequest request = pendingNodes.get(pair.getKey()); if (request == null) { long t = System.currentTimeMillis(); logger.debug("Received node which was not requested: " + Hex.toHexString(pair.getKey()) + " from " + idle); return; } Set<Long> intersection = request.requestIdsSnapshot(); intersection.retainAll(sentRequestIds); if (!intersection.isEmpty()) { Long inter = intersection.iterator().next(); request.requestSent.remove(inter); purgePending(pair.getKey()); request.response = pair.getValue(); processResponse(request); } } FastSyncManager.this.notifyAll(); idle.getNodeStatistics().eth63NodesRequested.add(hashes.size()); idle.getNodeStatistics().eth63NodesReceived.add(result.size()); idle.getNodeStatistics().eth63NodesRetrieveTime.add(System.currentTimeMillis() - reqTime); } } catch (Exception e) { logger.error("Unexpected error processing nodes", e); } } @Override public void onFailure(Throwable t) { logger.warn("Error with Trie Node request: " + t); synchronized (FastSyncManager.this) { for (byte[] hash : hashes) { final TrieNodeRequest request = pendingNodes.get(hash); if (request == null) continue; Set<Long> intersection = request.requestIdsSnapshot(); intersection.retainAll(sentRequestIds); if (!intersection.isEmpty()) { Long inter = intersection.iterator().next(); request.requestSent.remove(inter); nodesQueue.addFirst(request); purgePending(hash); } } FastSyncManager.this.notifyAll(); } } }); return true; } else { // idle.getEthHandler().setStatus(SyncState.IDLE); return false; } } else { return false; } } void retrieveLoop() { try { while (!nodesQueue.isEmpty() || !pendingNodes.isEmpty()) { try { processTimeouts(); while (requestNextNodes(REQUEST_MAX_NODES)) ; synchronized (this) { wait(10); } waitDbQueueSizeBelow(MSX_DB_QUEUE_SIZE); logStat(); } catch (InterruptedException e) { throw e; } catch (Throwable t) { logger.error("Error", t); } } waitDbQueueSizeBelow(0); dbWriterThread.interrupt(); } catch (InterruptedException e) { logger.warn("Main fast sync loop was interrupted", e); } } long last = 0; long lastNodeCount = 0; private void logStat() { long cur = System.currentTimeMillis(); if (cur - last > 5000) { logger.info("FastSync: received: " + nodesInserted + ", known: " + nodesQueue.size() + ", pending: " + pendingNodes.size() + String.format(", nodes/sec: %1$.2f", 1000d * (nodesInserted - lastNodeCount) / (cur - last))); last = cur; lastNodeCount = nodesInserted; } } private void setSyncStage(EthereumListener.SyncState stage) { if (stage == null) { blockchainDB.delete(FASTSYNC_DB_KEY_SYNC_STAGE); } else { blockchainDB.put(FASTSYNC_DB_KEY_SYNC_STAGE, new byte[]{(byte) stage.ordinal()}); } } private EthereumListener.SyncState getSyncStage() { byte[] bytes = blockchainDB.get(FASTSYNC_DB_KEY_SYNC_STAGE); if (bytes == null) return UNSECURE; return EthereumListener.SyncState.values()[bytes[0]]; } private void syncUnsecure(BlockHeader pivot) { byte[] pivotStateRoot = pivot.getStateRoot(); TrieNodeRequest request = new TrieNodeRequest(TrieNodeType.STATE, pivotStateRoot); nodesQueue.add(request); logger.info("FastSync: downloading state trie at pivot block: " + pivot.getShortDescr()); setSyncStage(UNSECURE); retrieveLoop(); logger.info("FastSync: state trie download complete! (Nodes count: state: " + stateNodesCnt + ", storage: " +storageNodesCnt + ", code: " +codeNodesCnt + ")"); last = 0; logStat(); logger.info("FastSync: downloading 256 blocks prior to pivot block (" + pivot.getShortDescr() + ")"); downloader.startImporting(pivot.getHash(), 260); downloader.waitForStop(); logger.info("FastSync: complete downloading 256 blocks prior to pivot block (" + pivot.getShortDescr() + ")"); blockchain.setBestBlock(blockStore.getBlockByHash(pivot.getHash())); logger.info("FastSync: proceeding to regular sync..."); final CountDownLatch syncDoneLatch = new CountDownLatch(1); listener.addListener(new EthereumListenerAdapter() { @Override public void onSyncDone(SyncState state) { syncDoneLatch.countDown(); } }); syncManager.initRegularSync(UNSECURE); logger.info("FastSync: waiting for regular sync to reach the blockchain head..."); // try { // syncDoneLatch.await(); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } blockchainDB.put(FASTSYNC_DB_KEY_PIVOT, pivot.getEncoded()); dbFlushManager.commit(); dbFlushManager.flush(); logger.info("FastSync: regular sync reached the blockchain head."); } private void syncSecure() { pivot = new BlockHeader(blockchainDB.get(FASTSYNC_DB_KEY_PIVOT)); logger.info("FastSync: downloading headers from pivot down to genesis block for ensure pivot block (" + pivot.getShortDescr() + ") is secure..."); headersDownloader = applicationContext.getBean(HeadersDownloader.class); headersDownloader.init(pivot.getHash()); setSyncStage(EthereumListener.SyncState.SECURE); headersDownloader.waitForStop(); if (!FastByteComparisons.equal(headersDownloader.getGenesisHash(), config.getGenesis().getHash())) { logger.error("FASTSYNC FATAL ERROR: after downloading header chain starting from the pivot block (" + pivot.getShortDescr() + ") obtained genesis block doesn't match ours: " + Hex.toHexString(headersDownloader.getGenesisHash())); logger.error("Can't recover and exiting now. You need to restart from scratch (all DBs will be reset)"); System.exit(-666); } dbFlushManager.commit(); dbFlushManager.flush(); logger.info("FastSync: all headers downloaded. The state is SECURE now."); } private void syncBlocksReceipts() { pivot = new BlockHeader(blockchainDB.get(FASTSYNC_DB_KEY_PIVOT)); logger.info("FastSync: Downloading Block bodies up to pivot block (" + pivot.getShortDescr() + ")..."); blockBodiesDownloader = applicationContext.getBean(BlockBodiesDownloader.class); setSyncStage(EthereumListener.SyncState.COMPLETE); blockBodiesDownloader.startImporting(); blockBodiesDownloader.waitForStop(); logger.info("FastSync: Block bodies downloaded"); logger.info("FastSync: Downloading receipts..."); receiptsDownloader = applicationContext.getBean (ReceiptsDownloader.class, 1, pivot.getNumber() + 1); receiptsDownloader.startImporting(); receiptsDownloader.waitForStop(); logger.info("FastSync: receipts downloaded"); logger.info("FastSync: updating totDifficulties starting from the pivot block..."); blockchain.updateBlockTotDifficulties((int) pivot.getNumber()); synchronized (blockchain) { Block bestBlock = blockchain.getBestBlock(); BigInteger totalDifficulty = blockchain.getTotalDifficulty(); logger.info("FastSync: totDifficulties updated: bestBlock: " + bestBlock.getShortDescr() + ", totDiff: " + totalDifficulty); } setSyncStage(null); blockchainDB.delete(FASTSYNC_DB_KEY_PIVOT); dbFlushManager.commit(); dbFlushManager.flush(); } public void main() { if (blockchain.getBestBlock().getNumber() == 0 || getSyncStage() == SECURE || getSyncStage() == COMPLETE) { // either no DB at all (clear sync or DB was deleted due to UNSECURE stage while initializing // or we have incomplete headers/blocks/receipts download fastSyncInProgress = true; pool.setNodesSelector(new Functional.Predicate<NodeHandler>() { @Override public boolean test(NodeHandler handler) { if (!handler.getNodeStatistics().capabilities.contains(ETH63_CAPABILITY)) return false; return true; } }); try { EthereumListener.SyncState origSyncStage = getSyncStage(); switch (origSyncStage) { case UNSECURE: pivot = getPivotBlock(); if (pivot.getNumber() == 0) { logger.info("FastSync: too short blockchain, proceeding with regular sync..."); syncManager.initRegularSync(EthereumListener.SyncState.COMPLETE); return; } syncUnsecure(pivot); // regularSync should be inited here case SECURE: if (origSyncStage == SECURE) { logger.info("FastSync: UNSECURE sync was completed prior to this run, proceeding with next stage..."); logger.info("Initializing regular sync"); syncManager.initRegularSync(EthereumListener.SyncState.UNSECURE); } syncSecure(); listener.onSyncDone(EthereumListener.SyncState.SECURE); case COMPLETE: if (origSyncStage == COMPLETE) { logger.info("FastSync: SECURE sync was completed prior to this run, proceeding with next stage..."); logger.info("Initializing regular sync"); syncManager.initRegularSync(EthereumListener.SyncState.SECURE); } syncBlocksReceipts(); listener.onSyncDone(EthereumListener.SyncState.COMPLETE); } logger.info("FastSync: Full sync done."); } catch (InterruptedException ex) { logger.info("Shutting down due to interruption"); } finally { fastSyncInProgress = false; pool.setNodesSelector(null); } } else { logger.info("FastSync: fast sync was completed, best block: (" + blockchain.getBestBlock().getShortDescr() + "). " + "Continue with regular sync..."); syncManager.initRegularSync(EthereumListener.SyncState.COMPLETE); } } public boolean isFastSyncInProgress() { return fastSyncInProgress; } private BlockHeader getPivotBlock() throws InterruptedException { byte[] pivotBlockHash = config.getFastSyncPivotBlockHash(); long pivotBlockNumber = 0; long start = System.currentTimeMillis(); long s = start; if (pivotBlockHash != null) { logger.info("FastSync: fetching trusted pivot block with hash " + Hex.toHexString(pivotBlockHash)); } else { logger.info("FastSync: looking for best block number..."); BlockIdentifier bestKnownBlock; while (true) { List<Channel> allIdle = pool.getAllIdle(); forceSyncRemains = FORCE_SYNC_TIMEOUT - (System.currentTimeMillis() - start); if (allIdle.size() >= MIN_PEERS_FOR_PIVOT_SELECTION || forceSyncRemains < 0 && !allIdle.isEmpty()) { Channel bestPeer = allIdle.get(0); for (Channel channel : allIdle) { if (bestPeer.getEthHandler().getBestKnownBlock().getNumber() < channel.getEthHandler().getBestKnownBlock().getNumber()) { bestPeer = channel; } } bestKnownBlock = bestPeer.getEthHandler().getBestKnownBlock(); if (bestKnownBlock.getNumber() > 1000) { logger.info("FastSync: best block " + bestKnownBlock + " found with peer " + bestPeer); break; } } long t = System.currentTimeMillis(); if (t - s > 5000) { logger.info("FastSync: waiting for at least " + MIN_PEERS_FOR_PIVOT_SELECTION + " peers or " + forceSyncRemains / 1000 + " sec to select pivot block... (" + allIdle.size() + " peers so far)"); s = t; } Thread.sleep(500); } pivotBlockNumber = Math.max(bestKnownBlock.getNumber() - PIVOT_DISTANCE_FROM_HEAD, 0); logger.info("FastSync: fetching pivot block #" + pivotBlockNumber); } try { while (true) { BlockHeader result = null; if (pivotBlockHash != null) { result = getPivotHeaderByHash(pivotBlockHash); } else { Pair<BlockHeader, Long> pivotResult = getPivotHeaderByNumber(pivotBlockNumber); if (pivotResult != null) { if (pivotResult.getRight() != null) { pivotBlockNumber = pivotResult.getRight(); if (pivotBlockNumber == 0) { throw new RuntimeException("Cannot fastsync with current set of peers"); } } else { result = pivotResult.getLeft(); } } } if (result != null) return result; long t = System.currentTimeMillis(); if (t - s > 5000) { logger.info("FastSync: waiting for a peer to fetch pivot block..."); s = t; } Thread.sleep(500); } } catch (InterruptedException e) { throw e; } catch (Exception e) { logger.error("Unexpected", e); throw new RuntimeException(e); } } private BlockHeader getPivotHeaderByHash(byte[] pivotBlockHash) throws Exception { Channel bestIdle = pool.getAnyIdle(); if (bestIdle != null) { try { ListenableFuture<List<BlockHeader>> future = bestIdle.getEthHandler().sendGetBlockHeaders(pivotBlockHash, 1, 0, false); List<BlockHeader> blockHeaders = future.get(3, TimeUnit.SECONDS); if (!blockHeaders.isEmpty()) { BlockHeader ret = blockHeaders.get(0); if (FastByteComparisons.equal(pivotBlockHash, ret.getHash())) { logger.info("Pivot header fetched: " + ret.getShortDescr()); return ret; } logger.warn("Peer " + bestIdle + " returned pivot block with another hash: " + Hex.toHexString(ret.getHash()) + " Dropping the peer."); bestIdle.disconnect(ReasonCode.USELESS_PEER); } else { logger.warn("Peer " + bestIdle + " doesn't returned correct pivot block. Dropping the peer."); bestIdle.getNodeStatistics().wrongFork = true; bestIdle.disconnect(ReasonCode.USELESS_PEER); } } catch (TimeoutException e) { logger.debug("Timeout waiting for answer", e); } } return null; } /** * 1. Get pivotBlockNumber blocks from all peers * 2. Ensure that pivot block available from 50% + 1 peer * 3. Otherwise proposes new pivotBlockNumber (stepped back) * @param pivotBlockNumber Pivot block number * @return null - if no peers available * null, newPivotBlockNumber - if it's better to try other pivot block number * BlockHeader, null - if pivot successfully fetched and verified by majority of peers */ private Pair<BlockHeader, Long> getPivotHeaderByNumber(long pivotBlockNumber) throws Exception { List<Channel> allIdle = pool.getAllIdle(); if (!allIdle.isEmpty()) { try { List<ListenableFuture<List<BlockHeader>>> result = new ArrayList<>(); for (Channel channel : allIdle) { ListenableFuture<List<BlockHeader>> future = channel.getEthHandler().sendGetBlockHeaders(pivotBlockNumber, 1, false); result.add(future); } ListenableFuture<List<List<BlockHeader>>> successfulRequests = Futures.successfulAsList(result); List<List<BlockHeader>> results = successfulRequests.get(3, TimeUnit.SECONDS); Map<BlockHeader, Integer> pivotMap = new HashMap<>(); for (List<BlockHeader> blockHeaders : results) { if (!blockHeaders.isEmpty()) { BlockHeader currentHeader = blockHeaders.get(0); if (pivotMap.containsKey(currentHeader)) { pivotMap.put(currentHeader, pivotMap.get(currentHeader) + 1); } else { pivotMap.put(currentHeader, 1); } } } int peerCount = allIdle.size(); for (Map.Entry<BlockHeader, Integer> pivotEntry : pivotMap.entrySet()) { // Require 50% + 1 peer to trust pivot if (pivotEntry.getValue() * 2 > peerCount) { logger.info("Pivot header fetched: " + pivotEntry.getKey().getShortDescr()); return Pair.of(pivotEntry.getKey(), null); } } Long newPivotBlockNumber = Math.max(0, pivotBlockNumber - 1000); logger.info("Current pivot candidate not verified by majority of peers, " + "stepping back to block #{}", newPivotBlockNumber); return Pair.of(null, newPivotBlockNumber); } catch (TimeoutException e) { logger.debug("Timeout waiting for answer", e); } } return null; } public void close() { logger.info("Closing FastSyncManager"); try { fastSyncThread.interrupt(); fastSyncInProgress = false; dbWriterThread.interrupt(); dbFlushManager.commit(); dbFlushManager.flushSync(); fastSyncThread.join(10 * 1000); dbWriterThread.join(10 * 1000); } catch (Exception e) { logger.warn("Problems closing FastSyncManager", e); } } }