From 09f2b98d70dfd7dfa1d399a78fc4990c4085745e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:11:09 +0100 Subject: [PATCH 1/8] Move common submission arguments to the submission configuration --- crates/driver/example.toml | 5 +++-- crates/driver/src/infra/config/file/load.rs | 4 ++-- crates/driver/src/infra/config/file/mod.rs | 22 +++++++++++---------- crates/driver/src/tests/setup/driver.rs | 3 +-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/driver/example.toml b/crates/driver/example.toml index 5b1311f989..10fd9c2c54 100644 --- a/crates/driver/example.toml +++ b/crates/driver/example.toml @@ -31,13 +31,14 @@ xstocks = ["0x0000000000000000000000000000000000000002"] [submission] gas-price-cap = "1000000000000" +# optional args +max-additional-tip = "5000000000" +additional-tip-percentage = 0.05 [[submission.mempool]] url = "https://your.custom.rpc.endpoint" # optional args name = "custom_name" -max-additional-tip = "5000000000" -additional-tip-percentage = 0.05 mines-reverting-txs = true [contracts] # Optionally override the contract addresses, necessary on less popular blockchains diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index 8844c58b47..0a9d2d9ca3 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -353,8 +353,8 @@ pub async fn load(chain: Chain, path: &Path) -> infra::Config { true => mempool::RevertProtection::Disabled, false => mempool::RevertProtection::Enabled, }, - max_additional_tip: mempool.max_additional_tip, - additional_tip_percentage: mempool.additional_tip_percentage, + max_additional_tip: config.submission.max_additional_tip, + additional_tip_percentage: config.submission.additional_tip_percentage, }) .collect(), simulator: config.simulator, diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index e8eebdbd56..26564dc239 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -126,6 +126,18 @@ struct SubmissionConfig { #[serde(default)] nonce_block_number: Option, + /// Maximum additional tip in Gwei that we are willing to give to + /// the validator above regular gas price estimation. + #[serde(default = "default_max_additional_tip")] + #[serde_as(as = "serde_ext::U256")] + max_additional_tip: eth::U256, + + /// Additional tip in percentage of max_fee_per_gas we are giving to + /// validator above regular gas price estimation. Expects a + /// floating point value between 0 and 1. + #[serde(default = "default_additional_tip_percentage")] + additional_tip_percentage: f64, + /// The mempools to submit settlement transactions to. Can be the public /// mempool of a node or the private MEVBlocker mempool. #[serde(rename = "mempool", default)] @@ -158,16 +170,6 @@ struct Mempool { name: Option, /// The RPC URL to use. url: Url, - /// Maximum additional tip in Gwei that we are willing to give to - /// the validator above regular gas price estimation. - #[serde(default = "default_max_additional_tip")] - #[serde_as(as = "serde_ext::U256")] - max_additional_tip: eth::U256, - /// Additional tip in percentage of max_fee_per_gas we are giving to - /// validator above regular gas price estimation. Expects a - /// floating point value between 0 and 1. - #[serde(default = "default_additional_tip_percentage")] - additional_tip_percentage: f64, /// Informs the submission logic whether a reverting transaction will /// actually be mined or just ignored. This is an advanced feature /// for private mempools so for most configured mempools you have to diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 219a3eb156..a8a4938dfa 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -265,6 +265,7 @@ async fn create_config_file( [submission] gas-price-cap = "1000000000000" + additional-tip-percentage = 0.0 "#, blockchain.settlement.address(), blockchain.weth.address(), @@ -281,7 +282,6 @@ async fn create_config_file( file, r#"[[submission.mempool]] url = "{}" - additional-tip-percentage = 0.0 "#, blockchain.web3_url, ) @@ -293,7 +293,6 @@ async fn create_config_file( file, r#"[[submission.mempool]] url = "{}" - additional-tip-percentage = 0.0 mines-reverting-txs = {mines_reverting_txs} "#, url.clone().unwrap_or(blockchain.web3_url.clone()), From e7e4e6c94498f06ece471984b3f2c6bced2ba2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:08:07 +0100 Subject: [PATCH 2/8] Address Martin's comments --- crates/driver/src/infra/blockchain/gas.rs | 36 ++++----------------- crates/driver/src/infra/config/file/load.rs | 10 +++--- crates/driver/src/infra/config/mod.rs | 3 +- crates/driver/src/infra/mempool/mod.rs | 22 ------------- crates/driver/src/run.rs | 4 +-- crates/driver/src/tests/setup/solver.rs | 9 ++++-- 6 files changed, 22 insertions(+), 62 deletions(-) diff --git a/crates/driver/src/infra/blockchain/gas.rs b/crates/driver/src/infra/blockchain/gas.rs index 445df3b604..2c30d8ba91 100644 --- a/crates/driver/src/infra/blockchain/gas.rs +++ b/crates/driver/src/infra/blockchain/gas.rs @@ -4,7 +4,7 @@ /// private submission networks are used. use { super::Error, - crate::infra::{config::file::GasEstimatorType, mempool}, + crate::infra::config::file::GasEstimatorType, alloy::eips::eip1559::Eip1559Estimation, anyhow::anyhow, eth_domain_types as eth, @@ -112,40 +112,16 @@ impl GasPriceEstimating for GasPriceEstimator { } /// Set of tweaks to fine tune the computed gas price. +#[derive(Debug, Clone)] pub struct GasPriceParameters { /// Maximum priority_fee_per_gas added on top of the price suggested by the /// estimator. - max_additional_tip: eth::U256, + pub max_additional_tip: eth::U256, /// The max_priority_fee_per_gas suggested by the gas price estimator gets /// multiplied with this factor and get capped at `max_additional_tip`. - additional_tip_factor: f64, + pub additional_tip_factor: f64, /// Highest `max_fee_per_gas` at which we still want to submit a tx. - max_fee_per_gas: eth::U256, + pub max_fee_per_gas: eth::U256, /// We'll always tip at least this value for `max_priority_fee_per_gas`. - min_priority_fee: eth::U256, -} - -pub fn adjustments(mempools: &[mempool::Config]) -> GasPriceParameters { - // TODO: make configuration of those parameters more obvious by moving them - // into a separate config field instead of deriving them from the mempool - // configs - let mut max_additional_tip = eth::U256::ZERO; - let mut additional_tip_percentage = 0.0f64; - let mut max_fee_per_gas = eth::U256::MAX; - let mut min_priority_fee = eth::U256::ZERO; - - for mempool in mempools { - max_additional_tip = max_additional_tip.max(mempool.max_additional_tip); - additional_tip_percentage = - additional_tip_percentage.max(mempool.additional_tip_percentage); - max_fee_per_gas = max_fee_per_gas.min(mempool.gas_price_cap); - min_priority_fee = min_priority_fee.max(mempool.min_priority_fee); - } - - GasPriceParameters { - max_additional_tip, - additional_tip_factor: additional_tip_percentage, - max_fee_per_gas, - min_priority_fee, - } + pub min_priority_fee: eth::U256, } diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index 0a9d2d9ca3..b50d197586 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -333,14 +333,18 @@ pub async fn load(chain: Chain, path: &Path) -> infra::Config { }), } }), + gas_price_parameters: blockchain::gas::GasPriceParameters { + max_additional_tip: config.submission.max_additional_tip, + additional_tip_factor: config.submission.additional_tip_percentage, + max_fee_per_gas: config.submission.gas_price_cap, + min_priority_fee: config.submission.min_priority_fee, + }, mempools: config .submission .mempools .iter() .enumerate() .map(|(index, mempool)| mempool::Config { - min_priority_fee: config.submission.min_priority_fee, - gas_price_cap: config.submission.gas_price_cap, target_confirm_time: config.submission.target_confirm_time, retry_interval: config.submission.retry_interval, nonce_block_number: config.submission.nonce_block_number.map(Into::into), @@ -353,8 +357,6 @@ pub async fn load(chain: Chain, path: &Path) -> infra::Config { true => mempool::RevertProtection::Disabled, false => mempool::RevertProtection::Enabled, }, - max_additional_tip: config.submission.max_additional_tip, - additional_tip_percentage: config.submission.additional_tip_percentage, }) .collect(), simulator: config.simulator, diff --git a/crates/driver/src/infra/config/mod.rs b/crates/driver/src/infra/config/mod.rs index e823c2c04f..bf233fef0b 100644 --- a/crates/driver/src/infra/config/mod.rs +++ b/crates/driver/src/infra/config/mod.rs @@ -1,6 +1,6 @@ use { crate::infra::{ - blockchain, + blockchain::{self, gas::GasPriceParameters}, config::file::{AppDataFetching, GasEstimatorType, OrderPriorityStrategy}, liquidity, mempool, @@ -22,6 +22,7 @@ pub struct Config { pub liquidity_sources_notifier: Option, pub simulator: configs::simulator::Config, pub gas_estimator: GasEstimatorType, + pub gas_price_parameters: GasPriceParameters, pub mempools: Vec, pub contracts: blockchain::contracts::Addresses, pub order_priority_strategies: Vec, diff --git a/crates/driver/src/infra/mempool/mod.rs b/crates/driver/src/infra/mempool/mod.rs index 81b26baff8..4a0560049d 100644 --- a/crates/driver/src/infra/mempool/mod.rs +++ b/crates/driver/src/infra/mempool/mod.rs @@ -21,8 +21,6 @@ use { #[derive(Debug, Clone)] pub struct Config { - pub min_priority_fee: eth::U256, - pub gas_price_cap: eth::U256, pub target_confirm_time: std::time::Duration, pub retry_interval: std::time::Duration, /// Optional block number to use when fetching nonces. If None, uses the @@ -31,26 +29,6 @@ pub struct Config { pub url: Url, pub name: String, pub revert_protection: RevertProtection, - pub max_additional_tip: eth::U256, - pub additional_tip_percentage: f64, -} - -#[cfg(test)] -impl Config { - pub fn test_config(url: Url) -> Self { - Self { - min_priority_fee: Default::default(), - gas_price_cap: eth::U256::from(1000000000000_u128), - target_confirm_time: Default::default(), - retry_interval: Default::default(), - name: "default_rpc".to_string(), - max_additional_tip: eth::U256::from(3000000000_u128), - additional_tip_percentage: 0., - revert_protection: infra::mempool::RevertProtection::Disabled, - nonce_block_number: None, - url, - } - } } /// Don't submit transactions with high revert risk (i.e. transactions diff --git a/crates/driver/src/run.rs b/crates/driver/src/run.rs index e37221ade1..72760c643b 100644 --- a/crates/driver/src/run.rs +++ b/crates/driver/src/run.rs @@ -4,7 +4,7 @@ use { infra::{ self, Api, - blockchain::{self, Ethereum, gas}, + blockchain::{self, Ethereum}, cli, config, liquidity, @@ -74,7 +74,7 @@ async fn run_with(args: cli::Args, addr_sender: Option Date: Fri, 16 Oct 2026 11:04:37 +0100 Subject: [PATCH 3/8] Simplify the driver's mempool configuration --- crates/driver/example.toml | 15 +- .../src/domain/competition/solution/mod.rs | 13 -- .../domain/competition/solution/settlement.rs | 4 - crates/driver/src/domain/mempools.rs | 47 +----- crates/driver/src/domain/mod.rs | 2 +- crates/driver/src/infra/config/file/load.rs | 22 +-- crates/driver/src/infra/config/file/mod.rs | 153 +++++++++++++++--- crates/driver/src/infra/notify/mod.rs | 2 +- crates/driver/src/infra/observe/mod.rs | 4 - crates/driver/src/tests/cases/settle.rs | 25 --- crates/driver/src/tests/setup/driver.rs | 35 ++-- crates/driver/src/tests/setup/mod.rs | 21 --- crates/e2e/src/setup/colocation.rs | 4 +- playground/configs/driver.toml | 4 +- 14 files changed, 162 insertions(+), 189 deletions(-) diff --git a/crates/driver/example.toml b/crates/driver/example.toml index 10fd9c2c54..63fac70339 100644 --- a/crates/driver/example.toml +++ b/crates/driver/example.toml @@ -35,11 +35,16 @@ gas-price-cap = "1000000000000" max-additional-tip = "5000000000" additional-tip-percentage = 0.05 -[[submission.mempool]] -url = "https://your.custom.rpc.endpoint" -# optional args -name = "custom_name" -mines-reverting-txs = true +# Where to send the settlement to. Set exactly one of +# `[submission.public-mempool]` or `[submission.private-relay]`. +[submission.public-mempool] +urls = ["https://your.custom.rpc.endpoint", "https://your.backup.rpc.endpoint"] + +# A private relay, e.g. MEV Blocker: +# [submission.private-relay] +# url = "https://rpc.mevblocker.io" +# # optional args +# mines-reverting-txs = false [contracts] # Optionally override the contract addresses, necessary on less popular blockchains gp-v2-settlement = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 6fce4d25a4..a6f45d80c9 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -696,19 +696,6 @@ impl Solution { let token = token.as_erc20(self.weth); self.prices.get(&token).map(ToOwned::to_owned) } - - /// Whether there is a reasonable risk of this solution reverting on chain. - pub fn revertable(&self) -> bool { - self.interactions - .iter() - .any(|interaction| !interaction.internalize()) - || self.user_trades().any(|trade| { - matches!( - trade.order().signature.scheme, - order::signature::Scheme::Eip1271 - ) - }) - } } /// Given two solutions returns the factors with diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 805ac0fe19..ca6c736c29 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -241,7 +241,6 @@ impl Settlement { internalized: encode_one(Internalization::Enable)?, uninternalized: encode_one(Internalization::Disable)?, gas: self.gas, - may_revert: self.solution.revertable(), gas_fee_override: self.solution.gas_fee_override(), }) } @@ -349,9 +348,6 @@ pub struct EncodedSettlement { /// Gas parameters (limit used for mempool submission, estimate for /// observability). pub gas: Gas, - /// Effectively indicates whether the settlement interacts with - /// AMMs which can revert when arbitrage bots snipe good liquidity. - pub may_revert: bool, /// Solver-provided override for the gas fee at submission time. pub gas_fee_override: Option, } diff --git a/crates/driver/src/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index fc9419c2ea..823fdd81a7 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -59,7 +59,7 @@ impl Mempools { } } - /// Race the enabled mempools concurrently; first success wins. Pending + /// Race the mempools concurrently; first success wins. Pending /// submission futures are dropped at that point and every other mempool is /// recorded as `Superseded`. If every mempool fails, return one of the /// failure errors. @@ -102,30 +102,6 @@ impl Mempools { Ok(res?.tx_hash) } - /// A mempool is disabled if all of the following are true: - /// * the settlement may revert (see [`EncodedSettlement::may_revert`]) - /// * the pool has revert protection enabled (see - /// [`Self::revert_protection`]) - /// * reverts can get mined (see [`infra::Mempool::reverts_can_get_mined`]) - fn is_disabled(&self, mempool: &infra::Mempool, settlement: &EncodedSettlement) -> bool { - settlement.may_revert - && matches!(self.revert_protection(), RevertProtection::Enabled) - && mempool.reverts_can_get_mined() - } - - /// Defines if the mempools are configured in a way that guarantees that - /// settled solution will not revert. - pub fn revert_protection(&self) -> RevertProtection { - match self - .mempools - .iter() - .all(|mempool| mempool.reverts_can_get_mined()) - { - true => RevertProtection::Disabled, - false => RevertProtection::Enabled, - } - } - async fn submit( &self, mempool: &infra::mempool::Mempool, @@ -133,10 +109,6 @@ impl Mempools { submission_deadline: BlockNo, mode: &SubmissionMode, ) -> Result { - if self.is_disabled(mempool, settlement) { - return Err(Error::Disabled); - } - let tx = prepare_submission(&settlement.internalized, mode); let signer = tx.from; @@ -476,7 +448,6 @@ enum Outcome { reason: &'static str, blocks_passed: Option, }, - Disabled, } impl Outcome { @@ -485,13 +456,12 @@ impl Outcome { Outcome::Superseded => "Superseded", Outcome::Success { .. } => "Success", Outcome::Failed { reason, .. } => reason, - Outcome::Disabled => "Disabled", } } fn blocks_passed(self) -> Option { match self { - Outcome::Superseded | Outcome::Disabled => None, + Outcome::Superseded => None, Outcome::Success { blocks_passed } => Some(blocks_passed), Outcome::Failed { blocks_passed, .. } => blocks_passed, } @@ -504,7 +474,6 @@ impl From<&Result> for Outcome { Ok(s) => Outcome::Success { blocks_passed: s.blocks_passed(), }, - Err(Error::Disabled) => Outcome::Disabled, Err(err @ (Error::Revert { .. } | Error::SimulationRevert { .. })) => Outcome::Failed { reason: "Revert", blocks_passed: err.blocks_passed(), @@ -604,14 +573,6 @@ impl SubmissionSuccess { #[error("no mempools configured, cannot execute settlements")] pub struct NoMempools; -/// Defines if the mempools are configured in a way that guarantees that -/// /settle'd solution will not revert. -#[derive(Debug, Clone, Copy)] -pub enum RevertProtection { - Enabled, - Disabled, -} - #[derive(Debug, thiserror::Error)] pub enum Error { #[error( @@ -640,8 +601,6 @@ pub enum Error { submitted_at_block: BlockNo, submission_deadline: BlockNo, }, - #[error("Strategy disabled for this tx")] - Disabled, #[error("Failed to submit: {0:?}")] Other(#[from] anyhow::Error), } @@ -665,7 +624,7 @@ impl Error { submission_deadline, .. } => (*submitted_at_block, *submission_deadline), - Self::Disabled | Self::Other(_) => return None, + Self::Other(_) => return None, }; Some(end.saturating_sub(start).0) } diff --git a/crates/driver/src/domain/mod.rs b/crates/driver/src/domain/mod.rs index f17cb1ec9f..ba13ba581e 100644 --- a/crates/driver/src/domain/mod.rs +++ b/crates/driver/src/domain/mod.rs @@ -12,5 +12,5 @@ pub use { flashloan::Flashloan, interaction::Interaction, liquidity::Liquidity, - mempools::{Mempools, RevertProtection}, + mempools::Mempools, }; diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index b50d197586..b6fd7375c4 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -6,7 +6,6 @@ use { blockchain, config::file, liquidity, - mempool, notify, solver::{self, Account, BadOrderDetection, SolutionMerging}, }, @@ -339,26 +338,7 @@ pub async fn load(chain: Chain, path: &Path) -> infra::Config { max_fee_per_gas: config.submission.gas_price_cap, min_priority_fee: config.submission.min_priority_fee, }, - mempools: config - .submission - .mempools - .iter() - .enumerate() - .map(|(index, mempool)| mempool::Config { - target_confirm_time: config.submission.target_confirm_time, - retry_interval: config.submission.retry_interval, - nonce_block_number: config.submission.nonce_block_number.map(Into::into), - name: mempool - .name - .clone() - .unwrap_or_else(|| format!("mempool_{index}")), - url: mempool.url.clone(), - revert_protection: match mempool.mines_reverting_txs { - true => mempool::RevertProtection::Disabled, - false => mempool::RevertProtection::Enabled, - }, - }) - .collect(), + mempools: config.submission.to_domain(), simulator: config.simulator, contracts: blockchain::contracts::Addresses { settlement: config.contracts.gp_v2_settlement.map(Into::into), diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 26564dc239..989301d883 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -1,6 +1,6 @@ pub use load::load; use { - crate::infra, + crate::{infra, infra::mempool::RevertProtection}, alloy::{eips::BlockNumberOrTag, primitives::Address}, anyhow::Context, configs::gas_price_estimation::{default_past_blocks, default_reward_percentile}, @@ -41,7 +41,6 @@ struct Config { gas_estimator: GasEstimatorType, /// Parameters related to settlement submission. - #[serde(default)] submission: SubmissionConfig, /// Override smart contract addresses. @@ -95,7 +94,7 @@ struct Config { } #[serde_as] -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct SubmissionConfig { /// The minimum priority fee in Gwei the solver is ensuring to pay in a @@ -138,10 +137,44 @@ struct SubmissionConfig { #[serde(default = "default_additional_tip_percentage")] additional_tip_percentage: f64, - /// The mempools to submit settlement transactions to. Can be the public - /// mempool of a node or the private MEVBlocker mempool. - #[serde(rename = "mempool", default)] - mempools: Vec, + /// Where to send settlement transactions to. + #[serde(flatten)] + receiver: Receiver, +} + +impl SubmissionConfig { + /// One mempool for each receiver URL, named after the URL host. + fn to_domain(&self) -> Vec { + let mempool = |name, url, revert_protection| infra::mempool::Config { + target_confirm_time: self.target_confirm_time, + retry_interval: self.retry_interval, + nonce_block_number: self.nonce_block_number.map(Into::into), + name, + url, + revert_protection, + }; + match &self.receiver { + Receiver::PublicMempool { urls } => urls + .iter() + .map(|url| mempool(host(url), url.clone(), RevertProtection::Disabled)) + .collect(), + Receiver::PrivateRelay { + url, + mines_reverting_txs, + } => { + let revert_protection = match mines_reverting_txs { + true => RevertProtection::Disabled, + false => RevertProtection::Enabled, + }; + vec![mempool(host(url), url.clone(), revert_protection)] + } + } + } +} + +/// Name of a receiver in logs and metrics. +fn host(url: &Url) -> String { + url.host_str().unwrap_or_default().to_owned() } #[derive(Debug, Clone, Copy, Deserialize)] @@ -162,20 +195,29 @@ impl From for BlockNumberOrTag { } } -#[serde_as] +/// Set as `[submission.public-mempool]` or `[submission.private-relay]`. #[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -struct Mempool { - /// Name for better logging and metrics. - name: Option, - /// The RPC URL to use. - url: Url, - /// Informs the submission logic whether a reverting transaction will - /// actually be mined or just ignored. This is an advanced feature - /// for private mempools so for most configured mempools you have to - /// assume reverting transactions will get mined eventually. - #[serde(default = "default_mines_reverting_txs")] - mines_reverting_txs: bool, +#[serde( + rename_all = "kebab-case", + rename_all_fields = "kebab-case", + deny_unknown_fields +)] +enum Receiver { + /// Nodes that forward settlement transactions to the public mempool. + PublicMempool { + /// The RPC URLs of the nodes. The driver submits to all of them. + urls: Vec, + }, + /// A private relay like MEV Blocker. + PrivateRelay { + /// The RPC URL of the relay. + url: Url, + /// Informs the submission logic whether a reverting transaction will + /// actually be mined or just ignored. Set this to `false` only if the + /// relay is known to drop reverting transactions. + #[serde(default = "default_mines_reverting_txs")] + mines_reverting_txs: bool, + }, } #[derive(Debug, Deserialize)] @@ -1210,6 +1252,9 @@ mod tests { const TOKEN_GROUPS_CONFIG: &str = r#" tx-gas-limit = "45000000" + [submission.public-mempool] + urls = ["http://localhost:8545"] + [token-groups] ondo = [ "0x0000000000000000000000000000000000000001", @@ -1354,4 +1399,72 @@ mod tests { assert!(err.to_string().contains("must be signers")); } + + #[test] + fn submission_public_mempool() { + let config: SubmissionConfig = toml::from_str( + r#" + gas-price-cap = "1" + [public-mempool] + urls = ["http://node-1", "http://node-2"] + "#, + ) + .unwrap(); + + assert!(matches!( + config.receiver, + Receiver::PublicMempool { urls } if urls.len() == 2 + )); + } + + #[test] + fn submission_private_relay() { + let config: SubmissionConfig = toml::from_str( + r#" + [private-relay] + url = "http://relay" + mines-reverting-txs = false + "#, + ) + .unwrap(); + + assert!(matches!( + config.receiver, + Receiver::PrivateRelay { + mines_reverting_txs: false, + .. + } + )); + } + + #[test] + fn submission_rejects_invalid_receivers() { + for config in [ + // no receiver + r#"gas-price-cap = "1""#, + // more than one receiver + r#" + [public-mempool] + urls = ["http://node"] + [private-relay] + url = "http://relay" + "#, + // a private relay takes a single URL + r#" + [private-relay] + urls = ["http://relay"] + "#, + // unknown receiver field + r#" + [public-mempool] + urls = ["http://node"] + name = "node" + "#, + ] { + assert!( + toml::from_str::(config).is_err(), + "{config}" + ); + } + } } diff --git a/crates/driver/src/infra/notify/mod.rs b/crates/driver/src/infra/notify/mod.rs index 483bccd871..7032214b82 100644 --- a/crates/driver/src/infra/notify/mod.rs +++ b/crates/driver/src/infra/notify/mod.rs @@ -120,7 +120,7 @@ pub fn executed( Err(Error::Revert { tx_id: hash, .. }) => notification::Settlement::Revert(*hash), Err(Error::SimulationRevert { .. }) => notification::Settlement::SimulationRevert, Err(Error::Expired { .. }) => notification::Settlement::Expired, - Err(Error::Other(_) | Error::Disabled) => notification::Settlement::Fail, + Err(Error::Other(_)) => notification::Settlement::Fail, }; solver.notify( diff --git a/crates/driver/src/infra/observe/mod.rs b/crates/driver/src/infra/observe/mod.rs index e7e1ece2c0..4262ce376c 100644 --- a/crates/driver/src/infra/observe/mod.rs +++ b/crates/driver/src/infra/observe/mod.rs @@ -389,10 +389,6 @@ pub fn mempool_log( ?settlement, "sending transaction via mempool succeeded", ), - Err(mempools::Error::Disabled) => tracing::debug!( - %mempool, - "mempool disabled, not sending transaction", - ), Err(err) => tracing::warn!( ?err, %mempool, diff --git a/crates/driver/src/tests/cases/settle.rs b/crates/driver/src/tests/cases/settle.rs index c3dd890cd9..2690ff1d94 100644 --- a/crates/driver/src/tests/cases/settle.rs +++ b/crates/driver/src/tests/cases/settle.rs @@ -53,31 +53,6 @@ async fn solution_not_available() { test.settle(99).await.err().kind("SolutionNotAvailable"); } -/// Checks that settlements with revert risk are not submitted via public -/// mempool if at least 1 revert protected mempool exists. -#[tokio::test] -#[ignore] -async fn private_rpc_with_high_risk_solution() { - let test = tests::setup() - .name("private rpc") - .pool(ab_pool()) - .order(ab_order()) - .solution(ab_solution()) - .mempools(vec![ - tests::setup::Mempool::Default, - tests::setup::Mempool::Private { - url: Some("http://non-existant:8545".to_string()), - mines_reverting_txs: false, - }, - ]) - .done() - .await; - - let id = test.solve().await.ok().id(); - // Public cannot be used and private RPC is not available - test.settle(id).await.err().kind("FailedToSubmit"); -} - #[tokio::test] #[ignore] async fn too_much_gas() { diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index a8a4938dfa..1872f6cccb 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -1,5 +1,5 @@ use { - super::{Mempool, Partial, Solver, Test, blockchain::Blockchain}, + super::{Partial, Solver, Test, blockchain::Blockchain}, crate::{ domain::competition::order, infra::config::file::OrderPriorityStrategy, @@ -17,7 +17,6 @@ pub struct Config { /// temporary file will be created with reasonable values. pub config_file: Option, pub enable_simulation: bool, - pub mempools: Vec, pub order_priority_strategies: Vec, pub orderbook: Orderbook, pub flashloans_enabled: bool, @@ -276,30 +275,14 @@ async fn create_config_file( ) .unwrap(); - for mempool in &config.mempools { - match mempool { - Mempool::Default => write!( - file, - r#"[[submission.mempool]] - url = "{}" - "#, - blockchain.web3_url, - ) - .unwrap(), - Mempool::Private { - url, - mines_reverting_txs, - } => write!( - file, - r#"[[submission.mempool]] - url = "{}" - mines-reverting-txs = {mines_reverting_txs} - "#, - url.clone().unwrap_or(blockchain.web3_url.clone()), - ) - .unwrap(), - } - } + write!( + file, + r#"[submission.public-mempool] + urls = ["{}"] + "#, + blockchain.web3_url, + ) + .unwrap(); for strategy in &config.order_priority_strategies { match strategy { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 4dad089920..72e97c2324 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -485,23 +485,11 @@ fn ceil_div(x: eth::U256, y: eth::U256) -> eth::U256 { (x + y - eth::U256::from(1)) / y } -#[derive(Debug)] -pub enum Mempool { - /// Uses the driver's main RPC URL - Default, - Private { - /// Uses ethrpc node if None - url: Option, - mines_reverting_txs: bool, - }, -} - /// Create a builder for the setup process. pub fn setup() -> Setup { Setup { solvers: vec![test_solver()], enable_simulation: true, - mempools: vec![Mempool::Default], rpc_args: vec!["--gas-limit".into(), "10000000".into()], allow_multiple_solve_requests: false, auction_id: 1, @@ -538,9 +526,6 @@ pub struct Setup { balances_address: Option, /// Ensure the Signatures contract is deployed on a specific address? signatures_address: Option, - /// Mempools that should be used for solution submission in addition - /// to the main RPC URL. - mempools: Vec, /// Extra configuration for the RPC node rpc_args: Vec, /// List of jit orders returned by the solver @@ -886,11 +871,6 @@ impl Setup { self } - pub fn mempools(mut self, mempools: Vec) -> Self { - self.mempools = mempools; - self - } - pub fn rpc_args(mut self, rpc_args: Vec) -> Self { self.rpc_args = rpc_args; self @@ -1042,7 +1022,6 @@ impl Setup { &driver::Config { config_file, enable_simulation: self.enable_simulation, - mempools: self.mempools, order_priority_strategies: self.order_priority_strategies, orderbook, flashloans_enabled: self.flashloans_enabled, diff --git a/crates/e2e/src/setup/colocation.rs b/crates/e2e/src/setup/colocation.rs index c9244a89e3..fbb681583a 100644 --- a/crates/e2e/src/setup/colocation.rs +++ b/crates/e2e/src/setup/colocation.rs @@ -220,8 +220,8 @@ base-tokens = [{encoded_base_tokens}] [submission] gas-price-cap = "1000000000000" -[[submission.mempool]] -url = "{NODE_HOST}" +[submission.public-mempool] +urls = ["{NODE_HOST}"] [balance-cache] refresh-cooldown = "0s" diff --git a/playground/configs/driver.toml b/playground/configs/driver.toml index 008a9082a3..9b70e2d605 100644 --- a/playground/configs/driver.toml +++ b/playground/configs/driver.toml @@ -16,8 +16,8 @@ account = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" [submission] gas-price-cap = "1000000000000" -[[submission.mempool]] -url = "http://chain:8545" +[submission.public-mempool] +urls = ["http://chain:8545"] [liquidity] base-tokens = [ From 82d20ca26c2e5dc37d4bbc50d7f69cf66aea7d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:59:12 +0100 Subject: [PATCH 4/8] Document single private-relay url --- crates/driver/src/infra/config/file/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 989301d883..897a9c1fcc 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -210,7 +210,9 @@ enum Receiver { }, /// A private relay like MEV Blocker. PrivateRelay { - /// The RPC URL of the relay. + /// The RPC URL of the relay. Only one URL, because a proxy in front of + /// the relays (e.g. our MEV Blocker proxy) handles the failover between + /// them, and the driver also fetches nonces through this URL. url: Url, /// Informs the submission logic whether a reverting transaction will /// actually be mined or just ignored. Set this to `false` only if the From 3506948424286cda46c82252bda632f03e4fcfde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:10:54 +0100 Subject: [PATCH 5/8] Reject an empty public mempool url list Co-Authored-By: Claude Opus 5.5 --- crates/driver/src/infra/config/file/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 897a9c1fcc..b244dc9425 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -206,6 +206,7 @@ enum Receiver { /// Nodes that forward settlement transactions to the public mempool. PublicMempool { /// The RPC URLs of the nodes. The driver submits to all of them. + #[serde(deserialize_with = "serde_ext::deserialize_nonempty_vec")] urls: Vec, }, /// A private relay like MEV Blocker. @@ -1451,6 +1452,11 @@ mod tests { [private-relay] url = "http://relay" "#, + // a public mempool needs at least one URL + r#" + [public-mempool] + urls = [] + "#, // a private relay takes a single URL r#" [private-relay] From bc3e2e9b7b800defc509707f4a363401fb13a65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:13:51 +0100 Subject: [PATCH 6/8] Test that unknown submission keys are rejected Co-Authored-By: Claude Opus 5.5 --- crates/driver/src/infra/config/file/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index b244dc9425..fdd2c0aea7 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -1441,7 +1441,7 @@ mod tests { } #[test] - fn submission_rejects_invalid_receivers() { + fn submission_rejects_invalid_config() { for config in [ // no receiver r#"gas-price-cap = "1""#, @@ -1468,6 +1468,13 @@ mod tests { urls = ["http://node"] name = "node" "#, + // unknown submission field, still rejected next to the flattened + // receiver + r#" + gas-price-caps = "1" + [public-mempool] + urls = ["http://node"] + "#, ] { assert!( toml::from_str::(config).is_err(), From 94aee2a70b1f1edc8c10276599fe1dd72fb4b6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Duarte?= <15343819+jmg-duarte@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:19:01 +0100 Subject: [PATCH 7/8] Remove the NoMempools error Co-Authored-By: Claude Opus 5.5 --- crates/driver/src/domain/mempools.rs | 13 ++----------- crates/driver/src/run.rs | 5 ++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/driver/src/domain/mempools.rs b/crates/driver/src/domain/mempools.rs index 823fdd81a7..3412c54a89 100644 --- a/crates/driver/src/domain/mempools.rs +++ b/crates/driver/src/domain/mempools.rs @@ -11,7 +11,6 @@ use { futures::{FutureExt, StreamExt, future::select_ok}, itertools::Itertools, num::Saturating, - thiserror::Error, tracing::Instrument, }; @@ -51,12 +50,8 @@ pub struct Mempools { } impl Mempools { - pub fn try_new(mempools: Vec, ethereum: Ethereum) -> Result { - if mempools.is_empty() { - Err(NoMempools) - } else { - Ok(Self { mempools, ethereum }) - } + pub fn new(mempools: Vec, ethereum: Ethereum) -> Self { + Self { mempools, ethereum } } /// Race the mempools concurrently; first success wins. Pending @@ -569,10 +564,6 @@ impl SubmissionSuccess { } } -#[derive(Debug, Error)] -#[error("no mempools configured, cannot execute settlements")] -pub struct NoMempools; - #[derive(Debug, thiserror::Error)] pub enum Error { #[error( diff --git a/crates/driver/src/run.rs b/crates/driver/src/run.rs index 72760c643b..ea396f9243 100644 --- a/crates/driver/src/run.rs +++ b/crates/driver/src/run.rs @@ -110,7 +110,7 @@ async fn run_with(args: cli::Args, addr_sender: Option Date: Mon, 19 Oct 2026 10:56:32 +0100 Subject: [PATCH 8/8] Address Martin's comment --- crates/driver/src/infra/config/file/mod.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 493413d872..0ca5137f93 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -211,9 +211,17 @@ enum Receiver { }, /// A private relay like MEV Blocker. PrivateRelay { - /// The RPC URL of the relay. Only one URL, because a proxy in front of - /// the relays (e.g. our MEV Blocker proxy) handles the failover between - /// them, and the driver also fetches nonces through this URL. + /// The RPC URL of the relay. + /// + /// Forwarding the information to more than one private relay does not + /// offer better protection, rather the opposite — different relays + /// forward different information to searchers, as such, searchers will + /// usually be connected to multiple private relays, allowing them to + /// piece together more information than each individual private relay + /// originally intented, resulting in reduced MEV protection. + /// + /// Additionally, we run a relay proxy that handles the failover between + /// them; the driver also fetches nonces through this URL. url: Url, /// Informs the submission logic whether a reverting transaction will /// actually be mined or just ignored. Set this to `false` only if the