Skip to content

Commit 33d9f42

Browse files
feat: Support Pectra EVM Type 4 Transaction Encoding (EIP-7702) (#2774)
Signed-off-by: emiliyank <e.kadiyski@gmail.com> Signed-off-by: Mustafa Uzun <mustafa.uzun@limechain.tech> Co-authored-by: emiliyank <e.kadiyski@gmail.com>
1 parent 0ab7995 commit 33d9f42

5 files changed

Lines changed: 687 additions & 13 deletions

File tree

sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransaction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public byte[] getEthereumData() {
5959
}
6060

6161
/**
62-
* Sets the raw Ethereum transaction (RLP encoded type 0, 1, and 2). Complete
62+
* Sets the raw Ethereum transaction (RLP encoded type 0, 1, 2 and 4). Complete
6363
* unless the callDataFileId is set.
6464
*
6565
* @param ethereumData raw ethereum transaction bytes

sdk/src/main/java/com/hedera/hashgraph/sdk/EthereumTransactionData.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
/**
88
* This class represents the data of an Ethereum transaction.
99
* <p>
10-
* It may be of subclass {@link EthereumTransactionDataLegacy}, {@link EthereumTransactionDataEip2930} or
11-
* {@link EthereumTransactionDataEip1559}.
10+
* It may be of subclass {@link EthereumTransactionDataLegacy} or of subclass {@link EthereumTransactionDataEip1559}
1211
*/
1312
public abstract class EthereumTransactionData {
1413
/**
@@ -25,15 +24,15 @@ static EthereumTransactionData fromBytes(byte[] bytes) {
2524
var rlpItem = decoder.next();
2625
if (rlpItem.isList()) {
2726
return EthereumTransactionDataLegacy.fromBytes(bytes);
28-
}
29-
byte typeByte = rlpItem.asByte();
30-
switch (typeByte) {
31-
case 0x01:
32-
return EthereumTransactionDataEip2930.fromBytes(bytes);
33-
case 0x02:
34-
return EthereumTransactionDataEip1559.fromBytes(bytes);
35-
default:
36-
throw new IllegalArgumentException("rlp type byte " + typeByte + " is not supported");
27+
} else {
28+
var typeByte = rlpItem.asByte();
29+
30+
return switch (typeByte) {
31+
case 0x01 -> EthereumTransactionDataEip2930.fromBytes(bytes);
32+
case 0x02 -> EthereumTransactionDataEip1559.fromBytes(bytes);
33+
case 0x04 -> EthereumTransactionDataEip7702.fromBytes(bytes);
34+
default -> throw new IllegalArgumentException("rlp type byte " + typeByte + "is not supported");
35+
};
3736
}
3837
}
3938

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package com.hedera.hashgraph.sdk;
3+
4+
import com.esaulpaugh.headlong.rlp.RLPDecoder;
5+
import com.esaulpaugh.headlong.rlp.RLPEncoder;
6+
import com.esaulpaugh.headlong.rlp.RLPItem;
7+
import com.esaulpaugh.headlong.util.Integers;
8+
import com.google.common.base.MoreObjects;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.stream.Collectors;
12+
import org.bouncycastle.util.encoders.Hex;
13+
14+
/**
15+
* The ethereum transaction data, in the format defined in
16+
* <a href="https://eips.ethereum.org/EIPS/eip-7702">EIP-7702</a>
17+
*/
18+
public class EthereumTransactionDataEip7702 extends EthereumTransactionData {
19+
20+
/**
21+
* ID of the chain.
22+
*/
23+
public byte[] chainId;
24+
25+
/**
26+
* Transaction's nonce.
27+
*/
28+
public byte[] nonce;
29+
30+
/**
31+
* An 'optional' additional fee in Ethereum that is paid directly to miners in order to incentivize them to include
32+
* your transaction in a block. Not used in Hedera.
33+
*/
34+
public byte[] maxPriorityGas;
35+
36+
/**
37+
* The maximum amount, in tinybars, that the payer of the hedera transaction is willing to pay to complete the
38+
* transaction.
39+
*/
40+
public byte[] maxGas;
41+
42+
/**
43+
* The amount of gas available for the transaction.
44+
*/
45+
public byte[] gasLimit;
46+
47+
/**
48+
* The receiver of the transaction.
49+
*/
50+
public byte[] to;
51+
52+
/**
53+
* The transaction value.
54+
*/
55+
public byte[] value;
56+
57+
/**
58+
* Specifies an array of addresses and storage keys that the transaction plans to access.
59+
*/
60+
public List<byte[]> accessList;
61+
62+
/**
63+
* The list of delegation authorizations.
64+
*/
65+
public List<AuthorizationTuple> authorizationList;
66+
67+
/**
68+
* Recovery parameter used to ease the signature verification.
69+
*/
70+
public byte[] recoveryId;
71+
72+
/**
73+
* The R value of the signature.
74+
*/
75+
public byte[] r;
76+
77+
/**
78+
* The S value of the signature.
79+
*/
80+
public byte[] s;
81+
82+
EthereumTransactionDataEip7702(
83+
HeaderData headerData,
84+
byte[] callData,
85+
List<byte[]> accessList,
86+
List<AuthorizationTuple> authorizationList,
87+
SignatureData signatureData) {
88+
super(callData);
89+
90+
this.chainId = headerData.chainId;
91+
this.nonce = headerData.nonce;
92+
this.maxPriorityGas = headerData.maxPriorityGas;
93+
this.maxGas = headerData.maxGas;
94+
this.gasLimit = headerData.gasLimit;
95+
this.to = headerData.to;
96+
this.value = headerData.value;
97+
this.accessList = accessList;
98+
this.authorizationList = authorizationList;
99+
this.recoveryId = signatureData.recoveryId;
100+
this.r = signatureData.r;
101+
this.s = signatureData.s;
102+
}
103+
104+
/**
105+
* Convert a byte array to an ethereum transaction data.
106+
*
107+
* @param bytes the byte array
108+
* @return the ethereum transaction data
109+
*/
110+
public static EthereumTransactionDataEip7702 fromBytes(byte[] bytes) {
111+
var decoder = RLPDecoder.RLP_STRICT.sequenceIterator(bytes);
112+
var rlpItem = decoder.next();
113+
114+
// typed transaction?
115+
byte typeByte = rlpItem.asByte();
116+
if (typeByte != 4) {
117+
throw new IllegalArgumentException("rlp type byte " + typeByte + " is not supported");
118+
}
119+
rlpItem = decoder.next();
120+
if (!rlpItem.isList()) {
121+
throw new IllegalArgumentException("expected RLP element list");
122+
}
123+
List<RLPItem> rlpList = rlpItem.asRLPList().elements();
124+
if (rlpList.size() != 13) {
125+
throw new IllegalArgumentException("expected 13 RLP encoded elements, found " + rlpList.size());
126+
}
127+
128+
var accessList = new ArrayList<byte[]>();
129+
for (var accessListItem : rlpList.get(8).asRLPList().elements()) {
130+
accessList.add(accessListItem.data());
131+
}
132+
133+
var authorizationList = new ArrayList<AuthorizationTuple>();
134+
for (var authorizationTuple : rlpList.get(9).asRLPList().elements()) {
135+
var tupleElements = authorizationTuple.asRLPList().elements();
136+
if (tupleElements.size() != 6) {
137+
throw new IllegalArgumentException("invalid authorization list entry: must have 6 elements");
138+
}
139+
authorizationList.add(new AuthorizationTuple(
140+
tupleElements.get(0).data(),
141+
tupleElements.get(1).data(),
142+
tupleElements.get(2).data(),
143+
tupleElements.get(3).data(),
144+
tupleElements.get(4).data(),
145+
tupleElements.get(5).data()));
146+
}
147+
148+
var headerData = new HeaderData(
149+
rlpList.get(0).data(),
150+
rlpList.get(1).data(),
151+
rlpList.get(2).data(),
152+
rlpList.get(3).data(),
153+
rlpList.get(4).data(),
154+
rlpList.get(5).data(),
155+
rlpList.get(6).data());
156+
157+
var signatureData = new SignatureData(
158+
rlpList.get(10).data(), rlpList.get(11).data(), rlpList.get(12).data());
159+
160+
return new EthereumTransactionDataEip7702(
161+
headerData, rlpList.get(7).data(), accessList, authorizationList, signatureData);
162+
}
163+
164+
public byte[] toBytes() {
165+
List<Object> encodedAuthorizationList = new ArrayList<>();
166+
for (var tuple : authorizationList) {
167+
encodedAuthorizationList.add(
168+
List.of(tuple.chainId, tuple.address, tuple.nonce, tuple.yParity, tuple.r, tuple.s));
169+
}
170+
171+
List<Object> encodedAccessList = new ArrayList<>(accessList);
172+
173+
return RLPEncoder.sequence(
174+
Integers.toBytes(0x04),
175+
List.of(
176+
chainId,
177+
nonce,
178+
maxPriorityGas,
179+
maxGas,
180+
gasLimit,
181+
to,
182+
value,
183+
callData,
184+
encodedAccessList,
185+
encodedAuthorizationList,
186+
recoveryId,
187+
r,
188+
s));
189+
}
190+
191+
public String toString() {
192+
return MoreObjects.toStringHelper(this)
193+
.add("chainId", Hex.toHexString(chainId))
194+
.add("nonce", Hex.toHexString(nonce))
195+
.add("maxPriorityGas", Hex.toHexString(maxPriorityGas))
196+
.add("maxGas", Hex.toHexString(maxGas))
197+
.add("gasLimit", Hex.toHexString(gasLimit))
198+
.add("to", Hex.toHexString(to))
199+
.add("value", Hex.toHexString(value))
200+
.add("callData", Hex.toHexString(callData))
201+
.add("accessList", accessList.stream().map(Hex::toHexString).collect(Collectors.toList()))
202+
.add(
203+
"authorizationList",
204+
authorizationList.stream()
205+
.map(AuthorizationTuple::toString)
206+
.collect(Collectors.toList()))
207+
.add("recoveryId", Hex.toHexString(recoveryId))
208+
.add("r", Hex.toHexString(r))
209+
.add("s", Hex.toHexString(s))
210+
.toString();
211+
}
212+
213+
/**
214+
* A helper class to hold core transaction fields for EIP-7702 transactions.
215+
*/
216+
static class HeaderData {
217+
public byte[] chainId;
218+
public byte[] nonce;
219+
public byte[] maxPriorityGas;
220+
public byte[] maxGas;
221+
public byte[] gasLimit;
222+
public byte[] to;
223+
public byte[] value;
224+
225+
public HeaderData(
226+
byte[] chainId,
227+
byte[] nonce,
228+
byte[] maxPriorityGas,
229+
byte[] maxGas,
230+
byte[] gasLimit,
231+
byte[] to,
232+
byte[] value) {
233+
this.chainId = chainId;
234+
this.nonce = nonce;
235+
this.maxPriorityGas = maxPriorityGas;
236+
this.maxGas = maxGas;
237+
this.gasLimit = gasLimit;
238+
this.to = to;
239+
this.value = value;
240+
}
241+
}
242+
243+
/**
244+
* A helper class to hold signature data for EIP-7702 transactions.
245+
*/
246+
static class SignatureData {
247+
public byte[] recoveryId;
248+
public byte[] r;
249+
public byte[] s;
250+
251+
public SignatureData(byte[] recoveryId, byte[] r, byte[] s) {
252+
this.recoveryId = recoveryId;
253+
this.r = r;
254+
this.s = s;
255+
}
256+
}
257+
258+
@Override
259+
public byte[] sign(PrivateKey privateKey) {
260+
EthereumTransactionData.SignatureData signature = signTypedTransaction(toUnsignedRlp(), 0x04, privateKey);
261+
this.r = signature.r;
262+
this.s = signature.s;
263+
this.recoveryId = EthereumEncoding.uint64ToEthBytes(signature.recoveryId);
264+
return toBytes();
265+
}
266+
267+
/**
268+
* RLP-encode the unsigned payload (9 fields through the access list).
269+
*/
270+
byte[] toUnsignedRlp() {
271+
return RLPEncoder.list(
272+
chainId, nonce, maxPriorityGas, maxGas, gasLimit, to, value, callData, accessList, authorizationList);
273+
}
274+
275+
/**
276+
* A tuple describing an authorization entry for EIP-7702 transactions.
277+
*/
278+
public static class AuthorizationTuple {
279+
public byte[] chainId;
280+
public byte[] address;
281+
public byte[] nonce;
282+
public byte[] yParity;
283+
public byte[] r;
284+
public byte[] s;
285+
286+
public AuthorizationTuple(byte[] chainId, byte[] address, byte[] nonce, byte[] yParity, byte[] r, byte[] s) {
287+
this.chainId = chainId;
288+
this.address = address;
289+
this.nonce = nonce;
290+
this.yParity = yParity;
291+
this.r = r;
292+
this.s = s;
293+
}
294+
295+
@Override
296+
public String toString() {
297+
return MoreObjects.toStringHelper(this)
298+
.add("chainId", Hex.toHexString(chainId))
299+
.add("address", Hex.toHexString(address))
300+
.add("nonce", Hex.toHexString(nonce))
301+
.add("yParity", Hex.toHexString(yParity))
302+
.add("r", Hex.toHexString(r))
303+
.add("s", Hex.toHexString(s))
304+
.toString();
305+
}
306+
}
307+
}

0 commit comments

Comments
 (0)