diff --git a/.gitignore b/.gitignore index 44162106ff10..faac71735eef 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +# Generated by VSCode +.vscode + # Generated by Intellij-based IDEs. .idea diff --git a/crates/executor/Cargo.toml b/crates/executor/Cargo.toml index c57347abf5a0..53acae3de9f0 100644 --- a/crates/executor/Cargo.toml +++ b/crates/executor/Cargo.toml @@ -55,3 +55,4 @@ parking_lot = "0.12" [features] test-utils = ["parking_lot"] +optimism = [] diff --git a/crates/executor/src/executor.rs b/crates/executor/src/executor.rs index 2e56a72090ca..ee4dc17e164e 100644 --- a/crates/executor/src/executor.rs +++ b/crates/executor/src/executor.rs @@ -25,6 +25,9 @@ use std::{ sync::Arc, }; +#[cfg(feature = "optimism")] +use crate::optimism; + /// Main block executor pub struct Executor where @@ -385,6 +388,9 @@ where self.init_env(&block.header, total_difficulty); + #[cfg(feature = "optimism")] + let mut l1_block_info = optimism::L1BlockInfo::new(block)?; + let mut cumulative_gas_used = 0; let mut post_state = PostState::with_tx_capacity(block.body.len()); for (transaction, sender) in block.body.iter().zip(senders.into_iter()) { @@ -397,33 +403,147 @@ where block_available_gas, }) } - // Execute transaction. - let ResultAndState { result, state } = self.transact(transaction, sender)?; - - // commit changes - self.commit_changes( - state, - self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block.number), - &mut post_state, - ); - // append gas used - cumulative_gas_used += result.gas_used(); - - // cast revm logs to reth logs - let logs: Vec = result.logs().into_iter().map(into_reth_log).collect(); - - // Push transaction changeset and calculate header bloom filter for receipt. - post_state.add_receipt(Receipt { - tx_type: transaction.tx_type(), - // Success flag was added in `EIP-658: Embedding transaction status code in - // receipts`. - success: result.is_success(), - cumulative_gas_used, - bloom: logs_bloom(logs.iter()), - logs, - }); - post_state.finish_transition(); + #[cfg(feature = "optimism")] + { + let db = self.db(); + let l1_cost = l1_block_info.calculate_tx_l1_cost(transaction); + + let sender_account = db.load_account(sender).map_err(|_| Error::ProviderError)?; + let old_sender_info = to_reth_acc(&sender_account.info); + if let Some(m) = transaction.mint() { + // Add balance to the caller account equal to the minted amount. + // Note: this is unconditional, and will not be reverted if the tx fails + // (unless the block can't be built at all due to gas limit constraints) + sender_account.info.balance += U256::from(m); + } + + // Check if the sender balance can cover the L1 cost. + // Deposits pay for their gas directly on L1 so they are exempt from this + if !transaction.is_deposit() { + if sender_account.info.balance.cmp(&l1_cost) == std::cmp::Ordering::Less { + return Err(Error::InsufficientFundsForL1Cost { + have: sender_account.info.balance.to::(), + want: l1_cost.to::(), + }) + } + + // Safely take l1_cost from sender (the rest will be deducted by the + // internal EVM execution and included in result.gas_used()) + // TODO: need to handle calls with `disable_balance_check` flag set? + sender_account.info.balance -= l1_cost; + } + + let new_sender_info = to_reth_acc(&sender_account.info); + post_state.change_account(sender, old_sender_info, new_sender_info); + + // Execute transaction. + let ResultAndState { result, state } = self.transact(transaction, sender)?; + + if transaction.is_deposit() && !result.is_success() { + // If the Deposited transaction failed, the deposit must still be included. + // In this case, we need to increment the sender nonce and disregard the + // state changes. The transaction is also recorded as using all gas. + let db = self.db(); + let sender_account = + db.load_account(sender).map_err(|_| Error::ProviderError)?; + let old_sender_info = to_reth_acc(&sender_account.info); + sender_account.info.nonce += 1; + let new_sender_info = to_reth_acc(&sender_account.info); + + post_state.change_account(sender, old_sender_info, new_sender_info); + if !transaction.is_system_transaction() { + cumulative_gas_used += transaction.gas_limit(); + } + + post_state.add_receipt(Receipt { + tx_type: transaction.tx_type(), + success: false, + cumulative_gas_used, + bloom: Bloom::zero(), + logs: vec![], + deposit_nonce: Some(transaction.nonce()), + }); + post_state.finish_transition(); + continue + } + + // commit changes + self.commit_changes( + state, + self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block.number), + &mut post_state, + ); + + if !transaction.is_system_transaction() { + // After Regolith, deposits are reported as using the actual gas used instead of + // all the gas. System transactions are not reported as using any gas. + cumulative_gas_used += result.gas_used() + } + + // Route the l1 cost and base fee to the appropriate optimism vaults + self.increment_account_balance( + optimism::l1_cost_recipient(), + l1_cost, + &mut post_state, + )?; + self.increment_account_balance( + optimism::base_fee_recipient(), + U256::from( + block + .base_fee_per_gas + .unwrap_or_default() + .saturating_mul(result.gas_used()), + ), + &mut post_state, + )?; + + // cast revm logs to reth logs + let logs: Vec = result.logs().into_iter().map(into_reth_log).collect(); + + // Push transaction changeset and calculate header bloom filter for receipt. + post_state.add_receipt(Receipt { + tx_type: transaction.tx_type(), + // Success flag was added in `EIP-658: Embedding transaction status code in + // receipts`. + success: result.is_success(), + cumulative_gas_used, + bloom: logs_bloom(logs.iter()), + logs, + deposit_nonce: Some(transaction.nonce()), + }); + post_state.finish_transition(); + } + + #[cfg(not(feature = "optimism"))] + { + // Execute transaction. + let ResultAndState { result, state } = self.transact(transaction, sender)?; + + // commit changes + self.commit_changes( + state, + self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(block.number), + &mut post_state, + ); + + cumulative_gas_used += result.gas_used(); + + // cast revm logs to reth logs + let logs: Vec = result.logs().into_iter().map(into_reth_log).collect(); + + // Push transaction changeset and calculate header bloom filter for receipt. + post_state.add_receipt(Receipt { + tx_type: transaction.tx_type(), + // Success flag was added in `EIP-658: Embedding transaction status code in + // receipts`. + success: result.is_success(), + cumulative_gas_used, + bloom: logs_bloom(logs.iter()), + logs, + }); + post_state.finish_transition(); + } } Ok((post_state, cumulative_gas_used)) diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs index fbdb1cc1736e..c4e4b13664e9 100644 --- a/crates/executor/src/lib.rs +++ b/crates/executor/src/lib.rs @@ -25,3 +25,7 @@ pub use factory::Factory; #[cfg(any(test, feature = "test-utils"))] /// Common test helpers for mocking out executor and executor factory pub mod test_utils; + +#[cfg(feature = "optimism")] +/// Optimism-specific utilities for the executor +pub mod optimism; diff --git a/crates/executor/src/optimism.rs b/crates/executor/src/optimism.rs new file mode 100644 index 000000000000..9f579d8b688f --- /dev/null +++ b/crates/executor/src/optimism.rs @@ -0,0 +1,110 @@ +use std::str::FromStr; + +use reth_interfaces::executor; +use reth_primitives::{Address, Block, TransactionKind, TransactionSigned, U256}; + +const L1_FEE_RECIPIENT: &str = "0x420000000000000000000000000000000000001A"; +const BASE_FEE_RECIPIENT: &str = "0x4200000000000000000000000000000000000019"; +const L1_BLOCK_CONTRACT: &str = "0x4200000000000000000000000000000000000015"; + +const ZERO_BYTE_COST: u64 = 4; +const NON_ZERO_BYTE_COST: u64 = 16; + +/// L1 block info +/// +/// We can extract L1 epoch data from each L2 block, by looking at the `setL1BlockValues` +/// transaction data. This data is then used to calculate the L1 cost of a transaction. +/// +/// Here is the format of the `setL1BlockValues` transaction data: +/// +/// setL1BlockValues(uint64 _number, uint64 _timestamp, uint256 _basefee, bytes32 _hash, +/// uint64 _sequenceNumber, bytes32 _batcherHash, uint256 _l1FeeOverhead, uint256 _l1FeeScalar) +/// +/// For now, we only care about the fields necessary for L1 cost calculation. +pub struct L1BlockInfo { + l1_base_fee: U256, + l1_fee_overhead: U256, + l1_fee_scalar: U256, +} + +impl L1BlockInfo { + /// Create a new L1 block info struct from a L2 block + pub fn new(block: &Block) -> Result { + let l1_block_contract = Address::from_str(L1_BLOCK_CONTRACT).unwrap(); + + let l1_info_tx_data = block + .body + .iter() + .find(|tx| matches!(tx.kind(), TransactionKind::Call(to) if to == &l1_block_contract)) + .ok_or(executor::Error::L1BlockInfoError { + message: "could not find l1 block info tx in the L2 block".to_string(), + }) + .and_then(|tx| { + tx.input().get(4..).ok_or(executor::Error::L1BlockInfoError { + message: "could not get l1 block info tx calldata bytes".to_string(), + }) + })?; + + // The setL1BlockValues tx calldata must be exactly 184 bytes long, considering that + // we already removed the first 4 bytes (the function selector). Detailed breakdown: + // 8 bytes for the block number + // + 8 bytes for the block timestamp + // + 32 bytes for the base fee + // + 32 bytes for the block hash + // + 8 bytes for the block sequence number + // + 32 bytes for the batcher hash + // + 32 bytes for the fee overhead + // + 32 bytes for the fee scalar + if l1_info_tx_data.len() != 184 { + return Err(executor::Error::L1BlockInfoError { + message: "unexpected l1 block info tx calldata length found".to_string(), + }) + } + + let l1_base_fee = U256::try_from_le_slice(&l1_info_tx_data[16..48]).ok_or( + executor::Error::L1BlockInfoError { + message: "could not convert l1 base fee".to_string(), + }, + )?; + let l1_fee_overhead = U256::try_from_le_slice(&l1_info_tx_data[120..152]).ok_or( + executor::Error::L1BlockInfoError { + message: "could not convert l1 fee overhead".to_string(), + }, + )?; + let l1_fee_scalar = U256::try_from_le_slice(&l1_info_tx_data[152..184]).ok_or( + executor::Error::L1BlockInfoError { + message: "could not convert l1 fee scalar".to_string(), + }, + )?; + + Ok(Self { l1_base_fee, l1_fee_overhead, l1_fee_scalar }) + } + + /// Calculate the gas cost of a transaction based on L1 block data posted on L2 + pub fn calculate_tx_l1_cost(&mut self, tx: &TransactionSigned) -> U256 { + let rollup_data_gas_cost = U256::from(tx.input().iter().fold(0, |acc, byte| { + acc + if byte == &0x00 { ZERO_BYTE_COST } else { NON_ZERO_BYTE_COST } + })); + + if tx.is_deposit() || rollup_data_gas_cost == U256::ZERO { + return U256::ZERO + } + + rollup_data_gas_cost + .saturating_add(self.l1_fee_overhead) + .saturating_mul(self.l1_base_fee) + .saturating_mul(self.l1_fee_scalar) + .checked_div(U256::from(1_000_000)) + .unwrap_or_default() + } +} + +/// Get the base fee recipient address +pub fn base_fee_recipient() -> Address { + Address::from_str(BASE_FEE_RECIPIENT).unwrap() +} + +/// Get the L1 cost recipient address +pub fn l1_cost_recipient() -> Address { + Address::from_str(L1_FEE_RECIPIENT).unwrap() +} diff --git a/crates/interfaces/Cargo.toml b/crates/interfaces/Cargo.toml index d8a81d115423..1ebc1a37111c 100644 --- a/crates/interfaces/Cargo.toml +++ b/crates/interfaces/Cargo.toml @@ -47,4 +47,5 @@ secp256k1 = { version = "0.26.0", default-features = false, features = [ [features] bench = [] +optimism = [] test-utils = ["tokio-stream/sync", "secp256k1"] diff --git a/crates/interfaces/src/executor.rs b/crates/interfaces/src/executor.rs index a40f60a0e252..de8ddbd83c2a 100644 --- a/crates/interfaces/src/executor.rs +++ b/crates/interfaces/src/executor.rs @@ -7,7 +7,7 @@ use thiserror::Error; pub enum Error { #[error("EVM reported invalid transaction ({hash:?}): {message}")] EVM { hash: H256, message: String }, - #[error("Example of error.")] + #[error("Verification failed")] VerificationFailed, #[error("Fatal internal error")] ExecutionFatalError, @@ -64,4 +64,11 @@ pub enum Error { CanonicalCommit { inner: String }, #[error("Transaction error on pipeline status update: {inner:?}")] PipelineStatusUpdate { inner: String }, + + #[cfg(feature = "optimism")] + #[error("Could not get L1 block info from L2 block: {message:?}")] + L1BlockInfoError { message: String }, + #[cfg(feature = "optimism")] + #[error("Insufficient funds to cover transaction L1 cost: want {want}, have {have}")] + InsufficientFundsForL1Cost { want: u64, have: u64 }, } diff --git a/crates/net/eth-wire/Cargo.toml b/crates/net/eth-wire/Cargo.toml index 831298a449b5..45562a34c4c6 100644 --- a/crates/net/eth-wire/Cargo.toml +++ b/crates/net/eth-wire/Cargo.toml @@ -57,6 +57,7 @@ proptest-derive = "0.3" default = ["serde"] serde = ["dep:serde", "smol_str/serde"] arbitrary = ["reth-primitives/arbitrary", "dep:arbitrary", "dep:proptest", "dep:proptest-derive"] +optimism = [] [[test]] name = "fuzz_roundtrip" diff --git a/crates/net/eth-wire/src/types/receipts.rs b/crates/net/eth-wire/src/types/receipts.rs index 52516d0b8c17..5fb4d3b55504 100644 --- a/crates/net/eth-wire/src/types/receipts.rs +++ b/crates/net/eth-wire/src/types/receipts.rs @@ -40,6 +40,8 @@ mod test { cumulative_gas_used: 0, bloom: Default::default(), logs: vec![], + #[cfg(feature = "optimism")] + deposit_nonce: None, }]]); let mut out = vec![]; @@ -108,6 +110,8 @@ mod test { }, ], success: false, + #[cfg(feature = "optimism")] + deposit_nonce: None, }, ], ]), @@ -142,6 +146,8 @@ mod test { }, ], success: false, + #[cfg(feature = "optimism")] + deposit_nonce: None, }, ], ]), diff --git a/crates/primitives/src/proofs.rs b/crates/primitives/src/proofs.rs index 28f7f6e8b2e7..5f4d5b93c0a8 100644 --- a/crates/primitives/src/proofs.rs +++ b/crates/primitives/src/proofs.rs @@ -128,6 +128,8 @@ mod tests { cumulative_gas_used: 102068, bloom, logs, + #[cfg(feature = "optimism")] + deposit_nonce: None, }; let receipt = vec![receipt]; let root = calculate_receipt_root(receipt.iter()); diff --git a/crates/primitives/src/receipt.rs b/crates/primitives/src/receipt.rs index ae4303856f90..5ea18afe75e5 100644 --- a/crates/primitives/src/receipt.rs +++ b/crates/primitives/src/receipt.rs @@ -20,6 +20,9 @@ pub struct Receipt { pub bloom: Bloom, /// Log send from contracts. pub logs: Vec, + /// Deposit nonce for Optimism deposited transactions + #[cfg(feature = "optimism")] + pub deposit_nonce: Option, } impl Receipt { @@ -31,6 +34,13 @@ impl Receipt { rlp_head.payload_length += self.cumulative_gas_used.length(); rlp_head.payload_length += self.bloom.length(); rlp_head.payload_length += self.logs.length(); + #[cfg(feature = "optimism")] + if self.tx_type == TxType::DEPOSIT { + // Transactions pre-Regolith don't have a deposit nonce + if let Some(nonce) = self.deposit_nonce { + rlp_head.payload_length += nonce.length(); + } + } rlp_head } @@ -42,6 +52,10 @@ impl Receipt { self.cumulative_gas_used.encode(out); self.bloom.encode(out); self.logs.encode(out); + #[cfg(feature = "optimism")] + if let Some(nonce) = self.deposit_nonce { + nonce.encode(out); + } } /// Encode receipt with or without the header data. @@ -61,13 +75,12 @@ impl Receipt { } match self.tx_type { - TxType::EIP2930 => { - out.put_u8(0x01); - } - TxType::EIP1559 => { - out.put_u8(0x02); - } - _ => unreachable!("legacy handled; qed."), + TxType::EIP2930 => out.put_u8(0x01), + TxType::EIP1559 => out.put_u8(0x02), + TxType::Legacy => unreachable!("legacy handled; qed."), + + #[cfg(feature = "optimism")] + TxType::DEPOSIT => out.put_u8(0x7E), } out.put_slice(payload.as_ref()); } @@ -86,13 +99,28 @@ impl Receipt { return Err(reth_rlp::DecodeError::UnexpectedString) } let started_len = b.len(); - let this = Self { - tx_type, - success: reth_rlp::Decodable::decode(b)?, - cumulative_gas_used: reth_rlp::Decodable::decode(b)?, - bloom: reth_rlp::Decodable::decode(b)?, - logs: reth_rlp::Decodable::decode(b)?, + + let this = match tx_type { + #[cfg(feature = "optimism")] + TxType::DEPOSIT => Self { + tx_type, + success: reth_rlp::Decodable::decode(b)?, + cumulative_gas_used: reth_rlp::Decodable::decode(b)?, + bloom: reth_rlp::Decodable::decode(b)?, + logs: reth_rlp::Decodable::decode(b)?, + deposit_nonce: Some(reth_rlp::Decodable::decode(b)?), + }, + _ => Self { + tx_type, + success: reth_rlp::Decodable::decode(b)?, + cumulative_gas_used: reth_rlp::Decodable::decode(b)?, + bloom: reth_rlp::Decodable::decode(b)?, + logs: reth_rlp::Decodable::decode(b)?, + #[cfg(feature = "optimism")] + deposit_nonce: None, + }, }; + let consumed = started_len - b.len(); if consumed != rlp_head.payload_length { return Err(reth_rlp::DecodeError::ListLengthMismatch { @@ -138,14 +166,22 @@ impl Decodable for Receipt { let receipt_type = *buf.first().ok_or(reth_rlp::DecodeError::Custom( "typed receipt cannot be decoded from an empty slice", ))?; - if receipt_type == 0x01 { - buf.advance(1); - Self::decode_receipt(buf, TxType::EIP2930) - } else if receipt_type == 0x02 { - buf.advance(1); - Self::decode_receipt(buf, TxType::EIP1559) - } else { - Err(reth_rlp::DecodeError::Custom("invalid receipt type")) + + match receipt_type { + 0x01 => { + buf.advance(1); + Self::decode_receipt(buf, TxType::EIP2930) + } + 0x02 => { + buf.advance(1); + Self::decode_receipt(buf, TxType::EIP1559) + } + #[cfg(feature = "optimism")] + 0x7E => { + buf.advance(1); + Self::decode_receipt(buf, TxType::DEPOSIT) + } + _ => Err(reth_rlp::DecodeError::Custom("invalid receipt type")), } } Ordering::Equal => { @@ -189,6 +225,8 @@ mod tests { data: Bytes::from_str("0100ff").unwrap().0.into(), }], success: false, + #[cfg(feature = "optimism")] + deposit_nonce: None, }; receipt.encode(&mut data); @@ -223,6 +261,8 @@ mod tests { data: Bytes::from_str("0100ff").unwrap().0.into(), }], success: false, + #[cfg(feature = "optimism")] + deposit_nonce: None, }; let receipt = Receipt::decode(&mut &data[..]).unwrap(); diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 33ad192b1233..c4aef2009fba 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -344,6 +344,12 @@ impl Transaction { } } + /// Returns whether or not the transaction is an Optimism Deposited transaction. + #[cfg(feature = "optimism")] + pub fn is_deposit(&self) -> bool { + matches!(self, Transaction::Deposit(_)) + } + /// Encodes EIP-155 arguments into the desired buffer. Only encodes values for legacy /// transactions. pub(crate) fn encode_eip155_fields(&self, out: &mut dyn bytes::BufMut) { diff --git a/crates/revm/revm-primitives/src/env.rs b/crates/revm/revm-primitives/src/env.rs index 712fe7cf75df..0cc2018605b4 100644 --- a/crates/revm/revm-primitives/src/env.rs +++ b/crates/revm/revm-primitives/src/env.rs @@ -173,13 +173,9 @@ where .collect(); } #[cfg(feature = "optimism")] - Transaction::Deposit(TxDeposit { to, mint, value, gas_limit, input, .. }) => { + Transaction::Deposit(TxDeposit { to, value, gas_limit, input, .. }) => { tx_env.gas_limit = *gas_limit; - if let Some(m) = mint { - tx_env.gas_price = U256::from(*m); - } else { - tx_env.gas_price = U256::ZERO; - } + tx_env.gas_price = U256::ZERO; tx_env.gas_priority_fee = None; match to { TransactionKind::Call(to) => tx_env.transact_to = TransactTo::Call(*to), diff --git a/crates/transaction-pool/src/traits.rs b/crates/transaction-pool/src/traits.rs index 081df8d9c503..21754ea3736a 100644 --- a/crates/transaction-pool/src/traits.rs +++ b/crates/transaction-pool/src/traits.rs @@ -434,7 +434,8 @@ impl FromRecoveredTransaction for PooledTransaction { } #[cfg(feature = "optimism")] Transaction::Deposit(t) => { - // TODO: fix this gas price estimate + // Gas price is always set to 0 for deposits in order to zero out ETH refunds, + // because they already pay for their gas on L1. let gas_price = U256::from(0); let cost = U256::from(gas_price) * U256::from(t.gas_limit) + U256::from(t.value); let effective_gas_price = 0u128;