/* * 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.samples; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.spi.FilterReply; import org.ethereum.config.SystemProperties; import org.ethereum.core.*; import org.ethereum.facade.Ethereum; import org.ethereum.facade.EthereumFactory; import org.ethereum.listener.EthereumListener; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.net.eth.message.StatusMessage; import org.ethereum.net.message.Message; import org.ethereum.net.p2p.HelloMessage; import org.ethereum.net.rlpx.Node; import org.ethereum.net.server.Channel; import org.ethereum.util.ByteUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import javax.annotation.PostConstruct; import java.util.*; /** * The base sample class which creates EthereumJ instance, tracks and report all the stages * of starting up like discovering nodes, connecting, syncing * * The class can be started as a standalone sample it should just run until full blockchain * sync and then just hanging, listening for new blocks and importing them into a DB * * This class is a Spring Component which makes it convenient to easily get access (autowire) to * all components created within EthereumJ. However almost all this could be done without dealing * with the Spring machinery from within a simple main method * * Created by Anton Nashatyrev on 05.02.2016. */ public class BasicSample implements Runnable { public static final Logger sLogger = LoggerFactory.getLogger("sample"); private static CustomFilter CUSTOM_FILTER; private String loggerName; protected Logger logger; @Autowired protected Ethereum ethereum; @Autowired protected SystemProperties config; private volatile long txCount; private volatile long gasSpent; // Spring config class which add this sample class as a bean to the components collections // and make it possible for autowiring other components private static class Config { @Bean public BasicSample basicSample() { return new BasicSample(); } } public static void main(String[] args) throws Exception { sLogger.info("Starting EthereumJ!"); // Based on Config class the BasicSample would be created by Spring // and its springInit() method would be called as an entry point EthereumFactory.createEthereum(Config.class); } public BasicSample() { this("sample"); } /** * logger name can be passed if more than one EthereumJ instance is created * in a single JVM to distinguish logging output from different instances */ public BasicSample(String loggerName) { this.loggerName = loggerName; } protected void setupLogging() { addSampleLogger(loggerName); logger = LoggerFactory.getLogger(loggerName); } /** * Allow only selected logger to print DEBUG events to STDOUT and FILE. * Other loggers are allowed to print ERRORS only. */ private static void addSampleLogger(final String loggerName) { if (CUSTOM_FILTER == null) { CUSTOM_FILTER = new CustomFilter(); final LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); Appender ca = loggerContext.getLogger("ROOT").getAppender("STDOUT"); ca.clearAllFilters(); ca.addFilter(CUSTOM_FILTER); } CUSTOM_FILTER.addVisibleLogger(loggerName); } /** * The method is called after all EthereumJ instances are created */ @PostConstruct private void springInit() { setupLogging(); // adding the main EthereumJ callback to be notified on different kind of events ethereum.addListener(listener); logger.info("Sample component created. Listening for ethereum events..."); // starting lifecycle tracking method run() new Thread(this, "SampleWorkThread").start(); } /** * The method tracks step-by-step the instance lifecycle from node discovery till sync completion. * At the end the method onSyncDone() is called which might be overridden by a sample subclass * to start making other things with the Ethereum network */ public void run() { try { logger.info("Sample worker thread started."); if (config.peerDiscovery()) { waitForDiscovery(); } else { logger.info("Peer discovery disabled. We should actively connect to another peers or wait for incoming connections"); } waitForAvailablePeers(); waitForSyncPeers(); waitForFirstBlock(); waitForSync(); onSyncDone(); } catch (Exception e) { logger.error("Error occurred in Sample: ", e); } } /** * Is called when the whole blockchain sync is complete */ public void onSyncDone() throws Exception { logger.info("Monitoring new blocks in real-time..."); } protected List<Node> nodesDiscovered = new Vector<>(); /** * Waits until any new nodes are discovered by the UDP discovery protocol */ protected void waitForDiscovery() throws Exception { logger.info("Waiting for nodes discovery..."); int bootNodes = config.peerDiscoveryIPList().size(); int cnt = 0; while(true) { Thread.sleep(cnt < 30 ? 300 : 5000); if (nodesDiscovered.size() > bootNodes) { logger.info("[v] Discovery works, new nodes started being discovered."); return; } if (cnt >= 30) logger.warn("Discovery keeps silence. Waiting more..."); if (cnt > 50) { logger.error("Looks like discovery failed, no nodes were found.\n" + "Please check your Firewall/NAT UDP protocol settings.\n" + "Your IP interface was detected as " + config.bindIp() + ", please check " + "if this interface is correct, otherwise set it manually via 'peer.discovery.bind.ip' option."); throw new RuntimeException("Discovery failed."); } cnt++; } } protected Map<Node, StatusMessage> ethNodes = new Hashtable<>(); /** * Discovering nodes is only the first step. No we need to find among discovered nodes * those ones which are live, accepting inbound connections, and has compatible subprotocol versions */ protected void waitForAvailablePeers() throws Exception { logger.info("Waiting for available Eth capable nodes..."); int cnt = 0; while(true) { Thread.sleep(cnt < 30 ? 1000 : 5000); if (ethNodes.size() > 0) { logger.info("[v] Available Eth nodes found."); return; } if (cnt >= 30) logger.info("No Eth nodes found so far. Keep searching..."); if (cnt > 60) { logger.error("No eth capable nodes found. Logs need to be investigated."); // throw new RuntimeException("Eth nodes failed."); } cnt++; } } protected List<Node> syncPeers = new Vector<>(); /** * When live nodes found SyncManager should select from them the most * suitable and add them as peers for syncing the blocks */ protected void waitForSyncPeers() throws Exception { logger.info("Searching for peers to sync with..."); int cnt = 0; while(true) { Thread.sleep(cnt < 30 ? 1000 : 5000); if (syncPeers.size() > 0) { logger.info("[v] At least one sync peer found."); return; } if (cnt >= 30) logger.info("No sync peers found so far. Keep searching..."); if (cnt > 60) { logger.error("No sync peers found. Logs need to be investigated."); // throw new RuntimeException("Sync peers failed."); } cnt++; } } protected Block bestBlock = null; /** * Waits until blocks import started */ protected void waitForFirstBlock() throws Exception { Block currentBest = ethereum.getBlockchain().getBestBlock(); logger.info("Current BEST block: " + currentBest.getShortDescr()); logger.info("Waiting for blocks start importing (may take a while)..."); int cnt = 0; while(true) { Thread.sleep(cnt < 300 ? 1000 : 60000); if (bestBlock != null && bestBlock.getNumber() > currentBest.getNumber()) { logger.info("[v] Blocks import started."); return; } if (cnt >= 300) logger.info("Still no blocks. Be patient..."); if (cnt > 330) { logger.error("No blocks imported during a long period. Must be a problem, logs need to be investigated."); // throw new RuntimeException("Block import failed."); } cnt++; } } boolean synced = false; boolean syncComplete = false; /** * Waits until the whole blockchain sync is complete */ private void waitForSync() throws Exception { logger.info("Waiting for the whole blockchain sync (will take up to several hours for the whole chain)..."); while(true) { Thread.sleep(10000); if (synced) { logger.info("[v] Sync complete! The best block: " + bestBlock.getShortDescr()); syncComplete = true; return; } logger.info("Blockchain sync in progress. Last imported block: " + bestBlock.getShortDescr() + " (Total: txs: " + txCount + ", gas: " + (gasSpent / 1000) + "k)"); txCount = 0; gasSpent = 0; } } /** * The main EthereumJ callback. */ EthereumListener listener = new EthereumListenerAdapter() { @Override public void onSyncDone(SyncState state) { synced = true; } @Override public void onNodeDiscovered(Node node) { if (nodesDiscovered.size() < 1000) { nodesDiscovered.add(node); } } @Override public void onEthStatusUpdated(Channel channel, StatusMessage statusMessage) { ethNodes.put(channel.getNode(), statusMessage); } @Override public void onPeerAddedToSyncPool(Channel peer) { syncPeers.add(peer.getNode()); } @Override public void onBlock(Block block, List<TransactionReceipt> receipts) { bestBlock = block; txCount += receipts.size(); for (TransactionReceipt receipt : receipts) { gasSpent += ByteUtil.byteArrayToLong(receipt.getGasUsed()); } if (syncComplete) { logger.info("New block: " + block.getShortDescr()); } } @Override public void onRecvMessage(Channel channel, Message message) { } @Override public void onSendMessage(Channel channel, Message message) { } @Override public void onPeerDisconnect(String host, long port) { } @Override public void onPendingTransactionsReceived(List<Transaction> transactions) { } @Override public void onPendingStateChanged(PendingState pendingState) { } @Override public void onHandShakePeer(Channel channel, HelloMessage helloMessage) { } @Override public void onNoConnections() { } @Override public void onVMTraceCreated(String transactionHash, String trace) { } @Override public void onTransactionExecuted(TransactionExecutionSummary summary) { } }; private static class CustomFilter extends Filter<ILoggingEvent> { private Set<String> visibleLoggers = new HashSet<>(); @Override public synchronized FilterReply decide(ILoggingEvent event) { return visibleLoggers.contains(event.getLoggerName()) && event.getLevel().isGreaterOrEqual(Level.INFO) || event.getLevel().isGreaterOrEqual(Level.ERROR) ? FilterReply.NEUTRAL : FilterReply.DENY; } public synchronized void addVisibleLogger(String name) { visibleLoggers.add(name); } } }