From 576c12dd46e351bd9ea6b9e52f64f057bad9c337 Mon Sep 17 00:00:00 2001 From: Lucas Saldanha Date: Fri, 7 Jun 2019 14:34:51 +1200 Subject: [PATCH] PAN-2445: Onchain account permissioning (#1507) Signed-off-by: Adrian Sutton --- .../mainnet/MainnetTransactionValidator.java | 1 + .../mainnet/MutableProtocolSchedule.java | 7 + .../ethereum/mainnet/ProtocolSchedule.java | 4 + .../ethereum/mainnet/ProtocolSpec.java | 5 + .../mainnet/TransactionValidator.java | 3 + .../MainnetTransactionValidatorTest.java | 8 +- ...untLocalConfigPermissioningController.java | 18 ++- ...nSmartContractPermissioningController.java | 19 +++ .../AccountPermissioningController.java | 52 ++++++- ...rtContractPermissioningControllerTest.java | 4 + .../AccountPermissioningControllerTest.java | 5 +- .../tech/pegasys/pantheon/RunnerBuilder.java | 32 ++--- .../pegasys/pantheon/cli/PantheonCommand.java | 136 +++++++++++++++--- .../pantheon/cli/CommandTestAbstract.java | 14 +- .../pantheon/cli/PantheonCommandTest.java | 93 ++++++++++-- .../src/test/resources/everything_config.toml | 2 + 16 files changed, 339 insertions(+), 64 deletions(-) diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java index 26e9fc4cb4..960f05df2d 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java @@ -166,6 +166,7 @@ public class MainnetTransactionValidator implements TransactionValidator { return transactionFilter.map(c -> c.permitted(transaction, isStateChange)).orElse(true); } + @Override public void setTransactionFilter(final TransactionFilter transactionFilter) { this.transactionFilter = Optional.of(transactionFilter); } diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java index ee195947bd..89d0345acd 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java @@ -14,6 +14,8 @@ package tech.pegasys.pantheon.ethereum.mainnet; import static com.google.common.base.Preconditions.checkArgument; +import tech.pegasys.pantheon.ethereum.core.TransactionFilter; + import java.math.BigInteger; import java.util.Comparator; import java.util.NavigableSet; @@ -69,4 +71,9 @@ public class MutableProtocolSchedule implements ProtocolSchedule { .map(spec -> spec.getSpec().getName() + ": " + spec.getBlock()) .collect(Collectors.joining(", ", "[", "]")); } + + @Override + public void setTransactionFilter(final TransactionFilter transactionFilter) { + protocolSpecs.forEach(spec -> spec.getSpec().setTransactionFilter(transactionFilter)); + } } diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSchedule.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSchedule.java index 12f0e71b9e..ecb6f8ead2 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSchedule.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSchedule.java @@ -12,6 +12,8 @@ */ package tech.pegasys.pantheon.ethereum.mainnet; +import tech.pegasys.pantheon.ethereum.core.TransactionFilter; + import java.math.BigInteger; import java.util.Optional; @@ -20,4 +22,6 @@ public interface ProtocolSchedule { ProtocolSpec getByBlockNumber(long number); Optional getChainId(); + + void setTransactionFilter(TransactionFilter transactionFilter); } diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSpec.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSpec.java index 61bf45c3e7..59c43d8a8f 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSpec.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/ProtocolSpec.java @@ -15,6 +15,7 @@ package tech.pegasys.pantheon.ethereum.mainnet; import tech.pegasys.pantheon.ethereum.BlockValidator; import tech.pegasys.pantheon.ethereum.core.BlockHeaderFunctions; import tech.pegasys.pantheon.ethereum.core.BlockImporter; +import tech.pegasys.pantheon.ethereum.core.TransactionFilter; import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockProcessor.TransactionReceiptFactory; import tech.pegasys.pantheon.ethereum.vm.EVM; @@ -243,4 +244,8 @@ public class ProtocolSpec { public PrecompileContractRegistry getPrecompileContractRegistry() { return precompileContractRegistry; } + + public void setTransactionFilter(final TransactionFilter transactionFilter) { + transactionValidator.setTransactionFilter(transactionFilter); + } } diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/TransactionValidator.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/TransactionValidator.java index 4a3168cb07..d4f0f361ea 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/TransactionValidator.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/TransactionValidator.java @@ -14,6 +14,7 @@ package tech.pegasys.pantheon.ethereum.mainnet; import tech.pegasys.pantheon.ethereum.core.Account; import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.TransactionFilter; /** Validates transaction based on some criteria. */ public interface TransactionValidator { @@ -54,6 +55,8 @@ public interface TransactionValidator { ValidationResult validateForSender( Transaction transaction, Account sender, TransactionValidationParams validationParams); + void setTransactionFilter(TransactionFilter transactionFilter); + enum TransactionInvalidReason { WRONG_CHAIN_ID, REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED, diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java index c6ce984c01..16039b4696 100644 --- a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java @@ -158,7 +158,6 @@ public class MainnetTransactionValidatorTest { public void shouldRejectTransactionIfAccountIsNotPermitted() { final MainnetTransactionValidator validator = new MainnetTransactionValidator(gasCalculator, false, Optional.empty()); - validator.setTransactionFilter(transactionFilter(false)); assertThat(validator.validateForSender(basicTransaction, accountWithNonce(0), true)) @@ -169,7 +168,6 @@ public class MainnetTransactionValidatorTest { public void shouldAcceptValidTransactionIfAccountIsPermitted() { final MainnetTransactionValidator validator = new MainnetTransactionValidator(gasCalculator, false, Optional.empty()); - validator.setTransactionFilter(transactionFilter(true)); assertThat(validator.validateForSender(basicTransaction, accountWithNonce(0), true)) @@ -178,13 +176,13 @@ public class MainnetTransactionValidatorTest { @Test public void shouldPropagateCorrectStateChangeParamToTransactionFilter() { - final MainnetTransactionValidator validator = - new MainnetTransactionValidator(gasCalculator, false, Optional.empty()); - final ArgumentCaptor stateChangeParamCaptor = ArgumentCaptor.forClass(Boolean.class); final TransactionFilter transactionFilter = mock(TransactionFilter.class); when(transactionFilter.permitted(any(Transaction.class), stateChangeParamCaptor.capture())) .thenReturn(true); + + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, Optional.empty()); validator.setTransactionFilter(transactionFilter); final TransactionValidationParams validationParams = diff --git a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountLocalConfigPermissioningController.java b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountLocalConfigPermissioningController.java index cd2528d06a..6a3657877d 100644 --- a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountLocalConfigPermissioningController.java +++ b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/AccountLocalConfigPermissioningController.java @@ -13,6 +13,7 @@ package tech.pegasys.pantheon.ethereum.permissioning; import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.permissioning.account.TransactionPermissioningProvider; import tech.pegasys.pantheon.metrics.Counter; @@ -232,17 +233,32 @@ public class AccountLocalConfigPermissioningController implements TransactionPer @Override public boolean isPermitted(final Transaction transaction) { - this.checkCounter.inc(); + final Hash transactionHash = transaction.hash(); final Address sender = transaction.getSender(); + + LOG.trace("Account permissioning - Local Config: Checking transaction {}", transactionHash); + + this.checkCounter.inc(); if (sender == null) { this.checkCounterUnpermitted.inc(); + LOG.trace( + "Account permissioning - Local Config: Rejected transaction {} without sender", + transactionHash); return false; } else { if (contains(sender.toString())) { this.checkCounterPermitted.inc(); + LOG.trace( + "Account permissioning - Local Config: Permitted transaction {} from {}", + transactionHash, + sender); return true; } else { this.checkCounterUnpermitted.inc(); + LOG.trace( + "Account permissioning - Local Config: Rejected transaction {} from {}", + transactionHash, + sender); return false; } } diff --git a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningController.java b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningController.java index 9eea6695db..9aca2d3e31 100644 --- a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningController.java +++ b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningController.java @@ -30,12 +30,18 @@ import tech.pegasys.pantheon.util.bytes.BytesValues; import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + /** * Controller that can read from a smart contract that exposes the permissioning call * transactionAllowed(address,address,uint256,uint256,uint256,bytes) */ public class TransactionSmartContractPermissioningController implements TransactionPermissioningProvider { + + private static final Logger LOG = LogManager.getLogger(); + private final Address contractAddress; private final TransactionSimulator transactionSimulator; @@ -101,6 +107,11 @@ public class TransactionSmartContractPermissioningController */ @Override public boolean isPermitted(final Transaction transaction) { + final tech.pegasys.pantheon.ethereum.core.Hash transactionHash = transaction.hash(); + final Address sender = transaction.getSender(); + + LOG.trace("Account permissioning - Smart Contract : Checking transaction {}", transactionHash); + this.checkCounter.inc(); final BytesValue payload = createPayload(transaction); final CallParameter callParams = @@ -131,9 +142,17 @@ public class TransactionSmartContractPermissioningController if (result.map(r -> checkTransactionResult(r.getOutput())).orElse(false)) { this.checkCounterPermitted.inc(); + LOG.trace( + "Account permissioning - Smart Contract: Permitted transaction {} from {}", + transactionHash, + sender); return true; } else { this.checkCounterUnpermitted.inc(); + LOG.trace( + "Account permissioning - Smart Contract: Rejected transaction {} from {}", + transactionHash, + sender); return false; } } diff --git a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningController.java b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningController.java index 2304208890..522cdc73df 100644 --- a/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningController.java +++ b/ethereum/permissioning/src/main/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningController.java @@ -12,19 +12,30 @@ */ package tech.pegasys.pantheon.ethereum.permissioning.account; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.permissioning.AccountLocalConfigPermissioningController; import tech.pegasys.pantheon.ethereum.permissioning.TransactionSmartContractPermissioningController; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + public class AccountPermissioningController { - private final AccountLocalConfigPermissioningController accountLocalConfigPermissioningController; - private final TransactionSmartContractPermissioningController + private static final Logger LOG = LogManager.getLogger(); + + private final Optional + accountLocalConfigPermissioningController; + private final Optional transactionSmartContractPermissioningController; public AccountPermissioningController( - final AccountLocalConfigPermissioningController accountLocalConfigPermissioningController, - final TransactionSmartContractPermissioningController + final Optional + accountLocalConfigPermissioningController, + final Optional transactionSmartContractPermissioningController) { this.accountLocalConfigPermissioningController = accountLocalConfigPermissioningController; this.transactionSmartContractPermissioningController = @@ -32,11 +43,38 @@ public class AccountPermissioningController { } public boolean isPermitted(final Transaction transaction, final boolean includeOnChainCheck) { + final Hash transactionHash = transaction.hash(); + final Address sender = transaction.getSender(); + + LOG.trace("Account permissioning: Checking transaction {}", transactionHash); + + boolean permitted; if (includeOnChainCheck) { - return accountLocalConfigPermissioningController.isPermitted(transaction) - && transactionSmartContractPermissioningController.isPermitted(transaction); + permitted = + accountLocalConfigPermissioningController + .map(c -> c.isPermitted(transaction)) + .orElse(true) + && transactionSmartContractPermissioningController + .map(c -> c.isPermitted(transaction)) + .orElse(true); } else { - return accountLocalConfigPermissioningController.isPermitted(transaction); + permitted = + accountLocalConfigPermissioningController + .map(c -> c.isPermitted(transaction)) + .orElse(true); } + + if (permitted) { + LOG.trace("Account permissioning: Permitted transaction {} from {}", transactionHash, sender); + } else { + LOG.trace("Account permissioning: Rejected transaction {} from {}", transactionHash, sender); + } + + return permitted; + } + + public Optional + getAccountLocalConfigPermissioningController() { + return accountLocalConfigPermissioningController; } } diff --git a/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningControllerTest.java b/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningControllerTest.java index 7d364d33d4..878f253f56 100644 --- a/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningControllerTest.java +++ b/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/TransactionSmartContractPermissioningControllerTest.java @@ -22,6 +22,7 @@ import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.create import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryWorldStateArchive; import tech.pegasys.pantheon.config.GenesisConfigFile; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; import tech.pegasys.pantheon.ethereum.chain.GenesisState; import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; import tech.pegasys.pantheon.ethereum.core.Address; @@ -37,6 +38,7 @@ import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.IOException; +import java.math.BigInteger; import com.google.common.io.Resources; import org.junit.Test; @@ -97,6 +99,8 @@ public class TransactionSmartContractPermissioningControllerTest { .gasPrice(Wei.ZERO) .gasLimit(0) .payload(BytesValue.EMPTY) + .nonce(1) + .signature(Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 1)) .build(); } diff --git a/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningControllerTest.java b/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningControllerTest.java index 32bb0ca61b..83948c4e79 100644 --- a/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningControllerTest.java +++ b/ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/account/AccountPermissioningControllerTest.java @@ -23,6 +23,8 @@ import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.permissioning.AccountLocalConfigPermissioningController; import tech.pegasys.pantheon.ethereum.permissioning.TransactionSmartContractPermissioningController; +import java.util.Optional; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,7 +42,8 @@ public class AccountPermissioningControllerTest { @Before public void before() { permissioningController = - new AccountPermissioningController(localConfigController, smartContractController); + new AccountPermissioningController( + Optional.of(localConfigController), Optional.of(smartContractController)); } @Test diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 8feb2b0d36..41cfd0277c 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -62,10 +62,10 @@ import tech.pegasys.pantheon.ethereum.p2p.permissions.PeerPermissionsBlacklist; import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; import tech.pegasys.pantheon.ethereum.p2p.wire.SubProtocol; import tech.pegasys.pantheon.ethereum.permissioning.AccountLocalConfigPermissioningController; -import tech.pegasys.pantheon.ethereum.permissioning.LocalPermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.NodeLocalConfigPermissioningController; import tech.pegasys.pantheon.ethereum.permissioning.NodePermissioningControllerFactory; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; +import tech.pegasys.pantheon.ethereum.permissioning.account.AccountPermissioningController; import tech.pegasys.pantheon.ethereum.permissioning.node.NodePermissioningController; import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulator; import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; @@ -110,6 +110,7 @@ public class RunnerBuilder { private MetricsSystem metricsSystem; private Optional permissioningConfiguration = Optional.empty(); private Collection staticNodes = Collections.emptyList(); + private AccountPermissioningController accountPermissioningController; public RunnerBuilder vertx(final Vertx vertx) { this.vertx = vertx; @@ -197,6 +198,12 @@ public class RunnerBuilder { return this; } + public RunnerBuilder accountPermissioningController( + final AccountPermissioningController accountPermissioningController) { + this.accountPermissioningController = accountPermissioningController; + return this; + } + public Runner build() { Preconditions.checkNotNull(pantheonController); @@ -245,9 +252,6 @@ public class RunnerBuilder { final List bootnodes = discoveryConfiguration.getBootnodes(); - final Optional localPermissioningConfiguration = - permissioningConfiguration.flatMap(PermissioningConfiguration::getLocalConfig); - final Synchronizer synchronizer = pantheonController.getSynchronizer(); final TransactionSimulator transactionSimulator = @@ -289,16 +293,6 @@ public class RunnerBuilder { final TransactionPool transactionPool = pantheonController.getTransactionPool(); final MiningCoordinator miningCoordinator = pantheonController.getMiningCoordinator(); - final Optional accountWhitelistController = - localPermissioningConfiguration - .filter(LocalPermissioningConfiguration::isAccountWhitelistEnabled) - .map( - configuration -> { - final AccountLocalConfigPermissioningController whitelistController = - new AccountLocalConfigPermissioningController(configuration, metricsSystem); - transactionPool.setAccountFilter(whitelistController::contains); - return whitelistController; - }); final PrivacyParameters privacyParameters = pantheonController.getPrivacyParameters(); final FilterManager filterManager = createFilterManager(vertx, context, transactionPool); @@ -312,6 +306,12 @@ public class RunnerBuilder { final Optional nodeLocalConfigPermissioningController = nodePermissioningController.flatMap(NodePermissioningController::localConfigController); + final Optional + accountLocalConfigPermissioningController = + accountPermissioningController != null + ? accountPermissioningController.getAccountLocalConfigPermissioningController() + : Optional.empty(); + Optional jsonRpcHttpService = Optional.empty(); if (jsonRpcConfiguration.isEnabled()) { final Map jsonRpcMethods = @@ -327,7 +327,7 @@ public class RunnerBuilder { supportedCapabilities, jsonRpcConfiguration.getRpcApis(), filterManager, - accountWhitelistController, + accountLocalConfigPermissioningController, nodeLocalConfigPermissioningController, privacyParameters, jsonRpcConfiguration, @@ -378,7 +378,7 @@ public class RunnerBuilder { supportedCapabilities, webSocketConfiguration.getRpcApis(), filterManager, - accountWhitelistController, + accountLocalConfigPermissioningController, nodeLocalConfigPermissioningController, privacyParameters, jsonRpcConfiguration, diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 72fc90fe91..c95795ff44 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -40,6 +40,7 @@ import tech.pegasys.pantheon.cli.rlp.RLPSubCommand; import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.controller.KeyPairUtil; import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.MiningParameters; import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; @@ -54,12 +55,17 @@ 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.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; import tech.pegasys.pantheon.ethereum.p2p.peers.StaticNodesParser; +import tech.pegasys.pantheon.ethereum.permissioning.AccountLocalConfigPermissioningController; import tech.pegasys.pantheon.ethereum.permissioning.LocalPermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfigurationBuilder; import tech.pegasys.pantheon.ethereum.permissioning.SmartContractPermissioningConfiguration; +import tech.pegasys.pantheon.ethereum.permissioning.TransactionSmartContractPermissioningController; +import tech.pegasys.pantheon.ethereum.permissioning.account.AccountPermissioningController; +import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulator; import tech.pegasys.pantheon.metrics.MetricCategory; import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; @@ -514,6 +520,18 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { description = "Enable node level permissions via smart contract (default: ${DEFAULT-VALUE})") private final Boolean permissionsNodesContractEnabled = false; + @Option( + names = {"--permissions-accounts-contract-address"}, + description = "Address of the account permissioning smart contract", + arity = "1") + private final Address permissionsAccountsContractAddress = null; + + @Option( + names = {"--permissions-accounts-contract-enabled"}, + description = + "Enable account level permissions via smart contract (default: ${DEFAULT-VALUE})") + private final Boolean permissionsAccountsContractEnabled = false; + @Option( names = {"--privacy-enabled"}, description = "Enable private transactions (default: ${DEFAULT-VALUE})") @@ -733,6 +751,14 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { final PantheonController pantheonController = buildController(); final MetricsConfiguration metricsConfiguration = metricsConfiguration(); + final AccountPermissioningController accountPermissioningController = + buildAccountPermissioningController(permissioningConfiguration, pantheonController); + if (permissionsAccountsEnabled || permissionsAccountsContractEnabled) { + pantheonController + .getProtocolSchedule() + .setTransactionFilter(accountPermissioningController::isPermitted); + } + pantheonPluginContext.addService( PantheonEvents.class, new PantheonEventsImpl((pantheonController.getProtocolManager().getBlockBroadcaster()))); @@ -751,12 +777,64 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { webSocketConfiguration, metricsConfiguration, permissioningConfiguration, - staticNodes); + staticNodes, + accountPermissioningController); } catch (final Exception e) { throw new ParameterException(this.commandLine, e.getMessage(), e); } } + private AccountPermissioningController buildAccountPermissioningController( + final Optional permissioningConfiguration, + final PantheonController pantheonController) { + + Optional accountLocalConfigPermissioningController = + Optional.empty(); + Optional + transactionSmartContractPermissioningController = Optional.empty(); + + if (permissioningConfiguration.isPresent()) { + final PermissioningConfiguration config = permissioningConfiguration.get(); + if (config.getLocalConfig().isPresent()) { + final LocalPermissioningConfiguration localPermissioningConfiguration = + config.getLocalConfig().get(); + + if (localPermissioningConfiguration.isAccountWhitelistEnabled()) { + accountLocalConfigPermissioningController = + Optional.of( + new AccountLocalConfigPermissioningController( + localPermissioningConfiguration, metricsSystem.get())); + } + } + + if (config.getSmartContractConfig().isPresent()) { + final SmartContractPermissioningConfiguration smartContractPermissioningConfiguration = + config.getSmartContractConfig().get(); + + if (smartContractPermissioningConfiguration.isSmartContractAccountWhitelistEnabled()) { + final Address accountSmartContractAddress = + smartContractPermissioningConfiguration.getAccountSmartContractAddress(); + final ProtocolContext protocolContext = pantheonController.getProtocolContext(); + final ProtocolSchedule protocolSchedule = pantheonController.getProtocolSchedule(); + + final TransactionSimulator transactionSimulator = + new TransactionSimulator( + protocolContext.getBlockchain(), + protocolContext.getWorldStateArchive(), + protocolSchedule); + + transactionSmartContractPermissioningController = + Optional.of( + new TransactionSmartContractPermissioningController( + accountSmartContractAddress, transactionSimulator, metricsSystem.get())); + } + } + } + + return new AccountPermissioningController( + accountLocalConfigPermissioningController, transactionSmartContractPermissioningController); + } + private NetworkName getNetwork() { // noinspection ConstantConditions network is not always null but injected by // PicoCLI if used @@ -924,10 +1002,6 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { } private Optional permissioningConfiguration() throws Exception { - final Optional localPermissioningConfigurationOptional; - final Optional - smartContractPermissioningConfigurationOptional; - if (!(localPermissionsEnabled() || contractPermissionsEnabled())) { if (rpcHttpApis.contains(RpcApis.PERM) || rpcWsApis.contains(RpcApis.PERM)) { logger.warn( @@ -936,6 +1010,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { return Optional.empty(); } + final Optional localPermissioningConfigurationOptional; if (localPermissionsEnabled()) { final Optional nodePermissioningConfigFile = Optional.ofNullable(nodePermissionsConfigFile()); @@ -965,30 +1040,46 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { localPermissioningConfigurationOptional = Optional.empty(); } - if (contractPermissionsEnabled()) { + final SmartContractPermissioningConfiguration smartContractPermissioningConfiguration = + SmartContractPermissioningConfiguration.createDefault(); + if (permissionsNodesContractEnabled) { if (permissionsNodesContractAddress == null) { throw new ParameterException( this.commandLine, - "No contract address specified. Cannot enable contract based permissions."); - } - final SmartContractPermissioningConfiguration smartContractPermissioningConfiguration = - PermissioningConfigurationBuilder.smartContractPermissioningConfiguration( - permissionsNodesContractAddress, permissionsNodesContractEnabled); - smartContractPermissioningConfigurationOptional = - Optional.of(smartContractPermissioningConfiguration); - } else { - if (permissionsNodesContractAddress != null) { - logger.warn( - "Smart contract address set {} but no contract permissions enabled", + "No node permissioning contract address specified. Cannot enable smart contract based node permissioning."); + } else { + smartContractPermissioningConfiguration.setSmartContractNodeWhitelistEnabled( + permissionsNodesContractEnabled); + smartContractPermissioningConfiguration.setNodeSmartContractAddress( permissionsNodesContractAddress); } - smartContractPermissioningConfigurationOptional = Optional.empty(); + } else if (permissionsNodesContractAddress != null) { + logger.warn( + "Node permissioning smart contract address set {} but smart contract node permissioning is disabled.", + permissionsNodesContractAddress); + } + + if (permissionsAccountsContractEnabled) { + if (permissionsAccountsContractAddress == null) { + throw new ParameterException( + this.commandLine, + "No account permissioning contract address specified. Cannot enable smart contract based account permissioning."); + } else { + smartContractPermissioningConfiguration.setSmartContractAccountWhitelistEnabled( + permissionsAccountsContractEnabled); + smartContractPermissioningConfiguration.setAccountSmartContractAddress( + permissionsAccountsContractAddress); + } + } else if (permissionsAccountsContractAddress != null) { + logger.warn( + "Account permissioning smart contract address set {} but smart contract account permissioning is disabled.", + permissionsAccountsContractAddress); } final PermissioningConfiguration permissioningConfiguration = new PermissioningConfiguration( localPermissioningConfigurationOptional, - smartContractPermissioningConfigurationOptional); + Optional.of(smartContractPermissioningConfiguration)); return Optional.of(permissioningConfiguration); } @@ -998,8 +1089,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { } private boolean contractPermissionsEnabled() { - // TODO add permissionsAccountsContractEnabled - return permissionsNodesContractEnabled; + return permissionsNodesContractEnabled || permissionsAccountsContractEnabled; } private PrivacyParameters privacyParameters() throws IOException { @@ -1054,7 +1144,8 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { final WebSocketConfiguration webSocketConfiguration, final MetricsConfiguration metricsConfiguration, final Optional permissioningConfiguration, - final Collection staticNodes) { + final Collection staticNodes, + final AccountPermissioningController accountPermissioningController) { checkNotNull(runnerBuilder); @@ -1079,6 +1170,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { .metricsSystem(metricsSystem) .metricsConfiguration(metricsConfiguration) .staticNodes(staticNodes) + .accountPermissioningController(accountPermissioningController) .build(); addShutdownHook(runner); diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index 3e3581a984..29953294b7 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -17,6 +17,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; import tech.pegasys.pantheon.Runner; @@ -25,6 +26,7 @@ import tech.pegasys.pantheon.cli.PublicKeySubCommand.KeyLoader; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.controller.PantheonControllerBuilder; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.eth.EthereumWireProtocolConfiguration; import tech.pegasys.pantheon.ethereum.eth.manager.EthProtocolManager; import tech.pegasys.pantheon.ethereum.eth.sync.BlockBroadcaster; @@ -32,7 +34,9 @@ import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; import tech.pegasys.pantheon.ethereum.graphql.GraphQLConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; +import tech.pegasys.pantheon.ethereum.permissioning.account.AccountPermissioningController; import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.services.PantheonPluginContextImpl; import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration; @@ -80,6 +84,8 @@ public abstract class CommandTestAbstract { @Mock PantheonControllerBuilder mockControllerBuilder; @Mock EthProtocolManager mockEthProtocolManager; + @Mock ProtocolSchedule mockProtocolSchedule; + @Mock ProtocolContext mockProtocolContext; @Mock BlockBroadcaster mockBlockBroadcaster; @Mock SynchronizerConfiguration.Builder mockSyncConfBuilder; @Mock EthereumWireProtocolConfiguration.Builder mockEthereumWireProtocolConfigurationBuilder; @@ -105,6 +111,9 @@ public abstract class CommandTestAbstract { @Captor ArgumentCaptor permissioningConfigurationArgumentCaptor; + @Captor + ArgumentCaptor accountPermissioningControllerArgumentCaptor; + @Rule public final TemporaryFolder temp = new TemporaryFolder(); @Before @@ -134,7 +143,9 @@ public abstract class CommandTestAbstract { // doReturn used because of generic PantheonController doReturn(mockController).when(mockControllerBuilder).build(); - when(mockController.getProtocolManager()).thenReturn(mockEthProtocolManager); + lenient().when(mockController.getProtocolManager()).thenReturn(mockEthProtocolManager); + lenient().when(mockController.getProtocolSchedule()).thenReturn(mockProtocolSchedule); + lenient().when(mockController.getProtocolContext()).thenReturn(mockProtocolContext); when(mockEthProtocolManager.getBlockBroadcaster()).thenReturn(mockBlockBroadcaster); @@ -165,6 +176,7 @@ public abstract class CommandTestAbstract { when(mockRunnerBuilder.metricsSystem(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.metricsConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.staticNodes(any())).thenReturn(mockRunnerBuilder); + when(mockRunnerBuilder.accountPermissioningController(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.build()).thenReturn(mockRunner); } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index 8632819266..d0a427c079 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -17,6 +17,7 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.Mockito.atLeast; @@ -48,6 +49,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.LocalPermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.SmartContractPermissioningConfiguration; +import tech.pegasys.pantheon.ethereum.permissioning.account.AccountPermissioningController; import tech.pegasys.pantheon.metrics.MetricCategory; import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -339,7 +341,7 @@ public class PantheonCommandTest extends CommandTestAbstract { } @Test - public void permissionsSmartContractWithoutOptionMustError() { + public void nodePermissionsSmartContractWithoutOptionMustError() { parseCommand("--permissions-nodes-contract-address"); verifyZeroInteractions(mockRunnerBuilder); @@ -350,17 +352,18 @@ public class PantheonCommandTest extends CommandTestAbstract { } @Test - public void permissionsEnabledWithoutContractAddressMustError() { + public void nodePermissionsEnabledWithoutContractAddressMustError() { parseCommand("--permissions-nodes-contract-enabled"); verifyZeroInteractions(mockRunnerBuilder); - assertThat(commandErrorOutput.toString()).contains("No contract address specified"); + assertThat(commandErrorOutput.toString()) + .contains("No node permissioning contract address specified"); assertThat(commandOutput.toString()).isEmpty(); } @Test - public void permissionsEnabledWithInvalidContractAddressMustError() { + public void nodePermissionsEnabledWithInvalidContractAddressMustError() { parseCommand( "--permissions-nodes-contract-enabled", "--permissions-nodes-contract-address", @@ -373,7 +376,7 @@ public class PantheonCommandTest extends CommandTestAbstract { } @Test - public void permissionsEnabledWithTooShortContractAddressMustError() { + public void nodePermissionsEnabledWithTooShortContractAddressMustError() { parseCommand( "--permissions-nodes-contract-enabled", "--permissions-nodes-contract-address", "0x1234"); @@ -384,7 +387,7 @@ public class PantheonCommandTest extends CommandTestAbstract { } @Test - public void permissionsSmartContractMustUseOption() { + public void nodePermissionsSmartContractMustUseOption() { String smartContractAddress = "0x0000000000000000000000000000000000001234"; @@ -410,6 +413,75 @@ public class PantheonCommandTest extends CommandTestAbstract { assertThat(commandOutput.toString()).isEmpty(); } + @Test + public void accountPermissionsSmartContractWithoutOptionMustError() { + parseCommand("--permissions-accounts-contract-address"); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandErrorOutput.toString()) + .startsWith( + "Missing required parameter for option '--permissions-accounts-contract-address'"); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void accountPermissionsEnabledWithoutContractAddressMustError() { + parseCommand("--permissions-accounts-contract-enabled"); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandErrorOutput.toString()) + .contains("No account permissioning contract address specified"); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void accountPermissionsEnabledWithInvalidContractAddressMustError() { + parseCommand( + "--permissions-accounts-contract-enabled", + "--permissions-accounts-contract-address", + "invalid-smart-contract-address"); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandErrorOutput.toString()).contains("Invalid value"); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void accountPermissionsEnabledWithTooShortContractAddressMustError() { + parseCommand( + "--permissions-accounts-contract-enabled", + "--permissions-accounts-contract-address", + "0x1234"); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandErrorOutput.toString()).contains("Invalid value"); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void accountPermissionsSmartContractMustUseOption() { + String smartContractAddress = "0x0000000000000000000000000000000000001234"; + + parseCommand( + "--permissions-accounts-contract-enabled", + "--permissions-accounts-contract-address", + smartContractAddress); + final SmartContractPermissioningConfiguration smartContractPermissioningConfiguration = + new SmartContractPermissioningConfiguration(); + smartContractPermissioningConfiguration.setAccountSmartContractAddress( + Address.fromHexString(smartContractAddress)); + smartContractPermissioningConfiguration.setSmartContractAccountWhitelistEnabled(true); + + verify(mockController.getProtocolSchedule()).setTransactionFilter(any()); + + assertThat(commandErrorOutput.toString()).isEmpty(); + assertThat(commandOutput.toString()).isEmpty(); + } + @Test public void nodePermissioningTomlPathWithoutOptionMustDisplayUsage() { parseCommand("--permissions-nodes-config-file"); @@ -549,12 +621,11 @@ public class PantheonCommandTest extends CommandTestAbstract { Collections.singletonList("0x0000000000000000000000000000000000000009")); verify(mockRunnerBuilder) - .permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); + .accountPermissioningController(accountPermissioningControllerArgumentCaptor.capture()); - PermissioningConfiguration config = permissioningConfigurationArgumentCaptor.getValue(); - assertThat(config.getLocalConfig().get()) - .isEqualToComparingFieldByField(localPermissioningConfiguration); + AccountPermissioningController controller = + accountPermissioningControllerArgumentCaptor.getValue(); + assertThat(controller.getAccountLocalConfigPermissioningController()).isPresent(); assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(commandOutput.toString()).isEmpty(); diff --git a/pantheon/src/test/resources/everything_config.toml b/pantheon/src/test/resources/everything_config.toml index ace9b1bdc9..ec13a72937 100644 --- a/pantheon/src/test/resources/everything_config.toml +++ b/pantheon/src/test/resources/everything_config.toml @@ -84,6 +84,8 @@ permissions-accounts-config-file-enabled=false permissions-accounts-config-file="./permissions_config.toml" permissions-nodes-contract-enabled=false permissions-nodes-contract-address="0x0000000000000000000000000000000000001234" +permissions-accounts-contract-enabled=false +permissions-accounts-contract-address="0x0000000000000000000000000000000000006789" # Privacy privacy-url="http://127.0.0.1:8888"