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 4 commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Features Added
- Java 21 for build and runtime. [#995](https://github.com/Consensys/web3signer/pull/995)
- Electra fork support. [#1020](https://github.com/Consensys/web3signer/pull/1020) and [#1023](https://github.com/Consensys/web3signer/pull/1023)
- BlobTx/EIP4844 transaction support for `eth_signTransaction` [#1026](https://github.com/Consensys/web3signer/pull/1026)

### Bugs fixed
- Override protobuf-java to 3.25.5 which is a transitive dependency from google-cloud-secretmanager. It fixes CVE-2024-7254.
Expand Down
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,9 @@ 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 +87,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 +132,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 @@ -12,13 +12,19 @@
*/
package tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.transaction;

import org.apache.tuweni.bytes.Bytes;
import org.web3j.crypto.Sign;
import org.web3j.rlp.RlpString;
import org.web3j.utils.Numeric;
import tech.pegasys.web3signer.core.service.jsonrpc.EthSendTransactionJsonParameters;
import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequest;
import tech.pegasys.web3signer.core.service.jsonrpc.JsonRpcRequestId;
import tech.pegasys.web3signer.core.service.jsonrpc.handlers.sendtransaction.NonceProvider;

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

import com.google.common.base.MoreObjects;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -71,6 +77,41 @@ public byte[] rlpEncode(final SignatureData signatureData) {
return RlpEncoder.encode(rlpList);
}

@Override
public byte[] rlpEncodeToSign() {
if (!isEip4844()) {
return rlpEncode(null);
}
// eip4844 transaction should be rlp encoded in another way
List<RlpType> resultTx = new ArrayList<>();

resultTx.add(RlpString.create(chainId));
resultTx.add(RlpString.create(nonce));
resultTx.add(RlpString.create(transactionJsonParameters.maxPriorityFeePerGas().orElseThrow()));
resultTx.add(RlpString.create(transactionJsonParameters.maxFeePerGas().orElseThrow()));
resultTx.add(RlpString.create(transactionJsonParameters.gas().orElse(DEFAULT_GAS)));

String to = transactionJsonParameters.receiver().orElse(DEFAULT_TO);
if (to.length() > 0) {
resultTx.add(RlpString.create(Numeric.hexStringToByteArray(to)));
} else {
resultTx.add(RlpString.create(""));
}

resultTx.add(RlpString.create(transactionJsonParameters.value().orElse(DEFAULT_VALUE)));
byte[] data = Numeric.hexStringToByteArray(transactionJsonParameters.data().orElse(DEFAULT_DATA));
resultTx.add(RlpString.create(data));

// access list
resultTx.add(new RlpList());

// Blob Transaction: max_fee_per_blob_gas and versioned_hashes
resultTx.add(RlpString.create(transactionJsonParameters.maxFeePerBlobGas().orElseThrow()));
resultTx.add(new RlpList(getRlpVersionedHashes(transactionJsonParameters.versionedHashes().orElseThrow())));

return RlpEncoder.encode(new RlpList(resultTx));
}

@Override
public boolean isNonceUserSpecified() {
return transactionJsonParameters.nonce().isPresent();
Expand Down Expand Up @@ -98,6 +139,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 +157,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 All @@ -130,4 +189,10 @@ protected RawTransaction createTransaction() {
transactionJsonParameters.data().orElse(DEFAULT_DATA));
}
}

public List<RlpType> getRlpVersionedHashes(List<Bytes> versionedHashes) {
return versionedHashes.stream()
.map(hash -> RlpString.create(hash.toArray()))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public byte[] rlpEncode(final SignatureData signatureData) {
return RlpEncoder.encode(rlpList);
}

@Override
public byte[] rlpEncodeToSign() {
return rlpEncode(null);
}

@Override
public boolean isNonceUserSpecified() {
return transactionJsonParameters.nonce().isPresent();
Expand Down Expand Up @@ -97,6 +102,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 @@ -33,6 +33,7 @@ public interface Transaction {
void updateFieldsIfRequired();

byte[] rlpEncode(SignatureData signatureData);
byte[] rlpEncodeToSign();

boolean isNonceUserSpecified();

Expand Down Expand Up @@ -60,4 +61,6 @@ static JsonRpcRequest jsonRpcRequest(
JsonRpcRequestId getId();

boolean isEip1559();

boolean isEip4844();
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
import tech.pegasys.web3signer.signing.secp256k1.Signature;

import java.nio.ByteBuffer;
import java.util.Base64;

import org.apache.tuweni.bytes.Bytes;
import org.web3j.crypto.Sign.SignatureData;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.crypto.transaction.type.TransactionType;

import javax.crypto.spec.PSource;

public class TransactionSerializer {

protected final SignerForIdentifier<SecpArtifactSignature> secpSigner;
Expand All @@ -42,7 +45,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 +82,35 @@ private static byte[] prependEip1559TransactionType(byte[] bytesToSign) {
.array();
}

private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use Bytes toHexString as that is used extensively in the code.

char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}

private byte[] serializeEip4844(final Transaction transaction) {
byte[] bytesToSign = transaction.rlpEncodeToSign();
bytesToSign = prependEip4844TransactionType(bytesToSign);
System.out.println("signing eip4844 " +bytesToHex(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