From 00fb103b2484297510bcdbf7215bbe5c03063034 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 16 Sep 2024 14:26:45 +0200 Subject: [PATCH 01/11] Add configuration for multiple winners --- crates/autopilot/src/arguments.rs | 7 +++++++ crates/autopilot/src/run.rs | 2 ++ crates/autopilot/src/run_loop.rs | 1 + crates/autopilot/src/shadow.rs | 4 ++++ 4 files changed, 14 insertions(+) diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index 9c2457b655..a717f70ccc 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -227,6 +227,11 @@ pub struct Arguments { /// before continuing the run loop. #[clap(long, env, default_value = "2s", value_parser = humantime::parse_duration)] pub max_run_loop_delay: Duration, + + #[clap(long, env, default_value = "1")] + /// The maximum number of winners per auction. Each winner will be allowed + /// to settle their winning orders at the same time. + pub max_winners_per_auction: usize, } impl std::fmt::Display for Arguments { @@ -271,6 +276,7 @@ impl std::fmt::Display for Arguments { cow_amm_configs, run_loop_mode, max_run_loop_delay, + max_winners_per_auction, } = self; write!(f, "{}", shared)?; @@ -345,6 +351,7 @@ impl std::fmt::Display for Arguments { writeln!(f, "cow_amm_configs: {:?}", cow_amm_configs)?; writeln!(f, "run_loop_mode: {:?}", run_loop_mode)?; writeln!(f, "max_run_loop_delay: {:?}", max_run_loop_delay)?; + writeln!(f, "max_winners_per_auction: {:?}", max_winners_per_auction)?; Ok(()) } } diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index e30a663caa..b84cd98058 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -544,6 +544,7 @@ pub async fn run(args: Arguments) { synchronization: args.run_loop_mode, max_run_loop_delay: args.max_run_loop_delay, maintenance: Arc::new(maintenance), + max_winners_per_auction: args.max_winners_per_auction, }; run.run_forever(args.auction_update_interval).await; unreachable!("run loop exited"); @@ -618,6 +619,7 @@ async fn shadow_mode(args: Arguments) -> ! { liveness.clone(), args.run_loop_mode, current_block, + args.max_winners_per_auction, ); shadow.run_forever().await; diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index c62e259ba8..8311d5cf6a 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -55,6 +55,7 @@ pub struct RunLoop { /// allowed to start before it has to re-synchronize to the blockchain /// by waiting for the next block to appear. pub max_run_loop_delay: Duration, + pub max_winners_per_auction: usize, /// Maintenance tasks that should run before every runloop to have /// the most recent data available. pub maintenance: Arc, diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 6e1d219631..284bb64eb6 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -38,9 +38,11 @@ pub struct RunLoop { liveness: Arc, synchronization: RunLoopMode, current_block: CurrentBlockWatcher, + _max_winners_per_auction: usize, } impl RunLoop { + #[allow(clippy::too_many_arguments)] pub fn new( orderbook: infra::shadow::Orderbook, drivers: Vec, @@ -49,6 +51,7 @@ impl RunLoop { liveness: Arc, synchronization: RunLoopMode, current_block: CurrentBlockWatcher, + _max_winners_per_auction: usize, ) -> Self { Self { orderbook, @@ -60,6 +63,7 @@ impl RunLoop { liveness, synchronization, current_block, + _max_winners_per_auction, } } From 348fd2c7d6f78c001209aa6e9c91160d5496cf2a Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 27 Sep 2024 16:22:45 +0200 Subject: [PATCH 02/11] multiple winners in autopilot runloop --- .../autopilot/src/domain/competition/mod.rs | 4 +- crates/autopilot/src/run_loop.rs | 82 +++++++++++++------ 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/crates/autopilot/src/domain/competition/mod.rs b/crates/autopilot/src/domain/competition/mod.rs index 783f55bf2e..1cf3bc803e 100644 --- a/crates/autopilot/src/domain/competition/mod.rs +++ b/crates/autopilot/src/domain/competition/mod.rs @@ -10,7 +10,7 @@ use { type SolutionId = u64; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SolutionWithId { id: SolutionId, solution: Solution, @@ -55,7 +55,7 @@ impl SolutionWithId { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Solution { solver: eth::Address, score: Score, diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index 14a0e83dc8..f5b6284c0d 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -234,36 +234,42 @@ impl RunLoop { return; } - let competition_simulation_block = self.eth.current_block().borrow().number; + let winners = self.select_winners(&solutions); + if winners.is_empty() { + tracing::info!("no winners for auction"); + return; + } - // TODO: Keep going with other solutions until some deadline. - if let Some(Participant { driver, solution }) = solutions.last() { - tracing::info!(driver = %driver.name, solution = %solution.id(), "winner"); + let competition_simulation_block = self.eth.current_block().borrow().number; + let block_deadline = competition_simulation_block + self.config.submission_deadline; - let block_deadline = competition_simulation_block + self.config.submission_deadline; + // Post-processing should not be executed asynchronously since it includes steps + // of storing all the competition/auction-related data to the DB. + if let Err(err) = self + .post_processing( + auction_id, + auction, + competition_simulation_block, + // TODO: Support multiple winners + // https://github.com/cowprotocol/services/issues/3021 + &winners.first().expect("must exist").solution, + &solutions, + block_deadline, + ) + .await + { + tracing::error!(?err, "failed to post-process competition"); + return; + } - // Post-processing should not be executed asynchronously since it includes steps - // of storing all the competition/auction-related data to the DB. - if let Err(err) = self - .post_processing( - auction_id, - auction, - competition_simulation_block, - solution, - &solutions, - block_deadline, - ) - .await - { - tracing::error!(?err, "failed to post-process competition"); - return; - } + for Participant { driver, solution } in winners { + tracing::info!(driver = %driver.name, solution = %solution.id(), "winner"); self.start_settlement_execution( auction_id, single_run_start, - driver, - solution, + &driver, + &solution, block_deadline, ) .await; @@ -526,6 +532,35 @@ impl RunLoop { solutions } + /// Chooses the winners from the given participants. + /// + /// Participants are already sorted by their score (worst to best). + /// + /// Winners are selected one by one, starting from the best solution, + /// until `max_winners_per_auction` is hit. The solution can become winner + /// it is swaps tokens that are not yet swapped by any other already + /// selected winner. + fn select_winners(&self, participants: &[Participant]) -> Vec { + let mut winners = Vec::new(); + let mut already_swapped_tokens = HashSet::new(); + for participant in participants.iter().rev() { + let swapped_tokens = participant + .solution + .orders() + .iter() + .map(|(_, order)| (order.sell.token, order.buy.token)) + .collect::>(); + if swapped_tokens.is_disjoint(&already_swapped_tokens) { + winners.push(participant.clone()); + already_swapped_tokens.extend(swapped_tokens); + if winners.len() >= self.config.max_winners_per_auction { + break; + } + } + } + winners + } + /// Records metrics, order events and logs for the given solutions. /// Expects the winning solution to be the last in the list. fn report_on_solutions(&self, solutions: &[Participant], auction: &domain::Auction) { @@ -853,6 +888,7 @@ impl RunLoop { } } +#[derive(Clone)] struct Participant { driver: Arc, solution: competition::SolutionWithId, From 6ce361819ce6415527edc4840c08a1a3addef54f Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 30 Sep 2024 11:37:16 +0200 Subject: [PATCH 03/11] fix shadow --- .../autopilot/src/infra/solvers/dto/solve.rs | 43 ++++--- crates/autopilot/src/run_loop.rs | 21 +-- crates/autopilot/src/shadow.rs | 120 +++++++++++++----- 3 files changed, 119 insertions(+), 65 deletions(-) diff --git a/crates/autopilot/src/infra/solvers/dto/solve.rs b/crates/autopilot/src/infra/solvers/dto/solve.rs index 8a9581d41c..3a78b41aaa 100644 --- a/crates/autopilot/src/infra/solvers/dto/solve.rs +++ b/crates/autopilot/src/infra/solvers/dto/solve.rs @@ -99,27 +99,7 @@ impl Solution { domain::competition::Score::new(self.score.into())?, self.orders .into_iter() - .map(|(o, amounts)| { - ( - o.into(), - domain::competition::TradedOrder { - sell: eth::Asset { - token: amounts.sell_token.into(), - amount: amounts.limit_sell.into(), - }, - buy: eth::Asset { - token: amounts.buy_token.into(), - amount: amounts.limit_buy.into(), - }, - side: match amounts.side { - Side::Buy => domain::auction::order::Side::Buy, - Side::Sell => domain::auction::order::Side::Sell, - }, - executed_sell: amounts.executed_sell.into(), - executed_buy: amounts.executed_buy.into(), - }, - ) - }) + .map(|(o, amounts)| (o.into(), amounts.into_domain())) .collect(), self.clearing_prices .into_iter() @@ -156,6 +136,27 @@ pub struct TradedOrder { executed_buy: U256, } +impl TradedOrder { + pub fn into_domain(self) -> domain::competition::TradedOrder { + domain::competition::TradedOrder { + sell: eth::Asset { + token: self.sell_token.into(), + amount: self.limit_sell.into(), + }, + buy: eth::Asset { + token: self.buy_token.into(), + amount: self.limit_buy.into(), + }, + side: match self.side { + Side::Buy => domain::auction::order::Side::Buy, + Side::Sell => domain::auction::order::Side::Sell, + }, + executed_sell: self.executed_sell.into(), + executed_buy: self.executed_buy.into(), + } + } +} + #[serde_as] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index f5b6284c0d..51fa44e1b3 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -487,7 +487,7 @@ impl RunLoop { } /// Runs the solver competition, making all configured drivers participate. - /// Returns all fair solutions sorted by their score (worst to best). + /// Returns all fair solutions sorted by their score (best to worst). async fn competition( &self, id: domain::auction::Id, @@ -517,11 +517,14 @@ impl RunLoop { // Shuffle so that sorting randomly splits ties. solutions.shuffle(&mut rand::thread_rng()); - solutions.sort_unstable_by_key(|participant| participant.solution.score().get().0); + solutions.sort_unstable_by_key(|participant| { + std::cmp::Reverse(participant.solution.score().get().0) + }); // Make sure the winning solution is fair. - while !Self::is_solution_fair(solutions.last(), &solutions, auction) { - let unfair_solution = solutions.pop().expect("must exist"); + while !Self::is_solution_fair(solutions.first(), &solutions, auction) { + // remove the solution.first() from the solutions + let unfair_solution = solutions.remove(0); tracing::warn!( invalidated = unfair_solution.driver.name, "fairness check invalidated of solution" @@ -534,7 +537,7 @@ impl RunLoop { /// Chooses the winners from the given participants. /// - /// Participants are already sorted by their score (worst to best). + /// Participants are already sorted by their score (best to worst). /// /// Winners are selected one by one, starting from the best solution, /// until `max_winners_per_auction` is hit. The solution can become winner @@ -543,7 +546,7 @@ impl RunLoop { fn select_winners(&self, participants: &[Participant]) -> Vec { let mut winners = Vec::new(); let mut already_swapped_tokens = HashSet::new(); - for participant in participants.iter().rev() { + for participant in participants.iter() { let swapped_tokens = participant .solution .orders() @@ -562,9 +565,9 @@ impl RunLoop { } /// Records metrics, order events and logs for the given solutions. - /// Expects the winning solution to be the last in the list. + /// Expects the winning solution to be the first in the list. fn report_on_solutions(&self, solutions: &[Participant], auction: &domain::Auction) { - let Some(winner) = solutions.last() else { + let Some(winner) = solutions.first() else { // no solutions means nothing to report return; }; @@ -583,7 +586,7 @@ impl RunLoop { .flat_map(|solution| solution.solution.order_ids().copied()) .collect(); let winning_orders: HashSet<_> = solutions - .last() + .first() .into_iter() .flat_map(|solution| solution.solution.order_ids().copied()) .collect(); diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 284bb64eb6..8804739ae4 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -10,7 +10,7 @@ use { crate::{ arguments::RunLoopMode, - domain, + domain::{self, competition::TradedOrder}, infra::{ self, solvers::dto::{reveal, solve}, @@ -24,7 +24,12 @@ use { primitive_types::{H160, U256}, rand::seq::SliceRandom, shared::token_list::AutoUpdatingTokenList, - std::{cmp, sync::Arc, time::Duration}, + std::{ + cmp, + collections::{HashMap, HashSet}, + sync::Arc, + time::Duration, + }, tracing::Instrument, }; @@ -38,7 +43,7 @@ pub struct RunLoop { liveness: Arc, synchronization: RunLoopMode, current_block: CurrentBlockWatcher, - _max_winners_per_auction: usize, + max_winners_per_auction: usize, } impl RunLoop { @@ -51,7 +56,7 @@ impl RunLoop { liveness: Arc, synchronization: RunLoopMode, current_block: CurrentBlockWatcher, - _max_winners_per_auction: usize, + max_winners_per_auction: usize, ) -> Self { Self { orderbook, @@ -63,7 +68,7 @@ impl RunLoop { liveness, synchronization, current_block, - _max_winners_per_auction, + max_winners_per_auction, } } @@ -123,30 +128,25 @@ impl RunLoop { .orders .set(i64::try_from(auction.orders.len()).unwrap_or(i64::MAX)); - let mut participants = self.competition(id, auction).await; - - // Shuffle so that sorting randomly splits ties. - participants.shuffle(&mut rand::thread_rng()); - participants.sort_unstable_by_key(|participant| cmp::Reverse(participant.score())); + let participants = self.competition(id, auction).await; + let winners = self.select_winners(&participants); - if let Some(Participant { - driver, - solution: Ok(solution), - }) = participants.first() - { - let reference_score = participants - .get(1) - .map(|participant| participant.score()) + for (i, Participant { driver, solution }) in winners.iter().enumerate() { + let reference_score = winners + .get(i + 1) + .map(|winner| winner.score()) + .unwrap_or_default(); + let score = solution + .as_ref() + .map(|solution| solution.score.get()) .unwrap_or_default(); - let reward = solution - .score - .get() + let reward = score .checked_sub(reference_score) .expect("reference score unexpectedly larger than winner's score"); tracing::info!( driver =% driver.name, - score =% solution.score, + score =% score, %reward, "winner" ); @@ -158,7 +158,7 @@ impl RunLoop { } let hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes)); - for Participant { driver, solution } in participants { + for Participant { driver, solution } in &participants { match solution { Ok(solution) => { let uninternalized = (solution.calldata.internalized @@ -199,11 +199,48 @@ impl RunLoop { solve::Request::new(id, auction, &self.trusted_tokens.all(), self.solve_deadline); let request = &request; - futures::future::join_all(self.drivers.iter().map(|driver| async move { - let solution = self.participate(driver, request).await; - Participant { driver, solution } - })) - .await + let mut participants = + futures::future::join_all(self.drivers.iter().map(|driver| async move { + let solution = self.participate(driver, request).await; + Participant { driver, solution } + })) + .await; + + // Shuffle so that sorting randomly splits ties. + participants.shuffle(&mut rand::thread_rng()); + participants.sort_unstable_by_key(|participant| cmp::Reverse(participant.score())); + + participants + } + + /// Chooses the winners from the given participants. + /// + /// Participants are already sorted by their score (best to worst). + /// + /// Winners are selected one by one, starting from the best solution, + /// until `max_winners_per_auction` is hit. The solution can become winner + /// it is swaps tokens that are not yet swapped by any other already + /// selected winner. + fn select_winners<'a>(&self, participants: &'a [Participant<'a>]) -> Vec<&'a Participant<'a>> { + let mut winners = Vec::new(); + let mut already_swapped_tokens = HashSet::new(); + for participant in participants.iter() { + if let Ok(solution) = &participant.solution { + let swapped_tokens = solution + .orders() + .iter() + .map(|(_, order)| (order.sell.token, order.buy.token)) + .collect::>(); + if swapped_tokens.is_disjoint(&already_swapped_tokens) { + winners.push(participant); + already_swapped_tokens.extend(swapped_tokens); + if winners.len() >= self.max_winners_per_auction { + break; + } + } + } + } + winners } /// Computes a driver's solutions in the shadow competition. @@ -216,7 +253,7 @@ impl RunLoop { .await .map_err(|_| Error::Timeout)? .map_err(Error::Solve)?; - let (score, solution_id, submission_address) = proposed + let (score, solution_id, submission_address, orders) = proposed .solutions .into_iter() .max_by_key(|solution| solution.score) @@ -225,11 +262,16 @@ impl RunLoop { solution.score, solution.solution_id, solution.submission_address, + solution.orders, ) }) .ok_or(Error::NoSolutions)?; let score = NonZeroU256::new(score).ok_or(Error::ZeroScore)?; + let orders = orders + .into_iter() + .map(|(order_uid, amounts)| (order_uid.into(), amounts.into_domain())) + .collect(); let revealed = driver .reveal(&reveal::Request { solution_id }) @@ -247,6 +289,7 @@ impl RunLoop { score, account: submission_address, calldata: revealed.calldata, + orders, }) } } @@ -256,12 +299,6 @@ struct Participant<'a> { solution: Result, } -struct Solution { - score: NonZeroU256, - account: H160, - calldata: reveal::Calldata, -} - impl Participant<'_> { fn score(&self) -> U256 { self.solution @@ -271,6 +308,19 @@ impl Participant<'_> { } } +struct Solution { + score: NonZeroU256, + account: H160, + calldata: reveal::Calldata, + orders: HashMap, +} + +impl Solution { + fn orders(&self) -> &HashMap { + &self.orders + } +} + #[derive(Debug, thiserror::Error)] enum Error { #[error("the solver timed out")] From 7e7b9d95cc1e15c482b4817122e997c2ae4fdbac Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 30 Sep 2024 11:43:55 +0200 Subject: [PATCH 04/11] self cr --- crates/autopilot/src/run_loop.rs | 8 +------- crates/autopilot/src/shadow.rs | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index 51fa44e1b3..d9ea663dad 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -229,11 +229,6 @@ impl RunLoop { let auction = self.remove_in_flight_orders(auction).await; let solutions = self.competition(auction_id, &auction).await; - if solutions.is_empty() { - tracing::info!("no solutions for auction"); - return; - } - let winners = self.select_winners(&solutions); if winners.is_empty() { tracing::info!("no winners for auction"); @@ -523,7 +518,6 @@ impl RunLoop { // Make sure the winning solution is fair. while !Self::is_solution_fair(solutions.first(), &solutions, auction) { - // remove the solution.first() from the solutions let unfair_solution = solutions.remove(0); tracing::warn!( invalidated = unfair_solution.driver.name, @@ -541,7 +535,7 @@ impl RunLoop { /// /// Winners are selected one by one, starting from the best solution, /// until `max_winners_per_auction` is hit. The solution can become winner - /// it is swaps tokens that are not yet swapped by any other already + /// if it swaps tokens that are not yet swapped by any other already /// selected winner. fn select_winners(&self, participants: &[Participant]) -> Vec { let mut winners = Vec::new(); diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 8804739ae4..e88f734eeb 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -158,7 +158,7 @@ impl RunLoop { } let hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes)); - for Participant { driver, solution } in &participants { + for Participant { driver, solution } in participants { match solution { Ok(solution) => { let uninternalized = (solution.calldata.internalized @@ -219,7 +219,7 @@ impl RunLoop { /// /// Winners are selected one by one, starting from the best solution, /// until `max_winners_per_auction` is hit. The solution can become winner - /// it is swaps tokens that are not yet swapped by any other already + /// if it swaps tokens that are not yet swapped by any other already /// selected winner. fn select_winners<'a>(&self, participants: &'a [Participant<'a>]) -> Vec<&'a Participant<'a>> { let mut winners = Vec::new(); From 54b20a3560927e55ea7622e0298556eaeb304d68 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 30 Sep 2024 11:49:39 +0200 Subject: [PATCH 05/11] remove clone --- crates/autopilot/src/run_loop.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index d9ea663dad..8c17556a78 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -263,8 +263,8 @@ impl RunLoop { self.start_settlement_execution( auction_id, single_run_start, - &driver, - &solution, + driver, + solution, block_deadline, ) .await; @@ -537,7 +537,7 @@ impl RunLoop { /// until `max_winners_per_auction` is hit. The solution can become winner /// if it swaps tokens that are not yet swapped by any other already /// selected winner. - fn select_winners(&self, participants: &[Participant]) -> Vec { + fn select_winners<'a>(&self, participants: &'a [Participant]) -> Vec<&'a Participant> { let mut winners = Vec::new(); let mut already_swapped_tokens = HashSet::new(); for participant in participants.iter() { @@ -548,7 +548,7 @@ impl RunLoop { .map(|(_, order)| (order.sell.token, order.buy.token)) .collect::>(); if swapped_tokens.is_disjoint(&already_swapped_tokens) { - winners.push(participant.clone()); + winners.push(participant); already_swapped_tokens.extend(swapped_tokens); if winners.len() >= self.config.max_winners_per_auction { break; @@ -885,7 +885,6 @@ impl RunLoop { } } -#[derive(Clone)] struct Participant { driver: Arc, solution: competition::SolutionWithId, From 83f2b2b18e8ffe0040d804a860aaca7d085d3013 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 30 Sep 2024 11:52:57 +0200 Subject: [PATCH 06/11] remove clone 2 --- crates/autopilot/src/domain/competition/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/autopilot/src/domain/competition/mod.rs b/crates/autopilot/src/domain/competition/mod.rs index 1cf3bc803e..783f55bf2e 100644 --- a/crates/autopilot/src/domain/competition/mod.rs +++ b/crates/autopilot/src/domain/competition/mod.rs @@ -10,7 +10,7 @@ use { type SolutionId = u64; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct SolutionWithId { id: SolutionId, solution: Solution, @@ -55,7 +55,7 @@ impl SolutionWithId { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Solution { solver: eth::Address, score: Score, From 90a743ecfdf3821edb7c028803ad7b36e9dbbac4 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 30 Sep 2024 12:44:03 +0200 Subject: [PATCH 07/11] fix saving of solver competition table --- crates/autopilot/src/run_loop.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index 8c17556a78..839c89c041 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -375,8 +375,7 @@ impl RunLoop { let winner = winning_solution.solver().into(); let winning_score = winning_solution.score().get().0; let reference_score = solutions - .iter() - .nth_back(1) + .get(1) .map(|participant| participant.solution.score().get().0) .unwrap_or_default(); let participants = solutions @@ -420,6 +419,8 @@ impl RunLoop { }, solutions: solutions .iter() + // reverse as solver competition table is sorted from worst to best + .rev() .enumerate() .map(|(index, participant)| SolverSettlement { solver: participant.driver.name.clone(), From 561cec2a9949334d5a035f0738b8f6f5ac9d7dec Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 3 Oct 2024 11:22:28 +0200 Subject: [PATCH 08/11] fix reference score --- crates/autopilot/src/shadow.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 202e46816d..75af8b23a7 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -139,7 +139,19 @@ impl RunLoop { let reference_score = winners .get(i + 1) .map(|winner| winner.score()) - .unwrap_or_default(); + .unwrap_or_else(|| { + participants + .iter() + // assumes one solution per driver and unique driver names + .position(|participant| participant.driver.name == driver.name) + .and_then(|winner_position| { + participants + .get(winner_position + 1) + .map(|participant| participant.score()) + }) + .unwrap_or_default() + }); + let score = solution .as_ref() .map(|solution| solution.score.get()) From db4e8696cd91e46a45d7add18d998710218d368b Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 3 Oct 2024 11:34:11 +0200 Subject: [PATCH 09/11] fix comments --- crates/autopilot/src/run_loop.rs | 6 +++--- crates/autopilot/src/shadow.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index baa0007b21..0e7a090278 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -85,8 +85,8 @@ impl RunLoop { maintenance: Arc, ) -> Self { // Added to make sure no more than one winner is activated by accident - // Supposed to be removed after the implementation of "multiple winners per - // auction" is done + // Should be removed once we decide to activate "multiple winners per auction" + // feature. assert_eq!( config.max_winners_per_auction, 1, "only one winner is supported" @@ -542,7 +542,7 @@ impl RunLoop { /// Participants are already sorted by their score (best to worst). /// /// Winners are selected one by one, starting from the best solution, - /// until `max_winners_per_auction` is hit. The solution can become winner + /// until `max_winners_per_auction` are selected. The solution is a winner /// if it swaps tokens that are not yet swapped by any other already /// selected winner. fn select_winners<'a>(&self, participants: &'a VecDeque) -> Vec<&'a Participant> { diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 75af8b23a7..360b477bd7 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -59,8 +59,8 @@ impl RunLoop { max_winners_per_auction: usize, ) -> Self { // Added to make sure no more than one winner is activated by accident - // Supposed to be removed after the implementation of "multiple winners per - // auction" is done + // Should be removed once we decide to activate "multiple winners per auction" + // feature. assert_eq!(max_winners_per_auction, 1, "only one winner is supported"); Self { orderbook, @@ -229,7 +229,7 @@ impl RunLoop { /// Participants are already sorted by their score (best to worst). /// /// Winners are selected one by one, starting from the best solution, - /// until `max_winners_per_auction` is hit. The solution can become winner + /// until `max_winners_per_auction` are selected. The solution is a winner /// if it swaps tokens that are not yet swapped by any other already /// selected winner. fn select_winners<'a>(&self, participants: &'a [Participant<'a>]) -> Vec<&'a Participant<'a>> { From ca768bcc7cea1908d162fd24a873ba4e755b63b1 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 4 Oct 2024 09:33:21 +0200 Subject: [PATCH 10/11] flat map --- crates/autopilot/src/run_loop.rs | 2 +- crates/autopilot/src/shadow.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index b2e3b7ac4d..ac2ed3c197 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -559,7 +559,7 @@ impl RunLoop { .solution .orders() .iter() - .map(|(_, order)| (order.sell.token, order.buy.token)) + .flat_map(|(_, order)| vec![order.sell.token, order.buy.token]) .collect::>(); if swapped_tokens.is_disjoint(&already_swapped_tokens) { winners.push(participant); diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 360b477bd7..adf8ddbea8 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -240,7 +240,7 @@ impl RunLoop { let swapped_tokens = solution .orders() .iter() - .map(|(_, order)| (order.sell.token, order.buy.token)) + .flat_map(|(_, order)| vec![order.sell.token, order.buy.token]) .collect::>(); if swapped_tokens.is_disjoint(&already_swapped_tokens) { winners.push(participant); From a9bfa4c01c2c6ff0c70439d64e6d57047b678315 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 4 Oct 2024 09:41:45 +0200 Subject: [PATCH 11/11] small refactor --- crates/autopilot/src/shadow.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index adf8ddbea8..13007cf302 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -136,26 +136,24 @@ impl RunLoop { let winners = self.select_winners(&participants); for (i, Participant { driver, solution }) in winners.iter().enumerate() { + let score = solution + .as_ref() + .map(|solution| solution.score.get()) + .unwrap_or_default(); let reference_score = winners .get(i + 1) .map(|winner| winner.score()) .unwrap_or_else(|| { + // If this was the last winning solution pick the first worse overall + // solution that came from a different driver (or 0) as the reference score. participants .iter() - // assumes one solution per driver and unique driver names - .position(|participant| participant.driver.name == driver.name) - .and_then(|winner_position| { - participants - .get(winner_position + 1) - .map(|participant| participant.score()) - }) + .filter(|p| p.driver.name != driver.name) + .filter_map(|p| p.solution.as_ref().ok()) + .map(|p| p.score.get()) + .find(|other_score| *other_score <= score) .unwrap_or_default() }); - - let score = solution - .as_ref() - .map(|solution| solution.score.get()) - .unwrap_or_default(); let reward = score .checked_sub(reference_score) .expect("reference score unexpectedly larger than winner's score");