[NC-2118] Method to reload permissions file (#834)

* Extracting EnodeURL logic to specific object

* Moving permissioning config builder to permissioning package

* Validating accounts in permissions file

* Implemented controller reload method

* Reload whitelist from file API method

* Spotless

* Refactoring account validation

* Errorprone

* Fixing tests after rebase

* Spotless

* PR review

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Lucas Saldanha 6 years ago committed by GitHub
parent 50c5baaf5a
commit 6551183a8d
  1. 15
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java
  2. 57
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermReloadPermissionsFromFile.java
  3. 4
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java
  4. 94
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermReloadPermissionsFromFileTest.java
  5. 2
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/NoopP2PNetwork.java
  6. 35
      ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistController.java
  7. 56
      ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistControllerTest.java
  8. 2
      ethereum/permissioning/build.gradle
  9. 38
      ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistController.java
  10. 16
      ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfigurationBuilder.java
  11. 2
      ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/TomlConfigFileParser.java
  12. 46
      ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistControllerTest.java
  13. 118
      ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/PermissioningConfigurationBuilderTest.java
  14. 4
      ethereum/permissioning/src/test/resources/permissioning_config.toml
  15. 0
      ethereum/permissioning/src/test/resources/permissioning_config_absent_whitelists.toml
  16. 0
      ethereum/permissioning/src/test/resources/permissioning_config_account_whitelist_only.toml
  17. 0
      ethereum/permissioning/src/test/resources/permissioning_config_empty_whitelists.toml
  18. 3
      ethereum/permissioning/src/test/resources/permissioning_config_invalid_account.toml
  19. 0
      ethereum/permissioning/src/test/resources/permissioning_config_invalid_enode.toml
  20. 0
      ethereum/permissioning/src/test/resources/permissioning_config_node_whitelist_only.toml
  21. 0
      ethereum/permissioning/src/test/resources/permissioning_config_node_whitelist_only_multiline.toml
  22. 0
      ethereum/permissioning/src/test/resources/permissioning_config_unrecognized_key.toml
  23. 2
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  24. 98
      pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/EnodeToURIPropertyConverter.java
  25. 206
      pantheon/src/test/java/tech/pegasys/pantheon/cli/custom/EnodeToURIPropertyConverterTest.java
  26. 2
      pantheon/src/test/java/tech/pegasys/pantheon/util/PermissioningConfigurationValidatorTest.java
  27. 165
      util/src/main/java/tech/pegasys/pantheon/util/enode/EnodeURL.java
  28. 230
      util/src/test/java/tech/pegasys/pantheon/util/enode/EnodeURLTest.java

@ -72,6 +72,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.Per
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermAddNodesToWhitelist; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermAddNodesToWhitelist;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermGetAccountsWhitelist; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermGetAccountsWhitelist;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermGetNodesWhitelist; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermGetNodesWhitelist;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermReloadPermissionsFromFile;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermRemoveAccountsFromWhitelist; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermRemoveAccountsFromWhitelist;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermRemoveNodesFromWhitelist; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermRemoveNodesFromWhitelist;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.privacy.EeaSendRawTransaction; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.privacy.EeaSendRawTransaction;
@ -240,19 +241,17 @@ public class JsonRpcMethodsFactory {
enabledMethods, enabledMethods,
new PermAddNodesToWhitelist(p2pNetwork, parameter), new PermAddNodesToWhitelist(p2pNetwork, parameter),
new PermRemoveNodesFromWhitelist(p2pNetwork, parameter), new PermRemoveNodesFromWhitelist(p2pNetwork, parameter),
new PermGetNodesWhitelist(p2pNetwork)); new PermGetNodesWhitelist(p2pNetwork),
new PermGetAccountsWhitelist(accountsWhitelistController),
new PermAddAccountsToWhitelist(accountsWhitelistController, parameter),
new PermRemoveAccountsFromWhitelist(accountsWhitelistController, parameter),
new PermReloadPermissionsFromFile(
accountsWhitelistController, p2pNetwork.getNodeWhitelistController()));
} }
if (rpcApis.contains(RpcApis.ADMIN)) { if (rpcApis.contains(RpcApis.ADMIN)) {
addMethods(enabledMethods, new AdminPeers(p2pNetwork)); addMethods(enabledMethods, new AdminPeers(p2pNetwork));
addMethods(enabledMethods, new AdminAddPeer(p2pNetwork, parameter)); addMethods(enabledMethods, new AdminAddPeer(p2pNetwork, parameter));
} }
if (rpcApis.contains(RpcApis.PERM)) {
addMethods(
enabledMethods,
new PermGetAccountsWhitelist(accountsWhitelistController),
new PermAddAccountsToWhitelist(accountsWhitelistController, parameter),
new PermRemoveAccountsFromWhitelist(accountsWhitelistController, parameter));
}
if (rpcApis.contains(RpcApis.EEA)) { if (rpcApis.contains(RpcApis.EEA)) {
addMethods( addMethods(
enabledMethods, enabledMethods,

@ -0,0 +1,57 @@
/*
* 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.jsonrpc.internal.methods.permissioning;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController;
import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController;
import java.util.Optional;
public class PermReloadPermissionsFromFile implements JsonRpcMethod {
private final Optional<AccountWhitelistController> accountWhitelistController;
private final Optional<NodeWhitelistController> nodesWhitelistController;
public PermReloadPermissionsFromFile(
final Optional<AccountWhitelistController> accountWhitelistController,
final Optional<NodeWhitelistController> nodesWhitelistController) {
this.accountWhitelistController = accountWhitelistController;
this.nodesWhitelistController = nodesWhitelistController;
}
@Override
public String getName() {
return "perm_reloadPermissionsFromFile";
}
@Override
public JsonRpcResponse response(final JsonRpcRequest request) {
if (!accountWhitelistController.isPresent() && !nodesWhitelistController.isPresent()) {
return new JsonRpcErrorResponse(request.getId(), JsonRpcError.PERMISSIONING_NOT_ENABLED);
}
try {
accountWhitelistController.ifPresent(AccountWhitelistController::reload);
nodesWhitelistController.ifPresent(NodeWhitelistController::reload);
return new JsonRpcSuccessResponse(request.getId());
} catch (Exception e) {
return new JsonRpcErrorResponse(request.getId(), JsonRpcError.WHITELIST_RELOAD_ERROR);
}
}
}

@ -71,6 +71,10 @@ public enum JsonRpcError {
WHITELIST_FILE_SYNC( WHITELIST_FILE_SYNC(
-32000, -32000,
"The permissioning whitelist configuration file is out of sync. The changes have been applied, but not persisted to disk"), "The permissioning whitelist configuration file is out of sync. The changes have been applied, but not persisted to disk"),
WHITELIST_RELOAD_ERROR(
-32000,
"Error reloading permissions file. Please use perm_getAccountsWhitelist and perm_getNodesWhitelist to review the current state of the whitelists."),
PERMISSIONING_NOT_ENABLED(-32000, "Node/Account whitelisting has not been enabled"),
// Private transaction errors // Private transaction errors
ENCLAVE_IS_DOWN(-32000, "Enclave is down"); ENCLAVE_IS_DOWN(-32000, "Enclave is down");

@ -0,0 +1,94 @@
/*
* 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.jsonrpc.internal.methods.permissioning;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse;
import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController;
import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class PermReloadPermissionsFromFileTest {
@Mock private AccountWhitelistController accountWhitelistController;
@Mock private NodeWhitelistController nodeWhitelistController;
private PermReloadPermissionsFromFile method;
@Before
public void before() {
method =
new PermReloadPermissionsFromFile(
Optional.of(accountWhitelistController), Optional.of(nodeWhitelistController));
}
@Test
public void getNameShouldReturnExpectedName() {
assertThat(method.getName()).isEqualTo("perm_reloadPermissionsFromFile");
}
@Test
public void whenBothControllersAreNotPresentMethodShouldReturnPermissioningDisabled() {
JsonRpcResponse expectedErrorResponse =
new JsonRpcErrorResponse(null, JsonRpcError.PERMISSIONING_NOT_ENABLED);
method = new PermReloadPermissionsFromFile(Optional.empty(), Optional.empty());
JsonRpcResponse response = method.response(reloadRequest());
assertThat(response).isEqualToComparingFieldByField(expectedErrorResponse);
}
@Test
public void whenControllersReloadSucceedsMethodShouldReturnSuccess() {
JsonRpcResponse response = method.response(reloadRequest());
verify(accountWhitelistController).reload();
verify(nodeWhitelistController).reload();
assertThat(response).isEqualToComparingFieldByField(successResponse());
}
@Test
public void whenControllerReloadFailsMethodShouldReturnError() {
doThrow(new RuntimeException()).when(accountWhitelistController).reload();
JsonRpcResponse expectedErrorResponse =
new JsonRpcErrorResponse(null, JsonRpcError.WHITELIST_RELOAD_ERROR);
JsonRpcResponse response = method.response(reloadRequest());
assertThat(response).isEqualToComparingFieldByField(expectedErrorResponse);
}
private JsonRpcSuccessResponse successResponse() {
return new JsonRpcSuccessResponse(null);
}
private JsonRpcRequest reloadRequest() {
return new JsonRpcRequest("2.0", "perm_reloadPermissionsFromFile", null);
}
}

@ -85,7 +85,7 @@ public class NoopP2PNetwork implements P2PNetwork {
@Override @Override
public Optional<NodeWhitelistController> getNodeWhitelistController() { public Optional<NodeWhitelistController> getNodeWhitelistController() {
throw new P2pDisabledException("P2P networking disabled. Node whitelist unavailable."); return Optional.empty();
} }
@Override @Override

@ -15,6 +15,7 @@ package tech.pegasys.pantheon.ethereum.p2p.permissioning;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; import tech.pegasys.pantheon.ethereum.p2p.peers.Peer;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfigurationBuilder;
import tech.pegasys.pantheon.ethereum.permissioning.WhitelistFileSyncException; import tech.pegasys.pantheon.ethereum.permissioning.WhitelistFileSyncException;
import tech.pegasys.pantheon.ethereum.permissioning.WhitelistOperationResult; import tech.pegasys.pantheon.ethereum.permissioning.WhitelistOperationResult;
import tech.pegasys.pantheon.ethereum.permissioning.WhitelistPersistor; import tech.pegasys.pantheon.ethereum.permissioning.WhitelistPersistor;
@ -29,9 +30,14 @@ import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class NodeWhitelistController { public class NodeWhitelistController {
private static final Logger LOG = LogManager.getLogger();
private PermissioningConfiguration configuration;
private List<Peer> nodesWhitelist = new ArrayList<>(); private List<Peer> nodesWhitelist = new ArrayList<>();
private final WhitelistPersistor whitelistPersistor; private final WhitelistPersistor whitelistPersistor;
@ -43,7 +49,12 @@ public class NodeWhitelistController {
public NodeWhitelistController( public NodeWhitelistController(
final PermissioningConfiguration configuration, final WhitelistPersistor whitelistPersistor) { final PermissioningConfiguration configuration, final WhitelistPersistor whitelistPersistor) {
this.configuration = configuration;
this.whitelistPersistor = whitelistPersistor; this.whitelistPersistor = whitelistPersistor;
readNodesFromConfig(configuration);
}
private void readNodesFromConfig(final PermissioningConfiguration configuration) {
if (configuration.isNodeWhitelistEnabled() && configuration.getNodeWhitelist() != null) { if (configuration.isNodeWhitelistEnabled() && configuration.getNodeWhitelist() != null) {
for (URI uri : configuration.getNodeWhitelist()) { for (URI uri : configuration.getNodeWhitelist()) {
nodesWhitelist.add(DefaultPeer.fromURI(uri)); nodesWhitelist.add(DefaultPeer.fromURI(uri));
@ -180,6 +191,30 @@ public class NodeWhitelistController {
return nodesWhitelist; return nodesWhitelist;
} }
public synchronized void reload() throws RuntimeException {
final List<Peer> currentAccountsList = new ArrayList<>(nodesWhitelist);
nodesWhitelist.clear();
try {
final PermissioningConfiguration updatedConfig =
PermissioningConfigurationBuilder.permissioningConfigurationFromToml(
configuration.getConfigurationFilePath(),
configuration.isNodeWhitelistEnabled(),
configuration.isAccountWhitelistEnabled());
readNodesFromConfig(updatedConfig);
configuration = updatedConfig;
} catch (Exception e) {
LOG.warn(
"Error reloading permissions file. In-memory whitelisted nodes will be reverted to previous valid configuration. "
+ "Details: {}",
e.getMessage());
nodesWhitelist.clear();
nodesWhitelist.addAll(currentAccountsList);
throw new RuntimeException(e);
}
}
public static class NodesWhitelistResult { public static class NodesWhitelistResult {
private final WhitelistOperationResult result; private final WhitelistOperationResult result;
private final Optional<String> message; private final Optional<String> message;

@ -14,11 +14,14 @@ package tech.pegasys.pantheon.ethereum.p2p.permissioning;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController.NodesWhitelistResult; import static tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController.NodesWhitelistResult;
import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer;
@ -30,6 +33,10 @@ import tech.pegasys.pantheon.ethereum.permissioning.WhitelistPersistor;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -265,4 +272,53 @@ public class NodeWhitelistControllerTest {
verify(whitelistPersistor, times(2)).updateConfig(any(), any()); verify(whitelistPersistor, times(2)).updateConfig(any(), any());
verifyNoMoreInteractions(whitelistPersistor); verifyNoMoreInteractions(whitelistPersistor);
} }
@Test
public void reloadNodeWhitelistWithValidConfigFileShouldUpdateWhitelist() throws Exception {
final String expectedEnodeURL =
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567";
final Path permissionsFile = createPermissionsFileWithNode(expectedEnodeURL);
final PermissioningConfiguration permissioningConfig = mock(PermissioningConfiguration.class);
when(permissioningConfig.getConfigurationFilePath())
.thenReturn(permissionsFile.toAbsolutePath().toString());
when(permissioningConfig.isNodeWhitelistEnabled()).thenReturn(true);
when(permissioningConfig.getNodeWhitelist())
.thenReturn(Arrays.asList(URI.create(expectedEnodeURL)));
controller = new NodeWhitelistController(permissioningConfig);
controller.reload();
assertThat(controller.getNodesWhitelist())
.containsExactly(DefaultPeer.fromURI(expectedEnodeURL));
}
@Test
public void reloadNodeWhitelistWithErrorReadingConfigFileShouldKeepOldWhitelist() {
final String expectedEnodeURI =
"enode://aaaa80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567";
final PermissioningConfiguration permissioningConfig = mock(PermissioningConfiguration.class);
when(permissioningConfig.getConfigurationFilePath()).thenReturn("foo");
when(permissioningConfig.isNodeWhitelistEnabled()).thenReturn(true);
when(permissioningConfig.getNodeWhitelist())
.thenReturn(Arrays.asList(URI.create(expectedEnodeURI)));
controller = new NodeWhitelistController(permissioningConfig);
final Throwable thrown = catchThrowable(() -> controller.reload());
assertThat(thrown)
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Unable to read permissions TOML config file");
assertThat(controller.getNodesWhitelist())
.containsExactly(DefaultPeer.fromURI(expectedEnodeURI));
}
private Path createPermissionsFileWithNode(final String node) throws IOException {
final String nodePermissionsFileContent = "nodes-whitelist=[\"" + node + "\"]";
final Path permissionsFile = Files.createTempFile("node_permissions", "");
Files.write(permissionsFile, nodePermissionsFileContent.getBytes(StandardCharsets.UTF_8));
return permissionsFile;
}
} }

@ -27,8 +27,10 @@ jar {
dependencies { dependencies {
implementation project(':util') implementation project(':util')
implementation 'com.google.guava:guava' implementation 'com.google.guava:guava'
implementation 'net.consensys.cava:cava-toml' implementation 'net.consensys.cava:cava-toml'
implementation 'org.apache.logging.log4j:log4j-api'
testImplementation 'junit:junit' testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core' testImplementation 'org.assertj:assertj-core'

@ -20,9 +20,15 @@ import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class AccountWhitelistController { public class AccountWhitelistController {
private static final Logger LOG = LogManager.getLogger();
private static final int ACCOUNT_BYTES_SIZE = 20; private static final int ACCOUNT_BYTES_SIZE = 20;
private PermissioningConfiguration configuration;
private List<String> accountWhitelist = new ArrayList<>(); private List<String> accountWhitelist = new ArrayList<>();
private final WhitelistPersistor whitelistPersistor; private final WhitelistPersistor whitelistPersistor;
@ -32,7 +38,12 @@ public class AccountWhitelistController {
public AccountWhitelistController( public AccountWhitelistController(
final PermissioningConfiguration configuration, final WhitelistPersistor whitelistPersistor) { final PermissioningConfiguration configuration, final WhitelistPersistor whitelistPersistor) {
this.configuration = configuration;
this.whitelistPersistor = whitelistPersistor; this.whitelistPersistor = whitelistPersistor;
readAccountsFromConfig(configuration);
}
private void readAccountsFromConfig(final PermissioningConfiguration configuration) {
if (configuration != null && configuration.isAccountWhitelistEnabled()) { if (configuration != null && configuration.isAccountWhitelistEnabled()) {
if (!configuration.getAccountWhitelist().isEmpty()) { if (!configuration.getAccountWhitelist().isEmpty()) {
addAccounts(configuration.getAccountWhitelist()); addAccounts(configuration.getAccountWhitelist());
@ -135,10 +146,10 @@ public class AccountWhitelistController {
} }
private boolean containsInvalidAccount(final List<String> accounts) { private boolean containsInvalidAccount(final List<String> accounts) {
return !accounts.stream().allMatch(this::isValidAccountString); return !accounts.stream().allMatch(AccountWhitelistController::isValidAccountString);
} }
private boolean isValidAccountString(final String account) { static boolean isValidAccountString(final String account) {
try { try {
BytesValue bytesValue = BytesValue.fromHexString(account); BytesValue bytesValue = BytesValue.fromHexString(account);
return bytesValue.size() == ACCOUNT_BYTES_SIZE; return bytesValue.size() == ACCOUNT_BYTES_SIZE;
@ -146,4 +157,27 @@ public class AccountWhitelistController {
return false; return false;
} }
} }
public synchronized void reload() throws RuntimeException {
final ArrayList<String> currentAccountsList = new ArrayList<>(accountWhitelist);
accountWhitelist.clear();
try {
final PermissioningConfiguration updatedConfig =
PermissioningConfigurationBuilder.permissioningConfigurationFromToml(
configuration.getConfigurationFilePath(),
configuration.isNodeWhitelistEnabled(),
configuration.isAccountWhitelistEnabled());
readAccountsFromConfig(updatedConfig);
configuration = updatedConfig;
} catch (Exception e) {
LOG.warn(
"Error reloading permissions file. In-memory whitelisted accounts will be reverted to previous valid configuration. "
+ "Details: {}",
e.getMessage());
accountWhitelist.clear();
accountWhitelist.addAll(currentAccountsList);
throw new RuntimeException(e);
}
}
} }

@ -10,10 +10,9 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 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. * specific language governing permissions and limitations under the License.
*/ */
package tech.pegasys.pantheon; package tech.pegasys.pantheon.ethereum.permissioning;
import tech.pegasys.pantheon.cli.custom.EnodeToURIPropertyConverter; import tech.pegasys.pantheon.util.enode.EnodeURL;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
@ -67,6 +66,15 @@ public class PermissioningConfigurationBuilder {
.parallelStream() .parallelStream()
.map(Object::toString) .map(Object::toString)
.collect(Collectors.toList()); .collect(Collectors.toList());
accountsWhitelistToml.stream()
.filter(s -> !AccountWhitelistController.isValidAccountString(s))
.findFirst()
.ifPresent(
s -> {
throw new IllegalArgumentException("Invalid account " + s);
});
permissioningConfiguration.setAccountWhitelist(accountsWhitelistToml); permissioningConfiguration.setAccountWhitelist(accountsWhitelistToml);
} else { } else {
throw new Exception( throw new Exception(
@ -81,7 +89,7 @@ public class PermissioningConfigurationBuilder {
.toList() .toList()
.parallelStream() .parallelStream()
.map(Object::toString) .map(Object::toString)
.map(EnodeToURIPropertyConverter::convertToURI) .map(EnodeURL::asURI)
.collect(Collectors.toList()); .collect(Collectors.toList());
permissioningConfiguration.setNodeWhitelist(nodesWhitelistToml); permissioningConfiguration.setNodeWhitelist(nodesWhitelistToml);
} else { } else {

@ -10,7 +10,7 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 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. * specific language governing permissions and limitations under the License.
*/ */
package tech.pegasys.pantheon; package tech.pegasys.pantheon.ethereum.permissioning;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;

@ -14,6 +14,7 @@ package tech.pegasys.pantheon.ethereum.permissioning;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -22,6 +23,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -178,4 +182,46 @@ public class AccountWhitelistControllerTest {
verify(whitelistPersistor, times(2)).updateConfig(any(), any()); verify(whitelistPersistor, times(2)).updateConfig(any(), any());
verifyNoMoreInteractions(whitelistPersistor); verifyNoMoreInteractions(whitelistPersistor);
} }
@Test
public void reloadAccountWhitelistWithValidConfigFileShouldUpdateWhitelist() throws Exception {
final String expectedAccount = "0x627306090abab3a6e1400e9345bc60c78a8bef57";
final Path permissionsFile = createPermissionsFileWithAccount(expectedAccount);
when(permissioningConfig.getConfigurationFilePath())
.thenReturn(permissionsFile.toAbsolutePath().toString());
when(permissioningConfig.isAccountWhitelistEnabled()).thenReturn(true);
when(permissioningConfig.getAccountWhitelist())
.thenReturn(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"));
controller = new AccountWhitelistController(permissioningConfig, whitelistPersistor);
controller.reload();
assertThat(controller.getAccountWhitelist()).containsExactly(expectedAccount);
}
@Test
public void reloadAccountWhitelistWithErrorReadingConfigFileShouldKeepOldWhitelist() {
when(permissioningConfig.getConfigurationFilePath()).thenReturn("foo");
when(permissioningConfig.isAccountWhitelistEnabled()).thenReturn(true);
when(permissioningConfig.getAccountWhitelist())
.thenReturn(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"));
controller = new AccountWhitelistController(permissioningConfig, whitelistPersistor);
final Throwable thrown = catchThrowable(() -> controller.reload());
assertThat(thrown)
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Unable to read permissions TOML config file");
assertThat(controller.getAccountWhitelist())
.containsExactly("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73");
}
private Path createPermissionsFileWithAccount(final String account) throws IOException {
final String nodePermissionsFileContent = "accounts-whitelist=[\"" + account + "\"]";
final Path permissionsFile = Files.createTempFile("account_permissions", "");
Files.write(permissionsFile, nodePermissionsFileContent.getBytes(StandardCharsets.UTF_8));
return permissionsFile;
}
} }

@ -10,37 +10,38 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 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. * specific language governing permissions and limitations under the License.
*/ */
package tech.pegasys.pantheon; package tech.pegasys.pantheon.ethereum.permissioning;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.fail; import static org.assertj.core.api.Assertions.catchThrowable;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import org.junit.Test; import org.junit.Test;
public class PermissioningConfigurationBuilderTest { public class PermissioningConfigurationBuilderTest {
static final String PERMISSIONING_CONFIG_VALID = "permissioning_config.toml"; private static final String PERMISSIONING_CONFIG_VALID = "permissioning_config.toml";
static final String PERMISSIONING_CONFIG_ACCOUNT_WHITELIST_ONLY = private static final String PERMISSIONING_CONFIG_ACCOUNT_WHITELIST_ONLY =
"permissioning_config_account_whitelist_only.toml"; "permissioning_config_account_whitelist_only.toml";
static final String PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY = private static final String PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY =
"permissioning_config_node_whitelist_only.toml"; "permissioning_config_node_whitelist_only.toml";
static final String PERMISSIONING_CONFIG_INVALID_ENODE = private static final String PERMISSIONING_CONFIG_INVALID_ENODE =
"permissioning_config_invalid_enode.toml"; "permissioning_config_invalid_enode.toml";
static final String PERMISSIONING_CONFIG_EMPTY_WHITELISTS = private static final String PERMISSIONING_CONFIG_INVALID_ACCOUNT =
"permissioning_config_invalid_account.toml";
private static final String PERMISSIONING_CONFIG_EMPTY_WHITELISTS =
"permissioning_config_empty_whitelists.toml"; "permissioning_config_empty_whitelists.toml";
static final String PERMISSIONING_CONFIG_ABSENT_WHITELISTS = private static final String PERMISSIONING_CONFIG_ABSENT_WHITELISTS =
"permissioning_config_absent_whitelists.toml"; "permissioning_config_absent_whitelists.toml";
static final String PERMISSIONING_CONFIG_UNRECOGNIZED_KEY = private static final String PERMISSIONING_CONFIG_UNRECOGNIZED_KEY =
"permissioning_config_unrecognized_key.toml"; "permissioning_config_unrecognized_key.toml";
static final String PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY_MULTILINE = private static final String PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY_MULTILINE =
"permissioning_config_node_whitelist_only_multiline.toml"; "permissioning_config_node_whitelist_only_multiline.toml";
private final String VALID_NODE_ID = private final String VALID_NODE_ID =
@ -48,7 +49,6 @@ public class PermissioningConfigurationBuilderTest {
@Test @Test
public void permissioningConfig() throws Exception { public void permissioningConfig() throws Exception {
final String uri = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567"; final String uri = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567";
final String uri2 = "enode://" + VALID_NODE_ID + "@192.169.0.9:4568"; final String uri2 = "enode://" + VALID_NODE_ID + "@192.169.0.9:4568";
@ -56,9 +56,7 @@ public class PermissioningConfigurationBuilderTest {
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
PermissioningConfiguration permissioningConfiguration = PermissioningConfiguration permissioningConfiguration = permissioningConfig(toml);
PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true);
assertThat(permissioningConfiguration.isAccountWhitelistEnabled()).isTrue(); assertThat(permissioningConfiguration.isAccountWhitelistEnabled()).isTrue();
assertThat(permissioningConfiguration.getAccountWhitelist()) assertThat(permissioningConfiguration.getAccountWhitelist())
@ -70,7 +68,6 @@ public class PermissioningConfigurationBuilderTest {
@Test @Test
public void permissioningConfigWithOnlyNodeWhitelistSet() throws Exception { public void permissioningConfigWithOnlyNodeWhitelistSet() throws Exception {
final String uri = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567"; final String uri = "enode://" + VALID_NODE_ID + "@192.168.0.9:4567";
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_NODE_WHITELIST_ONLY);
@ -88,7 +85,6 @@ public class PermissioningConfigurationBuilderTest {
@Test @Test
public void permissioningConfigWithOnlyAccountWhitelistSet() throws Exception { public void permissioningConfigWithOnlyAccountWhitelistSet() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ACCOUNT_WHITELIST_ONLY); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ACCOUNT_WHITELIST_ONLY);
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
@ -104,31 +100,38 @@ public class PermissioningConfigurationBuilderTest {
} }
@Test @Test
public void permissioningConfigWithInvalidEnode() throws Exception { public void permissioningConfigWithInvalidAccount() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_INVALID_ACCOUNT);
final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile));
final Throwable thrown = catchThrowable(() -> permissioningConfig(toml));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("Invalid account 0xfoo");
}
@Test
public void permissioningConfigWithInvalidEnode() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_INVALID_ENODE); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_INVALID_ENODE);
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
try { final Throwable thrown = catchThrowable(() -> permissioningConfig(toml));
PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true); assertThat(thrown)
fail("Expecting IllegalArgumentException: Enode URL contains an invalid node ID"); .isInstanceOf(IllegalArgumentException.class)
} catch (IllegalArgumentException e) { .hasMessageStartingWith("Enode URL contains an invalid node ID");
assertThat(e.getMessage()).startsWith("Enode URL contains an invalid node ID");
}
} }
@Test @Test
public void permissioningConfigWithEmptyWhitelistMustNotError() throws Exception { public void permissioningConfigWithEmptyWhitelistMustNotError() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_EMPTY_WHITELISTS); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_EMPTY_WHITELISTS);
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
PermissioningConfiguration permissioningConfiguration = PermissioningConfiguration permissioningConfiguration = permissioningConfig(toml);
PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true);
assertThat(permissioningConfiguration.isNodeWhitelistEnabled()).isTrue(); assertThat(permissioningConfiguration.isNodeWhitelistEnabled()).isTrue();
assertThat(permissioningConfiguration.getNodeWhitelist()).isEmpty(); assertThat(permissioningConfiguration.getNodeWhitelist()).isEmpty();
@ -138,57 +141,41 @@ public class PermissioningConfigurationBuilderTest {
@Test @Test
public void permissioningConfigWithAbsentWhitelistMustThrowException() throws Exception { public void permissioningConfigWithAbsentWhitelistMustThrowException() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ABSENT_WHITELISTS); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_ABSENT_WHITELISTS);
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
try { final Throwable thrown = catchThrowable(() -> permissioningConfig(toml));
PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true); assertThat(thrown).isInstanceOf(Exception.class).hasMessageContaining("Unexpected end of line");
fail("expected exception: no valid whitelists in the TOML file");
} catch (Exception e) {
assertThat(e.getMessage().contains("Unexpected end of line")).isTrue();
}
} }
@Test @Test
public void permissioningConfigWithUnrecognizedKeyMustThrowException() throws Exception { public void permissioningConfigWithUnrecognizedKeyMustThrowException() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_UNRECOGNIZED_KEY); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_UNRECOGNIZED_KEY);
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
try { final Throwable thrown = catchThrowable(() -> permissioningConfig(toml));
PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true); assertThat(thrown)
fail("expected exception: didn't find a recognized key in the TOML file"); .isInstanceOf(Exception.class)
} catch (Exception e) { .hasMessageContaining("config option missing")
assertThat(e.getMessage().contains("config option missing")).isTrue(); .hasMessageContaining(PermissioningConfigurationBuilder.ACCOUNTS_WHITELIST);
assertThat(e.getMessage().contains(PermissioningConfigurationBuilder.ACCOUNTS_WHITELIST))
.isTrue();
}
} }
@Test @Test
public void permissioningConfigWithEmptyFileMustThrowException() throws Exception { public void permissioningConfigWithEmptyFileMustThrowException() throws Exception {
// write an empty file // write an empty file
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
try { final Throwable thrown = catchThrowable(() -> permissioningConfig(toml));
PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true);
fail("expected exception: empty TOML file");
} catch (Exception e) { assertThat(thrown).isInstanceOf(Exception.class).hasMessageContaining("Empty TOML result");
assertThat(e.getMessage().contains("Empty TOML result")).isTrue();
}
} }
@Test @Test
public void permissioningConfigFromFileMustSetFilePath() throws Exception { public void permissioningConfigFromFileMustSetFilePath() throws Exception {
final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_VALID); final URL configFile = Resources.getResource(PERMISSIONING_CONFIG_VALID);
final Path toml = Files.createTempFile("toml", ""); final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile)); Files.write(toml, Resources.toByteArray(configFile));
@ -202,14 +189,12 @@ public class PermissioningConfigurationBuilderTest {
@Test @Test
public void permissioningConfigFromNonexistentFileMustThrowException() { public void permissioningConfigFromNonexistentFileMustThrowException() {
final Throwable thrown =
catchThrowable(() -> permissioningConfig(Paths.get("file-does-not-exist")));
try { assertThat(thrown)
PermissioningConfigurationBuilder.permissioningConfigurationFromToml( .isInstanceOf(Exception.class)
"file-does-not-exist", true, true); .hasMessageContaining("Configuration file does not exist");
fail("expected exception: file does not exist");
} catch (Exception e) {
assertThat(e.getMessage().contains("Configuration file does not exist")).isTrue();
}
} }
@Test @Test
@ -223,4 +208,9 @@ public class PermissioningConfigurationBuilderTest {
assertThat(permissioningConfiguration.isNodeWhitelistEnabled()).isTrue(); assertThat(permissioningConfiguration.isNodeWhitelistEnabled()).isTrue();
assertThat(permissioningConfiguration.getNodeWhitelist().size()).isEqualTo(5); assertThat(permissioningConfiguration.getNodeWhitelist().size()).isEqualTo(5);
} }
private PermissioningConfiguration permissioningConfig(final Path toml) throws Exception {
return PermissioningConfigurationBuilder.permissioningConfiguration(
toml.toAbsolutePath().toString(), true, true);
}
} }

