Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: eip-7702 transaction support #1

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
76 changes: 76 additions & 0 deletions crates/primitives/src/authorization.rs
Original file line number Diff line number Diff line change
@@ -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<u64>,
/// 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<Address> {
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))
})
}
}
5 changes: 5 additions & 0 deletions crates/primitives/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
14 changes: 11 additions & 3 deletions crates/primitives/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -584,6 +584,13 @@ pub struct TxEnv {
/// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
pub max_fee_per_blob_gas: Option<U256>,

/// 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<Authorization>,

/// EOF Initcodes for EOF CREATE transaction
///
/// Incorporated as part of the Prague upgrade via [EOF]
Expand Down Expand Up @@ -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")]
Expand Down
4 changes: 4 additions & 0 deletions crates/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ extern crate alloc as std;

mod bytecode;
mod constants;

pub mod authorization;
pub mod db;
pub mod env;

Expand All @@ -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::*;
Expand Down
3 changes: 3 additions & 0 deletions crates/revm/src/handler/mainnet/post_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub fn end<EXT, DB: Database>(
/// Clear handle clears error and journal state.
#[inline]
pub fn clear<EXT, DB: Database>(context: &mut Context<EXT, DB>) {
// 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();
Expand Down
51 changes: 51 additions & 0 deletions crates/revm/src/handler/mainnet/pre_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,57 @@ pub fn load_accounts<SPEC: Spec, EXT, DB: Database>(
)?;
}

// 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(())
}
Expand Down
31 changes: 31 additions & 0 deletions crates/revm/src/journaled_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DB: Database>(
&mut self,
address: Address,
from_address: Address,
db: &mut DB,
) -> Result<(&mut Account, bool), EVMError<DB::Error>> {
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
Expand Down