Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Maintain a staticnodes.json #1106

Merged
merged 14 commits into from
Mar 20, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<EnodeURL> 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<EnodeURL> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<EnodeURL> 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<EnodeURL> 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<EnodeURL> 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<EnodeURL> enodes = StaticNodesParser.fromPath(tempFile.toPath());
assertThat(enodes.size()).isZero();
}
}
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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"]
20 changes: 18 additions & 2 deletions pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -95,6 +97,7 @@ public class RunnerBuilder {
private MetricsConfiguration metricsConfiguration;
private MetricsSystem metricsSystem;
private Optional<PermissioningConfiguration> permissioningConfiguration = Optional.empty();
private Set<EnodeURL> staticNodes;

private EnodeURL getSelfEnode() {
String nodeId = pantheonController.getLocalNodeKeyPair().getPublicKey().toString();
Expand Down Expand Up @@ -177,6 +180,11 @@ public RunnerBuilder metricsSystem(final MetricsSystem metricsSystem) {
return this;
}

public RunnerBuilder staticNodes(final Set<EnodeURL> staticNodes) {
this.staticNodes = staticNodes;
return this;
}

public Runner build() {

Preconditions.checkNotNull(pantheonController);
Expand Down Expand Up @@ -291,14 +299,22 @@ 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> jsonRpcHttpService = Optional.empty();
if (jsonRpcConfiguration.isEnabled()) {
final Map<String, JsonRpcMethod> jsonRpcMethods =
jsonRpcMethods(
context,
protocolSchedule,
pantheonController,
networkRunner.getNetwork(),
peerNetwork,
synchronizer,
transactionPool,
miningCoordinator,
Expand All @@ -321,7 +337,7 @@ public Runner build() {
context,
protocolSchedule,
pantheonController,
networkRunner.getNetwork(),
peerNetwork,
synchronizer,
transactionPool,
miningCoordinator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -907,7 +908,8 @@ private void synchronize(
final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration,
final MetricsConfiguration metricsConfiguration,
final Optional<PermissioningConfiguration> permissioningConfiguration) {
final Optional<PermissioningConfiguration> permissioningConfiguration)
throws IOException {

checkNotNull(runnerBuilder);

Expand All @@ -929,6 +931,7 @@ private void synchronize(
.bannedNodeIds(bannedNodeIds)
.metricsSystem(metricsSystem.get())
.metricsConfiguration(metricsConfiguration)
.staticNodes(loadStaticNodes())
.build();

addShutdownHook(runner);
Expand Down Expand Up @@ -1159,4 +1162,11 @@ public MetricsSystem getMetricsSystem() {
public PantheonExceptionHandler exceptionHandler() {
return exceptionHandlerSupplier.get();
}

private Set<EnodeURL> loadStaticNodes() throws IOException {
final String staticNodesFilname = "static-nodes.json";
final Path staticNodesPath = dataDir().resolve(staticNodesFilname);

return StaticNodesParser.fromPath(staticNodesPath);
}
}
4 changes: 3 additions & 1 deletion pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down