diff --git a/.dockerignore b/.dockerignore index 4f0245993..9847736d3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ /scripts/benchmark-results.* /test/ /integration_logs +Dockerfile # editors .code diff --git a/Dockerfile b/Dockerfile index f42aceeb8..4461168be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,15 +6,20 @@ # # Based on https://depot.dev/blog/rust-dockerfile-best-practices # +ARG FEATURES +ARG RBUILDER_BIN="rbuilder" + FROM rust:1.82 as base -ARG FEATURES +RUN apt-get update \ + && apt-get install -y clang libclang-dev + +RUN rustup component add clippy rustfmt + RUN cargo install sccache --version ^0.8 RUN cargo install cargo-chef --version ^0.1 -RUN apt-get update \ - && apt-get install -y clang libclang-dev ENV CARGO_HOME=/usr/local/cargo ENV RUSTC_WRAPPER=sccache @@ -37,34 +42,39 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ FROM base as builder WORKDIR /app COPY --from=planner /app/recipe.json recipe.json -RUN --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git \ + --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ cargo chef cook --release --recipe-path recipe.json COPY . . FROM builder as rbuilder -ARG RBUILDER_BIN="rbuilder" +ARG RBUILDER_BIN +ARG FEATURES RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ cargo build --release --features="$FEATURES" --package=${RBUILDER_BIN} FROM builder as test-relay +ARG FEATURES RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ cargo build --release --features="$FEATURES" --package=test-relay +# Runtime container for test-relay +FROM gcr.io/distroless/cc-debian12 as test-relay-runtime +WORKDIR /app +COPY --from=test-relay /app/target/release/test-relay /app/test-relay +ENTRYPOINT ["/app/test-relay"] # Runtime container for rbuilder FROM gcr.io/distroless/cc-debian12 as rbuilder-runtime +ARG RBUILDER_BIN WORKDIR /app -COPY --from=rbuilder /app/target/release/rbuilder /app/rbuilder +COPY --from=rbuilder /app/target/release/${RBUILDER_BIN} /app/rbuilder ENTRYPOINT ["/app/rbuilder"] -# Runtime container for test-relay -FROM gcr.io/distroless/cc-debian12 as test-relay-runtime -WORKDIR /app -COPY --from=test-relay /app/target/release/test-relay /app/test-relay -ENTRYPOINT ["/app/test-relay"] diff --git a/Makefile b/Makefile index a93e3c356..e39178100 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean: ## Clean up build: ## Build (debug version) cargo build --features "$(FEATURES)" -.PHONY: docker-image-rubilder +.PHONY: docker-image-rbuilder docker-image-rubilder: ## Build a rbuilder Docker image docker build --platform linux/amd64 --target rbuilder-runtime --build-arg FEATURES="$(FEATURES)" . -t rbuilder diff --git a/crates/op-rbuilder/src/payload_builder_vanilla.rs b/crates/op-rbuilder/src/payload_builder_vanilla.rs index 2fd93205e..8a5693ad3 100644 --- a/crates/op-rbuilder/src/payload_builder_vanilla.rs +++ b/crates/op-rbuilder/src/payload_builder_vanilla.rs @@ -1,12 +1,3 @@ -use alloy_rpc_types_eth::Withdrawals; -use reth::core::primitives::InMemorySize; -use reth_node_api::NodePrimitives; -use reth_optimism_evm::BasicOpReceiptBuilder; -use reth_optimism_evm::{OpReceiptBuilder, ReceiptBuilderCtx}; -use reth_optimism_payload_builder::OpPayloadPrimitives; -use reth_transaction_pool::PoolTransaction; -use std::{fmt::Display, sync::Arc, time::Instant}; - use crate::generator::BlockPayloadJobGenerator; use crate::generator::BuildArguments; use crate::{ @@ -14,16 +5,19 @@ use crate::{ metrics::OpRBuilderMetrics, tx_signer::Signer, }; +use alloy_consensus::constants::EMPTY_WITHDRAWALS; use alloy_consensus::{ Eip658Value, Header, Transaction, TxEip1559, Typed2718, EMPTY_OMMER_ROOT_HASH, }; use alloy_eips::merge::BEACON_NONCE; +use alloy_primitives::private::alloy_rlp::Encodable; use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types_engine::PayloadId; +use alloy_rpc_types_eth::Withdrawals; use op_alloy_consensus::{OpDepositReceipt, OpTypedTransaction}; use reth::builder::{components::PayloadServiceBuilder, node::FullNodeTypes, BuilderContext}; +use reth::core::primitives::InMemorySize; use reth::payload::PayloadBuilderHandle; -use reth_basic_payload_builder::commit_withdrawals; use reth_basic_payload_builder::{ BasicPayloadJobGeneratorConfig, BuildOutcome, BuildOutcomeKind, PayloadConfig, }; @@ -34,19 +28,25 @@ use reth_evm::{ EvmError, InvalidTxError, NextBlockEnvAttributes, }; use reth_execution_types::ExecutionOutcome; +use reth_node_api::NodePrimitives; use reth_node_api::NodeTypesWithEngine; use reth_node_api::TxTy; 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_forks::OpHardforks; use reth_optimism_node::OpEngineTypes; +use reth_optimism_payload_builder::config::{OpBuilderConfig, OpDAConfig}; +use reth_optimism_payload_builder::OpPayloadPrimitives; use reth_optimism_payload_builder::{ error::OpPayloadBuilderError, payload::{OpBuiltPayload, OpPayloadBuilderAttributes}, }; -use reth_optimism_primitives::OpPrimitives; -use reth_optimism_primitives::OpTransactionSigned; +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; @@ -63,6 +63,7 @@ use reth_provider::{ }; 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}, @@ -70,6 +71,7 @@ use revm::{ 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}; @@ -183,6 +185,8 @@ pub struct OpPayloadBuilderVanilla client: Client, receipt_builder: Arc>, ) -> Self { - Self { + Self::with_builder_config( evm_config, builder_signer, pool, client, + receipt_builder, + Default::default(), + ) + } + + pub fn with_builder_config( + evm_config: EvmConfig, + builder_signer: Option, + pool: Pool, + client: Client, + receipt_builder: Arc>, + config: OpBuilderConfig, + ) -> Self { + Self { + pool, + client, + receipt_builder, + config, + evm_config, best_transactions: (), metrics: Default::default(), - receipt_builder, + builder_signer, } } } @@ -301,6 +324,7 @@ where let ctx = OpPayloadBuilderCtx { evm_config: self.evm_config.clone(), + da_config: self.config.da_config.clone(), chain_spec: self.client.chain_spec(), config, evm_env, @@ -421,6 +445,20 @@ impl OpBuilder<'_, Txs> { .builder_signer() .map_or(0, |_| estimate_gas_for_builder_tx(message.clone())); let block_gas_limit = ctx.block_gas_limit() - builder_tx_gas; + // Save some space in the block_da_limit for builder tx + let builder_tx_da_size = ctx + .estimate_builder_tx_da_size(state, builder_tx_gas, message.clone()) + .unwrap_or(0); + let block_da_limit = ctx + .da_config + .max_da_block_size() + .map(|da_size| da_size - builder_tx_da_size as u64); + // Check that it's possible to create builder tx, considering max_da_tx_size, otherwise panic + if let Some(tx_da_limit) = ctx.da_config.max_da_tx_size() { + // Panic indicate max_da_tx_size misconfiguration + assert!(tx_da_limit >= builder_tx_da_size as u64); + } + if !ctx.attributes().no_tx_pool { let best_txs_start_time = Instant::now(); let best_txs = best(ctx.best_transaction_attributes()); @@ -428,7 +466,13 @@ impl OpBuilder<'_, Txs> { .transaction_pool_fetch_duration .record(best_txs_start_time.elapsed()); if ctx - .execute_best_transactions(&mut info, state, best_txs, block_gas_limit)? + .execute_best_transactions( + &mut info, + state, + best_txs, + block_gas_limit, + block_da_limit, + )? .is_some() { return Ok(BuildOutcomeKind::Cancelled); @@ -438,8 +482,6 @@ impl OpBuilder<'_, Txs> { // Add builder tx to the block ctx.add_builder_tx(&mut info, state, builder_tx_gas, message); - let withdrawals_root = ctx.commit_withdrawals(state)?; - let state_merge_start_time = Instant::now(); // merge all transitions into bundle state, this would apply the withdrawal balance changes @@ -453,12 +495,27 @@ impl OpBuilder<'_, Txs> { .payload_num_tx .record(info.executed_transactions.len() as f64); - Ok(BuildOutcomeKind::Better { - payload: ExecutedPayload { - info, - withdrawals_root, - }, - }) + let withdrawals_root = if ctx.is_isthmus_active() { + // withdrawals root field in block header is used for storage root of L2 predeploy + // `l2tol1-message-passer` + Some( + state + .database + .as_ref() + .storage_root(ADDRESS_L2_TO_L1_MESSAGE_PASSER, Default::default())?, + ) + } else if ctx.is_canyon_active() { + Some(EMPTY_WITHDRAWALS) + } else { + None + }; + + let payload = ExecutedPayload { + info, + withdrawals_root, + }; + + Ok(BuildOutcomeKind::Better { payload }) } /// Builds the payload on top of the state. @@ -651,6 +708,8 @@ pub struct ExecutionInfo { 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, } @@ -663,9 +722,36 @@ impl ExecutionInfo { 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. @@ -673,6 +759,8 @@ impl ExecutionInfo { 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. @@ -802,8 +890,13 @@ where .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() } @@ -813,19 +906,6 @@ where self.builder_signer } - /// Commits the withdrawals from the payload attributes to the state. - pub fn commit_withdrawals(&self, db: &mut State) -> Result, ProviderError> - where - DB: Database, - { - commit_withdrawals( - db, - &self.chain_spec, - self.attributes().payload_attributes.timestamp, - &self.attributes().payload_attributes.withdrawals, - ) - } - /// 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 @@ -1020,6 +1100,7 @@ where Transaction: PoolTransaction, >, block_gas_limit: u64, + block_da_limit: Option, ) -> Result, PayloadBuilderError> where DB: Database, @@ -1030,14 +1111,14 @@ where 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.cumulative_gas_used + tx.gas_limit() > block_gas_limit { + 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 @@ -1207,6 +1288,54 @@ where 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(); + // 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 eip1559 = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: self.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() + }); + let tx = eip1559; + // Sign the transaction + let builder_tx = signer.sign_tx(tx).map_err(PayloadBuilderError::other)?; + + Ok(builder_tx.length()) + }) + .transpose() + .unwrap_or_else(|err: PayloadBuilderError| { + warn!(target: "payload_builder", %err, "Failed to add builder transaction"); + None + }) + } } fn estimate_gas_for_builder_tx(input: Vec) -> u64 { diff --git a/crates/test-relay/src/main.rs b/crates/test-relay/src/main.rs index 700fd2483..72fa810db 100644 --- a/crates/test-relay/src/main.rs +++ b/crates/test-relay/src/main.rs @@ -1,4 +1,5 @@ use crate::validation_api_client::ValidationAPIClient; +use ahash::HashMap; use metrics::spawn_metrics_server; use rbuilder::{ beacon_api_client::Client, @@ -60,9 +61,19 @@ struct Cli { #[clap( long, help = "CL clients to fetch mev boost slot data", - env = "CL_CLIENTS" + env = "CL_CLIENTS", + value_delimiter = ',', + value_parser )] cl_clients: Vec, + #[clap( + long, + help = "Map builder relay key to name, e.g. abb3..ca6c:staging-01", + env = "BUILDER_NAMES", + value_delimiter = ',', + value_parser + )] + builder_names: Vec, } #[tokio::main] @@ -102,11 +113,24 @@ async fn main() -> eyre::Result<()> { None }; + let builder_names = { + let mut map = HashMap::default(); + for arg in cli.builder_names { + let arg: Vec<_> = arg.split(':').collect(); + if arg.len() != 2 { + eyre::bail!("Expected builder name with format \"[]:\" (e.g. \"abb3..ca6c:staging-01\""); + } + map.insert(arg[0].to_string(), arg[1].to_string()); + } + map + }; + spawn_relay_server( cli.listen_address, validation_client, cl_clients, relay, + builder_names, global_cancellation.clone(), )?; diff --git a/crates/test-relay/src/relay.rs b/crates/test-relay/src/relay.rs index ca4793d10..52151f1fc 100644 --- a/crates/test-relay/src/relay.rs +++ b/crates/test-relay/src/relay.rs @@ -3,7 +3,7 @@ use crate::{ add_payload_processing_time, add_payload_validation_time, add_winning_bid, inc_payload_validation_errors, inc_payloads_received, inc_relay_errors, }, - validation_api_client::ValidationAPIClient, + validation_api_client::{ValidationAPIClient, ValidationError}, }; use ahash::HashMap; use alloy_consensus::proofs::calculate_withdrawals_root; @@ -29,7 +29,7 @@ use std::{io::Read, net::SocketAddr}; use time::OffsetDateTime; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, warn}; +use tracing::{debug, error, info, warn}; use warp::body; use warp::{ http::status::StatusCode, @@ -94,12 +94,14 @@ pub fn spawn_relay_server( validation_client: Option, cl_clients: Vec, relay: MevBoostRelaySlotInfoProvider, + builder_names: HashMap, cancellation_token: CancellationToken, ) -> eyre::Result<()> { let relay_state = RelayState { validation_client, relay_for_slot_data: relay.clone(), pending_slot_data: Arc::new(Mutex::new(None)), + builder_names, }; spawn_mev_boost_slot_data_generator( relay_state.clone(), @@ -147,6 +149,7 @@ struct RelayState { relay_for_slot_data: MevBoostRelaySlotInfoProvider, // Slot data for the last payload arguments received from CL nodes and relay pending_slot_data: Arc>>, + builder_names: HashMap, } impl RelayState { @@ -228,7 +231,10 @@ impl RelayState { } }; - let builder_id = builder_id(submission.bid_trace().builder_pubkey.as_ref()); + let builder_id = builder_id( + submission.bid_trace().builder_pubkey.as_ref(), + &self.builder_names, + ); inc_payloads_received(&builder_id); @@ -270,10 +276,15 @@ impl RelayState { .await { Ok(_) => {} - Err(err) => { - warn!(?err, "Failed to validate block"); + Err(ValidationError::ValidationFailed(payload)) => { + error!(err = ?payload, "Block validation failed"); inc_payload_validation_errors(&builder_id); - return RelayError::SimulationFailed(err.to_string()).reply(); + let msg = serde_json::to_string(&payload).unwrap_or_default(); + return RelayError::SimulationFailed(msg).reply(); + } + Err(err) => { + warn!(?err, "Unable to validate block"); + return RelayError::BlockProcessing.reply(); } } @@ -390,7 +401,7 @@ async fn run_winner_sampler(relay_state: RelayState, cancellation_token: Cancell continue 'sampling; } - let builder = builder_id(&best_bid.builder); + let builder = builder_id(&best_bid.builder, &relay_state.builder_names); add_winning_bid(&builder, best_bid.advantage); } } @@ -557,15 +568,21 @@ struct BestBidData { } /// short readable builder id for metrics -fn builder_id(pubkey: &[u8]) -> String { +fn builder_id(pubkey: &[u8], builder_names: &HashMap) -> String { let pubkey_hex = alloy_primitives::hex::encode(pubkey); if pubkey_hex.len() < 8 { return "incorrect_pubkey".to_string(); } - format!( + let pubkey_name = format!( "{}..{}", &pubkey_hex[0..4], &pubkey_hex[pubkey_hex.len() - 4..] - ) + ); + + if let Some(name) = builder_names.get(&pubkey_name) { + name.clone() + } else { + pubkey_name + } } diff --git a/crates/test-relay/src/validation_api_client.rs b/crates/test-relay/src/validation_api_client.rs index b5049039b..d479a7bda 100644 --- a/crates/test-relay/src/validation_api_client.rs +++ b/crates/test-relay/src/validation_api_client.rs @@ -34,7 +34,7 @@ pub enum ValidationError { FailedToSerializeRequest, #[error("Failed to validate block, no valid responses from validation nodes")] NoValidResponseFromValidationNodes, - #[error("Validation failed")] + #[error("Validation failed: {0}")] ValidationFailed(ErrorPayload), #[error("Local usage error: {0}")]