diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfiguration.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfiguration.java new file mode 100644 index 0000000000..8a53e63a5a --- /dev/null +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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.permissioning; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class PermissioningConfiguration { + private List nodeWhitelist; + private boolean nodeWhitelistSet; + + public List getNodeWhitelist() { + return nodeWhitelist; + } + + public static PermissioningConfiguration createDefault() { + final PermissioningConfiguration config = new PermissioningConfiguration(); + config.nodeWhitelist = new ArrayList<>(); + return config; + } + + public void setNodeWhitelist(final Collection nodeWhitelist) { + if (nodeWhitelist != null) { + this.nodeWhitelist.addAll(nodeWhitelist); + this.nodeWhitelistSet = true; + } + } + + public boolean isNodeWhitelistSet() { + return nodeWhitelistSet; + } +} diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfigurationTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfigurationTest.java new file mode 100644 index 0000000000..fa388287fa --- /dev/null +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfigurationTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 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.permissioning; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; + +import org.junit.Test; + +public class PermissioningConfigurationTest { + + @Test + public void defaultConfiguration() { + final PermissioningConfiguration configuration = PermissioningConfiguration.createDefault(); + assertThat(configuration.getNodeWhitelist()).isEmpty(); + assertThat(configuration.isNodeWhitelistSet()).isFalse(); + } + + @Test + public void setNodeWhitelist() { + final String[] nodes = {"enode://001@123:4567", "enode://002@123:4567", "enode://003@123:4567"}; + final PermissioningConfiguration configuration = PermissioningConfiguration.createDefault(); + configuration.setNodeWhitelist(Arrays.asList(nodes)); + assertThat(configuration.getNodeWhitelist()).containsExactlyInAnyOrder(nodes); + assertThat(configuration.isNodeWhitelistSet()).isTrue(); + } + + @Test + public void setNodeWhiteListPassingNull() { + final PermissioningConfiguration configuration = PermissioningConfiguration.createDefault(); + configuration.setNodeWhitelist(null); + assertThat(configuration.getNodeWhitelist()).isEmpty(); + assertThat(configuration.isNodeWhitelistSet()).isFalse(); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java index 662aee5868..fab716ff9a 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon; +import tech.pegasys.pantheon.controller.NodeWhitelistController; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketService; @@ -46,19 +47,23 @@ public class Runner implements AutoCloseable { private final PantheonController pantheonController; private final Path dataDir; + private final NodeWhitelistController nodeWhitelistController; + Runner( final Vertx vertx, final NetworkRunner networkRunner, final Optional jsonRpc, final Optional websocketRpc, final PantheonController pantheonController, - final Path dataDir) { + final Path dataDir, + final NodeWhitelistController nodeWhitelistController) { this.vertx = vertx; this.networkRunner = networkRunner; this.jsonRpc = jsonRpc; this.websocketRpc = websocketRpc; this.pantheonController = pantheonController; this.dataDir = dataDir; + this.nodeWhitelistController = nodeWhitelistController; } public void execute() { diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 6707aa65f0..a3f8cccbf7 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon; +import tech.pegasys.pantheon.controller.NodeWhitelistController; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.ProtocolContext; @@ -50,6 +51,7 @@ import tech.pegasys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.peers.PeerBlacklist; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -78,6 +80,7 @@ public class RunnerBuilder { private WebSocketConfiguration webSocketConfiguration; private Path dataDir; private Collection bannedNodeIds; + private PermissioningConfiguration permissioningConfiguration; public RunnerBuilder vertx(final Vertx vertx) { this.vertx = vertx; @@ -124,6 +127,12 @@ public class RunnerBuilder { return this; } + public RunnerBuilder permissioningConfiguration( + final PermissioningConfiguration permissioningConfiguration) { + this.permissioningConfiguration = permissioningConfiguration; + return this; + } + public RunnerBuilder dataDir(final Path dataDir) { this.dataDir = dataDir; return this; @@ -259,8 +268,17 @@ public class RunnerBuilder { vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods)); } + NodeWhitelistController nodeWhitelistController = + new NodeWhitelistController(permissioningConfiguration); + return new Runner( - vertx, networkRunner, jsonRpcHttpService, webSocketService, pantheonController, dataDir); + vertx, + networkRunner, + jsonRpcHttpService, + webSocketService, + pantheonController, + dataDir, + nodeWhitelistController); } private FilterManager createFilterManager( 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 2659c3cdf4..ae240ec217 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -33,6 +33,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.DefaultPeer; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.util.InvalidConfigurationException; import tech.pegasys.pantheon.util.BlockImporter; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -377,6 +378,17 @@ public class PantheonCommand implements Runnable { ) private final BytesValue extraData = DEFAULT_EXTRA_DATA; + // Permissioning: A list of whitelist nodes can be passed. + @Option( + names = {"--nodes-whitelist"}, + paramLabel = "", + description = + "Comma separated enode URLs for permissioned networks. You may specify an empty list.", + split = ",", + arity = "0..*" + ) + private final Collection nodesWhitelist = null; + public PantheonCommand( final BlockImporter blockImporter, final RunnerBuilder runnerBuilder, @@ -442,7 +454,8 @@ public class PantheonCommand implements Runnable { maxPeers, p2pHostAndPort, jsonRpcConfiguration(), - webSocketConfiguration()); + webSocketConfiguration(), + permissioningConfiguration()); } PantheonController buildController() { @@ -487,6 +500,13 @@ public class PantheonCommand implements Runnable { return webSocketConfiguration; } + private PermissioningConfiguration permissioningConfiguration() { + final PermissioningConfiguration permissioningConfiguration = + PermissioningConfiguration.createDefault(); + permissioningConfiguration.setNodeWhitelist(nodesWhitelist); + return permissioningConfiguration; + } + private SynchronizerConfiguration buildSyncConfig(final SyncMode syncMode) { checkNotNull(syncMode); synchronizerConfigurationBuilder.syncMode(syncMode); @@ -502,11 +522,12 @@ public class PantheonCommand implements Runnable { final int maxPeers, final HostAndPort discoveryHostAndPort, final JsonRpcConfiguration jsonRpcConfiguration, - final WebSocketConfiguration webSocketConfiguration) { + final WebSocketConfiguration webSocketConfiguration, + final PermissioningConfiguration permissioningConfiguration) { checkNotNull(runnerBuilder); - final Runner runner = + Runner runner = runnerBuilder .vertx(Vertx.vertx()) .pantheonController(controller) @@ -520,6 +541,7 @@ public class PantheonCommand implements Runnable { .webSocketConfiguration(webSocketConfiguration) .dataDir(dataDir) .bannedNodeIds(bannedNodeIds) + .permissioningConfiguration(permissioningConfiguration) .build(); addShutdownHook(runner); diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/controller/NodeWhitelistController.java b/pantheon/src/main/java/tech/pegasys/pantheon/controller/NodeWhitelistController.java new file mode 100644 index 0000000000..6ffb666c8d --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/controller/NodeWhitelistController.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 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.controller; + +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class NodeWhitelistController { + + private static final Logger LOG = LogManager.getLogger(); + + private static List nodeWhitelist; + private static boolean nodeWhitelistSet = false; + + public NodeWhitelistController(final PermissioningConfiguration configuration) { + nodeWhitelist = new ArrayList<>(); + if (configuration != null && configuration.getNodeWhitelist() != null) { + nodeWhitelist.addAll(configuration.getNodeWhitelist()); + nodeWhitelistSet = true; + } + } + + public boolean addNode(final String nodeId) { + return nodeWhitelist.add(nodeId); + } + + public boolean removeNode(final String nodeId) { + return nodeWhitelist.remove(nodeId); + } + + public static boolean isNodeWhitelistSet() { + return nodeWhitelistSet; + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java index dd7785df58..166f0e24f6 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java @@ -34,6 +34,7 @@ import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSpec; import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.storage.StorageProvider; import tech.pegasys.pantheon.ethereum.storage.keyvalue.RocksDbStorageProvider; import tech.pegasys.pantheon.util.uint.UInt256; @@ -118,6 +119,7 @@ public final class RunnerTest { final ExecutorService executorService = Executors.newFixedThreadPool(2); final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration(); final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration(); + final PermissioningConfiguration aheadPermissioningConfiguration = permissioningConfiguration(); final RunnerBuilder runnerBuilder = new RunnerBuilder() .vertx(Vertx.vertx()) @@ -134,6 +136,7 @@ public final class RunnerTest { .jsonRpcConfiguration(aheadJsonRpcConfiguration) .webSocketConfiguration(aheadWebSocketConfiguration) .dataDir(dbAhead) + .permissioningConfiguration(aheadPermissioningConfiguration) .build(); try { @@ -251,6 +254,11 @@ public final class RunnerTest { return configuration; } + private PermissioningConfiguration permissioningConfiguration() { + final PermissioningConfiguration configuration = PermissioningConfiguration.createDefault(); + return configuration; + } + private static void setupState( final int count, final ProtocolSchedule protocolSchedule, 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 46289b7947..b8a668eafd 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -22,6 +22,7 @@ import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.util.BlockImporter; import java.io.ByteArrayOutputStream; @@ -71,6 +72,7 @@ public abstract class CommandTestAbstract { @Captor ArgumentCaptor intArgumentCaptor; @Captor ArgumentCaptor jsonRpcConfigArgumentCaptor; @Captor ArgumentCaptor wsRpcConfigArgumentCaptor; + @Captor ArgumentCaptor permissioningConfigurationArgumentCaptor; @Before public void initMocks() throws Exception { diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index 0fda207ae5..650b4de919 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -93,6 +93,7 @@ public class PantheonCommandTest extends CommandTestAbstract { when(mockRunnerBuilder.maxPeers(anyInt())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.jsonRpcConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.webSocketConfiguration(any())).thenReturn(mockRunnerBuilder); + when(mockRunnerBuilder.permissioningConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.build()).thenReturn(mockRunner); @@ -420,7 +421,39 @@ public class PantheonCommandTest extends CommandTestAbstract { } @Test - public void banNodeIdsOptionMustBeUsed() { + public void callingWithNodesWhitelistOptionButNoValueMustNotError() { + parseCommand("--nodes-whitelist"); + + verify(mockRunnerBuilder) + .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(permissioningConfigurationArgumentCaptor.getValue().getNodeWhitelist()).isEmpty(); + assertThat(permissioningConfigurationArgumentCaptor.getValue().isNodeWhitelistSet()).isTrue(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void nodesWhitelistOptionMustBeUsed() { + final String[] nodes = {"enode://001@123:4567", "enode://002@123:4567", "enode://003@123:4567"}; + parseCommand("--nodes-whitelist", String.join(",", nodes)); + + verify(mockRunnerBuilder) + .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(permissioningConfigurationArgumentCaptor.getValue().getNodeWhitelist()) + .containsExactlyInAnyOrder(nodes); + assertThat(permissioningConfigurationArgumentCaptor.getValue().isNodeWhitelistSet()).isTrue(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void bannedNodeIdsOptionMustBeUsed() { final String[] nodes = {"0001", "0002", "0003"}; parseCommand("--banned-nodeids", String.join(",", nodes));