Skip to content

Commit 853a2f1

Browse files
committed
Add eip-2930 transaction decoding
1 parent bbecec8 commit 853a2f1

File tree

7 files changed

+378
-2
lines changed

7 files changed

+378
-2
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2021 Web3 Labs Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package org.web3j.crypto;
14+
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
public class AccessListObject {
19+
private String address;
20+
private List<String> storageKeys;
21+
22+
public AccessListObject() {}
23+
24+
public AccessListObject(String address, List<String> storageKeys) {
25+
this.address = address;
26+
this.storageKeys = storageKeys;
27+
}
28+
29+
public String getAddress() {
30+
return address;
31+
}
32+
33+
public void setAddress(String address) {
34+
this.address = address;
35+
}
36+
37+
public List<String> getStorageKeys() {
38+
return storageKeys;
39+
}
40+
41+
public void setStorageKeys(List<String> storageKeys) {
42+
this.storageKeys = storageKeys;
43+
}
44+
45+
@Override
46+
public boolean equals(Object o) {
47+
if (this == o) return true;
48+
if (o == null || getClass() != o.getClass()) return false;
49+
AccessListObject that = (AccessListObject) o;
50+
return Objects.equals(getAddress(), that.getAddress())
51+
&& Objects.equals(getStorageKeys(), that.getStorageKeys());
52+
}
53+
54+
@Override
55+
public int hashCode() {
56+
return Objects.hash(getAddress(), getStorageKeys());
57+
}
58+
}

crypto/src/main/java/org/web3j/crypto/RawTransaction.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
package org.web3j.crypto;
1414

1515
import java.math.BigInteger;
16+
import java.util.List;
1617

1718
import org.web3j.crypto.transaction.type.ITransaction;
1819
import org.web3j.crypto.transaction.type.LegacyTransaction;
1920
import org.web3j.crypto.transaction.type.Transaction1559;
21+
import org.web3j.crypto.transaction.type.Transaction2930;
2022
import org.web3j.crypto.transaction.type.TransactionType;
2123

