From e0d08c2aa1966af384e7f424791ba423807c3a91 Mon Sep 17 00:00:00 2001 From: ilya Date: Fri, 26 Dec 2025 14:18:20 +0000 Subject: [PATCH 1/6] Refactor participant score --- .../autopilot/src/domain/competition/mod.rs | 15 +-- .../src/domain/competition/participant.rs | 68 ++++++++---- .../domain/competition/winner_selection.rs | 102 +++++++++--------- crates/autopilot/src/infra/persistence/mod.rs | 2 +- crates/autopilot/src/run_loop.rs | 12 +-- crates/autopilot/src/shadow.rs | 8 +- 6 files changed, 114 insertions(+), 93 deletions(-) diff --git a/crates/autopilot/src/domain/competition/mod.rs b/crates/autopilot/src/domain/competition/mod.rs index b2ac1f2204..47d46f4db2 100644 --- a/crates/autopilot/src/domain/competition/mod.rs +++ b/crates/autopilot/src/domain/competition/mod.rs @@ -12,7 +12,7 @@ mod participation_guard; pub mod winner_selection; pub use { - participant::{Participant, Ranked, Unranked}, + participant::{Participant, RankType, Ranked, Scored, Unscored}, participation_guard::SolverParticipationGuard, }; @@ -25,10 +25,6 @@ pub struct Solution { solver: Address, orders: HashMap, prices: auction::Prices, - /// Score computed by the autopilot based on the solution - /// of the solver. - // TODO: refactor this to compute the score in the constructor - score: Option, } impl Solution { @@ -43,10 +39,11 @@ impl Solution { solver, orders, prices, - score: None, } } +} +impl Solution { pub fn id(&self) -> SolutionId { self.id } @@ -55,12 +52,6 @@ impl Solution { self.solver } - pub fn score(&self) -> Score { - self.score.expect( - "this function only gets called after the winner selection populated this value", - ) - } - pub fn order_ids(&self) -> impl Iterator + std::fmt::Debug { self.orders.keys() } diff --git a/crates/autopilot/src/domain/competition/participant.rs b/crates/autopilot/src/domain/competition/participant.rs index 766ea9b9c4..85c14f8615 100644 --- a/crates/autopilot/src/domain/competition/participant.rs +++ b/crates/autopilot/src/domain/competition/participant.rs @@ -1,62 +1,88 @@ -use { - super::{Score, Solution}, - crate::infra, - std::sync::Arc, -}; +use {super::Score, crate::infra, std::sync::Arc}; #[derive(Clone)] pub struct Participant { - solution: Solution, + solution: super::Solution, driver: Arc, state: State, } #[derive(Clone)] -pub struct Unranked; -pub enum Ranked { +pub struct Unscored; + +#[derive(Clone)] +pub struct Scored { + pub(super) score: Score, +} + +#[derive(Clone)] +pub struct Ranked { + pub(super) rank_type: RankType, + pub(super) score: Score, +} + +#[derive(Clone)] +pub enum RankType { Winner, NonWinner, FilteredOut, } impl Participant { - pub fn solution(&self) -> &Solution { + pub fn solution(&self) -> &super::Solution { &self.solution } - pub fn set_score(&mut self, score: Score) { - self.solution.score = Some(score); - } - pub fn driver(&self) -> &Arc { &self.driver } } -impl Participant { - pub fn new(solution: Solution, driver: Arc) -> Self { +impl Participant { + pub fn new(solution: super::Solution, driver: Arc) -> Self { Self { solution, driver, - state: Unranked, + state: Unscored, } } - pub fn rank(self, rank: Ranked) -> Participant { - Participant:: { - state: rank, + pub fn with_score(self, score: Score) -> Participant { + Participant { solution: self.solution, driver: self.driver, + state: Scored { score }, + } + } +} + +impl Participant { + pub fn score(&self) -> Score { + self.state.score + } + + pub fn rank(self, rank_type: RankType) -> Participant { + Participant { + solution: self.solution, + driver: self.driver, + state: Ranked { + rank_type, + score: self.state.score, + }, } } } impl Participant { + pub fn score(&self) -> Score { + self.state.score + } + pub fn is_winner(&self) -> bool { - matches!(self.state, Ranked::Winner) + matches!(self.state.rank_type, RankType::Winner) } pub fn filtered_out(&self) -> bool { - matches!(self.state, Ranked::FilteredOut) + matches!(self.state.rank_type, RankType::FilteredOut) } } diff --git a/crates/autopilot/src/domain/competition/winner_selection.rs b/crates/autopilot/src/domain/competition/winner_selection.rs index fbadf69e9e..7150083546 100644 --- a/crates/autopilot/src/domain/competition/winner_selection.rs +++ b/crates/autopilot/src/domain/competition/winner_selection.rs @@ -32,7 +32,7 @@ use { Prices, order::{self, TargetAmount}, }, - competition::{Participant, Ranked, Score, Solution, Unranked}, + competition::{Participant, RankType, Ranked, Score, Scored, Solution, Unscored}, eth::{self, WrappedNativeToken}, fee, settlement::{ @@ -57,14 +57,14 @@ impl Arbitrator { /// Runs the entire auction mechanism on the passed in solutions. pub fn arbitrate( &self, - participants: Vec>, + participants: Vec>, auction: &domain::Auction, ) -> Ranking { let partitioned = self.partition_unfair_solutions(participants, auction); let filtered_out = partitioned .discarded .into_iter() - .map(|participant| participant.rank(Ranked::FilteredOut)) + .map(|participant| participant.rank(RankType::FilteredOut)) .collect(); let mut ranked = self.mark_winners(partitioned.kept); @@ -73,7 +73,7 @@ impl Arbitrator { // winners before non-winners std::cmp::Reverse(participant.is_winner()), // high score before low score - std::cmp::Reverse(participant.solution().score()), + std::cmp::Reverse(participant.score()), ) }); Ranking { @@ -85,16 +85,17 @@ impl Arbitrator { /// Removes unfair solutions from the set of all solutions. fn partition_unfair_solutions( &self, - mut participants: Vec>, + participants: Vec>, auction: &domain::Auction, ) -> PartitionedSolutions { // Discard all solutions where we can't compute the aggregate scores // accurately because the fairness guarantees heavily rely on them. - let scores_by_solution = compute_scores_by_solution(&mut participants, auction); + let (mut participants, scores_by_solution) = + compute_scores_by_solution(participants, auction); participants.sort_by_key(|participant| { std::cmp::Reverse( // we use the computed score to not trust the score provided by solvers - participant.solution().score().get().0, + participant.score().get().0, ) }); let baseline_scores = compute_baseline_scores(&scores_by_solution); @@ -130,15 +131,15 @@ impl Arbitrator { /// Picks winners and sorts all solutions where winners come before /// non-winners and higher scores come before lower scores. - fn mark_winners(&self, participants: Vec>) -> Vec { + fn mark_winners(&self, participants: Vec>) -> Vec> { let winner_indexes = self.pick_winners(participants.iter().map(|p| p.solution())); participants .into_iter() .enumerate() .map(|(index, participant)| { let rank = match winner_indexes.contains(&index) { - true => Ranked::Winner, - false => Ranked::NonWinner, + true => RankType::Winner, + false => RankType::NonWinner, }; participant.rank(rank) }) @@ -165,17 +166,19 @@ impl Arbitrator { continue; } - let solutions_without_solver = ranking + let participants_without_solver: Vec<_> = ranking .ranked .iter() .filter(|p| p.driver().submission_address != solver) - .map(|p| p.solution()); - let winner_indices = self.pick_winners(solutions_without_solver.clone()); + .collect(); + let solutions = participants_without_solver.iter().map(|p| p.solution()); + let winner_indices = self.pick_winners(solutions); - let score = solutions_without_solver + let score = participants_without_solver + .iter() .enumerate() .filter(|(index, _)| winner_indices.contains(index)) - .filter_map(|(_, solution)| solution.score) + .map(|(_, p)| p.score()) .reduce(Score::saturating_add) .unwrap_or_default(); reference_scores.insert(solver, score); @@ -244,39 +247,40 @@ fn compute_baseline_scores(scores_by_solution: &ScoresBySolution) -> ScoreByDire /// Solutions get discarded because fairness guarantees heavily /// depend on these scores being accurate. fn compute_scores_by_solution( - participants: &mut Vec>, + participants: Vec>, auction: &domain::Auction, -) -> ScoresBySolution { +) -> (Vec>, ScoresBySolution) { let auction = Auction::from(auction); - let mut scores = HashMap::default(); - - participants.retain_mut(|p| match score_by_token_pair(p.solution(), &auction) { - Ok(score) => { - let total_score = score - .values() - .fold(Score::default(), |acc, score| acc.saturating_add(*score)); - scores.insert( - SolutionKey { - driver: p.driver().submission_address, - solution_id: p.solution().id, - }, - score, - ); - p.set_score(total_score); - true - } - Err(err) => { - tracing::warn!( - driver = p.driver().name, - ?err, - solution = ?p.solution(), - "discarding solution where scores could not be computed" - ); - false + let mut scores_by_solution = HashMap::default(); + let mut scored_participants = Vec::new(); + + for participant in participants { + match score_by_token_pair(participant.solution(), &auction) { + Ok(score) => { + let total_score = score + .values() + .fold(Score::default(), |acc, score| acc.saturating_add(*score)); + scores_by_solution.insert( + SolutionKey { + driver: participant.driver().submission_address, + solution_id: participant.solution().id, + }, + score, + ); + scored_participants.push(participant.with_score(total_score)); + } + Err(err) => { + tracing::warn!( + driver = participant.driver().name, + ?err, + solution = ?participant.solution(), + "discarding solution where scores could not be computed" + ); + } } - }); + } - scores + (scored_participants, scores_by_solution) } /// Returns the total scores for each directed token pair of the solution. @@ -450,8 +454,8 @@ impl Ranking { } struct PartitionedSolutions { - kept: Vec>, - discarded: Vec>, + kept: Vec>, + discarded: Vec>, } #[cfg(test)] @@ -466,7 +470,7 @@ mod tests { Price, order::{self, AppDataHash}, }, - competition::{Participant, Solution, TradedOrder, Unranked}, + competition::{Participant, Solution, TradedOrder, Unscored}, eth::{self, TokenAddress}, }, infra::Driver, @@ -1214,7 +1218,7 @@ mod tests { // winners before non-winners std::cmp::Reverse(a.is_winner()), // high score before low score - std::cmp::Reverse(a.solution().score()) + std::cmp::Reverse(a.score()) ))); assert_eq!(winners.len(), self.expected_winners.len()); for (actual, expected) in winners.iter().zip(&self.expected_winners) { @@ -1374,7 +1378,7 @@ mod tests { solver_address: Address, trades: Vec<(OrderUid, TradedOrder)>, prices: Option>, - ) -> Participant { + ) -> Participant { // The prices of the tokens do not affect the result but they keys must exist // for every token of every trade let prices = prices.unwrap_or({ diff --git a/crates/autopilot/src/infra/persistence/mod.rs b/crates/autopilot/src/infra/persistence/mod.rs index b15421d350..b72faf2019 100644 --- a/crates/autopilot/src/infra/persistence/mod.rs +++ b/crates/autopilot/src/infra/persistence/mod.rs @@ -228,7 +228,7 @@ impl Persistence { is_winner: participant.is_winner(), filtered_out: participant.filtered_out(), score: number::conversions::alloy::u256_to_big_decimal( - &participant.solution().score().get().0, + &participant.score().get().0, ), orders: participant .solution() diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index 104cd3d30b..3da7271700 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -10,7 +10,7 @@ use { Solution, SolutionError, SolverParticipationGuard, - Unranked, + Unscored, winner_selection::{self, Ranking}, }, eth::{self, TxId}, @@ -495,7 +495,7 @@ impl RunLoop { .map(|(index, participant)| SolverSettlement { solver: participant.driver().name.clone(), solver_address: participant.solution().solver(), - score: Some(Score::Solver(participant.solution().score().get().0)), + score: Some(Score::Solver(participant.score().get().0)), ranking: index + 1, orders: participant .solution() @@ -610,7 +610,7 @@ impl RunLoop { async fn fetch_solutions( &self, auction: &domain::Auction, - ) -> Vec> { + ) -> Vec> { let request = solve::Request::new( auction, &self.trusted_tokens.all(), @@ -662,7 +662,7 @@ impl RunLoop { &self, driver: Arc, request: solve::Request, - ) -> Vec> { + ) -> Vec> { let start = Instant::now(); let result = self.try_solve(Arc::clone(&driver), request).await; let solutions = match result { @@ -1135,7 +1135,7 @@ pub mod observe { use { crate::domain::{ self, - competition::{Unranked, winner_selection::Ranking}, + competition::{Unscored, winner_selection::Ranking}, }, std::collections::HashSet, }; @@ -1168,7 +1168,7 @@ pub mod observe { ); } - pub fn solutions(solutions: &[domain::competition::Participant]) { + pub fn solutions(solutions: &[domain::competition::Participant]) { if solutions.is_empty() { tracing::info!("no solutions for auction"); } diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 74985b3c64..3d47e6ca4a 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -11,7 +11,7 @@ use { crate::{ domain::{ self, - competition::{Participant, Score, Unranked, winner_selection}, + competition::{Participant, Score, Unscored, winner_selection}, eth::WrappedNativeToken, }, infra::{ @@ -135,7 +135,7 @@ impl RunLoop { let total_score = ranking .winners() - .map(|p| p.solution().score()) + .map(|p| p.score()) .reduce(Score::saturating_add) .unwrap_or_default(); @@ -177,7 +177,7 @@ impl RunLoop { /// Runs the solver competition, making all configured drivers participate. #[instrument(skip_all)] - async fn competition(&self, auction: &domain::Auction) -> Vec> { + async fn competition(&self, auction: &domain::Auction) -> Vec> { let request = solve::Request::new(auction, &self.trusted_tokens.all(), self.solve_deadline); futures::future::join_all( @@ -198,7 +198,7 @@ impl RunLoop { driver: Arc, request: solve::Request, auction_id: i64, - ) -> Vec> { + ) -> Vec> { let solutions = match self.fetch_solutions(&driver, request).await { Ok(response) => { Metrics::get() From 6140d1079e1407e2365fa79bb18af5aabf25f6d5 Mon Sep 17 00:00:00 2001 From: ilya Date: Fri, 26 Dec 2025 18:32:44 +0000 Subject: [PATCH 2/6] Nit --- crates/autopilot/src/domain/competition/winner_selection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/autopilot/src/domain/competition/winner_selection.rs b/crates/autopilot/src/domain/competition/winner_selection.rs index 7150083546..54d8a204e0 100644 --- a/crates/autopilot/src/domain/competition/winner_selection.rs +++ b/crates/autopilot/src/domain/competition/winner_selection.rs @@ -263,7 +263,7 @@ fn compute_scores_by_solution( scores_by_solution.insert( SolutionKey { driver: participant.driver().submission_address, - solution_id: participant.solution().id, + solution_id: participant.solution().id(), }, score, ); From dd8b019c06cbadce9d00dec72459eb8f1c272c5d Mon Sep 17 00:00:00 2001 From: ilya Date: Tue, 30 Dec 2025 10:39:05 +0000 Subject: [PATCH 3/6] Nits --- crates/autopilot/src/domain/competition/participant.rs | 10 +++++++--- .../src/domain/competition/winner_selection.rs | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/autopilot/src/domain/competition/participant.rs b/crates/autopilot/src/domain/competition/participant.rs index 85c14f8615..6932a3a0b6 100644 --- a/crates/autopilot/src/domain/competition/participant.rs +++ b/crates/autopilot/src/domain/competition/participant.rs @@ -1,8 +1,12 @@ -use {super::Score, crate::infra, std::sync::Arc}; +use { + super::Score, + crate::{domain::competition::Solution, infra}, + std::sync::Arc, +}; #[derive(Clone)] pub struct Participant { - solution: super::Solution, + solution: Solution, driver: Arc, state: State, } @@ -61,7 +65,7 @@ impl Participant { self.state.score } - pub fn rank(self, rank_type: RankType) -> Participant { + pub fn with_rank(self, rank_type: RankType) -> Participant { Participant { solution: self.solution, driver: self.driver, diff --git a/crates/autopilot/src/domain/competition/winner_selection.rs b/crates/autopilot/src/domain/competition/winner_selection.rs index 54d8a204e0..c05d789416 100644 --- a/crates/autopilot/src/domain/competition/winner_selection.rs +++ b/crates/autopilot/src/domain/competition/winner_selection.rs @@ -64,7 +64,7 @@ impl Arbitrator { let filtered_out = partitioned .discarded .into_iter() - .map(|participant| participant.rank(RankType::FilteredOut)) + .map(|participant| participant.with_rank(RankType::FilteredOut)) .collect(); let mut ranked = self.mark_winners(partitioned.kept); @@ -141,7 +141,7 @@ impl Arbitrator { true => RankType::Winner, false => RankType::NonWinner, }; - participant.rank(rank) + participant.with_rank(rank) }) .collect() } From bbfdfffd5e32d2a46f54dd8ae43a651c9420e03d Mon Sep 17 00:00:00 2001 From: ilya Date: Tue, 30 Dec 2025 10:55:30 +0000 Subject: [PATCH 4/6] nit --- crates/autopilot/src/domain/competition/participant.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/autopilot/src/domain/competition/participant.rs b/crates/autopilot/src/domain/competition/participant.rs index 6932a3a0b6..0f5fbdc03e 100644 --- a/crates/autopilot/src/domain/competition/participant.rs +++ b/crates/autopilot/src/domain/competition/participant.rs @@ -33,7 +33,7 @@ pub enum RankType { } impl Participant { - pub fn solution(&self) -> &super::Solution { + pub fn solution(&self) -> &Solution { &self.solution } @@ -43,7 +43,7 @@ impl Participant { } impl Participant { - pub fn new(solution: super::Solution, driver: Arc) -> Self { + pub fn new(solution: Solution, driver: Arc) -> Self { Self { solution, driver, From ce49ae1bd66eeb1b1f9765676606bd981e59d144 Mon Sep 17 00:00:00 2001 From: ilya Date: Fri, 2 Jan 2026 09:44:09 +0000 Subject: [PATCH 5/6] Use iterator --- .../autopilot/src/domain/competition/winner_selection.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/autopilot/src/domain/competition/winner_selection.rs b/crates/autopilot/src/domain/competition/winner_selection.rs index c05d789416..5c967c4f0a 100644 --- a/crates/autopilot/src/domain/competition/winner_selection.rs +++ b/crates/autopilot/src/domain/competition/winner_selection.rs @@ -166,16 +166,14 @@ impl Arbitrator { continue; } - let participants_without_solver: Vec<_> = ranking + let participants_without_solver = ranking .ranked .iter() - .filter(|p| p.driver().submission_address != solver) - .collect(); - let solutions = participants_without_solver.iter().map(|p| p.solution()); + .filter(|p| p.driver().submission_address != solver); + let solutions = participants_without_solver.clone().map(|p| p.solution()); let winner_indices = self.pick_winners(solutions); let score = participants_without_solver - .iter() .enumerate() .filter(|(index, _)| winner_indices.contains(index)) .map(|(_, p)| p.score()) From 8aa8ebf754f49d0e044440d13cdc9cfeb629b545 Mon Sep 17 00:00:00 2001 From: ilya Date: Fri, 2 Jan 2026 09:47:48 +0000 Subject: [PATCH 6/6] Vec capacity --- crates/autopilot/src/domain/competition/winner_selection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/autopilot/src/domain/competition/winner_selection.rs b/crates/autopilot/src/domain/competition/winner_selection.rs index 5c967c4f0a..35a3a6ec15 100644 --- a/crates/autopilot/src/domain/competition/winner_selection.rs +++ b/crates/autopilot/src/domain/competition/winner_selection.rs @@ -250,7 +250,7 @@ fn compute_scores_by_solution( ) -> (Vec>, ScoresBySolution) { let auction = Auction::from(auction); let mut scores_by_solution = HashMap::default(); - let mut scored_participants = Vec::new(); + let mut scored_participants = Vec::with_capacity(participants.len()); for participant in participants { match score_by_token_pair(participant.solution(), &auction) {