[PAN-2647] Validate Private Transaction nonce before submitting to Transaction Pool (#1449)

* Validate private transaction nonce before submitting to Transaction Pool
* Update tests for Incorrect Nonce and Nonce Too Low exceptions
* Differentiate Incorrect Nonce and Nonce Too Low error messages
* Fixed flaky tests
* Change log level from Info to Debug

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Ivaylo Kirilov 6 years ago committed by Eric Kellstrand
parent 7e29b0954b
commit fc4f9917b7
  1. 4
      enclave/src/main/java/tech/pegasys/pantheon/enclave/Enclave.java
  2. 5
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/TransactionValidator.java
  3. 8
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContract.java
  4. 122
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandler.java
  5. 140
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionHandlerTest.java
  6. 5
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java
  7. 56
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransaction.java
  8. 2
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java
  9. 61
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/privacy/EeaSendRawTransactionTest.java

@ -76,8 +76,8 @@ public class Enclave {
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
return objectMapper.readValue(response.body().string(), responseType); return objectMapper.readValue(response.body().string(), responseType);
} catch (IOException e) { } catch (IOException e) {
LOG.error("Enclave failed to execute ", request); LOG.error("Enclave failed to execute {}", request, e);
throw new IOException("Enclave failed to execute post", e); throw new IOException("Enclave failed to execute post");
} }
} }
} }

@ -58,6 +58,9 @@ public interface TransactionValidator {
EXCEEDS_BLOCK_GAS_LIMIT, EXCEEDS_BLOCK_GAS_LIMIT,
TX_SENDER_NOT_AUTHORIZED, TX_SENDER_NOT_AUTHORIZED,
CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE, CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE,
PRIVATE_TRANSACTION_FAILED // Private Transaction Invalid Reasons
PRIVATE_TRANSACTION_FAILED,
PRIVATE_NONCE_TOO_LOW,
INCORRECT_PRIVATE_NONCE
} }
} }

