getTo() {
+ return to;
+ }
+
+ /**
+ * Returns the value transferred in the transaction.
+ *
+ * @return the value transferred in the transaction
+ */
+ public Wei getValue() {
+ return value;
+ }
+
+ /**
+ * Returns the signature used to sign the transaction.
+ *
+ * @return the signature used to sign the transaction
+ */
+ public SECP256K1.Signature getSignature() {
+ return signature;
+ }
+
+ /**
+ * Returns the transaction payload.
+ *
+ * @return the transaction payload
+ */
+ public BytesValue getPayload() {
+ return payload;
+ }
+
+ /**
+ * Return the transaction chain id (if it exists)
+ *
+ * The {@code OptionalInt} will be {@code OptionalInt.empty()} if the transaction is not tied
+ * to a specific chain.
+ *
+ * @return the transaction chain id if it exists; otherwise {@code OptionalInt.empty()}
+ */
+ public OptionalInt getChainId() {
+ return chainId;
+ }
+
+ /**
+ * Returns the enclave public key of the sender.
+ *
+ * @return the enclave public key of the sender.
+ */
+ public BytesValue getPrivateFrom() {
+ return privateFrom;
+ }
+
+ /**
+ * Returns the enclave public keys of the receivers.
+ *
+ * @return the enclave public keys of the receivers
+ */
+ public List getPrivateFor() {
+ return privateFor;
+ }
+
+ /**
+ * Returns the restriction of this private transaction.
+ *
+ * @return the restriction
+ */
+ public BytesValue getRestriction() {
+ return restriction;
+ }
+
+ /**
+ * Returns the transaction sender.
+ *
+ * @return the transaction sender
+ */
+ public Address getSender() {
+ if (sender == null) {
+ final SECP256K1.PublicKey publicKey =
+ SECP256K1.PublicKey.recoverFromSignature(getOrComputeSenderRecoveryHash(), signature)
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ "Cannot recover public key from " + "signature for " + this));
+ sender = Address.extract(Hash.hash(publicKey.getEncodedBytes()));
+ }
+ return sender;
+ }
+
+ private Bytes32 getOrComputeSenderRecoveryHash() {
+ if (hashNoSignature == null) {
+ hashNoSignature =
+ computeSenderRecoveryHash(
+ nonce,
+ gasPrice,
+ gasLimit,
+ to.orElse(null),
+ value,
+ payload,
+ chainId,
+ privateFrom,
+ privateFor,
+ restriction);
+ }
+ return hashNoSignature;
+ }
+
+ /**
+ * Writes the transaction to RLP
+ *
+ * @param out the output to write the transaction to
+ */
+ public void writeTo(final RLPOutput out) {
+ out.startList();
+
+ out.writeLongScalar(getNonce());
+ out.writeUInt256Scalar(getGasPrice());
+ out.writeLongScalar(getGasLimit());
+ out.writeBytesValue(getTo().isPresent() ? getTo().get() : BytesValue.EMPTY);
+ out.writeUInt256Scalar(getValue());
+ out.writeBytesValue(getPayload());
+ writeSignature(out);
+ out.writeBytesValue(getPrivateFrom());
+ out.writeList(getPrivateFor(), (bv, rlpO) -> rlpO.writeBytesValue(bv));
+ out.writeBytesValue(getRestriction());
+
+ out.endList();
+ }
+
+ private void writeSignature(final RLPOutput out) {
+ out.writeIntScalar(getV());
+ out.writeBigIntegerScalar(getSignature().getR());
+ out.writeBigIntegerScalar(getSignature().getS());
+ }
+
+ public BigInteger getR() {
+ return signature.getR();
+ }
+
+ public BigInteger getS() {
+ return signature.getS();
+ }
+
+ public int getV() {
+ final int v;
+ if (!chainId.isPresent()) {
+ v = signature.getRecId() + REPLAY_UNPROTECTED_V_BASE;
+ } else {
+ v = (getSignature().getRecId() + REPLAY_PROTECTED_V_BASE + 2 * chainId.getAsInt());
+ }
+ return v;
+ }
+
+ /**
+ * Returns the transaction hash.
+ *
+ * @return the transaction hash
+ */
+ public Hash hash() {
+ if (hash == null) {
+ final BytesValue rlp = RLP.encode(this::writeTo);
+ hash = Hash.hash(rlp);
+ }
+ return hash;
+ }
+
+ /**
+ * Returns whether the transaction is a contract creation
+ *
+ * @return {@code true} if this is a contract-creation transaction; otherwise {@code false}
+ */
+ public boolean isContractCreation() {
+ return !getTo().isPresent();
+ }
+
+ /**
+ * Calculates the up-front cost for the gas the transaction can use.
+ *
+ * @return the up-front cost for the gas the transaction can use.
+ */
+ public Wei getUpfrontGasCost() {
+ return Wei.of(getGasLimit()).times(getGasPrice());
+ }
+
+ /**
+ * Calculates the up-front cost for the transaction.
+ *
+ * The up-front cost is paid by the sender account before the transaction is executed. The
+ * sender must have the amount in its account balance to execute and some of this amount may be
+ * refunded after the transaction has executed.
+ *
+ * @return the up-front gas cost for the transaction
+ */
+ public Wei getUpfrontCost() {
+ return getUpfrontGasCost().plus(getValue());
+ }
+
+ private static Bytes32 computeSenderRecoveryHash(
+ final long nonce,
+ final Wei gasPrice,
+ final long gasLimit,
+ final Address to,
+ final Wei value,
+ final BytesValue payload,
+ final OptionalInt chainId,
+ final BytesValue privateFrom,
+ final List privateFor,
+ final BytesValue restriction) {
+ return keccak256(
+ RLP.encode(
+ out -> {
+ out.startList();
+ out.writeLongScalar(nonce);
+ out.writeUInt256Scalar(gasPrice);
+ out.writeLongScalar(gasLimit);
+ out.writeBytesValue(to == null ? BytesValue.EMPTY : to);
+ out.writeUInt256Scalar(value);
+ out.writeBytesValue(payload);
+ if (chainId.isPresent()) {
+ out.writeIntScalar(chainId.getAsInt());
+ out.writeUInt256Scalar(UInt256.ZERO);
+ out.writeUInt256Scalar(UInt256.ZERO);
+ }
+ out.writeBytesValue(privateFrom);
+ out.writeList(privateFor, (bv, rlpO) -> rlpO.writeBytesValue(bv));
+ out.writeBytesValue(restriction);
+ out.endList();
+ }));
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (!(other instanceof PrivateTransaction)) {
+ return false;
+ }
+ final PrivateTransaction that = (PrivateTransaction) other;
+ return this.chainId.equals(that.chainId)
+ && this.gasLimit == that.gasLimit
+ && this.gasPrice.equals(that.gasPrice)
+ && this.nonce == that.nonce
+ && this.payload.equals(that.payload)
+ && this.signature.equals(that.signature)
+ && this.to.equals(that.to)
+ && this.value.equals(that.value)
+ && this.privateFor.equals(that.privateFor)
+ && this.privateFrom.equals(that.privateFrom)
+ && this.restriction.equals(that.restriction);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ nonce,
+ gasPrice,
+ gasLimit,
+ to,
+ value,
+ payload,
+ signature,
+ chainId,
+ privateFor,
+ privateFrom,
+ restriction);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(isContractCreation() ? "ContractCreation" : "MessageCall").append("{");
+ sb.append("nonce=").append(getNonce()).append(", ");
+ sb.append("gasPrice=").append(getGasPrice()).append(", ");
+ sb.append("gasLimit=").append(getGasLimit()).append(", ");
+ if (getTo().isPresent()) sb.append("to=").append(getTo().get()).append(", ");
+ sb.append("value=").append(getValue()).append(", ");
+ sb.append("sig=").append(getSignature()).append(", ");
+ if (chainId.isPresent()) sb.append("chainId=").append(getChainId().getAsInt()).append(", ");
+ sb.append("payload=").append(getPayload());
+ sb.append("privateFrom=").append(getPrivateFrom());
+ sb.append("privateFor=").append(Arrays.toString(getPrivateFor().toArray()));
+ sb.append("restriction=").append(getRestriction());
+ return sb.append("}").toString();
+ }
+
+ public Optional contractAddress() {
+ if (isContractCreation()) {
+ return Optional.of(Address.contractAddress(getSender(), getNonce()));
+ }
+ return Optional.empty();
+ }
+
+ public static class Builder {
+
+ protected long nonce = -1L;
+
+ protected Wei gasPrice;
+
+ protected long gasLimit = -1L;
+
+ protected Address to;
+
+ protected Wei value;
+
+ protected SECP256K1.Signature signature;
+
+ protected BytesValue payload;
+
+ protected Address sender;
+
+ protected int chainId = -1;
+
+ protected BytesValue privateFrom;
+
+ protected List privateFor;
+
+ protected BytesValue restriction;
+
+ public Builder chainId(final int chainId) {
+ this.chainId = chainId;
+ return this;
+ }
+
+ public Builder gasPrice(final Wei gasPrice) {
+ this.gasPrice = gasPrice;
+ return this;
+ }
+
+ public Builder gasLimit(final long gasLimit) {
+ this.gasLimit = gasLimit;
+ return this;
+ }
+
+ public Builder nonce(final long nonce) {
+ this.nonce = nonce;
+ return this;
+ }
+
+ public Builder value(final Wei value) {
+ this.value = value;
+ return this;
+ }
+
+ public Builder to(final Address to) {
+ this.to = to;
+ return this;
+ }
+
+ public Builder payload(final BytesValue payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ public Builder sender(final Address sender) {
+ this.sender = sender;
+ return this;
+ }
+
+ public Builder signature(final SECP256K1.Signature signature) {
+ this.signature = signature;
+ return this;
+ }
+
+ public Builder privateFrom(final BytesValue privateFrom) {
+ this.privateFrom = privateFrom;
+ return this;
+ }
+
+ public Builder privateFor(final List privateFor) {
+ this.privateFor = privateFor;
+ return this;
+ }
+
+ public Builder restriction(final BytesValue restriction) {
+ this.restriction = restriction;
+ return this;
+ }
+
+ public PrivateTransaction build() {
+ return new PrivateTransaction(
+ nonce,
+ gasPrice,
+ gasLimit,
+ Optional.ofNullable(to),
+ value,
+ signature,
+ payload,
+ sender,
+ chainId,
+ privateFrom,
+ privateFor,
+ restriction);
+ }
+
+ public PrivateTransaction signAndBuild(final SECP256K1.KeyPair keys) {
+ checkState(
+ signature == null, "The transaction signature has already been provided to this builder");
+ signature(computeSignature(keys));
+ sender(Address.extract(Hash.hash(keys.getPublicKey().getEncodedBytes())));
+ return build();
+ }
+
+ protected SECP256K1.Signature computeSignature(final SECP256K1.KeyPair keys) {
+ final OptionalInt optionalChainId =
+ chainId > 0 ? OptionalInt.of(chainId) : OptionalInt.empty();
+ final Bytes32 hash =
+ computeSenderRecoveryHash(
+ nonce,
+ gasPrice,
+ gasLimit,
+ to,
+ value,
+ payload,
+ optionalChainId,
+ privateFrom,
+ privateFor,
+ restriction);
+ return SECP256K1.sign(hash, keys);
+ }
+ }
+}
diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionTest.java
new file mode 100644
index 0000000000..ea1493c4a2
--- /dev/null
+++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.privacy;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import tech.pegasys.pantheon.crypto.SECP256K1;
+import tech.pegasys.pantheon.ethereum.core.Address;
+import tech.pegasys.pantheon.ethereum.core.Wei;
+import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPInput;
+import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput;
+import tech.pegasys.pantheon.ethereum.rlp.RLPException;
+import tech.pegasys.pantheon.util.bytes.BytesValue;
+
+import java.math.BigInteger;
+import java.util.Optional;
+
+import com.google.common.collect.Lists;
+import org.junit.Test;
+
+public class PrivateTransactionTest {
+
+ private static final String INVALID_RLP =
+ "0xf87f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d87"
+ + "a0fffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ + "fffffff801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff75"
+ + "9b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43"
+ + "601b4ab949f53faa07bd2c804";
+
+ private static final String VALID_PRIVATE_TRANSACTION_RLP =
+ "0xf90113800182520894095e7baea6a6c7c4c2dfeb977efac326af552d87"
+ + "a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ + "ffff801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d"
+ + "495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab94"
+ + "9f53faa07bd2c804ac41316156744d784c4355486d425648586f5a7a7a4267"
+ + "5062572f776a3561784470573958386c393153476f3df85aac41316156744d"
+ + "784c4355486d425648586f5a7a7a42675062572f776a356178447057395838"
+ + "6c393153476f3dac4b6f32625671442b6e4e6c4e594c35454537793349644f"
+ + "6e766966746a69697a706a52742b4854754642733d8a726573747269637465"
+ + "64";
+
+ private static final PrivateTransaction VALID_PRIVATE_TRANSACTION =
+ new PrivateTransaction(
+ 0L,
+ Wei.of(1),
+ 21000L,
+ Optional.of(
+ Address.wrap(BytesValue.fromHexString("0x095e7baea6a6c7c4c2dfeb977efac326af552d87"))),
+ Wei.of(
+ new BigInteger(
+ "115792089237316195423570985008687907853269984665640564039457584007913129639935")),
+ SECP256K1.Signature.create(
+ new BigInteger(
+ "32886959230931919120748662916110619501838190146643992583529828535682419954515"),
+ new BigInteger(
+ "14473701025599600909210599917245952381483216609124029382871721729679842002948"),
+ Byte.valueOf("0")),
+ BytesValue.fromHexString("0x"),
+ Address.wrap(BytesValue.fromHexString("0x8411b12666f68ef74cace3615c9d5a377729d03f")),
+ 0,
+ BytesValue.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8)),
+ Lists.newArrayList(
+ BytesValue.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8)),
+ BytesValue.wrap("Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=".getBytes(UTF_8))),
+ BytesValue.wrap("restricted".getBytes(UTF_8)));
+
+ @Test
+ public void testWriteTo() {
+ BytesValueRLPOutput bvrlpo = new BytesValueRLPOutput();
+ VALID_PRIVATE_TRANSACTION.writeTo(bvrlpo);
+ assertEquals(VALID_PRIVATE_TRANSACTION_RLP, bvrlpo.encoded().toString());
+ }
+
+ @Test
+ public void testReadFrom() {
+ PrivateTransaction p =
+ PrivateTransaction.readFrom(
+ new BytesValueRLPInput(BytesValue.fromHexString(VALID_PRIVATE_TRANSACTION_RLP), false));
+
+ assertEquals(VALID_PRIVATE_TRANSACTION, p);
+ }
+
+ @Test(expected = RLPException.class)
+ public void testReadFromInvalid() {
+ PrivateTransaction.readFrom(
+ new BytesValueRLPInput(BytesValue.fromHexString(INVALID_RLP), false));
+ }
+}