diff --git a/crates/autopilot/src/domain/competition/participation_guard/db.rs b/crates/autopilot/src/domain/competition/participation_guard/db.rs index c843c2fb7e..bf0e5fff84 100644 --- a/crates/autopilot/src/domain/competition/participation_guard/db.rs +++ b/crates/autopilot/src/domain/competition/participation_guard/db.rs @@ -4,6 +4,7 @@ use { infra, }, ethrpc::block_stream::CurrentBlockWatcher, + model::time::now_in_epoch_seconds, std::{ collections::HashMap, sync::Arc, @@ -85,17 +86,27 @@ impl Validator { tracing::debug!(solvers = ?non_settling_solver_names, "found non-settling solvers"); - let now = Instant::now(); - non_settling_drivers + let non_settling_drivers = non_settling_drivers .into_iter() // Check if solver accepted this feature. This should be removed once a CIP is // approved. - .filter_map(|driver| { - driver.accepts_unsettled_blocking.then_some(driver.submission_address) - }) - .for_each(|solver| { - self_.0.banned_solvers.insert(solver, now); - }); + .filter(|driver| driver.accepts_unsettled_blocking) + .collect::>(); + + let now = Instant::now(); + let banned_until_timestamp = + u64::from(now_in_epoch_seconds()) + self_.0.ttl.as_secs(); + infra::notify_non_settling_solvers( + &non_settling_drivers, + banned_until_timestamp, + ); + + for driver in non_settling_drivers { + self_ + .0 + .banned_solvers + .insert(driver.submission_address, now); + } } Err(err) => { tracing::warn!(?err, "error while searching for non-settling solvers") diff --git a/crates/autopilot/src/domain/competition/participation_guard/mod.rs b/crates/autopilot/src/domain/competition/participation_guard/mod.rs index 75e89d0679..63eb301e3a 100644 --- a/crates/autopilot/src/domain/competition/participation_guard/mod.rs +++ b/crates/autopilot/src/domain/competition/participation_guard/mod.rs @@ -2,11 +2,7 @@ mod db; mod onchain; use { - crate::{ - arguments::DbBasedSolverParticipationGuardConfig, - domain::eth, - infra::{self, Ethereum}, - }, + crate::{arguments::DbBasedSolverParticipationGuardConfig, domain::eth, infra}, std::{collections::HashMap, sync::Arc}, }; @@ -22,7 +18,7 @@ struct Inner { impl SolverParticipationGuard { pub fn new( - eth: Ethereum, + eth: infra::Ethereum, persistence: infra::Persistence, competition_updates_receiver: tokio::sync::mpsc::UnboundedReceiver<()>, db_based_validator_config: DbBasedSolverParticipationGuardConfig, diff --git a/crates/autopilot/src/domain/competition/participation_guard/onchain.rs b/crates/autopilot/src/domain/competition/participation_guard/onchain.rs index 82d0ef3fb7..598da9cf26 100644 --- a/crates/autopilot/src/domain/competition/participation_guard/onchain.rs +++ b/crates/autopilot/src/domain/competition/participation_guard/onchain.rs @@ -1,9 +1,9 @@ -use crate::{domain::eth, infra::Ethereum}; +use crate::{domain::eth, infra}; /// Calls Authenticator contract to check if a solver has a sufficient /// permission. pub(super) struct Validator { - pub eth: Ethereum, + pub eth: infra::Ethereum, } #[async_trait::async_trait] diff --git a/crates/autopilot/src/infra/mod.rs b/crates/autopilot/src/infra/mod.rs index e61a7f5dc8..b8d71918e7 100644 --- a/crates/autopilot/src/infra/mod.rs +++ b/crates/autopilot/src/infra/mod.rs @@ -7,5 +7,5 @@ pub use { blockchain::Ethereum, order_validation::banned, persistence::Persistence, - solvers::Driver, + solvers::{notify_non_settling_solvers, Driver}, }; diff --git a/crates/autopilot/src/infra/solvers/dto/mod.rs b/crates/autopilot/src/infra/solvers/dto/mod.rs index d3a156294c..5365700ce8 100644 --- a/crates/autopilot/src/infra/solvers/dto/mod.rs +++ b/crates/autopilot/src/infra/solvers/dto/mod.rs @@ -1,6 +1,7 @@ //! Types for communicating with drivers as defined in //! `crates/driver/openapi.yml`. +pub mod notify; pub mod reveal; pub mod settle; pub mod solve; diff --git a/crates/autopilot/src/infra/solvers/dto/notify.rs b/crates/autopilot/src/infra/solvers/dto/notify.rs new file mode 100644 index 0000000000..0ac2e786f9 --- /dev/null +++ b/crates/autopilot/src/infra/solvers/dto/notify.rs @@ -0,0 +1,18 @@ +use {serde::Serialize, serde_with::serde_as}; + +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Request { + Banned { + reason: BanReason, + until_timestamp: u64, + }, +} + +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum BanReason { + UnsettledConsecutiveAuctions, +} diff --git a/crates/autopilot/src/infra/solvers/mod.rs b/crates/autopilot/src/infra/solvers/mod.rs index aae623ebaa..264ab0931b 100644 --- a/crates/autopilot/src/infra/solvers/mod.rs +++ b/crates/autopilot/src/infra/solvers/mod.rs @@ -1,9 +1,10 @@ use { self::dto::{reveal, settle, solve}, - crate::{arguments::Account, domain::eth, util}, + crate::{arguments::Account, domain::eth, infra::solvers::dto::notify, util}, anyhow::{anyhow, Context, Result}, + ethcontract::jsonrpc::futures_util::future::join_all, reqwest::{Client, StatusCode}, - std::time::Duration, + std::{sync::Arc, time::Duration}, thiserror::Error, url::Url, }; @@ -116,6 +117,10 @@ impl Driver { Ok(()) } + pub async fn notify(&self, request: ¬ify::Request) -> Result<()> { + self.request_response("notify", request, None).await + } + async fn request_response( &self, path: &str, @@ -172,3 +177,29 @@ pub async fn response_body_with_size_limit( } Ok(bytes) } + +/// Try to notify all the non-settling solvers in a background task. +pub fn notify_non_settling_solvers( + non_settling_drivers: &[Arc], + banned_until_timestamp: u64, +) { + let futures = non_settling_drivers + .iter() + .cloned() + .map(|driver| async move { + if let Err(err) = driver + .notify(¬ify::Request::Banned { + reason: notify::BanReason::UnsettledConsecutiveAuctions, + until_timestamp: banned_until_timestamp, + }) + .await + { + tracing::debug!(solver = ?driver.name, ?err, "unable to notify external solver"); + } + }) + .collect::>(); + + tokio::spawn(async move { + join_all(futures).await; + }); +} diff --git a/crates/driver/openapi.yml b/crates/driver/openapi.yml index a8d43d199f..3e810c4ac6 100644 --- a/crates/driver/openapi.yml +++ b/crates/driver/openapi.yml @@ -138,6 +138,23 @@ paths: $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalServerError" + /notify: + post: + description: | + Receive a notification with a specific reason. + requestBody: + required: true + content: + application/json: + schema: + type: string + enum: + - banned + description: |- + The reason for the notification with optional additional context. + responses: + "200": + description: notification successfully received. components: schemas: Address: diff --git a/crates/driver/src/infra/api/mod.rs b/crates/driver/src/infra/api/mod.rs index dfbc33fa97..d153b34979 100644 --- a/crates/driver/src/infra/api/mod.rs +++ b/crates/driver/src/infra/api/mod.rs @@ -78,6 +78,7 @@ impl Api { let router = routes::solve(router); let router = routes::reveal(router); let router = routes::settle(router); + let router = routes::notify(router); let bad_token_config = solver.bad_token_detection(); let mut bad_tokens = diff --git a/crates/driver/src/infra/api/routes/mod.rs b/crates/driver/src/infra/api/routes/mod.rs index ee1027b0e9..962dd5587e 100644 --- a/crates/driver/src/infra/api/routes/mod.rs +++ b/crates/driver/src/infra/api/routes/mod.rs @@ -1,6 +1,7 @@ mod healthz; mod info; mod metrics; +mod notify; mod quote; mod reveal; mod settle; @@ -10,6 +11,7 @@ pub(super) use { healthz::healthz, info::info, metrics::metrics, + notify::notify, quote::{quote, OrderError}, reveal::reveal, settle::settle, diff --git a/crates/driver/src/infra/api/routes/notify/dto/mod.rs b/crates/driver/src/infra/api/routes/notify/dto/mod.rs new file mode 100644 index 0000000000..9a24eedbc1 --- /dev/null +++ b/crates/driver/src/infra/api/routes/notify/dto/mod.rs @@ -0,0 +1,3 @@ +mod notify_request; + +pub use notify_request::NotifyRequest; diff --git a/crates/driver/src/infra/api/routes/notify/dto/notify_request.rs b/crates/driver/src/infra/api/routes/notify/dto/notify_request.rs new file mode 100644 index 0000000000..4dbb8210d5 --- /dev/null +++ b/crates/driver/src/infra/api/routes/notify/dto/notify_request.rs @@ -0,0 +1,37 @@ +use {crate::infra::notify, serde::Deserialize, serde_with::serde_as}; + +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum NotifyRequest { + Banned { + reason: BanReason, + until_timestamp: u64, + }, +} + +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum BanReason { + /// The driver won multiple consecutive auctions but never settled them. + UnsettledConsecutiveAuctions, +} + +impl From for notify::Kind { + fn from(value: NotifyRequest) -> Self { + match value { + NotifyRequest::Banned { + reason, + until_timestamp, + } => notify::Kind::Banned { + reason: match reason { + BanReason::UnsettledConsecutiveAuctions => { + notify::BanReason::UnsettledConsecutiveAuctions + } + }, + until_timestamp, + }, + } + } +} diff --git a/crates/driver/src/infra/api/routes/notify/mod.rs b/crates/driver/src/infra/api/routes/notify/mod.rs new file mode 100644 index 0000000000..d8922b7631 --- /dev/null +++ b/crates/driver/src/infra/api/routes/notify/mod.rs @@ -0,0 +1,21 @@ +mod dto; + +use crate::infra::api::{Error, State}; + +pub(in crate::infra::api) fn notify(router: axum::Router) -> axum::Router { + router.route("/notify", axum::routing::post(route)) +} + +async fn route( + state: axum::extract::State, + req: axum::Json, +) -> Result)> { + let solver = &state.solver().name().0; + tracing::debug!(?req, ?solver, "received a notification"); + + if let Err(err) = state.solver().notify(None, None, req.0.into()).await { + tracing::debug!(?err, "failed to notify solver"); + } + + Ok(hyper::StatusCode::OK) +} diff --git a/crates/driver/src/infra/notify/mod.rs b/crates/driver/src/infra/notify/mod.rs index 161f068c81..4baf6f0266 100644 --- a/crates/driver/src/infra/notify/mod.rs +++ b/crates/driver/src/infra/notify/mod.rs @@ -5,18 +5,25 @@ use { mod notification; -pub use notification::{Kind, Notification, ScoreKind, Settlement, SimulationSucceededAtLeastOnce}; +pub use notification::{ + BanReason, + Kind, + Notification, + ScoreKind, + Settlement, + SimulationSucceededAtLeastOnce, +}; use { super::simulator, crate::domain::{eth, mempools::Error}, }; pub fn solver_timeout(solver: &Solver, auction_id: Option) { - solver.notify(auction_id, None, notification::Kind::Timeout); + solver.notify_and_forget(auction_id, None, notification::Kind::Timeout); } pub fn empty_solution(solver: &Solver, auction_id: Option, solution: solution::Id) { - solver.notify( + solver.notify_and_forget( auction_id, Some(solution), notification::Kind::EmptySolution, @@ -45,7 +52,7 @@ pub fn scoring_failed( } }; - solver.notify(auction_id, Some(solution_id.clone()), notification); + solver.notify_and_forget(auction_id, Some(solution_id.clone()), notification); } pub fn encoding_failed( @@ -76,7 +83,7 @@ pub fn encoding_failed( solution::Error::Encoding(_) => return, }; - solver.notify(auction_id, Some(solution_id.clone()), notification); + solver.notify_and_forget(auction_id, Some(solution_id.clone()), notification); } pub fn simulation_failed( @@ -94,7 +101,7 @@ pub fn simulation_failed( ), simulator::Error::Other(error) => notification::Kind::DriverError(error.to_string()), }; - solver.notify(auction_id, Some(solution_id.clone()), kind); + solver.notify_and_forget(auction_id, Some(solution_id.clone()), kind); } pub fn executed( @@ -111,7 +118,7 @@ pub fn executed( Err(Error::Other(_) | Error::Disabled) => notification::Settlement::Fail, }; - solver.notify( + solver.notify_and_forget( Some(auction_id), Some(solution_id.clone()), notification::Kind::Settled(kind), @@ -123,7 +130,7 @@ pub fn duplicated_solution_id( auction_id: Option, solution_id: &solution::Id, ) { - solver.notify( + solver.notify_and_forget( auction_id, Some(solution_id.clone()), notification::Kind::DuplicatedSolutionId, @@ -131,5 +138,5 @@ pub fn duplicated_solution_id( } pub fn postprocessing_timed_out(solver: &Solver, auction_id: Option) { - solver.notify(auction_id, None, notification::Kind::PostprocessingTimedOut); + solver.notify_and_forget(auction_id, None, notification::Kind::PostprocessingTimedOut); } diff --git a/crates/driver/src/infra/notify/notification.rs b/crates/driver/src/infra/notify/notification.rs index 0a178e9af5..2b9fe40003 100644 --- a/crates/driver/src/infra/notify/notification.rs +++ b/crates/driver/src/infra/notify/notification.rs @@ -45,6 +45,11 @@ pub enum Kind { DriverError(String), /// On-chain solution postprocessing timed out. PostprocessingTimedOut, + /// The solver has been banned for a specific reason. + Banned { + reason: BanReason, + until_timestamp: u64, + }, } #[derive(Debug)] @@ -58,6 +63,12 @@ pub enum ScoreKind { MissingPrice(TokenAddress), } +#[derive(Debug)] +pub enum BanReason { + /// The driver won multiple consecutive auctions but never settled them. + UnsettledConsecutiveAuctions, +} + #[derive(Debug)] pub enum Settlement { /// Winning solver settled successfully transaction onchain. diff --git a/crates/driver/src/infra/solver/dto/notification.rs b/crates/driver/src/infra/solver/dto/notification.rs index 08969c5c35..49f55ac399 100644 --- a/crates/driver/src/infra/solver/dto/notification.rs +++ b/crates/driver/src/infra/solver/dto/notification.rs @@ -61,6 +61,17 @@ impl Notification { notify::Settlement::Expired => Kind::Expired, }, notify::Kind::PostprocessingTimedOut => Kind::PostprocessingTimedOut, + notify::Kind::Banned { + reason, + until_timestamp, + } => Kind::Banned { + reason: match reason { + notify::BanReason::UnsettledConsecutiveAuctions => { + BanReason::UnsettledConsecutiveAuctions + } + }, + until_timestamp, + }, }, } } @@ -144,6 +155,17 @@ pub enum Kind { Expired, Fail, PostprocessingTimedOut, + Banned { + reason: BanReason, + until_timestamp: u64, + }, +} + +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase", tag = "reason")] +pub enum BanReason { + UnsettledConsecutiveAuctions, } type BlockNo = u64; diff --git a/crates/driver/src/infra/solver/mod.rs b/crates/driver/src/infra/solver/mod.rs index 3e63af9e86..bda080e0b1 100644 --- a/crates/driver/src/infra/solver/mod.rs +++ b/crates/driver/src/infra/solver/mod.rs @@ -260,27 +260,71 @@ impl Solver { } /// Make a fire and forget POST request to notify the solver about an event. - pub fn notify( + pub fn notify_and_forget( &self, auction_id: Option, solution_id: Option, kind: notify::Kind, ) { + let base_url = self.config.endpoint.clone(); + let client = self.client.clone(); + let response_limit = self.config.response_size_limit_max_bytes; + let future = async move { + if let Err(error) = Self::notify_inner( + base_url, + client, + response_limit, + auction_id, + solution_id, + kind, + ) + .await + { + tracing::warn!(?error, "failed to notify solver"); + } + }; + + tokio::task::spawn(future.in_current_span()); + } + + pub async fn notify( + &self, + auction_id: Option, + solution_id: Option, + kind: notify::Kind, + ) -> Result<(), crate::util::http::Error> { + let base_url = self.config.endpoint.clone(); + let client = self.client.clone(); + + Self::notify_inner( + base_url, + client, + self.config.response_size_limit_max_bytes, + auction_id, + solution_id, + kind, + ) + .await + } + + async fn notify_inner( + base_url: url::Url, + client: reqwest::Client, + response_limit: usize, + auction_id: Option, + solution_id: Option, + kind: notify::Kind, + ) -> Result<(), crate::util::http::Error> { let body = serde_json::to_string(&dto::Notification::new(auction_id, solution_id, kind)).unwrap(); - let url = shared::url::join(&self.config.endpoint, "notify"); + let url = shared::url::join(&base_url, "notify"); super::observe::solver_request(&url, &body); - let mut req = self.client.post(url).body(body); + let mut req = client.post(url).body(body); if let Some(id) = observe::request_id::from_current_span() { req = req.header("X-REQUEST-ID", id); } - let response_size = self.config.response_size_limit_max_bytes; - let future = async move { - if let Err(error) = util::http::send(response_size, req).await { - tracing::warn!(?error, "failed to notify solver"); - } - }; - tokio::task::spawn(future.in_current_span()); + + util::http::send(response_limit, req).await.map(|_| ()) } } diff --git a/crates/solvers/openapi.yml b/crates/solvers/openapi.yml index af9562efd2..0b79459413 100644 --- a/crates/solvers/openapi.yml +++ b/crates/solvers/openapi.yml @@ -89,6 +89,7 @@ paths: - cancelled - fail - postprocessingTimedOut + - banned responses: "200": description: notification successfully received.