[PRIV] Add Private transaction abstraction (#592)

* Implement PrivateTransaction

* Add PrivateTransaction Test

* Fix PrivateTransaction comment

- add params for private transaction specific fields

* Fix PrivateTransaction signature calculation

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Ivaylo Kirilov 6 years ago committed by Rob Dawson
parent efc8653da2
commit 8136b0fcb2
  1. 631
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransaction.java
  2. 99
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/privacy/PrivateTransactionTest.java

@ -0,0 +1,631 @@
/*
* Copyright 2018 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 com.google.common.base.Preconditions.checkState;
import static tech.pegasys.pantheon.crypto.Hash.keccak256;
import tech.pegasys.pantheon.crypto.SECP256K1;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.Wei;
import tech.pegasys.pantheon.ethereum.rlp.RLP;
import tech.pegasys.pantheon.ethereum.rlp.RLPException;
import tech.pegasys.pantheon.ethereum.rlp.RLPInput;
import tech.pegasys.pantheon.ethereum.rlp.RLPOutput;
import tech.pegasys.pantheon.util.bytes.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.bytes.BytesValues;
import tech.pegasys.pantheon.util.uint.UInt256;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
/** An operation submitted by an external actor to be applied to the system. */
public class PrivateTransaction {
// Used for transactions that are not tied to a specific chain
// (e.g. does not have a chain id associated with it).
private static final int REPLAY_UNPROTECTED_V_BASE = 27;
private static final int REPLAY_PROTECTED_V_BASE = 35;
// The v signature parameter starts at 36 because 1 is the first valid chainId so:
// chainId > 1 implies that 2 * chainId + V_BASE > 36.
private static final int REPLAY_PROTECTED_V_MIN = 36;
private final long nonce;
private final Wei gasPrice;
private final long gasLimit;
private final Optional<Address> to;
private final Wei value;
private final SECP256K1.Signature signature;
private final BytesValue payload;
private final OptionalInt chainId;
private final BytesValue privateFrom;
private final List<BytesValue> privateFor;
private final BytesValue restriction;
// Caches a "hash" of a portion of the transaction used for sender recovery.
// Note that this hash does not include the transaction signature so it does not
// fully identify the transaction (use the result of the {@code hash()} for that).
// It is only used to compute said signature and recover the sender from it.
protected volatile Bytes32 hashNoSignature;
// Caches the transaction sender.
protected volatile Address sender;
// Caches the hash used to uniquely identify the transaction.
protected volatile Hash hash;
public static Builder builder() {
return new Builder();
}
public static PrivateTransaction readFrom(final RLPInput input) throws RLPException {
input.enterList();
final Builder builder =
builder()
.nonce(input.readLongScalar())
.gasPrice(input.readUInt256Scalar(Wei::wrap))
.gasLimit(input.readLongScalar())
.to(input.readBytesValue(v -> v.size() == 0 ? null : Address.wrap(v)))
.value(input.readUInt256Scalar(Wei::wrap))
.payload(input.readBytesValue());
final int v = input.readIntScalar();
final byte recId;
int chainId = -1;
if (v == REPLAY_UNPROTECTED_V_BASE || v == REPLAY_UNPROTECTED_V_BASE + 1) {
recId = (byte) (v - REPLAY_UNPROTECTED_V_BASE);
} else if (v > REPLAY_PROTECTED_V_MIN) {
chainId = (v - REPLAY_PROTECTED_V_BASE) / 2;
recId = (byte) (v - (2 * chainId + REPLAY_PROTECTED_V_BASE));
} else {
throw new RuntimeException(
String.format("An unsupported encoded `v` value of %s was found", v));
}
final BigInteger r = BytesValues.asUnsignedBigInteger(input.readUInt256Scalar().getBytes());
final BigInteger s = BytesValues.asUnsignedBigInteger(input.readUInt256Scalar().getBytes());
final SECP256K1.Signature signature = SECP256K1.Signature.create(r, s, recId);
final BytesValue privateFrom = input.readBytesValue();
final List<BytesValue> privateFor = input.readList(RLPInput::readBytesValue);
final BytesValue restriction = input.readBytesValue();
input.leaveList();
return builder
.chainId(chainId)
.signature(signature)
.privateFrom(privateFrom)
.privateFor(privateFor)
.restriction(restriction)
.build();
}
/**
* Instantiates a transaction instance.
*
* @param nonce the nonce
* @param gasPrice the gas price
* @param gasLimit the gas limit
* @param to the transaction recipient
* @param value the value being transferred to the recipient
* @param signature the signature
* @param payload the payload
* @param sender the transaction sender
* @param chainId the chain id to apply the transaction to
* <p>The {@code to} will be an {@code Optional.empty()} for a contract creation transaction;
* otherwise it should contain an address.
* <p>The {@code chainId} must be greater than 0 to be applied to a specific chain; otherwise
* it will default to any chain.
* @param privateFrom The public key of the sender of this private transaction
* @param privateFor An array of the public keys of the intended recipients of this private
* transaction
* @param restriction the restriction of this private transaction
*/
protected PrivateTransaction(
final long nonce,
final Wei gasPrice,
final long gasLimit,
final Optional<Address> to,
final Wei value,
final SECP256K1.Signature signature,
final BytesValue payload,
final Address sender,
final int chainId,
final BytesValue privateFrom,
final List<BytesValue> privateFor,
final BytesValue restriction) {
this.nonce = nonce;
this.gasPrice = gasPrice;
this.gasLimit = gasLimit;
this.to = to;
this.value = value;
this.signature = signature;
this.payload = payload;
this.sender = sender;
this.chainId = chainId > 0 ? OptionalInt.of(chainId) : OptionalInt.empty();
this.privateFrom = privateFrom;
this.privateFor = privateFor;
this.restriction = restriction;
}
/**
* Returns the transaction nonce.
*
* @return the transaction nonce
*/
public long getNonce() {
return nonce;
}
/**
* Return the transaction gas price.
*
* @return the transaction gas price
*/
public Wei getGasPrice() {
return gasPrice;
}
/**
* Returns the transaction gas limit.
*
* @return the transaction gas limit
*/
public long getGasLimit() {
return gasLimit;
}
/**
* Returns the transaction recipient.
*
* <p>The {@code Optional<Address>} will be {@code Optional.empty()} if the transaction is a
* contract creation; otherwise it will contain the message call transaction recipient.
*
* @return the transaction recipient if a message call; otherwise {@code Optional.empty()}
*/
public Optional<Address> 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)
*
* <p>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<BytesValue> 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.
*
* <p>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<BytesValue> 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<Address> 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<BytesValue> 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<BytesValue> 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);
}
}
}

@ -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));
}
}
Loading…
Cancel
Save