Skip to content

Commit 8b75605

Browse files
committed
Implement EIP-7702 (Type 4 transactions)
Signed-off-by: emiliyank <e.kadiyski@gmail.com>
1 parent ff1a4fd commit 8b75605

5 files changed

Lines changed: 693 additions & 81 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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ static EthereumTransactionData fromBytes(byte[] bytes) {
2424
if (rlpItem.isList()) {
2525
return EthereumTransactionDataLegacy.fromBytes(bytes);
2626
} else {
27-
return EthereumTransactionDataEip1559.fromBytes(bytes);
27+
var typeByte = rlpItem.asByte();
28+
29+
return switch (typeByte) {
30+
case 0x02 -> EthereumTransactionDataEip1559.fromBytes(bytes);
31+
case 0x04 -> EthereumTransactionDataEip7702.fromBytes(bytes);
32+
default -> throw new IllegalArgumentException("rlp type byte " + typeByte + "is not supported");
33+
};
2834
}
2935
}
3036

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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+
byte[] chainId,
84+
byte[] nonce,
85+
byte[] maxPriorityGas,
86+
byte[] maxGas,
87+
byte[] gasLimit,
88+
byte[] to,
89+
byte[] value,
90+
byte[] callData,
91+
List<byte[]> accessList,
92+
List<AuthorizationTuple> authorizationList,
93+
SignatureData signatureData) {
94+
super(callData);
95+
96+
this.chainId = chainId;
97+
this.nonce = nonce;
98+
this.maxPriorityGas = maxPriorityGas;
99+
this.maxGas = maxGas;
100+
this.gasLimit = gasLimit;
101+
this.to = to;
102+
this.value = value;
103+
this.accessList = accessList;
104+
this.authorizationList = authorizationList;
105+
this.recoveryId = signatureData.recoveryId;
106+
this.r = signatureData.r;
107+
this.s = signatureData.s;
108+
}
109+
110+
/**
111+
* Convert a byte array to an ethereum transaction data.
112+
*
113+
* @param bytes the byte array
114+
* @return the ethereum transaction data
115+
*/
116+
public static EthereumTransactionDataEip7702 fromBytes(byte[] bytes) {
117+
var decoder = RLPDecoder.RLP_STRICT.sequenceIterator(bytes);
118+
var rlpItem = decoder.next();
119+
120+
// typed transaction?
121+
byte typeByte = rlpItem.asByte();
122+
if (typeByte != 4) {
123+
throw new IllegalArgumentException("rlp type byte " + typeByte + " is not supported");
124+
}
125+
rlpItem = decoder.next();
126+
if (!rlpItem.isList()) {
127+
throw new IllegalArgumentException("expected RLP element list");
128+
}
129+
List<RLPItem> rlpList = rlpItem.asRLPList().elements();
130+
if (rlpList.size() != 13) {
131+
throw new IllegalArgumentException("expected 13 RLP encoded elements, found " + rlpList.size());
132+
}
133+
134+
var accessList = new ArrayList<byte[]>();
135+
for (var accessListItem : rlpList.get(8).asRLPList().elements()) {
136+
accessList.add(accessListItem.data());
137+
}
138+
139+
var authorizationList = new ArrayList<AuthorizationTuple>();
140+
for (var authorizationTuple : rlpList.get(9).asRLPList().elements()) {
141+
var tupleElements = authorizationTuple.asRLPList().elements();
142+
if (tupleElements.size() != 6) {
143+
throw new IllegalArgumentException("invalid authorization list entry: must have 6 elements");
144+
}
145+
authorizationList.add(new AuthorizationTuple(
146+
tupleElements.get(0).data(),
147+
tupleElements.get(1).data(),
148+
tupleElements.get(2).data(),
149+
tupleElements.get(3).data(),
150+
tupleElements.get(4).data(),
151+
tupleElements.get(5).data()));
152+
}
153+
154+
return new EthereumTransactionDataEip7702(
155+
rlpList.get(0).data(),
156+
rlpList.get(1).data(),
157+
rlpList.get(2).data(),
158+
rlpList.get(3).data(),
159+
rlpList.get(4).data(),
160+
rlpList.get(5).data(),
161+
rlpList.get(6).data(),
162+
rlpList.get(7).data(),
163+
accessList,
164+
authorizationList,
165+
new SignatureData(
166+
rlpList.get(10).data(),
167+
rlpList.get(11).data(),
168+
rlpList.get(12).data()));
169+
}
170+
171+
public byte[] toBytes() {
172+
List<Object> encodedAuthorizationList = new ArrayList<>();
173+
for (var tuple : authorizationList) {
174+
encodedAuthorizationList.add(
175+
List.of(tuple.chainId, tuple.address, tuple.nonce, tuple.yParity, tuple.r, tuple.s));
176+
}
177+
178+
List<Object> encodedAccessList = new ArrayList<>(accessList);
179+
180+
return RLPEncoder.sequence(
181+
Integers.toBytes(0x04),
182+
List.of(
183+
chainId,
184+
nonce,
185+
maxPriorityGas,
186+
maxGas,
187+
gasLimit,
188+
to,
189+
value,
190+
callData,
191+
encodedAccessList,
192+
encodedAuthorizationList,
193+
recoveryId,
194+
r,
195+
s));
196+
}
197+
198+
public String toString() {
199+
return MoreObjects.toStringHelper(this)
200+
.add("chainId", Hex.toHexString(chainId))
201+
.add("nonce", Hex.toHexString(nonce))
202+
.add("maxPriorityGas", Hex.toHexString(maxPriorityGas))
203+
.add("maxGas", Hex.toHexString(maxGas))
204+
.add("gasLimit", Hex.toHexString(gasLimit))
205+
.add("to", Hex.toHexString(to))
206+
.add("value", Hex.toHexString(value))
207+
.add("callData", Hex.toHexString(callData))
208+
.add("accessList", accessList.stream().map(Hex::toHexString).collect(Collectors.toList()))
209+
.add(
210+
"authorizationList",
211+
authorizationList.stream()
212+
.map(AuthorizationTuple::toString)
213+
.collect(Collectors.toList()))
214+
.add("recoveryId", Hex.toHexString(recoveryId))
215+
.add("r", Hex.toHexString(r))
216+
.add("s", Hex.toHexString(s))
217+
.toString();
218+
}
219+
220+
/**
221+
* A helper class to hold signature data for EIP-7702 transactions.
222+
*/
223+
static class SignatureData {
224+
public byte[] recoveryId;
225+
public byte[] r;
226+
public byte[] s;
227+
228+
public SignatureData(byte[] recoveryId, byte[] r, byte[] s) {
229+
this.recoveryId = recoveryId;
230+
this.r = r;
231+
this.s = s;
232+
}
233+
}
234+
235+
/**
236+
* A tuple describing an authorization entry for EIP-7702 transactions.
237+
*/
238+
public static class AuthorizationTuple {
239+
public byte[] chainId;
240+
public byte[] address;
241+
public byte[] nonce;
242+
public byte[] yParity;
243+
public byte[] r;
244+
public byte[] s;
245+
246+
public AuthorizationTuple(byte[] chainId, byte[] address, byte[] nonce, byte[] yParity, byte[] r, byte[] s) {
247+
this.chainId = chainId;
248+
this.address = address;
249+
this.nonce = nonce;
250+
this.yParity = yParity;
251+
this.r = r;
252+
this.s = s;
253+
}
254+
255+
@Override
256+
public String toString() {
257+
return MoreObjects.toStringHelper(this)
258+
.add("chainId", Hex.toHexString(chainId))
259+
.add("address", Hex.toHexString(address))
260+
.add("nonce", Hex.toHexString(nonce))
261+
.add("yParity", Hex.toHexString(yParity))
262+
.add("r", Hex.toHexString(r))
263+
.add("s", Hex.toHexString(s))
264+
.toString();
265+
}
266+
}
267+
}

0 commit comments

Comments
 (0)