/* * 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.net.eth.handler; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.netty.channel.ChannelHandlerContext; import org.apache.commons.lang3.tuple.Pair; import org.ethereum.config.SystemProperties; import org.ethereum.core.*; import org.ethereum.db.BlockStore; import org.ethereum.listener.CompositeEthereumListener; import org.ethereum.net.eth.EthVersion; import org.ethereum.net.eth.message.*; import org.ethereum.net.message.ReasonCode; import org.ethereum.net.rlpx.discover.NodeManager; import org.ethereum.net.submit.TransactionExecutor; import org.ethereum.net.submit.TransactionTask; import org.ethereum.sync.SyncManager; import org.ethereum.sync.PeerState; import org.ethereum.sync.SyncStatistics; import org.ethereum.util.ByteUtil; import org.ethereum.util.Utils; import org.ethereum.validator.BlockHeaderRule; import org.ethereum.validator.BlockHeaderValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.annotation.Nullable; import java.math.BigInteger; import java.util.*; import static java.lang.Math.min; import static java.util.Collections.singletonList; import static org.ethereum.net.eth.EthVersion.V62; import static org.ethereum.net.message.ReasonCode.USELESS_PEER; import static org.ethereum.sync.PeerState.*; import static org.ethereum.sync.PeerState.BLOCK_RETRIEVING; import static org.ethereum.util.Utils.longToTimePeriod; import static org.spongycastle.util.encoders.Hex.toHexString; /** * Eth 62 * * @author Mikhail Kalinin * @since 04.09.2015 */ @Component("Eth62") @Scope("prototype") public class Eth62 extends EthHandler { protected static final int MAX_HASHES_TO_SEND = 65536; protected final static Logger logger = LoggerFactory.getLogger("sync"); protected final static Logger loggerNet = LoggerFactory.getLogger("net"); @Autowired protected BlockStore blockstore; @Autowired protected SyncManager syncManager; @Autowired protected PendingState pendingState; @Autowired protected NodeManager nodeManager; protected EthState ethState = EthState.INIT; protected PeerState peerState = IDLE; protected boolean syncDone = false; /** * Number and hash of best known remote block */ protected BlockIdentifier bestKnownBlock; private BigInteger totalDifficulty; /** * Header list sent in GET_BLOCK_BODIES message, * used to create blocks from headers and bodies * also, is useful when returned BLOCK_BODIES msg doesn't cover all sent hashes * or in case when peer is disconnected */ protected final List<BlockHeaderWrapper> sentHeaders = Collections.synchronizedList(new ArrayList<BlockHeaderWrapper>()); protected SettableFuture<List<Block>> futureBlocks; protected final SyncStatistics syncStats = new SyncStatistics(); protected GetBlockHeadersMessageWrapper headerRequest; private Map<Long, BlockHeaderValidator> validatorMap; protected long lastReqSentTime; protected long connectedTime = System.currentTimeMillis(); protected long processingTime = 0; private static final EthVersion version = V62; public Eth62() { this(version); } Eth62(final EthVersion version) { super(version); } @Autowired public Eth62(final SystemProperties config, final Blockchain blockchain, final BlockStore blockStore, final CompositeEthereumListener ethereumListener) { this(version, config, blockchain, blockStore, ethereumListener); } Eth62(final EthVersion version, final SystemProperties config, final Blockchain blockchain, final BlockStore blockStore, final CompositeEthereumListener ethereumListener) { super(version, config, blockchain, blockStore, ethereumListener); } @Override public void channelRead0(final ChannelHandlerContext ctx, EthMessage msg) throws InterruptedException { super.channelRead0(ctx, msg); switch (msg.getCommand()) { case STATUS: processStatus((StatusMessage) msg, ctx); break; case NEW_BLOCK_HASHES: processNewBlockHashes((NewBlockHashesMessage) msg); break; case TRANSACTIONS: processTransactions((TransactionsMessage) msg); break; case GET_BLOCK_HEADERS: processGetBlockHeaders((GetBlockHeadersMessage) msg); break; case BLOCK_HEADERS: processBlockHeaders((BlockHeadersMessage) msg); break; case GET_BLOCK_BODIES: processGetBlockBodies((GetBlockBodiesMessage) msg); break; case BLOCK_BODIES: processBlockBodies((BlockBodiesMessage) msg); break; case NEW_BLOCK: processNewBlock((NewBlockMessage) msg); break; default: break; } } /************************* * Message Sending * *************************/ @Override public synchronized void sendStatus() { byte protocolVersion = getVersion().getCode(); int networkId = config.networkId(); final BigInteger totalDifficulty; final byte[] bestHash; if (syncManager.isFastSyncRunning()) { // while fastsync is not complete reporting block #0 // until all blocks/receipts are downloaded bestHash = blockstore.getBlockHashByNumber(0); Block genesis = blockstore.getBlockByHash(bestHash); totalDifficulty = genesis.getDifficultyBI(); } else { // Getting it from blockstore, not blocked by blockchain sync bestHash = blockstore.getBestBlock().getHash(); totalDifficulty = blockchain.getTotalDifficulty(); } StatusMessage msg = new StatusMessage(protocolVersion, networkId, ByteUtil.bigIntegerToBytes(totalDifficulty), bestHash, config.getGenesis().getHash()); sendMessage(msg); ethState = EthState.STATUS_SENT; sendNextHeaderRequest(); } @Override public synchronized void sendNewBlockHashes(Block block) { BlockIdentifier identifier = new BlockIdentifier(block.getHash(), block.getNumber()); NewBlockHashesMessage msg = new NewBlockHashesMessage(singletonList(identifier)); sendMessage(msg); } @Override public synchronized void sendTransaction(List<Transaction> txs) { TransactionsMessage msg = new TransactionsMessage(txs); sendMessage(msg); } @Override public synchronized ListenableFuture<List<BlockHeader>> sendGetBlockHeaders(long blockNumber, int maxBlocksAsk, boolean reverse) { if (ethState == EthState.STATUS_SUCCEEDED && peerState != IDLE) return null; if(logger.isTraceEnabled()) logger.trace( "Peer {}: queue GetBlockHeaders, blockNumber [{}], maxBlocksAsk [{}]", channel.getPeerIdShort(), blockNumber, maxBlocksAsk ); if (headerRequest != null) { throw new RuntimeException("The peer is waiting for headers response: " + this); } GetBlockHeadersMessage headersRequest = new GetBlockHeadersMessage(blockNumber, null, maxBlocksAsk, 0, reverse); GetBlockHeadersMessageWrapper messageWrapper = new GetBlockHeadersMessageWrapper(headersRequest); headerRequest = messageWrapper; sendNextHeaderRequest(); return messageWrapper.getFutureHeaders(); } @Override public synchronized ListenableFuture<List<BlockHeader>> sendGetBlockHeaders(byte[] blockHash, int maxBlocksAsk, int skip, boolean reverse) { return sendGetBlockHeaders(blockHash, maxBlocksAsk, skip, reverse, false); } protected synchronized void sendGetNewBlockHeaders(byte[] blockHash, int maxBlocksAsk, int skip, boolean reverse) { sendGetBlockHeaders(blockHash, maxBlocksAsk, skip, reverse, true); } protected synchronized ListenableFuture<List<BlockHeader>> sendGetBlockHeaders(byte[] blockHash, int maxBlocksAsk, int skip, boolean reverse, boolean newHashes) { if (peerState != IDLE) return null; if(logger.isTraceEnabled()) logger.trace( "Peer {}: queue GetBlockHeaders, blockHash [{}], maxBlocksAsk [{}], skip[{}], reverse [{}]", channel.getPeerIdShort(), "0x" + toHexString(blockHash).substring(0, 8), maxBlocksAsk, skip, reverse ); if (headerRequest != null) { throw new RuntimeException("The peer is waiting for headers response: " + this); } GetBlockHeadersMessage headersRequest = new GetBlockHeadersMessage(0, blockHash, maxBlocksAsk, skip, reverse); GetBlockHeadersMessageWrapper messageWrapper = new GetBlockHeadersMessageWrapper(headersRequest, newHashes); headerRequest = messageWrapper; sendNextHeaderRequest(); lastReqSentTime = System.currentTimeMillis(); return messageWrapper.getFutureHeaders(); } @Override public synchronized ListenableFuture<List<Block>> sendGetBlockBodies(List<BlockHeaderWrapper> headers) { if (peerState != IDLE) return null; peerState = BLOCK_RETRIEVING; sentHeaders.clear(); sentHeaders.addAll(headers); if(logger.isTraceEnabled()) logger.trace( "Peer {}: send GetBlockBodies, hashes.count [{}]", channel.getPeerIdShort(), sentHeaders.size() ); List<byte[]> hashes = new ArrayList<>(headers.size()); for (BlockHeaderWrapper header : headers) { hashes.add(header.getHash()); } GetBlockBodiesMessage msg = new GetBlockBodiesMessage(hashes); sendMessage(msg); lastReqSentTime = System.currentTimeMillis(); futureBlocks = SettableFuture.create(); return futureBlocks; } @Override public synchronized void sendNewBlock(Block block) { BigInteger parentTD = blockstore.getTotalDifficultyForHash(block.getParentHash()); byte[] td = ByteUtil.bigIntegerToBytes(parentTD.add(new BigInteger(1, block.getDifficulty()))); NewBlockMessage msg = new NewBlockMessage(block, td); sendMessage(msg); } /************************* * Message Processing * *************************/ protected synchronized void processStatus(StatusMessage msg, ChannelHandlerContext ctx) throws InterruptedException { try { if (!Arrays.equals(msg.getGenesisHash(), config.getGenesis().getHash())) { if (!peerDiscoveryMode) { loggerNet.debug("Removing EthHandler for {} due to protocol incompatibility", ctx.channel().remoteAddress()); } ethState = EthState.STATUS_FAILED; disconnect(ReasonCode.INCOMPATIBLE_PROTOCOL); ctx.pipeline().remove(this); // Peer is not compatible for the 'eth' sub-protocol return; } if (msg.getNetworkId() != config.networkId()) { ethState = EthState.STATUS_FAILED; disconnect(ReasonCode.NULL_IDENTITY); return; } // basic checks passed, update statistics channel.getNodeStatistics().ethHandshake(msg); ethereumListener.onEthStatusUpdated(channel, msg); if (peerDiscoveryMode) { loggerNet.trace("Peer discovery mode: STATUS received, disconnecting..."); disconnect(ReasonCode.REQUESTED); ctx.close().sync(); ctx.disconnect().sync(); return; } // update bestKnownBlock info sendGetBlockHeaders(msg.getBestHash(), 1, 0, false); } catch (NoSuchElementException e) { loggerNet.debug("EthHandler already removed"); } } protected synchronized void processNewBlockHashes(NewBlockHashesMessage msg) { if(logger.isTraceEnabled()) logger.trace( "Peer {}: processing NewBlockHashes, size [{}]", channel.getPeerIdShort(), msg.getBlockIdentifiers().size() ); List<BlockIdentifier> identifiers = msg.getBlockIdentifiers(); if (identifiers.isEmpty()) return; updateBestBlock(identifiers); // queueing new blocks doesn't make sense // while Long sync is in progress if (!syncDone) return; if (peerState != HEADER_RETRIEVING) { long firstBlockAsk = Long.MAX_VALUE; long lastBlockAsk = 0; byte[] firstBlockHash = null; for (BlockIdentifier identifier : identifiers) { long blockNumber = identifier.getNumber(); if (blockNumber < firstBlockAsk) { firstBlockAsk = blockNumber; firstBlockHash = identifier.getHash(); } if (blockNumber > lastBlockAsk) { lastBlockAsk = blockNumber; } } long maxBlocksAsk = lastBlockAsk - firstBlockAsk + 1; if (firstBlockHash != null && maxBlocksAsk > 0 && maxBlocksAsk < MAX_HASHES_TO_SEND) { sendGetNewBlockHeaders(firstBlockHash, (int) maxBlocksAsk, 0, false); } } } protected synchronized void processTransactions(TransactionsMessage msg) { if(!processTransactions) { return; } List<Transaction> txSet = msg.getTransactions(); List<Transaction> newPending = pendingState.addPendingTransactions(txSet); if (!newPending.isEmpty()) { TransactionTask transactionTask = new TransactionTask(newPending, channel.getChannelManager(), channel); TransactionExecutor.instance.submitTransaction(transactionTask); } } protected synchronized void processGetBlockHeaders(GetBlockHeadersMessage msg) { List<BlockHeader> headers = blockchain.getListOfHeadersStartFrom( msg.getBlockIdentifier(), msg.getSkipBlocks(), min(msg.getMaxHeaders(), MAX_HASHES_TO_SEND), msg.isReverse() ); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } protected synchronized void processBlockHeaders(BlockHeadersMessage msg) { if(logger.isTraceEnabled()) logger.trace( "Peer {}: processing BlockHeaders, size [{}]", channel.getPeerIdShort(), msg.getBlockHeaders().size() ); GetBlockHeadersMessageWrapper request = headerRequest; headerRequest = null; if (!isValid(msg, request)) { dropConnection(); return; } List<BlockHeader> received = msg.getBlockHeaders(); if (ethState == EthState.STATUS_SENT || ethState == EthState.HASH_CONSTRAINTS_CHECK) processInitHeaders(received); else { syncStats.addHeaders(received.size()); request.getFutureHeaders().set(received); } processingTime += lastReqSentTime > 0 ? (System.currentTimeMillis() - lastReqSentTime) : 0; lastReqSentTime = 0; peerState = IDLE; } protected synchronized void processGetBlockBodies(GetBlockBodiesMessage msg) { List<byte[]> bodies = blockchain.getListOfBodiesByHashes(msg.getBlockHashes()); BlockBodiesMessage response = new BlockBodiesMessage(bodies); sendMessage(response); } protected synchronized void processBlockBodies(BlockBodiesMessage msg) { if (logger.isTraceEnabled()) logger.trace( "Peer {}: process BlockBodies, size [{}]", channel.getPeerIdShort(), msg.getBlockBodies().size() ); if (!isValid(msg)) { dropConnection(); return; } syncStats.addBlocks(msg.getBlockBodies().size()); List<Block> blocks = null; try { blocks = validateAndMerge(msg); } catch (Exception e) { logger.info("Fatal validation error while processing block bodies from peer {}", channel.getPeerIdShort()); } if (blocks == null) { // headers will be returned by #onShutdown() dropConnection(); return; } futureBlocks.set(blocks); futureBlocks = null; processingTime += (System.currentTimeMillis() - lastReqSentTime); lastReqSentTime = 0; peerState = IDLE; } protected synchronized void processNewBlock(NewBlockMessage newBlockMessage) { Block newBlock = newBlockMessage.getBlock(); logger.debug("New block received: block.index [{}]", newBlock.getNumber()); updateTotalDifficulty(newBlockMessage.getDifficultyAsBigInt()); updateBestBlock(newBlock); if (!syncManager.validateAndAddNewBlock(newBlock, channel.getNodeId())) { dropConnection(); } } /************************* * Sync Management * *************************/ @Override public synchronized void onShutdown() { } @Override public synchronized void fetchBodies(List<BlockHeaderWrapper> headers) { syncStats.reset(); sendGetBlockBodies(headers); } protected synchronized void sendNextHeaderRequest() { // do not send header requests if status hasn't been passed yet if (ethState == EthState.INIT) return; GetBlockHeadersMessageWrapper wrapper = headerRequest; if (wrapper == null || wrapper.isSent()) return; peerState = HEADER_RETRIEVING; wrapper.send(); sendMessage(wrapper.getMessage()); lastReqSentTime = System.currentTimeMillis(); } protected synchronized void processInitHeaders(List<BlockHeader> received) { final BlockHeader blockHeader = received.get(0); final long blockNumber = blockHeader.getNumber(); if (ethState == EthState.STATUS_SENT) { updateBestBlock(blockHeader); logger.trace("Peer {}: init request succeeded, best known block {}", channel.getPeerIdShort(), bestKnownBlock); // checking if the peer has expected block hashes ethState = EthState.HASH_CONSTRAINTS_CHECK; validatorMap = Collections.synchronizedMap(new HashMap<Long, BlockHeaderValidator>()); List<Pair<Long, BlockHeaderValidator>> validators = config.getBlockchainConfig(). getConfigForBlock(blockNumber).headerValidators(); for (Pair<Long, BlockHeaderValidator> validator : validators) { if (validator.getLeft() <= getBestKnownBlock().getNumber()) { validatorMap.put(validator.getLeft(), validator.getRight()); } } logger.trace("Peer " + channel.getPeerIdShort() + ": Requested " + validatorMap.size() + " headers for hash check: " + validatorMap.keySet()); requestNextHashCheck(); } else { BlockHeaderValidator validator = validatorMap.get(blockNumber); if (validator != null) { BlockHeaderRule.ValidationResult result = validator.validate(blockHeader); if (result.success) { validatorMap.remove(blockNumber); requestNextHashCheck(); } else { logger.debug("Peer {}: wrong fork ({}). Drop the peer and reduce reputation.", channel.getPeerIdShort(), result.error); channel.getNodeStatistics().wrongFork = true; dropConnection(); } } } if (validatorMap.isEmpty()) { ethState = EthState.STATUS_SUCCEEDED; logger.trace("Peer {}: all validations passed", channel.getPeerIdShort()); } } private void requestNextHashCheck() { if (!validatorMap.isEmpty()) { final Long checkHeader = validatorMap.keySet().iterator().next(); sendGetBlockHeaders(checkHeader, 1, false); logger.trace("Peer {}: Requested #{} header for hash check.", channel.getPeerIdShort(), checkHeader); } } private void updateBestBlock(Block block) { updateBestBlock(block.getHeader()); } private void updateBestBlock(BlockHeader header) { if (bestKnownBlock == null || header.getNumber() > bestKnownBlock.getNumber()) { bestKnownBlock = new BlockIdentifier(header.getHash(), header.getNumber()); } } private void updateBestBlock(List<BlockIdentifier> identifiers) { for (BlockIdentifier id : identifiers) if (bestKnownBlock == null || id.getNumber() > bestKnownBlock.getNumber()) { bestKnownBlock = id; } } @Override public BlockIdentifier getBestKnownBlock() { return bestKnownBlock; } private void updateTotalDifficulty(BigInteger totalDiff) { channel.getNodeStatistics().setEthTotalDifficulty(totalDiff); this.totalDifficulty = totalDiff; } @Override public BigInteger getTotalDifficulty() { return totalDifficulty != null ? totalDifficulty : channel.getNodeStatistics().getEthTotalDifficulty(); } /************************* * Getters, setters * *************************/ @Override public boolean isHashRetrievingDone() { return peerState == DONE_HASH_RETRIEVING; } @Override public boolean isHashRetrieving() { return peerState == HEADER_RETRIEVING; } @Override public boolean hasStatusPassed() { return ethState.ordinal() > EthState.HASH_CONSTRAINTS_CHECK.ordinal(); } @Override public boolean hasStatusSucceeded() { return ethState == EthState.STATUS_SUCCEEDED; } @Override public boolean isIdle() { return peerState == IDLE; } @Override public void enableTransactions() { processTransactions = true; } @Override public void disableTransactions() { processTransactions = false; } @Override public SyncStatistics getStats() { return syncStats; } @Override public void onSyncDone(boolean done) { syncDone = done; } /************************* * Validation * *************************/ @Nullable private List<Block> validateAndMerge(BlockBodiesMessage response) { // merging received block bodies with requested headers // the assumption is the following: // - response may miss any bodies present in the request // - response may not contain non-requested bodies // - order of response bodies should be preserved // Otherwise the response is assumed invalid and all bodies are dropped List<byte[]> bodyList = response.getBlockBodies(); Iterator<byte[]> bodies = bodyList.iterator(); Iterator<BlockHeaderWrapper> wrappers = sentHeaders.iterator(); List<Block> blocks = new ArrayList<>(bodyList.size()); List<BlockHeaderWrapper> coveredHeaders = new ArrayList<>(sentHeaders.size()); boolean blockMerged = true; byte[] body = null; while (bodies.hasNext() && wrappers.hasNext()) { BlockHeaderWrapper wrapper = wrappers.next(); if (blockMerged) { body = bodies.next(); } Block b = new Block.Builder() .withHeader(wrapper.getHeader()) .withBody(body) .create(); if (b == null) { blockMerged = false; } else { blockMerged = true; coveredHeaders.add(wrapper); blocks.add(b); } } if (bodies.hasNext()) { logger.info("Peer {}: invalid BLOCK_BODIES response: at least one block body doesn't correspond to any of requested headers: ", channel.getPeerIdShort(), Hex.toHexString(bodies.next())); return null; } // remove headers covered by response sentHeaders.removeAll(coveredHeaders); return blocks; } private boolean isValid(BlockBodiesMessage response) { return response.getBlockBodies().size() <= sentHeaders.size(); } protected boolean isValid(BlockHeadersMessage response, GetBlockHeadersMessageWrapper requestWrapper) { GetBlockHeadersMessage request = requestWrapper.getMessage(); List<BlockHeader> headers = response.getBlockHeaders(); // max headers if (headers.size() > request.getMaxHeaders()) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to {}, exceeds maxHeaders limit, headers count={}", channel.getPeerIdShort(), request, headers.size() ); return false; } // emptiness against best known block if (headers.isEmpty()) { // initial call after handshake if (ethState == EthState.STATUS_SENT || ethState == EthState.HASH_CONSTRAINTS_CHECK) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to initial {}, empty", channel.getPeerIdShort(), request ); return false; } if (request.getBlockHash() == null && request.getBlockNumber() <= bestKnownBlock.getNumber()) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to {}, it's empty while bestKnownBlock is {}", channel.getPeerIdShort(), request, bestKnownBlock ); return false; } return true; } // first header BlockHeader first = headers.get(0); if (request.getBlockHash() != null) { if (!Arrays.equals(request.getBlockHash(), first.getHash())) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to {}, first header is invalid {}", channel.getPeerIdShort(), request, first ); return false; } } else { if (request.getBlockNumber() != first.getNumber()) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to {}, first header is invalid {}", channel.getPeerIdShort(), request, first ); return false; } } // skip following checks in case of NEW_BLOCK_HASHES handling if (requestWrapper.isNewHashesHandling()) return true; // numbers and ancestors int offset = 1 + request.getSkipBlocks(); if (request.isReverse()) offset = -offset; for (int i = 1; i < headers.size(); i++) { BlockHeader cur = headers.get(i); BlockHeader prev = headers.get(i - 1); long num = cur.getNumber(); long expectedNum = prev.getNumber() + offset; if (num != expectedNum) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to {}, got #{}, expected #{}", channel.getPeerIdShort(), request, num, expectedNum ); return false; } if (request.getSkipBlocks() == 0) { BlockHeader parent; BlockHeader child; if (request.isReverse()) { parent = cur; child = prev; } else { parent = prev; child = cur; } if (!Arrays.equals(child.getParentHash(), parent.getHash())) { if (logger.isInfoEnabled()) logger.info( "Peer {}: invalid response to {}, got parent hash {} for #{}, expected {}", channel.getPeerIdShort(), request, toHexString(child.getParentHash()), prev.getNumber(), toHexString(parent.getHash()) ); return false; } } } return true; } @Override public synchronized void dropConnection() { logger.info("Peer {}: is a bad one, drop", channel.getPeerIdShort()); disconnect(USELESS_PEER); } /************************* * Logging * *************************/ @Override public String getSyncStats() { int waitResp = lastReqSentTime > 0 ? (int) (System.currentTimeMillis() - lastReqSentTime) / 1000 : 0; long lifeTime = System.currentTimeMillis() - connectedTime; return String.format( "Peer %s: [ %s, %18s, ping %6s ms, difficulty %s, best block %s%s]: (idle %s of %s) %s", getVersion(), channel.getPeerIdShort(), peerState, (int)channel.getPeerStats().getAvgLatency(), getTotalDifficulty(), getBestKnownBlock().getNumber(), waitResp > 5 ? ", wait " + waitResp + "s" : " ", longToTimePeriod(lifeTime - processingTime), longToTimePeriod(lifeTime), channel.getNodeStatistics().getClientId()); } protected enum EthState { INIT, STATUS_SENT, HASH_CONSTRAINTS_CHECK, STATUS_SUCCEEDED, STATUS_FAILED } }