From 84aad958194256d31f017e376623fa8802ba2f13 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Mon, 24 Feb 2025 15:08:12 +0700 Subject: [PATCH 1/4] Payload builder service refactoring --- Cargo.lock | 1 + crates/op-rbuilder/Cargo.toml | 1 + crates/op-rbuilder/src/main.rs | 3 +- .../src/payload_builder_vanilla.rs | 2 +- crates/op-rbuilder/src/primitives/mod.rs | 4 + .../src/primitives/payload_builder_service.rs | 342 ++++++++++++++++++ 6 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 crates/op-rbuilder/src/primitives/mod.rs create mode 100644 crates/op-rbuilder/src/primitives/payload_builder_service.rs diff --git a/Cargo.lock b/Cargo.lock index c05e94d1a..ee30c291e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6724,6 +6724,7 @@ dependencies = [ "tikv-jemallocator", "time", "tokio", + "tokio-stream", "tokio-tungstenite 0.26.1", "tokio-util", "tower 0.4.13", diff --git a/crates/op-rbuilder/Cargo.toml b/crates/op-rbuilder/Cargo.toml index 7c49203a1..e12d0f051 100644 --- a/crates/op-rbuilder/Cargo.toml +++ b/crates/op-rbuilder/Cargo.toml @@ -67,6 +67,7 @@ serde_with.workspace = true serde.workspace = true secp256k1.workspace = true tokio.workspace = true +tokio-stream.workspace = true jsonrpsee = { workspace = true } async-trait = { workspace = true } clap_builder = { workspace = true } diff --git a/crates/op-rbuilder/src/main.rs b/crates/op-rbuilder/src/main.rs index f6ce402b5..0d0f817c3 100644 --- a/crates/op-rbuilder/src/main.rs +++ b/crates/op-rbuilder/src/main.rs @@ -5,7 +5,6 @@ use reth::providers::CanonStateSubscriptions; use reth_optimism_cli::{chainspec::OpChainSpecParser, Cli}; use reth_optimism_node::node::OpAddOnsBuilder; use reth_optimism_node::OpNode; - /// CLI argument parsing. pub mod args; @@ -20,6 +19,8 @@ mod payload_builder_vanilla; mod tester; mod tx_signer; +mod primitives; + fn main() { Cli::::parse() .run(|builder, builder_args| async move { diff --git a/crates/op-rbuilder/src/payload_builder_vanilla.rs b/crates/op-rbuilder/src/payload_builder_vanilla.rs index f983b1513..f9efed8dc 100644 --- a/crates/op-rbuilder/src/payload_builder_vanilla.rs +++ b/crates/op-rbuilder/src/payload_builder_vanilla.rs @@ -3,6 +3,7 @@ use crate::generator::BuildArguments; use crate::{ generator::{BlockCell, PayloadBuilder}, metrics::OpRBuilderMetrics, + primitives::PayloadBuilderService, tx_signer::Signer, }; use alloy_consensus::constants::EMPTY_WITHDRAWALS; @@ -48,7 +49,6 @@ use reth_optimism_payload_builder::{ use reth_optimism_primitives::{ OpPrimitives, OpTransactionSigned, ADDRESS_L2_TO_L1_MESSAGE_PASSER, }; -use reth_payload_builder::PayloadBuilderService; use reth_payload_builder_primitives::PayloadBuilderError; use reth_payload_primitives::PayloadBuilderAttributes; use reth_payload_util::BestPayloadTransactions; diff --git a/crates/op-rbuilder/src/primitives/mod.rs b/crates/op-rbuilder/src/primitives/mod.rs new file mode 100644 index 000000000..bc35af8c0 --- /dev/null +++ b/crates/op-rbuilder/src/primitives/mod.rs @@ -0,0 +1,4 @@ +//! This module contains types from the reth that weren't heavily modified +mod payload_builder_service; + +pub use payload_builder_service::PayloadBuilderService; diff --git a/crates/op-rbuilder/src/primitives/payload_builder_service.rs b/crates/op-rbuilder/src/primitives/payload_builder_service.rs new file mode 100644 index 000000000..dc1d6d2aa --- /dev/null +++ b/crates/op-rbuilder/src/primitives/payload_builder_service.rs @@ -0,0 +1,342 @@ +//! This struct is copied from reth almost as it is https://github.com/paradigmxyz/reth/blob/v1.2.0/crates/payload/builder/src/service.rs +//! +//! Support for building payloads. +//! +//! The payload builder is responsible for building payloads. +//! Once a new payload is created, it is continuously updated. + +use alloy_consensus::BlockHeader; +use alloy_rpc_types_engine::PayloadId; +use futures_util::{future::FutureExt, Stream, StreamExt}; +use reth_chain_state::CanonStateNotification; +use reth_payload_builder::{ + KeepPayloadJobAlive, PayloadBuilderHandle, PayloadJob, PayloadJobGenerator, + PayloadServiceCommand, +}; +use reth_payload_builder_primitives::{Events, PayloadBuilderError}; +use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes, PayloadKind, PayloadTypes}; +use reth_primitives_traits::NodePrimitives; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::sync::{broadcast, mpsc}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tracing::{debug, info, trace, warn}; + +type PayloadFuture

= Pin> + Send + Sync>>; + +/// A service that manages payload building tasks. +/// +/// This type is an endless future that manages the building of payloads. +/// +/// It tracks active payloads and their build jobs that run in a worker pool. +/// +/// By design, this type relies entirely on the [`PayloadJobGenerator`] to create new payloads and +/// does know nothing about how to build them, it just drives their jobs to completion. +#[derive(Debug)] +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct PayloadBuilderService +where + T: PayloadTypes, + Gen: PayloadJobGenerator, + Gen::Job: PayloadJob, +{ + /// The type that knows how to create new payloads. + generator: Gen, + /// All active payload jobs. + payload_jobs: Vec<(Gen::Job, PayloadId)>, + /// Copy of the sender half, so new [`PayloadBuilderHandle`] can be created on demand. + service_tx: mpsc::UnboundedSender>, + /// Receiver half of the command channel. + command_rx: UnboundedReceiverStream>, + /// Metrics for the payload builder service + metrics: PayloadBuilderServiceMetrics, + /// Chain events notification stream + chain_events: St, + /// Payload events handler, used to broadcast and subscribe to payload events. + payload_events: broadcast::Sender>, +} + +const PAYLOAD_EVENTS_BUFFER_SIZE: usize = 20; + +// === impl PayloadBuilderService === + +impl PayloadBuilderService +where + T: PayloadTypes, + Gen: PayloadJobGenerator, + Gen::Job: PayloadJob, + ::BuiltPayload: Into, +{ + /// Creates a new payload builder service and returns the [`PayloadBuilderHandle`] to interact + /// with it. + /// + /// This also takes a stream of chain events that will be forwarded to the generator to apply + /// additional logic when new state is committed. See also + /// [`PayloadJobGenerator::on_new_state`]. + pub fn new(generator: Gen, chain_events: St) -> (Self, PayloadBuilderHandle) { + let (service_tx, command_rx) = mpsc::unbounded_channel(); + let (payload_events, _) = broadcast::channel(PAYLOAD_EVENTS_BUFFER_SIZE); + + let service = Self { + generator, + payload_jobs: Vec::new(), + service_tx, + command_rx: UnboundedReceiverStream::new(command_rx), + metrics: Default::default(), + chain_events, + payload_events, + }; + + let handle = service.handle(); + (service, handle) + } + + /// Returns a handle to the service. + pub fn handle(&self) -> PayloadBuilderHandle { + PayloadBuilderHandle::new(self.service_tx.clone()) + } + + /// Returns true if the given payload is currently being built. + fn contains_payload(&self, id: PayloadId) -> bool { + self.payload_jobs.iter().any(|(_, job_id)| *job_id == id) + } + + /// Returns the best payload for the given identifier that has been built so far. + fn best_payload(&self, id: PayloadId) -> Option> { + let res = self + .payload_jobs + .iter() + .find(|(_, job_id)| *job_id == id) + .map(|(j, _)| j.best_payload().map(|p| p.into())); + if let Some(Ok(ref best)) = res { + self.metrics + .set_best_revenue(best.block().number(), f64::from(best.fees())); + } + + res + } + + /// Returns the best payload for the given identifier that has been built so far and terminates + /// the job if requested. + fn resolve( + &mut self, + id: PayloadId, + kind: PayloadKind, + ) -> Option> { + trace!(%id, "resolving payload job"); + + let job = self + .payload_jobs + .iter() + .position(|(_, job_id)| *job_id == id)?; + let (fut, keep_alive) = self.payload_jobs[job].0.resolve_kind(kind); + + if keep_alive == KeepPayloadJobAlive::No { + let (_, id) = self.payload_jobs.swap_remove(job); + trace!(%id, "terminated resolved job"); + } + + // Since the fees will not be known until the payload future is resolved / awaited, we wrap + // the future in a new future that will update the metrics. + let resolved_metrics = self.metrics.clone(); + let payload_events = self.payload_events.clone(); + + let fut = async move { + let res = fut.await; + if let Ok(ref payload) = res { + payload_events + .send(Events::BuiltPayload(payload.clone().into())) + .ok(); + + resolved_metrics + .set_resolved_revenue(payload.block().number(), f64::from(payload.fees())); + } + res.map(|p| p.into()) + }; + + Some(Box::pin(fut)) + } +} + +impl PayloadBuilderService +where + T: PayloadTypes, + Gen: PayloadJobGenerator, + Gen::Job: PayloadJob, + ::BuiltPayload: Into, +{ + /// Returns the payload attributes for the given payload. + fn payload_attributes( + &self, + id: PayloadId, + ) -> Option::PayloadAttributes, PayloadBuilderError>> { + let attributes = self + .payload_jobs + .iter() + .find(|(_, job_id)| *job_id == id) + .map(|(j, _)| j.payload_attributes()); + + if attributes.is_none() { + trace!(%id, "no matching payload job found to get attributes for"); + } + + attributes + } +} + +impl Future for PayloadBuilderService +where + T: PayloadTypes, + N: NodePrimitives, + Gen: PayloadJobGenerator + Unpin + 'static, + ::Job: Unpin + 'static, + St: Stream> + Send + Unpin + 'static, + Gen::Job: PayloadJob, + ::BuiltPayload: Into, +{ + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + loop { + // notify the generator of new chain events + while let Poll::Ready(Some(new_head)) = this.chain_events.poll_next_unpin(cx) { + this.generator.on_new_state(new_head); + } + + // we poll all jobs first, so we always have the latest payload that we can report if + // requests + // we don't care about the order of the jobs, so we can just swap_remove them + for idx in (0..this.payload_jobs.len()).rev() { + let (mut job, id) = this.payload_jobs.swap_remove(idx); + + // drain better payloads from the job + match job.poll_unpin(cx) { + Poll::Ready(Ok(_)) => { + this.metrics.set_active_jobs(this.payload_jobs.len()); + trace!(%id, "payload job finished"); + } + Poll::Ready(Err(err)) => { + warn!(%err, ?id, "Payload builder job failed; resolving payload"); + this.metrics.inc_failed_jobs(); + this.metrics.set_active_jobs(this.payload_jobs.len()); + } + Poll::Pending => { + // still pending, put it back + this.payload_jobs.push((job, id)); + } + } + } + + // marker for exit condition + let mut new_job = false; + + // drain all requests + while let Poll::Ready(Some(cmd)) = this.command_rx.poll_next_unpin(cx) { + match cmd { + PayloadServiceCommand::BuildNewPayload(attr, tx) => { + let id = attr.payload_id(); + let mut res = Ok(id); + + if this.contains_payload(id) { + debug!(%id, parent = %attr.parent(), "Payload job already in progress, ignoring."); + } else { + // no job for this payload yet, create one + let parent = attr.parent(); + match this.generator.new_payload_job(attr.clone()) { + Ok(job) => { + info!(%id, %parent, "New payload job created"); + this.metrics.inc_initiated_jobs(); + new_job = true; + this.payload_jobs.push((job, id)); + this.payload_events + .send(Events::Attributes(attr.clone())) + .ok(); + } + Err(err) => { + this.metrics.inc_failed_jobs(); + warn!(%err, %id, "Failed to create payload builder job"); + res = Err(err); + } + } + } + + // return the id of the payload + let _ = tx.send(res); + } + PayloadServiceCommand::BestPayload(id, tx) => { + let _ = tx.send(this.best_payload(id)); + } + PayloadServiceCommand::PayloadAttributes(id, tx) => { + let attributes = this.payload_attributes(id); + let _ = tx.send(attributes); + } + PayloadServiceCommand::Resolve(id, strategy, tx) => { + let _ = tx.send(this.resolve(id, strategy)); + } + PayloadServiceCommand::Subscribe(tx) => { + let new_rx = this.payload_events.subscribe(); + let _ = tx.send(new_rx); + } + } + } + + if !new_job { + return Poll::Pending; + } + } + } +} + +/// This section is copied from <> +use reth_metrics::{ + metrics::{Counter, Gauge}, + Metrics, +}; + +/// Payload builder service metrics +#[derive(Metrics, Clone)] +#[metrics(scope = "payloads")] +pub(crate) struct PayloadBuilderServiceMetrics { + /// Number of active jobs + pub(crate) active_jobs: Gauge, + /// Total number of initiated jobs + pub(crate) initiated_jobs: Counter, + /// Total number of failed jobs + pub(crate) failed_jobs: Counter, + /// Coinbase revenue for best payloads + pub(crate) best_revenue: Gauge, + /// Current block returned as the best payload + pub(crate) best_block: Gauge, + /// Coinbase revenue for resolved payloads + pub(crate) resolved_revenue: Gauge, + /// Current block returned as the resolved payload + pub(crate) resolved_block: Gauge, +} + +impl PayloadBuilderServiceMetrics { + pub(crate) fn inc_initiated_jobs(&self) { + self.initiated_jobs.increment(1); + } + + pub(crate) fn inc_failed_jobs(&self) { + self.failed_jobs.increment(1); + } + + pub(crate) fn set_active_jobs(&self, value: usize) { + self.active_jobs.set(value as f64) + } + + pub(crate) fn set_best_revenue(&self, block: u64, value: f64) { + self.best_block.set(block as f64); + self.best_revenue.set(value) + } + + pub(crate) fn set_resolved_revenue(&self, block: u64, value: f64) { + self.resolved_block.set(block as f64); + self.resolved_revenue.set(value) + } +} From 9ed4f94f07b5a2b24b820701f9c261ea37dffdc7 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Mon, 24 Feb 2025 15:52:24 +0700 Subject: [PATCH 2/4] ExecutionInfo and ExecutedPayload Refactoring --- .../src/payload_builder_vanilla.rs | 70 +------------------ .../op-rbuilder/src/primitives/execution.rs | 70 +++++++++++++++++++ crates/op-rbuilder/src/primitives/mod.rs | 3 + 3 files changed, 75 insertions(+), 68 deletions(-) create mode 100644 crates/op-rbuilder/src/primitives/execution.rs diff --git a/crates/op-rbuilder/src/payload_builder_vanilla.rs b/crates/op-rbuilder/src/payload_builder_vanilla.rs index f9efed8dc..1921bf9ac 100644 --- a/crates/op-rbuilder/src/payload_builder_vanilla.rs +++ b/crates/op-rbuilder/src/payload_builder_vanilla.rs @@ -3,7 +3,7 @@ use crate::generator::BuildArguments; use crate::{ generator::{BlockCell, PayloadBuilder}, metrics::OpRBuilderMetrics, - primitives::PayloadBuilderService, + primitives::{ExecutedPayload, ExecutionInfo, PayloadBuilderService}, tx_signer::Signer, }; use alloy_consensus::constants::EMPTY_WITHDRAWALS; @@ -13,7 +13,7 @@ use alloy_consensus::{ }; use alloy_eips::merge::BEACON_NONCE; use alloy_primitives::private::alloy_rlp::Encodable; -use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; +use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::Withdrawals; use op_alloy_consensus::{OpDepositReceipt, OpTypedTransaction}; @@ -692,72 +692,6 @@ impl OpPayloadTransactions for () { } } -/// Holds the state after execution -#[derive(Debug)] -pub struct ExecutedPayload { - /// Tracked execution info - pub info: ExecutionInfo, - /// Withdrawal hash. - pub withdrawals_root: Option, -} - -/// This acts as the container for executed transactions and its byproducts (receipts, gas used) -#[derive(Default, Debug)] -pub struct ExecutionInfo { - /// All executed transactions (unrecovered). - pub executed_transactions: Vec, - /// The recovered senders for the executed transactions. - pub executed_senders: Vec

, - /// The transaction receipts - pub receipts: Vec, - /// All gas used so far - pub cumulative_gas_used: u64, - /// Estimated DA size - pub cumulative_da_bytes_used: u64, - /// Tracks fees from executed mempool transactions - pub total_fees: U256, -} - -impl ExecutionInfo { - /// Create a new instance with allocated slots. - pub fn with_capacity(capacity: usize) -> Self { - Self { - executed_transactions: Vec::with_capacity(capacity), - executed_senders: Vec::with_capacity(capacity), - receipts: Vec::with_capacity(capacity), - cumulative_gas_used: 0, - cumulative_da_bytes_used: 0, - total_fees: U256::ZERO, - } - } - - /// Returns true if the transaction would exceed the block limits: - /// - block gas limit: ensures the transaction still fits into the block. - /// - tx DA limit: if configured, ensures the tx does not exceed the maximum allowed DA limit - /// per tx. - /// - block DA limit: if configured, ensures the transaction's DA size does not exceed the - /// maximum allowed DA limit per block. - pub fn is_tx_over_limits( - &self, - tx: &N::SignedTx, - block_gas_limit: u64, - tx_data_limit: Option, - block_data_limit: Option, - ) -> bool { - if tx_data_limit.is_some_and(|da_limit| tx.length() as u64 > da_limit) { - return true; - } - - if block_data_limit - .is_some_and(|da_limit| self.cumulative_da_bytes_used + (tx.length() as u64) > da_limit) - { - return true; - } - - self.cumulative_gas_used + tx.gas_limit() > block_gas_limit - } -} - /// Container type that holds all necessities to build a new payload. #[derive(Debug)] pub struct OpPayloadBuilderCtx { diff --git a/crates/op-rbuilder/src/primitives/execution.rs b/crates/op-rbuilder/src/primitives/execution.rs new file mode 100644 index 000000000..795afb9c3 --- /dev/null +++ b/crates/op-rbuilder/src/primitives/execution.rs @@ -0,0 +1,70 @@ +use alloy_consensus::Transaction; +use alloy_primitives::private::alloy_rlp::Encodable; +use alloy_primitives::{Address, B256, U256}; +use reth_primitives_traits::NodePrimitives; + +/// Holds the state after execution +#[derive(Debug)] +pub struct ExecutedPayload { + /// Tracked execution info + pub info: ExecutionInfo, + /// Withdrawal hash. + pub withdrawals_root: Option, +} + +/// This acts as the container for executed transactions and its byproducts (receipts, gas used) +#[derive(Default, Debug)] +pub struct ExecutionInfo { + /// All executed transactions (unrecovered). + pub executed_transactions: Vec, + /// The recovered senders for the executed transactions. + pub executed_senders: Vec
, + /// The transaction receipts + pub receipts: Vec, + /// All gas used so far + pub cumulative_gas_used: u64, + /// Estimated DA size + pub cumulative_da_bytes_used: u64, + /// Tracks fees from executed mempool transactions + pub total_fees: U256, +} + +impl ExecutionInfo { + /// Create a new instance with allocated slots. + pub fn with_capacity(capacity: usize) -> Self { + Self { + executed_transactions: Vec::with_capacity(capacity), + executed_senders: Vec::with_capacity(capacity), + receipts: Vec::with_capacity(capacity), + cumulative_gas_used: 0, + cumulative_da_bytes_used: 0, + total_fees: U256::ZERO, + } + } + + /// Returns true if the transaction would exceed the block limits: + /// - block gas limit: ensures the transaction still fits into the block. + /// - tx DA limit: if configured, ensures the tx does not exceed the maximum allowed DA limit + /// per tx. + /// - block DA limit: if configured, ensures the transaction's DA size does not exceed the + /// maximum allowed DA limit per block. + pub fn is_tx_over_limits( + &self, + tx: &N::SignedTx, + block_gas_limit: u64, + tx_data_limit: Option, + block_data_limit: Option, + ) -> bool { + if tx_data_limit.is_some_and(|da_limit| tx.length() as u64 > da_limit) { + return true; + } + + if block_data_limit + .is_some_and(|da_limit| self.cumulative_da_bytes_used + (tx.length() as u64) > da_limit) + { + return true; + } + + self.cumulative_gas_used + tx.gas_limit() > block_gas_limit + } +} diff --git a/crates/op-rbuilder/src/primitives/mod.rs b/crates/op-rbuilder/src/primitives/mod.rs index bc35af8c0..839640f20 100644 --- a/crates/op-rbuilder/src/primitives/mod.rs +++ b/crates/op-rbuilder/src/primitives/mod.rs @@ -1,4 +1,7 @@ //! This module contains types from the reth that weren't heavily modified +mod execution; mod payload_builder_service; pub use payload_builder_service::PayloadBuilderService; + +pub use execution::{ExecutedPayload, ExecutionInfo}; From d843d54c41212ddc6fd9932fcfe698a4c7ad89ac Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Mon, 24 Feb 2025 16:28:23 +0700 Subject: [PATCH 3/4] Move estimate_gas_for_builder_tx and signed_builder_tx to helpers --- .../src/payload_builder_vanilla.rs | 69 ++----------------- crates/op-rbuilder/src/primitives/helpers.rs | 64 +++++++++++++++++ crates/op-rbuilder/src/primitives/mod.rs | 3 + crates/op-rbuilder/src/tester/mod.rs | 1 - 4 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 crates/op-rbuilder/src/primitives/helpers.rs diff --git a/crates/op-rbuilder/src/payload_builder_vanilla.rs b/crates/op-rbuilder/src/payload_builder_vanilla.rs index 1921bf9ac..47480f43b 100644 --- a/crates/op-rbuilder/src/payload_builder_vanilla.rs +++ b/crates/op-rbuilder/src/payload_builder_vanilla.rs @@ -3,20 +3,20 @@ use crate::generator::BuildArguments; use crate::{ generator::{BlockCell, PayloadBuilder}, metrics::OpRBuilderMetrics, - primitives::{ExecutedPayload, ExecutionInfo, PayloadBuilderService}, + primitives::{ + estimate_gas_for_builder_tx, signed_builder_tx, ExecutedPayload, ExecutionInfo, + PayloadBuilderService, + }, tx_signer::Signer, }; use alloy_consensus::constants::EMPTY_WITHDRAWALS; -use alloy_consensus::transaction::Recovered; -use alloy_consensus::{ - Eip658Value, Header, Transaction, TxEip1559, Typed2718, EMPTY_OMMER_ROOT_HASH, -}; +use alloy_consensus::{Eip658Value, Header, Transaction, Typed2718, EMPTY_OMMER_ROOT_HASH}; use alloy_eips::merge::BEACON_NONCE; use alloy_primitives::private::alloy_rlp::Encodable; -use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_primitives::{Bytes, U256}; use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::Withdrawals; -use op_alloy_consensus::{OpDepositReceipt, OpTypedTransaction}; +use op_alloy_consensus::OpDepositReceipt; use reth::builder::{components::PayloadServiceBuilder, node::FullNodeTypes, BuilderContext}; use reth::core::primitives::InMemorySize; use reth::payload::PayloadBuilderHandle; @@ -1232,58 +1232,3 @@ where }) } } - -/// Creates signed builder tx to Address::ZERO and specified message as input -pub fn signed_builder_tx( - db: &mut State, - builder_tx_gas: u64, - message: Vec, - signer: Signer, - base_fee: u64, - chain_id: u64, -) -> Result, PayloadBuilderError> -where - DB: Database, -{ - // Create message with block number for the builder to sign - let nonce = db - .load_cache_account(signer.address) - .map(|acc| acc.account_info().unwrap_or_default().nonce) - .map_err(|_| { - PayloadBuilderError::other(OpPayloadBuilderError::AccountLoadFailed(signer.address)) - })?; - - // Create the EIP-1559 transaction - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id, - nonce, - gas_limit: builder_tx_gas, - max_fee_per_gas: base_fee.into(), - max_priority_fee_per_gas: 0, - to: TxKind::Call(Address::ZERO), - // Include the message as part of the transaction data - input: message.into(), - ..Default::default() - }); - // Sign the transaction - let builder_tx = signer.sign_tx(tx).map_err(PayloadBuilderError::other)?; - - Ok(builder_tx) -} - -fn estimate_gas_for_builder_tx(input: Vec) -> u64 { - // Count zero and non-zero bytes - let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { - if byte == 0 { - (zeros + 1, nonzeros) - } else { - (zeros, nonzeros + 1) - } - }); - - // Calculate gas cost (4 gas per zero byte, 16 gas per non-zero byte) - let zero_cost = zero_bytes * 4; - let nonzero_cost = nonzero_bytes * 16; - - zero_cost + nonzero_cost + 21_000 -} diff --git a/crates/op-rbuilder/src/primitives/helpers.rs b/crates/op-rbuilder/src/primitives/helpers.rs new file mode 100644 index 000000000..bdac9b513 --- /dev/null +++ b/crates/op-rbuilder/src/primitives/helpers.rs @@ -0,0 +1,64 @@ +use crate::tx_signer::Signer; +use alloy_consensus::transaction::Recovered; +use alloy_consensus::TxEip1559; +use alloy_primitives::{Address, TxKind}; +use op_alloy_consensus::OpTypedTransaction; +use reth_evm::Database; +use reth_optimism_payload_builder::error::OpPayloadBuilderError; +use reth_optimism_primitives::OpTransactionSigned; +use reth_payload_primitives::PayloadBuilderError; +use reth_provider::ProviderError; +use revm::db::State; +pub fn estimate_gas_for_builder_tx(input: Vec) -> u64 { + // Count zero and non-zero bytes + let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { + if byte == 0 { + (zeros + 1, nonzeros) + } else { + (zeros, nonzeros + 1) + } + }); + + // Calculate gas cost (4 gas per zero byte, 16 gas per non-zero byte) + let zero_cost = zero_bytes * 4; + let nonzero_cost = nonzero_bytes * 16; + + zero_cost + nonzero_cost + 21_000 +} +/// Creates signed builder tx to Address::ZERO and specified message as input +pub fn signed_builder_tx( + db: &mut State, + builder_tx_gas: u64, + message: Vec, + signer: Signer, + base_fee: u64, + chain_id: u64, +) -> Result, PayloadBuilderError> +where + DB: Database, +{ + // Create message with block number for the builder to sign + let nonce = db + .load_cache_account(signer.address) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + .map_err(|_| { + PayloadBuilderError::other(OpPayloadBuilderError::AccountLoadFailed(signer.address)) + })?; + + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit: builder_tx_gas, + max_fee_per_gas: base_fee.into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(Address::ZERO), + // Include the message as part of the transaction data + input: message.into(), + ..Default::default() + }); + // Sign the transaction + let builder_tx = signer.sign_tx(tx).map_err(PayloadBuilderError::other)?; + + Ok(builder_tx) +} diff --git a/crates/op-rbuilder/src/primitives/mod.rs b/crates/op-rbuilder/src/primitives/mod.rs index 839640f20..604fda1d3 100644 --- a/crates/op-rbuilder/src/primitives/mod.rs +++ b/crates/op-rbuilder/src/primitives/mod.rs @@ -1,7 +1,10 @@ //! This module contains types from the reth that weren't heavily modified mod execution; +mod helpers; mod payload_builder_service; pub use payload_builder_service::PayloadBuilderService; +pub use helpers::{estimate_gas_for_builder_tx, signed_builder_tx}; + pub use execution::{ExecutedPayload, ExecutionInfo}; diff --git a/crates/op-rbuilder/src/tester/mod.rs b/crates/op-rbuilder/src/tester/mod.rs index 7261a3b5f..0da1ed05b 100644 --- a/crates/op-rbuilder/src/tester/mod.rs +++ b/crates/op-rbuilder/src/tester/mod.rs @@ -24,7 +24,6 @@ use reth_node_api::{EngineTypes, PayloadTypes}; use reth_optimism_node::OpEngineTypes; use reth_payload_builder::PayloadId; use reth_rpc_layer::{AuthClientLayer, AuthClientService, JwtSecret}; -use serde_json; use serde_json::Value; use std::str::FromStr; use std::time::SystemTime; From 80e788d6c3809c0ba92e96efe2a54c90031f7b9b Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Mon, 24 Feb 2025 20:29:54 +0700 Subject: [PATCH 4/4] Move OpPayloadBuilderCtx to dedicated crate --- .../src/payload_builder_vanilla.rs | 586 +----------------- crates/op-rbuilder/src/primitives/mod.rs | 3 + .../src/primitives/payload_builder_ctx.rs | 581 +++++++++++++++++ 3 files changed, 597 insertions(+), 573 deletions(-) create mode 100644 crates/op-rbuilder/src/primitives/payload_builder_ctx.rs diff --git a/crates/op-rbuilder/src/payload_builder_vanilla.rs b/crates/op-rbuilder/src/payload_builder_vanilla.rs index 47480f43b..aab722df1 100644 --- a/crates/op-rbuilder/src/payload_builder_vanilla.rs +++ b/crates/op-rbuilder/src/payload_builder_vanilla.rs @@ -4,31 +4,21 @@ use crate::{ generator::{BlockCell, PayloadBuilder}, metrics::OpRBuilderMetrics, primitives::{ - estimate_gas_for_builder_tx, signed_builder_tx, ExecutedPayload, ExecutionInfo, - PayloadBuilderService, + estimate_gas_for_builder_tx, ExecutedPayload, OpPayloadBuilderCtx, PayloadBuilderService, }, tx_signer::Signer, }; use alloy_consensus::constants::EMPTY_WITHDRAWALS; -use alloy_consensus::{Eip658Value, Header, Transaction, Typed2718, EMPTY_OMMER_ROOT_HASH}; +use alloy_consensus::{Header, EMPTY_OMMER_ROOT_HASH}; use alloy_eips::merge::BEACON_NONCE; -use alloy_primitives::private::alloy_rlp::Encodable; -use alloy_primitives::{Bytes, U256}; -use alloy_rpc_types_engine::PayloadId; -use alloy_rpc_types_eth::Withdrawals; -use op_alloy_consensus::OpDepositReceipt; +use alloy_primitives::U256; use reth::builder::{components::PayloadServiceBuilder, node::FullNodeTypes, BuilderContext}; use reth::core::primitives::InMemorySize; use reth::payload::PayloadBuilderHandle; -use reth_basic_payload_builder::{ - BasicPayloadJobGeneratorConfig, BuildOutcome, BuildOutcomeKind, PayloadConfig, -}; +use reth_basic_payload_builder::{BasicPayloadJobGeneratorConfig, BuildOutcome, BuildOutcomeKind}; use reth_chain_state::{ExecutedBlock, ExecutedBlockWithTrieUpdates}; -use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks}; -use reth_evm::{ - env::EvmEnv, system_calls::SystemCaller, ConfigureEvmEnv, ConfigureEvmFor, Database, Evm, - EvmError, InvalidTxError, NextBlockEnvAttributes, -}; +use reth_chainspec::{ChainSpecProvider, EthChainSpec}; +use reth_evm::{env::EvmEnv, ConfigureEvmFor, Database, NextBlockEnvAttributes}; use reth_execution_types::ExecutionOutcome; use reth_node_api::NodePrimitives; use reth_node_api::NodeTypesWithEngine; @@ -37,15 +27,12 @@ use reth_optimism_chainspec::OpChainSpec; use reth_optimism_consensus::calculate_receipt_root_no_memo_optimism; use reth_optimism_evm::BasicOpReceiptBuilder; use reth_optimism_evm::OpEvmConfig; -use reth_optimism_evm::{OpReceiptBuilder, ReceiptBuilderCtx}; +use reth_optimism_evm::OpReceiptBuilder; use reth_optimism_forks::OpHardforks; use reth_optimism_node::OpEngineTypes; -use reth_optimism_payload_builder::config::{OpBuilderConfig, OpDAConfig}; +use reth_optimism_payload_builder::config::OpBuilderConfig; +use reth_optimism_payload_builder::payload::{OpBuiltPayload, OpPayloadBuilderAttributes}; use reth_optimism_payload_builder::OpPayloadPrimitives; -use reth_optimism_payload_builder::{ - error::OpPayloadBuilderError, - payload::{OpBuiltPayload, OpPayloadBuilderAttributes}, -}; use reth_optimism_primitives::{ OpPrimitives, OpTransactionSigned, ADDRESS_L2_TO_L1_MESSAGE_PASSER, }; @@ -53,7 +40,7 @@ use reth_payload_builder_primitives::PayloadBuilderError; use reth_payload_primitives::PayloadBuilderAttributes; use reth_payload_util::BestPayloadTransactions; use reth_payload_util::PayloadTransactions; -use reth_primitives::{transaction::SignedTransactionIntoRecoveredExt, BlockBody, SealedHeader}; +use reth_primitives::BlockBody; use reth_primitives_traits::proofs; use reth_primitives_traits::Block; use reth_primitives_traits::RecoveredBlock; @@ -66,15 +53,9 @@ use reth_revm::database::StateProviderDatabase; use reth_transaction_pool::BestTransactionsAttributes; use reth_transaction_pool::PoolTransaction; use reth_transaction_pool::TransactionPool; -use revm::{ - db::{states::bundle_state::BundleRetention, State}, - primitives::{ExecutionResult, ResultAndState}, - DatabaseCommit, -}; -use std::error::Error as StdError; -use std::{fmt::Display, sync::Arc, time::Instant}; -use tokio_util::sync::CancellationToken; -use tracing::{info, trace, warn}; +use revm::db::{states::bundle_state::BundleRetention, State}; +use std::{sync::Arc, time::Instant}; +use tracing::{info, warn}; #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] @@ -691,544 +672,3 @@ impl OpPayloadTransactions for () { BestPayloadTransactions::new(pool.best_transactions_with_attributes(attr)) } } - -/// Container type that holds all necessities to build a new payload. -#[derive(Debug)] -pub struct OpPayloadBuilderCtx { - /// The type that knows how to perform system calls and configure the evm. - pub evm_config: EvmConfig, - /// The DA config for the payload builder - pub da_config: OpDAConfig, - /// The chainspec - pub chain_spec: Arc, - /// How to build the payload. - pub config: PayloadConfig>, - /// Evm Settings - pub evm_env: EvmEnv, - /// Marker to check whether the job has been cancelled. - pub cancel: CancellationToken, - /// Receipt builder. - pub receipt_builder: Arc>, - /// The builder signer - pub builder_signer: Option, - /// The metrics for the builder - pub metrics: OpRBuilderMetrics, -} - -impl OpPayloadBuilderCtx -where - EvmConfig: ConfigureEvmEnv, - ChainSpec: EthChainSpec + OpHardforks, - N: NodePrimitives, -{ - /// Returns the parent block the payload will be build on. - pub fn parent(&self) -> &SealedHeader { - &self.config.parent_header - } - - /// Returns the builder attributes. - pub const fn attributes(&self) -> &OpPayloadBuilderAttributes { - &self.config.attributes - } - - /// Returns the withdrawals if shanghai is active. - pub fn withdrawals(&self) -> Option<&Withdrawals> { - self.chain_spec - .is_shanghai_active_at_timestamp(self.attributes().timestamp()) - .then(|| &self.attributes().payload_attributes.withdrawals) - } - - /// Returns the block gas limit to target. - pub fn block_gas_limit(&self) -> u64 { - self.attributes() - .gas_limit - .unwrap_or_else(|| self.evm_env.block_env.gas_limit.saturating_to()) - } - - /// Returns the block number for the block. - pub fn block_number(&self) -> u64 { - self.evm_env.block_env.number.to() - } - - /// Returns the current base fee - pub fn base_fee(&self) -> u64 { - self.evm_env.block_env.basefee.to() - } - - /// Returns the current blob gas price. - pub fn get_blob_gasprice(&self) -> Option { - self.evm_env - .block_env - .get_blob_gasprice() - .map(|gasprice| gasprice as u64) - } - - /// Returns the blob fields for the header. - /// - /// This will always return `Some(0)` after ecotone. - pub fn blob_fields(&self) -> (Option, Option) { - // OP doesn't support blobs/EIP-4844. - // https://specs.optimism.io/protocol/exec-engine.html#ecotone-disable-blob-transactions - // Need [Some] or [None] based on hardfork to match block hash. - if self.is_ecotone_active() { - (Some(0), Some(0)) - } else { - (None, None) - } - } - - /// Returns the extra data for the block. - /// - /// After holocene this extracts the extradata from the paylpad - pub fn extra_data(&self) -> Result { - if self.is_holocene_active() { - self.attributes() - .get_holocene_extra_data( - self.chain_spec.base_fee_params_at_timestamp( - self.attributes().payload_attributes.timestamp, - ), - ) - .map_err(PayloadBuilderError::other) - } else { - Ok(Default::default()) - } - } - - /// Returns the current fee settings for transactions from the mempool - pub fn best_transaction_attributes(&self) -> BestTransactionsAttributes { - BestTransactionsAttributes::new(self.base_fee(), self.get_blob_gasprice()) - } - - /// Returns the unique id for this payload job. - pub fn payload_id(&self) -> PayloadId { - self.attributes().payload_id() - } - - /// Returns true if regolith is active for the payload. - pub fn is_regolith_active(&self) -> bool { - self.chain_spec - .is_regolith_active_at_timestamp(self.attributes().timestamp()) - } - - /// Returns true if ecotone is active for the payload. - pub fn is_ecotone_active(&self) -> bool { - self.chain_spec - .is_ecotone_active_at_timestamp(self.attributes().timestamp()) - } - - /// Returns true if canyon is active for the payload. - pub fn is_canyon_active(&self) -> bool { - self.chain_spec - .is_canyon_active_at_timestamp(self.attributes().timestamp()) - } - - /// Returns true if holocene is active for the payload. - pub fn is_holocene_active(&self) -> bool { - self.chain_spec - .is_holocene_active_at_timestamp(self.attributes().timestamp()) - } - - /// Returns true if isthmus is active for the payload. - pub fn is_isthmus_active(&self) -> bool { - self.chain_spec - .is_isthmus_active_at_timestamp(self.attributes().timestamp()) - } - - /// Returns the chain id - pub fn chain_id(&self) -> u64 { - self.chain_spec.chain_id() - } - - /// Returns the builder signer - pub fn builder_signer(&self) -> Option { - self.builder_signer - } - - /// Ensure that the create2deployer is force-deployed at the canyon transition. Optimism - /// blocks will always have at least a single transaction in them (the L1 info transaction), - /// so we can safely assume that this will always be triggered upon the transition and that - /// the above check for empty blocks will never be hit on OP chains. - pub fn ensure_create2_deployer(&self, db: &mut State) -> Result<(), PayloadBuilderError> - where - DB: Database, - DB::Error: Display, - { - reth_optimism_evm::ensure_create2_deployer( - self.chain_spec.clone(), - self.attributes().payload_attributes.timestamp, - db, - ) - .map_err(|err| { - warn!(target: "payload_builder", %err, "missing create2 deployer, skipping block."); - PayloadBuilderError::other(OpPayloadBuilderError::ForceCreate2DeployerFail) - }) - } -} - -impl OpPayloadBuilderCtx -where - EvmConfig: ConfigureEvmFor, - ChainSpec: EthChainSpec + OpHardforks, - N: OpPayloadPrimitives<_TX = OpTransactionSigned>, -{ - /// apply eip-4788 pre block contract call - pub fn apply_pre_beacon_root_contract_call( - &self, - db: &mut DB, - ) -> Result<(), PayloadBuilderError> - where - DB: Database + DatabaseCommit, - DB::Error: Display, - ::Error: StdError, - { - SystemCaller::new(self.evm_config.clone(), self.chain_spec.clone()) - .pre_block_beacon_root_contract_call( - db, - &self.evm_env, - self.attributes() - .payload_attributes - .parent_beacon_block_root, - ) - .map_err(|err| { - warn!(target: "payload_builder", - parent_header=%self.parent().hash(), - %err, - "failed to apply beacon root contract call for payload" - ); - PayloadBuilderError::Internal(err.into()) - })?; - - Ok(()) - } - - /// Constructs a receipt for the given transaction. - fn build_receipt( - &self, - info: &ExecutionInfo, - result: ExecutionResult, - deposit_nonce: Option, - tx: &N::SignedTx, - ) -> N::Receipt { - match self.receipt_builder.build_receipt(ReceiptBuilderCtx { - tx, - result, - cumulative_gas_used: info.cumulative_gas_used, - }) { - Ok(receipt) => receipt, - Err(ctx) => { - let receipt = alloy_consensus::Receipt { - // Success flag was added in `EIP-658: Embedding transaction status code - // in receipts`. - status: Eip658Value::Eip658(ctx.result.is_success()), - cumulative_gas_used: ctx.cumulative_gas_used, - logs: ctx.result.into_logs(), - }; - - self.receipt_builder - .build_deposit_receipt(OpDepositReceipt { - inner: receipt, - deposit_nonce, - // The deposit receipt version was introduced in Canyon to indicate an - // update to how receipt hashes should be computed - // when set. The state transition process ensures - // this is only set for post-Canyon deposit - // transactions. - deposit_receipt_version: self.is_canyon_active().then_some(1), - }) - } - } - } - - /// Executes all sequencer transactions that are included in the payload attributes. - pub fn execute_sequencer_transactions( - &self, - db: &mut State, - ) -> Result, PayloadBuilderError> - where - DB: Database, - { - let mut info = ExecutionInfo::with_capacity(self.attributes().transactions.len()); - - let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); - - for sequencer_tx in &self.attributes().transactions { - // A sequencer's block should never contain blob transactions. - if sequencer_tx.value().is_eip4844() { - return Err(PayloadBuilderError::other( - OpPayloadBuilderError::BlobTransactionRejected, - )); - } - - // Convert the transaction to a [Recovered]. This is - // purely for the purposes of utilizing the `evm_config.tx_env`` function. - // Deposit transactions do not have signatures, so if the tx is a deposit, this - // will just pull in its `from` address. - let sequencer_tx = sequencer_tx - .value() - .try_clone_into_recovered() - .map_err(|_| { - PayloadBuilderError::other(OpPayloadBuilderError::TransactionEcRecoverFailed) - })?; - - // Cache the depositor account prior to the state transition for the deposit nonce. - // - // Note that this *only* needs to be done post-regolith hardfork, as deposit nonces - // were not introduced in Bedrock. In addition, regular transactions don't have deposit - // nonces, so we don't need to touch the DB for those. - let depositor_nonce = (self.is_regolith_active() && sequencer_tx.is_deposit()) - .then(|| { - evm.db_mut() - .load_cache_account(sequencer_tx.signer()) - .map(|acc| acc.account_info().unwrap_or_default().nonce) - }) - .transpose() - .map_err(|_| { - PayloadBuilderError::other(OpPayloadBuilderError::AccountLoadFailed( - sequencer_tx.signer(), - )) - })?; - - let tx_env = self - .evm_config - .tx_env(sequencer_tx.tx(), sequencer_tx.signer()); - - let ResultAndState { result, state } = match evm.transact(tx_env) { - Ok(res) => res, - Err(err) => { - if err.is_invalid_tx_err() { - trace!(target: "payload_builder", %err, ?sequencer_tx, "Error in sequencer transaction, skipping."); - continue; - } - // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(Box::new(err))); - } - }; - - // commit changes - evm.db_mut().commit(state); - - let gas_used = result.gas_used(); - - // add gas used by the transaction to cumulative gas used, before creating the receipt - info.cumulative_gas_used += gas_used; - - info.receipts.push(self.build_receipt( - &info, - result, - depositor_nonce, - sequencer_tx.tx(), - )); - - // append sender and transaction to the respective lists - info.executed_senders.push(sequencer_tx.signer()); - info.executed_transactions.push(sequencer_tx.into_tx()); - } - - Ok(info) - } - - /// Executes the given best transactions and updates the execution info. - /// - /// Returns `Ok(Some(())` if the job was cancelled. - pub fn execute_best_transactions( - &self, - info: &mut ExecutionInfo, - db: &mut State, - mut best_txs: impl PayloadTransactions< - Transaction: PoolTransaction, - >, - block_gas_limit: u64, - block_da_limit: Option, - ) -> Result, PayloadBuilderError> - where - DB: Database, - { - let execute_txs_start_time = Instant::now(); - let mut num_txs_considered = 0; - let mut num_txs_simulated = 0; - let mut num_txs_simulated_success = 0; - let mut num_txs_simulated_fail = 0; - let base_fee = self.base_fee(); - let tx_da_limit = self.da_config.max_da_tx_size(); - let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); - - while let Some(tx) = best_txs.next(()) { - let tx = tx.into_consensus(); - num_txs_considered += 1; - // ensure we still have capacity for this transaction - if info.is_tx_over_limits(tx.tx(), block_gas_limit, tx_da_limit, block_da_limit) { - // we can't fit this transaction into the block, so we need to mark it as - // invalid which also removes all dependent transaction from - // the iterator before we can continue - best_txs.mark_invalid(tx.signer(), tx.nonce()); - continue; - } - - // A sequencer's block should never contain blob or deposit transactions from the pool. - if tx.is_eip4844() || tx.is_deposit() { - best_txs.mark_invalid(tx.signer(), tx.nonce()); - continue; - } - - // check if the job was cancelled, if so we can exit early - if self.cancel.is_cancelled() { - return Ok(Some(())); - } - - // Configure the environment for the tx. - let tx_env = self.evm_config.tx_env(tx.tx(), tx.signer()); - - let tx_simulation_start_time = Instant::now(); - - let ResultAndState { result, state } = match evm.transact(tx_env) { - Ok(res) => res, - Err(err) => { - if let Some(err) = err.as_invalid_tx_err() { - if err.is_nonce_too_low() { - // if the nonce is too low, we can skip this transaction - trace!(target: "payload_builder", %err, ?tx, "skipping nonce too low transaction"); - } else { - // if the transaction is invalid, we can skip it and all of its - // descendants - trace!(target: "payload_builder", %err, ?tx, "skipping invalid transaction and its descendants"); - best_txs.mark_invalid(tx.signer(), tx.nonce()); - } - - continue; - } - // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(Box::new(err))); - } - }; - - self.metrics - .tx_simulation_duration - .record(tx_simulation_start_time.elapsed()); - self.metrics.tx_byte_size.record(tx.tx().size() as f64); - num_txs_simulated += 1; - if result.is_success() { - num_txs_simulated_success += 1; - } else { - num_txs_simulated_fail += 1; - continue; - } - - // commit changes - evm.db_mut().commit(state); - - let gas_used = result.gas_used(); - - // add gas used by the transaction to cumulative gas used, before creating the - // receipt - info.cumulative_gas_used += gas_used; - - // Push transaction changeset and calculate header bloom filter for receipt. - info.receipts - .push(self.build_receipt(info, result, None, &tx)); - - // update add to total fees - let miner_fee = tx - .effective_tip_per_gas(base_fee) - .expect("fee is always valid; execution succeeded"); - info.total_fees += U256::from(miner_fee) * U256::from(gas_used); - - // append sender and transaction to the respective lists - info.executed_senders.push(tx.signer()); - info.executed_transactions.push(tx.into_tx()); - } - - self.metrics - .payload_tx_simulation_duration - .record(execute_txs_start_time.elapsed()); - self.metrics - .payload_num_tx_considered - .record(num_txs_considered as f64); - self.metrics - .payload_num_tx_simulated - .record(num_txs_simulated as f64); - self.metrics - .payload_num_tx_simulated_success - .record(num_txs_simulated_success as f64); - self.metrics - .payload_num_tx_simulated_fail - .record(num_txs_simulated_fail as f64); - - Ok(None) - } - - pub fn add_builder_tx( - &self, - info: &mut ExecutionInfo, - db: &mut State, - builder_tx_gas: u64, - message: Vec, - ) -> Option<()> - where - DB: Database, - { - self.builder_signer() - .map(|signer| { - let base_fee = self.base_fee(); - let chain_id = self.chain_id(); - // Create and sign the transaction - let builder_tx = - signed_builder_tx(db, builder_tx_gas, message, signer, base_fee, chain_id)?; - - let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); - let tx_env = self.evm_config.tx_env(builder_tx.tx(), builder_tx.signer()); - - let ResultAndState { result, state } = evm - .transact(tx_env) - .map_err(|err| PayloadBuilderError::EvmExecutionError(Box::new(err)))?; - - // Release the db reference by dropping evm - drop(evm); - // Commit changes - db.commit(state); - - let gas_used = result.gas_used(); - - // Add gas used by the transaction to cumulative gas used, before creating the receipt - info.cumulative_gas_used += gas_used; - - info.receipts - .push(self.build_receipt(info, result, None, &builder_tx)); - - // Append sender and transaction to the respective lists - info.executed_senders.push(builder_tx.signer()); - info.executed_transactions.push(builder_tx.into_tx()); - Ok(()) - }) - .transpose() - .unwrap_or_else(|err: PayloadBuilderError| { - warn!(target: "payload_builder", %err, "Failed to add builder transaction"); - None - }) - } - - /// Calculates EIP 2718 builder transaction size - pub fn estimate_builder_tx_da_size( - &self, - db: &mut State, - builder_tx_gas: u64, - message: Vec, - ) -> Option - where - DB: Database, - { - self.builder_signer() - .map(|signer| { - let base_fee = self.base_fee(); - let chain_id = self.chain_id(); - // Create and sign the transaction - let builder_tx = - signed_builder_tx(db, builder_tx_gas, message, signer, base_fee, chain_id)?; - Ok(builder_tx.length()) - }) - .transpose() - .unwrap_or_else(|err: PayloadBuilderError| { - warn!(target: "payload_builder", %err, "Failed to add builder transaction"); - None - }) - } -} diff --git a/crates/op-rbuilder/src/primitives/mod.rs b/crates/op-rbuilder/src/primitives/mod.rs index 604fda1d3..3fd6782a7 100644 --- a/crates/op-rbuilder/src/primitives/mod.rs +++ b/crates/op-rbuilder/src/primitives/mod.rs @@ -1,6 +1,7 @@ //! This module contains types from the reth that weren't heavily modified mod execution; mod helpers; +mod payload_builder_ctx; mod payload_builder_service; pub use payload_builder_service::PayloadBuilderService; @@ -8,3 +9,5 @@ pub use payload_builder_service::PayloadBuilderService; pub use helpers::{estimate_gas_for_builder_tx, signed_builder_tx}; pub use execution::{ExecutedPayload, ExecutionInfo}; + +pub use payload_builder_ctx::OpPayloadBuilderCtx; diff --git a/crates/op-rbuilder/src/primitives/payload_builder_ctx.rs b/crates/op-rbuilder/src/primitives/payload_builder_ctx.rs new file mode 100644 index 000000000..c73cee83b --- /dev/null +++ b/crates/op-rbuilder/src/primitives/payload_builder_ctx.rs @@ -0,0 +1,581 @@ +use super::super::{metrics::OpRBuilderMetrics, tx_signer::Signer}; +use super::{signed_builder_tx, ExecutionInfo}; +use alloy_consensus::{Eip658Value, Transaction, Typed2718}; +use alloy_primitives::private::alloy_rlp::Encodable; +use alloy_primitives::{Bytes, U256}; +use alloy_rpc_types_engine::PayloadId; +use alloy_rpc_types_eth::Withdrawals; +use op_alloy_consensus::OpDepositReceipt; +use reth::core::primitives::InMemorySize; +use reth_basic_payload_builder::PayloadConfig; +use reth_chainspec::{EthChainSpec, EthereumHardforks}; +use reth_evm::{ + env::EvmEnv, system_calls::SystemCaller, ConfigureEvmEnv, ConfigureEvmFor, Database, Evm, + EvmError, InvalidTxError, +}; +use reth_node_api::NodePrimitives; +use reth_optimism_evm::{OpReceiptBuilder, ReceiptBuilderCtx}; +use reth_optimism_forks::OpHardforks; +use reth_optimism_payload_builder::config::OpDAConfig; +use reth_optimism_payload_builder::OpPayloadPrimitives; +use reth_optimism_payload_builder::{ + error::OpPayloadBuilderError, payload::OpPayloadBuilderAttributes, +}; +use reth_optimism_primitives::OpTransactionSigned; +use reth_payload_builder_primitives::PayloadBuilderError; +use reth_payload_primitives::PayloadBuilderAttributes; +use reth_payload_util::PayloadTransactions; +use reth_primitives::{transaction::SignedTransactionIntoRecoveredExt, SealedHeader}; +use reth_provider::ProviderError; +use reth_transaction_pool::BestTransactionsAttributes; +use reth_transaction_pool::PoolTransaction; +use revm::{ + db::State, + primitives::{ExecutionResult, ResultAndState}, + DatabaseCommit, +}; +use std::error::Error as StdError; +use std::{fmt::Display, sync::Arc, time::Instant}; +use tokio_util::sync::CancellationToken; +use tracing::{trace, warn}; + +/// Container type that holds all necessities to build a new payload. +#[derive(Debug)] +pub struct OpPayloadBuilderCtx { + /// The type that knows how to perform system calls and configure the evm. + pub evm_config: EvmConfig, + /// The DA config for the payload builder + pub da_config: OpDAConfig, + /// The chainspec + pub chain_spec: Arc, + /// How to build the payload. + pub config: PayloadConfig>, + /// Evm Settings + pub evm_env: EvmEnv, + /// Marker to check whether the job has been cancelled. + pub cancel: CancellationToken, + /// Receipt builder. + pub receipt_builder: Arc>, + /// The builder signer + pub builder_signer: Option, + /// The metrics for the builder + pub metrics: OpRBuilderMetrics, +} + +impl OpPayloadBuilderCtx +where + EvmConfig: ConfigureEvmEnv, + ChainSpec: EthChainSpec + OpHardforks, + N: NodePrimitives, +{ + /// Returns the parent block the payload will be build on. + pub fn parent(&self) -> &SealedHeader { + &self.config.parent_header + } + + /// Returns the builder attributes. + pub const fn attributes(&self) -> &OpPayloadBuilderAttributes { + &self.config.attributes + } + + /// Returns the withdrawals if shanghai is active. + pub fn withdrawals(&self) -> Option<&Withdrawals> { + self.chain_spec + .is_shanghai_active_at_timestamp(self.attributes().timestamp()) + .then(|| &self.attributes().payload_attributes.withdrawals) + } + + /// Returns the block gas limit to target. + pub fn block_gas_limit(&self) -> u64 { + self.attributes() + .gas_limit + .unwrap_or_else(|| self.evm_env.block_env.gas_limit.saturating_to()) + } + + /// Returns the block number for the block. + pub fn block_number(&self) -> u64 { + self.evm_env.block_env.number.to() + } + + /// Returns the current base fee + pub fn base_fee(&self) -> u64 { + self.evm_env.block_env.basefee.to() + } + + /// Returns the current blob gas price. + pub fn get_blob_gasprice(&self) -> Option { + self.evm_env + .block_env + .get_blob_gasprice() + .map(|gasprice| gasprice as u64) + } + + /// Returns the blob fields for the header. + /// + /// This will always return `Some(0)` after ecotone. + pub fn blob_fields(&self) -> (Option, Option) { + // OP doesn't support blobs/EIP-4844. + // https://specs.optimism.io/protocol/exec-engine.html#ecotone-disable-blob-transactions + // Need [Some] or [None] based on hardfork to match block hash. + if self.is_ecotone_active() { + (Some(0), Some(0)) + } else { + (None, None) + } + } + + /// Returns the extra data for the block. + /// + /// After holocene this extracts the extradata from the paylpad + pub fn extra_data(&self) -> Result { + if self.is_holocene_active() { + self.attributes() + .get_holocene_extra_data( + self.chain_spec.base_fee_params_at_timestamp( + self.attributes().payload_attributes.timestamp, + ), + ) + .map_err(PayloadBuilderError::other) + } else { + Ok(Default::default()) + } + } + + /// Returns the current fee settings for transactions from the mempool + pub fn best_transaction_attributes(&self) -> BestTransactionsAttributes { + BestTransactionsAttributes::new(self.base_fee(), self.get_blob_gasprice()) + } + + /// Returns the unique id for this payload job. + pub fn payload_id(&self) -> PayloadId { + self.attributes().payload_id() + } + + /// Returns true if regolith is active for the payload. + pub fn is_regolith_active(&self) -> bool { + self.chain_spec + .is_regolith_active_at_timestamp(self.attributes().timestamp()) + } + + /// Returns true if ecotone is active for the payload. + pub fn is_ecotone_active(&self) -> bool { + self.chain_spec + .is_ecotone_active_at_timestamp(self.attributes().timestamp()) + } + + /// Returns true if canyon is active for the payload. + pub fn is_canyon_active(&self) -> bool { + self.chain_spec + .is_canyon_active_at_timestamp(self.attributes().timestamp()) + } + + /// Returns true if holocene is active for the payload. + pub fn is_holocene_active(&self) -> bool { + self.chain_spec + .is_holocene_active_at_timestamp(self.attributes().timestamp()) + } + + /// Returns true if isthmus is active for the payload. + pub fn is_isthmus_active(&self) -> bool { + self.chain_spec + .is_isthmus_active_at_timestamp(self.attributes().timestamp()) + } + + /// Returns the chain id + pub fn chain_id(&self) -> u64 { + self.chain_spec.chain_id() + } + + /// Returns the builder signer + pub fn builder_signer(&self) -> Option { + self.builder_signer + } + + /// Ensure that the create2deployer is force-deployed at the canyon transition. Optimism + /// blocks will always have at least a single transaction in them (the L1 info transaction), + /// so we can safely assume that this will always be triggered upon the transition and that + /// the above check for empty blocks will never be hit on OP chains. + pub fn ensure_create2_deployer(&self, db: &mut State) -> Result<(), PayloadBuilderError> + where + DB: Database, + DB::Error: Display, + { + reth_optimism_evm::ensure_create2_deployer( + self.chain_spec.clone(), + self.attributes().payload_attributes.timestamp, + db, + ) + .map_err(|err| { + warn!(target: "payload_builder", %err, "missing create2 deployer, skipping block."); + PayloadBuilderError::other(OpPayloadBuilderError::ForceCreate2DeployerFail) + }) + } +} + +impl OpPayloadBuilderCtx +where + EvmConfig: ConfigureEvmFor, + ChainSpec: EthChainSpec + OpHardforks, + N: OpPayloadPrimitives<_TX = OpTransactionSigned>, +{ + /// apply eip-4788 pre block contract call + pub fn apply_pre_beacon_root_contract_call( + &self, + db: &mut DB, + ) -> Result<(), PayloadBuilderError> + where + DB: Database + DatabaseCommit, + DB::Error: Display, + ::Error: StdError, + { + SystemCaller::new(self.evm_config.clone(), self.chain_spec.clone()) + .pre_block_beacon_root_contract_call( + db, + &self.evm_env, + self.attributes() + .payload_attributes + .parent_beacon_block_root, + ) + .map_err(|err| { + warn!(target: "payload_builder", + parent_header=%self.parent().hash(), + %err, + "failed to apply beacon root contract call for payload" + ); + PayloadBuilderError::Internal(err.into()) + })?; + + Ok(()) + } + + /// Constructs a receipt for the given transaction. + fn build_receipt( + &self, + info: &ExecutionInfo, + result: ExecutionResult, + deposit_nonce: Option, + tx: &N::SignedTx, + ) -> N::Receipt { + match self.receipt_builder.build_receipt(ReceiptBuilderCtx { + tx, + result, + cumulative_gas_used: info.cumulative_gas_used, + }) { + Ok(receipt) => receipt, + Err(ctx) => { + let receipt = alloy_consensus::Receipt { + // Success flag was added in `EIP-658: Embedding transaction status code + // in receipts`. + status: Eip658Value::Eip658(ctx.result.is_success()), + cumulative_gas_used: ctx.cumulative_gas_used, + logs: ctx.result.into_logs(), + }; + + self.receipt_builder + .build_deposit_receipt(OpDepositReceipt { + inner: receipt, + deposit_nonce, + // The deposit receipt version was introduced in Canyon to indicate an + // update to how receipt hashes should be computed + // when set. The state transition process ensures + // this is only set for post-Canyon deposit + // transactions. + deposit_receipt_version: self.is_canyon_active().then_some(1), + }) + } + } + } + + /// Executes all sequencer transactions that are included in the payload attributes. + pub fn execute_sequencer_transactions( + &self, + db: &mut State, + ) -> Result, PayloadBuilderError> + where + DB: Database, + { + let mut info = ExecutionInfo::with_capacity(self.attributes().transactions.len()); + + let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); + + for sequencer_tx in &self.attributes().transactions { + // A sequencer's block should never contain blob transactions. + if sequencer_tx.value().is_eip4844() { + return Err(PayloadBuilderError::other( + OpPayloadBuilderError::BlobTransactionRejected, + )); + } + + // Convert the transaction to a [Recovered]. This is + // purely for the purposes of utilizing the `evm_config.tx_env`` function. + // Deposit transactions do not have signatures, so if the tx is a deposit, this + // will just pull in its `from` address. + let sequencer_tx = sequencer_tx + .value() + .try_clone_into_recovered() + .map_err(|_| { + PayloadBuilderError::other(OpPayloadBuilderError::TransactionEcRecoverFailed) + })?; + + // Cache the depositor account prior to the state transition for the deposit nonce. + // + // Note that this *only* needs to be done post-regolith hardfork, as deposit nonces + // were not introduced in Bedrock. In addition, regular transactions don't have deposit + // nonces, so we don't need to touch the DB for those. + let depositor_nonce = (self.is_regolith_active() && sequencer_tx.is_deposit()) + .then(|| { + evm.db_mut() + .load_cache_account(sequencer_tx.signer()) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + }) + .transpose() + .map_err(|_| { + PayloadBuilderError::other(OpPayloadBuilderError::AccountLoadFailed( + sequencer_tx.signer(), + )) + })?; + + let tx_env = self + .evm_config + .tx_env(sequencer_tx.tx(), sequencer_tx.signer()); + + let ResultAndState { result, state } = match evm.transact(tx_env) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + trace!(target: "payload_builder", %err, ?sequencer_tx, "Error in sequencer transaction, skipping."); + continue; + } + // this is an error that we should treat as fatal for this attempt + return Err(PayloadBuilderError::EvmExecutionError(Box::new(err))); + } + }; + + // commit changes + evm.db_mut().commit(state); + + let gas_used = result.gas_used(); + + // add gas used by the transaction to cumulative gas used, before creating the receipt + info.cumulative_gas_used += gas_used; + + info.receipts.push(self.build_receipt( + &info, + result, + depositor_nonce, + sequencer_tx.tx(), + )); + + // append sender and transaction to the respective lists + info.executed_senders.push(sequencer_tx.signer()); + info.executed_transactions.push(sequencer_tx.into_tx()); + } + + Ok(info) + } + + /// Executes the given best transactions and updates the execution info. + /// + /// Returns `Ok(Some(())` if the job was cancelled. + pub fn execute_best_transactions( + &self, + info: &mut ExecutionInfo, + db: &mut State, + mut best_txs: impl PayloadTransactions< + Transaction: PoolTransaction, + >, + block_gas_limit: u64, + block_da_limit: Option, + ) -> Result, PayloadBuilderError> + where + DB: Database, + { + let execute_txs_start_time = Instant::now(); + let mut num_txs_considered = 0; + let mut num_txs_simulated = 0; + let mut num_txs_simulated_success = 0; + let mut num_txs_simulated_fail = 0; + let base_fee = self.base_fee(); + let tx_da_limit = self.da_config.max_da_tx_size(); + let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); + + while let Some(tx) = best_txs.next(()) { + let tx = tx.into_consensus(); + num_txs_considered += 1; + // ensure we still have capacity for this transaction + if info.is_tx_over_limits(tx.tx(), block_gas_limit, tx_da_limit, block_da_limit) { + // we can't fit this transaction into the block, so we need to mark it as + // invalid which also removes all dependent transaction from + // the iterator before we can continue + best_txs.mark_invalid(tx.signer(), tx.nonce()); + continue; + } + + // A sequencer's block should never contain blob or deposit transactions from the pool. + if tx.is_eip4844() || tx.is_deposit() { + best_txs.mark_invalid(tx.signer(), tx.nonce()); + continue; + } + + // check if the job was cancelled, if so we can exit early + if self.cancel.is_cancelled() { + return Ok(Some(())); + } + + // Configure the environment for the tx. + let tx_env = self.evm_config.tx_env(tx.tx(), tx.signer()); + + let tx_simulation_start_time = Instant::now(); + + let ResultAndState { result, state } = match evm.transact(tx_env) { + Ok(res) => res, + Err(err) => { + if let Some(err) = err.as_invalid_tx_err() { + if err.is_nonce_too_low() { + // if the nonce is too low, we can skip this transaction + trace!(target: "payload_builder", %err, ?tx, "skipping nonce too low transaction"); + } else { + // if the transaction is invalid, we can skip it and all of its + // descendants + trace!(target: "payload_builder", %err, ?tx, "skipping invalid transaction and its descendants"); + best_txs.mark_invalid(tx.signer(), tx.nonce()); + } + + continue; + } + // this is an error that we should treat as fatal for this attempt + return Err(PayloadBuilderError::EvmExecutionError(Box::new(err))); + } + }; + + self.metrics + .tx_simulation_duration + .record(tx_simulation_start_time.elapsed()); + self.metrics.tx_byte_size.record(tx.tx().size() as f64); + num_txs_simulated += 1; + if result.is_success() { + num_txs_simulated_success += 1; + } else { + num_txs_simulated_fail += 1; + continue; + } + + // commit changes + evm.db_mut().commit(state); + + let gas_used = result.gas_used(); + + // add gas used by the transaction to cumulative gas used, before creating the + // receipt + info.cumulative_gas_used += gas_used; + + // Push transaction changeset and calculate header bloom filter for receipt. + info.receipts + .push(self.build_receipt(info, result, None, &tx)); + + // update add to total fees + let miner_fee = tx + .effective_tip_per_gas(base_fee) + .expect("fee is always valid; execution succeeded"); + info.total_fees += U256::from(miner_fee) * U256::from(gas_used); + + // append sender and transaction to the respective lists + info.executed_senders.push(tx.signer()); + info.executed_transactions.push(tx.into_tx()); + } + + self.metrics + .payload_tx_simulation_duration + .record(execute_txs_start_time.elapsed()); + self.metrics + .payload_num_tx_considered + .record(num_txs_considered as f64); + self.metrics + .payload_num_tx_simulated + .record(num_txs_simulated as f64); + self.metrics + .payload_num_tx_simulated_success + .record(num_txs_simulated_success as f64); + self.metrics + .payload_num_tx_simulated_fail + .record(num_txs_simulated_fail as f64); + + Ok(None) + } + + pub fn add_builder_tx( + &self, + info: &mut ExecutionInfo, + db: &mut State, + builder_tx_gas: u64, + message: Vec, + ) -> Option<()> + where + DB: Database, + { + self.builder_signer() + .map(|signer| { + let base_fee = self.base_fee(); + let chain_id = self.chain_id(); + // Create and sign the transaction + let builder_tx = + signed_builder_tx(db, builder_tx_gas, message, signer, base_fee, chain_id)?; + + let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); + let tx_env = self.evm_config.tx_env(builder_tx.tx(), builder_tx.signer()); + + let ResultAndState { result, state } = evm + .transact(tx_env) + .map_err(|err| PayloadBuilderError::EvmExecutionError(Box::new(err)))?; + + // Release the db reference by dropping evm + drop(evm); + // Commit changes + db.commit(state); + + let gas_used = result.gas_used(); + + // Add gas used by the transaction to cumulative gas used, before creating the receipt + info.cumulative_gas_used += gas_used; + + info.receipts + .push(self.build_receipt(info, result, None, &builder_tx)); + + // Append sender and transaction to the respective lists + info.executed_senders.push(builder_tx.signer()); + info.executed_transactions.push(builder_tx.into_tx()); + Ok(()) + }) + .transpose() + .unwrap_or_else(|err: PayloadBuilderError| { + warn!(target: "payload_builder", %err, "Failed to add builder transaction"); + None + }) + } + + /// Calculates EIP 2718 builder transaction size + pub fn estimate_builder_tx_da_size( + &self, + db: &mut State, + builder_tx_gas: u64, + message: Vec, + ) -> Option + where + DB: Database, + { + self.builder_signer() + .map(|signer| { + let base_fee = self.base_fee(); + let chain_id = self.chain_id(); + // Create and sign the transaction + let builder_tx = + signed_builder_tx(db, builder_tx_gas, message, signer, base_fee, chain_id)?; + Ok(builder_tx.length()) + }) + .transpose() + .unwrap_or_else(|err: PayloadBuilderError| { + warn!(target: "payload_builder", %err, "Failed to add builder transaction"); + None + }) + } +}