From ba4f848bf97339d230c74535b63dc260d79a1380 Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Thu, 26 Sep 2024 21:55:54 +0200 Subject: [PATCH 01/10] Added EIP-7702 and fixed tests --- Cargo.lock | 8 +- Cargo.toml | 8 +- engine-precompiles/src/lib.rs | 10 +- engine-standalone-storage/src/sync/types.rs | 8 + engine-tests/src/tests/repro.rs | 12 +- engine-tests/src/tests/standalone/sanity.rs | 1 + engine-tests/src/tests/uniswap.rs | 10 +- engine-tests/src/utils/solidity/erc20.rs | 1 + .../src/backwards_compatibility.rs | 5 + engine-transactions/src/eip_7702.rs | 262 ++++++++++++++++++ engine-transactions/src/lib.rs | 38 ++- engine/src/engine.rs | 43 ++- 12 files changed, 371 insertions(+), 35 deletions(-) create mode 100644 engine-transactions/src/eip_7702.rs diff --git a/Cargo.lock b/Cargo.lock index 6e51057e8..7e876b4ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "evm" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.45.4-aurora#1d9d1dc299fe379913347a71ca00f06344a07262" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" dependencies = [ "auto_impl", "environmental", @@ -2060,7 +2060,7 @@ dependencies = [ [[package]] name = "evm-core" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.45.4-aurora#1d9d1dc299fe379913347a71ca00f06344a07262" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" dependencies = [ "parity-scale-codec", "primitive-types 0.12.2", @@ -2071,7 +2071,7 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.45.4-aurora#1d9d1dc299fe379913347a71ca00f06344a07262" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" dependencies = [ "environmental", "evm-core", @@ -2082,7 +2082,7 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.45.4-aurora#1d9d1dc299fe379913347a71ca00f06344a07262" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" dependencies = [ "auto_impl", "environmental", diff --git a/Cargo.toml b/Cargo.toml index 402355115..7c1e9b9cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,10 @@ byte-slice-cast = { version = "1", default-features = false } criterion = "0.5" digest = "0.10" ethabi = { version = "18", default-features = false } -evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.45.4-aurora", default-features = false } -evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.45.4-aurora", default-features = false, features = ["std"] } -evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.45.4-aurora", default-features = false, features = ["std", "tracing"] } -evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.45.4-aurora", default-features = false, features = ["std", "tracing"] } +evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false } +evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false, features = ["std"] } +evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false, features = ["std", "tracing"] } +evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false, features = ["std", "tracing"] } fixed-hash = { version = "0.8", default-features = false } function_name = "0.3" git2 = "0.19" diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index 454ccd203..932c44475 100644 --- a/engine-precompiles/src/lib.rs +++ b/engine-precompiles/src/lib.rs @@ -41,11 +41,11 @@ use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::IO; use aurora_engine_sdk::promise::ReadOnlyPromiseHandler; use aurora_engine_types::{account_id::AccountId, types::Address, vec, BTreeMap, BTreeSet, Box}; +use evm::backend::Log; use evm::executor::{ self, stack::{PrecompileFailure, PrecompileHandle}, }; -use evm::{backend::Log, executor::stack::IsPrecompileResult}; use evm::{Context, ExitError, ExitFatal, ExitSucceed}; use promise_result::PromiseResult; use xcc::cross_contract_call; @@ -156,12 +156,8 @@ impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> executor::stack::Preco Some(result.and_then(|output| post_process(output, handle))) } - fn is_precompile(&self, address: prelude::H160, _remaining_gas: u64) -> IsPrecompileResult { - let is_precompile = self.all_precompiles.contains_key(&Address::new(address)); - IsPrecompileResult::Answer { - is_precompile, - extra_cost: 0, - } + fn is_precompile(&self, address: prelude::H160) -> bool { + self.all_precompiles.contains_key(&Address::new(address)) } } diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index 7a12ca69d..cd8266af1 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -219,6 +219,7 @@ impl TransactionKind { value, data, access_list: Vec::new(), + authorization_list: Vec::new(), } } Self::Deploy(data) => { @@ -236,6 +237,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } } Self::DeployErc20(_) => { @@ -254,6 +256,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } } Self::FtOnTransfer(args) => { @@ -275,6 +278,7 @@ impl TransactionKind { value, data: Vec::new(), access_list: Vec::new(), + authorization_list: Vec::new(), } } else { let from = Self::get_implicit_address(engine_account); @@ -311,6 +315,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } } } @@ -343,6 +348,7 @@ impl TransactionKind { value, data: Vec::new(), access_list: Vec::new(), + authorization_list: Vec::new(), } }, |erc20_address| { @@ -370,6 +376,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } }, ) @@ -468,6 +475,7 @@ impl TransactionKind { value: Wei::zero(), data: method_name.as_bytes().to_vec(), access_list: Vec::new(), + authorization_list: Vec::new(), } } diff --git a/engine-tests/src/tests/repro.rs b/engine-tests/src/tests/repro.rs index f5885b0b3..1ffcfb2aa 100644 --- a/engine-tests/src/tests/repro.rs +++ b/engine-tests/src/tests/repro.rs @@ -26,7 +26,7 @@ fn repro_GdASJ3KESs() { block_timestamp: 1_645_717_564_644_206_730, input_path: "src/tests/res/input_GdASJ3KESs.hex", evm_gas_used: 706_713, - near_gas_used: 115, + near_gas_used: 113, }); } @@ -51,7 +51,7 @@ fn repro_8ru7VEA() { block_timestamp: 1_648_829_935_343_349_589, input_path: "src/tests/res/input_8ru7VEA.hex", evm_gas_used: 1_732_181, - near_gas_used: 206, + near_gas_used: 202, }); } @@ -71,7 +71,7 @@ fn repro_FRcorNv() { block_timestamp: 1_650_960_438_774_745_116, input_path: "src/tests/res/input_FRcorNv.hex", evm_gas_used: 1_239_721, - near_gas_used: 168, + near_gas_used: 163, }); } @@ -88,7 +88,7 @@ fn repro_5bEgfRQ() { block_timestamp: 1_651_073_772_931_594_646, input_path: "src/tests/res/input_5bEgfRQ.hex", evm_gas_used: 6_414_105, - near_gas_used: 650, + near_gas_used: 508, }); } @@ -106,7 +106,7 @@ fn repro_D98vwmi() { block_timestamp: 1_651_753_443_421_003_245, input_path: "src/tests/res/input_D98vwmi.hex", evm_gas_used: 1_035_348, - near_gas_used: 169, + near_gas_used: 164, }); } @@ -125,7 +125,7 @@ fn repro_Emufid2() { block_timestamp: 1_662_118_048_636_713_538, input_path: "src/tests/res/input_Emufid2.hex", evm_gas_used: 1_156_364, - near_gas_used: 294, + near_gas_used: 293, }); } diff --git a/engine-tests/src/tests/standalone/sanity.rs b/engine-tests/src/tests/standalone/sanity.rs index 3638ac6d9..0e0f4cf8b 100644 --- a/engine-tests/src/tests/standalone/sanity.rs +++ b/engine-tests/src/tests/standalone/sanity.rs @@ -46,6 +46,7 @@ fn test_deploy_code() { None, u64::MAX, Vec::new(), + Vec::new(), &mut handler, ); diff --git a/engine-tests/src/tests/uniswap.rs b/engine-tests/src/tests/uniswap.rs index c24463539..c8e715d85 100644 --- a/engine-tests/src/tests/uniswap.rs +++ b/engine-tests/src/tests/uniswap.rs @@ -39,7 +39,7 @@ fn test_uniswap_input_multihop() { let (_amount_out, _evm_gas, profile) = context.exact_input(&tokens, INPUT_AMOUNT.into()); - assert_eq!(108, profile.all_gas() / 1_000_000_000_000); + assert_eq!(107, profile.all_gas() / 1_000_000_000_000); } #[test] @@ -53,8 +53,8 @@ fn test_uniswap_exact_output() { utils::assert_gas_bound(profile.all_gas(), 32); let wasm_fraction = 100 * profile.wasm_gas() / profile.all_gas(); assert!( - (40..=50).contains(&wasm_fraction), - "{wasm_fraction}% is not between 40% and 50%", + (37..=47).contains(&wasm_fraction), + "{wasm_fraction}% is not between 37% and 47%", ); let (_amount_in, profile) = @@ -62,8 +62,8 @@ fn test_uniswap_exact_output() { utils::assert_gas_bound(profile.all_gas(), 17); let wasm_fraction = 100 * profile.wasm_gas() / profile.all_gas(); assert!( - (40..=50).contains(&wasm_fraction), - "{wasm_fraction}% is not between 40% and 50%", + (37..=47).contains(&wasm_fraction), + "{wasm_fraction}% is not between 37% and 47%", ); } diff --git a/engine-tests/src/utils/solidity/erc20.rs b/engine-tests/src/utils/solidity/erc20.rs index eea55da60..cba2307ff 100644 --- a/engine-tests/src/utils/solidity/erc20.rs +++ b/engine-tests/src/utils/solidity/erc20.rs @@ -204,5 +204,6 @@ pub fn legacy_into_normalized_tx(tx: TransactionLegacy) -> NormalizedEthTransact value: tx.value, data: tx.data, access_list: Vec::new(), + authorization_list: Vec::new(), } } diff --git a/engine-transactions/src/backwards_compatibility.rs b/engine-transactions/src/backwards_compatibility.rs index 002562cfc..b5b7175db 100644 --- a/engine-transactions/src/backwards_compatibility.rs +++ b/engine-transactions/src/backwards_compatibility.rs @@ -49,6 +49,11 @@ impl EthTransactionKindAdapter { tx.transaction.to = None; } } + EthTransactionKind::Eip7702(tx) => { + if tx.transaction.to == ZERO_ADDRESS { + tx.transaction.to = None; + } + } } } diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs new file mode 100644 index 000000000..c0a57ed2c --- /dev/null +++ b/engine-transactions/src/eip_7702.rs @@ -0,0 +1,262 @@ +use crate::eip_2930::AccessTuple; +use crate::Error; +use aurora_engine_precompiles::secp256k1::ecrecover; +use aurora_engine_types::types::{Address, Wei}; +use aurora_engine_types::{Vec, H160, U256}; +use evm::executor::stack::Authorization; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Type indicator (per EIP-7702) +pub const TYPE_BYTE: u8 = 0x04; + +// EIP-7702 `MAGIC` number +pub const MAGIC: u8 = 0x5; + +/// The order of the secp256k1 curve, divided by two. Signatures that should be checked according +/// to EIP-2 should have an S value less than or equal to this. +/// +/// `57896044618658097711785492504343953926418782139537452191302581570759080747168` +pub const SECP256K1N_HALF: U256 = U256([ + 0xDFE9_2F46_681B_20A0, + 0x5D57_6E73_57A4_501D, + 0xFFFF_FFFF_FFFF_FFFF, + 0x7FFF_FFFF_FFFF_FFFF, +]); + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct AuthorizationTuple { + pub chain_id: U256, + pub address: H160, + pub nonce: u64, + pub parity: U256, + pub r: U256, + pub s: U256, +} + +impl Decodable for AuthorizationTuple { + fn decode(rlp: &Rlp<'_>) -> Result { + let chain_id = rlp.val_at(0)?; + let address = rlp.val_at(1)?; + let nonce = rlp.val_at(2)?; + let parity = rlp.val_at(3)?; + let r = rlp.val_at(4)?; + let s = rlp.val_at(5)?; + Ok(Self { + chain_id, + address, + nonce, + parity, + r, + s, + }) + } +} + +/// EIP-7702 transaction kind from the Prague hard fork. +/// +/// See [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) +/// for more details. +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Transaction7702 { + /// ID of chain which the transaction belongs. + pub chain_id: u64, + /// A monotonically increasing transaction counter for this sender + pub nonce: U256, + /// Determined by the sender and is optional. Priority Fee is also known as Miner Tip as it is + /// paid directly to block producers. + pub max_priority_fee_per_gas: U256, + /// Maximum amount the sender is willing to pay to get their transaction included in a block. + pub max_fee_per_gas: U256, + /// The maximum amount of gas the sender is willing to consume on a transaction. + pub gas_limit: U256, + /// The receiving address (`None` for the zero address). + pub to: Option
, + /// The amount of ETH to transfer. + pub value: Wei, + /// Arbitrary binary data for a contract call invocation. + pub data: Vec, + /// A list of addresses and storage keys that the transaction plans to access. + /// Accesses outside the list are possible, but become more expensive. + pub access_list: Vec, + /// A list of authorizations for EIP-7702 + pub authorization_list: Vec, +} + +impl Transaction7702 { + const TRANSACTION_FIELDS: usize = 10; + /// RLP encoding of the data for an unsigned message (used to make signature) + pub fn rlp_append_unsigned(&self, s: &mut RlpStream) { + self.rlp_append(s, Self::TRANSACTION_FIELDS); + } + + /// RLP encoding for a signed message (used to encode the transaction for sending to tx pool) + pub fn rlp_append_signed(&self, s: &mut RlpStream) { + self.rlp_append(s, SignedTransaction7702::TRANSACTION_FIELDS); + } + + fn rlp_append(&self, s: &mut RlpStream, list_len: usize) { + s.begin_list(list_len); + s.append(&self.chain_id); + s.append(&self.nonce); + s.append(&self.max_priority_fee_per_gas); + s.append(&self.max_fee_per_gas); + s.append(&self.gas_limit); + match self.to.as_ref() { + None => s.append(&""), + Some(address) => s.append(&address.raw()), + }; + s.append(&self.value.raw()); + s.append(&self.data); + s.begin_list(self.access_list.len()); + for tuple in &self.access_list { + s.begin_list(2); + s.append(&tuple.address); + s.begin_list(tuple.storage_keys.len()); + for key in &tuple.storage_keys { + s.append(key); + } + } + s.begin_list(self.authorization_list.len()); + for tuple in &self.authorization_list { + s.begin_list(6); + s.append(&tuple.chain_id); + s.append(&tuple.address); + s.append(&tuple.nonce); + s.append(&tuple.parity); + s.append(&tuple.r); + s.append(&tuple.s); + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct SignedTransaction7702 { + pub transaction: Transaction7702, + /// The parity (0 for even, 1 for odd) of the y-value of a secp256k1 signature. + pub parity: u8, + pub r: U256, + pub s: U256, +} + +impl SignedTransaction7702 { + const TRANSACTION_FIELDS: usize = 13; + + pub fn sender(&self) -> Result { + let mut rlp_stream = RlpStream::new(); + rlp_stream.append(&TYPE_BYTE); + self.transaction.rlp_append_unsigned(&mut rlp_stream); + let message_hash = aurora_engine_sdk::keccak(rlp_stream.as_raw()); + ecrecover( + message_hash, + &super::vrs_to_arr(self.parity, self.r, self.s), + ) + .map_err(|_e| Error::EcRecover) + } + + pub fn authorization_list(&self) -> Result, Error> { + if self.transaction.authorization_list.is_empty() { + return Err(Error::EmptyAuthorizationList); + } + let mut authorization_list = Vec::with_capacity(self.transaction.authorization_list.len()); + // According to EIP-7702 we should validate each authorization. We shouldn't skip any of them. + // And just put `is_valid` flag to `false` if any of them is invalid. It's related to + // gas calculation, as each `authorization_list` must be charged, even if it's invalid. + // The exception is invalid signature, as it's indicate wrong transaction. + for auth in &self.transaction.authorization_list { + // Validate the signature, as in tests it is possible to have invalid signatures values. + let v = auth.parity.0; + if !(v[0] < u64::from(u8::MAX) && v[1..4].iter().all(|&elem| elem == 0)) { + return Err(Error::InvalidAuthorizationSignature); + } + // Value `v` shouldn't be greater then 1 + let v = u8::try_from(v[0]).map_err(|_| Error::InvalidAuthorizationSignature)?; + if v > 1 { + return Err(Error::InvalidAuthorizationSignature); + } + // EIP-2 validation + if auth.s > SECP256K1N_HALF { + return Err(Error::InvalidAuthorizationSignature); + } + + // According to EIP-7702 step 1. validation, we should verify is `chain_id` is related to current network. + // We just ensure that `chain_id` is not zero, as it's possible different `chain_id` and + // `chain_id == 0` is related to Ethereum `mainnet`, that's not correct for Aurora. + let mut is_valid = !auth.chain_id.is_zero(); + + // 2. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) + let mut rlp_stream = RlpStream::new(); + rlp_stream.begin_list(4); + rlp_stream.append(&MAGIC); + rlp_stream.append(&auth.chain_id); + rlp_stream.append(&auth.address); + rlp_stream.append(&auth.nonce); + + let signature_hash = aurora_engine_sdk::keccak(rlp_stream.as_raw()); + + let auth_address = ecrecover(signature_hash, &super::vrs_to_arr(v, auth.r, auth.s)); + let auth_address = auth_address.unwrap_or_else(|_| { + is_valid = false; + Address::default() + }); + + // Validations steps 3-8 0f EIP-7702 provided by EVM itself. + authorization_list.push(Authorization { + authority: auth_address.raw(), + address: auth.address, + nonce: auth.nonce, + is_valid, + }); + } + Ok(authorization_list) + } +} + +impl Encodable for SignedTransaction7702 { + fn rlp_append(&self, s: &mut RlpStream) { + self.transaction.rlp_append_signed(s); + s.append(&self.parity); + s.append(&self.r); + s.append(&self.s); + } +} + +impl Decodable for SignedTransaction7702 { + fn decode(rlp: &Rlp<'_>) -> Result { + if rlp.item_count() != Ok(Self::TRANSACTION_FIELDS) { + return Err(DecoderError::RlpIncorrectListLen); + } + let chain_id = rlp.val_at(0)?; + let nonce = rlp.val_at(1)?; + let max_priority_fee_per_gas = rlp.val_at(2)?; + let max_fee_per_gas = rlp.val_at(3)?; + let gas_limit = rlp.val_at(4)?; + let to = super::rlp_extract_to(rlp, 5)?; + let value = Wei::new(rlp.val_at(6)?); + let data = rlp.val_at(7)?; + let access_list = rlp.list_at(8)?; + let authorization_list = rlp.list_at(9)?; + let parity = rlp.val_at(10)?; + let r = rlp.val_at(11)?; + let s = rlp.val_at(12)?; + Ok(Self { + transaction: Transaction7702 { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to, + value, + data, + access_list, + authorization_list, + }, + parity, + r, + s, + }) + } +} diff --git a/engine-transactions/src/lib.rs b/engine-transactions/src/lib.rs index 6ebd93286..97a8cca6b 100644 --- a/engine-transactions/src/lib.rs +++ b/engine-transactions/src/lib.rs @@ -11,12 +11,14 @@ use aurora_engine_types::types::{Address, Wei}; use aurora_engine_types::{vec, Vec, H160, U256}; use eip_2930::AccessTuple; +use evm::executor::stack::Authorization; use rlp::{Decodable, DecoderError, Rlp}; pub mod backwards_compatibility; pub mod eip_1559; pub mod eip_2930; pub mod eip_4844; +pub mod eip_7702; pub mod legacy; /// Typed Transaction Envelope (see `https://eips.ethereum.org/EIPS/eip-2718`) @@ -25,6 +27,7 @@ pub enum EthTransactionKind { Legacy(legacy::LegacyEthSignedTransaction), Eip2930(eip_2930::SignedTransaction2930), Eip1559(eip_1559::SignedTransaction1559), + Eip7702(eip_7702::SignedTransaction7702), } impl TryFrom<&[u8]> for EthTransactionKind { @@ -43,6 +46,10 @@ impl TryFrom<&[u8]> for EthTransactionKind { )?)) } else if bytes[0] == eip_4844::TYPE_BYTE { Err(Error::UnsupportedTransactionEip4844) + } else if bytes[0] == eip_7702::TYPE_BYTE { + Ok(Self::Eip7702(eip_7702::SignedTransaction7702::decode( + &Rlp::new(&bytes[1..]), + )?)) } else if bytes[0] <= 0x7f { Err(Error::UnknownTransactionType) } else if bytes[0] == 0xff { @@ -61,12 +68,16 @@ impl From<&EthTransactionKind> for Vec { EthTransactionKind::Legacy(tx) => { stream.append(tx); } + EthTransactionKind::Eip2930(tx) => { + stream.append(&eip_2930::TYPE_BYTE); + stream.append(tx); + } EthTransactionKind::Eip1559(tx) => { stream.append(&eip_1559::TYPE_BYTE); stream.append(tx); } - EthTransactionKind::Eip2930(tx) => { - stream.append(&eip_2930::TYPE_BYTE); + EthTransactionKind::Eip7702(tx) => { + stream.append(&eip_7702::TYPE_BYTE); stream.append(tx); } } @@ -87,13 +98,14 @@ pub struct NormalizedEthTransaction { pub value: Wei, pub data: Vec, pub access_list: Vec, + pub authorization_list: Vec, } impl TryFrom for NormalizedEthTransaction { type Error = Error; fn try_from(kind: EthTransactionKind) -> Result { - use EthTransactionKind::{Eip1559, Eip2930, Legacy}; + use EthTransactionKind::{Eip1559, Eip2930, Eip7702, Legacy}; Ok(match kind { Legacy(tx) => Self { address: tx.sender()?, @@ -106,6 +118,7 @@ impl TryFrom for NormalizedEthTransaction { value: tx.transaction.value, data: tx.transaction.data, access_list: vec![], + authorization_list: vec![], }, Eip2930(tx) => Self { address: tx.sender()?, @@ -118,6 +131,7 @@ impl TryFrom for NormalizedEthTransaction { value: tx.transaction.value, data: tx.transaction.data, access_list: tx.transaction.access_list, + authorization_list: vec![], }, Eip1559(tx) => Self { address: tx.sender()?, @@ -130,6 +144,20 @@ impl TryFrom for NormalizedEthTransaction { value: tx.transaction.value, data: tx.transaction.data, access_list: tx.transaction.access_list, + authorization_list: vec![], + }, + Eip7702(tx) => Self { + address: tx.sender()?, + chain_id: Some(tx.transaction.chain_id), + nonce: tx.transaction.nonce, + gas_limit: tx.transaction.gas_limit, + max_priority_fee_per_gas: tx.transaction.max_priority_fee_per_gas, + max_fee_per_gas: tx.transaction.max_fee_per_gas, + to: tx.transaction.to, + value: tx.transaction.value, + data: tx.transaction.data.clone(), + access_list: tx.transaction.access_list.clone(), + authorization_list: tx.authorization_list()?, }, }) } @@ -216,6 +244,8 @@ pub enum Error { #[cfg_attr(feature = "serde", serde(serialize_with = "decoder_err_to_str"))] RlpDecodeError(DecoderError), UnsupportedTransactionEip4844, + EmptyAuthorizationList, + InvalidAuthorizationSignature, } #[cfg(feature = "serde")] @@ -236,6 +266,8 @@ impl Error { Self::IntegerConversion => "ERR_INTEGER_CONVERSION", Self::RlpDecodeError(_) => "ERR_TX_RLP_DECODE", Self::UnsupportedTransactionEip4844 => "ERR_UNSUPPORTED_TX_EIP4844", + Self::EmptyAuthorizationList => "ERR_EMPTY_AUTHORIZATION_LIST", + Self::InvalidAuthorizationSignature => "ERR_INVALID_AUTHORIZATION_SIGNATURE", } } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 438500414..656a410d0 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -44,6 +44,7 @@ use aurora_engine_types::parameters::engine::FunctionCallArgsV2; use aurora_engine_types::types::EthGas; use core::cell::RefCell; use core::iter::once; +use evm::executor::stack::Authorization; /// Used as the first byte in the concatenation of data used to compute the blockhash. /// Could be useful in the future as a version byte, or to distinguish different types of blocks. @@ -507,7 +508,16 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { ) -> EngineResult { let origin = Address::new(self.origin()); let value = Wei::zero(); - self.deploy_code(origin, value, input, address, u64::MAX, Vec::new(), handler) + self.deploy_code( + origin, + value, + input, + address, + u64::MAX, + Vec::new(), + Vec::new(), + handler, + ) } #[allow(clippy::too_many_arguments)] @@ -518,7 +528,8 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input: Vec, address: Option
, gas_limit: u64, - access_list: Vec<(H160, Vec)>, // See EIP-2930 + access_list: Vec<(H160, Vec)>, // See EIP-2930 + authorization_list: Vec, // See EIP-7702 handler: &mut P, ) -> EngineResult { let pause_flags = EnginePrecompilesPauser::from_io(self.io).paused(); @@ -534,9 +545,14 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { ); let address = executor.create_address(scheme); let (exit_reason, return_value) = match scheme { - CreateScheme::Legacy { caller } => { - executor.transact_create(caller, value.raw(), input, gas_limit, access_list) - } + CreateScheme::Legacy { caller } => executor.transact_create( + caller, + value.raw(), + input, + gas_limit, + access_list, + authorization_list, + ), CreateScheme::Fixed(address) => executor.transact_create_fixed( origin.raw(), address, @@ -544,6 +560,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, gas_limit, access_list, + authorization_list, ), CreateScheme::Create2 { .. } => unreachable!(), }; @@ -583,6 +600,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, u64::MAX, Vec::new(), + Vec::new(), handler, ) } @@ -597,6 +615,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, u64::MAX, Vec::new(), + Vec::new(), handler, ) } @@ -611,7 +630,8 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { value: Wei, input: Vec, gas_limit: u64, - access_list: Vec<(H160, Vec)>, // See EIP-2930 + access_list: Vec<(H160, Vec)>, // See EIP-2930 + authorization_list: Vec, // See EIP-7702 handler: &mut P, ) -> EngineResult { let pause_flags = EnginePrecompilesPauser::from_io(self.io).paused(); @@ -626,6 +646,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, gas_limit, access_list, + authorization_list, ); let used_gas = executor.used_gas(); @@ -674,6 +695,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, executor_params.gas_limit, Vec::new(), + Vec::new(), ); status.into_result(result) } @@ -735,6 +757,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { Vec::new(), gas_limit, Vec::new(), + Vec::new(), handler, ) } @@ -808,6 +831,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { setup_receive_erc20_tokens_input(args, &recipient), u64::MAX, Vec::new(), // TODO: are there values we should put here? + Vec::new(), handler, ) .and_then(submit_result_or_err) @@ -1090,6 +1114,7 @@ pub fn submit_with_alt_modexp< .into_iter() .map(|a| (a.address, a.storage_keys)) .collect(); + let authorization_list = transaction.authorization_list; let result = if let Some(receiver) = transaction.to { engine.call( &sender, @@ -1098,6 +1123,7 @@ pub fn submit_with_alt_modexp< transaction.data, gas_limit, access_list, + authorization_list, handler, ) // TODO: charge for storage @@ -1110,6 +1136,7 @@ pub fn submit_with_alt_modexp< None, gas_limit, access_list, + authorization_list, handler, ) // TODO: charge for storage @@ -1174,6 +1201,7 @@ pub fn refund_on_error( input, u64::MAX, Vec::new(), + Vec::new(), handler, ) } else { @@ -1193,6 +1221,7 @@ pub fn refund_on_error( (exit_address.raw(), Vec::new()), (refund_address.raw(), Vec::new()), ], + Vec::new(), handler, ) } @@ -2504,6 +2533,7 @@ mod tests { value: Wei::default(), data: vec![], access_list: vec![], + authorization_list: vec![], }; let actual_result = engine .charge_gas(&origin, &transaction, None, None) @@ -2540,6 +2570,7 @@ mod tests { value: Wei::default(), data: vec![], access_list: vec![], + authorization_list: vec![], }; let actual_result = engine .charge_gas(&origin, &transaction, None, None) From 1e3e6fd058d99f74ac433816db2f283761c9e01b Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Mon, 7 Oct 2024 23:09:55 +0200 Subject: [PATCH 02/10] PR review changes. Added tests --- Cargo.lock | 8 +- Cargo.toml | 8 +- .../src/backwards_compatibility.rs | 6 +- engine-transactions/src/eip_7702.rs | 146 +++++++++++++++--- engine-transactions/src/lib.rs | 11 +- engine/src/engine.rs | 16 +- 6 files changed, 158 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e876b4ba..fec3241e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "evm" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" dependencies = [ "auto_impl", "environmental", @@ -2060,7 +2060,7 @@ dependencies = [ [[package]] name = "evm-core" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" dependencies = [ "parity-scale-codec", "primitive-types 0.12.2", @@ -2071,7 +2071,7 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" dependencies = [ "environmental", "evm-core", @@ -2082,7 +2082,7 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0.0-prague.1#cbdcc80b9a39b717a0c94f86c28de03386c3523d" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" dependencies = [ "auto_impl", "environmental", diff --git a/Cargo.toml b/Cargo.toml index 7c1e9b9cc..c5348f0f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,10 @@ byte-slice-cast = { version = "1", default-features = false } criterion = "0.5" digest = "0.10" ethabi = { version = "18", default-features = false } -evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false } -evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false, features = ["std"] } -evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false, features = ["std", "tracing"] } -evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0.0-prague.1", default-features = false, features = ["std", "tracing"] } +evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false } +evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false, features = ["std"] } +evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false, features = ["std", "tracing"] } +evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false, features = ["std", "tracing"] } fixed-hash = { version = "0.8", default-features = false } function_name = "0.3" git2 = "0.19" diff --git a/engine-transactions/src/backwards_compatibility.rs b/engine-transactions/src/backwards_compatibility.rs index b5b7175db..397dbb22b 100644 --- a/engine-transactions/src/backwards_compatibility.rs +++ b/engine-transactions/src/backwards_compatibility.rs @@ -49,10 +49,8 @@ impl EthTransactionKindAdapter { tx.transaction.to = None; } } - EthTransactionKind::Eip7702(tx) => { - if tx.transaction.to == ZERO_ADDRESS { - tx.transaction.to = None; - } + EthTransactionKind::Eip7702(_) => { + unreachable!(); } } } diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs index c0a57ed2c..2c2ae02c1 100644 --- a/engine-transactions/src/eip_7702.rs +++ b/engine-transactions/src/eip_7702.rs @@ -72,8 +72,8 @@ pub struct Transaction7702 { pub max_fee_per_gas: U256, /// The maximum amount of gas the sender is willing to consume on a transaction. pub gas_limit: U256, - /// The receiving address (`None` for the zero address). - pub to: Option
, + /// The receiving address. + pub to: Address, /// The amount of ETH to transfer. pub value: Wei, /// Arbitrary binary data for a contract call invocation. @@ -104,10 +104,7 @@ impl Transaction7702 { s.append(&self.max_priority_fee_per_gas); s.append(&self.max_fee_per_gas); s.append(&self.gas_limit); - match self.to.as_ref() { - None => s.append(&""), - Some(address) => s.append(&address.raw()), - }; + s.append(&self.to.raw()); s.append(&self.value.raw()); s.append(&self.data); s.begin_list(self.access_list.len()); @@ -156,7 +153,7 @@ impl SignedTransaction7702 { .map_err(|_e| Error::EcRecover) } - pub fn authorization_list(&self) -> Result, Error> { + pub fn authorization_list(&self) -> Result, Error> { if self.transaction.authorization_list.is_empty() { return Err(Error::EmptyAuthorizationList); } @@ -181,20 +178,20 @@ impl SignedTransaction7702 { return Err(Error::InvalidAuthorizationSignature); } - // According to EIP-7702 step 1. validation, we should verify is `chain_id` is related to current network. - // We just ensure that `chain_id` is not zero, as it's possible different `chain_id` and - // `chain_id == 0` is related to Ethereum `mainnet`, that's not correct for Aurora. - let mut is_valid = !auth.chain_id.is_zero(); + // According to EIP-7702 step 1. validation, we should verify is + // `chain_id = 0 || current_chain_id`. But we don't have `current_chain_id` here. And + // we should validate it in the context of the Engine submit execution. So, we just skip it. + let mut is_valid = true; // 2. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) let mut rlp_stream = RlpStream::new(); - rlp_stream.begin_list(4); - rlp_stream.append(&MAGIC); + rlp_stream.begin_list(3); rlp_stream.append(&auth.chain_id); rlp_stream.append(&auth.address); rlp_stream.append(&auth.nonce); - let signature_hash = aurora_engine_sdk::keccak(rlp_stream.as_raw()); + let message_bytes = [&[MAGIC], rlp_stream.as_raw()].concat(); + let signature_hash = aurora_engine_sdk::keccak(&message_bytes); let auth_address = ecrecover(signature_hash, &super::vrs_to_arr(v, auth.r, auth.s)); let auth_address = auth_address.unwrap_or_else(|_| { @@ -203,12 +200,15 @@ impl SignedTransaction7702 { }); // Validations steps 3-8 0f EIP-7702 provided by EVM itself. - authorization_list.push(Authorization { - authority: auth_address.raw(), - address: auth.address, - nonce: auth.nonce, - is_valid, - }); + authorization_list.push(( + auth.chain_id, + Authorization { + authority: auth_address.raw(), + address: auth.address, + nonce: auth.nonce, + is_valid, + }, + )); } Ok(authorization_list) } @@ -233,7 +233,7 @@ impl Decodable for SignedTransaction7702 { let max_priority_fee_per_gas = rlp.val_at(2)?; let max_fee_per_gas = rlp.val_at(3)?; let gas_limit = rlp.val_at(4)?; - let to = super::rlp_extract_to(rlp, 5)?; + let to = Address::new(rlp.val_at(5)?); let value = Wei::new(rlp.val_at(6)?); let data = rlp.val_at(7)?; let access_list = rlp.list_at(8)?; @@ -260,3 +260,107 @@ impl Decodable for SignedTransaction7702 { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use rlp::RlpStream; + + #[test] + fn test_authorization_tuple_decode() { + let chain_id = U256::from(1); + let address = H160::from_low_u64_be(0x1234); + let nonce = 1u64; + let parity = U256::from(0); + let r = U256::from(2); + let s = U256::from(3); + + let mut stream = RlpStream::new_list(6); + stream.append(&chain_id); + stream.append(&address); + stream.append(&nonce); + stream.append(&parity); + stream.append(&r); + stream.append(&s); + + let rlp = Rlp::new(stream.as_raw()); + let decoded: AuthorizationTuple = rlp.as_val().unwrap(); + + assert_eq!(decoded.chain_id, chain_id); + assert_eq!(decoded.address, address); + assert_eq!(decoded.nonce, nonce); + assert_eq!(decoded.parity, parity); + assert_eq!(decoded.r, r); + assert_eq!(decoded.s, s); + } + + #[test] + fn test_transaction7702_rlp_append_unsigned() { + let tx = Transaction7702 { + chain_id: 1, + nonce: U256::from(1), + max_priority_fee_per_gas: U256::from(2), + max_fee_per_gas: U256::from(3), + gas_limit: U256::from(4), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(U256::from(5)), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: U256::from(1), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: U256::from(0), + r: U256::from(2), + s: U256::from(3), + }], + }; + + let mut stream = RlpStream::new(); + tx.rlp_append_unsigned(&mut stream); + + let rlp = Rlp::new(stream.as_raw()); + assert_eq!( + rlp.item_count().unwrap(), + Transaction7702::TRANSACTION_FIELDS + ); + } + + #[test] + fn test_signed_transaction7702_rlp_encode_decode() { + let tx = Transaction7702 { + chain_id: 1, + nonce: U256::from(1), + max_priority_fee_per_gas: U256::from(2), + max_fee_per_gas: U256::from(3), + gas_limit: U256::from(4), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(U256::from(5)), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: U256::from(1), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: U256::from(0), + r: U256::from(2), + s: U256::from(3), + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: U256::from(7), + s: U256::from(8), + }; + + let mut stream = RlpStream::new(); + signed_tx.rlp_append(&mut stream); + + let rlp = Rlp::new(stream.as_raw()); + let decoded: SignedTransaction7702 = rlp.as_val().unwrap(); + + assert_eq!(decoded, signed_tx); + } +} diff --git a/engine-transactions/src/lib.rs b/engine-transactions/src/lib.rs index 97a8cca6b..e36ef1cc6 100644 --- a/engine-transactions/src/lib.rs +++ b/engine-transactions/src/lib.rs @@ -98,7 +98,8 @@ pub struct NormalizedEthTransaction { pub value: Wei, pub data: Vec, pub access_list: Vec, - pub authorization_list: Vec, + // Contains additional information - `chain_id` for each authorization item + pub authorization_list: Vec<(U256, Authorization)>, } impl TryFrom for NormalizedEthTransaction { @@ -153,7 +154,7 @@ impl TryFrom for NormalizedEthTransaction { gas_limit: tx.transaction.gas_limit, max_priority_fee_per_gas: tx.transaction.max_priority_fee_per_gas, max_fee_per_gas: tx.transaction.max_fee_per_gas, - to: tx.transaction.to, + to: Some(tx.transaction.to), value: tx.transaction.value, data: tx.transaction.data.clone(), access_list: tx.transaction.access_list.clone(), @@ -310,7 +311,7 @@ fn vrs_to_arr(v: u8, r: U256, s: U256) -> [u8; 65] { #[cfg(test)] mod tests { use super::{Error, EthTransactionKind}; - use crate::{eip_1559, eip_2930}; + use crate::{eip_1559, eip_2930, eip_7702}; #[test] fn test_try_parse_empty_input() { @@ -333,5 +334,9 @@ mod tests { EthTransactionKind::try_from([0x80].as_ref()), Err(Error::RlpDecodeError(_)) )); + assert!(matches!( + EthTransactionKind::try_from([eip_7702::TYPE_BYTE].as_ref()), + Err(Error::RlpDecodeError(_)) + )); } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 656a410d0..fdf1735ff 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -1052,6 +1052,20 @@ pub fn submit_with_alt_modexp< tx.try_into() .map_err(|_e| EngineErrorKind::InvalidSignature)? }; + + // EIP-7702: validate `chain_id` for each item of `authorization_list` + let mut authorization_list = Vec::new(); + if CONFIG.has_authorization_list { + let mut pre_authorization_list = transaction.authorization_list.clone(); + let current_chain_id = U256::from(state.chain_id); + for auth in &mut pre_authorization_list { + if auth.0 != current_chain_id { + auth.1.is_valid = false; + } + authorization_list.push(auth.1.clone()); + } + } + // Retrieve the signer of the transaction: let sender = transaction.address; @@ -1114,7 +1128,7 @@ pub fn submit_with_alt_modexp< .into_iter() .map(|a| (a.address, a.storage_keys)) .collect(); - let authorization_list = transaction.authorization_list; + let result = if let Some(receiver) = transaction.to { engine.call( &sender, From 976248fed50c0526337ea8e945021a72a79ca66e Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Tue, 8 Oct 2024 09:14:05 +0200 Subject: [PATCH 03/10] Changed auth-list chain_id validation --- Cargo.lock | 8 ++-- Cargo.toml | 8 ++-- engine-tests/src/tests/standalone/sanity.rs | 1 - engine-transactions/src/eip_7702.rs | 25 ++++++------ engine-transactions/src/lib.rs | 2 +- engine/src/engine.rs | 42 +++------------------ 6 files changed, 27 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fec3241e6..a8ae43086 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "evm" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.8#f0d9742076541965b45c3d5ff5de8402eb7ff226" dependencies = [ "auto_impl", "environmental", @@ -2060,7 +2060,7 @@ dependencies = [ [[package]] name = "evm-core" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.8#f0d9742076541965b45c3d5ff5de8402eb7ff226" dependencies = [ "parity-scale-codec", "primitive-types 0.12.2", @@ -2071,7 +2071,7 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.8#f0d9742076541965b45c3d5ff5de8402eb7ff226" dependencies = [ "environmental", "evm-core", @@ -2082,7 +2082,7 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.45.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.7#1476aa9a7d5da11748827443cbeb4658c02c9c62" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v1.0-opt.8#f0d9742076541965b45c3d5ff5de8402eb7ff226" dependencies = [ "auto_impl", "environmental", diff --git a/Cargo.toml b/Cargo.toml index c5348f0f8..f677958d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,10 @@ byte-slice-cast = { version = "1", default-features = false } criterion = "0.5" digest = "0.10" ethabi = { version = "18", default-features = false } -evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false } -evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false, features = ["std"] } -evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false, features = ["std", "tracing"] } -evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.7", default-features = false, features = ["std", "tracing"] } +evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.8", default-features = false } +evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.8", default-features = false, features = ["std"] } +evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.8", default-features = false, features = ["std", "tracing"] } +evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v1.0-opt.8", default-features = false, features = ["std", "tracing"] } fixed-hash = { version = "0.8", default-features = false } function_name = "0.3" git2 = "0.19" diff --git a/engine-tests/src/tests/standalone/sanity.rs b/engine-tests/src/tests/standalone/sanity.rs index 0e0f4cf8b..3638ac6d9 100644 --- a/engine-tests/src/tests/standalone/sanity.rs +++ b/engine-tests/src/tests/standalone/sanity.rs @@ -46,7 +46,6 @@ fn test_deploy_code() { None, u64::MAX, Vec::new(), - Vec::new(), &mut handler, ); diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs index 2c2ae02c1..950d4f11e 100644 --- a/engine-transactions/src/eip_7702.rs +++ b/engine-transactions/src/eip_7702.rs @@ -153,10 +153,11 @@ impl SignedTransaction7702 { .map_err(|_e| Error::EcRecover) } - pub fn authorization_list(&self) -> Result, Error> { + pub fn authorization_list(&self) -> Result, Error> { if self.transaction.authorization_list.is_empty() { return Err(Error::EmptyAuthorizationList); } + let current_tx_chain_id = U256::from(self.transaction.chain_id); let mut authorization_list = Vec::with_capacity(self.transaction.authorization_list.len()); // According to EIP-7702 we should validate each authorization. We shouldn't skip any of them. // And just put `is_valid` flag to `false` if any of them is invalid. It's related to @@ -179,9 +180,10 @@ impl SignedTransaction7702 { } // According to EIP-7702 step 1. validation, we should verify is - // `chain_id = 0 || current_chain_id`. But we don't have `current_chain_id` here. And - // we should validate it in the context of the Engine submit execution. So, we just skip it. - let mut is_valid = true; + // `chain_id = 0 || current_chain_id`. + // AS `current_chain_id` we used `transaction.chain_id` as we will validate `chain_id` in + // Engine `submit_transaction` method. + let mut is_valid = auth.chain_id.is_zero() || auth.chain_id == current_tx_chain_id; // 2. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) let mut rlp_stream = RlpStream::new(); @@ -200,15 +202,12 @@ impl SignedTransaction7702 { }); // Validations steps 3-8 0f EIP-7702 provided by EVM itself. - authorization_list.push(( - auth.chain_id, - Authorization { - authority: auth_address.raw(), - address: auth.address, - nonce: auth.nonce, - is_valid, - }, - )); + authorization_list.push(Authorization { + authority: auth_address.raw(), + address: auth.address, + nonce: auth.nonce, + is_valid, + }); } Ok(authorization_list) } diff --git a/engine-transactions/src/lib.rs b/engine-transactions/src/lib.rs index e36ef1cc6..054a641b2 100644 --- a/engine-transactions/src/lib.rs +++ b/engine-transactions/src/lib.rs @@ -99,7 +99,7 @@ pub struct NormalizedEthTransaction { pub data: Vec, pub access_list: Vec, // Contains additional information - `chain_id` for each authorization item - pub authorization_list: Vec<(U256, Authorization)>, + pub authorization_list: Vec, } impl TryFrom for NormalizedEthTransaction { diff --git a/engine/src/engine.rs b/engine/src/engine.rs index fdf1735ff..dec1f061d 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -508,16 +508,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { ) -> EngineResult { let origin = Address::new(self.origin()); let value = Wei::zero(); - self.deploy_code( - origin, - value, - input, - address, - u64::MAX, - Vec::new(), - Vec::new(), - handler, - ) + self.deploy_code(origin, value, input, address, u64::MAX, Vec::new(), handler) } #[allow(clippy::too_many_arguments)] @@ -528,8 +519,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input: Vec, address: Option
, gas_limit: u64, - access_list: Vec<(H160, Vec)>, // See EIP-2930 - authorization_list: Vec, // See EIP-7702 + access_list: Vec<(H160, Vec)>, // See EIP-2930 handler: &mut P, ) -> EngineResult { let pause_flags = EnginePrecompilesPauser::from_io(self.io).paused(); @@ -545,14 +535,9 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { ); let address = executor.create_address(scheme); let (exit_reason, return_value) = match scheme { - CreateScheme::Legacy { caller } => executor.transact_create( - caller, - value.raw(), - input, - gas_limit, - access_list, - authorization_list, - ), + CreateScheme::Legacy { caller } => { + executor.transact_create(caller, value.raw(), input, gas_limit, access_list) + } CreateScheme::Fixed(address) => executor.transact_create_fixed( origin.raw(), address, @@ -560,7 +545,6 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, gas_limit, access_list, - authorization_list, ), CreateScheme::Create2 { .. } => unreachable!(), }; @@ -1053,19 +1037,6 @@ pub fn submit_with_alt_modexp< .map_err(|_e| EngineErrorKind::InvalidSignature)? }; - // EIP-7702: validate `chain_id` for each item of `authorization_list` - let mut authorization_list = Vec::new(); - if CONFIG.has_authorization_list { - let mut pre_authorization_list = transaction.authorization_list.clone(); - let current_chain_id = U256::from(state.chain_id); - for auth in &mut pre_authorization_list { - if auth.0 != current_chain_id { - auth.1.is_valid = false; - } - authorization_list.push(auth.1.clone()); - } - } - // Retrieve the signer of the transaction: let sender = transaction.address; @@ -1137,7 +1108,7 @@ pub fn submit_with_alt_modexp< transaction.data, gas_limit, access_list, - authorization_list, + transaction.authorization_list, handler, ) // TODO: charge for storage @@ -1150,7 +1121,6 @@ pub fn submit_with_alt_modexp< None, gas_limit, access_list, - authorization_list, handler, ) // TODO: charge for storage From d6d2eebad2ad98240cf810e0d6d06871f61a3431 Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Thu, 9 Jan 2025 14:17:47 +0100 Subject: [PATCH 04/10] Changed gas cost for repro_Emufid2 --- engine-tests/src/tests/repro.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine-tests/src/tests/repro.rs b/engine-tests/src/tests/repro.rs index 1ffcfb2aa..151a887fc 100644 --- a/engine-tests/src/tests/repro.rs +++ b/engine-tests/src/tests/repro.rs @@ -125,7 +125,7 @@ fn repro_Emufid2() { block_timestamp: 1_662_118_048_636_713_538, input_path: "src/tests/res/input_Emufid2.hex", evm_gas_used: 1_156_364, - near_gas_used: 293, + near_gas_used: 294, }); } From c922ea152155d61a2688d15315180ccf1a0da1bf Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Wed, 15 Jan 2025 00:08:43 +0100 Subject: [PATCH 05/10] EIP-7702 validation changes --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 8 ++++---- engine-transactions/src/eip_7702.rs | 26 ++++++++++++++------------ engine-transactions/src/lib.rs | 2 -- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a482619e4..0ba92f14b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1686,8 +1686,8 @@ dependencies = [ [[package]] name = "evm" -version = "0.46.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.1#e4ee27342f0bac34b376d031953f697a3437dbdd" +version = "0.46.3" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.2#d0d9e01892c5c0a40cf12153188079e3c461e855" dependencies = [ "auto_impl", "environmental", @@ -1706,8 +1706,8 @@ dependencies = [ [[package]] name = "evm-core" -version = "0.46.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.1#e4ee27342f0bac34b376d031953f697a3437dbdd" +version = "0.46.3" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.2#d0d9e01892c5c0a40cf12153188079e3c461e855" dependencies = [ "parity-scale-codec", "primitive-types 0.13.1", @@ -1717,8 +1717,8 @@ dependencies = [ [[package]] name = "evm-gasometer" -version = "0.46.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.1#e4ee27342f0bac34b376d031953f697a3437dbdd" +version = "0.46.3" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.2#d0d9e01892c5c0a40cf12153188079e3c461e855" dependencies = [ "environmental", "evm-core", @@ -1728,8 +1728,8 @@ dependencies = [ [[package]] name = "evm-runtime" -version = "0.46.2" -source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.1#e4ee27342f0bac34b376d031953f697a3437dbdd" +version = "0.46.3" +source = "git+https://github.com/aurora-is-near/sputnikvm.git?tag=v0.47.0-beta.2#d0d9e01892c5c0a40cf12153188079e3c461e855" dependencies = [ "auto_impl", "environmental", diff --git a/Cargo.toml b/Cargo.toml index b2831f0bd..eb9d987c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,10 @@ bstr = "1" byte-slice-cast = { version = "1", default-features = false } criterion = "0.5" ethabi = { version = "18", default-features = false } -evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.1", default-features = false } -evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.1", default-features = false, features = ["std"] } -evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.1", default-features = false, features = ["std", "tracing"] } -evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.1", default-features = false, features = ["std", "tracing"] } +evm = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.2", default-features = false } +evm-core = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.2", default-features = false, features = ["std"] } +evm-gasometer = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.2", default-features = false, features = ["std", "tracing"] } +evm-runtime = { git = "https://github.com/aurora-is-near/sputnikvm.git", tag = "v0.47.0-beta.2", default-features = false, features = ["std", "tracing"] } fixed-hash = { version = "0.8", default-features = false } function_name = "0.3" git2 = "0.20" diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs index 950d4f11e..5793b0fb8 100644 --- a/engine-transactions/src/eip_7702.rs +++ b/engine-transactions/src/eip_7702.rs @@ -162,30 +162,32 @@ impl SignedTransaction7702 { // According to EIP-7702 we should validate each authorization. We shouldn't skip any of them. // And just put `is_valid` flag to `false` if any of them is invalid. It's related to // gas calculation, as each `authorization_list` must be charged, even if it's invalid. - // The exception is invalid signature, as it's indicate wrong transaction. for auth in &self.transaction.authorization_list { + // According to EIP-7702 step 1. validation, we should verify is + // `chain_id = 0 || current_chain_id`. + // AS `current_chain_id` we used `transaction.chain_id` as we will validate `chain_id` in + // Engine `submit_transaction` method. + let mut is_valid = auth.chain_id.is_zero() || auth.chain_id == current_tx_chain_id; + + // Step 2 - validation logic inside EVM itself. + // Step 3. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) + // Validate the signature, as in tests it is possible to have invalid signatures values. let v = auth.parity.0; if !(v[0] < u64::from(u8::MAX) && v[1..4].iter().all(|&elem| elem == 0)) { - return Err(Error::InvalidAuthorizationSignature); + is_valid = false; } + // `V` must be: `v < u8::MAX`. As we checked it early, then `map_err` impossible case. + let v = u8::try_from(v[0]).map_err(|_| Error::InvalidV)?; // Value `v` shouldn't be greater then 1 - let v = u8::try_from(v[0]).map_err(|_| Error::InvalidAuthorizationSignature)?; if v > 1 { - return Err(Error::InvalidAuthorizationSignature); + is_valid = false; } // EIP-2 validation if auth.s > SECP256K1N_HALF { - return Err(Error::InvalidAuthorizationSignature); + is_valid = false; } - // According to EIP-7702 step 1. validation, we should verify is - // `chain_id = 0 || current_chain_id`. - // AS `current_chain_id` we used `transaction.chain_id` as we will validate `chain_id` in - // Engine `submit_transaction` method. - let mut is_valid = auth.chain_id.is_zero() || auth.chain_id == current_tx_chain_id; - - // 2. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) let mut rlp_stream = RlpStream::new(); rlp_stream.begin_list(3); rlp_stream.append(&auth.chain_id); diff --git a/engine-transactions/src/lib.rs b/engine-transactions/src/lib.rs index 18a200288..4fc6e9a29 100644 --- a/engine-transactions/src/lib.rs +++ b/engine-transactions/src/lib.rs @@ -239,7 +239,6 @@ pub enum Error { RlpDecodeError(DecoderError), UnsupportedTransactionEip4844, EmptyAuthorizationList, - InvalidAuthorizationSignature, } #[cfg(feature = "serde")] @@ -261,7 +260,6 @@ impl Error { Self::RlpDecodeError(_) => "ERR_TX_RLP_DECODE", Self::UnsupportedTransactionEip4844 => "ERR_UNSUPPORTED_TX_EIP4844", Self::EmptyAuthorizationList => "ERR_EMPTY_AUTHORIZATION_LIST", - Self::InvalidAuthorizationSignature => "ERR_INVALID_AUTHORIZATION_SIGNATURE", } } } From 6618d952d5fec79112f7044e5777aebc4f357e5f Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Thu, 16 Jan 2025 16:00:31 +0100 Subject: [PATCH 06/10] Added tests: test_eip_7702 --- engine-tests/src/tests/transaction.rs | 159 +++++++++++++++++- engine-tests/src/utils/mod.rs | 45 +++-- .../src/backwards_compatibility.rs | 2 +- engine/src/engine.rs | 2 +- 4 files changed, 194 insertions(+), 14 deletions(-) diff --git a/engine-tests/src/tests/transaction.rs b/engine-tests/src/tests/transaction.rs index 13282da3f..e6347ca9c 100644 --- a/engine-tests/src/tests/transaction.rs +++ b/engine-tests/src/tests/transaction.rs @@ -5,9 +5,12 @@ use crate::prelude::Wei; use crate::prelude::{H256, U256}; use crate::utils; use aurora_engine::parameters::SubmitResult; -use aurora_engine_transactions::eip_2930; use aurora_engine_transactions::eip_2930::Transaction2930; +use aurora_engine_transactions::eip_7702::{AuthorizationTuple, Transaction7702}; +use aurora_engine_transactions::{eip_2930, eip_7702}; use aurora_engine_types::borsh::BorshDeserialize; +use aurora_engine_types::types::Address; +use aurora_engine_types::H160; use std::convert::TryFrom; use std::iter; @@ -22,6 +25,15 @@ const CONTRACT_BALANCE: Wei = Wei::new_u64(0x0de0b6b3a7640000); const EXAMPLE_TX_HEX: &str = "02f8c101010a8207d0833d090094cccccccccccccccccccccccccccccccccccccccc8000f85bf85994ccccccccccccccccccccccccccccccccccccccccf842a00000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000180a0d671815898b8dd34321adbba4cb6a57baa7017323c26946f3719b00e70c755c2a03528b9efe3be57ea65a933d1e6bbf3b7d0c78830138883c1201e0c641fee6464"; +/* + PUSH20 0xd16292912e440956828f1a847ca6efc8412f45b6 [0x73d16292912e440956828f1a847ca6efc8412f45b6] + EXTCODESIZE [0x3B] + PUSH1 0x00 [0x6000] + SSTORE [0x55] + STOP [0x00] +*/ +const CONTRACT_CODE_EIP7702: &str = "73d16292912e440956828f1a847ca6efc8412f45b63B60005500"; + // Test taken from https://github.com/ethereum/tests/blob/develop/GeneralStateTests/stExample/eip1559.json // TODO(#170): generally support Ethereum tests #[test] @@ -161,6 +173,129 @@ fn test_access_list_tx_encoding_decoding() { ); } +#[test] +fn test_eip_7702_tx_encoding_decoding() { + let secret_key = example_signer().secret_key; + let transaction = eip7702_transaction(0); + + let signed_tx = utils::sign_eip_7702_transaction(transaction, &secret_key); + let tx_bytes: Vec = iter::once(eip_7702::TYPE_BYTE) + .chain(rlp::encode(&signed_tx)) + .collect(); + + let decoded_tx = match EthTransactionKind::try_from(tx_bytes.as_slice()) { + Ok(EthTransactionKind::Eip7702(tx)) => tx, + Ok(_) => panic!("Unexpected transaction type"), + Err(e) => panic!("Transaction parsing failed: {e:?}"), + }; + + assert_eq!(signed_tx, decoded_tx); + assert_eq!( + signed_tx.sender().unwrap(), + utils::address_from_secret_key(&secret_key) + ); +} + +#[test] +fn test_eip_7702_success() { + // 0xa52a8a2229e3c512d6ed27b6e6e7d39958ca9fb3, + let mut runner = utils::deploy_runner(); + let signer = example_signer(); + let signer_address = utils::address_from_secret_key(&signer.secret_key); + let contract_address = utils::address_from_hex(CONTRACT_ADDRESS); + let contract_code = hex::decode(CONTRACT_CODE_EIP7702).unwrap(); + + runner.create_address(signer_address, INITIAL_BALANCE, signer.nonce.into()); + runner.create_address_with_code( + contract_address, + CONTRACT_BALANCE, + CONTRACT_NONCE.into(), + contract_code.clone(), + ); + + let mut transaction = eip7702_transaction(0); + transaction.chain_id = runner.chain_id; + let signed_tx = utils::sign_eip_7702_transaction(transaction, &signer.secret_key); + let tx_bytes: Vec = iter::once(eip_7702::TYPE_BYTE) + .chain(rlp::encode(&signed_tx)) + .collect(); + + let sender = "relay.aurora"; + let outcome = runner.call(utils::SUBMIT, sender, tx_bytes).unwrap(); + // Unwrapping execution results validates outcome + let result = SubmitResult::try_from_slice(&outcome.return_data.as_value().unwrap()).unwrap(); + + let delegated_designator = Address::decode("d16292912e440956828f1a847ca6efc8412f45b6").unwrap(); + + assert_eq!(result.gas_used, 68206); + assert_eq!(runner.get_nonce(signer_address), (signer.nonce + 1).into()); + assert_eq!(runner.get_balance(contract_address), CONTRACT_BALANCE); + assert_eq!(runner.get_nonce(contract_address), CONTRACT_NONCE.into()); + assert_eq!(runner.get_code(contract_address), contract_code); + // `EXTCODESIZE` should return size of `EF01` = 2 for delegated designator + assert_eq!( + runner.get_storage(contract_address, H256::zero()), + H256::from(H160::from_low_u64_be(2)) + ); + + // Authority address should increase Nonce + assert_eq!(runner.get_nonce(delegated_designator), 1.into()); + // Get delegated designator address + assert_eq!( + hex::encode(runner.get_code(delegated_designator)), + "ef0100cccccccccccccccccccccccccccccccccccccccc" + ); +} + +#[test] +fn test_eip_7702_wrong_auth_nonce() { + let mut runner = utils::deploy_runner(); + let signer = example_signer(); + let signer_address = utils::address_from_secret_key(&signer.secret_key); + let contract_address = utils::address_from_hex(CONTRACT_ADDRESS); + // Contract for auth address: 0xa52a8a2229e3c512d6ed27b6e6e7d39958ca9fb3 + let contract_code = + hex::decode("73a52a8a2229e3c512d6ed27b6e6e7d39958ca9fb33B60005500").unwrap(); + + runner.create_address(signer_address, INITIAL_BALANCE, signer.nonce.into()); + runner.create_address_with_code( + contract_address, + CONTRACT_BALANCE, + CONTRACT_NONCE.into(), + contract_code.clone(), + ); + + let mut transaction = eip7702_transaction(10); + transaction.chain_id = runner.chain_id; + let signed_tx = utils::sign_eip_7702_transaction(transaction, &signer.secret_key); + let tx_bytes: Vec = iter::once(eip_7702::TYPE_BYTE) + .chain(rlp::encode(&signed_tx)) + .collect(); + + let sender = "relay.aurora"; + let outcome = runner.call(utils::SUBMIT, sender, tx_bytes).unwrap(); + // Unwrapping execution results validates outcome + let result = SubmitResult::try_from_slice(&outcome.return_data.as_value().unwrap()).unwrap(); + + let delegated_designator = Address::decode("a52a8a2229e3c512d6ed27b6e6e7d39958ca9fb3").unwrap(); + + assert_eq!(result.gas_used, 50806); + assert_eq!(runner.get_nonce(signer_address), (signer.nonce + 1).into()); + assert_eq!(runner.get_balance(contract_address), CONTRACT_BALANCE); + assert_eq!(runner.get_nonce(contract_address), CONTRACT_NONCE.into()); + assert_eq!(runner.get_code(contract_address), contract_code); + // `EXTCODESIZE` should return zero, as `authorization_list` failed validation + assert_eq!( + runner.get_storage(contract_address, H256::zero()), + H256::zero() + ); + + // Authority address should increase Nonce + assert_eq!(runner.get_nonce(delegated_designator), 0.into()); + // Get delegated designator address: in that particular case it should be empty + assert!(runner.get_code(delegated_designator).is_empty()); +} + fn encode_tx(signed_tx: &SignedTransaction1559) -> Vec { iter::once(eip_1559::TYPE_BYTE) .chain(rlp::encode(signed_tx)) @@ -177,6 +312,28 @@ fn example_signer() -> utils::Signer { } } +fn eip7702_transaction(auth_chain_id: u64) -> Transaction7702 { + Transaction7702 { + chain_id: 1, + nonce: INITIAL_NONCE.into(), + gas_limit: U256::from(0x3d0900), + max_fee_per_gas: U256::from(0x07d0), + max_priority_fee_per_gas: U256::from(0x0a), + to: utils::address_from_hex(CONTRACT_ADDRESS), + value: Wei::zero(), + data: vec![], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: auth_chain_id.into(), + address: utils::address_from_hex(CONTRACT_ADDRESS).raw(), + nonce: 0, + parity: 1.into(), + r: 2.into(), + s: 3.into(), + }], + } +} + fn example_transaction() -> Transaction1559 { Transaction1559 { chain_id: 1, diff --git a/engine-tests/src/utils/mod.rs b/engine-tests/src/utils/mod.rs index a1c1ec6b5..3cc4280c6 100644 --- a/engine-tests/src/utils/mod.rs +++ b/engine-tests/src/utils/mod.rs @@ -1,5 +1,17 @@ +#[cfg(not(feature = "ext-connector"))] +use crate::prelude::parameters::InitCallArgs; +use crate::prelude::parameters::{StartHashchainArgs, SubmitResult, TransactionStatus}; +use crate::prelude::transactions::{ + eip_1559::{self, SignedTransaction1559, Transaction1559}, + eip_2930::{self, SignedTransaction2930, Transaction2930}, + legacy::{LegacyEthSignedTransaction, TransactionLegacy}, +}; +use crate::prelude::{sdk, Address, Wei, H256, U256}; +use crate::utils::solidity::{ContractConstructor, DeployedContract}; use aurora_engine::engine::{EngineError, EngineErrorKind, GasPaymentError}; use aurora_engine::parameters::{SubmitArgs, ViewCallArgs}; +use aurora_engine_transactions::eip_7702; +use aurora_engine_transactions::eip_7702::{SignedTransaction7702, Transaction7702}; use aurora_engine_types::account_id::AccountId; use aurora_engine_types::borsh::BorshDeserialize; #[cfg(not(feature = "ext-connector"))] @@ -25,17 +37,6 @@ use rlp::RlpStream; use std::borrow::Cow; use std::sync::Arc; -#[cfg(not(feature = "ext-connector"))] -use crate::prelude::parameters::InitCallArgs; -use crate::prelude::parameters::{StartHashchainArgs, SubmitResult, TransactionStatus}; -use crate::prelude::transactions::{ - eip_1559::{self, SignedTransaction1559, Transaction1559}, - eip_2930::{self, SignedTransaction2930, Transaction2930}, - legacy::{LegacyEthSignedTransaction, TransactionLegacy}, -}; -use crate::prelude::{sdk, Address, Wei, H256, U256}; -use crate::utils::solidity::{ContractConstructor, DeployedContract}; - pub const DEFAULT_AURORA_ACCOUNT_ID: &str = "aurora"; pub const SUBMIT: &str = "submit"; pub const SUBMIT_WITH_ARGS: &str = "submit_with_args"; @@ -945,6 +946,28 @@ pub fn sign_eip_1559_transaction( } } +pub fn sign_eip_7702_transaction( + tx: Transaction7702, + secret_key: &SecretKey, +) -> SignedTransaction7702 { + let mut rlp_stream = RlpStream::new(); + rlp_stream.append(&eip_7702::TYPE_BYTE); + tx.rlp_append_unsigned(&mut rlp_stream); + let message_hash = sdk::keccak(rlp_stream.as_raw()); + let message = Message::parse_slice(message_hash.as_bytes()).unwrap(); + + let (signature, recovery_id) = libsecp256k1::sign(&message, secret_key); + let r = U256::from_big_endian(&signature.r.b32()); + let s = U256::from_big_endian(&signature.s.b32()); + + SignedTransaction7702 { + transaction: tx, + parity: recovery_id.serialize(), + r, + s, + } +} + pub fn address_from_secret_key(sk: &SecretKey) -> Address { let pk = PublicKey::from_secret_key(sk); let hash = sdk::keccak(&pk.serialize()[1..]); diff --git a/engine-transactions/src/backwards_compatibility.rs b/engine-transactions/src/backwards_compatibility.rs index 397dbb22b..eeb5135d7 100644 --- a/engine-transactions/src/backwards_compatibility.rs +++ b/engine-transactions/src/backwards_compatibility.rs @@ -50,7 +50,7 @@ impl EthTransactionKindAdapter { } } EthTransactionKind::Eip7702(_) => { - unreachable!(); + // For Prague hard fork `tx.transaction.to` can't be `None` } } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 367785f75..18286f6ba 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -424,7 +424,7 @@ pub struct Engine<'env, I: IO, E: Env, M = AuroraModExp> { modexp_algorithm: PhantomData, } -pub(crate) const CONFIG: &Config = &Config::cancun(); +pub(crate) const CONFIG: &Config = &Config::prague(); impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { pub fn new( From 07c1498b65b7fdd7d977e474861c2ca29d4df438 Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Thu, 16 Jan 2025 19:26:09 +0100 Subject: [PATCH 07/10] Extend EIP-7702 tests --- engine-transactions/src/eip_7702.rs | 272 ++++++++++++++++++++++++---- 1 file changed, 240 insertions(+), 32 deletions(-) diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs index 5793b0fb8..1e2495cc0 100644 --- a/engine-transactions/src/eip_7702.rs +++ b/engine-transactions/src/eip_7702.rs @@ -171,18 +171,13 @@ impl SignedTransaction7702 { // Step 2 - validation logic inside EVM itself. // Step 3. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) - // Validate the signature, as in tests it is possible to have invalid signatures values. - let v = auth.parity.0; - if !(v[0] < u64::from(u8::MAX) && v[1..4].iter().all(|&elem| elem == 0)) { - is_valid = false; - } - // `V` must be: `v < u8::MAX`. As we checked it early, then `map_err` impossible case. - let v = u8::try_from(v[0]).map_err(|_| Error::InvalidV)?; + // Value `v` shouldn't be greater then 1 - if v > 1 { + if auth.parity > U256::from(1) { is_valid = false; } + let v = u8::try_from(auth.parity.as_u64()).map_err(|_| Error::InvalidV)?; // EIP-2 validation if auth.s > SECP256K1N_HALF { is_valid = false; @@ -203,7 +198,7 @@ impl SignedTransaction7702 { Address::default() }); - // Validations steps 3-8 0f EIP-7702 provided by EVM itself. + // Validations steps 2,4-9 0f EIP-7702 provided by EVM itself. authorization_list.push(Authorization { authority: auth_address.raw(), address: auth.address, @@ -269,12 +264,12 @@ mod tests { #[test] fn test_authorization_tuple_decode() { - let chain_id = U256::from(1); + let chain_id = 1.into(); let address = H160::from_low_u64_be(0x1234); let nonce = 1u64; - let parity = U256::from(0); - let r = U256::from(2); - let s = U256::from(3); + let parity = U256::zero(); + let r = 2.into(); + let s = 3.into(); let mut stream = RlpStream::new_list(6); stream.append(&chain_id); @@ -299,21 +294,21 @@ mod tests { fn test_transaction7702_rlp_append_unsigned() { let tx = Transaction7702 { chain_id: 1, - nonce: U256::from(1), - max_priority_fee_per_gas: U256::from(2), + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), max_fee_per_gas: U256::from(3), - gas_limit: U256::from(4), + gas_limit: 4.into(), to: Address::new(H160::from_low_u64_be(0x1234)), - value: Wei::new(U256::from(5)), + value: Wei::new(5.into()), data: vec![0x6], access_list: vec![], authorization_list: vec![AuthorizationTuple { - chain_id: U256::from(1), + chain_id: 1.into(), address: H160::from_low_u64_be(0x1234), nonce: 1u64, - parity: U256::from(0), - r: U256::from(2), - s: U256::from(3), + parity: U256::zero(), + r: 2.into(), + s: 3.into(), }], }; @@ -331,29 +326,29 @@ mod tests { fn test_signed_transaction7702_rlp_encode_decode() { let tx = Transaction7702 { chain_id: 1, - nonce: U256::from(1), - max_priority_fee_per_gas: U256::from(2), - max_fee_per_gas: U256::from(3), - gas_limit: U256::from(4), + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), to: Address::new(H160::from_low_u64_be(0x1234)), - value: Wei::new(U256::from(5)), + value: Wei::new(5.into()), data: vec![0x6], access_list: vec![], authorization_list: vec![AuthorizationTuple { - chain_id: U256::from(1), + chain_id: 1.into(), address: H160::from_low_u64_be(0x1234), nonce: 1u64, - parity: U256::from(0), - r: U256::from(2), - s: U256::from(3), + parity: U256::zero(), + r: 2.into(), + s: 3.into(), }], }; let signed_tx = SignedTransaction7702 { transaction: tx, parity: 0, - r: U256::from(7), - s: U256::from(8), + r: 7.into(), + s: 8.into(), }; let mut stream = RlpStream::new(); @@ -364,4 +359,217 @@ mod tests { assert_eq!(decoded, signed_tx); } + + #[test] + fn test_signed_transaction7702_invalid_chain_id() { + let mut tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 2.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 0.into(), + r: 2.into(), + s: 3.into(), + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + // Fail + let auth_list = signed_tx.authorization_list().unwrap(); + assert_eq!(auth_list.len(), 1); + assert!(!auth_list[0].is_valid); + + // Success + tx.chain_id = 2; + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(auth_list[0].is_valid); + + // Success + tx.authorization_list = vec![AuthorizationTuple { + chain_id: U256::zero(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 0.into(), + r: 2.into(), + s: 3.into(), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(auth_list[0].is_valid); + } + + #[test] + fn test_signed_transaction7702_empty_auth_list() { + let tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + if let Err(err) = signed_tx.authorization_list() { + assert_eq!(err, Error::EmptyAuthorizationList); + } + } + + #[test] + fn test_signed_transaction7702_invalid_signature_v() { + let mut tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 2.into(), + r: 2.into(), + s: 3.into(), + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + let auth_list = signed_tx.authorization_list().unwrap(); + assert_eq!(auth_list.len(), 1); + assert!(!auth_list[0].is_valid); + + tx.authorization_list = vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: u8::MAX.into(), + r: 2.into(), + s: 3.into(), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(!auth_list[0].is_valid); + + // Success + tx.authorization_list = vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 0.into(), + r: 2.into(), + s: 3.into(), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(auth_list[0].is_valid); + } + + #[test] + fn test_signed_transaction7702_invalid_signature_s() { + let mut tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 1.into(), + r: 2.into(), + s: SECP256K1N_HALF, + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + // Success + let auth_list = signed_tx.authorization_list().unwrap(); + assert_eq!(auth_list.len(), 1); + assert!(auth_list[0].is_valid); + + // Fails + tx.authorization_list = vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: u8::MAX.into(), + r: 2.into(), + s: SECP256K1N_HALF + U256::from(1), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(!auth_list[0].is_valid); + } } From 6e7ea18bd71211efb08a589c4c6cf6a4c079ff73 Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Thu, 16 Jan 2025 19:39:59 +0100 Subject: [PATCH 08/10] Fix clippy --- engine-transactions/src/eip_7702.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs index 1e2495cc0..5759e5f2e 100644 --- a/engine-transactions/src/eip_7702.rs +++ b/engine-transactions/src/eip_7702.rs @@ -167,16 +167,15 @@ impl SignedTransaction7702 { // `chain_id = 0 || current_chain_id`. // AS `current_chain_id` we used `transaction.chain_id` as we will validate `chain_id` in // Engine `submit_transaction` method. - let mut is_valid = auth.chain_id.is_zero() || auth.chain_id == current_tx_chain_id; // Step 2 - validation logic inside EVM itself. // Step 3. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) // Validate the signature, as in tests it is possible to have invalid signatures values. - // Value `v` shouldn't be greater then 1 - if auth.parity > U256::from(1) { - is_valid = false; - } + let mut is_valid = auth.chain_id.is_zero() + || auth.chain_id == current_tx_chain_id + || auth.parity <= U256::from(1); + let v = u8::try_from(auth.parity.as_u64()).map_err(|_| Error::InvalidV)?; // EIP-2 validation if auth.s > SECP256K1N_HALF { @@ -440,7 +439,7 @@ mod tests { }; let signed_tx = SignedTransaction7702 { - transaction: tx.clone(), + transaction: tx, parity: 0, r: 2.into(), s: 3.into(), @@ -511,7 +510,7 @@ mod tests { s: 3.into(), }]; let signed_tx = SignedTransaction7702 { - transaction: tx.clone(), + transaction: tx, parity: 0, r: 2.into(), s: 3.into(), @@ -564,7 +563,7 @@ mod tests { s: SECP256K1N_HALF + U256::from(1), }]; let signed_tx = SignedTransaction7702 { - transaction: tx.clone(), + transaction: tx, parity: 0, r: 2.into(), s: 3.into(), From 638fb47b2921ec51d3baf6850a5e3a9255c82444 Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Thu, 16 Jan 2025 20:55:49 +0100 Subject: [PATCH 09/10] Fix tests --- engine-transactions/src/eip_7702.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs index 5759e5f2e..ba0754316 100644 --- a/engine-transactions/src/eip_7702.rs +++ b/engine-transactions/src/eip_7702.rs @@ -172,9 +172,8 @@ impl SignedTransaction7702 { // Step 3. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) // Validate the signature, as in tests it is possible to have invalid signatures values. // Value `v` shouldn't be greater then 1 - let mut is_valid = auth.chain_id.is_zero() - || auth.chain_id == current_tx_chain_id - || auth.parity <= U256::from(1); + let mut is_valid = (auth.chain_id.is_zero() || auth.chain_id == current_tx_chain_id) + && auth.parity <= U256::from(1); let v = u8::try_from(auth.parity.as_u64()).map_err(|_| Error::InvalidV)?; // EIP-2 validation From 69dd68a764d13ae1b2b4efc2868e21d5be602004 Mon Sep 17 00:00:00 2001 From: Evgeny Ukhanov Date: Mon, 20 Jan 2025 21:29:17 +0100 Subject: [PATCH 10/10] After review small changes --- engine-tests/src/tests/transaction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine-tests/src/tests/transaction.rs b/engine-tests/src/tests/transaction.rs index e6347ca9c..14f0ebd54 100644 --- a/engine-tests/src/tests/transaction.rs +++ b/engine-tests/src/tests/transaction.rs @@ -248,7 +248,7 @@ fn test_eip_7702_success() { } #[test] -fn test_eip_7702_wrong_auth_nonce() { +fn test_eip_7702_wrong_auth_chain_id() { let mut runner = utils::deploy_runner(); let signer = example_signer(); let signer_address = utils::address_from_secret_key(&signer.secret_key); @@ -290,7 +290,7 @@ fn test_eip_7702_wrong_auth_nonce() { H256::zero() ); - // Authority address should increase Nonce + // Authority address should not increase Nonce because authorization failed assert_eq!(runner.get_nonce(delegated_designator), 0.into()); // Get delegated designator address: in that particular case it should be empty assert!(runner.get_code(delegated_designator).is_empty());