/* * 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 org.ethereum.config.NoAutoscan; import org.ethereum.config.SystemProperties; import org.ethereum.config.blockchain.FrontierConfig; import org.ethereum.config.net.MainNetConfig; import org.ethereum.core.*; import org.ethereum.facade.Ethereum; import org.ethereum.facade.EthereumFactory; import org.ethereum.listener.EthereumListenerAdapter; import org.ethereum.net.eth.handler.Eth62; import org.ethereum.net.eth.handler.EthHandler; import org.ethereum.net.eth.message.*; import org.ethereum.net.message.Message; import org.ethereum.net.p2p.DisconnectMessage; import org.ethereum.net.rlpx.Node; import org.ethereum.net.server.Channel; import org.ethereum.util.blockchain.StandaloneBlockchain; import org.junit.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.*; import java.util.concurrent.CountDownLatch; import static java.util.concurrent.TimeUnit.SECONDS; import static org.ethereum.util.FileUtil.recursiveDelete; import static org.junit.Assert.fail; import static org.spongycastle.util.encoders.Hex.decode; /** * @author Mikhail Kalinin * @since 14.12.2015 */ @Ignore("Long network tests") public class LongSyncTest { private static Node nodeA; private static List<Block> mainB1B10; private static Block b10; private Ethereum ethereumA; private Ethereum ethereumB; private EthHandler ethA; private String testDbA; private String testDbB; @BeforeClass public static void setup() throws IOException, URISyntaxException { nodeA = new Node("enode://3973cb86d7bef9c96e5d589601d788370f9e24670dcba0480c0b3b1b0647d13d0f0fffed115dd2d4b5ca1929287839dcd4e77bdc724302b44ae48622a8766ee6@localhost:30334"); SysPropConfigA.props.overrideParams( "peer.listen.port", "30334", "peer.privateKey", "3ec771c31cac8c0dba77a69e503765701d3c2bb62435888d4ffa38fed60c445c", // nodeId: 3973cb86d7bef9c96e5d589601d788370f9e24670dcba0480c0b3b1b0647d13d0f0fffed115dd2d4b5ca1929287839dcd4e77bdc724302b44ae48622a8766ee6 "genesis", "genesis-light-old.json" ); SysPropConfigA.props.setBlockchainConfig(StandaloneBlockchain.getEasyMiningConfig()); SysPropConfigB.props.overrideParams( "peer.listen.port", "30335", "peer.privateKey", "6ef8da380c27cea8fdf7448340ea99e8e2268fc2950d79ed47cbf6f85dc977ec", "genesis", "genesis-light-old.json", "sync.enabled", "true", "sync.max.hashes.ask", "3", "sync.max.blocks.ask", "2" ); SysPropConfigB.props.setBlockchainConfig(StandaloneBlockchain.getEasyMiningConfig()); /* 1 => ed1b6f07d738ad92c5bdc3b98fe25afea9c863dd351711776d9ce1ffb9e3d276 2 => 43808666b662d131c6cff336a0d13608767ead9c9d5f181e95caa3597f3faf14 3 => 1b5c231211f500bc73148dc9d9bdb9de2265465ba441a0db1790ba4b3f5f3e9c 4 => db517e04399dbf5a65caf6b2572b3966c8f98a1d29b1e50dc8db51e54c15d83d 5 => c42d6dbaa756eda7f4244a3507670d764232bd7068d43e6d8ef680c6920132f6 6 => 604c92e8d16dafb64134210d521fcc85aec27452e75aedf708ac72d8240585d3 7 => 3f51b0471eb345b1c5f3c6628e69744358ff81d3f64a3744bbb2edf2adbb0ebc 8 => 62cfd04e29d941954e68ac8ca18ef5cd78b19809eaed860ae72589ebad53a21d 9 => d32fc8e151f158d52fe0be6cba6d0b5c20793a00c4ad0d32db8ccd9269199a29 10 => 22d8c1d909eb142ea0d69d0a38711874f98d6eef1bc669836da36f6b557e9564 */ mainB1B10 = loadBlocks("sync/main-b1-b10.dmp"); b10 = mainB1B10.get(mainB1B10.size() - 1); } private static List<Block> loadBlocks(String path) throws URISyntaxException, IOException { URL url = ClassLoader.getSystemResource(path); File file = new File(url.toURI()); List<String> strData = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); List<Block> blocks = new ArrayList<>(strData.size()); for (String rlp : strData) { blocks.add(new Block(decode(rlp))); } return blocks; } @AfterClass public static void cleanup() { SystemProperties.resetToDefault(); } @Before public void setupTest() throws InterruptedException { testDbA = "test_db_" + new BigInteger(32, new Random()); testDbB = "test_db_" + new BigInteger(32, new Random()); SysPropConfigA.props.setDataBaseDir(testDbA); SysPropConfigB.props.setDataBaseDir(testDbB); } @After public void cleanupTest() { recursiveDelete(testDbA); recursiveDelete(testDbB); SysPropConfigA.eth62 = null; } // general case, A has imported 10 blocks // expected: B downloads blocks from A => B synced @Test public void test1() throws InterruptedException { setupPeers(); // A == b10, B == genesis final CountDownLatch semaphore = new CountDownLatch(1); ethereumB.addListener(new EthereumListenerAdapter() { @Override public void onBlock(Block block, List<TransactionReceipt> receipts) { if (block.isEqual(b10)) { semaphore.countDown(); } } }); semaphore.await(40, SECONDS); // check if B == b10 if(semaphore.getCount() > 0) { fail("PeerB bestBlock is incorrect"); } } // bodies validation: A doesn't send bodies for blocks lower than its best block // expected: B drops A @Test public void test2() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockBodies(GetBlockBodiesMessage msg) { List<byte[]> bodies = Arrays.asList( mainB1B10.get(0).getEncodedBody() ); BlockBodiesMessage response = new BlockBodiesMessage(bodies); sendMessage(response); } }; setupPeers(); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } // headers validation: headers count in A respond more than requested limit // expected: B drops A @Test public void test3() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockHeaders(GetBlockHeadersMessage msg) { if (Arrays.equals(msg.getBlockIdentifier().getHash(), b10.getHash())) { super.processGetBlockHeaders(msg); return; } List<BlockHeader> headers = Arrays.asList( mainB1B10.get(0).getHeader(), mainB1B10.get(1).getHeader(), mainB1B10.get(2).getHeader(), mainB1B10.get(3).getHeader() ); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } }; setupPeers(); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } // headers validation: A sends empty response // expected: B drops A @Test public void test4() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockHeaders(GetBlockHeadersMessage msg) { if (Arrays.equals(msg.getBlockIdentifier().getHash(), b10.getHash())) { super.processGetBlockHeaders(msg); return; } List<BlockHeader> headers = Collections.emptyList(); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } }; setupPeers(); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } // headers validation: first header in response doesn't meet expectations // expected: B drops A @Test public void test5() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockHeaders(GetBlockHeadersMessage msg) { if (Arrays.equals(msg.getBlockIdentifier().getHash(), b10.getHash())) { super.processGetBlockHeaders(msg); return; } List<BlockHeader> headers = Arrays.asList( mainB1B10.get(1).getHeader(), mainB1B10.get(2).getHeader(), mainB1B10.get(3).getHeader() ); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } }; setupPeers(); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } // headers validation: first header in response doesn't meet expectations - second story // expected: B drops A @Test public void test6() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockHeaders(GetBlockHeadersMessage msg) { List<BlockHeader> headers = Collections.singletonList( mainB1B10.get(1).getHeader() ); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } }; ethereumA = EthereumFactory.createEthereum(SysPropConfigA.props, SysPropConfigA.class); Blockchain blockchainA = (Blockchain) ethereumA.getBlockchain(); for (Block b : mainB1B10) { blockchainA.tryToConnect(b); } // A == b10 ethereumB = EthereumFactory.createEthereum(SysPropConfigB.props, SysPropConfigB.class); ethereumB.connect(nodeA); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } // headers validation: headers order is incorrect, reverse = false // expected: B drops A @Test public void test7() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockHeaders(GetBlockHeadersMessage msg) { if (Arrays.equals(msg.getBlockIdentifier().getHash(), b10.getHash())) { super.processGetBlockHeaders(msg); return; } List<BlockHeader> headers = Arrays.asList( mainB1B10.get(0).getHeader(), mainB1B10.get(2).getHeader(), mainB1B10.get(1).getHeader() ); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } }; setupPeers(); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } // headers validation: ancestor's parent hash and header's hash does not match, reverse = false // expected: B drops A @Test public void test8() throws InterruptedException { SysPropConfigA.eth62 = new Eth62() { @Override protected void processGetBlockHeaders(GetBlockHeadersMessage msg) { if (Arrays.equals(msg.getBlockIdentifier().getHash(), b10.getHash())) { super.processGetBlockHeaders(msg); return; } List<BlockHeader> headers = Arrays.asList( mainB1B10.get(0).getHeader(), new BlockHeader(new byte[32], new byte[32], new byte[32], new byte[32], new byte[32], 2, new byte[] {0}, 0, 0, new byte[0], new byte[0], new byte[0]), mainB1B10.get(2).getHeader() ); BlockHeadersMessage response = new BlockHeadersMessage(headers); sendMessage(response); } }; setupPeers(); // A == b10, B == genesis final CountDownLatch semaphoreDisconnect = new CountDownLatch(1); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onRecvMessage(Channel channel, Message message) { if (message instanceof DisconnectMessage) { semaphoreDisconnect.countDown(); } } }); semaphoreDisconnect.await(10, SECONDS); // check if peer was dropped if(semaphoreDisconnect.getCount() > 0) { fail("PeerA is not dropped"); } } private void setupPeers() throws InterruptedException { setupPeers(b10); } private void setupPeers(Block best) throws InterruptedException { ethereumA = EthereumFactory.createEthereum(SysPropConfigA.class); Blockchain blockchainA = (Blockchain) ethereumA.getBlockchain(); for (Block b : mainB1B10) { ImportResult result = blockchainA.tryToConnect(b); Assert.assertEquals(result, ImportResult.IMPORTED_BEST); if (b.equals(best)) break; } // A == best ethereumB = EthereumFactory.createEthereum(SysPropConfigB.props, SysPropConfigB.class); ethereumA.addListener(new EthereumListenerAdapter() { @Override public void onEthStatusUpdated(Channel channel, StatusMessage statusMessage) { ethA = (EthHandler) channel.getEthHandler(); } }); final CountDownLatch semaphore = new CountDownLatch(1); ethereumB.addListener(new EthereumListenerAdapter() { @Override public void onPeerAddedToSyncPool(Channel peer) { semaphore.countDown(); } }); ethereumB.connect(nodeA); semaphore.await(10, SECONDS); if(semaphore.getCount() > 0) { fail("Failed to set up peers"); } } @Configuration @NoAutoscan public static class SysPropConfigA { static SystemProperties props = new SystemProperties(); static Eth62 eth62 = null; @Bean public SystemProperties systemProperties() { return props; } @Bean @Scope("prototype") public Eth62 eth62() throws IllegalAccessException, InstantiationException { if (eth62 != null) return eth62; return new Eth62(); } } @Configuration @NoAutoscan public static class SysPropConfigB { static SystemProperties props = new SystemProperties(); @Bean public SystemProperties systemProperties() { return props; } } }