@ -138,11 +138,17 @@ public class PrivacyPrecompiledContract extends AbstractPrecompiledContract {
privacyGroupId); privacyGroupId);
if (result.isInvalid() || !result.isSuccessful()) { 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; return BytesValue.EMPTY;
} }
if (messageFrame.isPersistingState()) { if (messageFrame.isPersistingState()) {
LOG.trace(
"Persisting private state {} for privacyGroup {}",
disposablePrivateState.rootHash(),
privacyGroupId);
privateWorldStateUpdater.commit(); privateWorldStateUpdater.commit();
disposablePrivateState.persist(); disposablePrivateState.persist();

@ -12,21 +12,29 @@
*/ */
package tech.pegasys.pantheon.ethereum.privacy; 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.crypto.SECP256K1;
import tech.pegasys.pantheon.enclave.Enclave; 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.SendRequest;
import tech.pegasys.pantheon.enclave.types.SendResponse; 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.Address;
import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; import tech.pegasys.pantheon.ethereum.core.PrivacyParameters;
import tech.pegasys.pantheon.ethereum.core.Transaction; 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.rlp.BytesValueRLPOutput;
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.bytes.BytesValues; import tech.pegasys.pantheon.util.bytes.BytesValues;
import java.io.IOException; import java.io.IOException;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
@ -40,39 +48,97 @@ public class PrivateTransactionHandler {
private final Enclave enclave; private final Enclave enclave;
private final Address privacyPrecompileAddress; private final Address privacyPrecompileAddress;
private final SECP256K1.KeyPair nodeKeyPair; private final SECP256K1.KeyPair nodeKeyPair;
private final PrivateStateStorage privateStateStorage;
private final WorldStateArchive privateWorldStateArchive;
public PrivateTransactionHandler(final PrivacyParameters privacyParameters) { public PrivateTransactionHandler(final PrivacyParameters privacyParameters) {
this( this(
new Enclave(privacyParameters.getEnclaveUri()), new Enclave(privacyParameters.getEnclaveUri()),
Address.privacyPrecompiled(privacyParameters.getPrivacyAddress()), Address.privacyPrecompiled(privacyParameters.getPrivacyAddress()),
privacyParameters.getSigningKeyPair()); privacyParameters.getSigningKeyPair(),
privacyParameters.getPrivateStateStorage(),
privacyParameters.getPrivateWorldStateArchive());
} }
public PrivateTransactionHandler( public PrivateTransactionHandler(
final Enclave enclave, final Enclave enclave,
final Address privacyPrecompileAddress, final Address privacyPrecompileAddress,
final SECP256K1.KeyPair nodeKeyPair) { final SECP256K1.KeyPair nodeKeyPair,
final PrivateStateStorage privateStateStorage,
final WorldStateArchive privateWorldStateArchive) {
this.enclave = enclave; this.enclave = enclave;
this.privacyPrecompileAddress = privacyPrecompileAddress; this.privacyPrecompileAddress = privacyPrecompileAddress;
this.nodeKeyPair = nodeKeyPair; this.nodeKeyPair = nodeKeyPair;
this.privateStateStorage = privateStateStorage;
this.privateWorldStateArchive = privateWorldStateArchive;
} }
public Transaction handle( public String sendToOrion(final PrivateTransaction privateTransaction) throws IOException {
final PrivateTransaction privateTransaction, final Supplier<Long> nonceSupplier)
throws IOException {
LOG.trace("Handling private transaction {}", privateTransaction.toString());
final SendRequest sendRequest = createSendRequest(privateTransaction); final SendRequest sendRequest = createSendRequest(privateTransaction);
final SendResponse sendResponse; final SendResponse sendResponse;
try { try {
LOG.trace("Storing private transaction in enclave"); LOG.trace("Storing private transaction in enclave");
sendResponse = enclave.send(sendRequest); sendResponse = enclave.send(sendRequest);
return sendResponse.getKey();
} catch (IOException e) { } catch (IOException e) {
LOG.error("Failed to store private transaction in enclave", e); LOG.error("Failed to store private transaction in enclave", e);
throw e; throw e;
} }
}
return createPrivacyMarkerTransactionWithNonce( public String getPrivacyGroup(final String key, final BytesValue from) throws IOException {
sendResponse.getKey(), privateTransaction, nonceSupplier.get()); 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<TransactionInvalidReason> 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) { private SendRequest createSendRequest(final PrivateTransaction privateTransaction) {
@ -95,19 +161,29 @@ public class PrivateTransactionHandler {
privateFor); privateFor);
} }
private Transaction createPrivacyMarkerTransactionWithNonce( private long getSenderNonce(
final String transactionEnclaveKey, final PrivateTransaction privateTransaction, final String privacyGroupId) {
final PrivateTransaction privateTransaction, return privateStateStorage
final Long nonce) { .getPrivateAccountState(BytesValue.fromHexString(privacyGroupId))
.map(
return Transaction.builder() lastRootHash ->
.nonce(nonce) privateWorldStateArchive
.gasPrice(privateTransaction.getGasPrice()) .getMutable(lastRootHash)
.gasLimit(privateTransaction.getGasLimit()) .map(
.to(privacyPrecompileAddress) worldState -> {
.value(privateTransaction.getValue()) final Account maybePrivateSender =
.payload(BytesValue.wrap(transactionEnclaveKey.getBytes(Charsets.UTF_8))) worldState.get(privateTransaction.getSender());
.sender(privateTransaction.getSender())
.signAndBuild(nodeKeyPair); 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);
} }
} }

@ -17,19 +17,30 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.enclave.Enclave; 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.SendRequest;
import tech.pegasys.pantheon.enclave.types.SendResponse; 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.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.Transaction;
import tech.pegasys.pantheon.ethereum.core.Wei; 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 tech.pegasys.pantheon.util.bytes.BytesValue;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Optional;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -51,25 +62,6 @@ public class PrivateTransactionHandlerTest {
PrivateTransactionHandler privateTransactionHandler; PrivateTransactionHandler privateTransactionHandler;
PrivateTransactionHandler brokenPrivateTransactionHandler; 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 = private static final Transaction PUBLIC_TRANSACTION =
Transaction.builder() Transaction.builder()
.nonce(0) .nonce(0)
@ -85,7 +77,9 @@ public class PrivateTransactionHandlerTest {
Enclave mockEnclave() throws IOException { Enclave mockEnclave() throws IOException {
Enclave mockEnclave = mock(Enclave.class); Enclave mockEnclave = mock(Enclave.class);
SendResponse response = new SendResponse(TRANSACTION_KEY); SendResponse response = new SendResponse(TRANSACTION_KEY);
ReceiveResponse receiveResponse = new ReceiveResponse(new byte[0], "mock");
when(mockEnclave.send(any(SendRequest.class))).thenReturn(response); when(mockEnclave.send(any(SendRequest.class))).thenReturn(response);
when(mockEnclave.receive(any(ReceiveRequest.class))).thenReturn(receiveResponse);
return mockEnclave; return mockEnclave;
} }
@ -97,27 +91,107 @@ public class PrivateTransactionHandlerTest {
@Before @Before
public void setUp() throws IOException { 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 = privateTransactionHandler =
new PrivateTransactionHandler(mockEnclave(), Address.DEFAULT_PRIVACY, KEY_PAIR); new PrivateTransactionHandler(
mockEnclave(),
Address.DEFAULT_PRIVACY,
KEY_PAIR,
privateStateStorage,
worldStateArchive);
brokenPrivateTransactionHandler = brokenPrivateTransactionHandler =
new PrivateTransactionHandler(brokenMockEnclave(), Address.DEFAULT_PRIVACY, KEY_PAIR); new PrivateTransactionHandler(
brokenMockEnclave(),
Address.DEFAULT_PRIVACY,
KEY_PAIR,
privateStateStorage,
worldStateArchive);
} }
@Test @Test
public void validTransactionThroughHandler() throws IOException { public void validTransactionThroughHandler() throws Exception {
final Transaction transactionResponse =
privateTransactionHandler.handle(VALID_PRIVATE_TRANSACTION, () -> 0L); final PrivateTransaction transaction = buildPrivateTransaction(1);
assertThat(transactionResponse.contractAddress()) final String enclaveKey = privateTransactionHandler.sendToOrion(transaction);
.isEqualTo(PUBLIC_TRANSACTION.contractAddress());
assertThat(transactionResponse.getPayload()).isEqualTo(PUBLIC_TRANSACTION.getPayload()); final String privacyGroupId =
assertThat(transactionResponse.getNonce()).isEqualTo(PUBLIC_TRANSACTION.getNonce()); privateTransactionHandler.getPrivacyGroup(enclaveKey, transaction.getPrivateFrom());
assertThat(transactionResponse.getSender()).isEqualTo(PUBLIC_TRANSACTION.getSender());
assertThat(transactionResponse.getValue()).isEqualTo(PUBLIC_TRANSACTION.getValue()); final ValidationResult<TransactionInvalidReason> 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) @Test(expected = IOException.class)
public void enclaveIsDownWhileHandling() throws IOException { public void enclaveIsDownWhileHandling() throws Exception {
brokenPrivateTransactionHandler.handle(VALID_PRIVATE_TRANSACTION, () -> 0L); 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<TransactionInvalidReason> 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<TransactionInvalidReason> 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);
} }
} }

@ -34,8 +34,13 @@ public class JsonRpcErrorConverter {
return JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT; return JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT;
case TX_SENDER_NOT_AUTHORIZED: case TX_SENDER_NOT_AUTHORIZED:
return JsonRpcError.TX_SENDER_NOT_AUTHORIZED; return JsonRpcError.TX_SENDER_NOT_AUTHORIZED;
// Private Transaction Invalid Reasons
case CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE: case CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE:
return JsonRpcError.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: default:
return JsonRpcError.INVALID_PARAMS; return JsonRpcError.INVALID_PARAMS;

@ -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.JsonRpcErrorResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; 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.PrivateTransaction;
import tech.pegasys.pantheon.ethereum.privacy.PrivateTransactionHandler; import tech.pegasys.pantheon.ethereum.privacy.PrivateTransactionHandler;
import tech.pegasys.pantheon.ethereum.privacy.Restriction; import tech.pegasys.pantheon.ethereum.privacy.Restriction;
@ -89,34 +87,42 @@ public class EeaSendRawTransaction implements JsonRpcMethod {
request.getId(), JsonRpcError.UNIMPLEMENTED_PRIVATE_TRANSACTION_TYPE); request.getId(), JsonRpcError.UNIMPLEMENTED_PRIVATE_TRANSACTION_TYPE);
} }
final Transaction transaction; final String enclaveKey;
try { try {
transaction = handlePrivateTransaction(privateTransaction); enclaveKey = privateTransactionHandler.sendToOrion(privateTransaction);
} catch (final InvalidJsonRpcRequestException e) { } catch (final IOException e) {
return new JsonRpcErrorResponse(request.getId(), JsonRpcError.ENCLAVE_ERROR); return new JsonRpcErrorResponse(request.getId(), JsonRpcError.ENCLAVE_ERROR);
} }
final ValidationResult<TransactionInvalidReason> validationResult = final String privacyGroupId;
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 {
try { try {
return privateTransactionHandler.handle( privacyGroupId =
privateTransaction, () -> getNonce(privateTransaction.getSender())); privateTransactionHandler.getPrivacyGroup(
enclaveKey, privateTransaction.getPrivateFrom());
} catch (final IOException e) { } 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) private PrivateTransaction decodeRawTransaction(final String hash)
@ -128,4 +134,8 @@ public class EeaSendRawTransaction implements JsonRpcMethod {
throw new InvalidJsonRpcRequestException("Invalid raw private transaction hex", e); throw new InvalidJsonRpcRequestException("Invalid raw private transaction hex", e);
} }
} }
protected long getNonce(final Address address) {
return blockchain.getTransactionCount(address, blockchain.headBlockNumber());
}
} }

