diff --git a/crates/driver/src/tests/cases/deadline.rs b/crates/driver/src/tests/cases/deadline.rs new file mode 100644 index 0000000000..ec645452da --- /dev/null +++ b/crates/driver/src/tests/cases/deadline.rs @@ -0,0 +1,88 @@ +use { + crate::tests::{ + self, + setup::{ab_order, ab_pool, ab_solution, test_solver}, + }, + alloy::{primitives::b256, signers::local::PrivateKeySigner}, + std::time::Duration, +}; + +const SOLUTION_COUNT: usize = 30; + +fn submission_account() -> PrivateKeySigner { + // Well-known Anvil test key #1. Do not use as a production key. + PrivateKeySigner::from_bytes(&b256!( + "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + )) + .unwrap() +} + +#[tokio::test] +#[ignore] +async fn expired_solver_deadline_does_not_send_solve_request() { + let test = tests::setup() + .name("expired solver deadline") + .solvers(vec![test_solver().solving_time_share(0.0)]) + .pool(ab_pool()) + .order(ab_order()) + .solution(ab_solution()) + .done() + .await; + + test.solve().await.ok().empty(); + + assert_eq!(test.solve_requests(), 0); +} + +#[tokio::test] +#[ignore] +async fn postprocessing_timeout_drops_late_solution() { + let mut setup = tests::setup() + .name("postprocessing timeout") + .solvers(vec![ + test_solver() + .solving_time_share(1.0) + .max_solutions_to_propose(SOLUTION_COUNT) + .submission_account(submission_account()) + .post_processing_concurrency_limit(1), + ]) + .deadline_after(Duration::from_millis(600)) + .pool(ab_pool()) + .order(ab_order()); + + for _ in 0..SOLUTION_COUNT { + setup = setup.solution(ab_solution()); + } + + let test = setup.done().await; + + let solve = test.solve().await.ok(); + let solutions = solve.solutions(); + + assert!( + solutions.len() < SOLUTION_COUNT, + "expected post-processing timeout to cut off late solutions" + ); + + assert_eq!(test.solve_requests(), 1); +} + +#[tokio::test] +#[ignore] +async fn settlement_rejects_expired_submission_deadline() { + let test = tests::setup() + .name("expired settlement submission deadline") + .pool(ab_pool()) + .order(ab_order()) + .solution(ab_solution()) + .settle_submission_deadline(0) + .done() + .await; + + let solution_id = test.solve().await.ok().id(); + + test.settle(solution_id) + .await + .err() + .kind("DeadlineExceeded"); +} diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index e349d9b277..327b5ad432 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -9,6 +9,7 @@ use { }; pub mod buy_eth; +pub mod deadline; pub mod example_config; pub mod fees; mod flashloan_hints; diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index 099a7f7037..ce5462ff71 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -349,6 +349,13 @@ async fn create_config_file( .join(", "); writeln!(file, " submission-accounts = [{accounts}]").unwrap(); } + if let Some(limit) = solver.post_processing_concurrency_limit { + writeln!( + file, + " post-processing-concurrency-limit = {limit}" + ) + .unwrap(); + } } file.into_temp_path() } diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 2180cddae6..aaa9687667 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -54,6 +54,11 @@ use { collections::{HashMap, HashSet}, path::PathBuf, str::FromStr, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, }, }; @@ -365,6 +370,8 @@ pub struct Solver { max_solutions_to_propose: usize, /// Additional submission accounts for EIP-7702 parallel settlement. submission_accounts: Vec, + /// Maximum number of solutions post-processed concurrently by the driver. + post_processing_concurrency_limit: Option, } #[derive(Debug, Clone)] @@ -394,6 +401,7 @@ pub fn test_solver() -> Solver { haircut_bps: 0, max_solutions_to_propose: 1, submission_accounts: vec![], + post_processing_concurrency_limit: None, } } @@ -445,6 +453,11 @@ impl Solver { self } + pub fn post_processing_concurrency_limit(mut self, n: usize) -> Self { + self.post_processing_concurrency_limit = Some(n); + self + } + pub fn submission_account(mut self, signer: PrivateKeySigner) -> Self { self.submission_accounts.push(signer); self @@ -560,6 +573,8 @@ pub struct Setup { settle_submission_deadline: u64, /// Should flashloan-hint orders be accepted by the driver? True by default. flashloans_enabled: bool, + /// Deadline offset from fixed test time. Defaults to 2 seconds. + deadline_after: Option, } /// The validity of a solution. @@ -909,6 +924,11 @@ impl Setup { self } + pub fn deadline_after(mut self, deadline_after: Duration) -> Self { + self.deadline_after = Some(deadline_after); + self + } + /// Toggle acceptance of flashloan-hint orders at the driver level. pub fn flashloans_enabled(mut self, flashloans_enabled: bool) -> Self { self.flashloans_enabled = flashloans_enabled; @@ -995,7 +1015,7 @@ impl Setup { .into_iter() .map(|order| blockchain.quote(&order)) .collect::>(); - let solvers_with_address = join_all(self.solvers.iter().map(|solver| async { + let solver_instances = join_all(self.solvers.iter().map(|solver| async { let instance = SolverInstance::new(solver::Config { blockchain: &blockchain, solutions: &solutions, @@ -1012,9 +1032,17 @@ impl Setup { }) .await; - (solver.clone(), instance.addr) + (solver.clone(), instance) })) .await; + let solve_requests = solver_instances + .iter() + .find(|(solver, _)| solver.name == solver::NAME) + .map(|(_, instance)| Arc::clone(&instance.requests)); + let solvers_with_address = solver_instances + .iter() + .map(|(solver, instance)| (solver.clone(), instance.addr)) + .collect(); let driver = Driver::new( &driver::Config { @@ -1043,6 +1071,7 @@ impl Setup { quote: self.quote, surplus_capturing_jit_order_owners, auction_id: self.auction_id, + solve_requests, } } @@ -1061,7 +1090,9 @@ impl Setup { } fn deadline(&self) -> chrono::DateTime { - crate::infra::time::now() + chrono::Duration::seconds(2) + crate::infra::time::now() + + chrono::Duration::from_std(self.deadline_after.unwrap_or(Duration::from_secs(2))) + .unwrap() } pub fn allow_multiple_solve_requests(mut self) -> Self { @@ -1085,6 +1116,7 @@ pub struct Test { /// List of surplus capturing JIT-order owners surplus_capturing_jit_order_owners: Vec, auction_id: i64, + solve_requests: Option>, } impl Test { @@ -1248,6 +1280,13 @@ impl Test { pub async fn set_auto_mining(&self, enabled: bool) { self.blockchain.set_auto_mining(enabled).await } + + pub fn solve_requests(&self) -> usize { + self.solve_requests + .as_ref() + .expect("default solver not found") + .load(Ordering::SeqCst) + } } /// A /solve response. diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index e48ae9ea80..4f79b493b1 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -30,7 +30,10 @@ use { std::{ collections::{HashMap, HashSet}, net::SocketAddr, - sync::{Arc, Mutex}, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, }, }; @@ -38,6 +41,7 @@ pub const NAME: &str = "test-solver"; pub struct Solver { pub addr: SocketAddr, + pub requests: Arc, } #[derive(Debug)] @@ -501,16 +505,22 @@ impl Solver { ) .await; - let state = Arc::new(Mutex::new(StateInner { - called: false, + let requests = Arc::new(AtomicUsize::new(0)); + let state = State { + requests: Arc::clone(&requests), allow_multiple_solve_requests: config.allow_multiple_solve_requests, - })); + }; let app = axum::Router::new() .route( "/solve", axum::routing::post( move |axum::extract::State(state): axum::extract::State, - axum::extract::Json(req): axum::extract::Json| async move { + axum::extract::Json(req): axum::extract::Json| async move { + let previous_requests = state.requests.fetch_add(1, Ordering::SeqCst); + assert!( + previous_requests == 0 || state.allow_multiple_solve_requests, + "can't call /solve multiple times" + ); let base_fee = eth.current_block().borrow().base_fee; let effective_gas_price = eth.gas_price().await.unwrap().effective(base_fee).to_string(); let expected = json!({ @@ -523,23 +533,17 @@ impl Solver { "surplusCapturingJitOrderOwners": config.expected_surplus_capturing_jit_order_owners, }); check_solve_request(req, expected); - let mut state = state.0.lock().unwrap(); - assert!( - !state.called || state.allow_multiple_solve_requests, - "can't call /solve multiple times" - ); - state.called = true; axum::response::Json(json!({ "solutions": solutions_json, })) }, ), ) - .with_state(State(state)); + .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); - Self { addr } + Self { addr, requests } } } @@ -582,13 +586,7 @@ fn check_solve_request(request: Value, expected: Value) { } #[derive(Debug, Clone)] -struct StateInner { - /// Has this solver been called yet? If so, attempting to make another call - /// will result in a failed test. - called: bool, - /// In case you want to allow calling a solver multiple times. +struct State { + requests: Arc, allow_multiple_solve_requests: bool, } - -#[derive(Debug, Clone)] -struct State(Arc>);