Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Commit

Permalink
[PAN-2612] Transaction smart contract permissioning controller (#1433)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Mckay authored May 13, 2019
1 parent 3baae09 commit fc6fdac
Show file tree
Hide file tree
Showing 9 changed files with 401 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.ethereum.permissioning;

import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;

import tech.pegasys.pantheon.crypto.Hash;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Transaction;
import tech.pegasys.pantheon.ethereum.permissioning.account.TransactionPermissioningProvider;
import tech.pegasys.pantheon.ethereum.transaction.CallParameter;
import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulator;
import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulatorResult;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.bytes.BytesValues;

import java.util.Optional;

/**
* 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 final Address contractAddress;
private final TransactionSimulator transactionSimulator;

// full function signature for connection allowed call
private static final String FUNCTION_SIGNATURE =
"transactionAllowed(address,address,uint256,uint256,uint256,bytes)";
// hashed function signature for connection allowed call
private static final BytesValue FUNCTION_SIGNATURE_HASH = hashSignature(FUNCTION_SIGNATURE);

// 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
private static BytesValue hashSignature(final String signature) {
return Hash.keccak256(BytesValue.of(signature.getBytes(UTF_8))).slice(0, 4);
}

// True from a contract is 1 filled to 32 bytes
private static final BytesValue TRUE_RESPONSE =
BytesValue.fromHexString(
"0x0000000000000000000000000000000000000000000000000000000000000001");
private static final BytesValue FALSE_RESPONSE =
BytesValue.fromHexString(
"0x0000000000000000000000000000000000000000000000000000000000000000");

/**
* 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
*/
public TransactionSmartContractPermissioningController(
final Address contractAddress, final TransactionSimulator transactionSimulator) {
this.contractAddress = contractAddress;
this.transactionSimulator = transactionSimulator;
}

/**
* Check whether a given transaction should be permitted for the current head
*
* @param transaction The transaction to be examined
* @return boolean of whether or not to permit the connection to occur
*/
@Override
public boolean isPermitted(final Transaction transaction) {
final BytesValue payload = createPayload(transaction);
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("Transaction permissioning contract does not exist");
}

final Optional<TransactionSimulatorResult> result =
transactionSimulator.processAtHead(callParams);

if (result.isPresent()) {
switch (result.get().getResult().getStatus()) {
case INVALID:
throw new IllegalStateException(
"Transaction permissioning transaction found to be Invalid");
case FAILED:
throw new IllegalStateException(
"Transaction permissioning transaction failed when processing");
default:
break;
}
}

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
// understand
public static Boolean checkTransactionResult(final BytesValue result) {
// booleans are padded to 32 bytes
if (result.size() != 32) {
throw new IllegalArgumentException("Unexpected result size");
}

// 0 is false
if (result.compareTo(FALSE_RESPONSE) == 0) {
return false;
// 32 bytes of 1's is true
} else if (result.compareTo(TRUE_RESPONSE) == 0) {
return true;
// Anything else is wrong
} else {
throw new IllegalStateException("Unexpected result form");
}
}

// Assemble the bytevalue payload to call the contract
public static BytesValue createPayload(final Transaction transaction) {
return createPayload(FUNCTION_SIGNATURE_HASH, transaction);
}

public static BytesValue createPayload(
final BytesValue signature, final Transaction transaction) {
return BytesValues.concatenate(signature, encodeTransaction(transaction));
}

private static BytesValue encodeTransaction(final Transaction transaction) {
return BytesValues.concatenate(
encodeAddress(transaction.getSender()),
encodeAddress(transaction.getTo()),
transaction.getValue().getBytes(),
transaction.getGasPrice().getBytes(),
encodeLong(transaction.getGasLimit()),
encodeBytes(transaction.getPayload()));
}

// Case for empty address
private static BytesValue encodeAddress(final Optional<Address> address) {
return encodeAddress(address.orElse(Address.wrap(BytesValue.wrap(new byte[20]))));
}

// Address is the 20 bytes of value left padded by 12 bytes.
private static BytesValue encodeAddress(final Address address) {
return BytesValues.concatenate(BytesValue.wrap(new byte[12]), address);
}

// long to uint256, 8 bytes big endian, so left padded by 24 bytes
private static BytesValue encodeLong(final long l) {
checkArgument(l >= 0, "Unsigned value must be positive");
final byte[] longBytes = new byte[8];
for (int i = 0; i < 8; i++) {
longBytes[i] = (byte) ((l >> ((7 - i) * 8)) & 0xFF);
}
return BytesValues.concatenate(BytesValue.wrap(new byte[24]), BytesValue.wrap(longBytes));
}

// A bytes array is a uint256 of its length, then the bytes that make up its value, then pad to
// next 32 bytes interval
private static BytesValue encodeBytes(final BytesValue value) {
final BytesValue length = encodeLong(value.size());
final BytesValue padding = BytesValue.wrap(new byte[(32 - (value.size() % 32))]);
return BytesValues.concatenate(length, value, padding);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private NodeSmartContractPermissioningController setupController(
public void testIpv4Included() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(
Expand All @@ -76,7 +76,7 @@ public void testIpv4Included() throws IOException {
public void testIpv4DestinationMissing() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(
Expand All @@ -92,7 +92,7 @@ public void testIpv4DestinationMissing() throws IOException {
public void testIpv4SourceMissing() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(
Expand All @@ -108,7 +108,7 @@ public void testIpv4SourceMissing() throws IOException {
public void testIpv6Included() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(
Expand All @@ -124,7 +124,7 @@ public void testIpv6Included() throws IOException {
public void testIpv6SourceMissing() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(
Expand All @@ -140,7 +140,7 @@ public void testIpv6SourceMissing() throws IOException {
public void testIpv6DestinationMissing() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(
Expand All @@ -156,7 +156,7 @@ public void testIpv6DestinationMissing() throws IOException {
public void testPermissioningContractMissing() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/noSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/noSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThatThrownBy(
Expand All @@ -174,7 +174,7 @@ public void testPermissioningContractMissing() throws IOException {
public void testPermissioningContractCorrupt() throws IOException {
final NodeSmartContractPermissioningController controller =
setupController(
"/SmartContractPermissioningControllerTest/corruptSmartPermissioning.json",
"/NodeSmartContractPermissioningControllerTest/corruptSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThatThrownBy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.ethereum.permissioning;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryBlockchain;
import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryWorldStateArchive;

import tech.pegasys.pantheon.config.GenesisConfigFile;
import tech.pegasys.pantheon.ethereum.chain.GenesisState;
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Transaction;
import tech.pegasys.pantheon.ethereum.core.Wei;
import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule;
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule;
import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulator;
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive;
import tech.pegasys.pantheon.util.bytes.BytesValue;

import java.io.IOException;

import com.google.common.io.Resources;
import org.junit.Test;

public class TransactionSmartContractPermissioningControllerTest {
private TransactionSmartContractPermissioningController setupController(
final String resourceName, final String contractAddressString) throws IOException {
final ProtocolSchedule<Void> protocolSchedule = MainnetProtocolSchedule.create();

final String emptyContractFile =
Resources.toString(this.getClass().getResource(resourceName), UTF_8);
final GenesisState genesisState =
GenesisState.fromConfig(GenesisConfigFile.fromConfig(emptyContractFile), protocolSchedule);

final MutableBlockchain blockchain = createInMemoryBlockchain(genesisState.getBlock());
final WorldStateArchive worldArchive = createInMemoryWorldStateArchive();

genesisState.writeStateTo(worldArchive.getMutable());

final TransactionSimulator ts =
new TransactionSimulator(blockchain, worldArchive, protocolSchedule);
final Address contractAddress = Address.fromHexString(contractAddressString);

return new TransactionSmartContractPermissioningController(contractAddress, ts);
}

private Transaction transactionForAccount(final Address address) {
return Transaction.builder()
.sender(address)
.value(Wei.ZERO)
.gasPrice(Wei.ZERO)
.gasLimit(0)
.payload(BytesValue.EMPTY)
.build();
}

@Test
public void testAccountIncluded() throws IOException {
final TransactionSmartContractPermissioningController controller =
setupController(
"/TransactionSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(controller.isPermitted(transactionForAccount(Address.fromHexString("0x1"))))
.isTrue();
}

@Test
public void testAccountNotIncluded() throws IOException {
final TransactionSmartContractPermissioningController controller =
setupController(
"/TransactionSmartContractPermissioningControllerTest/preseededSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThat(controller.isPermitted(transactionForAccount(Address.fromHexString("0x2"))))
.isFalse();
}

@Test
public void testPermissioningContractMissing() throws IOException {
final TransactionSmartContractPermissioningController controller =
setupController(
"/TransactionSmartContractPermissioningControllerTest/noSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThatThrownBy(
() -> controller.isPermitted(transactionForAccount(Address.fromHexString("0x1"))))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Transaction permissioning contract does not exist");
}

@Test
public void testPermissioningContractCorrupt() throws IOException {
final TransactionSmartContractPermissioningController controller =
setupController(
"/TransactionSmartContractPermissioningControllerTest/corruptSmartPermissioning.json",
"0x0000000000000000000000000000000000001234");

assertThatThrownBy(
() -> controller.isPermitted(transactionForAccount(Address.fromHexString("0x1"))))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Transaction permissioning transaction failed when processing");
}
}
Loading

0 comments on commit fc6fdac

Please sign in to comment.