diff --git a/crates/consensus/beacon/src/engine/mod.rs b/crates/consensus/beacon/src/engine/mod.rs index 7c74bc39575..7c75f2adc4c 100644 --- a/crates/consensus/beacon/src/engine/mod.rs +++ b/crates/consensus/beacon/src/engine/mod.rs @@ -995,27 +995,30 @@ where // forkchoiceState.headBlockHash and identified via buildProcessId value if // payloadAttributes is not null and the forkchoice state has been updated successfully. // The build process is specified in the Payload building section. - let attributes = PayloadBuilderAttributes::new(state.head_block_hash, attrs); - - // send the payload to the builder and return the receiver for the pending payload id, - // initiating payload job is handled asynchronously - let pending_payload_id = self.payload_builder.send_new_payload(attributes); - - // Client software MUST respond to this method call in the following way: - // { - // payloadStatus: { - // status: VALID, - // latestValidHash: forkchoiceState.headBlockHash, - // validationError: null - // }, - // payloadId: buildProcessId - // } - // - // if the payload is deemed VALID and the build process has begun. - OnForkChoiceUpdated::updated_with_pending_payload_id( - PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)), - pending_payload_id, - ) + match PayloadBuilderAttributes::try_new(state.head_block_hash, attrs) { + Ok(attributes) => { + // send the payload to the builder and return the receiver for the pending payload + // id, initiating payload job is handled asynchronously + let pending_payload_id = self.payload_builder.send_new_payload(attributes); + + // Client software MUST respond to this method call in the following way: + // { + // payloadStatus: { + // status: VALID, + // latestValidHash: forkchoiceState.headBlockHash, + // validationError: null + // }, + // payloadId: buildProcessId + // } + // + // if the payload is deemed VALID and the build process has begun. + OnForkChoiceUpdated::updated_with_pending_payload_id( + PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)), + pending_payload_id, + ) + } + Err(_) => OnForkChoiceUpdated::invalid_payload_attributes(), + } } /// When the Consensus layer receives a new block via the consensus gossip protocol, diff --git a/crates/payload/basic/Cargo.toml b/crates/payload/basic/Cargo.toml index fd56911d338..9c55accb505 100644 --- a/crates/payload/basic/Cargo.toml +++ b/crates/payload/basic/Cargo.toml @@ -35,5 +35,6 @@ optimism = [ "reth-primitives/optimism", "reth-revm/optimism", "reth-transaction-pool/optimism", - "reth-provider/optimism" + "reth-provider/optimism", + "reth-payload-builder/optimism" ] diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index bf12796f2dd..6aec71c2db1 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -755,8 +755,22 @@ where let base_fee = initialized_block_env.basefee.to::(); let block_number = initialized_block_env.number.to::(); + + #[cfg(not(feature = "optimism"))] let block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); + #[cfg(feature = "optimism")] + let mut block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); + + #[cfg(feature = "optimism")] + { + if let Some(gas_limit) = attributes.gas_limit { + block_gas_limit = gas_limit; + } + // TODO(clabby): configure the gas limit of pending blocks with the miner gas limit config + // when using optimism + } + let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals( &mut db, &mut post_state, @@ -786,7 +800,14 @@ where gas_limit: block_gas_limit, difficulty: U256::ZERO, gas_used: 0, + #[cfg(not(feature = "optimism"))] extra_data: extra_data.into(), + #[cfg(feature = "optimism")] + extra_data: if chain_spec.optimism.is_none() { + extra_data.into() + } else { + Default::default() + }, }; let block = Block { header, body: vec![], ommers: vec![], withdrawals }; diff --git a/crates/payload/builder/src/payload.rs b/crates/payload/builder/src/payload.rs index 83cfe75ec1e..3f3d482703b 100644 --- a/crates/payload/builder/src/payload.rs +++ b/crates/payload/builder/src/payload.rs @@ -2,12 +2,17 @@ use reth_primitives::{Address, ChainSpec, Header, SealedBlock, Withdrawal, H256, U256}; use reth_revm_primitives::config::revm_spec_by_timestamp_after_merge; -use reth_rlp::Encodable; +use reth_rlp::{DecodeError, Encodable}; use reth_rpc_types::engine::{ ExecutionPayload, ExecutionPayloadEnvelope, PayloadAttributes, PayloadId, }; use revm_primitives::{BlockEnv, CfgEnv}; +#[cfg(feature = "optimism")] +use reth_primitives::TransactionSigned; +#[cfg(feature = "optimism")] +use reth_rlp::Decodable; + /// Contains the built payload. /// /// According to the [engine API specification](https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md) the execution layer should build the initial version of the payload with an empty transaction set and then keep update it in order to maximize the revenue. @@ -88,6 +93,15 @@ pub struct PayloadBuilderAttributes { pub prev_randao: H256, /// Withdrawals for the generated payload pub withdrawals: Vec, + /// NoTxPool option for the generated payload + #[cfg(feature = "optimism")] + pub no_tx_pool: bool, + /// Transactions for the generated payload + #[cfg(feature = "optimism")] + pub transactions: Vec, + /// The gas limit for the generated payload + #[cfg(feature = "optimism")] + pub gas_limit: Option, } // === impl PayloadBuilderAttributes === @@ -96,16 +110,36 @@ impl PayloadBuilderAttributes { /// Creates a new payload builder for the given parent block and the attributes. /// /// Derives the unique [PayloadId] for the given parent and attributes - pub fn new(parent: H256, attributes: PayloadAttributes) -> Self { + pub fn try_new(parent: H256, attributes: PayloadAttributes) -> Result { + #[cfg(feature = "optimism")] + let transactions = attributes + .transactions + .as_ref() + .unwrap_or(&Vec::default()) + .iter() + .map(|tx| TransactionSigned::decode(&mut &tx[..])) + .collect::>()?; + + #[cfg(not(feature = "optimism"))] let id = payload_id(&parent, &attributes); - Self { + + #[cfg(feature = "optimism")] + let id = payload_id(&parent, &attributes, &transactions); + + Ok(Self { id, parent, timestamp: attributes.timestamp.as_u64(), suggested_fee_recipient: attributes.suggested_fee_recipient, prev_randao: attributes.prev_randao, withdrawals: attributes.withdrawals.unwrap_or_default(), - } + #[cfg(feature = "optimism")] + no_tx_pool: attributes.no_tx_pool.unwrap_or_default(), + #[cfg(feature = "optimism")] + transactions, + #[cfg(feature = "optimism")] + gas_limit: attributes.gas_limit, + }) } /// Returns the configured [CfgEnv] and [BlockEnv] for the targeted payload (that has the @@ -149,7 +183,11 @@ impl PayloadBuilderAttributes { /// Generates the payload id for the configured payload /// /// Returns an 8-byte identifier by hashing the payload components with sha256 hash. -pub(crate) fn payload_id(parent: &H256, attributes: &PayloadAttributes) -> PayloadId { +pub(crate) fn payload_id( + parent: &H256, + attributes: &PayloadAttributes, + #[cfg(feature = "optimism")] txs: &Vec, +) -> PayloadId { use sha2::Digest; let mut hasher = sha2::Sha256::new(); hasher.update(parent.as_bytes()); @@ -161,6 +199,21 @@ pub(crate) fn payload_id(parent: &H256, attributes: &PayloadAttributes) -> Paylo withdrawals.encode(&mut buf); hasher.update(buf); } + + #[cfg(feature = "optimism")] + { + let no_tx_pool = attributes.no_tx_pool.unwrap_or_default(); + if no_tx_pool || !txs.is_empty() { + hasher.update([no_tx_pool as u8]); + hasher.update(txs.len().to_be_bytes()); + txs.iter().for_each(|tx| hasher.update(tx.hash())); + } + + if let Some(gas_limit) = attributes.gas_limit { + hasher.update(gas_limit.to_be_bytes()); + } + } + let out = hasher.finalize(); PayloadId::new(out.as_slice()[..8].try_into().expect("sufficient length")) } diff --git a/crates/payload/builder/src/service.rs b/crates/payload/builder/src/service.rs index 67d6bbbaf73..588cef1a9cf 100644 --- a/crates/payload/builder/src/service.rs +++ b/crates/payload/builder/src/service.rs @@ -242,6 +242,13 @@ where if this.contains_payload(id) { warn!(%id, parent = ?attr.parent, "Payload job already in progress, ignoring."); } else { + // Don't start the payload job if there is no tx pool to pull from. + #[cfg(feature = "optimism")] + if attr.no_tx_pool { + let _ = tx.send(res); + continue + } + // no job for this payload yet, create one match this.generator.new_payload_job(attr) { Ok(job) => { diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index baad89d513d..9324dbd0a74 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -29,4 +29,3 @@ once_cell = "1.17.0" [features] optimism = ["reth-primitives/optimism", "reth-revm-primitives/optimism"] - diff --git a/crates/revm/src/executor.rs b/crates/revm/src/executor.rs index d76eb644295..46e56992fa8 100644 --- a/crates/revm/src/executor.rs +++ b/crates/revm/src/executor.rs @@ -236,15 +236,127 @@ where let mut cumulative_gas_used = 0; let mut post_state = PostState::with_tx_capacity(block.number, block.body.len()); for (transaction, sender) in block.body.iter().zip(senders) { - // The sum of the transaction’s gas limit, Tg, and the gas utilised in this block prior, - // must be no greater than the block’s gasLimit. - let block_available_gas = block.header.gas_limit - cumulative_gas_used; - if transaction.gas_limit() > block_available_gas { - return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { - transaction_gas_limit: transaction.gas_limit(), - block_available_gas, + #[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(|_| BlockExecutionError::ProviderError)?; + let old_sender_info = to_reth_acc(&sender_account.info); + + if let Some(m) = transaction.mint() { + // Add balance to the caler 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 the L2 tx fee. + if !transaction.is_deposit() { + if sender_account.info.balance.cmp(&l1_cost) == std::cmp::Ordering::Less { + return Err(BlockExecutionError::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(block.number, 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(|_| BlockExecutionError::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( + block.number, + sender, + old_sender_info, + new_sender_info, + ); + if !transaction.is_system_transaction() { + cumulative_gas_used += transaction.gas_limit(); + } + + post_state.add_receipt( + block.number, + Receipt { + tx_type: transaction.tx_type(), + success: false, + cumulative_gas_used, + logs: vec![], + deposit_nonce: Some(transaction.nonce()), + }, + ); + continue + } + + // commit changes + self.commit_changes( + block.number, + 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() } - .into()) + + // Route the l1 cost and base fee to the appropriate optimism vaults + self.increment_account_balance( + block.number, + optimism::l1_cost_recipient(), + l1_cost, + &mut post_state, + )?; + self.increment_account_balance( + block.number, + 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 = result.logs().into_iter().map(into_reth_log).collect(); + + // Push transaction changeset and calculate header bloom filter for receipt. + post_state.add_receipt( + block.number, + 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, + logs, + deposit_nonce: Some(transaction.nonce()), + }, + ); } #[cfg(feature = "optimism")] diff --git a/crates/rpc/rpc-engine-api/src/engine_api.rs b/crates/rpc/rpc-engine-api/src/engine_api.rs index 2a0581595f4..242381c5a79 100644 --- a/crates/rpc/rpc-engine-api/src/engine_api.rs +++ b/crates/rpc/rpc-engine-api/src/engine_api.rs @@ -107,6 +107,11 @@ where attrs.timestamp.as_u64(), attrs.withdrawals.is_some(), )?; + + #[cfg(feature = "optimism")] + if attrs.gas_limit.is_none() && self.inner.chain_spec.optimism.is_some() { + return Err(EngineApiError::MissingGasLimitInPayloadAttributes) + } } Ok(self.inner.beacon_consensus.fork_choice_updated(state, payload_attrs).await?) } diff --git a/crates/rpc/rpc-types/src/eth/fee.rs b/crates/rpc/rpc-types/src/eth/fee.rs index 6463e7b433c..c7f93f6eaf6 100644 --- a/crates/rpc/rpc-types/src/eth/fee.rs +++ b/crates/rpc/rpc-types/src/eth/fee.rs @@ -12,10 +12,7 @@ pub struct TxGasAndReward { impl PartialOrd for TxGasAndReward { fn partial_cmp(&self, other: &Self) -> Option { - // compare only the reward - // see: - // - self.reward.partial_cmp(&other.reward) + Some(self.cmp(other)) } }