@ -0,0 +1,4 @@
# Permissioning TOML file
accounts-whitelist=["0x0000000000000000000000000000000000000009"]
nodes-whitelist=["enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567","enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.169.0.9:4568"]

@ -0,0 +1,3 @@
# Permissioning TOML file (account whitelist only)
accounts-whitelist=["0xfoo"]

@ -25,7 +25,6 @@ import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.DEFA
import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.DEFAULT_METRICS_PUSH_PORT; import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.DEFAULT_METRICS_PUSH_PORT;
import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.createDefault; import static tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration.createDefault;
import tech.pegasys.pantheon.PermissioningConfigurationBuilder;
import tech.pegasys.pantheon.Runner; import tech.pegasys.pantheon.Runner;
import tech.pegasys.pantheon.RunnerBuilder; import tech.pegasys.pantheon.RunnerBuilder;
import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty; import tech.pegasys.pantheon.cli.custom.CorsAllowedOriginsProperty;
@ -47,6 +46,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi;
import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis;
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfigurationBuilder;
import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.MetricsSystem;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem; import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem;

@ -12,103 +12,29 @@
*/ */
package tech.pegasys.pantheon.cli.custom; package tech.pegasys.pantheon.cli.custom;
import static com.google.common.base.Preconditions.checkArgument; import tech.pegasys.pantheon.util.enode.EnodeURL;
import tech.pegasys.pantheon.util.NetworkUtility;
import java.net.URI; import java.net.URI;
import java.util.regex.Matcher; import java.util.function.Function;
import java.util.regex.Pattern;
import com.google.common.annotations.VisibleForTesting;
import picocli.CommandLine.ITypeConverter; import picocli.CommandLine.ITypeConverter;
public class EnodeToURIPropertyConverter implements ITypeConverter<URI> { public class EnodeToURIPropertyConverter implements ITypeConverter<URI> {
private static final String IP_REPLACE_MARKER = "$$IP_PATTERN$$"; private final Function<String, URI> converter;
private static final String IPV4_PATTERN =
"(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}";
private static final String IPV6_PATTERN = "\\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\]";
private static final String IPV6_COMPACT_PATTERN =
"\\[((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\]";
private static final String DISCOVERY_PORT_PATTERN = "\\?discport=(?<discovery>\\d+)";
private static final String HEX_STRING_PATTERN = "[0-9a-fA-F]+";
private static final String ENODE_URL_PATTERN =
"enode://(?<nodeId>\\w+)@(?<ip>" + IP_REPLACE_MARKER + "):(?<listening>\\d+)";
@Override
public URI convert(final String value) throws IllegalArgumentException {
return convertToURI(value);
}
public static URI convertToURI(final String value) throws IllegalArgumentException {
checkArgument(
value != null && !value.isEmpty(), "Can't convert null/empty string to EnodeURLProperty.");
final boolean containsDiscoveryPort = value.contains("discport");
final boolean isIPV4 = Pattern.compile(".*" + IPV4_PATTERN + ".*").matcher(value).matches();
final boolean isIPV6 = Pattern.compile(".*" + IPV6_PATTERN + ".*").matcher(value).matches();
final boolean isIPV6Compact =
Pattern.compile(".*" + IPV6_COMPACT_PATTERN + ".*").matcher(value).matches();
String pattern = ENODE_URL_PATTERN;
if (isIPV4) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV4_PATTERN);
} else if (isIPV6) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV6_PATTERN);
} else if (isIPV6Compact) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV6_COMPACT_PATTERN);
} else {
throw new IllegalArgumentException("Invalid enode URL IP format.");
}
if (containsDiscoveryPort) {
pattern += DISCOVERY_PORT_PATTERN;
}
if (isIPV6) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV6_PATTERN);
} else {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV4_PATTERN);
}
final Matcher matcher = Pattern.compile(pattern).matcher(value);
checkArgument(
matcher.matches(),
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
final String nodeId = getAndValidateNodeId(matcher);
final String ip = matcher.group("ip");
final Integer listeningPort = getAndValidatePort(matcher, "listening");
if (containsDiscoveryPort(value)) {
final Integer discoveryPort = getAndValidatePort(matcher, "discovery");
return URI.create(
String.format("enode://%s@%s:%d?discport=%d", nodeId, ip, listeningPort, discoveryPort));
} else {
return URI.create(String.format("enode://%s@%s:%d", nodeId, ip, listeningPort));
}
}
private static String getAndValidateNodeId(final Matcher matcher) {
final String invalidNodeIdErrorMsg =
"Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix.";
final String nodeId = matcher.group("nodeId");
checkArgument(nodeId.matches(HEX_STRING_PATTERN), invalidNodeIdErrorMsg); EnodeToURIPropertyConverter() {
checkArgument(nodeId.length() == 128, invalidNodeIdErrorMsg); this.converter = (s) -> new EnodeURL(s).toURI();
return nodeId;
} }
private static Integer getAndValidatePort(final Matcher matcher, final String portName) { @VisibleForTesting
int port = Integer.valueOf(matcher.group(portName)); EnodeToURIPropertyConverter(final Function<String, URI> converter) {
checkArgument( this.converter = converter;
NetworkUtility.isValidPort(port),
"Invalid " + portName + " port range. Port should be between 0 - 65535");
return port;
} }
private static boolean containsDiscoveryPort(final String value) { @Override
return value.contains("discport"); public URI convert(final String value) {
return converter.apply(value);
} }
} }

