From 8c64766b0e8ea951baeeeebbb8c21831e09dd737 Mon Sep 17 00:00:00 2001 From: Matthew Smith Date: Sat, 8 Jun 2024 00:59:02 +0700 Subject: [PATCH] feat: add EIP-7702 support --- Cargo.lock | 2 + crates/primitives/Cargo.toml | 2 + crates/primitives/src/authorization.rs | 76 +++++++++++++++++++ crates/primitives/src/constants.rs | 5 ++ crates/primitives/src/env.rs | 14 +++- crates/primitives/src/lib.rs | 4 + .../src/handler/mainnet/post_execution.rs | 3 + .../revm/src/handler/mainnet/pre_execution.rs | 51 +++++++++++++ crates/revm/src/journaled_state.rs | 31 ++++++++ 9 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 crates/primitives/src/authorization.rs diff --git a/Cargo.lock b/Cargo.lock index c08fc32083..e25420ed61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2979,6 +2979,7 @@ name = "revm-primitives" version = "4.0.0" dependencies = [ "alloy-primitives", + "alloy-rlp", "auto_impl", "bitflags 2.5.0", "bitvec", @@ -2989,6 +2990,7 @@ dependencies = [ "enumn", "hashbrown", "hex", + "k256", "once_cell", "serde", ] diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index a9d009d2e4..031accc5c8 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -25,6 +25,8 @@ all = "warn" alloy-primitives = { version = "0.7.2", default-features = false, features = [ "rlp", ] } +alloy-rlp = { version = "0.3", default-features = false } +k256 = { version = "0.13.3", default-features = false, features = ["ecdsa"] } hashbrown = "0.14" auto_impl = "1.2" bitvec = { version = "1", default-features = false, features = ["alloc"] } diff --git a/crates/primitives/src/authorization.rs b/crates/primitives/src/authorization.rs new file mode 100644 index 0000000000..d9e03c7c66 --- /dev/null +++ b/crates/primitives/src/authorization.rs @@ -0,0 +1,76 @@ +//! [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) Set EOA account code for one transaction +//! +//! These transactions include a list of [`Authorization`] + +use crate::AUTHORIZATION_MAGIC_BYTE; +use alloy_primitives::{Address, ChainId, Keccak256, U256}; +use alloy_rlp::{BufMut, Encodable, Header}; +use k256::{ + ecdsa::{RecoveryId, Signature, VerifyingKey}, + FieldBytes, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Authorization { + /// Chain ID, will not be checked if set to zero + pub chain_id: ChainId, + /// The address of the code that will get set to the signer's address + pub address: Address, + /// Optional nonce + pub nonce: Option, + /// yParity: Signature Y parity + pub y_parity: bool, + /// The R field of the signature + pub r: U256, + /// The S field of the signature + pub s: U256, +} + +impl Authorization { + /// Recover the address of the signer for EIP-7702 transactions + /// Since these authorizations are fallible, we will ignore errors and optionally + /// return the signer's address + pub fn recovered_authority(&self) -> Option
{ + Signature::from_scalars( + *FieldBytes::from_slice(&self.r.to_be_bytes::<32>()), + *FieldBytes::from_slice(&self.s.to_be_bytes::<32>()), + ) + .ok() + .and_then(|sig| RecoveryId::from_byte(self.y_parity as u8).map(|recid| (sig, recid))) + .and_then(|(sig, recid)| { + let nonce = self.nonce.map(|n| vec![n]).unwrap_or(vec![]); + + let mut length = 0; + length += self.chain_id.length(); + length += self.address.length(); + length += nonce.length(); + + let mut buffer = Vec::new(); + + buffer.put_u8(AUTHORIZATION_MAGIC_BYTE); + + Header { + list: true, + payload_length: length, + } + .encode(&mut buffer); + self.chain_id.encode(&mut buffer); + self.address.encode(&mut buffer); + nonce.encode(&mut buffer); + + let mut hasher = Keccak256::new(); + hasher.update(buffer); + let hash = hasher.finalize(); + + let recovered_key = + VerifyingKey::recover_from_prehash(&hash.as_slice(), &sig, recid).ok()?; + let encoded_point = recovered_key.to_encoded_point(false); + + let mut hasher = Keccak256::new(); + hasher.update(&encoded_point.as_bytes()[1..]); + let address = &hasher.finalize()[12..]; + Some(Address::from_slice(address)) + }) + } +} diff --git a/crates/primitives/src/constants.rs b/crates/primitives/src/constants.rs index c8223300bf..9ddda36258 100644 --- a/crates/primitives/src/constants.rs +++ b/crates/primitives/src/constants.rs @@ -60,3 +60,8 @@ pub const BLOB_GASPRICE_UPDATE_FRACTION: u64 = 3338477; /// First version of the blob. pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; + +// === EIP-7702 constants === + +// Magic byte prepended to the rlp-encoded payload of authorizations +pub const AUTHORIZATION_MAGIC_BYTE: u8 = 0x05; diff --git a/crates/primitives/src/env.rs b/crates/primitives/src/env.rs index 99b5557149..4c9168fca5 100644 --- a/crates/primitives/src/env.rs +++ b/crates/primitives/src/env.rs @@ -3,9 +3,9 @@ pub mod handler_cfg; pub use handler_cfg::{CfgEnvWithHandlerCfg, EnvWithHandlerCfg, HandlerCfg}; use crate::{ - calc_blob_gasprice, Account, Address, Bytes, HashMap, InvalidHeader, InvalidTransaction, Spec, - SpecId, B256, GAS_PER_BLOB, KECCAK_EMPTY, MAX_BLOB_NUMBER_PER_BLOCK, MAX_INITCODE_SIZE, U256, - VERSIONED_HASH_VERSION_KZG, + calc_blob_gasprice, Account, Address, Authorization, Bytes, HashMap, InvalidHeader, + InvalidTransaction, Spec, SpecId, B256, GAS_PER_BLOB, KECCAK_EMPTY, MAX_BLOB_NUMBER_PER_BLOCK, + MAX_INITCODE_SIZE, U256, VERSIONED_HASH_VERSION_KZG, }; use core::cmp::{min, Ordering}; use core::hash::Hash; @@ -584,6 +584,13 @@ pub struct TxEnv { /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 pub max_fee_per_blob_gas: Option, + /// The list of authorizations for this transaction + /// + /// Incorporated as part of the Prague upgrade via [EIP-7702]. + /// + /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + pub authorization_list: Vec, + /// EOF Initcodes for EOF CREATE transaction /// /// Incorporated as part of the Prague upgrade via [EOF] @@ -642,6 +649,7 @@ impl Default for TxEnv { access_list: Vec::new(), blob_hashes: Vec::new(), max_fee_per_blob_gas: None, + authorization_list: Vec::new(), eof_initcodes: Vec::new(), eof_initcodes_hashed: HashMap::new(), #[cfg(feature = "optimism")] diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index c57f900b90..954e160e29 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -9,6 +9,8 @@ extern crate alloc as std; mod bytecode; mod constants; + +pub mod authorization; pub mod db; pub mod env; @@ -19,10 +21,12 @@ pub mod result; pub mod specification; pub mod state; pub mod utilities; + pub use alloy_primitives::{ self, address, b256, bytes, fixed_bytes, hex, hex_literal, ruint, uint, Address, Bytes, FixedBytes, Log, LogData, B256, I256, U256, }; +pub use authorization::*; pub use bitvec; pub use bytecode::*; pub use constants::*; diff --git a/crates/revm/src/handler/mainnet/post_execution.rs b/crates/revm/src/handler/mainnet/post_execution.rs index bf73236159..a36165e8a9 100644 --- a/crates/revm/src/handler/mainnet/post_execution.rs +++ b/crates/revm/src/handler/mainnet/post_execution.rs @@ -18,6 +18,9 @@ pub fn end( /// Clear handle clears error and journal state. #[inline] pub fn clear(context: &mut Context) { + // TODO(eip7702): teardown + // 'set the code of each authority back to empty' + // clear error and journaled state. let _ = context.evm.take_error(); context.evm.inner.journaled_state.clear(); diff --git a/crates/revm/src/handler/mainnet/pre_execution.rs b/crates/revm/src/handler/mainnet/pre_execution.rs index c0f4fe7bde..36c1e4190f 100644 --- a/crates/revm/src/handler/mainnet/pre_execution.rs +++ b/crates/revm/src/handler/mainnet/pre_execution.rs @@ -49,6 +49,57 @@ pub fn load_accounts( )?; } + // Load code into EOAs + // EIP-7702: Set EOA account code for one transaction + if SPEC::enabled(PRAGUE) { + // TODO(eip7702): This is currently UNTESTED and needs to be checked for correctness + + // These authorizations are fallible, so if we encounter an invalid authorization + // or an error, skip it and continue + for authorization in context.evm.inner.env.tx.authorization_list.iter() { + // Recover the signer address if possible + let Some(authority) = authorization.recovered_authority() else { + continue; + }; + + // Optionally match the chain id + if authorization.chain_id != 0 + && authorization.chain_id != context.evm.inner.env.cfg.chain_id + { + continue; + } + + // Verify that the code of authority is empty + // TODO(eip7702): better way to do this? + let Ok(authority_account) = context.evm.inner.journaled_state.initial_account_load( + authority, + &[], + &mut context.evm.inner.db, + ) else { + continue; + }; + if authority_account.info.code.is_some() { + continue; + }; + + // Check nonce if present + if let Some(nonce) = authorization.nonce { + if authority_account.info.nonce != nonce { + continue; + } + } + + // Load the code into the account + // This touches the access list for `address` but not for the `from_address` + // TODO(eip7702): better way to do this? + _ = context.evm.inner.journaled_state.load_code_into( + authority, + authorization.address, + &mut context.evm.inner.db, + ); + } + } + context.evm.load_access_list()?; Ok(()) } diff --git a/crates/revm/src/journaled_state.rs b/crates/revm/src/journaled_state.rs index a233a78ca0..077aa672de 100644 --- a/crates/revm/src/journaled_state.rs +++ b/crates/revm/src/journaled_state.rs @@ -625,6 +625,37 @@ impl JournaledState { Ok((acc, is_cold)) } + /// Load code from one account to another + /// + /// For EIP-7702: Set EOA account code for one transaction + /// This touches the access list for `address` but not for the `from_address` + #[inline] + pub fn load_code_into( + &mut self, + address: Address, + from_address: Address, + db: &mut DB, + ) -> Result<(&mut Account, bool), EVMError> { + let from_acc = db + .basic(from_address) + .map_err(EVMError::Database)? + .map(|i| i.into()) + .unwrap_or(Account::new_not_existing()); + let (acc, is_cold) = self.load_account(address, db)?; + if acc.info.code.is_none() { + if from_acc.info.code_hash == KECCAK_EMPTY { + let empty = Bytecode::default(); + acc.info.code = Some(empty); + } else { + let code = db + .code_by_hash(from_acc.info.code_hash) + .map_err(EVMError::Database)?; + acc.info.code = Some(code); + } + } + Ok((acc, is_cold)) + } + /// Load storage slot /// /// # Panics