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..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,15 +50,11 @@ 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 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 +97,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 +104,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 +443,6 @@ enum Outcome { reason: &'static str, blocks_passed: Option, }, - Disabled, } impl Outcome { @@ -485,13 +451,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 +469,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(), @@ -600,18 +564,6 @@ impl SubmissionSuccess { } } -#[derive(Debug, Error)] -#[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 +592,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 +615,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 d74e0a6530..66abc72813 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}, }, @@ -344,26 +343,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 abdbf0aa95..0ca5137f93 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,40 @@ 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. + #[serde(deserialize_with = "serde_ext::deserialize_nonempty_vec")] + urls: Vec, + }, + /// A private relay like MEV Blocker. + PrivateRelay { + /// 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 + /// relay is known to drop reverting transactions. + #[serde(default = "default_mines_reverting_txs")] + mines_reverting_txs: bool, + }, } #[derive(Debug, Deserialize)] @@ -1298,6 +1351,9 @@ mod tests { const TOKEN_GROUPS_CONFIG: &str = r#" tx-gas-limit = "45000000" + [submission.public-mempool] + urls = ["http://localhost:8545"] + [token-groups] ondo = [ "0x0000000000000000000000000000000000000001", @@ -1442,4 +1498,84 @@ 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_config() { + 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 public mempool needs at least one URL + r#" + [public-mempool] + urls = [] + "#, + // a private relay takes a single URL + r#" + [private-relay] + urls = ["http://relay"] + "#, + // unknown receiver field + r#" + [public-mempool] + 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(), + "{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 ca6f62eec9..a9542c1a40 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/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, 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 da113a6cb9..a53c39753b 100644 --- a/crates/e2e/src/setup/colocation.rs +++ b/crates/e2e/src/setup/colocation.rs @@ -221,8 +221,8 @@ refresh-cooldown = "0s" [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 = [