From 0a8f6cbddfbc504d9b047d847c1cf0f171a87027 Mon Sep 17 00:00:00 2001 From: Lucas Saldanha Date: Tue, 8 Jan 2019 16:37:41 +1300 Subject: [PATCH] Acc whitelist api (#487) * Implemented list/add/remove accounts from whitelist * Including account whitelist methods in the JSON-RPC API * Fixing json rpc response for eth_sendrawTransaction with account not authorized * Refactoring TransactionPool account whitelist logic * Acceptance test for accounts whitelist * Errorprone * Fixed account nonce tracking in ATs --- .../acceptance/dsl/AcceptanceTestBase.java | 3 + .../tests/acceptance/dsl/account/Account.java | 8 +- .../ExpectEthSendRawTransactionException.java | 41 +++++ .../AddAccountsToWhitelistSuccessfully.java | 35 +++++ .../perm/GetExpectedAccountsWhitelist.java | 39 +++++ ...moveAccountsFromWhitelistSuccessfully.java | 35 +++++ .../tests/acceptance/dsl/jsonrpc/Eth.java | 7 + .../tests/acceptance/dsl/jsonrpc/Perm.java | 44 ++++++ .../dsl/node/ProcessPantheonNodeRunner.java | 4 + .../dsl/node/factory/PantheonNodeFactory.java | 24 +++ .../dsl/transaction/PantheonWeb3j.java | 30 ++++ .../dsl/transaction/Transactions.java | 28 ++++ .../account/TransferTransaction.java | 38 +++-- .../EthGetTransactionCountTransaction.java | 45 ++++++ .../eth/EthSendRawTransactionTransaction.java | 42 ++++++ .../dsl/transaction/eth/EthTransactions.java | 5 + ...PermAddAccountsToWhitelistTransaction.java | 42 ++++++ .../PermGetAccountsWhitelistTransaction.java | 36 +++++ ...emoveAccountsFromWhitelistTransaction.java | 43 ++++++ .../AccountsWhitelistAcceptanceTest.java | 79 ++++++++++ .../ethereum/core/TransactionPool.java | 14 +- .../ethereum/core/TransactionPoolTest.java | 1 - ethereum/jsonrpc/build.gradle | 1 + .../jsonrpc/JsonRpcTestMethodsFactory.java | 4 + .../jsonrpc/JsonRpcErrorConverter.java | 2 + .../jsonrpc/JsonRpcMethodsFactory.java | 16 +- .../pantheon/ethereum/jsonrpc/RpcApis.java | 3 + .../PermAddAccountsToWhitelist.java | 62 ++++++++ .../PermGetAccountsWhitelist.java | 44 ++++++ .../PermRemoveAccountsFromWhitelist.java | 62 ++++++++ .../internal/response/JsonRpcError.java | 9 +- .../AbstractEthJsonRpcHttpServiceTest.java | 6 + .../JsonRpcHttpServiceHostWhitelistTest.java | 2 + .../JsonRpcHttpServiceRpcApisTest.java | 2 + .../jsonrpc/JsonRpcHttpServiceTest.java | 2 + .../methods/EthSendRawTransactionTest.java | 6 + .../PermAddAccountsToWhitelistTest.java | 105 +++++++++++++ .../PermGetAccountsWhitelistTest.java | 88 +++++++++++ .../PermRemoveAccountsFromWhitelistTest.java | 105 +++++++++++++ ethereum/permissioning/build.gradle | 1 + .../AccountWhitelistController.java | 79 +++++++--- .../AccountWhitelistControllerTest.java | 142 ++++++++++++++++++ .../tech/pegasys/pantheon/RunnerBuilder.java | 16 +- 43 files changed, 1349 insertions(+), 51 deletions(-) create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/eth/ExpectEthSendRawTransactionException.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddAccountsToWhitelistSuccessfully.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetExpectedAccountsWhitelist.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveAccountsFromWhitelistSuccessfully.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Perm.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthGetTransactionCountTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthSendRawTransactionTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddAccountsToWhitelistTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetAccountsWhitelistTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveAccountsFromWhitelistTransaction.java create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/permissioning/AccountsWhitelistAcceptanceTest.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelist.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelist.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelist.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelistTest.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelistTest.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelistTest.java create mode 100644 ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistControllerTest.java diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java index 46751edfb3..9634b1e000 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java @@ -18,6 +18,7 @@ import tech.pegasys.pantheon.tests.acceptance.dsl.condition.clique.CliqueConditi import tech.pegasys.pantheon.tests.acceptance.dsl.contract.ContractVerifier; import tech.pegasys.pantheon.tests.acceptance.dsl.jsonrpc.Eth; import tech.pegasys.pantheon.tests.acceptance.dsl.jsonrpc.Net; +import tech.pegasys.pantheon.tests.acceptance.dsl.jsonrpc.Perm; import tech.pegasys.pantheon.tests.acceptance.dsl.jsonrpc.Web3; import tech.pegasys.pantheon.tests.acceptance.dsl.node.cluster.Cluster; import tech.pegasys.pantheon.tests.acceptance.dsl.node.factory.PantheonNodeFactory; @@ -41,6 +42,7 @@ public class AcceptanceTestBase { protected final Web3 web3; protected final Eth eth; protected final Net net; + protected final Perm perm; protected final PantheonNodeFactory pantheon; protected final ContractVerifier contractVerifier; protected final WaitConditions wait; @@ -55,6 +57,7 @@ public class AcceptanceTestBase { net = new Net(new NetTransactions()); cluster = new Cluster(net); transactions = new Transactions(accounts); + perm = new Perm(transactions); web3 = new Web3(new Web3Transactions()); pantheon = new PantheonNodeFactory(); contractVerifier = new ContractVerifier(accounts.getPrimaryBenefactor()); diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/account/Account.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/account/Account.java index b06744d5c8..1fb54b020e 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/account/Account.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/account/Account.java @@ -54,14 +54,14 @@ public class Account { keyPair.getPrivateKey().toString(), keyPair.getPublicKey().toString()); } - public String getAddress() { - return Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes())).toString(); - } - public BigInteger getNextNonce() { return BigInteger.valueOf(nonce++); } + public String getAddress() { + return Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes())).toString(); + } + public Condition balanceEquals(final String expectedBalance, final Unit balanceUnit) { return new ExpectAccountBalance(eth, this, expectedBalance, balanceUnit); } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/eth/ExpectEthSendRawTransactionException.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/eth/ExpectEthSendRawTransactionException.java new file mode 100644 index 0000000000..7cb5a0df5f --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/eth/ExpectEthSendRawTransactionException.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.condition.eth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +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.eth.EthSendRawTransactionTransaction; + +import org.web3j.protocol.exceptions.ClientConnectionException; + +public class ExpectEthSendRawTransactionException implements Condition { + + private final EthSendRawTransactionTransaction transaction; + private final String expectedMessage; + + public ExpectEthSendRawTransactionException( + final EthSendRawTransactionTransaction transaction, final String expectedMessage) { + this.transaction = transaction; + this.expectedMessage = expectedMessage; + } + + @Override + public void verify(final Node node) { + final Throwable thrown = catchThrowable(() -> node.execute(transaction)); + assertThat(thrown).isInstanceOf(ClientConnectionException.class); + assertThat(thrown.getMessage()).contains(expectedMessage); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddAccountsToWhitelistSuccessfully.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddAccountsToWhitelistSuccessfully.java new file mode 100644 index 0000000000..6e7252e194 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/AddAccountsToWhitelistSuccessfully.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.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.PermAddAccountsToWhitelistTransaction; + +public class AddAccountsToWhitelistSuccessfully implements Condition { + + private final PermAddAccountsToWhitelistTransaction transaction; + + public AddAccountsToWhitelistSuccessfully( + final PermAddAccountsToWhitelistTransaction transaction) { + this.transaction = transaction; + } + + @Override + public void verify(final Node node) { + Boolean result = node.execute(transaction); + assertThat(result).isTrue(); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetExpectedAccountsWhitelist.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetExpectedAccountsWhitelist.java new file mode 100644 index 0000000000..aaa5e2e7c1 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/GetExpectedAccountsWhitelist.java @@ -0,0 +1,39 @@ +/* + * 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.PermGetAccountsWhitelistTransaction; + +import java.util.List; + +public class GetExpectedAccountsWhitelist implements Condition { + + private final PermGetAccountsWhitelistTransaction transaction; + private final List expectedList; + + public GetExpectedAccountsWhitelist( + final PermGetAccountsWhitelistTransaction transaction, final List expectedList) { + this.transaction = transaction; + this.expectedList = expectedList; + } + + @Override + public void verify(final Node node) { + List accounts = node.execute(transaction); + assertThat(accounts).containsExactlyInAnyOrder(expectedList.toArray(new String[] {})); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveAccountsFromWhitelistSuccessfully.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveAccountsFromWhitelistSuccessfully.java new file mode 100644 index 0000000000..6001f2ac56 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/condition/perm/RemoveAccountsFromWhitelistSuccessfully.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.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.PermRemoveAccountsFromWhitelistTransaction; + +public class RemoveAccountsFromWhitelistSuccessfully implements Condition { + + private final PermRemoveAccountsFromWhitelistTransaction transaction; + + public RemoveAccountsFromWhitelistSuccessfully( + final PermRemoveAccountsFromWhitelistTransaction transaction) { + this.transaction = transaction; + } + + @Override + public void verify(final Node node) { + Boolean result = node.execute(transaction); + assertThat(result).isTrue(); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Eth.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Eth.java index 10ee67b239..01a85e70dd 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Eth.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Eth.java @@ -17,6 +17,7 @@ import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.eth.ExpectEthAccountsException; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.eth.ExpectEthGetTransactionReceiptIsAbsent; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.eth.ExpectEthGetWorkException; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.eth.ExpectEthSendRawTransactionException; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.eth.ExpectSuccessfulEthGetTransactionReceipt; import tech.pegasys.pantheon.tests.acceptance.dsl.condition.eth.SanityCheckEthGetWorkValues; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.eth.EthTransactions; @@ -50,4 +51,10 @@ public class Eth { return new ExpectEthGetTransactionReceiptIsAbsent( transactions.getTransactionReceipt(transactionHash)); } + + public Condition sendRawTransactionExceptional( + final String transactionData, final String expectedMessage) { + return new ExpectEthSendRawTransactionException( + transactions.sendRawTransactionTransaction(transactionData), expectedMessage); + } } 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 new file mode 100644 index 0000000000..90a25d56e6 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/jsonrpc/Perm.java @@ -0,0 +1,44 @@ +/* + * 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.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.GetExpectedAccountsWhitelist; +import tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm.RemoveAccountsFromWhitelistSuccessfully; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transactions; + +import java.util.Arrays; + +public class Perm { + + public Perm(final Transactions transactions) { + this.transactions = transactions; + } + + private final Transactions transactions; + + public Condition addAccountsToWhitelist(final String... accounts) { + return new AddAccountsToWhitelistSuccessfully(transactions.addAccountsToWhitelist(accounts)); + } + + public Condition removeAccountsFromWhitelist(final String... accounts) { + return new RemoveAccountsFromWhitelistSuccessfully( + transactions.removeAccountsFromWhitelist(accounts)); + } + + public Condition expectAccountsWhitelist(final String... expectedAccounts) { + return new GetExpectedAccountsWhitelist( + transactions.getAccountsWhiteList(), Arrays.asList(expectedAccounts)); + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java index fa9dc38b6e..ec5aa33a80 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java @@ -76,6 +76,10 @@ public class ProcessPantheonNodeRunner implements PantheonNodeRunner { params.add("--nodes-whitelist"); params.add(String.join(",", permissioningConfiguration.getNodeWhitelist().toString())); } + if (permissioningConfiguration.isAccountWhitelistSet()) { + params.add("--accounts-whitelist"); + params.add(String.join(",", permissioningConfiguration.getAccountWhitelist().toString())); + } if (node.jsonRpcEnabled()) { params.add("--rpc-enabled"); 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 a5cef58fad..ebc0b46a71 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 @@ -21,6 +21,7 @@ import tech.pegasys.pantheon.consensus.clique.CliqueExtraData; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi; +import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode; @@ -138,6 +139,21 @@ public class PantheonNodeFactory { .build()); } + public PantheonNode createNodeWithAccountsWhitelist( + final String name, final List accountsWhitelist) throws IOException { + PermissioningConfiguration permissioningConfiguration = + PermissioningConfiguration.createDefault(); + permissioningConfiguration.setAccountWhitelist(accountsWhitelist); + + return create( + new PantheonFactoryConfigurationBuilder() + .setName(name) + .miningEnabled() + .setJsonRpcConfiguration(jsonRpcConfigWithPermissioning()) + .setPermissioningConfiguration(permissioningConfiguration) + .build()); + } + public PantheonNode createCliqueNode(final String name) throws IOException { return create( new PantheonFactoryConfigurationBuilder() @@ -215,4 +231,12 @@ public class PantheonNodeFactory { config.setPort(0); return config; } + + private JsonRpcConfiguration jsonRpcConfigWithPermissioning() { + final JsonRpcConfiguration jsonRpcConfig = createJsonRpcEnabledConfig(); + final List rpcApis = new ArrayList<>(jsonRpcConfig.getRpcApis()); + rpcApis.add(RpcApis.PERM); + jsonRpcConfig.setRpcApis(rpcApis); + return jsonRpcConfig; + } } 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 387ffa1e70..c0a285804f 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 @@ -19,6 +19,7 @@ import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Hash; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -78,4 +79,33 @@ public class PantheonWeb3j extends JsonRpc2_0Web3j { public static class SignersBlockResponse extends Response> {} public static class ProposalsResponse extends Response> {} + + public Request addAccountsToWhitelist( + final List accounts) { + return new Request<>( + "perm_addAccountsToWhitelist", + Collections.singletonList(accounts), + web3jService, + AddAccountsToWhitelistResponse.class); + } + + public Request removeAccountsFromWhitelist( + final List accounts) { + return new Request<>( + "perm_removeAccountsFromWhitelist", + Collections.singletonList(accounts), + web3jService, + RemoveAccountsFromWhitelistResponse.class); + } + + public Request getAccountsWhitelist() { + return new Request<>( + "perm_getAccountsWhitelist", null, web3jService, GetAccountsWhitelistResponse.class); + } + + public static class AddAccountsToWhitelistResponse extends Response {} + + public static class RemoveAccountsFromWhitelistResponse extends Response {} + + public static class GetAccountsWhitelistResponse 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 7ce0457f32..853408532d 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 @@ -16,8 +16,14 @@ import tech.pegasys.pantheon.tests.acceptance.dsl.account.Account; import tech.pegasys.pantheon.tests.acceptance.dsl.account.Accounts; import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.account.TransferTransaction; 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.PermGetAccountsWhitelistTransaction; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.perm.PermRemoveAccountsFromWhitelistTransaction; +import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.web3j.tx.Contract; @@ -40,6 +46,11 @@ public class Transactions { return new TransferTransaction(sender, recipient, String.valueOf(amount), Unit.ETHER); } + public TransferTransaction createTransfer( + final Account sender, final Account recipient, final int amount, final BigInteger nonce) { + return new TransferTransaction(sender, recipient, String.valueOf(amount), Unit.ETHER, nonce); + } + public TransferTransactionSet createIncrementalTransfers( final Account sender, final Account recipient, final int etherAmount) { final List transfers = new ArrayList<>(); @@ -55,4 +66,21 @@ public class Transactions { final Class clazz) { return new DeploySmartContractTransaction<>(clazz); } + + public PermAddAccountsToWhitelistTransaction addAccountsToWhitelist(final String... accounts) { + return new PermAddAccountsToWhitelistTransaction(Arrays.asList(accounts)); + } + + public PermRemoveAccountsFromWhitelistTransaction removeAccountsFromWhitelist( + final String... accounts) { + return new PermRemoveAccountsFromWhitelistTransaction(Arrays.asList(accounts)); + } + + public PermGetAccountsWhitelistTransaction getAccountsWhiteList() { + return new PermGetAccountsWhitelistTransaction(); + } + + public EthGetTransactionCountTransaction getTransactionCount(final String accountAddress) { + return new EthGetTransactionCountTransaction(accountAddress); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/account/TransferTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/account/TransferTransaction.java index 037cf9888f..88e04a5211 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/account/TransferTransaction.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/account/TransferTransaction.java @@ -21,6 +21,7 @@ import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; import java.io.IOException; import java.math.BigInteger; +import java.util.Optional; import org.web3j.crypto.RawTransaction; import org.web3j.crypto.TransactionEncoder; @@ -36,26 +37,33 @@ public class TransferTransaction implements Transaction { private final Account recipient; private final String amount; private final Unit unit; + private final Optional nonce; public TransferTransaction( final Account sender, final Account recipient, final String amount, final Unit unit) { + this(sender, recipient, amount, unit, null); + } + + public TransferTransaction( + final Account sender, + final Account recipient, + final String amount, + final Unit unit, + final BigInteger nonce) { this.sender = sender; this.recipient = recipient; this.amount = amount; this.unit = unit; + if (nonce != null) { + this.nonce = Optional.of(nonce); + } else { + this.nonce = Optional.empty(); + } } @Override public Hash execute(final PantheonWeb3j node) { - final RawTransaction transaction = - RawTransaction.createEtherTransaction( - sender.getNextNonce(), - MINIMUM_GAS_PRICE, - TRANSFER_GAS_COST, - recipient.getAddress(), - Convert.toWei(amount, unit).toBigIntegerExact()); - final String signedTransactionData = - toHexString(TransactionEncoder.signMessage(transaction, sender.web3jCredentials())); + final String signedTransactionData = signedTransactionData(); try { return Hash.fromHexString( node.ethSendRawTransaction(signedTransactionData).send().getTransactionHash()); @@ -63,4 +71,16 @@ public class TransferTransaction implements Transaction { throw new RuntimeException(e); } } + + public String signedTransactionData() { + final RawTransaction transaction = + RawTransaction.createEtherTransaction( + nonce.orElse(nonce.orElseGet(sender::getNextNonce)), + MINIMUM_GAS_PRICE, + TRANSFER_GAS_COST, + recipient.getAddress(), + Convert.toWei(amount, unit).toBigIntegerExact()); + + return toHexString(TransactionEncoder.signMessage(transaction, sender.web3jCredentials())); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthGetTransactionCountTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthGetTransactionCountTransaction.java new file mode 100644 index 0000000000..ae31f9783f --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthGetTransactionCountTransaction.java @@ -0,0 +1,45 @@ +/* + * 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.eth; + +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.Transaction; + +import java.io.IOException; +import java.math.BigInteger; + +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.response.EthGetTransactionCount; + +public class EthGetTransactionCountTransaction implements Transaction { + + private final String accountAddress; + + public EthGetTransactionCountTransaction(final String accountAddress) { + this.accountAddress = accountAddress; + } + + @Override + public BigInteger execute(final PantheonWeb3j node) { + try { + EthGetTransactionCount result = + node.ethGetTransactionCount(accountAddress, DefaultBlockParameterName.LATEST).send(); + assertThat(result).isNotNull(); + return result.getTransactionCount(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthSendRawTransactionTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthSendRawTransactionTransaction.java new file mode 100644 index 0000000000..6a17dcd0cb --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthSendRawTransactionTransaction.java @@ -0,0 +1,42 @@ +/* + * 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.eth; + +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.Transaction; + +import java.io.IOException; + +import org.web3j.protocol.core.methods.response.EthSendTransaction; + +public class EthSendRawTransactionTransaction implements Transaction { + + private final String transactionData; + + EthSendRawTransactionTransaction(final String transactionData) { + this.transactionData = transactionData; + } + + @Override + public String execute(final PantheonWeb3j node) { + try { + EthSendTransaction response = node.ethSendRawTransaction(transactionData).send(); + assertThat(response.getTransactionHash()).isNotNull(); + return response.getTransactionHash(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthTransactions.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthTransactions.java index 0a40616a2e..a1f594612f 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthTransactions.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/eth/EthTransactions.java @@ -46,4 +46,9 @@ public class EthTransactions { public EthGetTransactionReceiptTransaction getTransactionReceipt(final String transactionHash) { return new EthGetTransactionReceiptTransaction(transactionHash); } + + public EthSendRawTransactionTransaction sendRawTransactionTransaction( + final String transactionData) { + return new EthSendRawTransactionTransaction(transactionData); + } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddAccountsToWhitelistTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddAccountsToWhitelistTransaction.java new file mode 100644 index 0000000000..d624a185bc --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermAddAccountsToWhitelistTransaction.java @@ -0,0 +1,42 @@ +/* + * 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.AddAccountsToWhitelistResponse; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; +import java.util.List; + +public class PermAddAccountsToWhitelistTransaction implements Transaction { + + private final List accounts; + + public PermAddAccountsToWhitelistTransaction(final List accounts) { + this.accounts = accounts; + } + + @Override + public Boolean execute(final PantheonWeb3j node) { + try { + AddAccountsToWhitelistResponse response = node.addAccountsToWhitelist(accounts).send(); + assertThat(response.getResult()).isTrue(); + return response.getResult(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetAccountsWhitelistTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetAccountsWhitelistTransaction.java new file mode 100644 index 0000000000..42ccf85aaa --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermGetAccountsWhitelistTransaction.java @@ -0,0 +1,36 @@ +/* + * 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.GetAccountsWhitelistResponse; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; +import java.util.List; + +public class PermGetAccountsWhitelistTransaction implements Transaction> { + + @Override + public List execute(final PantheonWeb3j node) { + try { + GetAccountsWhitelistResponse response = node.getAccountsWhitelist().send(); + assertThat(response.getResult()).isNotNull(); + return response.getResult(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveAccountsFromWhitelistTransaction.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveAccountsFromWhitelistTransaction.java new file mode 100644 index 0000000000..5c85e49f76 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/transaction/perm/PermRemoveAccountsFromWhitelistTransaction.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.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.RemoveAccountsFromWhitelistResponse; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.Transaction; + +import java.io.IOException; +import java.util.List; + +public class PermRemoveAccountsFromWhitelistTransaction implements Transaction { + + private final List accounts; + + public PermRemoveAccountsFromWhitelistTransaction(final List accounts) { + this.accounts = accounts; + } + + @Override + public Boolean execute(final PantheonWeb3j node) { + try { + RemoveAccountsFromWhitelistResponse response = + node.removeAccountsFromWhitelist(accounts).send(); + assertThat(response.getResult()).isTrue(); + return response.getResult(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/permissioning/AccountsWhitelistAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/permissioning/AccountsWhitelistAcceptanceTest.java new file mode 100644 index 0000000000..1dabcd365a --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/permissioning/AccountsWhitelistAcceptanceTest.java @@ -0,0 +1,79 @@ +/* + * 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.permissioning; + +import static org.web3j.utils.Convert.Unit.ETHER; + +import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.pantheon.tests.acceptance.dsl.account.Account; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; +import tech.pegasys.pantheon.tests.acceptance.dsl.transaction.account.TransferTransaction; + +import java.math.BigInteger; +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; + +public class AccountsWhitelistAcceptanceTest extends AcceptanceTestBase { + + private Node node; + private Account senderA; + private Account senderB; + + @Before + public void setUp() throws Exception { + senderA = accounts.getPrimaryBenefactor(); + senderB = accounts.getSecondaryBenefactor(); + + node = pantheon.createNodeWithAccountsWhitelist("node", Arrays.asList(senderA.getAddress())); + cluster.start(node); + } + + @Test + public void onlyAllowedAccountCanSubmitTransactions() { + Account beneficiary = accounts.createAccount("beneficiary"); + + node.execute(transactions.createTransfer(senderA, beneficiary, 1)); + node.verify(beneficiary.balanceEquals("1", ETHER)); + + verifyTransferForbidden(senderB, beneficiary); + } + + @Test + public void manipulatingAccountsWhitelistViaJsonRpc() { + Account beneficiary = accounts.createAccount("beneficiary"); + node.verify(beneficiary.balanceEquals("0", ETHER)); + + verifyTransferForbidden(senderB, beneficiary); + + node.execute(transactions.addAccountsToWhitelist(senderB.getAddress())); + node.verify(perm.expectAccountsWhitelist(senderA.getAddress(), senderB.getAddress())); + + node.execute(transactions.createTransfer(senderB, beneficiary, 1)); + node.verify(beneficiary.balanceEquals("1", ETHER)); + + node.execute(transactions.removeAccountsFromWhitelist(senderB.getAddress())); + node.verify(perm.expectAccountsWhitelist(senderA.getAddress())); + verifyTransferForbidden(senderB, beneficiary); + } + + private void verifyTransferForbidden(final Account sender, final Account beneficiary) { + BigInteger nonce = node.execute(transactions.getTransactionCount(sender.getAddress())); + TransferTransaction transfer = transactions.createTransfer(sender, beneficiary, 1, nonce); + node.verify( + eth.sendRawTransactionExceptional( + transfer.signedTransactionData(), + "Sender account not authorized to send transactions")); + } +} diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/TransactionPool.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/TransactionPool.java index 19c4b52c2e..7cb4bb2c93 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/TransactionPool.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/TransactionPool.java @@ -134,7 +134,7 @@ public class TransactionPool implements BlockAddedObserver { return basicValidationResult; } - String sender = transaction.getSender().toString(); + final String sender = transaction.getSender().toString(); if (accountIsNotWhitelisted(sender)) { return ValidationResult.invalid( TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED, @@ -158,17 +158,7 @@ public class TransactionPool implements BlockAddedObserver { } private boolean accountIsNotWhitelisted(final String account) { - if (useWhitelist()) { - if (!accountWhitelistController.get().contains(account)) { - return true; - } - } - return false; - } - - public boolean useWhitelist() { - return (accountWhitelistController.isPresent() - && accountWhitelistController.get().isAccountWhiteListSet()); + return accountWhitelistController.map(c -> !c.contains(account)).orElse(false); } private Account getSenderAccount( diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/TransactionPoolTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/TransactionPoolTest.java index 309f502e30..1f62456939 100644 --- a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/TransactionPoolTest.java +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/TransactionPoolTest.java @@ -391,7 +391,6 @@ public class TransactionPoolTest { @Test public void shouldAllowTransactionWhenAccountWhitelistControllerIsNotPresent() { givenTransactionIsValid(transaction1); - assertThat(transactionPool.useWhitelist()).isFalse(); assertThat(transactionPool.addLocalTransaction(transaction1)).isEqualTo(valid()); diff --git a/ethereum/jsonrpc/build.gradle b/ethereum/jsonrpc/build.gradle index 3c423c8ac4..e43ce6c06b 100644 --- a/ethereum/jsonrpc/build.gradle +++ b/ethereum/jsonrpc/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation project(':ethereum:eth') implementation project(':ethereum:p2p') implementation project(':ethereum:rlp') + implementation project(':ethereum:permissioning') implementation project(':metrics') implementation 'com.google.guava:guava' diff --git a/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java b/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java index 7b707bbf4e..1eb70a986a 100644 --- a/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java +++ b/ethereum/jsonrpc/src/integration-test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java @@ -35,6 +35,7 @@ import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSpec; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; @@ -78,6 +79,8 @@ public class JsonRpcTestMethodsFactory { blockchainQueries, transactionPool, new FilterIdGenerator(), new FilterRepository()); final EthHashMiningCoordinator miningCoordinator = mock(EthHashMiningCoordinator.class); final MetricsSystem metricsSystem = new NoOpMetricsSystem(); + final AccountWhitelistController accountWhitelistController = + mock(AccountWhitelistController.class); return new JsonRpcMethodsFactory() .methods( @@ -91,6 +94,7 @@ public class JsonRpcTestMethodsFactory { miningCoordinator, metricsSystem, new HashSet<>(), + accountWhitelistController, RpcApis.DEFAULT_JSON_RPC_APIS); } } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java index 316a383ecf..1c22505aa1 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java @@ -32,6 +32,8 @@ public class JsonRpcErrorConverter { return JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE; case EXCEEDS_BLOCK_GAS_LIMIT: return JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT; + case TX_SENDER_NOT_AUTHORIZED: + return JsonRpcError.TX_SENDER_NOT_AUTHORIZED; default: return JsonRpcError.INVALID_PARAMS; 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 4202165e2f..0ee49343fd 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 @@ -68,6 +68,9 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerSetCoi import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerSetEtherbase; 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.PermGetAccountsWhitelist; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.permissioning.PermRemoveAccountsFromWhitelist; 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; @@ -77,6 +80,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactor import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; import tech.pegasys.pantheon.metrics.MetricsSystem; import java.util.Collection; @@ -101,7 +105,8 @@ public class JsonRpcMethodsFactory { final MetricsSystem metricsSystem, final Set supportedCapabilities, final Collection rpcApis, - final FilterManager filterManager) { + final FilterManager filterManager, + final AccountWhitelistController accountsWhitelistController) { final BlockchainQueries blockchainQueries = new BlockchainQueries(blockchain, worldStateArchive); return methods( @@ -115,6 +120,7 @@ public class JsonRpcMethodsFactory { miningCoordinator, metricsSystem, supportedCapabilities, + accountsWhitelistController, rpcApis); } @@ -129,6 +135,7 @@ public class JsonRpcMethodsFactory { final MiningCoordinator miningCoordinator, final MetricsSystem metricsSystem, final Set supportedCapabilities, + final AccountWhitelistController accountsWhitelistController, final Collection rpcApis) { final Map enabledMethods = new HashMap<>(); // @formatter:off @@ -221,6 +228,13 @@ public class JsonRpcMethodsFactory { if (rpcApis.contains(RpcApis.ADMIN)) { addMethods(enabledMethods, new AdminPeers(p2pNetwork)); } + if (rpcApis.contains(RpcApis.PERM)) { + addMethods( + enabledMethods, + new PermGetAccountsWhitelist(accountsWhitelistController), + new PermAddAccountsToWhitelist(accountsWhitelistController, parameter), + new PermRemoveAccountsFromWhitelist(accountsWhitelistController, parameter)); + } // @formatter:off return enabledMethods; } 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 a29c05652f..fb33feb4fd 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 @@ -23,6 +23,7 @@ public class RpcApis { public static final RpcApi NET = new RpcApi("NET"); 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); @@ -39,6 +40,8 @@ public class RpcApis { return Optional.of(WEB3); } else if (name.equals(ADMIN.getCliValue())) { return Optional.of(ADMIN); + } else if (name.equals(PERM.getCliValue())) { + return Optional.of(PERM); } else { return Optional.empty(); } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelist.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelist.java new file mode 100644 index 0000000000..0bee4e8d40 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelist.java @@ -0,0 +1,62 @@ +/* + * 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.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.permissioning.AccountWhitelistController; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController.AddResult; + +import java.util.List; + +public class PermAddAccountsToWhitelist implements JsonRpcMethod { + + private final JsonRpcParameter parameters; + private final AccountWhitelistController whitelistController; + + public PermAddAccountsToWhitelist( + final AccountWhitelistController whitelistController, final JsonRpcParameter parameters) { + this.whitelistController = whitelistController; + this.parameters = parameters; + } + + @Override + public String getName() { + return "perm_addAccountsToWhitelist"; + } + + @Override + @SuppressWarnings("unchecked") + public JsonRpcResponse response(final JsonRpcRequest request) { + final List accountsList = parameters.required(request.getParams(), 0, List.class); + final AddResult addResult = whitelistController.addAccounts(accountsList); + + switch (addResult) { + case ERROR_INVALID_ENTRY: + return new JsonRpcErrorResponse( + request.getId(), JsonRpcError.ACCOUNT_WHITELIST_INVALID_ENTRY); + case ERROR_DUPLICATED_ENTRY: + return new JsonRpcErrorResponse( + request.getId(), JsonRpcError.ACCOUNT_WHITELIST_DUPLICATED_ENTRY); + case SUCCESS: + return new JsonRpcSuccessResponse(request.getId(), true); + default: + throw new IllegalStateException("Unmapped result from AccountWhitelistController"); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelist.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelist.java new file mode 100644 index 0000000000..07c822fb0d --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelist.java @@ -0,0 +1,44 @@ +/* + * 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.permissioning.AccountWhitelistController; + +public class PermGetAccountsWhitelist implements JsonRpcMethod { + + private final AccountWhitelistController whitelistController; + + public PermGetAccountsWhitelist(final AccountWhitelistController whitelistController) { + this.whitelistController = whitelistController; + } + + @Override + public String getName() { + return "perm_getAccountsWhitelist"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + if (!whitelistController.isAccountWhiteListSet()) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.ACCOUNT_WHITELIST_NOT_SET); + } else { + return new JsonRpcSuccessResponse(request.getId(), whitelistController.getAccountWhitelist()); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelist.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelist.java new file mode 100644 index 0000000000..58b68422ab --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelist.java @@ -0,0 +1,62 @@ +/* + * 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.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.permissioning.AccountWhitelistController; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController.RemoveResult; + +import java.util.List; + +public class PermRemoveAccountsFromWhitelist implements JsonRpcMethod { + + private final JsonRpcParameter parameters; + private final AccountWhitelistController whitelistController; + + public PermRemoveAccountsFromWhitelist( + final AccountWhitelistController whitelistController, final JsonRpcParameter parameters) { + this.whitelistController = whitelistController; + this.parameters = parameters; + } + + @Override + public String getName() { + return "perm_removeAccountsFromWhitelist"; + } + + @Override + @SuppressWarnings("unchecked") + public JsonRpcResponse response(final JsonRpcRequest request) { + final List accountsList = parameters.required(request.getParams(), 0, List.class); + final RemoveResult removeResult = whitelistController.removeAccounts(accountsList); + + switch (removeResult) { + case ERROR_INVALID_ENTRY: + return new JsonRpcErrorResponse( + request.getId(), JsonRpcError.ACCOUNT_WHITELIST_INVALID_ENTRY); + case ERROR_ABSENT_ENTRY: + return new JsonRpcErrorResponse( + request.getId(), JsonRpcError.ACCOUNT_WHITELIST_ABSENT_ENTRY); + case SUCCESS: + return new JsonRpcSuccessResponse(request.getId(), true); + default: + throw new IllegalStateException("Unmapped result from AccountWhitelistController"); + } + } +} 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 174aec2dea..6753eab643 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 @@ -39,13 +39,20 @@ public enum JsonRpcError { TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE(-32004, "Upfront cost exceeds account balance"), EXCEEDS_BLOCK_GAS_LIMIT(-32005, "Transaction gas limit exceeds block gas limit"), INCORRECT_NONCE(-32006, "Incorrect nonce"), + TX_SENDER_NOT_AUTHORIZED(-32007, "Sender account not authorized to send transactions"), // Miner failures COINBASE_NOT_SET(-32010, "Coinbase not set. Unable to start mining without a coinbase."), NO_HASHES_PER_SECOND(-32011, "No hashes being generated by the current node."), // Wallet errors - COINBASE_NOT_SPECIFIED(-32000, "Coinbase must be explicitly specified"); + COINBASE_NOT_SPECIFIED(-32000, "Coinbase must be explicitly specified"), + + // Permissioning errors + 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"); private final int code; private final String message; diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java index 7006ca07df..b70106376b 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java @@ -46,6 +46,8 @@ import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSpec; import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; +import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.util.RawBlockIterator; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; @@ -165,6 +167,9 @@ public abstract class AbstractEthJsonRpcHttpServiceTest { supportedCapabilities.add(EthProtocol.ETH62); supportedCapabilities.add(EthProtocol.ETH63); + final AccountWhitelistController accountWhitelistController = + new AccountWhitelistController(PermissioningConfiguration.createDefault()); + final Map methods = new JsonRpcMethodsFactory() .methods( @@ -178,6 +183,7 @@ public abstract class AbstractEthJsonRpcHttpServiceTest { miningCoordinatorMock, new NoOpMetricsSystem(), supportedCapabilities, + accountWhitelistController, JSON_RPC_APIS); final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); config.setPort(0); diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceHostWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceHostWhitelistTest.java index 6e44557180..53ad439036 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceHostWhitelistTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceHostWhitelistTest.java @@ -27,6 +27,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import java.io.IOException; @@ -93,6 +94,7 @@ public class JsonRpcHttpServiceHostWhitelistTest { mock(EthHashMiningCoordinator.class), new NoOpMetricsSystem(), supportedCapabilities, + mock(AccountWhitelistController.class), JSON_RPC_APIS)); service = createJsonRpcHttpService(); service.start().join(); diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java index fe42260ec5..0a6aa1a6b7 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java @@ -28,6 +28,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import java.util.HashSet; @@ -175,6 +176,7 @@ public class JsonRpcHttpServiceRpcApisTest { mock(EthHashMiningCoordinator.class), new NoOpMetricsSystem(), supportedCapabilities, + mock(AccountWhitelistController.class), config.getRpcApis())); final JsonRpcHttpService jsonRpcHttpService = new JsonRpcHttpService( diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java index a4945ca6d3..1dda1cf15e 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java @@ -42,6 +42,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.api.P2PNetwork; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController; import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValues; @@ -122,6 +123,7 @@ public class JsonRpcHttpServiceTest { mock(EthHashMiningCoordinator.class), new NoOpMetricsSystem(), supportedCapabilities, + mock(AccountWhitelistController.class), JSON_RPC_APIS)); service = createJsonRpcHttpService(); service.start().join(); diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java index aae3532dce..69a16de61f 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java @@ -137,6 +137,12 @@ public class EthSendRawTransactionTest { TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT); } + @Test + public void transactionWithNotWhitelistedSenderAccountIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED, JsonRpcError.TX_SENDER_NOT_AUTHORIZED); + } + private void verifyErrorForInvalidTransaction( final TransactionInvalidReason transactionInvalidReason, final JsonRpcError expectedError) { when(parameter.required(any(Object[].class), anyInt(), any())).thenReturn(VALID_TRANSACTION); diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelistTest.java new file mode 100644 index 0000000000..2de51d0074 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermAddAccountsToWhitelistTest.java @@ -0,0 +1,105 @@ +/* + * 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.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +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.permissioning.AccountWhitelistController; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController.AddResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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 PermAddAccountsToWhitelistTest { + + @Mock private AccountWhitelistController accountWhitelist; + private PermAddAccountsToWhitelist method; + + @Before + public void before() { + method = new PermAddAccountsToWhitelist(accountWhitelist, new JsonRpcParameter()); + } + + @Test + public void getNameShouldReturnExpectedName() { + assertThat(method.getName()).isEqualTo("perm_addAccountsToWhitelist"); + } + + @Test + public void whenAccountsAreAddedToWhitelistShouldReturnTrue() { + List accounts = Arrays.asList("0x0", "0x1"); + JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, true); + when(accountWhitelist.addAccounts(eq(accounts))).thenReturn(AddResult.SUCCESS); + + JsonRpcResponse actualResponse = method.response(request(accounts)); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void whenAccountIsInvalidShouldReturnInvalidAccountErrorResponse() { + JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.ACCOUNT_WHITELIST_INVALID_ENTRY); + when(accountWhitelist.addAccounts(any())).thenReturn(AddResult.ERROR_INVALID_ENTRY); + + JsonRpcResponse actualResponse = method.response(request(new ArrayList<>())); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void whenAccountIsDuplicatedShouldReturnDuplicatedAccountErrorResponse() { + JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.ACCOUNT_WHITELIST_DUPLICATED_ENTRY); + when(accountWhitelist.addAccounts(any())).thenReturn(AddResult.ERROR_DUPLICATED_ENTRY); + + JsonRpcResponse actualResponse = method.response(request(new ArrayList<>())); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void whenEmptyParamOnRequestShouldThrowInvalidJsonRpcException() { + JsonRpcRequest request = + new JsonRpcRequest("2.0", "perm_addAccountsToWhitelist", new Object[] {}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + assertThat(thrown) + .hasNoCause() + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + } + + private JsonRpcRequest request(final List accounts) { + return new JsonRpcRequest("2.0", "perm_addAccountsToWhitelist", new Object[] {accounts}); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelistTest.java new file mode 100644 index 0000000000..205919710d --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermGetAccountsWhitelistTest.java @@ -0,0 +1,88 @@ +/* + * 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.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.permissioning.AccountWhitelistController; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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 PermGetAccountsWhitelistTest { + + private static final JsonRpcRequest request = + new JsonRpcRequest("2.0", "perm_getAccountsWhitelist", null); + + @Mock private AccountWhitelistController accountWhitelist; + private PermGetAccountsWhitelist method; + + @Before + public void before() { + method = new PermGetAccountsWhitelist(accountWhitelist); + } + + @Test + public void getNameShouldReturnExpectedName() { + assertThat(method.getName()).isEqualTo("perm_getAccountsWhitelist"); + } + + @Test + public void shouldReturnExpectedListOfAccountsWhenWhitelistHasBeenSet() { + List accountsList = Arrays.asList("0x0", "0x1"); + when(accountWhitelist.isAccountWhiteListSet()).thenReturn(true); + when(accountWhitelist.getAccountWhitelist()).thenReturn(accountsList); + JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, accountsList); + + JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnEmptyListOfAccountsWhenWhitelistHasBeenSetAndIsEmpty() { + List emptyAccountsList = new ArrayList<>(); + when(accountWhitelist.isAccountWhiteListSet()).thenReturn(true); + when(accountWhitelist.getAccountWhitelist()).thenReturn(emptyAccountsList); + JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, emptyAccountsList); + + JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnErrorWhenWhitelistHasNotBeenSet() { + when(accountWhitelist.isAccountWhiteListSet()).thenReturn(false); + JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.ACCOUNT_WHITELIST_NOT_SET); + + JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelistTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelistTest.java new file mode 100644 index 0000000000..ab9e66c745 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/permissioning/PermRemoveAccountsFromWhitelistTest.java @@ -0,0 +1,105 @@ +/* + * 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.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +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.permissioning.AccountWhitelistController; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController.RemoveResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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 PermRemoveAccountsFromWhitelistTest { + + @Mock private AccountWhitelistController accountWhitelist; + private PermRemoveAccountsFromWhitelist method; + + @Before + public void before() { + method = new PermRemoveAccountsFromWhitelist(accountWhitelist, new JsonRpcParameter()); + } + + @Test + public void getNameShouldReturnExpectedName() { + assertThat(method.getName()).isEqualTo("perm_removeAccountsFromWhitelist"); + } + + @Test + public void whenAccountsAreRemovedFromWhitelistShouldReturnTrue() { + List accounts = Arrays.asList("0x0", "0x1"); + JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, true); + when(accountWhitelist.removeAccounts(eq(accounts))).thenReturn(RemoveResult.SUCCESS); + + JsonRpcResponse actualResponse = method.response(request(accounts)); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void whenAccountIsInvalidShouldReturnInvalidAccountErrorResponse() { + JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.ACCOUNT_WHITELIST_INVALID_ENTRY); + when(accountWhitelist.removeAccounts(any())).thenReturn(RemoveResult.ERROR_INVALID_ENTRY); + + JsonRpcResponse actualResponse = method.response(request(new ArrayList<>())); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void whenAccountIsAbsentShouldReturnAbsentAccountErrorResponse() { + JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.ACCOUNT_WHITELIST_ABSENT_ENTRY); + when(accountWhitelist.removeAccounts(any())).thenReturn(RemoveResult.ERROR_ABSENT_ENTRY); + + JsonRpcResponse actualResponse = method.response(request(new ArrayList<>())); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void whenEmptyParamOnRequestShouldThrowInvalidJsonRpcException() { + JsonRpcRequest request = + new JsonRpcRequest("2.0", "perm_removeAccountsFromWhitelist", new Object[] {}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + assertThat(thrown) + .hasNoCause() + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + } + + private JsonRpcRequest request(final List accounts) { + return new JsonRpcRequest("2.0", "perm_removeAccountsFromWhitelist", new Object[] {accounts}); + } +} diff --git a/ethereum/permissioning/build.gradle b/ethereum/permissioning/build.gradle index 67d9b72de1..1d97ce6000 100644 --- a/ethereum/permissioning/build.gradle +++ b/ethereum/permissioning/build.gradle @@ -26,6 +26,7 @@ jar { } dependencies { + implementation project(':util') testImplementation 'junit:junit' testImplementation 'org.assertj:assertj-core' diff --git a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistController.java b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistController.java index b782719797..bde7b83895 100644 --- a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistController.java +++ b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistController.java @@ -12,40 +12,85 @@ */ package tech.pegasys.pantheon.ethereum.permissioning; +import tech.pegasys.pantheon.util.bytes.BytesValue; + import java.util.ArrayList; import java.util.List; public class AccountWhitelistController { - private final List accountWhitelist; - private boolean accountWhitelistSet = false; + private static final int ACCOUNT_BYTES_SIZE = 20; + private final List accountWhitelist = new ArrayList<>(); + private boolean isAccountWhitelistSet = false; public AccountWhitelistController(final PermissioningConfiguration configuration) { - accountWhitelist = new ArrayList<>(); - if (configuration != null && configuration.getAccountWhitelist() != null) { - for (String hexString : configuration.getAccountWhitelist()) { - accountWhitelist.add(hexString); - } - if (configuration.isNodeWhitelistSet()) { - accountWhitelistSet = true; - } + if (configuration != null && configuration.isAccountWhitelistSet()) { + addAccounts(configuration.getAccountWhitelist()); } } - public boolean addAccount(final String account) { - accountWhitelistSet = true; - return accountWhitelist.add(account); + public AddResult addAccounts(final List accounts) { + if (containsInvalidAccount(accounts)) { + return AddResult.ERROR_INVALID_ENTRY; + } + + boolean hasDuplicatedAccount = accounts.stream().anyMatch(accountWhitelist::contains); + if (hasDuplicatedAccount) { + return AddResult.ERROR_DUPLICATED_ENTRY; + } + + this.isAccountWhitelistSet = true; + this.accountWhitelist.addAll(accounts); + return AddResult.SUCCESS; } - public boolean removeAccount(final String account) { - return accountWhitelist.remove(account); + public RemoveResult removeAccounts(final List accounts) { + if (containsInvalidAccount(accounts)) { + return RemoveResult.ERROR_INVALID_ENTRY; + } + + if (!accountWhitelist.containsAll(accounts)) { + return RemoveResult.ERROR_ABSENT_ENTRY; + } + + this.accountWhitelist.removeAll(accounts); + return RemoveResult.SUCCESS; } public boolean contains(final String account) { - return (!accountWhitelistSet || (accountWhitelistSet && accountWhitelist.contains(account))); + return (!isAccountWhitelistSet || accountWhitelist.contains(account)); } public boolean isAccountWhiteListSet() { - return accountWhitelistSet; + return isAccountWhitelistSet; + } + + public List getAccountWhitelist() { + return new ArrayList<>(accountWhitelist); + } + + private boolean containsInvalidAccount(final List accounts) { + return !accounts.stream().allMatch(this::isValidAccountString); + } + + private boolean isValidAccountString(final String account) { + try { + BytesValue bytesValue = BytesValue.fromHexString(account); + return bytesValue.size() == ACCOUNT_BYTES_SIZE; + } catch (NullPointerException | IndexOutOfBoundsException | IllegalArgumentException e) { + return false; + } + } + + public enum AddResult { + SUCCESS, + ERROR_DUPLICATED_ENTRY, + ERROR_INVALID_ENTRY + } + + public enum RemoveResult { + SUCCESS, + ERROR_ABSENT_ENTRY, + ERROR_INVALID_ENTRY } } diff --git a/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistControllerTest.java b/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistControllerTest.java new file mode 100644 index 0000000000..8584cfdf5d --- /dev/null +++ b/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/AccountWhitelistControllerTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.ethereum.permissioning; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController.AddResult; +import tech.pegasys.pantheon.ethereum.permissioning.AccountWhitelistController.RemoveResult; + +import java.util.ArrayList; +import java.util.Arrays; + +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 AccountWhitelistControllerTest { + + private AccountWhitelistController controller; + @Mock private PermissioningConfiguration permissioningConfig; + + @Before + public void before() { + controller = new AccountWhitelistController(permissioningConfig); + } + + @Test + public void newInstanceWithNullPermConfigShouldHaveAccountWhitelistNotSet() { + controller = new AccountWhitelistController(null); + + assertThat(controller.isAccountWhiteListSet()).isFalse(); + } + + @Test + public void whenAccountWhitelistIsNotSetContainsShouldReturnTrue() { + when(permissioningConfig.isAccountWhitelistSet()).thenReturn(false); + controller = new AccountWhitelistController(permissioningConfig); + + assertThat(controller.contains("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")).isTrue(); + } + + @Test + public void whenPermConfigHasAccountsShouldSetAccountsWhitelist() { + when(permissioningConfig.isAccountWhitelistSet()).thenReturn(true); + when(permissioningConfig.getAccountWhitelist()) + .thenReturn(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + controller = new AccountWhitelistController(permissioningConfig); + + assertThat(controller.isAccountWhiteListSet()).isTrue(); + } + + @Test + public void whenPermConfigHasAccountsShouldAddAllAccountsToWhitelist() { + when(permissioningConfig.isAccountWhitelistSet()).thenReturn(true); + when(permissioningConfig.getAccountWhitelist()) + .thenReturn(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + controller = new AccountWhitelistController(permissioningConfig); + + assertThat(controller.getAccountWhitelist()) + .contains("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"); + } + + @Test + public void whenPermConfigContainsEmptyListOfAccountsContainsShouldReturnFalse() { + when(permissioningConfig.isAccountWhitelistSet()).thenReturn(true); + when(permissioningConfig.getAccountWhitelist()).thenReturn(new ArrayList<>()); + controller = new AccountWhitelistController(permissioningConfig); + + assertThat(controller.contains("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")).isFalse(); + } + + @Test + public void addAccountsWithInvalidAccountShouldReturnInvalidEntryResult() { + AddResult addResult = controller.addAccounts(Arrays.asList("0x0")); + + assertThat(addResult).isEqualTo(AddResult.ERROR_INVALID_ENTRY); + assertThat(controller.getAccountWhitelist()).isEmpty(); + } + + @Test + public void addDuplicatedAccountShouldReturnDuplicatedEntryResult() { + controller.addAccounts(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + AddResult addResult = + controller.addAccounts(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + + assertThat(addResult).isEqualTo(AddResult.ERROR_DUPLICATED_ENTRY); + assertThat(controller.getAccountWhitelist()) + .containsExactly("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"); + } + + @Test + public void addValidAccountsShouldReturnSuccessResult() { + AddResult addResult = + controller.addAccounts(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + + assertThat(addResult).isEqualTo(AddResult.SUCCESS); + assertThat(controller.getAccountWhitelist()) + .containsExactly("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"); + } + + @Test + public void removeExistingAccountShouldReturnSuccessResult() { + controller.addAccounts(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + + RemoveResult removeResult = + controller.removeAccounts(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + + assertThat(removeResult).isEqualTo(RemoveResult.SUCCESS); + assertThat(controller.getAccountWhitelist()).isEmpty(); + } + + @Test + public void removeAbsentAccountShouldReturnAbsentEntryResult() { + RemoveResult removeResult = + controller.removeAccounts(Arrays.asList("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")); + + assertThat(removeResult).isEqualTo(RemoveResult.ERROR_ABSENT_ENTRY); + assertThat(controller.getAccountWhitelist()).isEmpty(); + } + + @Test + public void removeInvalidAccountShouldReturnInvalidEntryResult() { + RemoveResult removeResult = controller.removeAccounts(Arrays.asList("0x0")); + + assertThat(removeResult).isEqualTo(RemoveResult.ERROR_INVALID_ENTRY); + assertThat(controller.getAccountWhitelist()).isEmpty(); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 7c9a15a9ae..f00dbf9b90 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -230,9 +230,9 @@ public class RunnerBuilder { final TransactionPool transactionPool = pantheonController.getTransactionPool(); final MiningCoordinator miningCoordinator = pantheonController.getMiningCoordinator(); + final AccountWhitelistController accountWhitelistController = + new AccountWhitelistController(permissioningConfiguration); if (permissioningConfiguration.isAccountWhitelistSet()) { - AccountWhitelistController accountWhitelistController = - new AccountWhitelistController(permissioningConfiguration); transactionPool.setAccountWhitelist(accountWhitelistController); } @@ -252,7 +252,8 @@ public class RunnerBuilder { metricsSystem, supportedCapabilities, jsonRpcConfiguration.getRpcApis(), - filterManager); + filterManager, + accountWhitelistController); jsonRpcHttpService = Optional.of( new JsonRpcHttpService( @@ -273,7 +274,8 @@ public class RunnerBuilder { metricsSystem, supportedCapabilities, webSocketConfiguration.getRpcApis(), - filterManager); + filterManager, + accountWhitelistController); final SubscriptionManager subscriptionManager = createSubscriptionManager( @@ -331,7 +333,8 @@ public class RunnerBuilder { final MetricsSystem metricsSystem, final Set supportedCapabilities, final Collection jsonRpcApis, - final FilterManager filterManager) { + final FilterManager filterManager, + final AccountWhitelistController accountWhitelistController) { final Map methods = new JsonRpcMethodsFactory() .methods( @@ -346,7 +349,8 @@ public class RunnerBuilder { metricsSystem, supportedCapabilities, jsonRpcApis, - filterManager); + filterManager, + accountWhitelistController); methods.putAll(pantheonController.getAdditionalJsonRpcMethods(jsonRpcApis)); return methods; }