From 6ca87e281edda086ecdfe472d0eddc7ea60d308d Mon Sep 17 00:00:00 2001 From: Trent Mohay <37158202+rain-on@users.noreply.github.com> Date: Wed, 20 Mar 2019 11:33:22 +1100 Subject: [PATCH] Maintain a staticnodes.json (#1106) * Maintain a staticnodes.json The staticnodes.json file resides in the pantheon data directory, is parsed at startup to determine which peers can be connected to (aside from bootnodes), and is updated whenever addPeer/removePeer RPC is invoked. This file is not updated when finding neighbour peers (ONLY on RPC call). --- .../ethereum/p2p/peers/DefaultPeer.java | 13 +++ .../ethereum/p2p/peers/StaticNodesParser.java | 77 +++++++++++++ .../p2p/peers/StaticNodesParserTest.java | 108 ++++++++++++++++++ .../p2p/peers/invalid_static_nodes.json | 1 + .../p2p/peers/valid_static_nodes.json | 4 + .../tech/pegasys/pantheon/RunnerBuilder.java | 20 +++- .../pegasys/pantheon/cli/PantheonCommand.java | 12 +- .../tech/pegasys/pantheon/RunnerTest.java | 4 +- .../pantheon/cli/CommandTestAbstract.java | 1 + 9 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParser.java create mode 100644 ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParserTest.java create mode 100644 ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/invalid_static_nodes.json create mode 100644 ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/valid_static_nodes.json diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/DefaultPeer.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/DefaultPeer.java index b1c94d19aa..0ba705b673 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/DefaultPeer.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/DefaultPeer.java @@ -20,6 +20,7 @@ import tech.pegasys.pantheon.ethereum.rlp.RLPInput; import tech.pegasys.pantheon.util.NetworkUtility; import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.enode.EnodeURL; import java.net.URI; import java.util.Objects; @@ -42,6 +43,18 @@ public class DefaultPeer extends DefaultPeerId implements Peer { private final Endpoint endpoint; + public static DefaultPeer fromEnodeURL(final EnodeURL enodeURL) { + final int udpPort = enodeURL.getDiscoveryPort().orElse(enodeURL.getListeningPort()); + + final Endpoint endpoint = + new Endpoint( + enodeURL.getInetAddress().getHostAddress(), + udpPort, + OptionalInt.of(enodeURL.getListeningPort())); + + return new DefaultPeer(BytesValue.fromHexString(enodeURL.getNodeId()), endpoint); + } + /** * Creates a {@link DefaultPeer} instance from a String representation of an enode URL. * diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParser.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParser.java new file mode 100644 index 0000000000..faeca524f1 --- /dev/null +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParser.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.ethereum.p2p.peers; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptySet; + +import tech.pegasys.pantheon.util.enode.EnodeURL; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Collectors; + +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonArray; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class StaticNodesParser { + + private static final Logger LOG = LogManager.getLogger(); + + public static Set fromPath(final Path path) + throws IOException, IllegalArgumentException { + + try { + return readEnodesFromPath(path); + } catch (FileNotFoundException | NoSuchFileException ex) { + LOG.info("No StaticNodes file ({}) exists, creating empty cache.", path); + return emptySet(); + } catch (IOException ex) { + LOG.info("Unable to parse static nodes file ({})", path); + throw ex; + } catch (DecodeException ex) { + LOG.info("Content of ({}} was invalid json, and could not be decoded.", path); + throw ex; + } catch (IllegalArgumentException ex) { + LOG.info("Parsing ({}) has failed due incorrectly formatted enode element.", path); + throw ex; + } + } + + private static Set readEnodesFromPath(final Path path) throws IOException { + final byte[] staticNodesContent = Files.readAllBytes(path); + if (staticNodesContent.length == 0) { + return emptySet(); + } + + final JsonArray enodeJsonArray = new JsonArray(new String(staticNodesContent, UTF_8)); + return enodeJsonArray.stream() + .map(obj -> decodeString((String) obj)) + .collect(Collectors.toSet()); + } + + private static EnodeURL decodeString(final String input) { + try { + return new EnodeURL(input); + } catch (IllegalArgumentException ex) { + LOG.info("Illegally constructed enode supplied ({})", input); + throw ex; + } + } +} diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParserTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParserTest.java new file mode 100644 index 0000000000..5b6cc53523 --- /dev/null +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/peers/StaticNodesParserTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.ethereum.p2p.peers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import tech.pegasys.pantheon.util.enode.EnodeURL; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.Lists; +import io.vertx.core.json.DecodeException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class StaticNodesParserTest { + + // NOTE: The invalid_static_nodes file is identical to the valid, however one node's port is set + // to "A". + + // First peer ion the valid_static_nodes file. + private final List validFileItems = + Lists.newArrayList( + new EnodeURL( + "50203c6bfca6874370e71aecc8958529fd723feb05013dc1abca8fc1fff845c5259faba05852e9dfe5ce172a7d6e7c2a3a5eaa8b541c8af15ea5518bbff5f2fa", + "127.0.0.1", + 30303), + new EnodeURL( + "02beb46bc17227616be44234071dfa18516684e45eed88049190b6cb56b0bae218f045fd0450f123b8f55c60b96b78c45e8e478004293a8de6818aa4e02eff97", + "127.0.0.1", + 30304), + new EnodeURL( + "819e5cbd81f123516b10f04bf620daa2b385efef06d77253148b814bf1bb6197ff58ebd1fd7bf5dc765b49a4440c733bf941e479c800173f2bfeb887e4fbcbc2", + "127.0.0.1", + 30305), + new EnodeURL( + "6cf53e25d2a98a22e7e205a86bda7077e3c8a7bc99e5ff88ddfd2037a550969ab566f069ffa455df0cfae0c21f7aec3447e414eccc473a3e8b20984b90f164ac", + "127.0.0.1", + 30306)); + + @Rule public TemporaryFolder testFolder = new TemporaryFolder(); + + @Test + public void validFileLoadsWithExpectedEnodes() throws IOException { + final URL resource = StaticNodesParserTest.class.getResource("valid_static_nodes.json"); + final Path path = Paths.get(resource.getPath()); + + final Set enodes = StaticNodesParser.fromPath(path); + + assertThat(enodes).containsExactly(validFileItems.toArray(new EnodeURL[validFileItems.size()])); + } + + @Test + public void invalidFileThrowsAnException() { + final URL resource = StaticNodesParserTest.class.getResource("invalid_static_nodes.json"); + final Path path = Paths.get(resource.getPath()); + + assertThatThrownBy(() -> StaticNodesParser.fromPath(path)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void nonJsonFileThrowsAnException() throws IOException { + final File tempFile = testFolder.newFile("file.txt"); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "This Is Not Json".getBytes(Charset.forName("UTF-8"))); + + assertThatThrownBy(() -> StaticNodesParser.fromPath(tempFile.toPath())) + .isInstanceOf(DecodeException.class); + } + + @Test + public void anEmptyCacheIsCreatedIfTheFileDoesNotExist() throws IOException { + final Path path = Paths.get("./arbirtraryFilename.txt"); + + final Set enodes = StaticNodesParser.fromPath(path); + assertThat(enodes.size()).isZero(); + } + + @Test + public void cacheIsCreatedIfFileExistsButIsEmpty() throws IOException { + final File tempFile = testFolder.newFile("file.txt"); + tempFile.deleteOnExit(); + + final Set enodes = StaticNodesParser.fromPath(tempFile.toPath()); + assertThat(enodes.size()).isZero(); + } +} diff --git a/ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/invalid_static_nodes.json b/ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/invalid_static_nodes.json new file mode 100644 index 0000000000..3b55676f90 --- /dev/null +++ b/ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/invalid_static_nodes.json @@ -0,0 +1 @@ +["enode://50203c6bfca6874370e71aecc8958529fd723feb05013dc1abca8fc1fff845c5259faba05852e9dfe5ce172a7d6e7c2a3a5eaa8b541c8af15ea5518bbff5f2fa@127.0.0.1:A","enode://02beb46bc17227616be44234071dfa18516684e45eed88049190b6cb56b0bae218f045fd0450f123b8f55c60b96b78c45e8e478004293a8de6818aa4e02eff97@127.0.0.1:30304","enode://819e5cbd81f123516b10f04bf620daa2b385efef06d77253148b814bf1bb6197ff58ebd1fd7bf5dc765b49a4440c733bf941e479c800173f2bfeb887e4fbcbc2@127.0.0.1:30305","enode://6cf53e25d2a98a22e7e205a86bda7077e3c8a7bc99e5ff88ddfd2037a550969ab566f069ffa455df0cfae0c21f7aec3447e414eccc473a3e8b20984b90f164ac@127.0.0.1:30306"] \ No newline at end of file diff --git a/ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/valid_static_nodes.json b/ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/valid_static_nodes.json new file mode 100644 index 0000000000..1e1764425d --- /dev/null +++ b/ethereum/p2p/src/test/resources/tech/pegasys/pantheon/ethereum/p2p/peers/valid_static_nodes.json @@ -0,0 +1,4 @@ +["enode://50203c6bfca6874370e71aecc8958529fd723feb05013dc1abca8fc1fff845c5259faba05852e9dfe5ce172a7d6e7c2a3a5eaa8b541c8af15ea5518bbff5f2fa@127.0.0.1:30303", + "enode://02beb46bc17227616be44234071dfa18516684e45eed88049190b6cb56b0bae218f045fd0450f123b8f55c60b96b78c45e8e478004293a8de6818aa4e02eff97@127.0.0.1:30304", + "enode://819e5cbd81f123516b10f04bf620daa2b385efef06d77253148b814bf1bb6197ff58ebd1fd7bf5dc765b49a4440c733bf941e479c800173f2bfeb887e4fbcbc2@127.0.0.1:30305", + "enode://6cf53e25d2a98a22e7e205a86bda7077e3c8a7bc99e5ff88ddfd2037a550969ab566f069ffa455df0cfae0c21f7aec3447e414eccc473a3e8b20984b90f164ac@127.0.0.1:30306"] \ No newline at end of file diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 3e7d7eaff7..793cc70943 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -49,6 +49,8 @@ import tech.pegasys.pantheon.ethereum.p2p.config.RlpxConfiguration; import tech.pegasys.pantheon.ethereum.p2p.config.SubProtocolConfiguration; import tech.pegasys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; +import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol; @@ -95,6 +97,7 @@ public class RunnerBuilder { private MetricsConfiguration metricsConfiguration; private MetricsSystem metricsSystem; private Optional permissioningConfiguration = Optional.empty(); + private Set staticNodes; private EnodeURL getSelfEnode() { String nodeId = pantheonController.getLocalNodeKeyPair().getPublicKey().toString(); @@ -177,6 +180,11 @@ public RunnerBuilder metricsSystem(final MetricsSystem metricsSystem) { return this; } + public RunnerBuilder staticNodes(final Set staticNodes) { + this.staticNodes = staticNodes; + return this; + } + public Runner build() { Preconditions.checkNotNull(pantheonController); @@ -291,6 +299,14 @@ public Runner build() { final PrivacyParameters privacyParameters = pantheonController.getPrivacyParameters(); final FilterManager filterManager = createFilterManager(vertx, context, transactionPool); + final P2PNetwork peerNetwork = networkRunner.getNetwork(); + staticNodes.stream() + .forEach( + enodeURL -> { + final Peer peer = DefaultPeer.fromEnodeURL(enodeURL); + peerNetwork.addMaintainConnectionPeer(peer); + }); + Optional jsonRpcHttpService = Optional.empty(); if (jsonRpcConfiguration.isEnabled()) { final Map jsonRpcMethods = @@ -298,7 +314,7 @@ public Runner build() { context, protocolSchedule, pantheonController, - networkRunner.getNetwork(), + peerNetwork, synchronizer, transactionPool, miningCoordinator, @@ -321,7 +337,7 @@ public Runner build() { context, protocolSchedule, pantheonController, - networkRunner.getNetwork(), + peerNetwork, synchronizer, transactionPool, miningCoordinator, diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 8c23194e66..cdd738bb8e 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -48,6 +48,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import tech.pegasys.pantheon.ethereum.p2p.peers.StaticNodesParser; import tech.pegasys.pantheon.ethereum.permissioning.LocalPermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfigurationBuilder; @@ -907,7 +908,8 @@ private void synchronize( final JsonRpcConfiguration jsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, final MetricsConfiguration metricsConfiguration, - final Optional permissioningConfiguration) { + final Optional permissioningConfiguration) + throws IOException { checkNotNull(runnerBuilder); @@ -929,6 +931,7 @@ private void synchronize( .bannedNodeIds(bannedNodeIds) .metricsSystem(metricsSystem.get()) .metricsConfiguration(metricsConfiguration) + .staticNodes(loadStaticNodes()) .build(); addShutdownHook(runner); @@ -1159,4 +1162,11 @@ public MetricsSystem getMetricsSystem() { public PantheonExceptionHandler exceptionHandler() { return exceptionHandlerSupplier.get(); } + + private Set loadStaticNodes() throws IOException { + final String staticNodesFilname = "static-nodes.json"; + final Path staticNodesPath = dataDir().resolve(staticNodesFilname); + + return StaticNodesParser.fromPath(staticNodesPath); + } } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java index 1bbc2bab5c..48230c028c 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon; +import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static tech.pegasys.pantheon.cli.EthNetworkConfig.DEV_NETWORK_ID; import static tech.pegasys.pantheon.cli.NetworkName.DEV; @@ -143,7 +144,8 @@ private void syncFromGenesis(final SyncMode mode) throws Exception { .discoveryPort(0) .maxPeers(3) .metricsSystem(noOpMetricsSystem) - .bannedNodeIds(Collections.emptySet()); + .bannedNodeIds(emptySet()) + .staticNodes(emptySet()); Runner runnerBehind = null; final Runner runnerAhead = diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index 3ab957c85d..c49c596c8c 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -124,6 +124,7 @@ public void initMocks() throws Exception { when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.metricsSystem(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.metricsConfiguration(any())).thenReturn(mockRunnerBuilder); + when(mockRunnerBuilder.staticNodes(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.build()).thenReturn(mockRunner); }