diff --git a/eth-types/src/geth_types.rs b/eth-types/src/geth_types.rs index 7c81808d29..c95475a5bc 100644 --- a/eth-types/src/geth_types.rs +++ b/eth-types/src/geth_types.rs @@ -5,10 +5,13 @@ use crate::{ keccak256, sign_types::{biguint_to_32bytes_le, ct_option_ok_or, recover_pk, SignData, SECP256K1_Q}, AccessList, Address, Block, Bytecode, Bytes, Error, GethExecTrace, Hash, ToBigEndian, - ToLittleEndian, ToWord, Word, U64, + ToLittleEndian, ToWord, Word, H256, U64, }; use ethers_core::{ - types::{transaction::response, NameOrAddress, TransactionRequest}, + types::{ + transaction::{eip2718::TypedTransaction, response}, + Eip1559TransactionRequest, Eip2930TransactionRequest, NameOrAddress, TransactionRequest, + }, utils::get_contract_address, }; use ethers_signers::{LocalWallet, Signer}; @@ -18,6 +21,117 @@ use num_bigint::BigUint; use serde::{Serialize, Serializer}; use serde_with::serde_as; use std::collections::HashMap; +use strum_macros::EnumIter; + +/// Tx type +#[derive(Default, Debug, Copy, Clone, EnumIter, Serialize, PartialEq, Eq)] +pub enum TxType { + /// EIP 155 tx + #[default] + Eip155 = 0, + /// Pre EIP 155 tx + PreEip155, + /// EIP 1559 tx + Eip1559, + /// EIP 2930 tx + Eip2930, +} + +impl From for usize { + fn from(value: TxType) -> Self { + value as usize + } +} + +impl From for u64 { + fn from(value: TxType) -> Self { + value as u64 + } +} + +impl TxType { + /// If this type is PreEip155 + pub fn is_pre_eip155(&self) -> bool { + matches!(*self, TxType::PreEip155) + } + + /// If this type is EIP155 or not + pub fn is_eip155(&self) -> bool { + matches!(*self, TxType::Eip155) + } + + /// If this type is Eip1559 or not + pub fn is_eip1559(&self) -> bool { + matches!(*self, TxType::Eip1559) + } + + /// If this type is Eip2930 or not + pub fn is_eip2930(&self) -> bool { + matches!(*self, TxType::Eip2930) + } + + /// Get the type of transaction + pub fn get_tx_type(tx: &crate::Transaction) -> Self { + match tx.transaction_type { + Some(x) if x == U64::from(1) => Self::Eip2930, + Some(x) if x == U64::from(2) => Self::Eip1559, + _ => match tx.v.as_u64() { + 0 | 1 | 27 | 28 => Self::PreEip155, + _ => Self::Eip155, + }, + } + } + + /// Return the recovery id of signature for recovering the signing pk + pub fn get_recovery_id(&self, v: u64) -> u8 { + let recovery_id = match *self { + TxType::Eip155 => (v + 1) % 2, + TxType::PreEip155 => { + assert!(v == 0x1b || v == 0x1c, "v: {v}"); + v - 27 + } + TxType::Eip1559 => { + assert!(v <= 1); + v + } + TxType::Eip2930 => { + assert!(v <= 1); + v + } + }; + + recovery_id as u8 + } +} + +/// Get the RLP bytes for signing +pub fn get_rlp_unsigned(tx: &crate::Transaction) -> Vec { + let sig_v = tx.v; + match TxType::get_tx_type(tx) { + TxType::Eip155 => { + let mut tx: TransactionRequest = tx.into(); + tx.chain_id = Some(tx.chain_id.unwrap_or_else(|| { + let recv_v = TxType::Eip155.get_recovery_id(sig_v.as_u64()) as u64; + (sig_v - recv_v - 35) / 2 + })); + tx.rlp().to_vec() + } + TxType::PreEip155 => { + let tx: TransactionRequest = tx.into(); + tx.rlp_unsigned().to_vec() + } + TxType::Eip1559 => { + let tx: Eip1559TransactionRequest = tx.into(); + let typed_tx: TypedTransaction = tx.into(); + typed_tx.rlp().to_vec() + } + TxType::Eip2930 => { + let tx: Eip2930TransactionRequest = tx.into(); + let typed_tx: TypedTransaction = tx.into(); + typed_tx.rlp().to_vec() + } + } +} /// Definition of all of the data related to an account. #[serde_as] @@ -156,6 +270,8 @@ pub struct Withdrawal { /// Definition of all of the constants related to an Ethereum transaction. #[derive(Debug, Default, Clone, Serialize)] pub struct Transaction { + /// Tx type + pub tx_type: TxType, /// Sender address pub from: Address, /// Recipient address (None for contract creation) @@ -172,9 +288,9 @@ pub struct Transaction { /// Gas Price pub gas_price: Word, /// Gas fee cap - pub gas_fee_cap: Word, + pub gas_fee_cap: Option, /// Gas tip cap - pub gas_tip_cap: Word, + pub gas_tip_cap: Option, /// The compiled code of a contract OR the first 4 bytes of the hash of the /// invoked method signature and encoded parameters. For details see /// Ethereum Contract ABI @@ -188,6 +304,14 @@ pub struct Transaction { pub r: Word, /// "s" value of the transaction signature pub s: Word, + + /// RLP bytes + pub rlp_bytes: Vec, + /// RLP unsigned bytes + pub rlp_unsigned_bytes: Vec, + + /// Transaction hash + pub hash: H256, } impl From<&Transaction> for crate::Transaction { @@ -199,8 +323,8 @@ impl From<&Transaction> for crate::Transaction { gas: tx.gas_limit.to_word(), value: tx.value, gas_price: Some(tx.gas_price), - max_priority_fee_per_gas: Some(tx.gas_tip_cap), - max_fee_per_gas: Some(tx.gas_fee_cap), + max_priority_fee_per_gas: tx.gas_tip_cap, + max_fee_per_gas: tx.gas_fee_cap, input: tx.call_data.clone(), access_list: tx.access_list.clone(), v: tx.v.into(), @@ -214,19 +338,23 @@ impl From<&Transaction> for crate::Transaction { impl From<&crate::Transaction> for Transaction { fn from(tx: &crate::Transaction) -> Transaction { Transaction { + tx_type: TxType::get_tx_type(tx), from: tx.from, to: tx.to, nonce: tx.nonce.as_u64().into(), gas_limit: tx.gas.as_u64().into(), value: tx.value, gas_price: tx.gas_price.unwrap_or_default(), - gas_tip_cap: tx.max_priority_fee_per_gas.unwrap_or_default(), - gas_fee_cap: tx.max_fee_per_gas.unwrap_or_default(), + gas_tip_cap: tx.max_priority_fee_per_gas, + gas_fee_cap: tx.max_fee_per_gas, call_data: tx.input.clone(), access_list: tx.access_list.clone(), v: tx.v.as_u64(), r: tx.r, s: tx.s, + rlp_bytes: tx.rlp().to_vec(), + rlp_unsigned_bytes: get_rlp_unsigned(tx), + hash: tx.hash, } } } @@ -256,13 +384,14 @@ impl Transaction { gas_limit: U64::zero(), value: Word::zero(), gas_price: Word::zero(), - gas_tip_cap: Word::zero(), - gas_fee_cap: Word::zero(), + gas_tip_cap: Some(Word::zero()), + gas_fee_cap: Some(Word::zero()), call_data: Bytes::new(), access_list: None, v: 0, r: Word::zero(), s: Word::zero(), + ..Default::default() } } /// Return the SignData associated with this Transaction. @@ -355,6 +484,9 @@ impl Transaction { s: self.s, v: U64::from(self.v), block_number: Some(block_number), + transaction_type: Some(U64::from(self.tx_type as u64)), + max_priority_fee_per_gas: self.gas_tip_cap, + max_fee_per_gas: self.gas_fee_cap, chain_id: Some(chain_id), ..response::Transaction::default() } diff --git a/eth-types/src/lib.rs b/eth-types/src/lib.rs index f1f9fb7d5c..9bebdc63d2 100644 --- a/eth-types/src/lib.rs +++ b/eth-types/src/lib.rs @@ -41,7 +41,10 @@ use ethers_core::types; pub use ethers_core::{ abi::ethereum_types::{BigEndianHash, U512}, types::{ - transaction::{eip2930::AccessList, response::Transaction}, + transaction::{ + eip2930::{AccessList, AccessListItem}, + response::Transaction, + }, Address, Block, Bytes, Signature, H160, H256, H64, U256, U64, }, }; diff --git a/external-tracer/src/lib.rs b/external-tracer/src/lib.rs index d144cc515e..7bb5c27db5 100644 --- a/external-tracer/src/lib.rs +++ b/external-tracer/src/lib.rs @@ -5,7 +5,7 @@ use eth_types::{ Address, Error, GethExecTrace, Word, }; use serde::Serialize; -use std::collections::HashMap; +use std::collections::BTreeMap; /// Configuration structure for `geth_utils::trace` #[derive(Debug, Default, Clone, Serialize)] @@ -18,7 +18,7 @@ pub struct TraceConfig { /// block constants pub block_constants: BlockConstants, /// accounts - pub accounts: HashMap, + pub accounts: BTreeMap, /// transaction pub transactions: Vec, /// withdrawal @@ -78,7 +78,8 @@ pub fn trace(config: &TraceConfig) -> Result, Error> { let allowed_cases = error.starts_with("nonce too low") || error.starts_with("nonce too high") || error.starts_with("intrinsic gas too low") - || error.starts_with("insufficient funds for gas * price + value"); + || error.starts_with("insufficient funds for gas * price + value") + || error.starts_with("insufficient funds for transfer"); if trace.invalid && !allowed_cases { return Err(Error::TracingError(error.clone())); } diff --git a/geth-utils/gethutil/trace.go b/geth-utils/gethutil/trace.go index f0cb76cffd..d48e46666c 100644 --- a/geth-utils/gethutil/trace.go +++ b/geth-utils/gethutil/trace.go @@ -99,19 +99,20 @@ type Account struct { } type Transaction struct { - From common.Address `json:"from"` - To *common.Address `json:"to"` - Nonce hexutil.Uint64 `json:"nonce"` - Value *hexutil.Big `json:"value"` - GasLimit hexutil.Uint64 `json:"gas_limit"` - GasPrice *hexutil.Big `json:"gas_price"` - GasFeeCap *hexutil.Big `json:"gas_fee_cap"` - GasTipCap *hexutil.Big `json:"gas_tip_cap"` - CallData hexutil.Bytes `json:"call_data"` - AccessList []struct { - Address common.Address `json:"address"` - StorageKeys []common.Hash `json:"storage_keys"` - } `json:"access_list"` + From common.Address `json:"from"` + To *common.Address `json:"to"` + Nonce hexutil.Uint64 `json:"nonce"` + Value *hexutil.Big `json:"value"` + GasLimit hexutil.Uint64 `json:"gas_limit"` + GasPrice *hexutil.Big `json:"gas_price"` + GasFeeCap *hexutil.Big `json:"gas_fee_cap"` + GasTipCap *hexutil.Big `json:"gas_tip_cap"` + CallData hexutil.Bytes `json:"call_data"` + AccessList types.AccessList `json:"access_list"` + Type string `json:"tx_type"` + V int64 `json:"v"` + R *hexutil.Big `json:"r"` + S *hexutil.Big `json:"s"` } type TraceConfig struct { @@ -160,10 +161,14 @@ func Trace(config TraceConfig) ([]*ExecutionResult, error) { blockGasLimit := toBigInt(config.Block.GasLimit).Uint64() messages := make([]core.Message, len(config.Transactions)) for i, tx := range config.Transactions { - // If gas price is specified directly, the tx is treated as legacy type. if tx.GasPrice != nil { - tx.GasFeeCap = tx.GasPrice - tx.GasTipCap = tx.GasPrice + // Set GasFeeCap and GasTipCap to GasPrice if not exist. + if tx.GasFeeCap == nil { + tx.GasFeeCap = tx.GasPrice + } + if tx.GasTipCap == nil { + tx.GasTipCap = tx.GasPrice + } } txAccessList := make(types.AccessList, len(tx.AccessList)) diff --git a/mock/src/transaction.rs b/mock/src/transaction.rs index 1184d418d4..e960181b82 100644 --- a/mock/src/transaction.rs +++ b/mock/src/transaction.rs @@ -133,8 +133,8 @@ pub struct MockTransaction { pub s: Option, pub transaction_type: U64, pub access_list: AccessList, - pub max_priority_fee_per_gas: Word, - pub max_fee_per_gas: Word, + pub max_priority_fee_per_gas: Option, + pub max_fee_per_gas: Option, pub chain_id: Word, pub invalid: bool, } @@ -158,8 +158,8 @@ impl Default for MockTransaction { s: None, transaction_type: U64::zero(), access_list: AccessList::default(), - max_priority_fee_per_gas: Word::zero(), - max_fee_per_gas: Word::zero(), + max_priority_fee_per_gas: None, + max_fee_per_gas: None, chain_id: *MOCK_CHAIN_ID, invalid: false, } @@ -185,8 +185,8 @@ impl From for Transaction { s: mock.s.unwrap_or_default(), transaction_type: Some(mock.transaction_type), access_list: Some(mock.access_list), - max_priority_fee_per_gas: Some(mock.max_priority_fee_per_gas), - max_fee_per_gas: Some(mock.max_fee_per_gas), + max_priority_fee_per_gas: mock.max_priority_fee_per_gas, + max_fee_per_gas: mock.max_fee_per_gas, chain_id: Some(mock.chain_id), other: OtherFields::default(), } @@ -289,13 +289,13 @@ impl MockTransaction { /// Set max_priority_fee_per_gas field for the MockTransaction. pub fn max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: Word) -> &mut Self { - self.max_priority_fee_per_gas = max_priority_fee_per_gas; + self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); self } /// Set max_fee_per_gas field for the MockTransaction. pub fn max_fee_per_gas(&mut self, max_fee_per_gas: Word) -> &mut Self { - self.max_fee_per_gas = max_fee_per_gas; + self.max_fee_per_gas = Some(max_fee_per_gas); self } diff --git a/testool/src/statetest/executor.rs b/testool/src/statetest/executor.rs index 2282d41eb8..0eff2d1452 100644 --- a/testool/src/statetest/executor.rs +++ b/testool/src/statetest/executor.rs @@ -1,14 +1,11 @@ use super::{AccountMatch, StateTest, StateTestResult}; -use crate::config::TestSuite; +use crate::{config::TestSuite, utils::ETH_CHAIN_ID}; use bus_mapping::{ circuit_input_builder::{CircuitInputBuilder, FixedCParams}, mock::BlockData, }; use eth_types::{geth_types, Address, Bytes, Error, GethExecTrace, U256, U64}; -use ethers_core::{ - k256::ecdsa::SigningKey, - types::{transaction::eip2718::TypedTransaction, TransactionRequest, Withdrawal}, -}; +use ethers_core::{k256::ecdsa::SigningKey, types::Withdrawal, utils::keccak256}; use ethers_signers::{LocalWallet, Signer}; use external_tracer::TraceConfig; use halo2_proofs::{dev::MockProver, halo2curves::bn256::Fr}; @@ -138,28 +135,22 @@ fn check_post( } fn into_traceconfig(st: StateTest) -> (String, TraceConfig, StateTestResult) { - let chain_id = 1; - let wallet = LocalWallet::from_str(&hex::encode(st.secret_key.0)).unwrap(); - let mut tx = TransactionRequest::new() - .chain_id(chain_id) - .from(st.from) - .nonce(st.nonce) - .value(st.value) - .data(st.data.clone()) - .gas(st.gas_limit) - .gas_price(st.gas_price); - - if let Some(to) = st.to { - tx = tx.to(to); - } - let tx: TypedTransaction = tx.into(); + let tx_type = st.tx_type(); + let tx = st.build_tx(); + + let wallet = LocalWallet::from_str(&hex::encode(&st.secret_key.0)).unwrap(); + let rlp_unsigned = tx.rlp().to_vec(); let sig = wallet.sign_transaction_sync(&tx).unwrap(); + let v = st.normalize_sig_v(sig.v); + let rlp_signed = tx.rlp_signed(&sig).to_vec(); + let tx_hash = keccak256(tx.rlp_signed(&sig)); + let accounts = st.pre; ( st.id, TraceConfig { - chain_id: U256::one(), + chain_id: U256::from(ETH_CHAIN_ID), history_hashes: vec![U256::from_big_endian(st.env.previous_hash.as_bytes())], block_constants: geth_types::BlockConstants { coinbase: st.env.current_coinbase, @@ -171,21 +162,25 @@ fn into_traceconfig(st: StateTest) -> (String, TraceConfig, StateTestResult) { }, transactions: vec![geth_types::Transaction { + tx_type, from: st.from, to: st.to, nonce: U64::from(st.nonce), value: st.value, gas_limit: U64::from(st.gas_limit), gas_price: st.gas_price, - gas_fee_cap: U256::zero(), - gas_tip_cap: U256::zero(), + gas_fee_cap: st.max_fee_per_gas, + gas_tip_cap: st.max_priority_fee_per_gas, call_data: st.data, - access_list: None, - v: sig.v, + access_list: st.access_list, + v, r: sig.r, s: sig.s, + rlp_bytes: rlp_signed, + rlp_unsigned_bytes: rlp_unsigned, + hash: tx_hash.into(), }], - accounts: st.pre.into_iter().collect(), + accounts, ..Default::default() }, st.result, diff --git a/testool/src/statetest/json.rs b/testool/src/statetest/json.rs index b60c5157f1..fc357adb9c 100644 --- a/testool/src/statetest/json.rs +++ b/testool/src/statetest/json.rs @@ -9,8 +9,6 @@ use ethers_core::{k256::ecdsa::SigningKey, utils::secret_key_to_address}; use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; -use serde_json::value::Value; - fn default_block_base_fee() -> String { DEFAULT_BASE_FEE.to_string() } @@ -71,8 +69,11 @@ struct JsonStateTest { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct Transaction { + access_list: Option, data: Vec, gas_limit: Vec, + max_priority_fee_per_gas: Option, + max_fee_per_gas: Option, gas_price: String, nonce: String, secret_key: String, @@ -109,8 +110,7 @@ impl<'a> JsonStateTestBuilder<'a> { /// generates `StateTest` vectors from a ethereum josn test specification pub fn load_json(&mut self, path: &str, source: &str) -> Result> { let mut state_tests = Vec::new(); - let tests: HashMap = - serde_json::from_str(&strip_json_comments(source))?; + let tests: HashMap = serde_json::from_str(source)?; for (test_name, test) in tests { let env = Self::parse_env(&test.env)?; @@ -120,13 +120,32 @@ impl<'a> JsonStateTestBuilder<'a> { let secret_key = parse::parse_bytes(&test.transaction.secret_key)?; let from = secret_key_to_address(&SigningKey::from_slice(&secret_key)?); let nonce = parse::parse_u64(&test.transaction.nonce)?; - let gas_price = parse::parse_u256(&test.transaction.gas_price)?; + + let max_priority_fee_per_gas = test + .transaction + .max_priority_fee_per_gas + .map_or(Ok(None), |s| parse::parse_u256(&s).map(Some))?; + let max_fee_per_gas = test + .transaction + .max_fee_per_gas + .map_or(Ok(None), |s| parse::parse_u256(&s).map(Some))?; + + // Set gas price to `min(max_priority_fee_per_gas + base_fee, max_fee_per_gas)` for + // EIP-1559 transaction. + // + let gas_price = parse::parse_u256(&test.transaction.gas_price).unwrap_or_else(|_| { + max_fee_per_gas + .unwrap() + .min(max_priority_fee_per_gas.unwrap() + env.current_base_fee) + }); + + let access_list = &test.transaction.access_list; let data_s: Vec<_> = test .transaction .data .iter() - .map(|item| parse::parse_calldata(self.compiler, item)) + .map(|item| parse::parse_calldata(self.compiler, item, access_list)) .collect::>()?; let gas_limit_s: Vec<_> = test @@ -167,7 +186,7 @@ impl<'a> JsonStateTestBuilder<'a> { } } - for (idx_data, data) in data_s.iter().enumerate() { + for (idx_data, calldata) in data_s.iter().enumerate() { for (idx_gas, gas_limit) in gas_limit_s.iter().enumerate() { for (idx_value, value) in value_s.iter().enumerate() { for (data_refs, gas_refs, value_refs, result) in &expects { @@ -193,10 +212,13 @@ impl<'a> JsonStateTestBuilder<'a> { to, secret_key: secret_key.clone(), nonce, + max_priority_fee_per_gas, + max_fee_per_gas, gas_price, gas_limit: *gas_limit, value: *value, - data: data.0.clone(), + data: calldata.data.clone(), + access_list: calldata.access_list.clone(), exception: false, }); } @@ -313,29 +335,10 @@ impl<'a> JsonStateTestBuilder<'a> { } } -fn strip_json_comments(json: &str) -> String { - fn strip(value: Value) -> Value { - use Value::*; - match value { - Array(vec) => Array(vec.into_iter().map(strip).collect()), - Object(map) => Object( - map.into_iter() - .filter(|(k, _)| !k.starts_with("//")) - .map(|(k, v)| (k, strip(v))) - .collect(), - ), - _ => value, - } - } - - let value: Value = serde_json::from_str(json).unwrap(); - strip(value).to_string() -} - #[cfg(test)] mod test { use super::*; - use eth_types::{Bytes, H256}; + use eth_types::{address, AccessList, AccessListItem, Bytes, H256}; use std::{collections::HashMap, str::FromStr}; const JSON: &str = r#" @@ -381,6 +384,15 @@ mod test { } }, "transaction" : { + "accessList" : [ + { + "address" : "0x009e7baea6a6c7c4c2dfeb977efac326af552d87", + "storageKeys" : [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001" + ] + } + ], "data" : [ "0x6001", "0x6002" @@ -430,9 +442,24 @@ mod test { )?), gas_limit: 400000, gas_price: U256::from(10u64), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, nonce: 0, value: U256::from(100000u64), data: Bytes::from(hex::decode("6001")?), + access_list: Some(AccessList(vec![AccessListItem { + address: address!("0x009e7baea6a6c7c4c2dfeb977efac326af552d87"), + storage_keys: vec![ + H256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + H256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(), + ], + }])), pre: BTreeMap::from([( acc095e, Account { @@ -460,13 +487,4 @@ mod test { Ok(()) } - - #[test] - fn test_strip() { - let original = r#"{"//a":"a1","b":[{"c":"c1","//d":"d1"}]}"#; - let expected = r#"{"b":[{"c":"c1"}]}"#; - - let stripped = strip_json_comments(original); - assert_eq!(expected, stripped); - } } diff --git a/testool/src/statetest/parse.rs b/testool/src/statetest/parse.rs index 0576cc8d35..7f41973755 100644 --- a/testool/src/statetest/parse.rs +++ b/testool/src/statetest/parse.rs @@ -1,15 +1,52 @@ -use std::collections::HashMap; - use crate::{abi, Compiler}; - use anyhow::{bail, Context, Result}; -use eth_types::{Address, Bytes, H256, U256}; +use eth_types::{address, AccessList, AccessListItem, Address, Bytes, H256, U256}; use log::debug; use once_cell::sync::Lazy; use regex::Regex; +use serde::Deserialize; +use std::{collections::HashMap, str::FromStr}; type Label = String; +/// Raw access list to parse +pub type RawAccessList = Vec; + +/// Raw access list item to parse +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RawAccessListItem { + address: String, + storage_keys: Vec, +} + +impl RawAccessListItem { + pub fn new(address: String, storage_keys: Vec) -> Self { + Self { + address, + storage_keys, + } + } +} + +/// parsed calldata +#[derive(Debug)] +pub struct Calldata { + pub data: Bytes, + pub label: Option