Skip to content

Commit b15e56a

Browse files
dancoombsandrii-kl
andauthored
Add eip-2930 transaction decoding (#1807)
* Add eip-2930 transaction decoding * Fix build for java 8 * Use streams over foreach Co-authored-by: Andrii <[email protected]>
1 parent 1e5aedb commit b15e56a

File tree

7 files changed

+370
-2
lines changed

7 files changed

+370
-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: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,27 @@
1414

1515
import java.math.BigInteger;
1616
import java.util.Arrays;
17+
import java.util.List;
1718

1819
import org.web3j.crypto.transaction.type.TransactionType;
1920
import org.web3j.rlp.RlpDecoder;
2021
import org.web3j.rlp.RlpList;
2122
import org.web3j.rlp.RlpString;
23+
import org.web3j.rlp.RlpType;
2224
import org.web3j.utils.Numeric;
2325

26+
import static java.util.stream.Collectors.toList;
27+
2428
public class TransactionDecoder {
2529
private static final int UNSIGNED_EIP1559TX_RLP_LIST_SIZE = 9;
30+
private static final int UNSIGNED_EIP2930TX_RLP_LIST_SIZE = 8;
2631

2732
public static RawTransaction decode(final String hexTransaction) {
2833
final byte[] transaction = Numeric.hexStringToByteArray(hexTransaction);
2934
if (getTransactionType(transaction) == TransactionType.EIP1559) {
3035
return decodeEIP1559Transaction(transaction);
36+
} else if (getTransactionType(transaction) == TransactionType.EIP2930) {
37+
return decodeEIP2930Transaction(transaction);
3138
}
3239
return decodeLegacyTransaction(transaction);
3340
}
@@ -36,7 +43,8 @@ private static TransactionType getTransactionType(final byte[] transaction) {
3643
// The first byte indicates a transaction type.
3744
byte firstByte = transaction[0];
3845
if (firstByte == TransactionType.EIP1559.getRlpType()) return TransactionType.EIP1559;
39-
return TransactionType.LEGACY;
46+
else if (firstByte == TransactionType.EIP2930.getRlpType()) return TransactionType.EIP2930;
47+
else return TransactionType.LEGACY;
4048
}
4149

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

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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.Collections;
18+
import java.util.List;
19+
20+
import org.web3j.crypto.AccessListObject;
21+
import org.web3j.crypto.Sign;
22+
import org.web3j.rlp.RlpList;
23+
import org.web3j.rlp.RlpString;
24+
import org.web3j.rlp.RlpType;
25+
import org.web3j.utils.Bytes;
26+
import org.web3j.utils.Numeric;
27+
28+
import static org.web3j.crypto.transaction.type.TransactionType.EIP2930;
29+
30+
public class Transaction2930 extends LegacyTransaction {
31+
private long chainId;
32+
private List<AccessListObject> accessList;
33+
34+
public Transaction2930(
35+
long chainId,
36+
BigInteger nonce,
37+
BigInteger gasPrice,
38+
BigInteger gasLimit,
39+
String to,
40+
BigInteger value,
41+
String data,
42+
List<AccessListObject> accessList) {
43+
super(EIP2930, nonce, gasPrice, gasLimit, to, value, data);
44+
this.chainId = chainId;
45+
this.accessList = accessList;
46+
}
47+
48+
@Override
49+
public List<RlpType> asRlpValues(Sign.SignatureData signatureData) {
50+
List<RlpType> result = new ArrayList<>();
51+
52+
result.add(RlpString.create(getChainId()));
53+
result.add(RlpString.create(getNonce()));
54+
result.add(RlpString.create(getGasPrice()));
55+
result.add(RlpString.create(getGasLimit()));
56+
57+
// an empty to address (contract creation) should not be encoded as a numeric 0 value
58+
String to = getTo();
59+
if (to != null && to.length() > 0) {
60+
// addresses that start with zeros should be encoded with the zeros included, not
61+
// as numeric values
62+
result.add(RlpString.create(Numeric.hexStringToByteArray(to)));
63+
} else {
64+
result.add(RlpString.create(""));
65+
}
66+
67+
result.add(RlpString.create(getValue()));
68+
69+
// value field will already be hex encoded, so we need to convert into binary first
70+
byte[] data = Numeric.hexStringToByteArray(getData());
71+
result.add(RlpString.create(data));
72+
73+
// access list
74+
List<AccessListObject> accessList = getAccessList();
75+
List<RlpType> rlpAccessList = new ArrayList<>();
76+
accessList.forEach(
77+
entry -> {
78+
List<RlpType> rlpAccessListObject = new ArrayList<>();
79+
rlpAccessListObject.add(
80+
RlpString.create(Numeric.hexStringToByteArray(entry.getAddress())));
81+
List<RlpType> keyList = new ArrayList<>();
82+
entry.getStorageKeys()
83+
.forEach(
84+
key -> {
85+
keyList.add(
86+
RlpString.create(
87+
Numeric.hexStringToByteArray(key)));
88+
});
89+
rlpAccessListObject.add(new RlpList(keyList));
90+
rlpAccessList.add(new RlpList(rlpAccessListObject));
91+
});
92+
result.add(new RlpList(rlpAccessList));
93+
94+
if (signatureData != null) {
95+
result.add(RlpString.create(Sign.getRecId(signatureData, getChainId())));
96+
result.add(RlpString.create(Bytes.trimLeadingZeroes(signatureData.getR())));
97+
result.add(RlpString.create(Bytes.trimLeadingZeroes(signatureData.getS())));
98+
}
99+
100+
return result;
101+
}
102+
103+
public static Transaction2930 createEtherTransaction(
104+
long chainId,
105+
BigInteger nonce,
106+
BigInteger gasPrice,
107+
BigInteger gasLimit,
108+
String to,
109+
BigInteger value) {
110+
return new Transaction2930(
111+
chainId, nonce, gasPrice, gasLimit, to, value, "", Collections.emptyList());
112+
}
113+
114+
public static Transaction2930 createTransaction(
115+
long chainId,
116+
BigInteger nonce,
117+
BigInteger gasPrice,
118+
BigInteger gasLimit,
119+
String to,
120+
BigInteger value,
121+
String data,
122+
List<AccessListObject> accessList) {
123+
return new Transaction2930(chainId, nonce, gasPrice, gasLimit, to, value, data, accessList);
124+
}
125+
126+
public long getChainId() {
127+
return chainId;
128+
}
129+
130+
public List<AccessListObject> getAccessList() {
131+
return accessList;
132+
}
133+
}

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)