From 637af126ca49a323c36e3321e52015aa3eb598ab Mon Sep 17 00:00:00 2001 From: mark-terry <36909937+mark-terry@users.noreply.github.com> Date: Wed, 9 Jan 2019 11:20:02 +1000 Subject: [PATCH] [NC-1938] Nodes whitelist JSON-RPC APIs (#476) --- .../dsl/condition/perm/AddNodeSuccess.java | 34 +++++ .../perm/GetNodesWhitelistPopulated.java | 40 +++++ .../dsl/condition/perm/RemoveNodeSuccess.java | 34 +++++ .../tests/acceptance/dsl/jsonrpc/Perm.java | 16 ++ .../dsl/node/factory/PantheonNodeFactory.java | 23 ++- .../dsl/transaction/PantheonWeb3j.java | 28 ++++ .../dsl/transaction/Transactions.java | 15 ++ .../perm/PermAddNodeTransaction.java | 41 ++++++ .../PermGetNodesWhitelistTransaction.java | 35 +++++ .../perm/PermRemoveNodeTransaction.java | 41 ++++++ ...PermAddNodesToWhitelistAcceptanceTest.java | 49 ++++++ .../PermGetNodesWhitelistAcceptanceTest.java | 48 ++++++ ...emoveNodesFromWhitelistAcceptanceTest.java | 53 +++++++ .../jsonrpc/JsonRpcMethodsFactory.java | 10 ++ .../pantheon/ethereum/jsonrpc/RpcApis.java | 4 +- .../PermAddNodesToWhitelist.java | 80 ++++++++++ .../permissioning/PermGetNodesWhitelist.java | 72 +++++++++ .../PermRemoveNodesFromWhitelist.java | 80 ++++++++++ .../parameters/StringListParameter.java | 34 +++++ .../internal/response/JsonRpcError.java | 8 +- .../PermAddNodesToWhitelistTest.java | 139 ++++++++++++++++++ .../PermGetNodesWhitelistTest.java | 130 ++++++++++++++++ .../PermRemoveNodesFromWhitelistTest.java | 129 ++++++++++++++++ ethereum/mock-p2p/build.gradle | 1 + .../ethereum/p2p/testing/MockNetwork.java | 7 + .../pantheon/ethereum/p2p/api/P2PNetwork.java | 3 + .../internal/PeerDiscoveryController.java | 6 +- .../ethereum/p2p/netty/NettyP2PNetwork.java | 8 + .../NodeWhitelistController.java | 81 +++++++++- .../NodeWhitelistControllerTest.java | 66 +++++++++ 30 files changed, 1301 insertions(+), 14 deletions(-) create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddNodeSuccess.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetNodesWhitelistPopulated.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveNodeSuccess.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddNodeTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetNodesWhitelistTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveNodeTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermAddNodesToWhitelistAcceptanceTest.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermGetNodesWhitelistAcceptanceTest.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermRemoveNodesFromWhitelistAcceptanceTest.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelist.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelist.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelist.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/parameters/StringListParameter.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelistTest.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelistTest.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelistTest.java create mode 100644 ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistControllerTest.java diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddNodeSuccess.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddNodeSuccess.java new file mode 100644 index 0000000000..f3a92c6266 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddNodeSuccess.java @@ -0,0 +1,34 @@ +/* + * 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.tests.acceptance.dsl.condition.perm; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermAddNodeTransaction; + +public class AddNodeSuccess implements Condition { + + private final PermAddNodeTransaction transaction; + + public AddNodeSuccess(final PermAddNodeTransaction transactions) { + this.transaction = transactions; + } + + @Override + public void verify(final Node node) { + final Boolean response = node.execute(transaction); + assertThat(response).isTrue(); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetNodesWhitelistPopulated.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetNodesWhitelistPopulated.java new file mode 100644 index 0000000000..0270f1b84c --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetNodesWhitelistPopulated.java @@ -0,0 +1,40 @@ +/* + * 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.tests.acceptance.dsl.condition.perm; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermGetNodesWhitelistTransaction; + +import java.util.List; + +public class GetNodesWhitelistPopulated implements Condition { + + private final PermGetNodesWhitelistTransaction transaction; + private final int expectedNodeNum; + + public GetNodesWhitelistPopulated( + final PermGetNodesWhitelistTransaction transaction, final int expectedNodeNum) { + this.transaction = transaction; + this.expectedNodeNum = expectedNodeNum; + } + + @Override + public void verify(final Node node) { + final List response = node.execute(transaction); + assertThat(response).isInstanceOf(List.class); + assertThat(response).size().isEqualTo(expectedNodeNum); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveNodeSuccess.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveNodeSuccess.java new file mode 100644 index 0000000000..0b8856888c --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveNodeSuccess.java @@ -0,0 +1,34 @@ +/* + * 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.tests.acceptance.dsl.condition.perm; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermRemoveNodeTransaction; + +public class RemoveNodeSuccess implements Condition { + + private final PermRemoveNodeTransaction transaction; + + public RemoveNodeSuccess(final PermRemoveNodeTransaction transaction) { + this.transaction = transaction; + } + + @Override + public void verify(final Node node) { + final Boolean response = node.execute(transaction); + assertThat(response).isTrue(); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Perm.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Perm.java index 90a25d56e6..e8bb0edd11 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Perm.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Perm.java @@ -14,11 +14,15 @@ package tech.pegasys.pantheon.tests.acceptance.dsl.jsonrpc; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.AddAccountsToWhitelistSuccessfully; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.AddNodeSuccess; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.GetExpectedAccountsWhitelist; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.GetNodesWhitelistPopulated; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.RemoveAccountsFromWhitelistSuccessfully; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.RemoveNodeSuccess; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transactions; import java.util.Arrays; +import java.util.List; public class Perm { @@ -41,4 +45,16 @@ public class Perm { return new GetExpectedAccountsWhitelist( transactions.getAccountsWhiteList(), Arrays.asList(expectedAccounts)); } + + public Condition addNodesToWhitelist(final List enodeList) { + return new AddNodeSuccess(transactions.addNodesToWhitelist(enodeList)); + } + + public Condition removeNodesFromWhitelist(final List enodeList) { + return new RemoveNodeSuccess(transactions.removeNodesFromWhitelist(enodeList)); + } + + public Condition getNodesWhitelist(final int expectedNodeNum) { + return new GetNodesWhitelistPopulated(transactions.getNodesWhiteList(), expectedNodeNum); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java index ebc0b46a71..ab9c736cdd 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java @@ -134,7 +134,7 @@ public class PantheonNodeFactory { return create( new PantheonFactoryConfigurationBuilder() .setName(name) - .jsonRpcEnabled() + .setJsonRpcConfiguration(jsonRpcConfigWithPermissioning()) .setPermissioningConfiguration(permissioningConfiguration) .build()); } @@ -154,6 +154,27 @@ public class PantheonNodeFactory { .build()); } + public PantheonNode createNodeWithNodesWhitelistAndPermRPC( + final String name, final List nodesWhitelist) throws IOException { + PermissioningConfiguration permissioningConfiguration = + PermissioningConfiguration.createDefault(); + permissioningConfiguration.setNodeWhitelist(nodesWhitelist); + + JsonRpcConfiguration rpcConfig = JsonRpcConfiguration.createDefault(); + + rpcConfig.setEnabled(true); + rpcConfig.setPort(0); + rpcConfig.setHostsWhitelist(singletonList("*")); + rpcConfig.addRpcApi(RpcApis.PERM); + + return create( + new PantheonFactoryConfigurationBuilder() + .setName(name) + .setJsonRpcConfiguration(rpcConfig) + .setPermissioningConfiguration(permissioningConfiguration) + .build()); + } + public PantheonNode createCliqueNode(final String name) throws IOException { return create( new PantheonFactoryConfigurationBuilder() diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/PantheonWeb3j.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/PantheonWeb3j.java index c0a285804f..d488a8d5c1 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/PantheonWeb3j.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/PantheonWeb3j.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; +import org.assertj.core.util.Lists; import org.web3j.protocol.Web3jService; import org.web3j.protocol.core.JsonRpc2_0Web3j; import org.web3j.protocol.core.Request; @@ -108,4 +109,31 @@ public class PantheonWeb3j extends JsonRpc2_0Web3j { public static class RemoveAccountsFromWhitelistResponse extends Response {} public static class GetAccountsWhitelistResponse extends Response> {} + + public Request addNodesToWhitelist(final List enodeList) { + return new Request<>( + "perm_addNodesToWhitelist", + Collections.singletonList(enodeList), + web3jService, + AddNodeResponse.class); + } + + public Request removeNodesFromWhitelist(final List enodeList) { + return new Request<>( + "perm_removeNodesFromWhitelist", + Collections.singletonList(enodeList), + web3jService, + RemoveNodeResponse.class); + } + + public Request getNodesWhitelist() { + return new Request<>( + "perm_getNodesWhitelist", Lists.emptyList(), web3jService, GetNodesWhitelistResponse.class); + } + + public static class AddNodeResponse extends Response {} + + public static class RemoveNodeResponse extends Response {} + + public static class GetNodesWhitelistResponse extends Response> {} } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/Transactions.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/Transactions.java index 853408532d..4ffc42051a 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/Transactions.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/Transactions.java @@ -18,8 +18,11 @@ import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.account.TransferTr import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.account.TransferTransactionSet; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.eth.EthGetTransactionCountTransaction; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermAddAccountsToWhitelistTransaction; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermAddNodeTransaction; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermGetAccountsWhitelistTransaction; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermGetNodesWhitelistTransaction; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermRemoveAccountsFromWhitelistTransaction; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermRemoveNodeTransaction; import java.math.BigInteger; import java.util.ArrayList; @@ -83,4 +86,16 @@ public class Transactions { public EthGetTransactionCountTransaction getTransactionCount(final String accountAddress) { return new EthGetTransactionCountTransaction(accountAddress); } + + public PermAddNodeTransaction addNodesToWhitelist(final List enodeList) { + return new PermAddNodeTransaction(enodeList); + } + + public PermRemoveNodeTransaction removeNodesFromWhitelist(final List enodeList) { + return new PermRemoveNodeTransaction(enodeList); + } + + public PermGetNodesWhitelistTransaction getNodesWhiteList() { + return new PermGetNodesWhitelistTransaction(); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddNodeTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddNodeTransaction.java new file mode 100644 index 0000000000..a1be4d199c --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddNodeTransaction.java @@ -0,0 +1,41 @@ +/* + * 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.tests.acceptance.dsl.transaction.perm; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j.AddNodeResponse; + +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; +import java.util.List; + +public class PermAddNodeTransaction implements Transaction { + private final List enodeList; + + public PermAddNodeTransaction(final List enodeList) { + this.enodeList = enodeList; + } + + @Override + public Boolean execute(final PantheonWeb3j node) { + try { + final AddNodeResponse result = node.addNodesToWhitelist(enodeList).send(); + assertThat(result).isNotNull(); + return result.getResult(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetNodesWhitelistTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetNodesWhitelistTransaction.java new file mode 100644 index 0000000000..0687bedad1 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetNodesWhitelistTransaction.java @@ -0,0 +1,35 @@ +/* + * 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.tests.acceptance.dsl.transaction.perm; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j.GetNodesWhitelistResponse; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; +import java.util.List; + +public class PermGetNodesWhitelistTransaction implements Transaction> { + @Override + public List execute(final PantheonWeb3j node) { + try { + GetNodesWhitelistResponse result = node.getNodesWhitelist().send(); + assertThat(result).isNotNull(); + return result.getResult(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveNodeTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveNodeTransaction.java new file mode 100644 index 0000000000..1a1ad6f9ea --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveNodeTransaction.java @@ -0,0 +1,41 @@ +/* + * 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.tests.acceptance.dsl.transaction.perm; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.PantheonWeb3j.RemoveNodeResponse; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; +import java.util.List; + +public class PermRemoveNodeTransaction implements Transaction { + private final List enodeList; + + public PermRemoveNodeTransaction(final List enodeList) { + this.enodeList = enodeList; + } + + @Override + public Boolean execute(final PantheonWeb3j node) { + try { + final RemoveNodeResponse result = node.removeNodesFromWhitelist(enodeList).send(); + assertThat(result).isNotNull(); + return result.getResult(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermAddNodesToWhitelistAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermAddNodesToWhitelistAcceptanceTest.java new file mode 100644 index 0000000000..1dfc79833a --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermAddNodesToWhitelistAcceptanceTest.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.tests.acceptance.jsonrpc.perm; + +import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; +import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; + +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; + +public class PermAddNodesToWhitelistAcceptanceTest extends AcceptanceTestBase { + + private Node node; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode3 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + + @Before + public void setUp() throws Exception { + node = pantheon.createArchiveNodeWithRpcApis("node1", RpcApis.WEB3, RpcApis.NET, RpcApis.PERM); + cluster.start(node); + } + + @Test + public void shouldAddSinglePeer() { + node.verify(perm.addNodesToWhitelist(Lists.newArrayList(enode1))); + } + + @Test + public void shouldAddMultiplePeers() { + node.verify(perm.addNodesToWhitelist(Lists.newArrayList(enode1, enode2, enode3))); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermGetNodesWhitelistAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermGetNodesWhitelistAcceptanceTest.java new file mode 100644 index 0000000000..c0b05ae705 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermGetNodesWhitelistAcceptanceTest.java @@ -0,0 +1,48 @@ +/* + * 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.tests.acceptance.jsonrpc.perm; + +import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; + +import java.net.URI; +import java.util.ArrayList; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class PermGetNodesWhitelistAcceptanceTest extends AcceptanceTestBase { + private Node node; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.1:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.2:4567"; + private final String enode3 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.3:4567"; + private final ArrayList nodesWhitelist = + Lists.newArrayList(URI.create(enode1), URI.create(enode2), URI.create(enode3)); + + @Before + public void setUp() throws Exception { + + node = pantheon.createNodeWithNodesWhitelist("node1", nodesWhitelist); + cluster.start(node); + } + + @Test + public void shouldGetNodesWhitelistContents() { + node.verify(perm.getNodesWhitelist(nodesWhitelist.size())); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermRemoveNodesFromWhitelistAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermRemoveNodesFromWhitelistAcceptanceTest.java new file mode 100644 index 0000000000..7ff1ea5783 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/perm/PermRemoveNodesFromWhitelistAcceptanceTest.java @@ -0,0 +1,53 @@ +/* + * 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.tests.acceptance.jsonrpc.perm; + +import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; + +import java.net.URI; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class PermRemoveNodesFromWhitelistAcceptanceTest extends AcceptanceTestBase { + + private Node node; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.1:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.2:4567"; + private final String enode3 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.3:4567"; + + @Before + public void setUp() throws Exception { + node = + pantheon.createNodeWithNodesWhitelist( + "node1", + Lists.newArrayList(URI.create(enode1), URI.create(enode2), URI.create(enode3))); + cluster.start(node); + } + + @Test + public void shouldRemoveSinglePeer() { + node.verify(perm.removeNodesFromWhitelist(Lists.newArrayList(enode1))); + } + + @Test + public void shouldRemoveMultiplePeers() { + node.verify(perm.removeNodesFromWhitelist(Lists.newArrayList(enode1, enode2, enode3))); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java index 0ee49343fd..6564759ea3 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java @@ -69,8 +69,11 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerSetEth import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerStart; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerStop; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermAddAccountsToWhitelist; +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.PermGetNodesWhitelist; 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.parameters.JsonRpcParameter; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.processor.BlockReplay; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTracer; @@ -225,6 +228,13 @@ public class JsonRpcMethodsFactory { minerSetCoinbase, new MinerSetEtherbase(minerSetCoinbase)); } + if (rpcApis.contains(RpcApis.PERM)) { + addMethods( + enabledMethods, + new PermAddNodesToWhitelist(p2pNetwork, parameter), + new PermRemoveNodesFromWhitelist(p2pNetwork, parameter), + new PermGetNodesWhitelist(p2pNetwork)); + } if (rpcApis.contains(RpcApis.ADMIN)) { addMethods(enabledMethods, new AdminPeers(p2pNetwork)); } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcApis.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcApis.java index fb33feb4fd..d22f560491 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcApis.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcApis.java @@ -21,9 +21,9 @@ public class RpcApis { public static final RpcApi DEBUG = new RpcApi("DEBUG"); public static final RpcApi MINER = new RpcApi("MINER"); public static final RpcApi NET = new RpcApi("NET"); + public static final RpcApi PERM = new RpcApi("PERM"); public static final RpcApi WEB3 = new RpcApi("WEB3"); public static final RpcApi ADMIN = new RpcApi("ADMIN"); - public static final RpcApi PERM = new RpcApi("PERM"); public static final Collection DEFAULT_JSON_RPC_APIS = Arrays.asList(ETH, NET, WEB3); @@ -36,6 +36,8 @@ public class RpcApis { return Optional.of(MINER); } else if (name.equals(NET.getCliValue())) { return Optional.of(NET); + } else if (name.equals(PERM.getCliValue())) { + return Optional.of(PERM); } else if (name.equals(WEB3.getCliValue())) { return Optional.of(WEB3); } else if (name.equals(ADMIN.getCliValue())) { diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelist.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelist.java new file mode 100644 index 0000000000..5dc99a0151 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelist.java @@ -0,0 +1,80 @@ +/* + * 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.parameters.JsonRpcParameter; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.StringListParameter; +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.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; + +import java.util.List; +import java.util.stream.Collectors; + +public class PermAddNodesToWhitelist implements JsonRpcMethod { + + private final P2PNetwork p2pNetwork; + private final JsonRpcParameter parameters; + + public PermAddNodesToWhitelist(final P2PNetwork p2pNetwork, final JsonRpcParameter parameters) { + this.p2pNetwork = p2pNetwork; + this.parameters = parameters; + } + + @Override + public String getName() { + return "perm_addNodesToWhitelist"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + final StringListParameter enodeListParam = + parameters.required(req.getParams(), 0, StringListParameter.class); + + try { + List peers = + enodeListParam + .getStringList() + .parallelStream() + .map(this::parsePeer) + .collect(Collectors.toList()); + + NodeWhitelistController.NodesWhitelistResult nodesWhitelistResult = + p2pNetwork.getNodeWhitelistController().addNodes(peers); + + switch (nodesWhitelistResult.result()) { + case SUCCESS: + return new JsonRpcSuccessResponse(req.getId(), true); + case ADD_ERROR_DUPLICATED_ENTRY: + return new JsonRpcErrorResponse( + req.getId(), JsonRpcError.NODE_WHITELIST_DUPLICATED_ENTRY); + default: + throw new Exception(); + } + } catch (IllegalArgumentException e) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + } catch (Exception e) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.INTERNAL_ERROR); + } + } + + private DefaultPeer parsePeer(final String enodeURI) { + return DefaultPeer.fromURI(enodeURI); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelist.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelist.java new file mode 100644 index 0000000000..5b50540c82 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelist.java @@ -0,0 +1,72 @@ +/* + * 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.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.peers.Endpoint; +import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; + +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.Collectors; + +import org.bouncycastle.util.encoders.Hex; + +public class PermGetNodesWhitelist implements JsonRpcMethod { + + private final P2PNetwork p2pNetwork; + + public PermGetNodesWhitelist(final P2PNetwork p2pNetwork) { + this.p2pNetwork = p2pNetwork; + } + + @Override + public String getName() { + return "perm_getNodesWhitelist"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + if (p2pNetwork.getNodeWhitelistController().nodeWhitelistSet()) { + List nodesWhitelist = p2pNetwork.getNodeWhitelistController().getNodesWhitelist(); + + List enodeList = + nodesWhitelist.parallelStream().map(this::buildEnodeURI).collect(Collectors.toList()); + + return new JsonRpcSuccessResponse(req.getId(), enodeList); + } else { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NODE_WHITELIST_NOT_SET); + } + } + + private String buildEnodeURI(final Peer s) { + String url = Hex.toHexString(s.getId().extractArray()); + Endpoint endpoint = s.getEndpoint(); + String nodeIp = endpoint.getHost(); + OptionalInt tcpPort = endpoint.getTcpPort(); + int udpPort = endpoint.getUdpPort(); + + if (tcpPort.isPresent() && (tcpPort.getAsInt() != udpPort)) { + return String.format( + "enode://%s@%s:%d?discport=%d", url, nodeIp, tcpPort.getAsInt(), udpPort); + } else { + return String.format("enode://%s@%s:%d", url, nodeIp, udpPort); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelist.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelist.java new file mode 100644 index 0000000000..5defdaa0dc --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelist.java @@ -0,0 +1,80 @@ +/* + * 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.parameters.JsonRpcParameter; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.StringListParameter; +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.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; + +import java.util.List; +import java.util.stream.Collectors; + +public class PermRemoveNodesFromWhitelist implements JsonRpcMethod { + + private final P2PNetwork p2pNetwork; + private final JsonRpcParameter parameters; + + public PermRemoveNodesFromWhitelist( + final P2PNetwork p2pNetwork, final JsonRpcParameter parameters) { + this.p2pNetwork = p2pNetwork; + this.parameters = parameters; + } + + @Override + public String getName() { + return "perm_removeNodesFromWhitelist"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + final StringListParameter enodeListParam = + parameters.required(req.getParams(), 0, StringListParameter.class); + + try { + List peers = + enodeListParam + .getStringList() + .parallelStream() + .map(this::parsePeer) + .collect(Collectors.toList()); + + NodeWhitelistController.NodesWhitelistResult nodesWhitelistResult = + p2pNetwork.getNodeWhitelistController().removeNodes(peers); + + switch (nodesWhitelistResult.result()) { + case SUCCESS: + return new JsonRpcSuccessResponse(req.getId(), true); + case REMOVE_ERROR_ABSENT_ENTRY: + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NODE_WHITELIST_MISSING_ENTRY); + default: + throw new Exception(); + } + } catch (IllegalArgumentException e) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + } catch (Exception e) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.INTERNAL_ERROR); + } + } + + private DefaultPeer parsePeer(final String enodeURI) { + return DefaultPeer.fromURI(enodeURI); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/parameters/StringListParameter.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/parameters/StringListParameter.java new file mode 100644 index 0000000000..4b1f3fd78a --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/parameters/StringListParameter.java @@ -0,0 +1,34 @@ +/* + * 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.parameters; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class StringListParameter { + + private final List stringList = new ArrayList<>(); + + @JsonCreator + public StringListParameter(final List strings) { + if (strings != null) { + stringList.addAll(strings); + } + } + + public List getStringList() { + return stringList; + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java index 6753eab643..cfcff68fc1 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java @@ -52,7 +52,13 @@ public enum JsonRpcError { ACCOUNT_WHITELIST_NOT_SET(-32000, "Account whitelist hasn't been set"), ACCOUNT_WHITELIST_DUPLICATED_ENTRY(-32000, "Account whitelist can't contain duplicated entries"), ACCOUNT_WHITELIST_ABSENT_ENTRY(-32000, "Can't remove absent account from whitelist"), - ACCOUNT_WHITELIST_INVALID_ENTRY(-32000, "Can't add invalid account address to the whitelist"); + ACCOUNT_WHITELIST_INVALID_ENTRY(-32000, "Can't add invalid account address to the whitelist"), + + // Node whitelist errors + NODE_WHITELIST_NOT_SET(-32000, "Node whitelist has not been set"), + NODE_WHITELIST_DUPLICATED_ENTRY(-32000, "Node whitelist cannot contain duplicated node entries"), + NODE_WHITELIST_MISSING_ENTRY(-32000, "Node whitelist does not contain a specified node"), + NODE_WHITELIST_INVALID_ENTRY(-32000, "Unable to add invalid node to the node whitelist"); private final int code; private final String message; diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelistTest.java new file mode 100644 index 0000000000..723651db6b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddNodesToWhitelistTest.java @@ -0,0 +1,139 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.NodesWhitelistResultType; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +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.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; + +import java.util.List; + +import org.assertj.core.util.Lists; +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 PermAddNodesToWhitelistTest { + + private static final boolean JSON_SUCCESS = true; + private PermAddNodesToWhitelist method; + private static final String METHOD_NAME = "perm_addNodesToWhitelist"; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode3 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String badEnode = "enod://dog@cat:fish"; + + @Mock private P2PNetwork p2pNetwork; + @Mock private NodeWhitelistController nodeWhitelistController; + + private JsonRpcParameter params = new JsonRpcParameter(); + + @Before + public void setUp() { + method = new PermAddNodesToWhitelist(p2pNetwork, params); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo(METHOD_NAME); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersExceptionWhenOnlyBadEnode() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(badEnode)); + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersExceptionWhenBadEnodeInList() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(enode2, badEnode, enode1)); + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersExceptionWhenNoEnode() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList("")); + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldAddSingleValidNode() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(enode1)); + final JsonRpcResponse expected = new JsonRpcSuccessResponse(request.getId(), JSON_SUCCESS); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.addNodes(any())) + .thenReturn(new NodesWhitelistResult(NodesWhitelistResultType.SUCCESS)); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).addNodes(any()); + verifyNoMoreInteractions(nodeWhitelistController); + } + + @Test + public void shouldAddMultipleValidNodes() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(enode1, enode2, enode3)); + final JsonRpcResponse expected = new JsonRpcSuccessResponse(request.getId(), JSON_SUCCESS); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.addNodes(any())) + .thenReturn(new NodesWhitelistResult(NodesWhitelistResultType.SUCCESS)); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).addNodes(any()); + verifyNoMoreInteractions(nodeWhitelistController); + } + + private JsonRpcRequest buildRequest(final List enodeList) { + return new JsonRpcRequest("2.0", METHOD_NAME, new Object[] {enodeList}); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelistTest.java new file mode 100644 index 0000000000..51e1f7940f --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetNodesWhitelistTest.java @@ -0,0 +1,130 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +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.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; +import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.assertj.core.util.Lists; +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 PermGetNodesWhitelistTest { + + private PermGetNodesWhitelist method; + private static final String METHOD_NAME = "perm_getNodesWhitelist"; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.11:4567"; + private final String enode3 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.12:4567"; + + @Mock private P2PNetwork p2pNetwork; + @Mock private NodeWhitelistController nodeWhitelistController; + + @Before + public void setUp() { + method = new PermGetNodesWhitelist(p2pNetwork); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo(METHOD_NAME); + } + + @Test + public void shouldReturnSuccessResponseWhenListPopulated() { + final JsonRpcRequest request = buildRequest(); + final JsonRpcResponse expected = + new JsonRpcSuccessResponse(request.getId(), Lists.newArrayList(enode1, enode2, enode3)); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.nodeWhitelistSet()).thenReturn(true); + when(nodeWhitelistController.getNodesWhitelist()) + .thenReturn(buildNodesList(enode1, enode2, enode3)); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).nodeWhitelistSet(); + verify(nodeWhitelistController, times(1)).getNodesWhitelist(); + verifyNoMoreInteractions(nodeWhitelistController); + } + + @Test + public void shouldReturnSuccessResponseWhenListSetAndEmpty() { + final JsonRpcRequest request = buildRequest(); + final JsonRpcResponse expected = new JsonRpcSuccessResponse(request.getId(), Lists.emptyList()); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.nodeWhitelistSet()).thenReturn(true); + when(nodeWhitelistController.getNodesWhitelist()).thenReturn(buildNodesList()); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).nodeWhitelistSet(); + verify(nodeWhitelistController, times(1)).getNodesWhitelist(); + verifyNoMoreInteractions(nodeWhitelistController); + } + + @Test + public void shouldReturnFailResponseWhenListNotSet() { + final JsonRpcRequest request = buildRequest(); + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.NODE_WHITELIST_NOT_SET); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.nodeWhitelistSet()).thenReturn(false); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).nodeWhitelistSet(); + verifyNoMoreInteractions(nodeWhitelistController); + } + + private JsonRpcRequest buildRequest() { + return new JsonRpcRequest("2.0", METHOD_NAME, new Object[] {}); + } + + private List buildNodesList(final String... enodes) { + return Arrays.stream(enodes).parallel().map(DefaultPeer::fromURI).collect(Collectors.toList()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelistTest.java new file mode 100644 index 0000000000..c0f78e6dae --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveNodesFromWhitelistTest.java @@ -0,0 +1,129 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.NodesWhitelistResultType; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +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.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; + +import java.util.List; + +import org.assertj.core.util.Lists; +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 PermRemoveNodesFromWhitelistTest { + + private static final boolean SUCCESS_RESULT = true; + private PermRemoveNodesFromWhitelist method; + private static final String METHOD_NAME = "perm_removeNodesFromWhitelist"; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode3 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String badEnode = "enod://dog@cat:fish"; + + @Mock private P2PNetwork p2pNetwork; + @Mock NodeWhitelistController nodeWhitelistController; + + private JsonRpcParameter params = new JsonRpcParameter(); + + @Before + public void setUp() { + method = new PermRemoveNodesFromWhitelist(p2pNetwork, params); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo(METHOD_NAME); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersExceptionWhenBadEnode() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(badEnode)); + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersExceptionWhenNoEnode() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList("")); + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.NODE_WHITELIST_INVALID_ENTRY); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldRemoveSingleValidNode() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(enode1)); + final JsonRpcResponse expected = new JsonRpcSuccessResponse(request.getId(), SUCCESS_RESULT); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.removeNodes(any())) + .thenReturn(new NodesWhitelistResult(NodesWhitelistResultType.SUCCESS)); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).removeNodes(any()); + verifyNoMoreInteractions(nodeWhitelistController); + } + + @Test + public void shouldRemoveMultipleValidNodes() { + final JsonRpcRequest request = buildRequest(Lists.newArrayList(enode1, enode2, enode3)); + final JsonRpcResponse expected = new JsonRpcSuccessResponse(request.getId(), SUCCESS_RESULT); + + when(p2pNetwork.getNodeWhitelistController()).thenReturn(nodeWhitelistController); + when(nodeWhitelistController.removeNodes(any())) + .thenReturn(new NodesWhitelistResult(NodesWhitelistResultType.SUCCESS)); + + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + + verify(nodeWhitelistController, times(1)).removeNodes(any()); + verifyNoMoreInteractions(nodeWhitelistController); + } + + private JsonRpcRequest buildRequest(final List enodeList) { + return new JsonRpcRequest("2.0", METHOD_NAME, new Object[] {enodeList}); + } +} diff --git a/ethereum/mock-p2p/build.gradle b/ethereum/mock-p2p/build.gradle index 61bd4a5215..b0003a5316 100644 --- a/ethereum/mock-p2p/build.gradle +++ b/ethereum/mock-p2p/build.gradle @@ -27,6 +27,7 @@ jar { dependencies { implementation project(':ethereum:p2p') + implementation project(':ethereum:permissioning') implementation project(':util') implementation 'io.vertx:vertx-core' diff --git a/ethereum/mock-p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/testing/MockNetwork.java b/ethereum/mock-p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/testing/MockNetwork.java index f1d939843b..d7469d4967 100644 --- a/ethereum/mock-p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/testing/MockNetwork.java +++ b/ethereum/mock-p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/testing/MockNetwork.java @@ -18,10 +18,12 @@ import tech.pegasys.pantheon.ethereum.p2p.api.MessageData; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.api.PeerConnection; import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.DefaultMessage; import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; import tech.pegasys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.util.Subscribers; import java.net.InetSocketAddress; @@ -187,6 +189,11 @@ public final class MockNetwork { public boolean isListening() { return true; } + + @Override + public NodeWhitelistController getNodeWhitelistController() { + return new NodeWhitelistController(PermissioningConfiguration.createDefault()); + } } /** diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/api/P2PNetwork.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/api/P2PNetwork.java index 152786ae28..ddf5671217 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/api/P2PNetwork.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/api/P2PNetwork.java @@ -13,6 +13,7 @@ package tech.pegasys.pantheon.ethereum.p2p.api; import tech.pegasys.pantheon.ethereum.p2p.peers.Peer; +import tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.PeerInfo; @@ -85,4 +86,6 @@ public interface P2PNetwork extends Closeable, Runnable { * @return true if the node is listening for network connections, false, otherwise. */ boolean isListening(); + + NodeWhitelistController getNodeWhitelistController(); } diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java index 49c6491c17..6c3d194ab7 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java @@ -152,7 +152,7 @@ public class PeerDiscoveryController { bootstrapNodes .stream() .filter(node -> peerTable.tryAdd(node).getOutcome() == Outcome.ADDED) - .filter(node -> nodeWhitelist.contains(node)) + .filter(node -> nodeWhitelist.isPermitted(node)) .forEach(node -> bond(node, true)); final long timerId = @@ -200,7 +200,7 @@ public class PeerDiscoveryController { return; } - if (!nodeWhitelist.contains(sender)) { + if (!nodeWhitelist.isPermitted(sender)) { return; } @@ -247,7 +247,7 @@ public class PeerDiscoveryController { .orElse(emptyList()); for (final DiscoveryPeer neighbor : neighbors) { - if (!nodeWhitelist.contains(neighbor) + if (!nodeWhitelist.isPermitted(neighbor) || peerBlacklist.contains(neighbor) || peerTable.get(neighbor).isPresent()) { continue; diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java index e4f2acc982..27a79635c9 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java @@ -142,6 +142,8 @@ public final class NettyP2PNetwork implements P2PNetwork { private final LabelledMetric outboundMessagesCounter; + private final NodeWhitelistController nodeWhitelistController; + /** * Creates a peer networking service for production purposes. * @@ -170,6 +172,7 @@ public final class NettyP2PNetwork implements P2PNetwork { connections = new PeerConnectionRegistry(metricsSystem); this.peerBlacklist = peerBlacklist; + this.nodeWhitelistController = nodeWhitelistController; peerDiscoveryAgent = new PeerDiscoveryAgent( vertx, @@ -441,6 +444,11 @@ public final class NettyP2PNetwork implements P2PNetwork { return peerDiscoveryAgent.isActive(); } + @Override + public NodeWhitelistController getNodeWhitelistController() { + return nodeWhitelistController; + } + private void onConnectionEstablished(final PeerConnection connection) { connections.registerConnection(connection); connectCallbacks.forEach(callback -> callback.accept(connection)); diff --git a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistController.java b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistController.java index 783700a4a2..d255b55f06 100644 --- a/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistController.java +++ b/ethereum/p2p/src/main/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistController.java @@ -19,17 +19,20 @@ import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + +import com.google.common.annotations.VisibleForTesting; public class NodeWhitelistController { - private final List nodeWhitelist; + private final List nodesWhitelist; private boolean nodeWhitelistSet = false; public NodeWhitelistController(final PermissioningConfiguration configuration) { - nodeWhitelist = new ArrayList<>(); + nodesWhitelist = new ArrayList<>(); if (configuration != null && configuration.getNodeWhitelist() != null) { for (URI uri : configuration.getNodeWhitelist()) { - nodeWhitelist.add(DefaultPeer.fromURI(uri)); + nodesWhitelist.add(DefaultPeer.fromURI(uri)); } if (configuration.isNodeWhitelistSet()) { nodeWhitelistSet = true; @@ -39,14 +42,76 @@ public class NodeWhitelistController { public boolean addNode(final Peer node) { nodeWhitelistSet = true; - return nodeWhitelist.add(node); + return nodesWhitelist.add(node); + } + + private boolean removeNode(final Peer node) { + return nodesWhitelist.remove(node); + } + + public NodesWhitelistResult addNodes(final List peers) { + for (DefaultPeer peer : peers) { + if (nodesWhitelist.contains(peer)) { + return new NodesWhitelistResult( + NodesWhitelistResultType.ADD_ERROR_DUPLICATED_ENTRY, + String.format("Specified peer: %s already exists in whitelist.", peer.getId())); + } + } + peers.forEach(this::addNode); + return new NodesWhitelistResult(NodesWhitelistResultType.SUCCESS); + } + + public NodesWhitelistResult removeNodes(final List peers) { + for (DefaultPeer peer : peers) { + if (!(nodesWhitelist.contains(peer))) { + return new NodesWhitelistResult( + NodesWhitelistResultType.REMOVE_ERROR_ABSENT_ENTRY, + String.format("Specified peer: %s does not exist in whitelist.", peer.getId())); + } + } + peers.forEach(this::removeNode); + return new NodesWhitelistResult(NodesWhitelistResultType.SUCCESS); + } + + public boolean isPermitted(final Peer node) { + return (!nodeWhitelistSet || (nodeWhitelistSet && nodesWhitelist.contains(node))); } - public boolean removeNode(final Peer node) { - return nodeWhitelist.remove(node); + public List getNodesWhitelist() { + return nodesWhitelist; + } + + public boolean nodeWhitelistSet() { + return nodeWhitelistSet; + } + + public static class NodesWhitelistResult { + private final NodesWhitelistResultType result; + private final Optional message; + + NodesWhitelistResult(final NodesWhitelistResultType fail, final String message) { + this.result = fail; + this.message = Optional.of(message); + } + + @VisibleForTesting + public NodesWhitelistResult(final NodesWhitelistResultType success) { + this.result = success; + this.message = Optional.empty(); + } + + public NodesWhitelistResultType result() { + return result; + } + + public Optional message() { + return message; + } } - public boolean contains(final Peer node) { - return (!nodeWhitelistSet || (nodeWhitelistSet && nodeWhitelist.contains(node))); + public enum NodesWhitelistResultType { + SUCCESS, + ADD_ERROR_DUPLICATED_ENTRY, + REMOVE_ERROR_ABSENT_ENTRY } } diff --git a/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistControllerTest.java b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistControllerTest.java new file mode 100644 index 0000000000..43a790b19a --- /dev/null +++ b/ethereum/p2p/src/test/java/tech/pegasys/pantheon/ethereum/p2p/permissioning/NodeWhitelistControllerTest.java @@ -0,0 +1,66 @@ +/* + * 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.p2p.permissioning; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController.NodesWhitelistResult; +import static tech.pegasys.pantheon.ethereum.p2p.permissioning.NodeWhitelistController.NodesWhitelistResultType; + +import tech.pegasys.pantheon.ethereum.p2p.peers.DefaultPeer; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NodeWhitelistControllerTest { + + private NodeWhitelistController controller; + + private final String enode1 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private final String enode2 = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + + @Before + public void setUp() { + controller = new NodeWhitelistController(new PermissioningConfiguration()); + } + + @Test + public void shouldNotAddDuplicateNodes() { + controller.addNode(DefaultPeer.fromURI(enode1)); + + NodesWhitelistResult expected = + new NodesWhitelistResult(NodesWhitelistResultType.ADD_ERROR_DUPLICATED_ENTRY); + NodesWhitelistResult actualResult = + controller.addNodes( + Lists.newArrayList(DefaultPeer.fromURI(enode1), DefaultPeer.fromURI(enode2))); + + assertThat(actualResult).isEqualToComparingOnlyGivenFields(expected, "result"); + } + + @Test + public void shouldNotRemoveNodesThatDoNotExist() { + NodesWhitelistResult expected = + new NodesWhitelistResult(NodesWhitelistResultType.REMOVE_ERROR_ABSENT_ENTRY); + NodesWhitelistResult actualResult = + controller.removeNodes( + Lists.newArrayList(DefaultPeer.fromURI(enode1), DefaultPeer.fromURI(enode2))); + + assertThat(actualResult).isEqualToComparingOnlyGivenFields(expected, "result"); + } +}