/* * 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 ch.qos.logback.classic.Level; import com.typesafe.config.ConfigFactory; import org.ethereum.config.SystemProperties; import org.ethereum.core.Block; import org.ethereum.core.BlockIdentifier; import org.ethereum.core.BlockSummary; import org.ethereum.core.Transaction; import org.ethereum.core.TransactionReceipt; import org.ethereum.crypto.ECKey; import org.ethereum.facade.Ethereum; import org.ethereum.facade.EthereumFactory; import org.ethereum.listener.EthereumListener; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.mine.Ethash; import org.ethereum.mine.MinerListener; import org.ethereum.net.eth.message.EthMessage; import org.ethereum.net.eth.message.EthMessageCodes; import org.ethereum.net.eth.message.NewBlockHashesMessage; import org.ethereum.net.eth.message.NewBlockMessage; import org.ethereum.net.eth.message.StatusMessage; import org.ethereum.net.eth.message.TransactionsMessage; import org.ethereum.net.message.Message; import org.ethereum.net.rlpx.Node; import org.ethereum.net.server.Channel; import org.ethereum.util.ByteUtil; import org.junit.Ignore; import org.junit.Test; 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.Bean; import javax.annotation.PostConstruct; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Vector; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Long running test * * 3 peers: A <-> B <-> C where A is miner, C is issuing txs, and B should forward Txs/Blocks */ @Ignore public class BlockTxForwardTest { static final Logger testLogger = LoggerFactory.getLogger("TestLogger"); public BlockTxForwardTest() { ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); root.setLevel(Level.INFO); } private static class BasicSample implements Runnable { static final Logger sLogger = LoggerFactory.getLogger("sample"); private String loggerName; public Logger logger; @Autowired protected Ethereum ethereum; @Autowired protected SystemProperties config; // 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; } /** * The method is called after all EthereumJ instances are created */ @PostConstruct private void springInit() { logger = LoggerFactory.getLogger(loggerName); // 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()) { logger.info("Peer discovery disabled. We should actively connect to another peers or wait for incoming connections"); } waitForSync(); onSyncDone(); } catch (Exception e) { logger.error("Error occurred in Sample: ", e); } } /** * 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; } } } /** * Is called when the whole blockchain sync is complete */ public void onSyncDone() throws Exception { logger.info("Monitoring new blocks in real-time..."); } protected Map<Node, StatusMessage> ethNodes = new Hashtable<>(); protected List<Node> syncPeers = new Vector<>(); protected Block bestBlock = null; boolean synced = false; boolean syncComplete = false; /** * The main EthereumJ callback. */ EthereumListener listener = new EthereumListenerAdapter() { @Override public void onSyncDone(SyncState state) { synced = true; } @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; if (syncComplete) { logger.info("New block: " + block.getShortDescr()); } } }; } /** * Spring configuration class for the Miner peer (A) */ private static class MinerConfig { private final String config = // no need for discovery in that small network "peer.discovery.enabled = false \n" + "peer.listen.port = 30335 \n" + // need to have different nodeId's for the peers "peer.privateKey = 6ef8da380c27cea8fdf7448340ea99e8e2268fc2950d79ed47cbf6f85dc977ec \n" + // our private net ID "peer.networkId = 555 \n" + // we have no peers to sync with "sync.enabled = false \n" + // genesis with a lower initial difficulty and some predefined known funded accounts "genesis = sample-genesis.json \n" + // two peers need to have separate database dirs "database.dir = sampleDB-1 \n" + "keyvalue.datasource = leveldb \n" + // when more than 1 miner exist on the network extraData helps to identify the block creator "mine.extraDataHex = cccccccccccccccccccc \n" + "mine.cpuMineThreads = 2 \n" + "cache.flush.blocks = 1"; @Bean public MinerNode node() { return new MinerNode(); } /** * Instead of supplying properties via config file for the peer * we are substituting the corresponding bean which returns required * config for this instance. */ @Bean public SystemProperties systemProperties() { SystemProperties props = new SystemProperties(); props.overrideParams(ConfigFactory.parseString(config.replaceAll("'", "\""))); return props; } } /** * Miner bean, which just start a miner upon creation and prints miner events */ static class MinerNode extends BasicSample implements MinerListener{ public MinerNode() { // peers need different loggers super("sampleMiner"); } // overriding run() method since we don't need to wait for any discovery, // networking or sync events @Override public void run() { if (config.isMineFullDataset()) { logger.info("Generating Full Dataset (may take up to 10 min if not cached)..."); // calling this just for indication of the dataset generation // basically this is not required Ethash ethash = Ethash.getForBlock(config, ethereum.getBlockchain().getBestBlock().getNumber()); ethash.getFullDataset(); logger.info("Full dataset generated (loaded)."); } ethereum.getBlockMiner().addListener(this); ethereum.getBlockMiner().startMining(); } @Override public void miningStarted() { logger.info("Miner started"); } @Override public void miningStopped() { logger.info("Miner stopped"); } @Override public void blockMiningStarted(Block block) { logger.info("Start mining block: " + block.getShortDescr()); } @Override public void blockMined(Block block) { logger.info("Block mined! : \n" + block); } @Override public void blockMiningCanceled(Block block) { logger.info("Cancel mining block: " + block.getShortDescr()); } } /** * Spring configuration class for the Regular peer (B) * It will see nodes A and C, which is not connected directly and proves that tx's from (C) reaches miner (A) * and new blocks both A and C */ private static class RegularConfig { private final String config = // no discovery: we are connecting directly to the generator and miner peers "peer.discovery.enabled = false \n" + "peer.listen.port = 30339 \n" + "peer.privateKey = 1f0bbd4ffd61128a7d150c07d3f5b7dcd078359cd708ada8b60e4b9ffd90b3f5 \n" + "peer.networkId = 555 \n" + // actively connecting to the miner and tx generator "peer.active = [" + // miner " { url = 'enode://26ba1aadaf59d7607ad7f437146927d79e80312f026cfa635c6b2ccf2c5d3521f5812ca2beb3b295b14f97110e6448c1c7ff68f14c5328d43a3c62b44143e9b1@localhost:30335' }, \n" + // tx generator " { url = 'enode://3973cb86d7bef9c96e5d589601d788370f9e24670dcba0480c0b3b1b0647d13d0f0fffed115dd2d4b5ca1929287839dcd4e77bdc724302b44ae48622a8766ee6@localhost:30336' } \n" + "] \n" + "sync.enabled = true \n" + // all peers in the same network need to use the same genesis block "genesis = sample-genesis.json \n" + // two peers need to have separate database dirs "database.dir = sampleDB-2 \n" + "keyvalue.datasource = leveldb \n"; @Bean public RegularNode node() { return new RegularNode(); } /** * Instead of supplying properties via config file for the peer * we are substituting the corresponding bean which returns required * config for this instance. */ @Bean public SystemProperties systemProperties() { SystemProperties props = new SystemProperties(); props.overrideParams(ConfigFactory.parseString(config.replaceAll("'", "\""))); return props; } } /** * This node doing nothing special, but by default as any other node will resend txs and new blocks */ static class RegularNode extends BasicSample { public RegularNode() { // peers need different loggers super("sampleNode"); } } /** * Spring configuration class for the TX-sender peer (C) */ private static class GeneratorConfig { private final String config = // no discovery: forwarder will connect to us "peer.discovery.enabled = false \n" + "peer.listen.port = 30336 \n" + "peer.privateKey = 3ec771c31cac8c0dba77a69e503765701d3c2bb62435888d4ffa38fed60c445c \n" + "peer.networkId = 555 \n" + "sync.enabled = true \n" + // all peers in the same network need to use the same genesis block "genesis = sample-genesis.json \n" + // two peers need to have separate database dirs "database.dir = sampleDB-3 \n" + "keyvalue.datasource = leveldb \n"; @Bean public GeneratorNode node() { return new GeneratorNode(); } /** * Instead of supplying properties via config file for the peer * we are substituting the corresponding bean which returns required * config for this instance. */ @Bean public SystemProperties systemProperties() { SystemProperties props = new SystemProperties(); props.overrideParams(ConfigFactory.parseString(config.replaceAll("'", "\""))); return props; } } /** * The tx generator node in the network which connects to the regular * waits for the sync and starts submitting transactions. * Those transactions should be included into mined blocks and the peer * should receive those blocks back */ static class GeneratorNode extends BasicSample { public GeneratorNode() { // peers need different loggers super("txSenderNode"); } @Override public void onSyncDone() { new Thread(new Runnable() { @Override public void run() { try { generateTransactions(); } catch (Exception e) { logger.error("Error generating tx: ", e); } } }).start(); } /** * Generate one simple value transfer transaction each 7 seconds. * Thus blocks will include one, several and none transactions */ private void generateTransactions() throws Exception{ logger.info("Start generating transactions..."); // the sender which some coins from the genesis ECKey senderKey = ECKey.fromPrivate(Hex.decode("6ef8da380c27cea8fdf7448340ea99e8e2268fc2950d79ed47cbf6f85dc977ec")); byte[] receiverAddr = Hex.decode("5db10750e8caff27f906b41c71b3471057dd2004"); for (int i = ethereum.getRepository().getNonce(senderKey.getAddress()).intValue(), j = 0; j < 20000; i++, j++) { { if (stopTxGeneration.get()) break; Transaction tx = new Transaction(ByteUtil.intToBytesNoLeadZeroes(i), ByteUtil.longToBytesNoLeadZeroes(50_000_000_000L), ByteUtil.longToBytesNoLeadZeroes(0xfffff), receiverAddr, new byte[]{77}, new byte[0]); tx.sign(senderKey); logger.info("<== Submitting tx: " + tx); ethereum.submitTransaction(tx); } Thread.sleep(7000); } } } private final static Map<String, Boolean> blocks = Collections.synchronizedMap(new HashMap<String, Boolean>()); private final static Map<String, Boolean> txs = Collections.synchronizedMap(new HashMap<String, Boolean>()); private final static AtomicInteger fatalErrors = new AtomicInteger(0); private final static AtomicBoolean stopTxGeneration = new AtomicBoolean(false); private final static long MAX_RUN_MINUTES = 360L; // Actually there will be several blocks mined after, it's a very soft shutdown private final static int STOP_ON_BLOCK = 100; private static ScheduledExecutorService statTimer = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { public Thread newThread(Runnable r) { return new Thread(r, "StatTimer"); } }); private boolean logStats() { testLogger.info("---------====---------"); int arrivedBlocks = 0; for (Boolean arrived : blocks.values()) { if (arrived) arrivedBlocks++; } testLogger.info("Arrived blocks / Total: {}/{}", arrivedBlocks, blocks.size()); int arrivedTxs = 0; for (Boolean arrived : txs.values()) { if (arrived) arrivedTxs++; } testLogger.info("Arrived txs / Total: {}/{}", arrivedTxs, txs.size()); testLogger.info("fatalErrors: {}", fatalErrors); testLogger.info("---------====---------"); return fatalErrors.get() == 0 && blocks.size() == arrivedBlocks && txs.size() == arrivedTxs; } /** * Creating 3 EthereumJ instances with different config classes * 1st - Miner node, no sync * 2nd - Regular node, synced with both Miner and Generator * 3rd - Generator node, sync is on, but can see only 2nd node * We want to check that blocks mined on Miner will reach Generator and * txs from Generator will reach Miner node */ @Test public void testTest() throws Exception { statTimer.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { logStats(); if (fatalErrors.get() > 0 || blocks.size() >= STOP_ON_BLOCK) { statTimer.shutdownNow(); } } catch (Throwable t) { testLogger.error("Unhandled exception", t); } } }, 0, 15, TimeUnit.SECONDS); testLogger.info("Starting EthereumJ miner instance!"); Ethereum miner = EthereumFactory.createEthereum(MinerConfig.class); miner.addListener(new EthereumListenerAdapter() { @Override public void onBlock(BlockSummary blockSummary) { if (blockSummary.getBlock().getNumber() != 0L) { blocks.put(Hex.toHexString(blockSummary.getBlock().getHash()), Boolean.FALSE); } } @Override public void onRecvMessage(Channel channel, Message message) { super.onRecvMessage(channel, message); if (!(message instanceof EthMessage)) return; switch (((EthMessage) message).getCommand()) { case NEW_BLOCK_HASHES: testLogger.error("Received new block hash message at miner: {}", message.toString()); fatalErrors.incrementAndGet(); break; case NEW_BLOCK: testLogger.error("Received new block message at miner: {}", message.toString()); fatalErrors.incrementAndGet(); break; case TRANSACTIONS: TransactionsMessage msgCopy = new TransactionsMessage(message.getEncoded()); for (Transaction transaction : msgCopy.getTransactions()) { if (txs.put(Hex.toHexString(transaction.getHash()), Boolean.TRUE) == null) { testLogger.error("Received strange transaction at miner: {}", transaction); fatalErrors.incrementAndGet(); }; } default: break; } } }); testLogger.info("Starting EthereumJ regular instance!"); EthereumFactory.createEthereum(RegularConfig.class); testLogger.info("Starting EthereumJ txSender instance!"); Ethereum txGenerator = EthereumFactory.createEthereum(GeneratorConfig.class); txGenerator.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { super.onRecvMessage(channel, message); if (!(message instanceof EthMessage)) return; switch (((EthMessage) message).getCommand()) { case NEW_BLOCK_HASHES: testLogger.info("Received new block hash message at generator: {}", message.toString()); NewBlockHashesMessage msgCopy = new NewBlockHashesMessage(message.getEncoded()); for (BlockIdentifier identifier : msgCopy.getBlockIdentifiers()) { if (blocks.put(Hex.toHexString(identifier.getHash()), Boolean.TRUE) == null) { testLogger.error("Received strange block: {}", identifier); fatalErrors.incrementAndGet(); }; } break; case NEW_BLOCK: testLogger.info("Received new block message at generator: {}", message.toString()); NewBlockMessage msgCopy2 = new NewBlockMessage(message.getEncoded()); Block block = msgCopy2.getBlock(); if (blocks.put(Hex.toHexString(block.getHash()), Boolean.TRUE) == null) { testLogger.error("Received strange block: {}", block); fatalErrors.incrementAndGet(); }; break; case BLOCK_BODIES: testLogger.info("Received block bodies message at generator: {}", message.toString()); break; case TRANSACTIONS: testLogger.warn("Received new transaction message at generator: {}, " + "allowed only after disconnect.", message.toString()); break; default: break; } } @Override public void onSendMessage(Channel channel, Message message) { super.onSendMessage(channel, message); if (!(message instanceof EthMessage)) return; if (((EthMessage) message).getCommand().equals(EthMessageCodes.TRANSACTIONS)) { TransactionsMessage msgCopy = new TransactionsMessage(message.getEncoded()); for (Transaction transaction : msgCopy.getTransactions()) { Transaction copyTransaction = new Transaction(transaction.getEncoded()); txs.put(Hex.toHexString(copyTransaction.getHash()), Boolean.FALSE); }; } } }); if(statTimer.awaitTermination(MAX_RUN_MINUTES, TimeUnit.MINUTES)) { logStats(); // Stop generating new txs stopTxGeneration.set(true); Thread.sleep(60000); // Stop miner miner.getBlockMiner().stopMining(); // Wait to be sure that last mined blocks will reach Generator Thread.sleep(60000); // Checking stats if (!logStats()) assert false; } } }