diff --git a/enclave/src/main/java/tech/pegasys/pantheon/enclave/Enclave.java b/enclave/src/main/java/tech/pegasys/pantheon/enclave/Enclave.java index 8830198b77..906099e00f 100644 --- a/enclave/src/main/java/tech/pegasys/pantheon/enclave/Enclave.java +++ b/enclave/src/main/java/tech/pegasys/pantheon/enclave/Enclave.java @@ -76,8 +76,8 @@ public class Enclave { try (Response response = client.newCall(request).execute()) { return objectMapper.readValue(response.body().string(), responseType); } catch (IOException e) { - LOG.error("Enclave failed to execute ", request); - throw new IOException("Enclave failed to execute post", e); + LOG.error("Enclave failed to execute {}", request, e); + throw new IOException("Enclave failed to execute post"); } } } 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 668199ab9d..70d76bdf97 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 @@ -58,6 +58,9 @@ public interface TransactionValidator { EXCEEDS_BLOCK_GAS_LIMIT, TX_SENDER_NOT_AUTHORIZED, CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE, - PRIVATE_TRANSACTION_FAILED + // Private Transaction Invalid Reasons + PRIVATE_TRANSACTION_FAILED, + PRIVATE_NONCE_TOO_LOW, + INCORRECT_PRIVATE_NONCE } } diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContract.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContract.java index 1e9aad00ea..62a5538924 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContract.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContract.java @@ -138,11 +138,17 @@ public class PrivacyPrecompiledContract extends AbstractPrecompiledContract { privacyGroupId); if (result.isInvalid() || !result.isSuccessful()) { - LOG.error("Unable to process the private transaction: {}", result.getValidationResult()); + LOG.error( + "Failed to process the private transaction: {}", + result.getValidationResult().getErrorMessage()); return BytesValue.EMPTY; } if (messageFrame.isPersistingState()) { + LOG.trace( + "Persisting private state {} for privacyGroup {}", + disposablePrivateState.rootHash(), + privacyGroupId); privateWorldStateUpdater.commit(); disposablePrivateState.persist(); diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandler.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandler.java index cf68e0fe9d..5e7808305a 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandler.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandler.java @@ -12,21 +12,29 @@ */ package tech.pegasys.pantheon.ethereum.privacy; +import static tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INCORRECT_PRIVATE_NONCE; +import static tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.PRIVATE_NONCE_TOO_LOW; + import tech.pegasys.pantheon.crypto.SECP256K1; import tech.pegasys.pantheon.enclave.Enclave; +import tech.pegasys.pantheon.enclave.types.ReceiveRequest; +import tech.pegasys.pantheon.enclave.types.ReceiveResponse; import tech.pegasys.pantheon.enclave.types.SendRequest; import tech.pegasys.pantheon.enclave.types.SendResponse; +import tech.pegasys.pantheon.ethereum.core.Account; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult; import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValues; import java.io.IOException; import java.util.Base64; import java.util.List; -import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.common.base.Charsets; @@ -40,39 +48,97 @@ public class PrivateTransactionHandler { private final Enclave enclave; private final Address privacyPrecompileAddress; private final SECP256K1.KeyPair nodeKeyPair; + private final PrivateStateStorage privateStateStorage; + private final WorldStateArchive privateWorldStateArchive; public PrivateTransactionHandler(final PrivacyParameters privacyParameters) { this( new Enclave(privacyParameters.getEnclaveUri()), Address.privacyPrecompiled(privacyParameters.getPrivacyAddress()), - privacyParameters.getSigningKeyPair()); + privacyParameters.getSigningKeyPair(), + privacyParameters.getPrivateStateStorage(), + privacyParameters.getPrivateWorldStateArchive()); } public PrivateTransactionHandler( final Enclave enclave, final Address privacyPrecompileAddress, - final SECP256K1.KeyPair nodeKeyPair) { + final SECP256K1.KeyPair nodeKeyPair, + final PrivateStateStorage privateStateStorage, + final WorldStateArchive privateWorldStateArchive) { this.enclave = enclave; this.privacyPrecompileAddress = privacyPrecompileAddress; this.nodeKeyPair = nodeKeyPair; + this.privateStateStorage = privateStateStorage; + this.privateWorldStateArchive = privateWorldStateArchive; } - public Transaction handle( - final PrivateTransaction privateTransaction, final Supplier nonceSupplier) - throws IOException { - LOG.trace("Handling private transaction {}", privateTransaction.toString()); + public String sendToOrion(final PrivateTransaction privateTransaction) throws IOException { final SendRequest sendRequest = createSendRequest(privateTransaction); final SendResponse sendResponse; + try { LOG.trace("Storing private transaction in enclave"); sendResponse = enclave.send(sendRequest); + return sendResponse.getKey(); } catch (IOException e) { LOG.error("Failed to store private transaction in enclave", e); throw e; } + } - return createPrivacyMarkerTransactionWithNonce( - sendResponse.getKey(), privateTransaction, nonceSupplier.get()); + public String getPrivacyGroup(final String key, final BytesValue from) throws IOException { + final ReceiveRequest receiveRequest = new ReceiveRequest(key, BytesValues.asString(from)); + LOG.debug("Getting privacy group for {}", BytesValues.asString(from)); + final ReceiveResponse receiveResponse; + try { + receiveResponse = enclave.receive(receiveRequest); + return BytesValue.wrap(receiveResponse.getPrivacyGroupId().getBytes(Charsets.UTF_8)) + .toString(); + } catch (IOException e) { + LOG.error("Failed to retrieve private transaction in enclave", e); + throw e; + } + } + + public Transaction createPrivacyMarkerTransaction( + final String transactionEnclaveKey, + final PrivateTransaction privateTransaction, + final Long nonce) { + + return Transaction.builder() + .nonce(nonce) + .gasPrice(privateTransaction.getGasPrice()) + .gasLimit(privateTransaction.getGasLimit()) + .to(privacyPrecompileAddress) + .value(privateTransaction.getValue()) + .payload(BytesValue.wrap(transactionEnclaveKey.getBytes(Charsets.UTF_8))) + .sender(privateTransaction.getSender()) + .signAndBuild(nodeKeyPair); + } + + public ValidationResult validatePrivateTransaction( + final PrivateTransaction privateTransaction, final String privacyGroupId) { + final long actualNonce = privateTransaction.getNonce(); + final long expectedNonce = getSenderNonce(privateTransaction, privacyGroupId); + LOG.debug("Validating actual nonce {} with expected nonce {}", actualNonce, expectedNonce); + if (expectedNonce > actualNonce) { + return ValidationResult.invalid( + PRIVATE_NONCE_TOO_LOW, + String.format( + "private transaction nonce %s does not match sender account nonce %s.", + actualNonce, expectedNonce)); + } + + if (expectedNonce != actualNonce) { + return ValidationResult.invalid( + INCORRECT_PRIVATE_NONCE, + String.format( + "private transaction nonce %s does not match sender account nonce %s.", + actualNonce, expectedNonce)); + } + + return ValidationResult.valid(); } private SendRequest createSendRequest(final PrivateTransaction privateTransaction) { @@ -95,19 +161,29 @@ public class PrivateTransactionHandler { privateFor); } - private Transaction createPrivacyMarkerTransactionWithNonce( - final String transactionEnclaveKey, - final PrivateTransaction privateTransaction, - final Long nonce) { - - return Transaction.builder() - .nonce(nonce) - .gasPrice(privateTransaction.getGasPrice()) - .gasLimit(privateTransaction.getGasLimit()) - .to(privacyPrecompileAddress) - .value(privateTransaction.getValue()) - .payload(BytesValue.wrap(transactionEnclaveKey.getBytes(Charsets.UTF_8))) - .sender(privateTransaction.getSender()) - .signAndBuild(nodeKeyPair); + private long getSenderNonce( + final PrivateTransaction privateTransaction, final String privacyGroupId) { + return privateStateStorage + .getPrivateAccountState(BytesValue.fromHexString(privacyGroupId)) + .map( + lastRootHash -> + privateWorldStateArchive + .getMutable(lastRootHash) + .map( + worldState -> { + final Account maybePrivateSender = + worldState.get(privateTransaction.getSender()); + + if (maybePrivateSender != null) { + return maybePrivateSender.getNonce(); + } + // account has not interacted in this private state + return Account.DEFAULT_NONCE; + }) + // private state does not exist + .orElse(Account.DEFAULT_NONCE)) + .orElse( + // private state does not exist + Account.DEFAULT_NONCE); } } diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandlerTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandlerTest.java index b11dff9404..e36f98141f 100644 --- a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandlerTest.java +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandlerTest.java @@ -17,19 +17,30 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INCORRECT_PRIVATE_NONCE; +import static tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.PRIVATE_NONCE_TOO_LOW; import tech.pegasys.pantheon.crypto.SECP256K1; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.enclave.Enclave; +import tech.pegasys.pantheon.enclave.types.ReceiveRequest; +import tech.pegasys.pantheon.enclave.types.ReceiveResponse; import tech.pegasys.pantheon.enclave.types.SendRequest; import tech.pegasys.pantheon.enclave.types.SendResponse; +import tech.pegasys.pantheon.ethereum.core.Account; import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.MutableWorldState; import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.IOException; import java.math.BigInteger; +import java.util.Optional; import com.google.common.base.Charsets; import com.google.common.collect.Lists; @@ -51,25 +62,6 @@ public class PrivateTransactionHandlerTest { PrivateTransactionHandler privateTransactionHandler; PrivateTransactionHandler brokenPrivateTransactionHandler; - private static final PrivateTransaction VALID_PRIVATE_TRANSACTION = - PrivateTransaction.builder() - .nonce(0) - .gasPrice(Wei.of(1000)) - .gasLimit(3000000) - .to(Address.fromHexString("0x627306090abab3a6e1400e9345bc60c78a8bef57")) - .value(Wei.ZERO) - .payload(BytesValue.fromHexString("0x")) - .sender(Address.fromHexString("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")) - .chainId(BigInteger.valueOf(2018)) - .privateFrom( - BytesValue.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8))) - .privateFor( - Lists.newArrayList( - BytesValue.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8)), - BytesValue.wrap("Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=".getBytes(UTF_8)))) - .restriction(Restriction.RESTRICTED) - .signAndBuild(KEY_PAIR); - private static final Transaction PUBLIC_TRANSACTION = Transaction.builder() .nonce(0) @@ -85,7 +77,9 @@ public class PrivateTransactionHandlerTest { Enclave mockEnclave() throws IOException { Enclave mockEnclave = mock(Enclave.class); SendResponse response = new SendResponse(TRANSACTION_KEY); + ReceiveResponse receiveResponse = new ReceiveResponse(new byte[0], "mock"); when(mockEnclave.send(any(SendRequest.class))).thenReturn(response); + when(mockEnclave.receive(any(ReceiveRequest.class))).thenReturn(receiveResponse); return mockEnclave; } @@ -97,27 +91,107 @@ public class PrivateTransactionHandlerTest { @Before public void setUp() throws IOException { + PrivateStateStorage privateStateStorage = mock(PrivateStateStorage.class); + Hash mockHash = mock(Hash.class); + when(privateStateStorage.getPrivateAccountState(any(BytesValue.class))) + .thenReturn(Optional.of(mockHash)); + WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); + Account account = mock(Account.class); + when(account.getNonce()).thenReturn(1L); + MutableWorldState mutableWorldState = mock(MutableWorldState.class); + when(worldStateArchive.getMutable(any(Hash.class))).thenReturn(Optional.of(mutableWorldState)); + when(mutableWorldState.get(any(Address.class))).thenReturn(account); + privateTransactionHandler = - new PrivateTransactionHandler(mockEnclave(), Address.DEFAULT_PRIVACY, KEY_PAIR); + new PrivateTransactionHandler( + mockEnclave(), + Address.DEFAULT_PRIVACY, + KEY_PAIR, + privateStateStorage, + worldStateArchive); brokenPrivateTransactionHandler = - new PrivateTransactionHandler(brokenMockEnclave(), Address.DEFAULT_PRIVACY, KEY_PAIR); + new PrivateTransactionHandler( + brokenMockEnclave(), + Address.DEFAULT_PRIVACY, + KEY_PAIR, + privateStateStorage, + worldStateArchive); } @Test - public void validTransactionThroughHandler() throws IOException { - final Transaction transactionResponse = - privateTransactionHandler.handle(VALID_PRIVATE_TRANSACTION, () -> 0L); - - assertThat(transactionResponse.contractAddress()) - .isEqualTo(PUBLIC_TRANSACTION.contractAddress()); - assertThat(transactionResponse.getPayload()).isEqualTo(PUBLIC_TRANSACTION.getPayload()); - assertThat(transactionResponse.getNonce()).isEqualTo(PUBLIC_TRANSACTION.getNonce()); - assertThat(transactionResponse.getSender()).isEqualTo(PUBLIC_TRANSACTION.getSender()); - assertThat(transactionResponse.getValue()).isEqualTo(PUBLIC_TRANSACTION.getValue()); + public void validTransactionThroughHandler() throws Exception { + + final PrivateTransaction transaction = buildPrivateTransaction(1); + + final String enclaveKey = privateTransactionHandler.sendToOrion(transaction); + + final String privacyGroupId = + privateTransactionHandler.getPrivacyGroup(enclaveKey, transaction.getPrivateFrom()); + + final ValidationResult validationResult = + privateTransactionHandler.validatePrivateTransaction(transaction, privacyGroupId); + + final Transaction markerTransaction = + privateTransactionHandler.createPrivacyMarkerTransaction(enclaveKey, transaction, 0L); + + assertThat(validationResult).isEqualTo(ValidationResult.valid()); + assertThat(markerTransaction.contractAddress()).isEqualTo(PUBLIC_TRANSACTION.contractAddress()); + assertThat(markerTransaction.getPayload()).isEqualTo(PUBLIC_TRANSACTION.getPayload()); + assertThat(markerTransaction.getNonce()).isEqualTo(PUBLIC_TRANSACTION.getNonce()); + assertThat(markerTransaction.getSender()).isEqualTo(PUBLIC_TRANSACTION.getSender()); + assertThat(markerTransaction.getValue()).isEqualTo(PUBLIC_TRANSACTION.getValue()); } @Test(expected = IOException.class) - public void enclaveIsDownWhileHandling() throws IOException { - brokenPrivateTransactionHandler.handle(VALID_PRIVATE_TRANSACTION, () -> 0L); + public void enclaveIsDownWhileHandling() throws Exception { + brokenPrivateTransactionHandler.sendToOrion(buildPrivateTransaction()); + } + + @Test + public void nonceTooLowError() throws Exception { + final PrivateTransaction transaction = buildPrivateTransaction(0); + + final String enclaveKey = privateTransactionHandler.sendToOrion(transaction); + final String privacyGroupId = + privateTransactionHandler.getPrivacyGroup(enclaveKey, transaction.getPrivateFrom()); + final ValidationResult validationResult = + privateTransactionHandler.validatePrivateTransaction(transaction, privacyGroupId); + assertThat(validationResult).isEqualTo(ValidationResult.invalid(PRIVATE_NONCE_TOO_LOW)); + } + + @Test + public void incorrectNonceError() throws Exception { + final PrivateTransaction transaction = buildPrivateTransaction(2); + + final String enclaveKey = privateTransactionHandler.sendToOrion(transaction); + final String privacyGroupId = + privateTransactionHandler.getPrivacyGroup(enclaveKey, transaction.getPrivateFrom()); + final ValidationResult validationResult = + privateTransactionHandler.validatePrivateTransaction(transaction, privacyGroupId); + assertThat(validationResult).isEqualTo(ValidationResult.invalid(INCORRECT_PRIVATE_NONCE)); + } + + private static PrivateTransaction buildPrivateTransaction() { + return buildPrivateTransaction(0); + } + + private static PrivateTransaction buildPrivateTransaction(final long nonce) { + return PrivateTransaction.builder() + .nonce(nonce) + .gasPrice(Wei.of(1000)) + .gasLimit(3000000) + .to(Address.fromHexString("0x627306090abab3a6e1400e9345bc60c78a8bef57")) + .value(Wei.ZERO) + .payload(BytesValue.fromHexString("0x")) + .sender(Address.fromHexString("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73")) + .chainId(BigInteger.valueOf(2018)) + .privateFrom( + BytesValue.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8))) + .privateFor( + Lists.newArrayList( + BytesValue.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8)), + BytesValue.wrap("Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=".getBytes(UTF_8)))) + .restriction(Restriction.RESTRICTED) + .signAndBuild(KEY_PAIR); } } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java index f990d26b07..d223ad66cf 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java @@ -34,8 +34,13 @@ public class JsonRpcErrorConverter { return JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT; case TX_SENDER_NOT_AUTHORIZED: return JsonRpcError.TX_SENDER_NOT_AUTHORIZED; + // Private Transaction Invalid Reasons case CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE: return JsonRpcError.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE; + case PRIVATE_NONCE_TOO_LOW: + return JsonRpcError.PRIVATE_NONCE_TOO_LOW; + case INCORRECT_PRIVATE_NONCE: + return JsonRpcError.INCORRECT_PRIVATE_NONCE; default: return JsonRpcError.INVALID_PARAMS; diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransaction.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransaction.java index 2261f469dd..262f531d4f 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransaction.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransaction.java @@ -27,8 +27,6 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; -import tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; -import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult; import tech.pegasys.pantheon.ethereum.privacy.PrivateTransaction; import tech.pegasys.pantheon.ethereum.privacy.PrivateTransactionHandler; import tech.pegasys.pantheon.ethereum.privacy.Restriction; @@ -89,34 +87,42 @@ public class EeaSendRawTransaction implements JsonRpcMethod { request.getId(), JsonRpcError.UNIMPLEMENTED_PRIVATE_TRANSACTION_TYPE); } - final Transaction transaction; + final String enclaveKey; try { - transaction = handlePrivateTransaction(privateTransaction); - } catch (final InvalidJsonRpcRequestException e) { + enclaveKey = privateTransactionHandler.sendToOrion(privateTransaction); + } catch (final IOException e) { return new JsonRpcErrorResponse(request.getId(), JsonRpcError.ENCLAVE_ERROR); } - final ValidationResult validationResult = - transactionPool.addLocalTransaction(transaction); - return validationResult.either( - () -> new JsonRpcSuccessResponse(request.getId(), transaction.hash().toString()), - errorReason -> - new JsonRpcErrorResponse( - request.getId(), convertTransactionInvalidReason(errorReason))); - } - - protected long getNonce(final Address address) { - return blockchain.getTransactionCount(address, blockchain.headBlockNumber()); - } - - private Transaction handlePrivateTransaction(final PrivateTransaction privateTransaction) - throws InvalidJsonRpcRequestException { + final String privacyGroupId; try { - return privateTransactionHandler.handle( - privateTransaction, () -> getNonce(privateTransaction.getSender())); + privacyGroupId = + privateTransactionHandler.getPrivacyGroup( + enclaveKey, privateTransaction.getPrivateFrom()); } catch (final IOException e) { - throw new InvalidJsonRpcRequestException("Unable to handle private transaction", e); + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.ENCLAVE_ERROR); } + + return privateTransactionHandler + .validatePrivateTransaction(privateTransaction, privacyGroupId) + .either( + () -> { + final Transaction privacyMarkerTransaction = + privateTransactionHandler.createPrivacyMarkerTransaction( + enclaveKey, privateTransaction, getNonce(privateTransaction.getSender())); + return transactionPool + .addLocalTransaction(privacyMarkerTransaction) + .either( + () -> + new JsonRpcSuccessResponse( + request.getId(), privacyMarkerTransaction.hash().toString()), + errorReason -> + new JsonRpcErrorResponse( + request.getId(), convertTransactionInvalidReason(errorReason))); + }, + (errorReason) -> + new JsonRpcErrorResponse( + request.getId(), convertTransactionInvalidReason(errorReason))); } private PrivateTransaction decodeRawTransaction(final String hash) @@ -128,4 +134,8 @@ public class EeaSendRawTransaction implements JsonRpcMethod { throw new InvalidJsonRpcRequestException("Invalid raw private transaction hex", e); } } + + protected long getNonce(final Address address) { + return blockchain.getTransactionCount(address, blockchain.headBlockNumber()); + } } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java index e99b406757..7526c8cc91 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java @@ -91,6 +91,8 @@ public enum JsonRpcError { // Private transaction errors ENCLAVE_ERROR(-50100, "Error communicating with enclave"), + PRIVATE_NONCE_TOO_LOW(-50100, "Private transaction nonce too low"), + INCORRECT_PRIVATE_NONCE(-50100, "Private transaction nonce is incorrect"), UNIMPLEMENTED_PRIVATE_TRANSACTION_TYPE(-50100, "Unimplemented private transaction type"), PRIVATE_TRANSACTION_RECEIPT_ERROR(-50100, "Error generating the private transaction receipt"), VALUE_NOT_ZERO(-50100, "We cannot transfer ether in private transaction yet."), diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransactionTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransactionTest.java index d241df9a47..9cc87fb233 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransactionTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransactionTest.java @@ -39,7 +39,6 @@ import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.IOException; import java.math.BigInteger; import java.util.Optional; -import java.util.function.Supplier; import org.junit.Before; import org.junit.Test; @@ -47,7 +46,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -@SuppressWarnings("unchecked") @RunWith(MockitoJUnitRunner.class) public class EeaSendRawTransactionTest { @@ -89,6 +87,9 @@ public class EeaSendRawTransactionTest { Address.wrap(BytesValue.fromHexString("0x8411b12666f68ef74cace3615c9d5a377729d03f")), Optional.empty()); + final String MOCK_ORION_KEY = ""; + final String MOCK_PRIVACY_GROUP = ""; + @Mock private TransactionPool transactionPool; @Mock private JsonRpcParameter parameter; @@ -177,10 +178,17 @@ public class EeaSendRawTransactionTest { } @Test - public void validTransactionIsSentToTransactionPool() throws IOException { + public void validTransactionIsSentToTransactionPool() throws Exception { when(parameter.required(any(Object[].class), anyInt(), any())) .thenReturn(VALID_PRIVATE_TRANSACTION_RLP); - when(privateTxHandler.handle(any(PrivateTransaction.class), any(Supplier.class))) + when(privateTxHandler.sendToOrion(any(PrivateTransaction.class))).thenReturn(MOCK_ORION_KEY); + when(privateTxHandler.getPrivacyGroup(any(String.class), any(BytesValue.class))) + .thenReturn(MOCK_PRIVACY_GROUP); + when(privateTxHandler.validatePrivateTransaction( + any(PrivateTransaction.class), any(String.class))) + .thenReturn(ValidationResult.valid()); + when(privateTxHandler.createPrivacyMarkerTransaction( + any(String.class), any(PrivateTransaction.class), any(Long.class))) .thenReturn(PUBLIC_TRANSACTION); when(transactionPool.addLocalTransaction(any(Transaction.class))) .thenReturn(ValidationResult.valid()); @@ -196,15 +204,21 @@ public class EeaSendRawTransactionTest { final JsonRpcResponse actualResponse = method.response(request); assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); - verify(privateTxHandler).handle(any(PrivateTransaction.class), any(Supplier.class)); + verify(privateTxHandler).sendToOrion(any(PrivateTransaction.class)); + verify(privateTxHandler).getPrivacyGroup(any(String.class), any(BytesValue.class)); + verify(privateTxHandler) + .validatePrivateTransaction(any(PrivateTransaction.class), any(String.class)); + verify(privateTxHandler) + .createPrivacyMarkerTransaction( + any(String.class), any(PrivateTransaction.class), any(Long.class)); verify(transactionPool).addLocalTransaction(any(Transaction.class)); } @Test - public void invalidTransactionIsSentToTransactionPool() throws IOException { + public void invalidTransactionIsSentToTransactionPool() throws Exception { when(parameter.required(any(Object[].class), anyInt(), any())) .thenReturn(VALID_PRIVATE_TRANSACTION_RLP); - when(privateTxHandler.handle(any(PrivateTransaction.class), any(Supplier.class))) + when(privateTxHandler.sendToOrion(any(PrivateTransaction.class))) .thenThrow(new IOException("enclave failed to execute")); final JsonRpcRequest request = @@ -220,55 +234,62 @@ public class EeaSendRawTransactionTest { } @Test - public void transactionWithNonceBelowAccountNonceIsRejected() throws IOException { + public void transactionWithNonceBelowAccountNonceIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.NONCE_TOO_LOW, JsonRpcError.NONCE_TOO_LOW); } @Test - public void transactionWithNonceAboveAccountNonceIsRejected() throws IOException { + public void transactionWithNonceAboveAccountNonceIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.INCORRECT_NONCE, JsonRpcError.INCORRECT_NONCE); } @Test - public void transactionWithInvalidSignatureIsRejected() throws IOException { + public void transactionWithInvalidSignatureIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.INVALID_SIGNATURE, JsonRpcError.INVALID_TRANSACTION_SIGNATURE); } @Test - public void transactionWithIntrinsicGasExceedingGasLimitIsRejected() throws IOException { + public void transactionWithIntrinsicGasExceedingGasLimitIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, JsonRpcError.INTRINSIC_GAS_EXCEEDS_LIMIT); } @Test - public void transactionWithUpfrontGasExceedingAccountBalanceIsRejected() throws IOException { + public void transactionWithUpfrontGasExceedingAccountBalanceIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE); } @Test - public void transactionWithGasLimitExceedingBlockGasLimitIsRejected() throws IOException { + public void transactionWithGasLimitExceedingBlockGasLimitIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT); } @Test - public void transactionWithNotWhitelistedSenderAccountIsRejected() throws IOException { + public void transactionWithNotWhitelistedSenderAccountIsRejected() throws Exception { verifyErrorForInvalidTransaction( TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED, JsonRpcError.TX_SENDER_NOT_AUTHORIZED); } private void verifyErrorForInvalidTransaction( final TransactionInvalidReason transactionInvalidReason, final JsonRpcError expectedError) - throws IOException { + throws Exception { when(parameter.required(any(Object[].class), anyInt(), any())) .thenReturn(VALID_PRIVATE_TRANSACTION_RLP); - when(privateTxHandler.handle(any(PrivateTransaction.class), any(Supplier.class))) + when(privateTxHandler.sendToOrion(any(PrivateTransaction.class))).thenReturn(MOCK_ORION_KEY); + when(privateTxHandler.getPrivacyGroup(any(String.class), any(BytesValue.class))) + .thenReturn(MOCK_PRIVACY_GROUP); + when(privateTxHandler.validatePrivateTransaction( + any(PrivateTransaction.class), any(String.class))) + .thenReturn(ValidationResult.valid()); + when(privateTxHandler.createPrivacyMarkerTransaction( + any(String.class), any(PrivateTransaction.class), any(Long.class))) .thenReturn(PUBLIC_TRANSACTION); when(transactionPool.addLocalTransaction(any(Transaction.class))) .thenReturn(ValidationResult.invalid(transactionInvalidReason)); @@ -283,7 +304,13 @@ public class EeaSendRawTransactionTest { final JsonRpcResponse actualResponse = method.response(request); assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); - verify(privateTxHandler).handle(any(PrivateTransaction.class), any(Supplier.class)); + verify(privateTxHandler).sendToOrion(any(PrivateTransaction.class)); + verify(privateTxHandler).getPrivacyGroup(any(String.class), any(BytesValue.class)); + verify(privateTxHandler) + .validatePrivateTransaction(any(PrivateTransaction.class), any(String.class)); + verify(privateTxHandler) + .createPrivacyMarkerTransaction( + any(String.class), any(PrivateTransaction.class), any(Long.class)); verify(transactionPool).addLocalTransaction(any(Transaction.class)); }