Skip to content

Commit e5db881

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

5 files changed

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

0 commit comments

Comments
 (0)