@ -12,212 +12,24 @@
*/ */
package tech.pegasys.pantheon.cli.custom; package tech.pegasys.pantheon.cli.custom;
import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq;
import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.net.URI; import java.net.URI;
import java.util.function.Function;
import org.junit.Test; import org.junit.Test;
public class EnodeToURIPropertyConverterTest { public class EnodeToURIPropertyConverterTest {
private final String VALID_NODE_ID =
"6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0";
private final String IPV4_ADDRESS = "192.168.0.1";
private final String IPV6_FULL_ADDRESS = "[2001:db8:85a3:0:0:8a2e:0370:7334]";
private final String IPV6_COMPACT_ADDRESS = "[2001:db8:85a3::8a2e:0370:7334]";
private final int P2P_PORT = 30303;
private final String DISCOVERY_QUERY = "discport=30301";
private final EnodeToURIPropertyConverter converter = new EnodeToURIPropertyConverter();
@Test
public void convertEnodeURLWithDiscoveryPortShouldBuildExpectedURI() {
final String value =
"enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT + "?" + DISCOVERY_QUERY;
final URI expectedURI = URI.create(value);
final URI convertedURI = converter.convert(value);
assertThat(convertedURI).isEqualTo(expectedURI);
assertThat(convertedURI.getUserInfo()).isEqualTo(VALID_NODE_ID);
assertThat(convertedURI.getHost()).isEqualTo(IPV4_ADDRESS);
assertThat(convertedURI.getPort()).isEqualTo(P2P_PORT);
assertThat(convertedURI.getQuery()).isEqualTo(DISCOVERY_QUERY);
}
@Test
public void convertEnodeURLWithoutDiscoveryPortShouldBuildExpectedURI() {
final String value = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT;
final URI expectedURI = URI.create(value);
final URI convertedURI = converter.convert(value);
assertThat(convertedURI).isEqualTo(expectedURI);
assertThat(convertedURI.getUserInfo()).isEqualTo(VALID_NODE_ID);
assertThat(convertedURI.getHost()).isEqualTo(IPV4_ADDRESS);
assertThat(convertedURI.getPort()).isEqualTo(P2P_PORT);
}
@Test @Test
public void convertEnodeURLWithIPV6ShouldBuildExpectedURI() { @SuppressWarnings("unchecked")
final String value = public void converterDelegatesToFunction() {
"enode://" Function<String, URI> function = mock(Function.class);
+ VALID_NODE_ID
+ "@"
+ IPV6_FULL_ADDRESS
+ ":"
+ P2P_PORT
+ "?"
+ DISCOVERY_QUERY;
final URI expectedURI = URI.create(value);
final URI convertedURI = converter.convert(value); new EnodeToURIPropertyConverter(function).convert("foo");
assertThat(convertedURI).isEqualTo(expectedURI);
assertThat(convertedURI.getUserInfo()).isEqualTo(VALID_NODE_ID);
assertThat(convertedURI.getHost()).isEqualTo(IPV6_FULL_ADDRESS);
assertThat(convertedURI.getPort()).isEqualTo(P2P_PORT);
assertThat(convertedURI.getQuery()).isEqualTo(DISCOVERY_QUERY);
}
@Test
public void convertEnodeURLWithIPV6InCompactFormShouldBuildExpectedURI() {
final String value =
"enode://"
+ VALID_NODE_ID
+ "@"
+ IPV6_COMPACT_ADDRESS
+ ":"
+ P2P_PORT
+ "?"
+ DISCOVERY_QUERY;
final URI expectedURI = URI.create(value);
final URI convertedURI = converter.convert(value);
assertThat(convertedURI).isEqualTo(expectedURI);
assertThat(convertedURI.getUserInfo()).isEqualTo(VALID_NODE_ID);
assertThat(convertedURI.getHost()).isEqualTo(IPV6_COMPACT_ADDRESS);
assertThat(convertedURI.getPort()).isEqualTo(P2P_PORT);
assertThat(convertedURI.getQuery()).isEqualTo(DISCOVERY_QUERY);
}
@Test
public void convertEnodeURLWithoutNodeIdShouldFail() {
final String value = "enode://@" + IPV4_ADDRESS + ":" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
}
@Test
public void convertEnodeURLWithInvalidSizeNodeIdShouldFail() {
final String value = "enode://wrong_size_string@" + IPV4_ADDRESS + ":" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix.");
}
@Test
public void convertEnodeURLWithInvalidHexCharacterNodeIdShouldFail() {
final String value =
"enode://0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000@"
+ IPV4_ADDRESS
+ ":"
+ P2P_PORT;
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix.");
}
@Test
public void convertEnodeURLWithoutIpShouldFail() {
final String value = "enode://" + VALID_NODE_ID + "@:" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid enode URL IP format.");
}
@Test
public void convertEnodeURLWithInvalidIpFormatShouldFail() {
final String value = "enode://" + VALID_NODE_ID + "@192.0.1:" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid enode URL IP format.");
}
@Test
public void convertEnodeURLWithoutListeningPortShouldFail() {
final String value = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":";
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
}
@Test
public void convertEnodeURLWithoutListeningPortAndWithDiscoveryPortShouldFail() {
final String value = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":?30301";
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
}
@Test
public void convertEnodeURLWithAboveRangeListeningPortShouldFail() {
final String value = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":98765";
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid listening port range. Port should be between 0 - 65535");
}
@Test
public void convertEnodeURLWithAboveRangeDiscoveryPortShouldFail() {
final String value =
"enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT + "?discport=98765";
final Throwable thrown = catchThrowable(() -> converter.convert(value));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid discovery port range. Port should be between 0 - 65535");
}
@Test
public void convertNullEnodeURLShouldFail() {
final Throwable thrown = catchThrowable(() -> converter.convert(null));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Can't convert null/empty string to EnodeURLProperty.");
}
@Test
public void convertEmptyEnodeURLShouldFail() {
final Throwable thrown = catchThrowable(() -> converter.convert(""));
assertThat(thrown) verify(function).apply(eq("foo"));
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Can't convert null/empty string to EnodeURLProperty.");
} }
} }

