Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BlobTx(eip4844) for eth_signTransaction #1026

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
package tech.pegasys.web3signer.core.service.jsonrpc;

import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.decodeBigInteger;
import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.decodeBytesList;
import static tech.pegasys.web3signer.core.service.jsonrpc.RpcUtil.validateNotEmpty;

import java.math.BigInteger;
import java.util.List;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import org.apache.tuweni.bytes.Bytes;

@JsonIgnoreProperties(ignoreUnknown = true)
public class EthSendTransactionJsonParameters {
Expand All @@ -34,6 +37,8 @@ public class EthSendTransactionJsonParameters {
private String data;
private BigInteger maxFeePerGas;
private BigInteger maxPriorityFeePerGas;
private BigInteger maxFeePerBlobGas;
private List<Bytes> versionedHashes;

@JsonCreator
public EthSendTransactionJsonParameters(@JsonProperty("from") final String sender) {
Expand Down Expand Up @@ -81,6 +86,16 @@ public void maxFeePerGas(final String maxFeePerGas) {
this.maxFeePerGas = decodeBigInteger(maxFeePerGas);
}

@JsonSetter("maxFeePerBlobGas")
public void maxFeePerBlobGas(final String maxFeePerBlobGas) {
this.maxFeePerBlobGas = decodeBigInteger(maxFeePerBlobGas);
}

@JsonSetter("versionedHashes")
public void versionedHashes(final List<String> versionedHashes) {
this.versionedHashes = decodeBytesList(versionedHashes);
}

public Optional<String> data() {
return Optional.ofNullable(data);
}
Expand Down Expand Up @@ -116,4 +131,12 @@ public Optional<BigInteger> maxPriorityFeePerGas() {
public Optional<BigInteger> maxFeePerGas() {
return Optional.ofNullable(maxFeePerGas);
}

public Optional<BigInteger> maxFeePerBlobGas() {
return Optional.ofNullable(maxFeePerBlobGas);
}

public Optional<List<Bytes>> versionedHashes() {
return Optional.ofNullable(versionedHashes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

import java.math.BigInteger;
import java.util.List;
import java.util.stream.Collectors;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import org.apache.tuweni.bytes.Bytes;

public class RpcUtil {
public static final String JSON_RPC_VERSION = "2.0";
Expand Down Expand Up @@ -58,6 +60,10 @@ static BigInteger decodeBigInteger(final String value) {
return value == null ? null : decodeQuantity(value);
}

static List<Bytes> decodeBytesList(final List<String> value) {
return value.stream().map(Bytes::fromHexString).collect(Collectors.toList());
}

public static JsonRpcError determineErrorCode(final String body, final JsonDecoder decoder) {
try {
final JsonRpcErrorResponse response =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ public boolean isEip1559() {
&& transactionJsonParameters.maxFeePerGas().isPresent();
}

@Override
public boolean isEip4844() {
return transactionJsonParameters.maxFeePerBlobGas().isPresent()
&& transactionJsonParameters.versionedHashes().isPresent();
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
Expand All @@ -110,7 +116,19 @@ public String toString() {
}

protected RawTransaction createTransaction() {
if (isEip1559()) {
if (isEip4844()) {
return RawTransaction.createTransaction(
chainId,
nonce,
transactionJsonParameters.maxPriorityFeePerGas().orElseThrow(),
transactionJsonParameters.maxFeePerGas().orElseThrow(),
transactionJsonParameters.gas().orElse(DEFAULT_GAS),
transactionJsonParameters.receiver().orElse(DEFAULT_TO),
transactionJsonParameters.value().orElse(DEFAULT_VALUE),
transactionJsonParameters.data().orElse(DEFAULT_DATA),
transactionJsonParameters.maxFeePerBlobGas().orElseThrow(),
transactionJsonParameters.versionedHashes().orElseThrow());
} else if (isEip1559()) {
return RawTransaction.createTransaction(
chainId,
nonce,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ public boolean isEip1559() {
&& transactionJsonParameters.maxFeePerGas().isPresent();
}

@Override
public boolean isEip4844() {
return false;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ static JsonRpcRequest jsonRpcRequest(
JsonRpcRequestId getId();

boolean isEip1559();

boolean isEip4844();
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ public TransactionSerializer(
}

public String serialize(final Transaction transaction) {
if (transaction.isEip1559()) {
if (transaction.isEip4844()) {
return toHexString(serializeEip4844(transaction));
} else if (transaction.isEip1559()) {
return toHexString(serializeEip1559(transaction));
} else {
return toHexString(serializeFrontier(transaction));
Expand Down Expand Up @@ -77,6 +79,23 @@ private static byte[] prependEip1559TransactionType(byte[] bytesToSign) {
.array();
}

private byte[] serializeEip4844(final Transaction transaction) {
byte[] bytesToSign = transaction.rlpEncode(null);
bytesToSign = prependEip4844TransactionType(bytesToSign);

final SignatureData signatureData = sign(transaction.sender(), bytesToSign);

byte[] serializedBytes = transaction.rlpEncode(signatureData);
return prependEip4844TransactionType(serializedBytes);
}

private static byte[] prependEip4844TransactionType(byte[] bytesToSign) {
return ByteBuffer.allocate(bytesToSign.length + 1)
.put(TransactionType.EIP4844.getRlpType())
.put(bytesToSign)
.array();
}

private SignatureData sign(final String eth1Address, final byte[] bytesToSign) {
final SecpArtifactSignature artifactSignature =
secpSigner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction.TransactionFactory;

import java.math.BigInteger;
import java.util.List;
import java.util.Optional;

import io.vertx.core.json.JsonObject;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand All @@ -40,6 +42,13 @@ private Optional<BigInteger> getStringAsOptionalBigInteger(
return Optional.of(new BigInteger(value.substring(2), 16));
}

private Optional<List<Bytes>> getListAsOptionalBytesList(
final JsonObject object, final String key) {
final List<String> value;
value = object.getJsonArray(key).getList();
return Optional.of(RpcUtil.decodeBytesList(value));
}

@Test
public void transactionStoredInJsonArrayCanBeDecoded() throws Throwable {
final JsonObject parameters = validEthTransactionParameters();
Expand Down Expand Up @@ -75,6 +84,29 @@ public void eip1559TransactionStoredInJsonArrayCanBeDecoded() {
.isEqualTo(getStringAsOptionalBigInteger(parameters, "maxFeePerGas"));
}

@Test
public void eip4844TransactionStoredInJsonArrayCanBeDecoded() {
final JsonObject parameters = validEip4844EthTransactionParameters();

final JsonRpcRequest request = wrapParametersInRequest(parameters);
final EthSendTransactionJsonParameters txnParams =
factory.fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request);

assertThat(txnParams.gas()).isEqualTo(getStringAsOptionalBigInteger(parameters, "gas"));
assertThat(txnParams.gasPrice()).isEmpty();
assertThat(txnParams.nonce()).isEqualTo(getStringAsOptionalBigInteger(parameters, "nonce"));
assertThat(txnParams.receiver()).isEqualTo(Optional.of(parameters.getString("to")));
assertThat(txnParams.value()).isEqualTo(getStringAsOptionalBigInteger(parameters, "value"));
assertThat(txnParams.maxPriorityFeePerGas())
.isEqualTo(getStringAsOptionalBigInteger(parameters, "maxPriorityFeePerGas"));
assertThat(txnParams.maxFeePerGas())
.isEqualTo(getStringAsOptionalBigInteger(parameters, "maxFeePerGas"));
assertThat(txnParams.maxFeePerBlobGas())
.isEqualTo(getStringAsOptionalBigInteger(parameters, "maxFeePerBlobGas"));
assertThat(txnParams.versionedHashes())
.isEqualTo(getListAsOptionalBytesList(parameters, "versionedHashes"));
}

@Test
public void transactionNotStoredInJsonArrayCanBeDecoded() throws Throwable {
final JsonObject parameters = validEthTransactionParameters();
Expand Down Expand Up @@ -139,6 +171,16 @@ private JsonObject validEip1559EthTransactionParameters() {
return parameters;
}

private JsonObject validEip4844EthTransactionParameters() {
final JsonObject parameters = validEip1559EthTransactionParameters();
parameters.put("maxFeePerBlobGas", "0x123456789");
parameters.put(
"versionedHashes",
List.of(
"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"));
return parameters;
}

private <T> JsonRpcRequest wrapParametersInRequest(final T parameters) {
final JsonObject input = new JsonObject();
input.put("jsonrpc", 2.0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.web3j.crypto.Sign.SignatureData;
import org.web3j.crypto.SignedRawTransaction;
import org.web3j.crypto.TransactionDecoder;
import org.web3j.crypto.transaction.type.Transaction1559;
import org.web3j.crypto.transaction.type.Transaction4844;
import org.web3j.crypto.transaction.type.TransactionType;
import org.web3j.utils.Numeric;

Expand Down Expand Up @@ -126,6 +130,59 @@ private static byte[] prependEip1559TransactionType(byte[] bytesToSign) {
.array();
}

@Test
public void rlpEncodesEip4844Transaction() {
final EthSendTransactionJsonParameters params =
new EthSendTransactionJsonParameters("0x7577919ae5df4941180eac211965f275cdce314d");
params.receiver("0xd46e8dd67c5d32be8058bb8eb970870f07244567");
params.gas("0x76c0");
params.maxPriorityFeePerGas("0x9184e72a000");
params.maxFeePerGas("0x9184e72a001");
params.nonce("0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2");
params.value("0x0");
params.data(
"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675");
params.maxFeePerBlobGas("0x123456789");
ArrayList<String> versionedHashes = new ArrayList<>();
versionedHashes.add("0x0102030000000000000000000000000000000000000000000000000000000000");
params.versionedHashes(versionedHashes);

ethTransaction =
new EthTransaction(1337L, params, () -> BigInteger.ZERO, new JsonRpcRequestId(1));

final SignatureData signatureData = null;
final byte[] rlpEncodedBytes = ethTransaction.rlpEncode(signatureData);
final String rlpString = Numeric.toHexString(prependEip4844TransactionType(rlpEncodedBytes));

final Transaction4844 decodedTransaction =
(Transaction4844) TransactionDecoder.decode(rlpString).getTransaction();
assertThat(decodedTransaction.getTo()).isEqualTo("0xd46e8dd67c5d32be8058bb8eb970870f07244567");
assertThat(decodedTransaction.getGasLimit()).isEqualTo(Numeric.decodeQuantity("0x76c0"));
assertThat(decodedTransaction.getMaxPriorityFeePerGas())
.isEqualTo(Numeric.decodeQuantity("0x9184e72a000"));
assertThat(decodedTransaction.getMaxFeePerGas())
.isEqualTo(Numeric.decodeQuantity("0x9184e72a001"));
assertThat(decodedTransaction.getNonce())
.isEqualTo(
Numeric.decodeQuantity(
"0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2"));
assertThat(decodedTransaction.getValue()).isEqualTo(Numeric.decodeQuantity("0x0"));
assertThat(decodedTransaction.getData())
.isEqualTo(
"d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675");
assertThat(decodedTransaction.getMaxFeePerBlobGas())
.isEqualTo(Numeric.decodeQuantity("0x123456789"));
assertThat(decodedTransaction.getVersionedHashes())
.isEqualTo(versionedHashes.stream().map(Bytes::fromHexString).collect(Collectors.toList()));
}

private static byte[] prependEip4844TransactionType(byte[] bytesToSign) {
return ByteBuffer.allocate(bytesToSign.length + 1)
.put(TransactionType.EIP4844.getRlpType())
.put(bytesToSign)
.array();
}

@Test
@SuppressWarnings("unchecked")
public void createsJsonRequest() {
Expand Down
Loading