@ -91,6 +91,8 @@ public enum JsonRpcError {
// Private transaction errors // Private transaction errors
ENCLAVE_ERROR(-50100, "Error communicating with enclave"), 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"), UNIMPLEMENTED_PRIVATE_TRANSACTION_TYPE(-50100, "Unimplemented private transaction type"),
PRIVATE_TRANSACTION_RECEIPT_ERROR(-50100, "Error generating the private transaction receipt"), PRIVATE_TRANSACTION_RECEIPT_ERROR(-50100, "Error generating the private transaction receipt"),
VALUE_NOT_ZERO(-50100, "We cannot transfer ether in private transaction yet."), VALUE_NOT_ZERO(-50100, "We cannot transfer ether in private transaction yet."),

@ -39,7 +39,6 @@ import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -47,7 +46,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class EeaSendRawTransactionTest { public class EeaSendRawTransactionTest {
@ -89,6 +87,9 @@ public class EeaSendRawTransactionTest {
Address.wrap(BytesValue.fromHexString("0x8411b12666f68ef74cace3615c9d5a377729d03f")), Address.wrap(BytesValue.fromHexString("0x8411b12666f68ef74cace3615c9d5a377729d03f")),
Optional.empty()); Optional.empty());
final String MOCK_ORION_KEY = "";
final String MOCK_PRIVACY_GROUP = "";
@Mock private TransactionPool transactionPool; @Mock private TransactionPool transactionPool;
@Mock private JsonRpcParameter parameter; @Mock private JsonRpcParameter parameter;
@ -177,10 +178,17 @@ public class EeaSendRawTransactionTest {
} }
@Test @Test
public void validTransactionIsSentToTransactionPool() throws IOException { public void validTransactionIsSentToTransactionPool() throws Exception {
when(parameter.required(any(Object[].class), anyInt(), any())) when(parameter.required(any(Object[].class), anyInt(), any()))
.thenReturn(VALID_PRIVATE_TRANSACTION_RLP); .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); .thenReturn(PUBLIC_TRANSACTION);
when(transactionPool.addLocalTransaction(any(Transaction.class))) when(transactionPool.addLocalTransaction(any(Transaction.class)))
.thenReturn(ValidationResult.valid()); .thenReturn(ValidationResult.valid());
@ -196,15 +204,21 @@ public class EeaSendRawTransactionTest {
final JsonRpcResponse actualResponse = method.response(request); final JsonRpcResponse actualResponse = method.response(request);
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); 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)); verify(transactionPool).addLocalTransaction(any(Transaction.class));
} }
@Test @Test
public void invalidTransactionIsSentToTransactionPool() throws IOException { public void invalidTransactionIsSentToTransactionPool() throws Exception {
when(parameter.required(any(Object[].class), anyInt(), any())) when(parameter.required(any(Object[].class), anyInt(), any()))
.thenReturn(VALID_PRIVATE_TRANSACTION_RLP); .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")); .thenThrow(new IOException("enclave failed to execute"));
final JsonRpcRequest request = final JsonRpcRequest request =
@ -220,55 +234,62 @@ public class EeaSendRawTransactionTest {
} }
@Test @Test
public void transactionWithNonceBelowAccountNonceIsRejected() throws IOException { public void transactionWithNonceBelowAccountNonceIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.NONCE_TOO_LOW, JsonRpcError.NONCE_TOO_LOW); TransactionInvalidReason.NONCE_TOO_LOW, JsonRpcError.NONCE_TOO_LOW);
} }
@Test @Test
public void transactionWithNonceAboveAccountNonceIsRejected() throws IOException { public void transactionWithNonceAboveAccountNonceIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.INCORRECT_NONCE, JsonRpcError.INCORRECT_NONCE); TransactionInvalidReason.INCORRECT_NONCE, JsonRpcError.INCORRECT_NONCE);
} }
@Test @Test
public void transactionWithInvalidSignatureIsRejected() throws IOException { public void transactionWithInvalidSignatureIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.INVALID_SIGNATURE, JsonRpcError.INVALID_TRANSACTION_SIGNATURE); TransactionInvalidReason.INVALID_SIGNATURE, JsonRpcError.INVALID_TRANSACTION_SIGNATURE);
} }
@Test @Test
public void transactionWithIntrinsicGasExceedingGasLimitIsRejected() throws IOException { public void transactionWithIntrinsicGasExceedingGasLimitIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT,
JsonRpcError.INTRINSIC_GAS_EXCEEDS_LIMIT); JsonRpcError.INTRINSIC_GAS_EXCEEDS_LIMIT);
} }
@Test @Test
public void transactionWithUpfrontGasExceedingAccountBalanceIsRejected() throws IOException { public void transactionWithUpfrontGasExceedingAccountBalanceIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE,
JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE); JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE);
} }
@Test @Test
public void transactionWithGasLimitExceedingBlockGasLimitIsRejected() throws IOException { public void transactionWithGasLimitExceedingBlockGasLimitIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT); TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT);
} }
@Test @Test
public void transactionWithNotWhitelistedSenderAccountIsRejected() throws IOException { public void transactionWithNotWhitelistedSenderAccountIsRejected() throws Exception {
verifyErrorForInvalidTransaction( verifyErrorForInvalidTransaction(
TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED, JsonRpcError.TX_SENDER_NOT_AUTHORIZED); TransactionInvalidReason.TX_SENDER_NOT_AUTHORIZED, JsonRpcError.TX_SENDER_NOT_AUTHORIZED);
} }
private void verifyErrorForInvalidTransaction( private void verifyErrorForInvalidTransaction(
final TransactionInvalidReason transactionInvalidReason, final JsonRpcError expectedError) final TransactionInvalidReason transactionInvalidReason, final JsonRpcError expectedError)
throws IOException { throws Exception {
when(parameter.required(any(Object[].class), anyInt(), any())) when(parameter.required(any(Object[].class), anyInt(), any()))
.thenReturn(VALID_PRIVATE_TRANSACTION_RLP); .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); .thenReturn(PUBLIC_TRANSACTION);
when(transactionPool.addLocalTransaction(any(Transaction.class))) when(transactionPool.addLocalTransaction(any(Transaction.class)))
.thenReturn(ValidationResult.invalid(transactionInvalidReason)); .thenReturn(ValidationResult.invalid(transactionInvalidReason));
@ -283,7 +304,13 @@ public class EeaSendRawTransactionTest {
final JsonRpcResponse actualResponse = method.response(request); final JsonRpcResponse actualResponse = method.response(request);
assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); 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)); verify(transactionPool).addLocalTransaction(any(Transaction.class));
} }

Loading…
Cancel
Save