2224
/**
@@ -116,6 +118,20 @@ public static RawTransaction createTransaction(
116118
maxFeePerGas));
117119
}
118120

121+
public static RawTransaction createTransaction(
122+
long chainId,
123+
BigInteger nonce,
124+
BigInteger gasPrice,
125+
BigInteger gasLimit,
126+
String to,
127+
BigInteger value,
128+
String data,
129+
List<AccessListObject> accessList) {
130+
return new RawTransaction(
131+
Transaction2930.createTransaction(
132+
chainId, nonce, gasPrice, gasLimit, to, value, data, accessList));
133+
}
134+
119135
public BigInteger getNonce() {
120136
return transaction.getNonce();
121137
}

crypto/src/main/java/org/web3j/crypto/TransactionDecoder.java

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,27 @@
1313
package org.web3j.crypto;
1414

1515
import java.math.BigInteger;
16+
import java.util.ArrayList;
1617
import java.util.Arrays;
18+
import java.util.List;
1719

1820
import org.web3j.crypto.transaction.type.TransactionType;
1921
import org.web3j.rlp.RlpDecoder;
2022
import org.web3j.rlp.RlpList;
2123
import org.web3j.rlp.RlpString;
24+
import org.web3j.rlp.RlpType;
2225
import org.web3j.utils.Numeric;
2326

2427
public class TransactionDecoder {
2528
private static final int UNSIGNED_EIP1559TX_RLP_LIST_SIZE = 9;
29+
private static final int UNSIGNED_EIP2930TX_RLP_LIST_SIZE = 8;
2630

2731
public static RawTransaction decode(final String hexTransaction) {
2832
final byte[] transaction = Numeric.hexStringToByteArray(hexTransaction);
2933
if (getTransactionType(transaction) == TransactionType.EIP1559) {
3034
return decodeEIP1559Transaction(transaction);
35+
} else if (getTransactionType(transaction) == TransactionType.EIP2930) {
36+
return decodeEIP2930Transaction(transaction);
3137
}
3238
return decodeLegacyTransaction(transaction);
3339
}
@@ -36,7 +42,8 @@ private static TransactionType getTransactionType(final byte[] transaction) {
3642
// The first byte indicates a transaction type.
3743
byte firstByte = transaction[0];
3844
if (firstByte == TransactionType.EIP1559.getRlpType()) return TransactionType.EIP1559;
39-
return TransactionType.LEGACY;
45+
else if (firstByte == TransactionType.EIP2930.getRlpType()) return TransactionType.EIP2930;
46+
else return TransactionType.LEGACY;
4047
}
4148

4249
private static RawTransaction decodeEIP1559Transaction(final byte[] transaction) {
@@ -120,4 +127,65 @@ private static RawTransaction decodeLegacyTransaction(final byte[] transaction)
120127
nonce, gasPrice, gasLimit, to, value, data, signatureData);
121128
}
122129
}
130+
131+
private static RawTransaction decodeEIP2930Transaction(final byte[] transaction) {
132+
final byte[] encodedTx = Arrays.copyOfRange(transaction, 1, transaction.length);
133+
final RlpList rlpList = RlpDecoder.decode(encodedTx);
134+
final RlpList values = (RlpList) rlpList.getValues().get(0);
135+
136+
final long chainId =
137+
((RlpString) values.getValues().get(0)).asPositiveBigInteger().longValue();
138+
final BigInteger nonce = ((RlpString) values.getValues().get(1)).asPositiveBigInteger();
139+
final BigInteger gasPrice = ((RlpString) values.getValues().get(2)).asPositiveBigInteger();
140+
final BigInteger gasLimit = ((RlpString) values.getValues().get(3)).asPositiveBigInteger();
141+
final String to = ((RlpString) values.getValues().get(4)).asString();
142+
final BigInteger value = ((RlpString) values.getValues().get(5)).asPositiveBigInteger();
143+
final String data = ((RlpString) values.getValues().get(6)).asString();
144+
List<AccessListObject> accessList =
145+
decodeAccessList(((RlpList) values.getValues().get(7)).getValues());
146+
147+
final RawTransaction rawTransaction =
148+
RawTransaction.createTransaction(
149+
chainId, nonce, gasPrice, gasLimit, to, value, data, accessList);
150+
151+
if (values.getValues().size() == UNSIGNED_EIP2930TX_RLP_LIST_SIZE) {
152+
return rawTransaction;
153+
} else {
154+
final byte[] v =
155+
Sign.getVFromRecId(
156+
Numeric.toBigInt(((RlpString) values.getValues().get(8)).getBytes())
157+
.intValue());
158+
final byte[] r =
159+
Numeric.toBytesPadded(
160+
Numeric.toBigInt(((RlpString) values.getValues().get(9)).getBytes()),
161+
32);
162+
final byte[] s =
163+
Numeric.toBytesPadded(
164+
Numeric.toBigInt(((RlpString) values.getValues().get(10)).getBytes()),
165+
32);
166+
final Sign.SignatureData signatureData = new Sign.SignatureData(v, r, s);
167+
return new SignedRawTransaction(rawTransaction.getTransaction(), signatureData);
168+
}
169+
}
170+
171+
private static List<AccessListObject> decodeAccessList(List<RlpType> rlp) {
172+
final List<AccessListObject> res = new ArrayList<AccessListObject>();
173+
rlp.forEach(
174+
rawEntry -> {
175+
AccessListObject entry = new AccessListObject();
176+
List<RlpType> values = ((RlpList) rawEntry).getValues();
177+
178+
entry.setAddress(((RlpString) values.get(0)).asString());
179+
List<RlpType> keyList = ((RlpList) values.get(1)).getValues();
180+
181+
List<String> keys = new ArrayList<>();
182+
keyList.forEach(
183+
rawKey -> {
184+
keys.add(((RlpString) rawKey).asString());
185+
});
186+
entry.setStorageKeys(keys);
187+
res.add(entry);
188+
});
189+
return res;
190+
}
123191
}

crypto/src/main/java/org/web3j/crypto/TransactionEncoder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData si
117117
RlpList rlpList = new RlpList(values);
118118
byte[] encoded = RlpEncoder.encode(rlpList);
119119

120-
if (rawTransaction.getType().isEip1559()) {
120+
if (rawTransaction.getType().isEip1559() || rawTransaction.getType().isEip2930()) {
121121
return ByteBuffer.allocate(encoded.length + 1)
122122
.put(rawTransaction.getType().getRlpType())
123123
.put(encoded)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2022 Web3 Labs Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package org.web3j.crypto.transaction.type;
14+
15+
import java.math.BigInteger;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
import org.web3j.crypto.AccessListObject;
20+
import org.web3j.crypto.Sign;
21+
import org.web3j.rlp.RlpList;
22+
import org.web3j.rlp.RlpString;
23+
import org.web3j.rlp.RlpType;
24+
import org.web3j.utils.Bytes;
25+
import org.web3j.utils.Numeric;
26+
27+
import static org.web3j.crypto.transaction.type.TransactionType.EIP2930;
28+
29+
public class Transaction2930 extends LegacyTransaction {
30+
private long chainId;
31+
private List<AccessListObject> accessList;
32+
33+
public Transaction2930(
34+
long chainId,
35+
BigInteger nonce,
36+
BigInteger gasPrice,
37+
BigInteger gasLimit,
38+
String to,
39+
BigInteger value,
40+
String data,
41+
List<AccessListObject> accessList) {
42+
super(EIP2930, nonce, gasPrice, gasLimit, to, value, data);
43+
this.chainId = chainId;
44+
this.accessList = accessList;
45+
}
46+
47+
@Override
48+
public List<RlpType> asRlpValues(Sign.SignatureData signatureData) {
49+
List<RlpType> result = new ArrayList<>();
50+
51+
result.add(RlpString.create(getChainId()));
52+
result.add(RlpString.create(getNonce()));
53+
result.add(RlpString.create(getGasPrice()));
54+
result.add(RlpString.create(getGasLimit()));
55+
56+
// an empty to address (contract creation) should not be encoded as a numeric 0 value
57+
String to = getTo();
58+
if (to != null && to.length() > 0) {
59+
// addresses that start with zeros should be encoded with the zeros included, not
60+
// as numeric values
61+
result.add(RlpString.create(Numeric.hexStringToByteArray(to)));
62+
} else {
63+
result.add(RlpString.create(""));
64+
}
65+
66+
result.add(RlpString.create(getValue()));
67+
68+
// value field will already be hex encoded, so we need to convert into binary first
69+
byte[] data = Numeric.hexStringToByteArray(getData());
70+
result.add(RlpString.create(data));
71+
72+
// access list
73+
List<AccessListObject> accessList = getAccessList();
74+
List<RlpType> rlpAccessList = new ArrayList<>();
75+
accessList.forEach(
76+
entry -> {
77+
List<RlpType> rlpAccessListObject = new ArrayList<>();
78+
rlpAccessListObject.add(
79+
RlpString.create(Numeric.hexStringToByteArray(entry.getAddress())));
80+
List<RlpType> keyList = new ArrayList<>();
81+
entry.getStorageKeys()
82+
.forEach(
83+
key -> {
84+
keyList.add(
85+
RlpString.create(
86+
Numeric.hexStringToByteArray(key)));
87+
});
88+
rlpAccessListObject.add(new RlpList(keyList));
89+
rlpAccessList.add(new RlpList(rlpAccessListObject));
90+
});
91+
result.add(new RlpList(rlpAccessList));
92+
93+
if (signatureData != null) {
94+
result.add(RlpString.create(Sign.getRecId(signatureData, getChainId())));
95+
result.add(RlpString.create(Bytes.trimLeadingZeroes(signatureData.getR())));
96+
result.add(RlpString.create(Bytes.trimLeadingZeroes(signatureData.getS())));
97+
}
98+
99+
return result;
100+
}
101+
102+
public static Transaction2930 createEtherTransaction(
103+
long chainId,
104+
BigInteger nonce,
105+
BigInteger gasPrice,
106+
BigInteger gasLimit,
107+
String to,
108+
BigInteger value) {
109+
return new Transaction2930(chainId, nonce, gasPrice, gasLimit, to, value, "", List.of());
110+
}
111+
112+
public static Transaction2930 createTransaction(
113+
long chainId,
114+
BigInteger nonce,
115+
BigInteger gasPrice,
116+
BigInteger gasLimit,
117+
String to,
118+
BigInteger value,
119+
String data,
120+
List<AccessListObject> accessList) {
121+
return new Transaction2930(chainId, nonce, gasPrice, gasLimit, to, value, data, accessList);
122+
}
123+
124+
public long getChainId() {
125+
return chainId;
126+
}
127+
128+
public List<AccessListObject> getAccessList() {
129+
return accessList;
130+
}
131+
}

crypto/src/main/java/org/web3j/crypto/transaction/type/TransactionType.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
public enum TransactionType {
1616
LEGACY(null),
17+
EIP2930(((byte) 0x01)),
1718
EIP1559(((byte) 0x02));
1819

1920
Byte type;
@@ -33,4 +34,8 @@ public boolean isLegacy() {
3334
public boolean isEip1559() {
3435
return this.equals(TransactionType.EIP1559);
3536
}
37+
38+
public boolean isEip2930() {
39+
return this.equals(TransactionType.EIP2930);
40+
}
3641
}

0 commit comments

Comments
 (0)