diff --git a/README.md b/README.md index f60c7fc..0ab6450 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Key | Required | Description `SEQUENCER_KEY` | No | AWS Key ID _OR_ local private key for the Sequencer; set IFF using local Sequencer signing instead of remote (via `QUINCEY_URL`) Quincey signing `TX_POOL_URL` | Yes | Transaction pool URL `FLASHBOTS_ENDPOINT` | No | Flashbots API to submit blocks to +`FLASHBOTS_SIMULATION_ENDPOINT` | No | Optional dedicated Flashbots simulation endpoint. Bundles are always simulated via `eth_callBundle` before submission. When set, simulation uses this endpoint; otherwise, the default `FLASHBOTS_ENDPOINT` is used. Simulation failures are logged but do not block submission `ROLLUP_BLOCK_GAS_LIMIT` | No | Override for rollup block gas limit `MAX_HOST_GAS_COEFFICIENT` | No | Optional maximum host gas coefficient, as a percentage, to use when building blocks `BUILDER_KEY` | Yes | AWS KMS key ID _or_ local private key for builder signin diff --git a/src/config.rs b/src/config.rs index 868423f..b5ef148 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,6 +93,17 @@ pub struct BuilderConfig { )] pub flashbots_endpoint: url::Url, + /// Optional dedicated Flashbots simulation endpoint. Bundles are always + /// simulated via `eth_callBundle` before submission. When set, simulation + /// uses this endpoint; otherwise, the default `flashbots_endpoint` is used. + #[from_env( + var = "FLASHBOTS_SIMULATION_ENDPOINT", + desc = "Optional dedicated Flashbots simulation endpoint. When set, bundle simulation uses this endpoint instead of the default FLASHBOTS_ENDPOINT", + infallible, + optional + )] + pub flashbots_simulation_endpoint: Option, + /// URL for remote Quincey Sequencer server to sign blocks. /// NB: Disregarded if a sequencer_signer is configured. #[from_env( @@ -253,6 +264,20 @@ impl BuilderConfig { }) } + /// Connect to a dedicated Flashbots simulation provider, if configured. + /// + /// Returns `None` if `FLASHBOTS_SIMULATION_ENDPOINT` is not set. When + /// `None`, the caller should fall back to the default Flashbots provider + /// for simulation. + pub async fn connect_flashbots_simulation(&self) -> Result> { + match &self.flashbots_simulation_endpoint { + Some(endpoint) => self.connect_builder_signer().await.map(|signer| { + Some(ProviderBuilder::new().wallet(signer).connect_http(endpoint.clone())) + }), + None => Ok(None), + } + } + /// Connect to the Zenith instance, using the specified provider. pub const fn connect_zenith(&self, provider: HostProvider) -> ZenithInstance { Zenith::new(self.constants.host_zenith(), provider) diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index ec04dca..2259936 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -10,7 +10,7 @@ use alloy::{ eips::Encodable2718, primitives::{Bytes, TxHash}, providers::ext::MevApi, - rpc::types::mev::EthSendBundle, + rpc::types::mev::{EthCallBundle, EthSendBundle}, }; use init4_bin_base::{deps::metrics::counter, utils::signer::LocalOrAws}; use std::time::{Duration, Instant}; @@ -29,6 +29,10 @@ pub struct FlashbotsTask { zenith: ZenithInstance, /// Provides access to a Flashbots-compatible bundle API. flashbots: FlashbotsProvider, + /// Optional dedicated Flashbots simulation provider. When `Some`, bundle + /// simulation uses this provider; when `None`, the default `flashbots` + /// provider is used instead. + flashbots_simulation: Option, /// The key used to sign requests to the Flashbots relay. signer: LocalOrAws, /// Channel for sending hashes of outbound transactions. @@ -41,16 +45,29 @@ impl FlashbotsTask { pub async fn new(outbound: mpsc::UnboundedSender) -> eyre::Result { let config = crate::config(); - let (quincey, host_provider, flashbots, builder_key) = tokio::try_join!( + let (quincey, host_provider, flashbots, flashbots_simulation, builder_key) = tokio::try_join!( config.connect_quincey(), config.connect_host_provider(), config.connect_flashbots(), + config.connect_flashbots_simulation(), config.connect_builder_signer() )?; let zenith = config.connect_zenith(host_provider); - Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound }) + if flashbots_simulation.is_some() { + debug!("flashbots simulation endpoint configured"); + } + + Ok(Self { + config, + quincey, + zenith, + flashbots, + flashbots_simulation, + signer: builder_key, + outbound, + }) } /// Prepares a MEV bundle from a simulation result. @@ -151,6 +168,55 @@ impl FlashbotsTask { .collect() } + /// Simulates a bundle before submission using `eth_callBundle`. + /// + /// If `FLASHBOTS_SIMULATION_ENDPOINT` is configured, the bundle is simulated + /// against that endpoint. Otherwise, the default Flashbots provider is used. + /// + /// # Arguments + /// + /// * `bundle` - The MEV bundle to simulate + #[instrument(skip_all, level = "debug")] + async fn simulate_bundle(&self, bundle: &EthSendBundle) -> eyre::Result<()> { + counter!("signet.builder.flashbots.simulation_attempts").increment(1); + + // Use the dedicated simulation provider if configured, otherwise fall + // back to the default flashbots provider. + let provider = self.flashbots_simulation.as_ref().unwrap_or(&self.flashbots); + + // Convert EthSendBundle to EthCallBundle for simulation + let call_bundle = EthCallBundle { + txs: bundle.txs.clone(), + block_number: bundle.block_number, + state_block_number: bundle.block_number.saturating_sub(1).into(), + timestamp: bundle.min_timestamp, + ..Default::default() + }; + + let using_dedicated = self.flashbots_simulation.is_some(); + debug!( + block_number = %bundle.block_number, + using_dedicated_endpoint = using_dedicated, + "simulating bundle" + ); + + let response = + provider.call_bundle(call_bundle).with_auth(self.signer.clone()).into_future().await?; + + if let Some(result) = response { + debug!( + bundle_gas_price = %result.bundle_gas_price, + total_gas_used = %result.total_gas_used, + coinbase_diff = %result.coinbase_diff, + "bundle simulation succeeded" + ); + } else { + warn!("bundle simulation returned no result"); + } + + Ok(()) + } + /// Main task loop that processes simulation results and submits bundles to Flashbots. /// /// Receives `SimResult`s from the inbound channel, prepares MEV bundles, and submits @@ -190,6 +256,19 @@ impl FlashbotsTask { } }; + // Simulate the bundle before submission. Simulation failures are + // logged but do not block submission. + match self.simulate_bundle(&bundle).instrument(span.clone()).await { + Ok(()) => { + counter!("signet.builder.flashbots.simulation_success").increment(1); + span_debug!(span, "bundle simulation succeeded, proceeding to submission"); + } + Err(error) => { + counter!("signet.builder.flashbots.simulation_failures").increment(1); + span_debug!(span, %error, "bundle simulation failed, continuing with submission"); + } + } + // Make a child span to cover submission, or use the current span // if debug is not enabled. let _guard = span.enter();