diff --git a/rskj-core/src/main/java/co/rsk/RskContext.java b/rskj-core/src/main/java/co/rsk/RskContext.java index 992f64395e6..d8d2aa84fc5 100644 --- a/rskj-core/src/main/java/co/rsk/RskContext.java +++ b/rskj-core/src/main/java/co/rsk/RskContext.java @@ -41,6 +41,7 @@ import co.rsk.metrics.HashRateCalculatorNonMining; import co.rsk.mine.*; import co.rsk.net.*; +import co.rsk.net.discovery.KnownPeersHandler; import co.rsk.net.discovery.PeerExplorer; import co.rsk.net.discovery.UDPServer; import co.rsk.net.discovery.table.KademliaOptions; @@ -88,6 +89,7 @@ import co.rsk.util.RskCustomCache; import co.rsk.validators.*; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; import org.ethereum.config.Constants; import org.ethereum.config.SystemProperties; import org.ethereum.config.blockchain.upgrades.ActivationConfig; @@ -157,7 +159,6 @@ public class RskContext implements NodeContext, NodeBootstrapper { private static final String CACHE_FILE_NAME = "rskcache"; private final CliArgs cliArgs; - private RskSystemProperties rskSystemProperties; private Blockchain blockchain; private MiningMainchainView miningMainchainView; @@ -252,7 +253,6 @@ public class RskContext implements NodeContext, NodeBootstrapper { private TxQuotaChecker txQuotaChecker; private GasPriceTracker gasPriceTracker; private BlockChainFlusher blockChainFlusher; - private final Map dbPathToDbKindMap = new HashMap<>(); private volatile boolean closed; @@ -1584,17 +1584,16 @@ protected PeerExplorer getPeerExplorer() { rskSystemProperties.getPublicIp(), rskSystemProperties.getPeerPort() ); - List initialBootNodes = rskSystemProperties.peerDiscoveryIPList(); - List activePeers = rskSystemProperties.peerActive(); - if (activePeers != null) { - for (Node n : activePeers) { - InetSocketAddress address = n.getAddress(); - initialBootNodes.add(address.getHostName() + ":" + address.getPort()); - } + + KnownPeersHandler knownPeersHandler = null; + if (rskSystemProperties.usePeersFromLastSession()) { + knownPeersHandler = new KnownPeersHandler(getRskSystemProperties().getLastKnewPeersFilePath()); + } + int bucketSize = rskSystemProperties.discoveryBucketSize(); peerExplorer = new PeerExplorer( - initialBootNodes, + getInitialBootNodes(knownPeersHandler), localNode, new NodeDistanceTable(KademliaOptions.BINS, bucketSize, localNode), key, @@ -1604,13 +1603,38 @@ protected PeerExplorer getPeerExplorer() { rskSystemProperties.networkId(), getPeerScoringManager(), rskSystemProperties.allowMultipleConnectionsPerHostPort(), - rskSystemProperties.peerDiscoveryMaxBootRetries() + rskSystemProperties.peerDiscoveryMaxBootRetries(), + knownPeersHandler ); } return peerExplorer; } + @VisibleForTesting + List getInitialBootNodes(KnownPeersHandler knownPeersHandler) { + Set initialBootNodes = new HashSet<>(); + RskSystemProperties rskSystemProperties = getRskSystemProperties(); + List peerDiscoveryIPList = rskSystemProperties.peerDiscoveryIPList(); + if (peerDiscoveryIPList != null) { + initialBootNodes.addAll(peerDiscoveryIPList); + } + List activePeers = rskSystemProperties.peerActive(); + if (activePeers != null) { + for (Node n : activePeers) { + InetSocketAddress address = n.getAddress(); + initialBootNodes.add(address.getHostName() + ":" + address.getPort()); + } + } + + if (rskSystemProperties.usePeersFromLastSession()) { + List peerLastSession = knownPeersHandler.readPeers(); + logger.debug("Loading peers from previous session: {}",peerLastSession); + initialBootNodes.addAll(peerLastSession); + } + return new ArrayList<>(initialBootNodes); + } + private BlockChainLoader getBlockChainLoader() { if (blockChainLoader == null) { blockChainLoader = new BlockChainLoader( diff --git a/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java b/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java index c1ee1116750..b2df37f1357 100644 --- a/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java +++ b/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java @@ -67,6 +67,7 @@ public class RskSystemProperties extends SystemProperties { private static final int CHUNK_SIZE = 192; public static final String PROPERTY_SYNC_TOP_BEST = "sync.topBest"; + public static final String USE_PEERS_FROM_LAST_SESSION = "peer.discovery.usePeersFromLastSession"; //TODO: REMOVE THIS WHEN THE LocalBLockTests starts working with REMASC private boolean remascEnabled = true; @@ -254,6 +255,10 @@ public boolean skipRemasc() { return getBoolean("rpc.skipRemasc", false); } + public boolean usePeersFromLastSession() { + return getBoolean(USE_PEERS_FROM_LAST_SESSION, false); + } + public long peerDiscoveryMessageTimeOut() { return getLong("peer.discovery.msg.timeout", PD_DEFAULT_TIMEOUT_MESSAGE); } diff --git a/rskj-core/src/main/java/co/rsk/net/discovery/KnownPeersHandler.java b/rskj-core/src/main/java/co/rsk/net/discovery/KnownPeersHandler.java new file mode 100644 index 00000000000..c9eea7e8612 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/net/discovery/KnownPeersHandler.java @@ -0,0 +1,71 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package co.rsk.net.discovery; + +import co.rsk.util.SimpleFileWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +public class KnownPeersHandler { + private static final Logger logger = LoggerFactory.getLogger(KnownPeersHandler.class); + private final Path peerListFileDir; + private final SimpleFileWriter fileDataSaver; + + public KnownPeersHandler(Path peerListFileDir) { + this(peerListFileDir, SimpleFileWriter.getInstance()); + } + + public KnownPeersHandler(Path peerListFileDir, SimpleFileWriter fileDataSaver) { + this.peerListFileDir = peerListFileDir; + this.fileDataSaver = fileDataSaver; + } + public void savePeers(Map knownPeers) { + logger.debug("Saving peers {} to file in {}", knownPeers, peerListFileDir); + Properties props = new Properties(); + props.putAll(knownPeers); + try { + fileDataSaver.savePropertiesIntoFile(props, peerListFileDir); + } catch (IOException e) { + logger.error("Error saving active peers to file: {}", e.getMessage()); + } + } + + public List readPeers(){ + File file = peerListFileDir.toFile(); + Properties props = new Properties(); + if (file.canRead()) { + try (FileReader reader = new FileReader(file)) { + props.load(reader); + } catch (IOException e) { + logger.error("Error reading active peers from file: {}", e.getMessage()); + return Collections.emptyList(); + } + } + return props.values().stream().map(Object::toString).collect(Collectors.toList()); + } +} diff --git a/rskj-core/src/main/java/co/rsk/net/discovery/PeerExplorer.java b/rskj-core/src/main/java/co/rsk/net/discovery/PeerExplorer.java index 47507ec5724..17b7d8cba8b 100644 --- a/rskj-core/src/main/java/co/rsk/net/discovery/PeerExplorer.java +++ b/rskj-core/src/main/java/co/rsk/net/discovery/PeerExplorer.java @@ -93,10 +93,20 @@ public class PeerExplorer { private UDPChannel udpChannel; + private final KnownPeersHandler knownPeersHandler; + public PeerExplorer(List initialBootNodes, Node localNode, NodeDistanceTable distanceTable, ECKey key, long reqTimeOut, long updatePeriod, long cleanPeriod, Integer networkId, PeerScoringManager peerScoringManager, boolean allowMultipleConnectionsPerHostPort, long maxBootRetries) { + this(initialBootNodes, localNode, distanceTable, key, reqTimeOut, updatePeriod, cleanPeriod, networkId, peerScoringManager, allowMultipleConnectionsPerHostPort, maxBootRetries, null); + } + + public PeerExplorer(List initialBootNodes, + Node localNode, NodeDistanceTable distanceTable, ECKey key, + long reqTimeOut, long updatePeriod, long cleanPeriod, Integer networkId, + PeerScoringManager peerScoringManager, boolean allowMultipleConnectionsPerHostPort, + long maxBootRetries, KnownPeersHandler knownPeersHandler) { this.localNode = localNode; this.key = key; this.distanceTable = distanceTable; @@ -108,13 +118,13 @@ public PeerExplorer(List initialBootNodes, this.cleaner = new PeerExplorerCleaner(updatePeriod, cleanPeriod, this); this.challengeManager = new NodeChallengeManager(); this.requestTimeout = reqTimeOut; - this.peerScoringManager = peerScoringManager; this.knownHosts = new ConcurrentHashMap<>(); this.allowMultipleConnectionsPerHostPort = allowMultipleConnectionsPerHostPort; this.maxBootRetries = maxBootRetries; + this.knownPeersHandler = knownPeersHandler; } void start() { @@ -142,6 +152,13 @@ public synchronized void dispose() { } state = ExecState.FINISHED; + if (knownPeersHandler != null) { + Map knownPeers = getKnownHosts().entrySet().stream() + .collect(Collectors.toMap(e -> e.getValue().toString(), Map.Entry::getKey)); + if (knownPeers.size() > 0) { + knownPeersHandler.savePeers(knownPeers); + } + } this.cleaner.dispose(); } @@ -601,4 +618,8 @@ private boolean isBanned(Node node) { return address != null && this.peerScoringManager.isAddressBanned(address) || this.peerScoringManager.isNodeIDBanned(node.getId()); } + + Map getKnownHosts() { + return knownHosts; + } } diff --git a/rskj-core/src/main/java/co/rsk/util/SimpleFileWriter.java b/rskj-core/src/main/java/co/rsk/util/SimpleFileWriter.java new file mode 100644 index 00000000000..a79b70064d7 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/util/SimpleFileWriter.java @@ -0,0 +1,62 @@ +/* + * This file is part of RskJ + * Copyright (C) 2023 RSK Labs Ltd. + * (derived from ethereumJ library, Copyright (c) 2016 ) + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package co.rsk.util; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +public class SimpleFileWriter { + private static final String TMP = ".tmp"; + private static SimpleFileWriter instance; + + private SimpleFileWriter() { + } + + public static SimpleFileWriter getInstance() { + if (instance == null) { + instance = new SimpleFileWriter(); + } + return instance; + } + + public void savePropertiesIntoFile(Properties properties, Path filePath) throws IOException { + File tempFile = File.createTempFile(filePath.toString(), TMP); + try (FileWriter writer = new FileWriter(tempFile, false)) { + properties.store(writer, null); + } + filePath.toFile().getParentFile().mkdirs(); + Files.move(tempFile.toPath(), filePath, REPLACE_EXISTING); + } + public void saveDataIntoFile(String data, Path filePath) throws IOException { + + File tempFile = File.createTempFile(filePath.toString(), TMP); + try (FileWriter writer = new FileWriter(tempFile, false)) { + writer.write(data); + } + filePath.toFile().getParentFile().mkdirs(); + Files.move(tempFile.toPath(), filePath, REPLACE_EXISTING); + } +} + diff --git a/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java b/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java index f35d2b00fc0..f91d6c30296 100644 --- a/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java +++ b/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java @@ -44,6 +44,8 @@ import java.net.URL; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -106,6 +108,7 @@ public abstract class SystemProperties { public static final String PROPERTY_PERSIST_BLOOMS_CACHE_SNAPSHOT = "cache.blooms.persist-snapshot"; /* Testing */ + public static final String LAST_KNEW_PEERS_FILE = "lastPeers.properties"; private static final Boolean DEFAULT_VMTEST_LOAD_LOCAL = false; protected final Config configFromFiles; @@ -312,6 +315,10 @@ public String databaseDir() { return databaseDir == null ? configFromFiles.getString(PROPERTY_BASE_PATH) : databaseDir; } + public Path getLastKnewPeersFilePath() { + return Paths.get(databaseDir(), LAST_KNEW_PEERS_FILE); + } + public void setDataBaseDir(String dataBaseDir) { this.databaseDir = dataBaseDir; } diff --git a/rskj-core/src/main/java/org/ethereum/net/server/ChannelManagerImpl.java b/rskj-core/src/main/java/org/ethereum/net/server/ChannelManagerImpl.java index 26f68a61244..9570d46203c 100644 --- a/rskj-core/src/main/java/org/ethereum/net/server/ChannelManagerImpl.java +++ b/rskj-core/src/main/java/org/ethereum/net/server/ChannelManagerImpl.java @@ -20,8 +20,8 @@ package org.ethereum.net.server; import co.rsk.config.RskSystemProperties; -import co.rsk.net.Peer; import co.rsk.net.NodeID; +import co.rsk.net.Peer; import co.rsk.net.Status; import co.rsk.net.messages.*; import co.rsk.scoring.InetAddressUtils; diff --git a/rskj-core/src/main/resources/expected.conf b/rskj-core/src/main/resources/expected.conf index bd1651f9365..551586b82fc 100644 --- a/rskj-core/src/main/resources/expected.conf +++ b/rskj-core/src/main/resources/expected.conf @@ -139,6 +139,7 @@ peer = { allowMultipleConnectionsPerHostPort = maxBootRetries = bucketSize = + usePeersFromLastSession = } port = networkId = diff --git a/rskj-core/src/main/resources/reference.conf b/rskj-core/src/main/resources/reference.conf index 3de9eba1319..ced28172cfe 100644 --- a/rskj-core/src/main/resources/reference.conf +++ b/rskj-core/src/main/resources/reference.conf @@ -145,8 +145,12 @@ peer { discovery = { # allow multiple connections per host by default allowMultipleConnectionsPerHostPort = true - # allows to specify a number of attempts to discover at least one peer. By default, it's -1, which means an infinite number of attempts - maxBootRetries = -1 + + # If true, the node will try to connect to the peers from the last session + usePeersFromLastSession = false + + # allows to specify a number of attempts to discover at least one peer. By default, it's -1, which means an infinite number of attempts + maxBootRetries = -1 } # flag that allows to propagate a received block without executing it and only checking basic validation rules. diff --git a/rskj-core/src/test/java/co/rsk/NodeRunnerImplTest.java b/rskj-core/src/test/java/co/rsk/NodeRunnerImplTest.java index b3e02eab3bd..16b4a0e63ae 100644 --- a/rskj-core/src/test/java/co/rsk/NodeRunnerImplTest.java +++ b/rskj-core/src/test/java/co/rsk/NodeRunnerImplTest.java @@ -153,4 +153,6 @@ void nodeIsAlreadyStopped_WhenStopNode_ThenShouldNotThrowError() throws Exceptio fail(); } } + + } diff --git a/rskj-core/src/test/java/co/rsk/RskContextTest.java b/rskj-core/src/test/java/co/rsk/RskContextTest.java index c54a9bbdb39..c8279150283 100644 --- a/rskj-core/src/test/java/co/rsk/RskContextTest.java +++ b/rskj-core/src/test/java/co/rsk/RskContextTest.java @@ -22,6 +22,7 @@ import co.rsk.config.*; import co.rsk.net.AsyncNodeBlockProcessor; import co.rsk.net.NodeBlockProcessor; +import co.rsk.net.discovery.KnownPeersHandler; import co.rsk.trie.MultiTrieStore; import co.rsk.trie.TrieStore; import co.rsk.trie.TrieStoreImpl; @@ -44,15 +45,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.IntStream; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class RskContextTest { @@ -89,10 +87,10 @@ void shouldResolveCacheSnapshotPath() { Path resolvedPath = rskContext.resolveCacheSnapshotPath(baseStorePath); - Assertions.assertNotNull(resolvedPath); + assertNotNull(resolvedPath); String pathSuffix = resolvedPath.toString().replace(baseStorePath.toString(), ""); - Assertions.assertEquals("/rskcache", pathSuffix); + assertEquals("/rskcache", pathSuffix); } @Test @@ -186,7 +184,7 @@ void buildInternalServicesWithPeerScoringSummaryService() { rskContext.buildInternalServices(); - Assertions.assertNotNull(rskContext.getPeerScoringReporterService()); + assertNotNull(rskContext.getPeerScoringReporterService()); Assertions.assertTrue(rskContext.getPeerScoringReporterService().initialized()); } @@ -253,7 +251,7 @@ void closedContextShouldThrowErrorWhenBeingUsed() throws IllegalAccessException method.invoke(rskContext, new Object[method.getParameterCount()]); Assertions.fail(method.getName() + " should throw an exception when called on closed context"); } catch (InvocationTargetException e) { - Assertions.assertEquals("RSK Context is closed and cannot be in use anymore", e.getTargetException().getMessage()); + assertEquals("RSK Context is closed and cannot be in use anymore", e.getTargetException().getMessage()); } } } @@ -277,6 +275,20 @@ void shouldMakeNewContext() throws Exception { Assertions.assertTrue(rskContext.isClosed()); } + @Test + void initialNodesAreLoadedWithoutDuplications(){ + KnownPeersHandler knownPeersHandler = mock(KnownPeersHandler.class); + List peerDiscoveryIPList = Arrays.asList("1.1.1.1:1234","1.2.3.4:4444"); + doReturn(peerDiscoveryIPList).when(testProperties).peerDiscoveryIPList(); + List peersFromLastSession = Arrays.asList("4.3.2.1:4444","1.1.1.1:1234"); + when(knownPeersHandler.readPeers()).thenReturn(peersFromLastSession); + doReturn(true).when(testProperties).usePeersFromLastSession(); + List initialBootNodes = rskContext.getInitialBootNodes(knownPeersHandler); + assertNotNull(initialBootNodes); + assertEquals(3, initialBootNodes.size(), "Initial nodes should be 3"); + assertEquals(initialBootNodes.stream().distinct().count(), initialBootNodes.size(), "Initial nodes should not have duplicates"); + } + private RskContext makeRskContext() { return new RskContext(new String[0]) { @Override @@ -292,12 +304,12 @@ public Genesis getGenesis() { @Override public synchronized List buildInternalServices() { // instantiate LevelDB instances which should be closed when the context is being closed - Assertions.assertNotNull(getBlockStore()); - Assertions.assertNotNull(getTrieStore()); - Assertions.assertNotNull(getReceiptStore()); - Assertions.assertNotNull(getStateRootsStore()); - Assertions.assertNotNull(getBlockStore()); - Assertions.assertNotNull(getWallet()); + assertNotNull(getBlockStore()); + assertNotNull(getTrieStore()); + assertNotNull(getReceiptStore()); + assertNotNull(getStateRootsStore()); + assertNotNull(getBlockStore()); + assertNotNull(getWallet()); return Collections.singletonList(internalService); } diff --git a/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java b/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java index f38c0334cd4..8b82cded3fe 100644 --- a/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java +++ b/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java @@ -261,4 +261,41 @@ void testGasPriceMultiplierThrowsErrorForNegativeValue() { Assertions.assertThrows(RskConfigurationException.class, testSystemProperties::gasPriceMultiplier); } + + @Test + void checkPeerLastSessionProperty(){ + TestSystemProperties testSystemProperties = new TestSystemProperties(rawConfig -> + ConfigFactory.parseString("{" + + "peer {\n" + + " discovery{ usePeersFromLastSession = true\n}" + + "}" + + " }").withFallback(rawConfig)); + + assertTrue(testSystemProperties.usePeersFromLastSession()); + + testSystemProperties = new TestSystemProperties(rawConfig -> + ConfigFactory.parseString("{" + + "peer {\n" + + " discovery{ usePeersFromLastSession = false\n}" + + "}" + + " }").withFallback(rawConfig)); + assertFalse(testSystemProperties.usePeersFromLastSession()); + + testSystemProperties = new TestSystemProperties(); + + assertFalse(testSystemProperties.usePeersFromLastSession()); + } + + @Test + void checkLastSessionPeersFilePathProperty(){ + TestSystemProperties testSystemProperties = new TestSystemProperties(rawConfig -> + ConfigFactory.parseString("{" + + "database {\n" + + " dir = \"/dbdir\"\n" + + "}" + + " }").withFallback(rawConfig)); + + assertEquals("/dbdir/lastPeers.properties", testSystemProperties.getLastKnewPeersFilePath().toString()); + } + } diff --git a/rskj-core/src/test/java/co/rsk/net/discovery/KnownPeersHandlerTest.java b/rskj-core/src/test/java/co/rsk/net/discovery/KnownPeersHandlerTest.java new file mode 100644 index 00000000000..64bab1e60cb --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/net/discovery/KnownPeersHandlerTest.java @@ -0,0 +1,87 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package co.rsk.net.discovery; + +import co.rsk.util.SimpleFileWriter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class KnownPeersHandlerTest { + + @Mock + private SimpleFileWriter mockFileDataSaver; + + @TempDir + Path tempDir; + + @Test + void testSavePeersSuccessfully() throws IOException { + Map knownPeers = new HashMap<>(); + knownPeers.put("peer1", "192.168.1.1"); + knownPeers.put("peer2", "192.168.1.2"); + ArgumentCaptor propertiesCaptor = ArgumentCaptor.forClass(Properties.class); + + KnownPeersHandler knownPeersHandler = new KnownPeersHandler(tempDir, mockFileDataSaver); + knownPeersHandler.savePeers(knownPeers); + + verify(mockFileDataSaver).savePropertiesIntoFile(propertiesCaptor.capture(), eq(tempDir)); + Properties properties = propertiesCaptor.getValue(); + assertEquals(2,properties.size()); + for (Map.Entry entry : knownPeers.entrySet()) { + assertEquals(entry.getValue(), properties.getProperty(entry.getKey())); + } + } + + @Test + void testReadPeersSuccessfully() throws Exception { + Path filePath = tempDir.resolve("peers.list"); + Properties props = new Properties(); + props.setProperty("peer1", "192.168.1.1"); + props.setProperty("peer2", "192.168.1.2"); + try (FileWriter writer = new FileWriter(filePath.toFile())) { + props.store(writer, null); + } + + KnownPeersHandler knownPeersHandler = new KnownPeersHandler(filePath, mockFileDataSaver); + // Since readPeers() reads from the file system directly, we don't use the mocked SimpleFileWriter here. + List peers = knownPeersHandler.readPeers(); + + assertEquals(2, peers.size()); + assertTrue(peers.contains("192.168.1.1")); + assertTrue(peers.contains("192.168.1.2")); + } + +} diff --git a/rskj-core/src/test/java/co/rsk/net/discovery/PeerExplorerTest.java b/rskj-core/src/test/java/co/rsk/net/discovery/PeerExplorerTest.java index 506fd993f2a..2a49c6d1c91 100644 --- a/rskj-core/src/test/java/co/rsk/net/discovery/PeerExplorerTest.java +++ b/rskj-core/src/test/java/co/rsk/net/discovery/PeerExplorerTest.java @@ -33,6 +33,7 @@ import org.ethereum.util.ByteUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.net.InetAddress; @@ -40,10 +41,8 @@ import java.net.UnknownHostException; import java.util.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; /** * Created by mario on 15/02/17. @@ -264,7 +263,7 @@ void handlePongMessage() throws Exception { channel.clearEvents(); channel.channelRead0(ctx, incomingPongEvent); assertEquals(1, peerExplorer.getNodes().size()); - + assertEquals(1,peerExplorer.getKnownHosts().size()); peerExplorer.dispose(); } @@ -676,7 +675,7 @@ void testUnlimitedRetriesWhenLimitIsNegativeOne() { for (int i = 0; i < 100; i++) { peerExplorer.update(); } - Assertions.assertEquals(peerExplorer.getRetryCounter(), 100); + Assertions.assertEquals(100,peerExplorer.getRetryCounter()); } @Test @@ -717,6 +716,42 @@ void testNoRetriesWhenLimitIsZero() { Assertions.assertEquals(0, peerExplorer.getRetryCounter(), "No retries should have occurred"); } + @Test + void disposeShouldSaveKnownPeers() { + KnownPeersHandler knownPeersHandler = mock(KnownPeersHandler.class); + PeerExplorer peerExplorer = new PeerExplorer(Collections.emptyList(), mock(Node.class), mock(NodeDistanceTable.class), + mock(ECKey.class), 199, UPDATE, CLEAN, NETWORK_ID1, mock(PeerScoringManager.class), + true, 0, knownPeersHandler); + PeerExplorer sPeerExplorer = spy(peerExplorer); + ArgumentCaptor map = ArgumentCaptor.forClass(Map.class); + Map knownPeers = new HashMap<>(); + NodeID nodeID = NodeID.ofHexString("1234"); + knownPeers.put("1.2.2.2:5050", nodeID); + + when(sPeerExplorer.getKnownHosts()).thenReturn(knownPeers); + + sPeerExplorer.dispose(); + + verify(knownPeersHandler, times(1)).savePeers(map.capture()); + + Map savedPeers = (Map) map.getValue(); + assertEquals(1, savedPeers.size()); + + Map.Entry entry = savedPeers.entrySet().iterator().next(); + assertEquals("1.2.2.2:5050",entry.getValue()); + assertEquals(nodeID.toString(),entry.getKey()); + } + + @Test + void disposeWithNowKnownPeersServiceWorks() { + PeerExplorer peerExplorer = new PeerExplorer(Collections.emptyList(), mock(Node.class), mock(NodeDistanceTable.class), mock(ECKey.class), 199, UPDATE, CLEAN, NETWORK_ID1, mock(PeerScoringManager.class), true, 0); + + assertNotEquals(ExecState.FINISHED, peerExplorer.getState()); + peerExplorer.dispose(); + assertEquals(ExecState.FINISHED, peerExplorer.getState()); + } + + private boolean containsNodeId(String nodeId, List nodes) { return nodes.stream().map(Node::getHexId).anyMatch(h -> StringUtils.equals(h, nodeId)); } diff --git a/rskj-core/src/test/java/co/rsk/util/SimpleFileWriterTest.java b/rskj-core/src/test/java/co/rsk/util/SimpleFileWriterTest.java new file mode 100644 index 00000000000..6daafa8e884 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/util/SimpleFileWriterTest.java @@ -0,0 +1,48 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package co.rsk.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SimpleFileWriterTest { + + private SimpleFileWriter simpleFileWriter; + private Path filePath; + + @BeforeEach + public void setup() throws IOException { + simpleFileWriter = SimpleFileWriter.getInstance(); + filePath = Files.createTempFile("test", ".txt"); + } + + @Test + void testSaveDataIntoFile() throws IOException { + String data = "Hello, World!"; + simpleFileWriter.saveDataIntoFile(data, filePath); + + String readData = new String(Files.readAllBytes(filePath)); + assertEquals(data, readData); + } +} \ No newline at end of file