Skip to content
This repository was archived by the owner on Jul 5, 2024. It is now read-only.

Commit 82e8d8f

Browse files
authoredApr 3, 2024··
[feat] support EIP 1559/2930 in testool (#1762)
### Description This pr enables support of EIP 1559/2930 in testool ### Issue Link [_link issue here_] ### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update - [ ] Refactor (no updates to logic)
1 parent 55754a9 commit 82e8d8f

File tree

11 files changed

+695
-233
lines changed

11 files changed

+695
-233
lines changed
 

‎eth-types/src/geth_types.rs

+142-10
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ use crate::{
55
keccak256,
66
sign_types::{biguint_to_32bytes_le, ct_option_ok_or, recover_pk, SignData, SECP256K1_Q},
77
AccessList, Address, Block, Bytecode, Bytes, Error, GethExecTrace, Hash, ToBigEndian,
8-
ToLittleEndian, ToWord, Word, U64,
8+
ToLittleEndian, ToWord, Word, H256, U64,
99
};
1010
use ethers_core::{
11-
types::{transaction::response, NameOrAddress, TransactionRequest},
11+
types::{
12+
transaction::{eip2718::TypedTransaction, response},
13+
Eip1559TransactionRequest, Eip2930TransactionRequest, NameOrAddress, TransactionRequest,
14+
},
1215
utils::get_contract_address,
1316
};
1417
use ethers_signers::{LocalWallet, Signer};
@@ -18,6 +21,117 @@ use num_bigint::BigUint;
1821
use serde::{Serialize, Serializer};
1922
use serde_with::serde_as;
2023
use std::collections::HashMap;
24+
use strum_macros::EnumIter;
25+
26+
/// Tx type
27+
#[derive(Default, Debug, Copy, Clone, EnumIter, Serialize, PartialEq, Eq)]
28+
pub enum TxType {
29+
/// EIP 155 tx
30+
#[default]
31+
Eip155 = 0,
32+
/// Pre EIP 155 tx
33+
PreEip155,
34+
/// EIP 1559 tx
35+
Eip1559,
36+
/// EIP 2930 tx
37+
Eip2930,
38+
}
39+
40+
impl From<TxType> for usize {
41+
fn from(value: TxType) -> Self {
42+
value as usize
43+
}
44+
}
45+
46+
impl From<TxType> for u64 {
47+
fn from(value: TxType) -> Self {
48+
value as u64
49+
}
50+
}
51+
52+
impl TxType {
53+
/// If this type is PreEip155
54+
pub fn is_pre_eip155(&self) -> bool {
55+
matches!(*self, TxType::PreEip155)
56+
}
57+
58+
/// If this type is EIP155 or not
59+
pub fn is_eip155(&self) -> bool {
60+
matches!(*self, TxType::Eip155)
61+
}
62+
63+
/// If this type is Eip1559 or not
64+
pub fn is_eip1559(&self) -> bool {
65+
matches!(*self, TxType::Eip1559)
66+
}
67+
68+
/// If this type is Eip2930 or not
69+
pub fn is_eip2930(&self) -> bool {
70+
matches!(*self, TxType::Eip2930)
71+
}
72+
73+
/// Get the type of transaction
74+
pub fn get_tx_type(tx: &crate::Transaction) -> Self {
75+
match tx.transaction_type {
76+
Some(x) if x == U64::from(1) => Self::Eip2930,
77+
Some(x) if x == U64::from(2) => Self::Eip1559,
78+
_ => match tx.v.as_u64() {
79+
0 | 1 | 27 | 28 => Self::PreEip155,
80+
_ => Self::Eip155,
81+
},
82+
}
83+
}
84+
85+
/// Return the recovery id of signature for recovering the signing pk
86+
pub fn get_recovery_id(&self, v: u64) -> u8 {
87+
let recovery_id = match *self {
88+
TxType::Eip155 => (v + 1) % 2,
89+
TxType::PreEip155 => {
90+
assert!(v == 0x1b || v == 0x1c, "v: {v}");
91+
v - 27
92+
}
93+
TxType::Eip1559 => {
94+
assert!(v <= 1);
95+
v
96+
}
97+
TxType::Eip2930 => {
98+
assert!(v <= 1);
99+
v
100+
}
101+
};
102+
103+
recovery_id as u8
104+
}
105+
}
106+
107+
/// Get the RLP bytes for signing
108+
pub fn get_rlp_unsigned(tx: &crate::Transaction) -> Vec<u8> {
109+
let sig_v = tx.v;
110+
match TxType::get_tx_type(tx) {
111+
TxType::Eip155 => {
112+
let mut tx: TransactionRequest = tx.into();
113+
tx.chain_id = Some(tx.chain_id.unwrap_or_else(|| {
114+
let recv_v = TxType::Eip155.get_recovery_id(sig_v.as_u64()) as u64;
115+
(sig_v - recv_v - 35) / 2
116+
}));
117+
tx.rlp().to_vec()
118+
}
119+
TxType::PreEip155 => {
120+
let tx: TransactionRequest = tx.into();
121+
tx.rlp_unsigned().to_vec()
122+
}
123+
TxType::Eip1559 => {
124+
let tx: Eip1559TransactionRequest = tx.into();
125+
let typed_tx: TypedTransaction = tx.into();
126+
typed_tx.rlp().to_vec()
127+
}
128+
TxType::Eip2930 => {
129+
let tx: Eip2930TransactionRequest = tx.into();
130+
let typed_tx: TypedTransaction = tx.into();
131+
typed_tx.rlp().to_vec()
132+
}
133+
}
134+
}
21135

22136
/// Definition of all of the data related to an account.
23137
#[serde_as]
@@ -156,6 +270,8 @@ pub struct Withdrawal {
156270
/// Definition of all of the constants related to an Ethereum transaction.
157271
#[derive(Debug, Default, Clone, Serialize)]
158272
pub struct Transaction {
273+
/// Tx type
274+
pub tx_type: TxType,
159275
/// Sender address
160276
pub from: Address,
161277
/// Recipient address (None for contract creation)
@@ -172,9 +288,9 @@ pub struct Transaction {
172288
/// Gas Price
173289
pub gas_price: Word,
174290
/// Gas fee cap
175-
pub gas_fee_cap: Word,
291+
pub gas_fee_cap: Option<Word>,
176292
/// Gas tip cap
177-
pub gas_tip_cap: Word,
293+
pub gas_tip_cap: Option<Word>,
178294
/// The compiled code of a contract OR the first 4 bytes of the hash of the
179295
/// invoked method signature and encoded parameters. For details see
180296
/// Ethereum Contract ABI
@@ -188,6 +304,14 @@ pub struct Transaction {
188304
pub r: Word,
189305
/// "s" value of the transaction signature
190306
pub s: Word,
307+
308+
/// RLP bytes
309+
pub rlp_bytes: Vec<u8>,
310+
/// RLP unsigned bytes
311+
pub rlp_unsigned_bytes: Vec<u8>,
312+
313+
/// Transaction hash
314+
pub hash: H256,
191315
}
192316

193317
impl From<&Transaction> for crate::Transaction {
@@ -199,8 +323,8 @@ impl From<&Transaction> for crate::Transaction {
199323
gas: tx.gas_limit.to_word(),
200324
value: tx.value,
201325
gas_price: Some(tx.gas_price),
202-
max_priority_fee_per_gas: Some(tx.gas_tip_cap),
203-
max_fee_per_gas: Some(tx.gas_fee_cap),
326+
max_priority_fee_per_gas: tx.gas_tip_cap,
327+
max_fee_per_gas: tx.gas_fee_cap,
204328
input: tx.call_data.clone(),
205329
access_list: tx.access_list.clone(),
206330
v: tx.v.into(),
@@ -214,19 +338,23 @@ impl From<&Transaction> for crate::Transaction {
214338
impl From<&crate::Transaction> for Transaction {
215339
fn from(tx: &crate::Transaction) -> Transaction {
216340
Transaction {
341+
tx_type: TxType::get_tx_type(tx),
217342
from: tx.from,
218343
to: tx.to,
219344
nonce: tx.nonce.as_u64().into(),
220345
gas_limit: tx.gas.as_u64().into(),
221346
value: tx.value,
222347
gas_price: tx.gas_price.unwrap_or_default(),
223-
gas_tip_cap: tx.max_priority_fee_per_gas.unwrap_or_default(),
224-
gas_fee_cap: tx.max_fee_per_gas.unwrap_or_default(),
348+
gas_tip_cap: tx.max_priority_fee_per_gas,
349+
gas_fee_cap: tx.max_fee_per_gas,
225350
call_data: tx.input.clone(),
226351
access_list: tx.access_list.clone(),
227352
v: tx.v.as_u64(),
228353
r: tx.r,
229354
s: tx.s,
355+
rlp_bytes: tx.rlp().to_vec(),
356+
rlp_unsigned_bytes: get_rlp_unsigned(tx),
357+
hash: tx.hash,
230358
}
231359
}
232360
}
@@ -256,13 +384,14 @@ impl Transaction {
256384
gas_limit: U64::zero(),
257385
value: Word::zero(),
258386
gas_price: Word::zero(),
259-
gas_tip_cap: Word::zero(),
260-
gas_fee_cap: Word::zero(),
387+
gas_tip_cap: Some(Word::zero()),
388+
gas_fee_cap: Some(Word::zero()),
261389
call_data: Bytes::new(),
262390
access_list: None,
263391
v: 0,
264392
r: Word::zero(),
265393
s: Word::zero(),
394+
..Default::default()
266395
}
267396
}
268397
/// Return the SignData associated with this Transaction.
@@ -355,6 +484,9 @@ impl Transaction {
355484
s: self.s,
356485
v: U64::from(self.v),
357486
block_number: Some(block_number),
487+
transaction_type: Some(U64::from(self.tx_type as u64)),
488+
max_priority_fee_per_gas: self.gas_tip_cap,
489+
max_fee_per_gas: self.gas_fee_cap,
358490
chain_id: Some(chain_id),
359491
..response::Transaction::default()
360492
}

‎eth-types/src/lib.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ use ethers_core::types;
4141
pub use ethers_core::{
4242
abi::ethereum_types::{BigEndianHash, U512},
4343
types::{
44-
transaction::{eip2930::AccessList, response::Transaction},
44+
transaction::{
45+
eip2930::{AccessList, AccessListItem},
46+
response::Transaction,
47+
},
4548
Address, Block, Bytes, Signature, H160, H256, H64, U256, U64,
4649
},
4750
};

‎external-tracer/src/lib.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use eth_types::{
55
Address, Error, GethExecTrace, Word,
66
};
77
use serde::Serialize;
8-
use std::collections::HashMap;
8+
use std::collections::BTreeMap;
99

1010
/// Configuration structure for `geth_utils::trace`
1111
#[derive(Debug, Default, Clone, Serialize)]
@@ -18,7 +18,7 @@ pub struct TraceConfig {
1818
/// block constants
1919
pub block_constants: BlockConstants,
2020
/// accounts
21-
pub accounts: HashMap<Address, Account>,
21+
pub accounts: BTreeMap<Address, Account>,
2222
/// transaction
2323
pub transactions: Vec<Transaction>,
2424
/// withdrawal
@@ -78,7 +78,8 @@ pub fn trace(config: &TraceConfig) -> Result<Vec<GethExecTrace>, Error> {
7878
let allowed_cases = error.starts_with("nonce too low")
7979
|| error.starts_with("nonce too high")
8080
|| error.starts_with("intrinsic gas too low")
81-
|| error.starts_with("insufficient funds for gas * price + value");
81+
|| error.starts_with("insufficient funds for gas * price + value")
82+
|| error.starts_with("insufficient funds for transfer");
8283
if trace.invalid && !allowed_cases {
8384
return Err(Error::TracingError(error.clone()));
8485
}

‎geth-utils/gethutil/trace.go

+21-16
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,20 @@ type Account struct {
9999
}
100100

101101
type Transaction struct {
102-
From common.Address `json:"from"`
103-
To *common.Address `json:"to"`
104-
Nonce hexutil.Uint64 `json:"nonce"`
105-
Value *hexutil.Big `json:"value"`
106-
GasLimit hexutil.Uint64 `json:"gas_limit"`
107-
GasPrice *hexutil.Big `json:"gas_price"`
108-
GasFeeCap *hexutil.Big `json:"gas_fee_cap"`
109-
GasTipCap *hexutil.Big `json:"gas_tip_cap"`
110-
CallData hexutil.Bytes `json:"call_data"`
111-
AccessList []struct {
112-
Address common.Address `json:"address"`
113-
StorageKeys []common.Hash `json:"storage_keys"`
114-
} `json:"access_list"`
102+
From common.Address `json:"from"`
103+
To *common.Address `json:"to"`
104+
Nonce hexutil.Uint64 `json:"nonce"`
105+
Value *hexutil.Big `json:"value"`
106+
GasLimit hexutil.Uint64 `json:"gas_limit"`
107+
GasPrice *hexutil.Big `json:"gas_price"`
108+
GasFeeCap *hexutil.Big `json:"gas_fee_cap"`
109+
GasTipCap *hexutil.Big `json:"gas_tip_cap"`
110+
CallData hexutil.Bytes `json:"call_data"`
111+
AccessList types.AccessList `json:"access_list"`
112+
Type string `json:"tx_type"`
113+
V int64 `json:"v"`
114+
R *hexutil.Big `json:"r"`
115+
S *hexutil.Big `json:"s"`
115116
}
116117

117118
type TraceConfig struct {
@@ -160,10 +161,14 @@ func Trace(config TraceConfig) ([]*ExecutionResult, error) {
160161
blockGasLimit := toBigInt(config.Block.GasLimit).Uint64()
161162
messages := make([]core.Message, len(config.Transactions))
162163
for i, tx := range config.Transactions {
163-
// If gas price is specified directly, the tx is treated as legacy type.
164164
if tx.GasPrice != nil {
165-
tx.GasFeeCap = tx.GasPrice
166-
tx.GasTipCap = tx.GasPrice
165+
// Set GasFeeCap and GasTipCap to GasPrice if not exist.
166+
if tx.GasFeeCap == nil {
167+
tx.GasFeeCap = tx.GasPrice
168+
}
169+
if tx.GasTipCap == nil {
170+
tx.GasTipCap = tx.GasPrice
171+
}
167172
}
168173

169174
txAccessList := make(types.AccessList, len(tx.AccessList))

‎mock/src/transaction.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ pub struct MockTransaction {
133133
pub s: Option<Word>,
134134
pub transaction_type: U64,
135135
pub access_list: AccessList,
136-
pub max_priority_fee_per_gas: Word,
137-
pub max_fee_per_gas: Word,
136+
pub max_priority_fee_per_gas: Option<Word>,
137+
pub max_fee_per_gas: Option<Word>,
138138
pub chain_id: Word,
139139
pub invalid: bool,
140140
}
@@ -158,8 +158,8 @@ impl Default for MockTransaction {
158158
s: None,
159159
transaction_type: U64::zero(),
160160
access_list: AccessList::default(),
161-
max_priority_fee_per_gas: Word::zero(),
162-
max_fee_per_gas: Word::zero(),
161+
max_priority_fee_per_gas: None,
162+
max_fee_per_gas: None,
163163
chain_id: *MOCK_CHAIN_ID,
164164
invalid: false,
165165
}
@@ -185,8 +185,8 @@ impl From<MockTransaction> for Transaction {
185185
s: mock.s.unwrap_or_default(),
186186
transaction_type: Some(mock.transaction_type),
187187
access_list: Some(mock.access_list),
188-
max_priority_fee_per_gas: Some(mock.max_priority_fee_per_gas),
189-
max_fee_per_gas: Some(mock.max_fee_per_gas),
188+
max_priority_fee_per_gas: mock.max_priority_fee_per_gas,
189+
max_fee_per_gas: mock.max_fee_per_gas,
190190
chain_id: Some(mock.chain_id),
191191
other: OtherFields::default(),
192192
}
@@ -289,13 +289,13 @@ impl MockTransaction {
289289

290290
/// Set max_priority_fee_per_gas field for the MockTransaction.
291291
pub fn max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: Word) -> &mut Self {
292-
self.max_priority_fee_per_gas = max_priority_fee_per_gas;
292+
self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
293293
self
294294
}
295295

296296
/// Set max_fee_per_gas field for the MockTransaction.
297297
pub fn max_fee_per_gas(&mut self, max_fee_per_gas: Word) -> &mut Self {
298-
self.max_fee_per_gas = max_fee_per_gas;
298+
self.max_fee_per_gas = Some(max_fee_per_gas);
299299
self
300300
}
301301

‎testool/src/statetest/executor.rs

+21-26
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
use super::{AccountMatch, StateTest, StateTestResult};
2-
use crate::config::TestSuite;
2+
use crate::{config::TestSuite, utils::ETH_CHAIN_ID};
33
use bus_mapping::{
44
circuit_input_builder::{CircuitInputBuilder, FixedCParams},
55
mock::BlockData,
66
};
77
use eth_types::{geth_types, Address, Bytes, Error, GethExecTrace, U256, U64};
8-
use ethers_core::{
9-
k256::ecdsa::SigningKey,
10-
types::{transaction::eip2718::TypedTransaction, TransactionRequest, Withdrawal},
11-
};
8+
use ethers_core::{k256::ecdsa::SigningKey, types::Withdrawal, utils::keccak256};
129
use ethers_signers::{LocalWallet, Signer};
1310
use external_tracer::TraceConfig;
1411
use halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr};
@@ -138,28 +135,22 @@ fn check_post(
138135
}
139136

140137
fn into_traceconfig(st: StateTest) -> (String, TraceConfig, StateTestResult) {
141-
let chain_id = 1;
142-
let wallet = LocalWallet::from_str(&hex::encode(st.secret_key.0)).unwrap();
143-
let mut tx = TransactionRequest::new()
144-
.chain_id(chain_id)
145-
.from(st.from)
146-
.nonce(st.nonce)
147-
.value(st.value)
148-
.data(st.data.clone())
149-
.gas(st.gas_limit)
150-
.gas_price(st.gas_price);
151-
152-
if let Some(to) = st.to {
153-
tx = tx.to(to);
154-
}
155-
let tx: TypedTransaction = tx.into();
138+
let tx_type = st.tx_type();
139+
let tx = st.build_tx();
140+
141+
let wallet = LocalWallet::from_str(&hex::encode(&st.secret_key.0)).unwrap();
156142

143+
let rlp_unsigned = tx.rlp().to_vec();
157144
let sig = wallet.sign_transaction_sync(&tx).unwrap();
145+
let v = st.normalize_sig_v(sig.v);
146+
let rlp_signed = tx.rlp_signed(&sig).to_vec();
147+
let tx_hash = keccak256(tx.rlp_signed(&sig));
148+
let accounts = st.pre;
158149

159150
(
160151
st.id,
161152
TraceConfig {
162-
chain_id: U256::one(),
153+
chain_id: U256::from(ETH_CHAIN_ID),
163154
history_hashes: vec![U256::from_big_endian(st.env.previous_hash.as_bytes())],
164155
block_constants: geth_types::BlockConstants {
165156
coinbase: st.env.current_coinbase,
@@ -171,21 +162,25 @@ fn into_traceconfig(st: StateTest) -> (String, TraceConfig, StateTestResult) {
171162
},
172163

173164
transactions: vec![geth_types::Transaction {
165+
tx_type,
174166
from: st.from,
175167
to: st.to,
176168
nonce: U64::from(st.nonce),
177169
value: st.value,
178170
gas_limit: U64::from(st.gas_limit),
179171
gas_price: st.gas_price,
180-
gas_fee_cap: U256::zero(),
181-
gas_tip_cap: U256::zero(),
172+
gas_fee_cap: st.max_fee_per_gas,
173+
gas_tip_cap: st.max_priority_fee_per_gas,
182174
call_data: st.data,
183-
access_list: None,
184-
v: sig.v,
175+
access_list: st.access_list,
176+
v,
185177
r: sig.r,
186178
s: sig.s,
179+
rlp_bytes: rlp_signed,
180+
rlp_unsigned_bytes: rlp_unsigned,
181+
hash: tx_hash.into(),
187182
}],
188-
accounts: st.pre.into_iter().collect(),
183+
accounts,
189184
..Default::default()
190185
},
191186
st.result,

‎testool/src/statetest/json.rs

+55-37
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ use ethers_core::{k256::ecdsa::SigningKey, utils::secret_key_to_address};
99
use serde::Deserialize;
1010
use std::collections::{BTreeMap, HashMap};
1111

12-
use serde_json::value::Value;
13-
1412
fn default_block_base_fee() -> String {
1513
DEFAULT_BASE_FEE.to_string()
1614
}
@@ -71,8 +69,11 @@ struct JsonStateTest {
7169
#[derive(Debug, Clone, Deserialize)]
7270
#[serde(rename_all = "camelCase")]
7371
struct Transaction {
72+
access_list: Option<parse::RawAccessList>,
7473
data: Vec<String>,
7574
gas_limit: Vec<String>,
75+
max_priority_fee_per_gas: Option<String>,
76+
max_fee_per_gas: Option<String>,
7677
gas_price: String,
7778
nonce: String,
7879
secret_key: String,
@@ -109,8 +110,7 @@ impl<'a> JsonStateTestBuilder<'a> {
109110
/// generates `StateTest` vectors from a ethereum josn test specification
110111
pub fn load_json(&mut self, path: &str, source: &str) -> Result<Vec<StateTest>> {
111112
let mut state_tests = Vec::new();
112-
let tests: HashMap<String, JsonStateTest> =
113-
serde_json::from_str(&strip_json_comments(source))?;
113+
let tests: HashMap<String, JsonStateTest> = serde_json::from_str(source)?;
114114

115115
for (test_name, test) in tests {
116116
let env = Self::parse_env(&test.env)?;
@@ -120,13 +120,32 @@ impl<'a> JsonStateTestBuilder<'a> {
120120
let secret_key = parse::parse_bytes(&test.transaction.secret_key)?;
121121
let from = secret_key_to_address(&SigningKey::from_slice(&secret_key)?);
122122
let nonce = parse::parse_u64(&test.transaction.nonce)?;
123-
let gas_price = parse::parse_u256(&test.transaction.gas_price)?;
123+
124+
let max_priority_fee_per_gas = test
125+
.transaction
126+
.max_priority_fee_per_gas
127+
.map_or(Ok(None), |s| parse::parse_u256(&s).map(Some))?;
128+
let max_fee_per_gas = test
129+
.transaction
130+
.max_fee_per_gas
131+
.map_or(Ok(None), |s| parse::parse_u256(&s).map(Some))?;
132+
133+
// Set gas price to `min(max_priority_fee_per_gas + base_fee, max_fee_per_gas)` for
134+
// EIP-1559 transaction.
135+
// <https://github.com/ethereum/go-ethereum/blob/1485814f89d8206bb4a1c8e10a4a2893920f683a/core/state_transition.go#L167>
136+
let gas_price = parse::parse_u256(&test.transaction.gas_price).unwrap_or_else(|_| {
137+
max_fee_per_gas
138+
.unwrap()
139+
.min(max_priority_fee_per_gas.unwrap() + env.current_base_fee)
140+
});
141+
142+
let access_list = &test.transaction.access_list;
124143

125144
let data_s: Vec<_> = test
126145
.transaction
127146
.data
128147
.iter()
129-
.map(|item| parse::parse_calldata(self.compiler, item))
148+
.map(|item| parse::parse_calldata(self.compiler, item, access_list))
130149
.collect::<Result<_>>()?;
131150

132151
let gas_limit_s: Vec<_> = test
@@ -167,7 +186,7 @@ impl<'a> JsonStateTestBuilder<'a> {
167186
}
168187
}
169188

170-
for (idx_data, data) in data_s.iter().enumerate() {
189+
for (idx_data, calldata) in data_s.iter().enumerate() {
171190
for (idx_gas, gas_limit) in gas_limit_s.iter().enumerate() {
172191
for (idx_value, value) in value_s.iter().enumerate() {
173192
for (data_refs, gas_refs, value_refs, result) in &expects {
@@ -193,10 +212,13 @@ impl<'a> JsonStateTestBuilder<'a> {
193212
to,
194213
secret_key: secret_key.clone(),
195214
nonce,
215+
max_priority_fee_per_gas,
216+
max_fee_per_gas,
196217
gas_price,
197218
gas_limit: *gas_limit,
198219
value: *value,
199-
data: data.0.clone(),
220+
data: calldata.data.clone(),
221+
access_list: calldata.access_list.clone(),
200222
exception: false,
201223
});
202224
}
@@ -313,29 +335,10 @@ impl<'a> JsonStateTestBuilder<'a> {
313335
}
314336
}
315337

316-
fn strip_json_comments(json: &str) -> String {
317-
fn strip(value: Value) -> Value {
318-
use Value::*;
319-
match value {
320-
Array(vec) => Array(vec.into_iter().map(strip).collect()),
321-
Object(map) => Object(
322-
map.into_iter()
323-
.filter(|(k, _)| !k.starts_with("//"))
324-
.map(|(k, v)| (k, strip(v)))
325-
.collect(),
326-
),
327-
_ => value,
328-
}
329-
}
330-
331-
let value: Value = serde_json::from_str(json).unwrap();
332-
strip(value).to_string()
333-
}
334-
335338
#[cfg(test)]
336339
mod test {
337340
use super::*;
338-
use eth_types::{Bytes, H256};
341+
use eth_types::{address, AccessList, AccessListItem, Bytes, H256};
339342
use std::{collections::HashMap, str::FromStr};
340343

341344
const JSON: &str = r#"
@@ -381,6 +384,15 @@ mod test {
381384
}
382385
},
383386
"transaction" : {
387+
"accessList" : [
388+
{
389+
"address" : "0x009e7baea6a6c7c4c2dfeb977efac326af552d87",
390+
"storageKeys" : [
391+
"0x0000000000000000000000000000000000000000000000000000000000000000",
392+
"0x0000000000000000000000000000000000000000000000000000000000000001"
393+
]
394+
}
395+
],
384396
"data" : [
385397
"0x6001",
386398
"0x6002"
@@ -430,9 +442,24 @@ mod test {
430442
)?),
431443
gas_limit: 400000,
432444
gas_price: U256::from(10u64),
445+
max_fee_per_gas: None,
446+
max_priority_fee_per_gas: None,
433447
nonce: 0,
434448
value: U256::from(100000u64),
435449
data: Bytes::from(hex::decode("6001")?),
450+
access_list: Some(AccessList(vec![AccessListItem {
451+
address: address!("0x009e7baea6a6c7c4c2dfeb977efac326af552d87"),
452+
storage_keys: vec![
453+
H256::from_str(
454+
"0x0000000000000000000000000000000000000000000000000000000000000000",
455+
)
456+
.unwrap(),
457+
H256::from_str(
458+
"0x0000000000000000000000000000000000000000000000000000000000000001",
459+
)
460+
.unwrap(),
461+
],
462+
}])),
436463
pre: BTreeMap::from([(
437464
acc095e,
438465
Account {
@@ -460,13 +487,4 @@ mod test {
460487

461488
Ok(())
462489
}
463-
464-
#[test]
465-
fn test_strip() {
466-
let original = r#"{"//a":"a1","b":[{"c":"c1","//d":"d1"}]}"#;
467-
let expected = r#"{"b":[{"c":"c1"}]}"#;
468-
469-
let stripped = strip_json_comments(original);
470-
assert_eq!(expected, stripped);
471-
}
472490
}

‎testool/src/statetest/parse.rs

+107-43
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,52 @@
1-
use std::collections::HashMap;
2-
31
use crate::{abi, Compiler};
4-
52
use anyhow::{bail, Context, Result};
6-
use eth_types::{Address, Bytes, H256, U256};
3+
use eth_types::{address, AccessList, AccessListItem, Address, Bytes, H256, U256};
74
use log::debug;
85
use once_cell::sync::Lazy;
96
use regex::Regex;
7+
use serde::Deserialize;
8+
use std::{collections::HashMap, str::FromStr};
109

1110
type Label = String;
1211

12+
/// Raw access list to parse
13+
pub type RawAccessList = Vec<RawAccessListItem>;
14+
15+
/// Raw access list item to parse
16+
#[derive(Clone, Debug, Deserialize)]
17+
#[serde(rename_all = "camelCase")]
18+
pub struct RawAccessListItem {
19+
address: String,
20+
storage_keys: Vec<String>,
21+
}
22+
23+
impl RawAccessListItem {
24+
pub fn new(address: String, storage_keys: Vec<String>) -> Self {
25+
Self {
26+
address,
27+
storage_keys,
28+
}
29+
}
30+
}
31+
32+
/// parsed calldata
33+
#[derive(Debug)]
34+
pub struct Calldata {
35+
pub data: Bytes,
36+
pub label: Option<Label>,
37+
pub access_list: Option<AccessList>,
38+
}
39+
40+
impl Calldata {
41+
fn new(data: Bytes, label: Option<Label>, access_list: Option<AccessList>) -> Self {
42+
Self {
43+
data,
44+
label,
45+
access_list,
46+
}
47+
}
48+
}
49+
1350
static YUL_FRAGMENT_PARSER: Lazy<Regex> =
1451
Lazy::new(|| Regex::new(r#"\s*(?P<version>\w+)?\s*(?P<code>\{[\S\s]*)"#).unwrap());
1552

@@ -66,47 +103,17 @@ fn decompose_tags(expr: &str) -> HashMap<String, String> {
66103

67104
/// returns the element as calldata bytes, supports 0x, :raw, :abi, :yul and
68105
/// { LLL }
69-
pub fn parse_calldata(compiler: &Compiler, as_str: &str) -> Result<(Bytes, Option<Label>)> {
70-
let tags = decompose_tags(as_str);
106+
pub fn parse_calldata(
107+
compiler: &Compiler,
108+
data: &str,
109+
raw_access_list: &Option<RawAccessList>,
110+
) -> Result<Calldata> {
111+
let tags = decompose_tags(data);
71112
let label = tags.get(":label").cloned();
113+
let bytes = parse_call_bytes(compiler, tags)?;
114+
let access_list = parse_access_list(raw_access_list)?;
72115

73-
if let Some(notag) = tags.get("") {
74-
let notag = notag.trim();
75-
if notag.is_empty() {
76-
Ok((Bytes::default(), label))
77-
} else if notag.starts_with('{') {
78-
Ok((compiler.lll(notag)?, label))
79-
} else if let Some(hex) = notag.strip_prefix("0x") {
80-
Ok((Bytes::from(hex::decode(hex)?), label))
81-
} else {
82-
bail!("do not know what to do with calldata (1): '{:?}'", as_str);
83-
}
84-
} else if let Some(raw) = tags.get(":raw") {
85-
if let Some(hex) = raw.strip_prefix("0x") {
86-
Ok((Bytes::from(hex::decode(hex)?), label))
87-
} else {
88-
bail!("bad encoded calldata (3) {:?}", as_str)
89-
}
90-
} else if let Some(abi) = tags.get(":abi") {
91-
Ok((abi::encode_funccall(abi)?, label))
92-
} else if let Some(yul) = tags.get(":yul") {
93-
let caps = YUL_FRAGMENT_PARSER
94-
.captures(yul)
95-
.ok_or_else(|| anyhow::anyhow!("do not know what to do with code(4) '{:?}'", as_str))?;
96-
Ok((
97-
compiler.yul(
98-
caps.name("code").unwrap().as_str(),
99-
caps.name("version").map(|m| m.as_str()),
100-
)?,
101-
label,
102-
))
103-
} else {
104-
bail!(
105-
"do not know what to do with calldata: (2) {:?} '{:?}'",
106-
tags,
107-
as_str
108-
)
109-
}
116+
Ok(Calldata::new(bytes, label, access_list))
110117
}
111118

112119
/// parse entry as code, can be 0x, :raw or { LLL }
@@ -182,3 +189,60 @@ pub fn parse_u64(as_str: &str) -> Result<u64> {
182189
Ok(U256::from_str_radix(as_str, 10)?.as_u64())
183190
}
184191
}
192+
193+
// Parse calldata to bytes
194+
fn parse_call_bytes(compiler: &Compiler, tags: HashMap<String, String>) -> Result<Bytes> {
195+
if let Some(notag) = tags.get("") {
196+
let notag = notag.trim();
197+
if notag.is_empty() {
198+
Ok(Bytes::default())
199+
} else if notag.starts_with('{') {
200+
Ok(compiler.lll(notag)?)
201+
} else if let Some(hex) = notag.strip_prefix("0x") {
202+
Ok(Bytes::from(hex::decode(hex)?))
203+
} else {
204+
bail!("do not know what to do with calldata (1): '{tags:?}'");
205+
}
206+
} else if let Some(raw) = tags.get(":raw") {
207+
if let Some(hex) = raw.strip_prefix("0x") {
208+
Ok(Bytes::from(hex::decode(hex)?))
209+
} else {
210+
bail!("bad encoded calldata (3) {:?}", tags)
211+
}
212+
} else if let Some(abi) = tags.get(":abi") {
213+
Ok(abi::encode_funccall(abi)?)
214+
} else if let Some(yul) = tags.get(":yul") {
215+
let caps = YUL_FRAGMENT_PARSER
216+
.captures(yul)
217+
.ok_or_else(|| anyhow::anyhow!("do not know what to do with code(4) '{:?}'", tags))?;
218+
Ok(compiler.yul(
219+
caps.name("code").unwrap().as_str(),
220+
caps.name("version").map(|m| m.as_str()),
221+
)?)
222+
} else {
223+
bail!("do not know what to do with calldata: (2) '{:?}'", tags,)
224+
}
225+
}
226+
227+
// Parse access list
228+
fn parse_access_list(raw_access_list: &Option<RawAccessList>) -> Result<Option<AccessList>> {
229+
if let Some(raw_access_list) = raw_access_list {
230+
let mut items = Vec::with_capacity(raw_access_list.len());
231+
for raw in raw_access_list {
232+
let storage_keys = raw
233+
.storage_keys
234+
.iter()
235+
.map(|key| H256::from_str(key))
236+
.collect::<Result<_, _>>()?;
237+
238+
items.push(AccessListItem {
239+
address: address!(raw.address),
240+
storage_keys,
241+
});
242+
}
243+
244+
return Ok(Some(AccessList(items)));
245+
}
246+
247+
Ok(None)
248+
}

‎testool/src/statetest/spec.rs

+119-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
use anyhow::{anyhow, bail, Context};
2-
use eth_types::{geth_types::Account, Address, Bytes, Word, H256, U256, U64};
3-
use ethers_core::{k256::ecdsa::SigningKey, utils::secret_key_to_address};
2+
use eth_types::{
3+
geth_types::{Account, TxType},
4+
Address, Bytes, Word, H256, U256, U64,
5+
};
6+
use ethers_core::{
7+
k256::ecdsa::SigningKey,
8+
types::{
9+
transaction::{eip2718::TypedTransaction, eip2930::AccessList},
10+
Eip1559TransactionRequest, TransactionRequest,
11+
},
12+
utils::secret_key_to_address,
13+
};
414
use std::{
515
collections::{BTreeMap, HashMap},
616
str::FromStr,
@@ -9,6 +19,8 @@ use std::{
919
/// <https://github.com/ethereum/tests/pull/857> "set default gasPrice to 10"
1020
pub const DEFAULT_BASE_FEE: u32 = 10;
1121

22+
const ETH_CHAIN_ID: u64 = 1;
23+
1224
#[derive(PartialEq, Eq, Debug, Clone)]
1325
pub struct Env {
1426
pub current_base_fee: U256,
@@ -53,10 +65,13 @@ pub struct StateTest {
5365
pub from: Address,
5466
pub to: Option<Address>,
5567
pub gas_limit: u64,
68+
pub max_priority_fee_per_gas: Option<U256>,
69+
pub max_fee_per_gas: Option<U256>,
5670
pub gas_price: U256,
5771
pub nonce: u64,
5872
pub value: U256,
5973
pub data: Bytes,
74+
pub access_list: Option<AccessList>,
6075
pub pre: BTreeMap<Address, Account>,
6176
pub result: StateTestResult,
6277
pub exception: bool,
@@ -74,14 +89,19 @@ impl std::fmt::Display for StateTest {
7489
format!("{k} :")
7590
};
7691
let max_len = max_len - k.len();
92+
let v = v.chars().collect::<Vec<_>>();
7793
for i in 0..=v.len() / max_len {
7894
if i == 0 && !k.is_empty() {
7995
text.push_str(&k);
8096
} else {
8197
let padding: String = " ".repeat(k.len());
8298
text.push_str(&padding);
8399
}
84-
text.push_str(&v[i * max_len..std::cmp::min((i + 1) * max_len, v.len())]);
100+
text.push_str(
101+
&v[i * max_len..std::cmp::min((i + 1) * max_len, v.len())]
102+
.iter()
103+
.collect::<String>(),
104+
);
85105
text.push('\n');
86106
}
87107
text
@@ -108,10 +128,19 @@ impl std::fmt::Display for StateTest {
108128
table.add_row(row!["from", format!("{:?}", self.from)]);
109129
table.add_row(row!["to", format!("{:?}", self.to)]);
110130
table.add_row(row!["gas_limit", format!("{}", self.gas_limit)]);
131+
table.add_row(row![
132+
"max_priority_fee_per_gas",
133+
format!("{:?}", self.max_priority_fee_per_gas)
134+
]);
135+
table.add_row(row![
136+
"max_fee_per_gas",
137+
format!("{:?}", self.max_fee_per_gas)
138+
]);
111139
table.add_row(row!["gas_price", format!("{}", self.gas_price)]);
112140
table.add_row(row!["nonce", format!("{}", self.nonce)]);
113141
table.add_row(row!["value", format!("{}", self.value)]);
114142
table.add_row(row!["data", format(&hex::encode(&self.data), "")]);
143+
table.add_row(row!["access_list", format!("{:?}", self.access_list)]);
115144
table.add_row(row!["exception", self.exception]);
116145

117146
let mut addrs: Vec<_> = self.pre.keys().collect();
@@ -273,15 +302,102 @@ impl StateTest {
273302
from,
274303
to,
275304
gas_limit,
305+
max_priority_fee_per_gas: None,
306+
max_fee_per_gas: None,
276307
gas_price: U256::one(),
277308
nonce: 0,
278309
value,
279310
data: data.into(),
311+
access_list: None,
280312
pre,
281313
result: HashMap::new(),
282314
exception: false,
283315
};
284316

285317
Ok(state_test)
286318
}
319+
320+
/// Parse transaction type.
321+
pub fn tx_type(&self) -> TxType {
322+
if self.max_priority_fee_per_gas.is_some() {
323+
// For EIP-1559, both maxPriorityFeePerGas and maxFeePerGas must
324+
// exist, and accessList should exist but may be empty.
325+
assert!(self.max_fee_per_gas.is_some());
326+
assert!(self.access_list.is_some());
327+
328+
TxType::Eip1559
329+
} else if self.access_list.is_some() {
330+
TxType::Eip2930
331+
} else {
332+
// Set transaction type to EIP-155 as default.
333+
TxType::Eip155
334+
}
335+
}
336+
337+
/// Normalize the signature back to 0/1.
338+
pub fn normalize_sig_v(&self, v: u64) -> u64 {
339+
match self.tx_type() {
340+
TxType::Eip1559 | TxType::Eip2930 => {
341+
// <https://github.com/gakonst/ethers-rs/blob/8421cfdbb4f26be3989bd11e525f8768d4323bfe/ethers-core/src/types/transaction/mod.rs#L40>
342+
if v > 1 {
343+
v - ETH_CHAIN_ID * 2 - 35
344+
} else {
345+
v
346+
}
347+
}
348+
_ => v,
349+
}
350+
}
351+
352+
/// Build a transaction from this test case.
353+
pub fn build_tx(&self) -> TypedTransaction {
354+
match self.tx_type() {
355+
TxType::Eip1559 => self.build_eip1559_tx(),
356+
TxType::Eip2930 => self.build_eip2930_tx(),
357+
_ => self.build_normal_tx_request().into(),
358+
}
359+
}
360+
361+
fn build_eip1559_tx(&self) -> TypedTransaction {
362+
let mut request = Eip1559TransactionRequest::new()
363+
.chain_id(ETH_CHAIN_ID)
364+
.from(self.from)
365+
.nonce(self.nonce)
366+
.value(self.value)
367+
.data(self.data.clone())
368+
.gas(self.gas_limit)
369+
.access_list(self.access_list.clone().unwrap())
370+
.max_priority_fee_per_gas(self.max_priority_fee_per_gas.unwrap())
371+
.max_fee_per_gas(self.max_fee_per_gas.unwrap());
372+
373+
if let Some(to) = self.to {
374+
request = request.to(to);
375+
}
376+
377+
request.into()
378+
}
379+
380+
fn build_eip2930_tx(&self) -> TypedTransaction {
381+
let request = self.build_normal_tx_request();
382+
request
383+
.with_access_list(self.access_list.clone().unwrap())
384+
.into()
385+
}
386+
387+
fn build_normal_tx_request(&self) -> TransactionRequest {
388+
let mut request = TransactionRequest::new()
389+
.chain_id(ETH_CHAIN_ID)
390+
.from(self.from)
391+
.nonce(self.nonce)
392+
.value(self.value)
393+
.data(self.data.clone())
394+
.gas(self.gas_limit)
395+
.gas_price(self.gas_price);
396+
397+
if let Some(to) = self.to {
398+
request = request.to(to);
399+
}
400+
401+
request
402+
}
287403
}

‎testool/src/statetest/yaml.rs

+211-86
Large diffs are not rendered by default.

‎testool/src/utils.rs

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ use log::{error, info};
66
use prettytable::Table;
77
use std::process::{Command, Stdio};
88

9+
/// Chain ID for ETH mainnet
10+
pub const ETH_CHAIN_ID: u64 = 1;
11+
912
#[derive(Debug, Eq, PartialEq, PartialOrd)]
1013
pub enum MainnetFork {
1114
Shanghai = 15,

0 commit comments

Comments
 (0)
This repository has been archived.