Added NodeSmartContractV2PermissioningController (#1435)

* Added NodeSmartContractV2PermissioningController

Signed-off-by: Lucas Saldanha <lucascrsaldanha@gmail.com>
pull/1473/head
Lucas Saldanha 4 years ago committed by GitHub
parent 74bc50207a
commit 112e7535cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 37
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/condition/perm/NodeSmartContractPermissioningV2Conditions.java
  3. 4
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
  4. 10
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/permissioning/PermissionedNodeBuilder.java
  5. 98
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/perm/NodeSmartContractPermissioningAllowNodeV2Transaction.java
  6. 83
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/perm/NodeSmartContractPermissioningConnectionIsAllowedV2Transaction.java
  7. 98
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/perm/NodeSmartContractPermissioningForbidNodeV2Transaction.java
  8. 45
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/transaction/perm/NodeSmartContractPermissioningV2Transactions.java
  9. 42
      acceptance-tests/tests/simple-permissioning-smart-contract/contracts/SimpleNodePermissioningV2.sol
  10. 109
      acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/NodeSmartContractPermissioningV2AcceptanceTest.java
  11. 136
      acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/NodeSmartContractPermissioningV2AcceptanceTestBase.java
  12. 50
      acceptance-tests/tests/src/test/resources/permissioning/simple_permissioning_v2_genesis.json
  13. 8
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  14. 54
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  15. 1
      besu/src/test/resources/everything_config.toml
  16. 4
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulatorResult.java
  17. 3
      ethereum/permissioning/build.gradle
  18. 109
      ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/AbstractNodeSmartContractPermissioningController.java
  19. 46
      ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodePermissioningControllerFactory.java
  20. 70
      ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodeSmartContractPermissioningController.java
  21. 123
      ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodeSmartContractV2PermissioningController.java
  22. 9
      ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/SmartContractPermissioningConfiguration.java
  23. 173
      ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/NodeSmartContractV2PermissioningControllerTest.java
  24. 6
      ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/node/NodePermissioningControllerFactoryTest.java

@ -4,6 +4,7 @@
* `--random-peer-priority-enabled` flag added. Allows for incoming connections to be prioritized randomly. This will prevent (typically small, stable) networks from forming impenetrable peer cliques. [#1440](https://github.com/hyperledger/besu/pull/1440)
* Hide deprecated `--host-whitelist` option. [\#1444](https://github.com/hyperledger/besu/pull/1444)
* Prioritize high gas prices during mining. Previously we ordered only by the order in which the transactions were received. This will increase expected profit when mining. [\#1449](https://github.com/hyperledger/besu/pull/1449)
* Added support for the updated smart contract-based [node permissioning EEA interface](https://entethalliance.github.io/client-spec/spec.html#dfn-connectionallowed). [\#1435](https://github.com/hyperledger/besu/pull/1435)
## Deprecated and Scheduled for removal in _Next_ Release

@ -0,0 +1,37 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.dsl.condition.perm;
import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.perm.NodeSmartContractPermissioningV2Transactions;
public class NodeSmartContractPermissioningV2Conditions {
private final NodeSmartContractPermissioningV2Transactions transactions;
public NodeSmartContractPermissioningV2Conditions(
final NodeSmartContractPermissioningV2Transactions transactions) {
this.transactions = transactions;
}
public Condition connectionIsAllowed(final String address, final Node node) {
return new WaitForTrueResponse(transactions.isConnectionAllowed(address, node));
}
public Condition connectionIsForbidden(final String address, final Node node) {
return new WaitForFalseResponse(transactions.isConnectionAllowed(address, node));
}
}

@ -274,6 +274,10 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner {
params.add("--permissions-accounts-contract-address");
params.add(permissioningConfiguration.getAccountSmartContractAddress().toString());
}
params.add("--permissions-nodes-contract-version");
params.add(
String.valueOf(
permissioningConfiguration.getNodeSmartContractInterfaceVersion()));
});
params.addAll(node.getExtraCLIOptions());

@ -67,6 +67,7 @@ public class PermissionedNodeBuilder {
private boolean nodeSmartContractPermissioningEnabled = false;
private String nodePermissioningSmartContractAddress = null;
private int nodePermissioningSmartContractInterfaceVersion = 1;
private boolean accountSmartContractPermissioningEnabled = false;
private String accountPermissioningSmartContractAddress = null;
@ -130,6 +131,13 @@ public class PermissionedNodeBuilder {
return this;
}
public PermissionedNodeBuilder nodesContractV2Enabled(final String address) {
this.nodeSmartContractPermissioningEnabled = true;
this.nodePermissioningSmartContractAddress = address;
this.nodePermissioningSmartContractInterfaceVersion = 2;
return this;
}
public PermissionedNodeBuilder accountsContractEnabled(final String address) {
this.accountSmartContractPermissioningEnabled = true;
this.accountPermissioningSmartContractAddress = address;
@ -264,6 +272,8 @@ public class PermissionedNodeBuilder {
config.setSmartContractAccountAllowlistEnabled(true);
}
config.setNodeSmartContractInterfaceVersion(nodePermissioningSmartContractInterfaceVersion);
return config;
}

@ -0,0 +1,98 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.dsl.transaction.perm;
import static org.web3j.utils.Numeric.toHexString;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.NodeSmartContractV2PermissioningController;
import org.hyperledger.besu.tests.acceptance.dsl.account.Account;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import org.apache.tuweni.bytes.Bytes;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.datatypes.Function;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
public class NodeSmartContractPermissioningAllowNodeV2Transaction implements Transaction<Hash> {
private final Account sender;
private final Address contractAddress;
private final Node node;
public NodeSmartContractPermissioningAllowNodeV2Transaction(
final Account sender, final Address contractAddress, final Node node) {
this.sender = sender;
this.contractAddress = contractAddress;
this.node = node;
}
@Override
public Hash execute(final NodeRequests node) {
final String signedTransactionData = signedTransactionData();
try {
String hash =
node.eth().ethSendRawTransaction(signedTransactionData).send().getTransactionHash();
return Hash.fromHexString(hash);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private String signedTransactionData() {
final EnodeURL enodeURL = EnodeURL.fromURI(((RunnableNode) node).enodeUrl());
final Bytes payload = createPayload(enodeURL);
RawTransaction transaction =
RawTransaction.createTransaction(
sender.getNextNonce(),
BigInteger.valueOf(1_000L),
BigInteger.valueOf(3_000_000L),
contractAddress.toString(),
payload.toString());
return toHexString(
TransactionEncoder.signMessage(transaction, sender.web3jCredentialsOrThrow()));
}
private Bytes createPayload(final EnodeURL enodeUrl) {
try {
final String hexNodeIdString = enodeUrl.getNodeId().toUnprefixedHexString();
final byte[] ip = NodeSmartContractV2PermissioningController.encodeIp(enodeUrl.getIp());
final int port = enodeUrl.getListeningPortOrZero();
final Function addNodeFunction =
FunctionEncoder.makeFunction(
"addEnode",
List.of("string", "bytes16", "uint16"),
List.of(hexNodeIdString, ip, port),
Collections.emptyList());
return Bytes.fromHexString(FunctionEncoder.encode(addNodeFunction));
} catch (Exception e) {
throw new RuntimeException("Error adding node to allowlist", e);
}
}
}

@ -0,0 +1,83 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.dsl.transaction.perm;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.NodeSmartContractV2PermissioningController;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.apache.tuweni.bytes.Bytes;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.datatypes.Function;
import org.web3j.protocol.core.DefaultBlockParameterName;
public class NodeSmartContractPermissioningConnectionIsAllowedV2Transaction
implements Transaction<Boolean> {
private final Address contractAddress;
private final Node node;
public NodeSmartContractPermissioningConnectionIsAllowedV2Transaction(
final Address contractAddress, final Node node) {
this.contractAddress = contractAddress;
this.node = node;
}
@Override
public Boolean execute(final NodeRequests node) {
try {
final String value =
node.eth().ethCall(payload(), DefaultBlockParameterName.LATEST).send().getValue();
return Bytes.fromHexString(value)
.equals(NodeSmartContractV2PermissioningController.TRUE_RESPONSE);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private org.web3j.protocol.core.methods.request.Transaction payload() {
final EnodeURL enodeURL = EnodeURL.fromURI(((RunnableNode) node).enodeUrl());
final Bytes payload = createPayload(enodeURL);
return org.web3j.protocol.core.methods.request.Transaction.createFunctionCallTransaction(
null, null, null, null, contractAddress.toString(), payload.toString());
}
private Bytes createPayload(final EnodeURL enodeUrl) {
try {
final String hexNodeIdString = enodeUrl.getNodeId().toUnprefixedHexString();
final byte[] ip = NodeSmartContractV2PermissioningController.encodeIp(enodeUrl.getIp());
final int port = enodeUrl.getListeningPortOrZero();
final Function connectionAllowedFunction =
FunctionEncoder.makeFunction(
"connectionAllowed",
List.of("string", "bytes16", "uint16"),
List.of(hexNodeIdString, ip, port),
Collections.emptyList());
return Bytes.fromHexString(FunctionEncoder.encode(connectionAllowedFunction));
} catch (Exception e) {
throw new RuntimeException("Error calling connectionAllowed", e);
}
}
}

@ -0,0 +1,98 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.dsl.transaction.perm;
import static org.web3j.utils.Numeric.toHexString;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.NodeSmartContractV2PermissioningController;
import org.hyperledger.besu.tests.acceptance.dsl.account.Account;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import org.apache.tuweni.bytes.Bytes;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.datatypes.Function;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
public class NodeSmartContractPermissioningForbidNodeV2Transaction implements Transaction<Hash> {
private final Account sender;
private final Address contractAddress;
private final Node node;
public NodeSmartContractPermissioningForbidNodeV2Transaction(
final Account sender, final Address contractAddress, final Node node) {
this.sender = sender;
this.contractAddress = contractAddress;
this.node = node;
}
@Override
public Hash execute(final NodeRequests node) {
final String signedTransactionData = signedTransactionData();
try {
String hash =
node.eth().ethSendRawTransaction(signedTransactionData).send().getTransactionHash();
return Hash.fromHexString(hash);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private String signedTransactionData() {
final EnodeURL enodeURL = EnodeURL.fromURI(((RunnableNode) node).enodeUrl());
final Bytes payload = createPayload(enodeURL);
RawTransaction transaction =
RawTransaction.createTransaction(
sender.getNextNonce(),
BigInteger.valueOf(1000),
BigInteger.valueOf(3_000_000L),
contractAddress.toString(),
payload.toString());
return toHexString(
TransactionEncoder.signMessage(transaction, sender.web3jCredentialsOrThrow()));
}
private Bytes createPayload(final EnodeURL enodeUrl) {
try {
final String hexNodeIdString = enodeUrl.getNodeId().toUnprefixedHexString();
final byte[] ip = NodeSmartContractV2PermissioningController.encodeIp(enodeUrl.getIp());
final int port = enodeUrl.getListeningPortOrZero();
final Function removeNodeFunction =
FunctionEncoder.makeFunction(
"removeEnode",
List.of("string", "bytes16", "uint16"),
List.of(hexNodeIdString, ip, port),
Collections.emptyList());
return Bytes.fromHexString(FunctionEncoder.encode(removeNodeFunction));
} catch (Exception e) {
throw new RuntimeException("Error removing node from allowlist", e);
}
}
}

@ -0,0 +1,45 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.dsl.transaction.perm;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
public class NodeSmartContractPermissioningV2Transactions {
private final Accounts accounts;
public NodeSmartContractPermissioningV2Transactions(final Accounts accounts) {
this.accounts = accounts;
}
public Transaction<Hash> allowNode(final String contractAddress, final Node node) {
return new NodeSmartContractPermissioningAllowNodeV2Transaction(
accounts.getPrimaryBenefactor(), Address.fromHexString(contractAddress), node);
}
public Transaction<Hash> forbidNode(final String contractAddress, final Node node) {
return new NodeSmartContractPermissioningForbidNodeV2Transaction(
accounts.getPrimaryBenefactor(), Address.fromHexString(contractAddress), node);
}
public Transaction<Boolean> isConnectionAllowed(final String contractAddress, final Node node) {
return new NodeSmartContractPermissioningConnectionIsAllowedV2Transaction(
Address.fromHexString(contractAddress), node);
}
}

@ -0,0 +1,42 @@
pragma solidity >=0.4.0 <0.6.0;
// THIS CONTRACT IS FOR TESTING PURPOSES ONLY
// DO NOT USE THIS CONTRACT IN PRODUCTION APPLICATIONS
contract SimpleNodePermissioning {
/*
Node port is not being considered because ATs use random ports
*/
struct Enode {
string enodeId;
bytes16 enodeHost;
}
mapping(bytes => Enode) private allowlist;
// Port is being ignored
function connectionAllowed(string memory enodeId, bytes16 ip, uint16) public view returns (bool) {
return enodeAllowed(enodeId, ip);
}
function enodeAllowed(string memory enodeId, bytes16 sourceEnodeIp) private view returns (bool){
bytes memory key = computeKey(enodeId, sourceEnodeIp);
// if enode was found, host (bytes) is greater than zero
return allowlist[key].enodeHost > 0;
}
function addEnode(string memory enodeId, bytes16 enodeIp, uint16) public {
Enode memory newEnode = Enode(enodeId, enodeIp);
bytes memory key = computeKey(enodeId, enodeIp);
allowlist[key] = newEnode;
}
function removeEnode(string memory enodeId, bytes16 enodeIp, uint16) public {
bytes memory key = computeKey(enodeId, enodeIp);
Enode memory zeros = Enode("", bytes16(0));
// replace enode with "zeros"
allowlist[key] = zeros;
}
function computeKey(string memory enodeId, bytes16 enodeIp) private pure returns (bytes memory) {
return abi.encode(enodeId, enodeIp);
}
}

@ -0,0 +1,109 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.permissioning;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.junit.Before;
import org.junit.Test;
public class NodeSmartContractPermissioningV2AcceptanceTest
extends NodeSmartContractPermissioningV2AcceptanceTestBase {
private Node bootnode;
private Node permissionedNode;
private Node allowedNode;
private Node forbiddenNode;
@Before
public void setUp() {
bootnode = bootnode("bootnode");
forbiddenNode = node("forbidden-node");
allowedNode = node("allowed-node");
permissionedNode = permissionedNode("permissioned-node");
permissionedCluster.start(bootnode, forbiddenNode, allowedNode, permissionedNode);
// updating permissioning smart contract with allowed nodes
permissionedNode.execute(allowNode(bootnode));
permissionedNode.verify(connectionIsAllowed(bootnode));
permissionedNode.execute(allowNode(allowedNode));
permissionedNode.verify(connectionIsAllowed(allowedNode));
permissionedNode.execute(allowNode(permissionedNode));
permissionedNode.verify(connectionIsAllowed(permissionedNode));
}
@Test
public void permissionedNodeShouldPeerOnlyWithAllowedNodes() {
bootnode.verify(net.awaitPeerCount(3));
allowedNode.verify(net.awaitPeerCount(3));
forbiddenNode.verify(net.awaitPeerCount(2));
permissionedNode.verify(net.awaitPeerCount(2));
}
@Test
public void permissionedNodeShouldDisconnectFromNodeNotPermittedAnymore() {
permissionedNode.verify(admin.addPeer(bootnode));
permissionedNode.verify(admin.addPeer(allowedNode));
permissionedNode.verify(net.awaitPeerCount(2));
permissionedNode.execute(forbidNode(allowedNode));
permissionedNode.verify(connectionIsForbidden(allowedNode));
permissionedNode.verify(net.awaitPeerCount(1));
}
@Test
public void permissionedNodeShouldConnectToNewlyPermittedNode() {
permissionedNode.verify(admin.addPeer(bootnode));
permissionedNode.verify(admin.addPeer(allowedNode));
permissionedNode.verify(net.awaitPeerCount(2));
permissionedNode.execute(allowNode(forbiddenNode));
permissionedNode.verify(connectionIsAllowed(forbiddenNode));
permissionedNode.verify(admin.addPeer(forbiddenNode));
permissionedNode.verify(net.awaitPeerCount(3));
}
@Test
public void permissioningUpdatesPropagateThroughNetwork() {
permissionedNode.verify(admin.addPeer(bootnode));
permissionedNode.verify(admin.addPeer(allowedNode));
permissionedNode.verify(net.awaitPeerCount(2));
// permissioning changes in peer should propagate to permissioned node
allowedNode.execute(allowNode(forbiddenNode));
allowedNode.verify(connectionIsAllowed(forbiddenNode));
permissionedNode.verify(connectionIsAllowed(forbiddenNode));
permissionedNode.verify(admin.addPeer(forbiddenNode));
permissionedNode.verify(net.awaitPeerCount(3));
}
@Test
public void onChainPermissioningAllowlistShouldPersistAcrossRestarts() {
permissionedCluster.stop();
permissionedCluster.start(bootnode, forbiddenNode, allowedNode, permissionedNode);
permissionedNode.verify(connectionIsAllowed(allowedNode));
permissionedNode.verify(connectionIsAllowed(bootnode));
permissionedNode.verify(connectionIsAllowed(permissionedNode));
permissionedNode.verify(connectionIsForbidden(forbiddenNode));
}
}

@ -0,0 +1,136 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.tests.acceptance.permissioning;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase;
import org.hyperledger.besu.tests.acceptance.dsl.WaitUtils;
import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition;
import org.hyperledger.besu.tests.acceptance.dsl.condition.perm.NodeSmartContractPermissioningV2Conditions;
import org.hyperledger.besu.tests.acceptance.dsl.node.Node;
import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.Cluster;
import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfiguration;
import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfigurationBuilder;
import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.permissioning.PermissionedNodeBuilder;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.perm.NodeSmartContractPermissioningV2Transactions;
import java.io.IOException;
import java.math.BigInteger;
class NodeSmartContractPermissioningV2AcceptanceTestBase extends AcceptanceTestBase {
private final NodeSmartContractPermissioningV2Transactions smartContractNodePermissioningV2;
private final NodeSmartContractPermissioningV2Conditions
nodeSmartContractPermissioningConditionsV2;
protected static final String CONTRACT_ADDRESS = "0x0000000000000000000000000000000000009999";
protected static final String GENESIS_FILE =
"/permissioning/simple_permissioning_v2_genesis.json";
protected final Cluster permissionedCluster;
protected NodeSmartContractPermissioningV2AcceptanceTestBase() {
super();
smartContractNodePermissioningV2 = new NodeSmartContractPermissioningV2Transactions(accounts);
nodeSmartContractPermissioningConditionsV2 =
new NodeSmartContractPermissioningV2Conditions(smartContractNodePermissioningV2);
this.permissionedCluster = permissionedCluster();
}
private Cluster permissionedCluster() {
final ClusterConfiguration clusterConfiguration =
new ClusterConfigurationBuilder().awaitPeerDiscovery(false).build();
return new Cluster(clusterConfiguration, net);
}
protected Node permissionedNode(final String name, final Node... localConfigAllowedNodes) {
return permissionedNode(name, GENESIS_FILE, localConfigAllowedNodes);
}
protected Node permissionedNode(
final String name, final String genesisFile, final Node... localConfigAllowedNodes) {
PermissionedNodeBuilder permissionedNodeBuilder =
this.permissionedNodeBuilder
.name(name)
.genesisFile(genesisFile)
.nodesContractV2Enabled(CONTRACT_ADDRESS);
if (localConfigAllowedNodes != null && localConfigAllowedNodes.length > 0) {
permissionedNodeBuilder.nodesPermittedInConfig(localConfigAllowedNodes);
}
return permissionedNodeBuilder.build();
}
protected Node bootnode(final String name) {
return bootnode(name, GENESIS_FILE);
}
protected Node bootnode(final String name, final String genesisFile) {
try {
return besu.createCustomGenesisNode(name, genesisFile, true);
} catch (IOException e) {
throw new RuntimeException("Error creating node", e);
}
}
protected Node node(final String name) {
try {
return besu.createCustomGenesisNode(name, GENESIS_FILE, false);
} catch (IOException e) {
throw new RuntimeException("Error creating node", e);
}
}
protected Node miner(final String name) {
try {
return besu.createCustomGenesisNode(name, GENESIS_FILE, false, true);
} catch (IOException e) {
throw new RuntimeException("Error creating node", e);
}
}
@Override
public void tearDownAcceptanceTestBase() {
permissionedCluster.stop();
super.tearDownAcceptanceTestBase();
}
protected Transaction<Hash> allowNode(final Node node) {
return smartContractNodePermissioningV2.allowNode(CONTRACT_ADDRESS, node);
}
protected Transaction<Hash> forbidNode(final Node node) {
return smartContractNodePermissioningV2.forbidNode(CONTRACT_ADDRESS, node);
}
protected Condition connectionIsForbidden(final Node node) {
return nodeSmartContractPermissioningConditionsV2.connectionIsForbidden(CONTRACT_ADDRESS, node);
}
protected Condition connectionIsAllowed(final Node node) {
return nodeSmartContractPermissioningConditionsV2.connectionIsAllowed(CONTRACT_ADDRESS, node);
}
protected void waitForBlockHeight(final Node node, final long blockchainHeight) {
WaitUtils.waitFor(
120,
() ->
assertThat(node.execute(ethTransactions.blockNumber()))
.isGreaterThanOrEqualTo(BigInteger.valueOf(blockchainHeight)));
}
}

@ -0,0 +1,50 @@
{
"config": {
"chainId": 999,
"homesteadBlock": 0,
"daoForkBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"constantinopleFixBlock": 0,
"ethash": {
"fixeddifficulty": 100
}
},
"nonce": "0x42",
"timestamp": "0x0",
"extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
"gasLimit": "0x1000000",
"difficulty": "0x10000",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
"privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63",
"comment": "private key and this comment are ignoredS. In a real chain, the private key should NOT be stored",
"balance": "0xad78ebc5ac6200000"
},
"627306090abaB3A6e1400e9345bC60c78a8BEf57": {
"privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
},
"0x0000000000000000000000000000000000009999": {
"comment": "Simple node permissioning smart contract V2",
"balance": "0",
"code": "608060405234801561001057600080fd5b50600436106100415760003560e01c80634df2430d146100465780636868e0d114610144578063d0515df61461022a575b600080fd5b61012a6004803603606081101561005c57600080fd5b810190808035906020019064010000000081111561007957600080fd5b82018360208201111561008b57600080fd5b803590602001918460018302840111640100000000831117156100ad57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929080356fffffffffffffffffffffffffffffffff19169060200190929190803561ffff169060200190929190505050610310565b604051808215151515815260200191505060405180910390f35b6102286004803603606081101561015a57600080fd5b810190808035906020019064010000000081111561017757600080fd5b82018360208201111561018957600080fd5b803590602001918460018302840111640100000000831117156101ab57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929080356fffffffffffffffffffffffffffffffff19169060200190929190803561ffff169060200190929190505050610325565b005b61030e6004803603606081101561024057600080fd5b810190808035906020019064010000000081111561025d57600080fd5b82018360208201111561026f57600080fd5b8035906020019184600183028401116401000000008311171561029157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929080356fffffffffffffffffffffffffffffffff19169060200190929190803561ffff169060200190929190505050610439565b005b600061031c848461053a565b90509392505050565b606061033184846105e6565b905061033b6106a3565b6040518060400160405280604051806020016040528060008152508152602001600060801b6fffffffffffffffffffffffffffffffff19168152509050806000836040518082805190602001908083835b602083106103af578051825260208201915060208101905060208303925061038c565b6001836020036101000a038019825116818451168082178552505050505050905001915050908152602001604051809103902060008201518160000190805190602001906103fe9291906106d0565b5060208201518160010160006101000a8154816fffffffffffffffffffffffffffffffff021916908360801c02179055509050505050505050565b6104416106a3565b6040518060400160405280858152602001846fffffffffffffffffffffffffffffffff19168152509050606061047785856105e6565b9050816000826040518082805190602001908083835b602083106104b0578051825260208201915060208101905060208303925061048d565b6001836020036101000a038019825116818451168082178552505050505050905001915050908152602001604051809103902060008201518160000190805190602001906104ff9291906106d0565b5060208201518160010160006101000a8154816fffffffffffffffffffffffffffffffff021916908360801c02179055509050505050505050565b6000606061054884846105e6565b9050600060801b6000826040518082805190602001908083835b602083106105855780518252602082019150602081019050602083039250610562565b6001836020036101000a038019825116818451168082178552505050505050905001915050908152602001604051809103902060010160009054906101000a900460801b6fffffffffffffffffffffffffffffffff19161191505092915050565b606082826040516020018080602001836fffffffffffffffffffffffffffffffff19166fffffffffffffffffffffffffffffffff19168152602001828103825284818151815260200191508051906020019080838360005b8381101561065957808201518184015260208101905061063e565b50505050905090810190601f1680156106865780820380516001836020036101000a031916815260200191505b509350505050604051602081830303815290604052905092915050565b60405180604001604052806060815260200160006fffffffffffffffffffffffffffffffff191681525090565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061071157805160ff191683800117855561073f565b8280016001018555821561073f579182015b8281111561073e578251825591602001919060010190610723565b5b50905061074c9190610750565b5090565b61077291905b8082111561076e576000816000905550600101610756565b5090565b9056fea265627a7a7230582059e21fece50f9faa9389e7ddaac9b6e34e20f4283c820bef12ea74bb18ecd7a664736f6c63430005090032",
"storage": {}
},
"0x0000000000000000000000000000000000008888": {
"comment": "Simple account permissioning smart contract",
"balance": "0",
"code": "608060405260043610610067576000357c010000000000000000000000000000000000000000000000000000000090048063936421d51461006c578063c4740a95146101aa578063de8fa431146101fb578063e1ab6e8e14610226578063e89b0e1e1461028f575b600080fd5b34801561007857600080fd5b50610190600480360360c081101561008f57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919080359060200190929190803590602001909291908035906020019064010000000081111561010a57600080fd5b82018360208201111561011c57600080fd5b8035906020019184600183028401116401000000008311171561013e57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506102e0565b604051808215151515815260200191505060405180910390f35b3480156101b657600080fd5b506101f9600480360360208110156101cd57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506102f7565b005b34801561020757600080fd5b506102106103b6565b6040518082815260200191505060405180910390f35b34801561023257600080fd5b506102756004803603602081101561024957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506103c0565b604051808215151515815260200191505060405180910390f35b34801561029b57600080fd5b506102de600480360360208110156102b257600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610429565b005b60006102eb876103c0565b90509695505050505050565b6000808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16156103b35760008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550600160008154809291906001900391905055505b50565b6000600154905090565b60008060015414156103d55760019050610424565b6000808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff1690505b919050565b6000808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff1615156104e55760016000808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055506001600081548092919060010191905055505b5056fea165627a7a72305820b2abf4388b5665d346f59eb2f3cdf3209a76e6dca7252764df886e9eef811bd50029",
"storage": {}
}
},
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}

@ -832,6 +832,11 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
arity = "1")
private final Address permissionsNodesContractAddress = null;
@Option(
names = {"--permissions-nodes-contract-version"},
description = "Version of the EEA Node Permissioning interface (default: ${DEFAULT-VALUE})")
private final Integer permissionsNodesContractVersion = 1;
@Option(
names = {"--permissions-nodes-contract-enabled"},
description = "Enable node level permissions via smart contract (default: ${DEFAULT-VALUE})")
@ -1787,6 +1792,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
permissionsNodesContractEnabled);
smartContractPermissioningConfiguration.setNodeSmartContractAddress(
permissionsNodesContractAddress);
smartContractPermissioningConfiguration.setNodeSmartContractInterfaceVersion(
permissionsNodesContractVersion);
}
} else if (permissionsNodesContractAddress != null) {
logger.warn(
@ -2292,6 +2299,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
}
private class BesuCommandConfigurationService implements BesuConfiguration {
@Override
public Path getStoragePath() {
return dataDir().resolve(DATABASE_PATH);

@ -426,6 +426,60 @@ public class BesuCommandTest extends CommandTestAbstract {
assertThat(commandOutput.toString()).isEmpty();
}
@Test
public void nodePermissionsContractVersionDefaultValue() {
final SmartContractPermissioningConfiguration expectedConfig =
new SmartContractPermissioningConfiguration();
expectedConfig.setNodeSmartContractAddress(
Address.fromHexString("0x0000000000000000000000000000000000001234"));
expectedConfig.setSmartContractNodeAllowlistEnabled(true);
expectedConfig.setNodeSmartContractInterfaceVersion(1);
parseCommand(
"--permissions-nodes-contract-enabled",
"--permissions-nodes-contract-address",
"0x0000000000000000000000000000000000001234");
verify(mockRunnerBuilder)
.permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
final PermissioningConfiguration config = permissioningConfigurationArgumentCaptor.getValue();
assertThat(config.getSmartContractConfig().get())
.isEqualToComparingFieldByField(expectedConfig);
assertThat(commandErrorOutput.toString()).isEmpty();
assertThat(commandOutput.toString()).isEmpty();
}
@Test
public void nodePermissionsContractVersionSetsValue() {
final SmartContractPermissioningConfiguration expectedConfig =
new SmartContractPermissioningConfiguration();
expectedConfig.setNodeSmartContractAddress(
Address.fromHexString("0x0000000000000000000000000000000000001234"));
expectedConfig.setSmartContractNodeAllowlistEnabled(true);
expectedConfig.setNodeSmartContractInterfaceVersion(2);
parseCommand(
"--permissions-nodes-contract-enabled",
"--permissions-nodes-contract-address",
"0x0000000000000000000000000000000000001234",
"--permissions-nodes-contract-version",
"2");
verify(mockRunnerBuilder)
.permissioningConfiguration(permissioningConfigurationArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
final PermissioningConfiguration config = permissioningConfigurationArgumentCaptor.getValue();
assertThat(config.getSmartContractConfig().get())
.isEqualToComparingFieldByField(expectedConfig);
assertThat(commandErrorOutput.toString()).isEmpty();
assertThat(commandOutput.toString()).isEmpty();
}
@Test
public void accountPermissionsSmartContractWithoutOptionMustError() {
parseCommand("--permissions-accounts-contract-address");

@ -124,6 +124,7 @@ permissions-accounts-config-file-enabled=false
permissions-accounts-config-file="./permissions_config.toml"
permissions-nodes-contract-enabled=false
permissions-nodes-contract-address="0x0000000000000000000000000000000000001234"
permissions-nodes-contract-version=1
permissions-accounts-contract-enabled=false
permissions-accounts-contract-address="0x0000000000000000000000000000000000006789"

@ -21,6 +21,7 @@ import org.hyperledger.besu.ethereum.mainnet.ValidationResult;
import java.util.Objects;
import com.google.common.annotations.VisibleForTesting;
import org.apache.tuweni.bytes.Bytes;
public class TransactionSimulatorResult {
@ -28,7 +29,8 @@ public class TransactionSimulatorResult {
private final Transaction transaction;
private final TransactionProcessor.Result result;
TransactionSimulatorResult(
@VisibleForTesting
public TransactionSimulatorResult(
final Transaction transaction, final TransactionProcessor.Result result) {
this.transaction = transaction;
this.result = result;

@ -39,10 +39,11 @@ dependencies {
implementation 'org.apache.tuweni:tuweni-bytes'
implementation 'org.apache.tuweni:tuweni-toml'
implementation 'org.apache.tuweni:tuweni-units'
implementation 'org.web3j:abi:5.0.0'
testImplementation project(':config')
testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
testImplementation project(':testutil')
testImplementation 'io.vertx:vertx-core'
testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core'

@ -0,0 +1,109 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.permissioning;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.node.NodePermissioningProvider;
import org.hyperledger.besu.ethereum.transaction.CallParameter;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
public abstract class AbstractNodeSmartContractPermissioningController
implements NodePermissioningProvider {
protected final Address contractAddress;
protected final TransactionSimulator transactionSimulator;
private final Counter checkCounter;
private final Counter checkCounterPermitted;
private final Counter checkCounterUnpermitted;
/**
* Creates a permissioning controller attached to a blockchain
*
* @param contractAddress The address at which the permissioning smart contract resides
* @param transactionSimulator A transaction simulator with attached blockchain and world state
* @param metricsSystem The metrics provider that is to be reported to
*/
protected AbstractNodeSmartContractPermissioningController(
final Address contractAddress,
final TransactionSimulator transactionSimulator,
final MetricsSystem metricsSystem) {
this.contractAddress = contractAddress;
this.transactionSimulator = transactionSimulator;
this.checkCounter =
metricsSystem.createCounter(
BesuMetricCategory.PERMISSIONING,
"node_smart_contract_check_count",
"Number of times the node smart contract permissioning provider has been checked");
this.checkCounterPermitted =
metricsSystem.createCounter(
BesuMetricCategory.PERMISSIONING,
"node_smart_contract_check_count_permitted",
"Number of times the node smart contract permissioning provider has been checked and returned permitted");
this.checkCounterUnpermitted =
metricsSystem.createCounter(
BesuMetricCategory.PERMISSIONING,
"node_smart_contract_check_count_unpermitted",
"Number of times the node smart contract permissioning provider has been checked and returned unpermitted");
}
/**
* Check whether a given connection from the source to destination enode should be permitted
*
* @param sourceEnode The enode url of the node initiating the connection
* @param destinationEnode The enode url of the node receiving the connection
* @return boolean of whether or not to permit the connection to occur
*/
@Override
public boolean isPermitted(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
this.checkCounter.inc();
if (!isContractDeployed()) {
throw new IllegalStateException("Permissioning contract does not exist");
}
if (checkSmartContractRules(sourceEnode, destinationEnode)) {
this.checkCounterPermitted.inc();
return true;
} else {
this.checkCounterUnpermitted.inc();
return false;
}
}
private boolean isContractDeployed() {
final Optional<Boolean> contractExists =
transactionSimulator.doesAddressExistAtHead(contractAddress);
return contractExists.isPresent() && contractExists.get();
}
abstract boolean checkSmartContractRules(
final EnodeURL sourceEnode, final EnodeURL destinationEnode);
protected CallParameter buildCallParameters(final Bytes payload) {
// Call parameters for simulation don't need other parameters, only the address and the payload
return new CallParameter(null, contractAddress, -1, null, null, payload);
}
}

@ -14,6 +14,7 @@
*/
package org.hyperledger.besu.ethereum.permissioning;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.Synchronizer;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.node.NodePermissioningController;
@ -65,15 +66,9 @@ public class NodePermissioningControllerFactory {
.getSmartContractConfig()
.get()
.isSmartContractNodeAllowlistEnabled()) {
NodeSmartContractPermissioningController smartContractProvider =
new NodeSmartContractPermissioningController(
permissioningConfiguration
.getSmartContractConfig()
.get()
.getNodeSmartContractAddress(),
transactionSimulator,
metricsSystem);
providers.add(smartContractProvider);
configureNodePermissioningSmartContractProvider(
permissioningConfiguration, transactionSimulator, metricsSystem, providers);
if (fixedNodes.isEmpty()) {
syncStatusProviderOptional = Optional.empty();
@ -101,6 +96,39 @@ public class NodePermissioningControllerFactory {
return nodePermissioningController;
}
private void configureNodePermissioningSmartContractProvider(
final PermissioningConfiguration permissioningConfiguration,
final TransactionSimulator transactionSimulator,
final MetricsSystem metricsSystem,
final List<NodePermissioningProvider> providers) {
final SmartContractPermissioningConfiguration smartContractPermissioningConfig =
permissioningConfiguration.getSmartContractConfig().get();
final Address nodePermissioningSmartContractAddress =
smartContractPermissioningConfig.getNodeSmartContractAddress();
final NodePermissioningProvider smartContractProvider;
switch (smartContractPermissioningConfig.getNodeSmartContractInterfaceVersion()) {
case 1:
{
smartContractProvider =
new NodeSmartContractPermissioningController(
nodePermissioningSmartContractAddress, transactionSimulator, metricsSystem);
break;
}
case 2:
{
smartContractProvider =
new NodeSmartContractV2PermissioningController(
nodePermissioningSmartContractAddress, transactionSimulator, metricsSystem);
break;
}
default:
throw new IllegalStateException(
"Invalid node Smart contract permissioning interface version");
}
providers.add(smartContractProvider);
}
private void validatePermissioningContract(
final NodePermissioningController nodePermissioningController) {
LOG.debug("Validating onchain node permissioning smart contract configuration");

@ -19,35 +19,29 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import org.hyperledger.besu.crypto.Hash;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.permissioning.node.NodePermissioningProvider;
import org.hyperledger.besu.ethereum.transaction.CallParameter;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.Counter;
import java.net.InetAddress;
import java.util.Optional;
import com.google.common.annotations.VisibleForTesting;
import org.apache.tuweni.bytes.Bytes;
/**
* Controller that can read from a smart contract that exposes the permissioning call
* connectionAllowed(bytes32,bytes32,bytes16,uint16,bytes32,bytes32,bytes16,uint16)
*/
public class NodeSmartContractPermissioningController implements NodePermissioningProvider {
private final Address contractAddress;
private final TransactionSimulator transactionSimulator;
public class NodeSmartContractPermissioningController
extends AbstractNodeSmartContractPermissioningController {
// full function signature for connection allowed call
private static final String FUNCTION_SIGNATURE =
"connectionAllowed(bytes32,bytes32,bytes16,uint16,bytes32,bytes32,bytes16,uint16)";
// hashed function signature for connection allowed call
private static final Bytes FUNCTION_SIGNATURE_HASH = hashSignature(FUNCTION_SIGNATURE);
private final Counter checkCounter;
private final Counter checkCounterPermitted;
private final Counter checkCounterUnpermitted;
// The first 4 bytes of the hash of the full textual signature of the function is used in
// contract calls to determine the function being called
@ -61,57 +55,17 @@ public class NodeSmartContractPermissioningController implements NodePermissioni
private static final Bytes FALSE_RESPONSE =
Bytes.fromHexString("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
/**
* Creates a permissioning controller attached to a blockchain
*
* @param contractAddress The address at which the permissioning smart contract resides
* @param transactionSimulator A transaction simulator with attached blockchain and world state
* @param metricsSystem The metrics provider that is to be reported to
*/
public NodeSmartContractPermissioningController(
final Address contractAddress,
final TransactionSimulator transactionSimulator,
final MetricsSystem metricsSystem) {
this.contractAddress = contractAddress;
this.transactionSimulator = transactionSimulator;
this.checkCounter =
metricsSystem.createCounter(
BesuMetricCategory.PERMISSIONING,
"node_smart_contract_check_count",
"Number of times the node smart contract permissioning provider has been checked");
this.checkCounterPermitted =
metricsSystem.createCounter(
BesuMetricCategory.PERMISSIONING,
"node_smart_contract_check_count_permitted",
"Number of times the node smart contract permissioning provider has been checked and returned permitted");
this.checkCounterUnpermitted =
metricsSystem.createCounter(
BesuMetricCategory.PERMISSIONING,
"node_smart_contract_check_count_unpermitted",
"Number of times the node smart contract permissioning provider has been checked and returned unpermitted");
super(contractAddress, transactionSimulator, metricsSystem);
}
/**
* Check whether a given connection from the source to destination enode should be permitted
*
* @param sourceEnode The enode url of the node initiating the connection
* @param destinationEnode The enode url of the node receiving the connection
* @return boolean of whether or not to permit the connection to occur
*/
@Override
public boolean isPermitted(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
this.checkCounter.inc();
boolean checkSmartContractRules(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
final Bytes payload = createPayload(sourceEnode, destinationEnode);
final CallParameter callParams =
new CallParameter(null, contractAddress, -1, null, null, payload);
final Optional<Boolean> contractExists =
transactionSimulator.doesAddressExistAtHead(contractAddress);
if (contractExists.isPresent() && !contractExists.get()) {
throw new IllegalStateException("Permissioning contract does not exist");
}
final CallParameter callParams = buildCallParameters(payload);
final Optional<TransactionSimulatorResult> result =
transactionSimulator.processAtHead(callParams);
@ -127,13 +81,7 @@ public class NodeSmartContractPermissioningController implements NodePermissioni
}
}
if (result.map(r -> checkTransactionResult(r.getOutput())).orElse(false)) {
this.checkCounterPermitted.inc();
return true;
} else {
this.checkCounterUnpermitted.inc();
return false;
}
return result.map(r -> checkTransactionResult(r.getOutput())).orElse(false);
}
// Checks the returned bytes from the permissioning contract call to see if it's a value we
@ -157,16 +105,18 @@ public class NodeSmartContractPermissioningController implements NodePermissioni
}
// Assemble the bytevalue payload to call the contract
public static Bytes createPayload(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
private static Bytes createPayload(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
return createPayload(FUNCTION_SIGNATURE_HASH, sourceEnode, destinationEnode);
}
@VisibleForTesting
public static Bytes createPayload(
final Bytes signature, final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
return Bytes.concatenate(
signature, encodeEnodeUrl(sourceEnode), encodeEnodeUrl(destinationEnode));
}
@VisibleForTesting
public static Bytes createPayload(final Bytes signature, final EnodeURL enodeURL) {
return Bytes.concatenate(signature, encodeEnodeUrl(enodeURL));
}

@ -0,0 +1,123 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.permissioning;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.transaction.CallParameter;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.net.InetAddress;
import java.util.List;
import com.google.common.annotations.VisibleForTesting;
import org.apache.tuweni.bytes.Bytes;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeEncoder;
import org.web3j.abi.datatypes.Bool;
import org.web3j.abi.datatypes.Function;
/**
* Controller that can read from a smart contract that exposes the EEA node permissioning v2 call
* connectionAllowed(string,bytes16,uint16)
*/
public class NodeSmartContractV2PermissioningController
extends AbstractNodeSmartContractPermissioningController {
public static final Bytes TRUE_RESPONSE = Bytes.fromHexString(TypeEncoder.encode(new Bool(true)));
public static final Bytes FALSE_RESPONSE =
Bytes.fromHexString(TypeEncoder.encode(new Bool(false)));
public NodeSmartContractV2PermissioningController(
final Address contractAddress,
final TransactionSimulator transactionSimulator,
final MetricsSystem metricsSystem) {
super(contractAddress, transactionSimulator, metricsSystem);
}
@Override
boolean checkSmartContractRules(final EnodeURL sourceEnode, final EnodeURL destinationEnode) {
return isPermitted(sourceEnode) && isPermitted(destinationEnode);
}
private boolean isPermitted(final EnodeURL enode) {
final Bytes payload = createPayload(enode);
final CallParameter callParams = buildCallParameters(payload);
return transactionSimulator.processAtHead(callParams).map(this::parseResult).orElse(false);
}
private Bytes createPayload(final EnodeURL enodeUrl) {
try {
final String hexNodeIdString = enodeUrl.getNodeId().toUnprefixedHexString();
final byte[] ip = encodeIp(enodeUrl.getIp());
final int port = enodeUrl.getListeningPortOrZero();
final Function connectionAllowedFunction =
FunctionEncoder.makeFunction(
"connectionAllowed",
List.of("string", "bytes16", "uint16"),
List.of(hexNodeIdString, ip, port),
List.of(Bool.TYPE_NAME));
return Bytes.fromHexString(FunctionEncoder.encode(connectionAllowedFunction));
} catch (Exception e) {
throw new RuntimeException(
"Error building payload to call node permissioning smart contract", e);
}
}
/*
* IPv4-Compatible IPv6 Address That hybrid address consists of 10 "0" bytes, followed by two "1"
* bytes, followed by the original 4-bytes IPv4 address (RFC 4291 Section 2.5.5.1 -
* https://tools.ietf.org/html/rfc4291#section-2.5.5)
*/
@VisibleForTesting
public static byte[] encodeIp(final InetAddress addr) {
// InetAddress deals with giving us the right number of bytes
final byte[] address = addr.getAddress();
final byte[] res = new byte[16];
if (address.length == 4) {
// lead with 10 bytes of 0's then 2 bytes of 1's
res[10] = (byte) 0xFF;
res[11] = (byte) 0xFF;
// then the ipv4
System.arraycopy(address, 0, res, 12, 4);
} else {
System.arraycopy(address, 0, res, 0, address.length);
}
return res;
}
private boolean parseResult(final TransactionSimulatorResult result) {
switch (result.getResult().getStatus()) {
case INVALID:
throw new IllegalStateException("Invalid node permissioning smart contract call");
case FAILED:
throw new IllegalStateException("Failed node permissioning smart contract call");
default:
break;
}
if (result.getOutput().equals(TRUE_RESPONSE)) {
return true;
} else if (result.getOutput().equals(FALSE_RESPONSE)) {
return false;
} else {
throw new IllegalStateException("Unexpected result from node permissioning smart contract");
}
}
}

@ -19,6 +19,7 @@ import org.hyperledger.besu.ethereum.core.Address;
public class SmartContractPermissioningConfiguration {
private boolean smartContractNodeAllowlistEnabled;
private Address nodeSmartContractAddress;
private int nodeSmartContractInterfaceVersion = 1;
private boolean smartContractAccountAllowlistEnabled;
private Address accountSmartContractAddress;
@ -60,4 +61,12 @@ public class SmartContractPermissioningConfiguration {
public void setAccountSmartContractAddress(final Address accountSmartContractAddress) {
this.accountSmartContractAddress = accountSmartContractAddress;
}
public void setNodeSmartContractInterfaceVersion(final int nodeSmartContractInterfaceVersion) {
this.nodeSmartContractInterfaceVersion = nodeSmartContractInterfaceVersion;
}
public int getNodeSmartContractInterfaceVersion() {
return nodeSmartContractInterfaceVersion;
}
}

@ -0,0 +1,173 @@
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.permissioning;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.BlockDataGenerator;
import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor.Result;
import org.hyperledger.besu.ethereum.mainnet.TransactionValidator.TransactionInvalidReason;
import org.hyperledger.besu.ethereum.mainnet.ValidationResult;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.transaction.CallParameter;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulator;
import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import org.junit.Before;
import org.junit.Test;
public class NodeSmartContractV2PermissioningControllerTest {
/*
Payloads created using Remix to call method connectionAllowed(string, bytes16, uint16)
*/
private static final Bytes SOURCE_ENODE_EXPECTED_PAYLOAD =
Bytes.fromHexString(
"0x4df2430d000000000000000000000000000000000000000000000000000000000000006000000000000000000000ffff7f00000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000765f00000000000000000000000000000000000000000000000000000000000000806663626539663833323138343837623363306235303837383139333838306536633235636664383637303863306130626630636139316630636536333337343661383932666532343061666135623961383830623862636134386538613232373034656639333766646461326437636336336534643431656431623431376165");
private static final Bytes DESTINATION_ENODE_EXPECTED_PAYLOAD =
Bytes.fromHexString(
"0x4df2430d000000000000000000000000000000000000000000000000000000000000006000000000000000000000ffff7f000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009dd400000000000000000000000000000000000000000000000000000000000000803335343863383762393932306666313661613462646366303163383566323531313761323961653135373464373539626164343863633934363364386539663763336331643165396662306432386537333839383935316639306530323731346162623737306664366432326539303337313838326134353635383830306539");
private static final EnodeURL SOURCE_ENODE_IPV4 =
EnodeURL.fromString(
"enode://fcbe9f83218487b3c0b50878193880e6c25cfd86708c0a0bf0ca91f0ce633746a892fe240afa5b9a880b8bca48e8a22704ef937fdda2d7cc63e4d41ed1b417ae@127.0.0.1:30303");
private static final EnodeURL DESTINATION_ENODE_IPV4 =
EnodeURL.fromString(
"enode://3548c87b9920ff16aa4bdcf01c85f25117a29ae1574d759bad48cc9463d8e9f7c3c1d1e9fb0d28e73898951f90e02714abb770fd6d22e90371882a45658800e9@127.0.0.1:40404");
private static final EnodeURL SOURCE_ENODE_IPV6 =
EnodeURL.fromString(
"enode://fcbe9f83218487b3c0b50878193880e6c25cfd86708c0a0bf0ca91f0ce633746a892fe240afa5b9a880b8bca48e8a22704ef937fdda2d7cc63e4d41ed1b417ae@[::ffff:7f00:0001]:30303");
private static final EnodeURL DESTINATION_ENODE_IPV6 =
EnodeURL.fromString(
"enode://3548c87b9920ff16aa4bdcf01c85f25117a29ae1574d759bad48cc9463d8e9f7c3c1d1e9fb0d28e73898951f90e02714abb770fd6d22e90371882a45658800e9@[::ffff:7f00:0001]:40404");
private final BlockDataGenerator blockDataGenerator = new BlockDataGenerator();
private final Address contractAddress = Address.ZERO;
private final TransactionSimulator transactionSimulator = mock(TransactionSimulator.class);
private final MetricsSystem metricsSystem = new NoOpMetricsSystem();
private NodeSmartContractV2PermissioningController permissioningController;
@Before
public void beforeEach() {
permissioningController =
new NodeSmartContractV2PermissioningController(
contractAddress, transactionSimulator, metricsSystem);
}
@Test
public void nonExpectedCallOutputThrowsIllegalState() {
final TransactionSimulatorResult txSimulatorResult =
transactionSimulatorResult(Bytes.random(10), ValidationResult.valid());
when(transactionSimulator.processAtHead(eq(callParams(SOURCE_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
assertThatIllegalStateException()
.isThrownBy(
() ->
permissioningController.checkSmartContractRules(
SOURCE_ENODE_IPV4, DESTINATION_ENODE_IPV4));
}
@Test
public void falseCallOutputReturnsNotPermitted() {
final TransactionSimulatorResult txSimulatorResult =
transactionSimulatorResult(
NodeSmartContractV2PermissioningController.FALSE_RESPONSE, ValidationResult.valid());
when(transactionSimulator.processAtHead(eq(callParams(SOURCE_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
boolean isPermitted =
permissioningController.checkSmartContractRules(SOURCE_ENODE_IPV4, DESTINATION_ENODE_IPV4);
assertThat(isPermitted).isFalse();
}
@Test
public void failedCallOutputReturnsNotPermitted() {
final TransactionSimulatorResult txSimulatorResult =
transactionSimulatorResult(
NodeSmartContractV2PermissioningController.TRUE_RESPONSE,
ValidationResult.invalid(TransactionInvalidReason.INTERNAL_ERROR));
when(transactionSimulator.processAtHead(eq(callParams(SOURCE_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
boolean isPermitted =
permissioningController.checkSmartContractRules(SOURCE_ENODE_IPV4, DESTINATION_ENODE_IPV4);
assertThat(isPermitted).isFalse();
}
@Test
public void expectedPayloadWhenCheckingPermissioningWithIPV4() {
final TransactionSimulatorResult txSimulatorResult =
transactionSimulatorResult(
NodeSmartContractV2PermissioningController.TRUE_RESPONSE, ValidationResult.valid());
when(transactionSimulator.processAtHead(eq(callParams(SOURCE_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
when(transactionSimulator.processAtHead(eq(callParams(DESTINATION_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
boolean isPermitted =
permissioningController.checkSmartContractRules(SOURCE_ENODE_IPV4, DESTINATION_ENODE_IPV4);
assertThat(isPermitted).isTrue();
verify(transactionSimulator, times(2)).processAtHead(any());
}
@Test
public void expectedPayloadWhenCheckingPermissioningWithIPV6() {
final TransactionSimulatorResult txSimulatorResult =
transactionSimulatorResult(
NodeSmartContractV2PermissioningController.TRUE_RESPONSE, ValidationResult.valid());
when(transactionSimulator.processAtHead(eq(callParams(SOURCE_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
when(transactionSimulator.processAtHead(eq(callParams(DESTINATION_ENODE_EXPECTED_PAYLOAD))))
.thenReturn(Optional.of(txSimulatorResult));
boolean isPermitted =
permissioningController.checkSmartContractRules(SOURCE_ENODE_IPV6, DESTINATION_ENODE_IPV6);
assertThat(isPermitted).isTrue();
verify(transactionSimulator, times(2)).processAtHead(any());
}
private CallParameter callParams(final Bytes payload) {
return new CallParameter(null, contractAddress, -1, null, null, payload);
}
private TransactionSimulatorResult transactionSimulatorResult(
final Bytes output, final ValidationResult<TransactionInvalidReason> validationResult) {
return new TransactionSimulatorResult(
blockDataGenerator.transaction(),
Result.successful(blockDataGenerator.logs(1, 1), 0L, 0L, output, validationResult));
}
}

@ -36,6 +36,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@ -55,6 +56,11 @@ public class NodePermissioningControllerFactoryTest {
SmartContractPermissioningConfiguration smartContractPermissioningConfiguration;
PermissioningConfiguration config;
@Before
public void before() {
when(transactionSimulator.doesAddressExistAtHead(any())).thenReturn(Optional.of(true));
}
@Test
public void testCreateWithNeitherPermissioningEnabled() {
config = new PermissioningConfiguration(Optional.empty(), Optional.empty());

Loading…
Cancel
Save