@ -15,10 +15,10 @@ package tech.pegasys.pantheon.util;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.fail;
import tech.pegasys.pantheon.PermissioningConfigurationBuilder;
import tech.pegasys.pantheon.cli.EthNetworkConfig; import tech.pegasys.pantheon.cli.EthNetworkConfig;
import tech.pegasys.pantheon.cli.NetworkName; import tech.pegasys.pantheon.cli.NetworkName;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfigurationBuilder;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;

@ -0,0 +1,165 @@
/*
* 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.util.enode;
import static com.google.common.base.Preconditions.checkArgument;
import tech.pegasys.pantheon.util.NetworkUtility;
import java.net.URI;
import java.util.OptionalInt;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Objects;
public class EnodeURL {
private static final String IP_REPLACE_MARKER = "$$IP_PATTERN$$";
private static final String IPV4_PATTERN =
"(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}";
private static final String IPV6_PATTERN = "\\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\]";
private static final String IPV6_COMPACT_PATTERN =
"\\[((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\]";
private static final String DISCOVERY_PORT_PATTERN = "\\?discport=(?<discovery>\\d+)";
private static final String HEX_STRING_PATTERN = "[0-9a-fA-F]+";
private static final String ENODE_URL_PATTERN =
"enode://(?<nodeId>\\w+)@(?<ip>" + IP_REPLACE_MARKER + "):(?<listening>\\d+)";
private final String nodeId;
private final String ip;
private final Integer listeningPort;
private final OptionalInt discoveryPort;
public EnodeURL(
final String nodeId,
final String ip,
final Integer listeningPort,
final OptionalInt discoveryPort) {
this.nodeId = nodeId;
this.ip = ip;
this.listeningPort = listeningPort;
this.discoveryPort = discoveryPort;
}
public EnodeURL(final String nodeId, final String ip, final Integer listeningPort) {
this.nodeId = nodeId;
this.ip = ip;
this.listeningPort = listeningPort;
this.discoveryPort = OptionalInt.empty();
}
public EnodeURL(final String value) {
checkArgument(
value != null && !value.isEmpty(), "Can't convert null/empty string to EnodeURLProperty.");
final boolean containsDiscoveryPort = value.contains("discport");
final boolean isIPV4 = Pattern.compile(".*" + IPV4_PATTERN + ".*").matcher(value).matches();
final boolean isIPV6 = Pattern.compile(".*" + IPV6_PATTERN + ".*").matcher(value).matches();
final boolean isIPV6Compact =
Pattern.compile(".*" + IPV6_COMPACT_PATTERN + ".*").matcher(value).matches();
String pattern = ENODE_URL_PATTERN;
if (isIPV4) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV4_PATTERN);
} else if (isIPV6) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV6_PATTERN);
} else if (isIPV6Compact) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV6_COMPACT_PATTERN);
} else {
throw new IllegalArgumentException("Invalid enode URL IP format.");
}
if (containsDiscoveryPort) {
pattern += DISCOVERY_PORT_PATTERN;
}
if (isIPV6) {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV6_PATTERN);
} else {
pattern = pattern.replace(IP_REPLACE_MARKER, IPV4_PATTERN);
}
final Matcher matcher = Pattern.compile(pattern).matcher(value);
checkArgument(
matcher.matches(),
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
this.nodeId = getAndValidateNodeId(matcher);
this.ip = matcher.group("ip");
this.listeningPort = getAndValidatePort(matcher, "listening");
if (containsDiscoveryPort(value)) {
this.discoveryPort = OptionalInt.of(getAndValidatePort(matcher, "discovery"));
} else {
this.discoveryPort = OptionalInt.empty();
}
}
public URI toURI() {
if (discoveryPort.isPresent()) {
return URI.create(
String.format(
"enode://%s@%s:%d?discport=%d", nodeId, ip, listeningPort, discoveryPort.getAsInt()));
} else {
return URI.create(String.format("enode://%s@%s:%d", nodeId, ip, listeningPort));
}
}
public static URI asURI(final String url) {
return new EnodeURL(url).toURI();
}
private static String getAndValidateNodeId(final Matcher matcher) {
final String invalidNodeIdErrorMsg =
"Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix.";
final String nodeId = matcher.group("nodeId");
checkArgument(nodeId.matches(HEX_STRING_PATTERN), invalidNodeIdErrorMsg);
checkArgument(nodeId.length() == 128, invalidNodeIdErrorMsg);
return nodeId;
}
private static Integer getAndValidatePort(final Matcher matcher, final String portName) {
int port = Integer.valueOf(matcher.group(portName));
checkArgument(
NetworkUtility.isValidPort(port),
"Invalid " + portName + " port range. Port should be between 0 - 65535");
return port;
}
private static boolean containsDiscoveryPort(final String value) {
return value.contains("discport");
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EnodeURL enodeURL = (EnodeURL) o;
return Objects.equal(nodeId, enodeURL.nodeId)
&& Objects.equal(ip, enodeURL.ip)
&& Objects.equal(listeningPort, enodeURL.listeningPort)
&& Objects.equal(discoveryPort, enodeURL.discoveryPort);
}
@Override
public int hashCode() {
return Objects.hashCode(nodeId, ip, listeningPort, discoveryPort);
}
}

@ -0,0 +1,230 @@
/*
* 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.util.enode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import java.net.URI;
import java.util.OptionalInt;
import org.junit.Test;
public class EnodeURLTest {
private final String VALID_NODE_ID =
"6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0";
private final String IPV4_ADDRESS = "192.168.0.1";
private final String IPV6_FULL_ADDRESS = "[2001:db8:85a3:0:0:8a2e:0370:7334]";
private final String IPV6_COMPACT_ADDRESS = "[2001:db8:85a3::8a2e:0370:7334]";
private final int P2P_PORT = 30303;
private final int DISCOVERY_PORT = 30301;
private final String DISCOVERY_QUERY = "discport=" + DISCOVERY_PORT;
@Test
public void createEnodeURLWithDiscoveryPortShouldBuildExpectedEnodeURLObject() {
final EnodeURL expectedEnodeURL =
new EnodeURL(VALID_NODE_ID, IPV4_ADDRESS, P2P_PORT, OptionalInt.of(DISCOVERY_PORT));
final String enodeURLString =
"enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT + "?" + DISCOVERY_QUERY;
final EnodeURL enodeURL = new EnodeURL(enodeURLString);
assertThat(enodeURL).isEqualTo(expectedEnodeURL);
}
@Test
public void createEnodeURLWithoutDiscoveryPortShouldBuildExpectedEnodeURLObject() {
final EnodeURL expectedEnodeURL = new EnodeURL(VALID_NODE_ID, IPV4_ADDRESS, P2P_PORT);
final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT;
final EnodeURL enodeURL = new EnodeURL(enodeURLString);
assertThat(enodeURL).isEqualTo(expectedEnodeURL);
}
@Test
public void createEnodeURLWithIPV6ShouldBuildExpectedEnodeURLObject() {
final EnodeURL expectedEnodeURL =
new EnodeURL(VALID_NODE_ID, IPV6_FULL_ADDRESS, P2P_PORT, OptionalInt.of(DISCOVERY_PORT));
final String enodeURLString =
"enode://"
+ VALID_NODE_ID
+ "@"
+ IPV6_FULL_ADDRESS
+ ":"
+ P2P_PORT
+ "?"
+ DISCOVERY_QUERY;
final EnodeURL enodeURL = new EnodeURL(enodeURLString);
assertThat(enodeURL).isEqualTo(expectedEnodeURL);
}
@Test
public void createEnodeURLWithIPV6InCompactFormShouldBuildExpectedEnodeURLObject() {
final EnodeURL expectedEnodeURL =
new EnodeURL(VALID_NODE_ID, IPV6_COMPACT_ADDRESS, P2P_PORT, OptionalInt.of(DISCOVERY_PORT));
final String enodeURLString =
"enode://"
+ VALID_NODE_ID
+ "@"
+ IPV6_COMPACT_ADDRESS
+ ":"
+ P2P_PORT
+ "?"
+ DISCOVERY_QUERY;
final EnodeURL enodeURL = new EnodeURL(enodeURLString);
assertThat(enodeURL).isEqualTo(expectedEnodeURL);
}
@Test
public void createEnodeURLWithoutNodeIdShouldFail() {
final String enodeURLString = "enode://@" + IPV4_ADDRESS + ":" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
}
@Test
public void createEnodeURLWithInvalidSizeNodeIdShouldFail() {
final String enodeURLString = "enode://wrong_size_string@" + IPV4_ADDRESS + ":" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix.");
}
@Test
public void createEnodeURLWithInvalidHexCharacterNodeIdShouldFail() {
final String enodeURLString =
"enode://0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000@"
+ IPV4_ADDRESS
+ ":"
+ P2P_PORT;
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Enode URL contains an invalid node ID. Node ID must have 128 characters and shouldn't include the '0x' hex prefix.");
}
@Test
public void createEnodeURLWithoutIpShouldFail() {
final String enodeURLString = "enode://" + VALID_NODE_ID + "@:" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid enode URL IP format.");
}
@Test
public void createEnodeURLWithInvalidIpFormatShouldFail() {
final String enodeURLString = "enode://" + VALID_NODE_ID + "@192.0.1:" + P2P_PORT;
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid enode URL IP format.");
}
@Test
public void createEnodeURLWithoutListeningPortShouldFail() {
final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":";
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
}
@Test
public void createEnodeURLWithoutListeningPortAndWithDiscoveryPortShouldFail() {
final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":?30301";
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Invalid enode URL syntax. Enode URL should have the following format 'enode://<node_id>@<ip>:<listening_port>[?discport=<discovery_port>]'.");
}
@Test
public void createEnodeURLWithAboveRangeListeningPortShouldFail() {
final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":98765";
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid listening port range. Port should be between 0 - 65535");
}
@Test
public void createEnodeURLWithAboveRangeDiscoveryPortShouldFail() {
final String enodeURLString =
"enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT + "?discport=98765";
final Throwable thrown = catchThrowable(() -> new EnodeURL(enodeURLString));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid discovery port range. Port should be between 0 - 65535");
}
@Test
public void createEnodeURLWithNullEnodeURLShouldFail() {
final Throwable thrown = catchThrowable(() -> new EnodeURL(null));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Can't convert null/empty string to EnodeURLProperty.");
}
@Test
public void createEnodeURLWithEmptyEnodeURLShouldFail() {
final Throwable thrown = catchThrowable(() -> new EnodeURL(""));
assertThat(thrown)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Can't convert null/empty string to EnodeURLProperty.");
}
@Test
public void toURIWithDiscoveryPortCreateExpectedURI() {
final String enodeURLString =
"enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT + "?" + DISCOVERY_QUERY;
final URI expectedURI = URI.create(enodeURLString);
final URI createdURI = new EnodeURL(enodeURLString).toURI();
assertThat(createdURI).isEqualTo(expectedURI);
}
@Test
public void toURIWithoutDiscoveryPortCreateExpectedURI() {
final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + IPV4_ADDRESS + ":" + P2P_PORT;
final URI expectedURI = URI.create(enodeURLString);
final URI createdURI = new EnodeURL(enodeURLString).toURI();
assertThat(createdURI).isEqualTo(expectedURI);
}
}
Loading…
Cancel
Save