-
Notifications
You must be signed in to change notification settings - Fork 183
Cache fast-path quote solutions in the driver #4678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
13bdda9
a156e8a
ffda40e
46801e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,10 @@ type Balances = HashMap<BalanceGroup, order::SellAmount>; | |
| /// auction still find its settlement. | ||
| const MAX_CONCURRENT_AUCTIONS: usize = 5; | ||
|
|
||
| /// Upper bound on cached fast-path quote solutions, each fast-path quote pushes | ||
| /// exactly one entry, at a cadence unrelated to auctions. | ||
| const MAX_CACHED_QUOTE_SOLUTIONS: usize = 100; | ||
|
|
||
| /// An ongoing competition. There is one competition going on per solver at any | ||
| /// time. The competition stores settlements to solutions generated by the | ||
| /// driver, and allows them to be executed onchain when requested later. The | ||
|
|
@@ -269,13 +273,29 @@ pub struct Competition { | |
| pub mempools: Mempools, | ||
| /// Cached solutions with the most recent solutions at the front. | ||
| pub settlements: Mutex<VecDeque<Settlement>>, | ||
| /// Cached fast-path quote solutions, most recent at the front. Unlike | ||
| /// `/solve` (which caches ready-to-submit settlements), a quote runs | ||
| /// against a throwaway auction whose order is synthetic and unsigned, | ||
| /// so its solution cannot be encoded into a submittable settlement | ||
| /// until the real order exists at settle time. We therefore cache the | ||
| /// solution and its auction and (re-)encode later, on the fast-path | ||
| /// settle. | ||
| pub quote_solutions: Mutex<VecDeque<CachedQuoteSolution>>, | ||
| /// bad token and orders detector | ||
| pub risk_detector: Arc<risk_detector::Detector>, | ||
| fetcher: Arc<pre_processing::DataAggregator>, | ||
| order_sorting_strategies: Vec<Arc<dyn sorting::SortingStrategy>>, | ||
| submitter_pool: SubmitterPool, | ||
| } | ||
|
|
||
| /// A fast-path quote solution cached for later settlement, together with the | ||
| /// throwaway auction it was solved against. | ||
| #[derive(Debug)] | ||
| pub struct CachedQuoteSolution { | ||
| pub auction: Auction, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we storing the full auction here? Since we don't compute any new data we only need to have the solution be associated with the auction_id, no? |
||
| pub solution: Solution, | ||
| } | ||
|
|
||
| impl Competition { | ||
| #[expect(clippy::too_many_arguments)] | ||
| pub fn new( | ||
|
|
@@ -308,6 +328,7 @@ impl Competition { | |
| simulator, | ||
| mempools, | ||
| settlements: Default::default(), | ||
| quote_solutions: Default::default(), | ||
| risk_detector, | ||
| fetcher, | ||
| order_sorting_strategies, | ||
|
|
@@ -532,6 +553,15 @@ impl Competition { | |
| Ok(scored.into_iter().map(|(solved, _)| solved).collect()) | ||
| } | ||
|
|
||
| /// Caches a fast-path quote's solution, keyed by the auction id (allocated | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the solution is not really keyed by anything since we are not storing it in a map to uphold uniqueness. |
||
| /// by the orderbook from the shared sequence) and the solution id, so it | ||
| /// can be settled later via the fast-path settle path. | ||
| pub fn cache_quote_solution(&self, auction: Auction, solution: Solution) { | ||
| let mut lock = self.quote_solutions.lock().unwrap(); | ||
| lock.push_front(CachedQuoteSolution { auction, solution }); | ||
| lock.truncate(MAX_CACHED_QUOTE_SOLUTIONS); | ||
| } | ||
|
|
||
| /// Re-simulate all proposed solutions on every new block and drop any | ||
| /// that start reverting. Returns once every solution has reverted; | ||
| /// otherwise runs forever and the caller must impose a deadline. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| use { | ||
| super::competition::{auction, risk_detector, solution}, | ||
| super::competition::{Competition, auction, solution}, | ||
| crate::{ | ||
| boundary, | ||
| domain::{ | ||
|
|
@@ -35,11 +35,16 @@ pub struct Quote { | |
| pub tx_origin: Option<eth::Address>, | ||
| #[debug(ignore)] | ||
| pub jit_orders: Vec<solution::trade::Jit>, | ||
| /// For fast-path quotes: the id of the cached solution and the auction id | ||
| /// it was cached under, so the caller can later settle it via `/settle`. | ||
| /// `None` for regular quotes. | ||
| pub solution_id: Option<u64>, | ||
| pub auction_id: Option<i64>, | ||
|
Comment on lines
+38
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels like this should actually not be part of the quote but rather be in the key the quote gets stored in a collection with. |
||
| } | ||
|
|
||
| impl Quote { | ||
| fn try_new(eth: &Ethereum, solution: competition::Solution) -> Result<Self, Error> { | ||
| let clearing_prices = Self::compute_clearing_prices(&solution)?; | ||
| fn try_new(eth: &Ethereum, solution: &competition::Solution) -> Result<Self, Error> { | ||
| let clearing_prices = Self::compute_clearing_prices(solution)?; | ||
|
|
||
| Ok(Self { | ||
| clearing_prices, | ||
|
|
@@ -63,6 +68,8 @@ impl Quote { | |
| _ => None, | ||
| }) | ||
| .collect(), | ||
| solution_id: None, | ||
| auction_id: None, | ||
| }) | ||
| } | ||
|
|
||
|
|
@@ -119,6 +126,9 @@ pub struct Order { | |
| pub side: order::Side, | ||
| pub deadline: chrono::DateTime<chrono::Utc>, | ||
| pub enable_fast_path: bool, | ||
| /// Real auction id the orderbook allocated from the shared `auctions` | ||
| /// sequence for a fast-path quote. `None` for regular quotes. | ||
| pub auction_id: Option<i64>, | ||
| } | ||
|
|
||
| impl Order { | ||
|
|
@@ -132,11 +142,12 @@ impl Order { | |
| solver: &Solver, | ||
| liquidity: &infra::liquidity::Fetcher, | ||
| tokens: &infra::tokens::Fetcher, | ||
| risk_detector: &risk_detector::Detector, | ||
| competition: &Competition, | ||
| ) -> Result<Quote, Error> { | ||
| if self.enable_fast_path && !solver.fast_path_enabled() { | ||
| return Err(Error::QuotingFailed(QuotingFailed::FastPathNotSupported)); | ||
| } | ||
| let fast_path = self.enable_fast_path && solver.fast_path_enabled(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point we should stop looking at |
||
|
|
||
| let liquidity = match solver.liquidity() { | ||
| solver::Liquidity::Fetch => { | ||
|
|
@@ -147,40 +158,59 @@ impl Order { | |
| solver::Liquidity::Skip => Default::default(), | ||
| }; | ||
|
|
||
| // For fast-path quotes the orderbook allocates a real auction id from the | ||
| // shared `auctions` sequence and forwards it here, so the solution can be | ||
| // encoded into a settlement and cached for a later `/settle`. | ||
|
Comment on lines
+161
to
+163
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The driver should not make any assumptions on how the id gets generated. The only important invariant is that there will be a unique auction_id for each fast path quote. |
||
| let auction_id = fast_path | ||
| .then_some(self.auction_id) | ||
| .flatten() | ||
| .map(auction::Id); | ||
| let auction = self | ||
| .fake_auction(eth, tokens, solver.quote_using_limit_orders()) | ||
| .fake_auction(eth, tokens, solver.quote_using_limit_orders(), auction_id) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we have to pass |
||
| .await?; | ||
| let auction = risk_detector | ||
| let auction = competition | ||
| .risk_detector | ||
| .filter_unsupported_orders_in_auction(auction) | ||
| .await; | ||
| if auction.orders.is_empty() { | ||
| return Err(QuotingFailed::UnsupportedToken.into()); | ||
| } | ||
| let solutions = solver.solve(&auction, &liquidity).await?; | ||
| Quote::try_new( | ||
| eth, | ||
| // TODO(#1468): choose the best solution in the future, but for now just pick the | ||
| // first solution | ||
| solutions | ||
| .into_iter() | ||
| .find(|solution| !solution.is_empty(auction.surplus_capturing_jit_order_owners())) | ||
| .ok_or(QuotingFailed::NoSolutions)?, | ||
| ) | ||
| // TODO(#1468): choose the best solution in the future, but for now just pick | ||
| // the first solution. | ||
| let solution = solutions | ||
| .into_iter() | ||
| .find(|solution| !solution.is_empty(auction.surplus_capturing_jit_order_owners())) | ||
| .ok_or(QuotingFailed::NoSolutions)?; | ||
| let mut quote = Quote::try_new(eth, &solution)?; | ||
|
|
||
| // Cache the solution so the autopilot can settle it during the fast-path | ||
| // exclusivity window. The quote's order is synthetic (unsigned), so the | ||
| // settlement is not encoded here but deferred to the fast-path settle, | ||
| // once the real order exists. | ||
| if let Some(auction_id) = auction_id { | ||
| let solution_id = solution.id().get(); | ||
| competition.cache_quote_solution(auction, solution); | ||
| quote.solution_id = Some(solution_id); | ||
| quote.auction_id = Some(auction_id.0); | ||
|
Comment on lines
+194
to
+195
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems wrong to add this data to the quote itself. Unless I overlook something I'd go with |
||
| } | ||
| Ok(quote) | ||
| } | ||
|
|
||
| async fn fake_auction( | ||
| &self, | ||
| eth: &Ethereum, | ||
| tokens: &infra::tokens::Fetcher, | ||
| quote_using_limit_orders: bool, | ||
| auction_id: Option<auction::Id>, | ||
| ) -> Result<competition::Auction, Error> { | ||
| let tokens = tokens.get(&[self.buy().token, self.sell().token]).await; | ||
|
|
||
| let buy_token_metadata = tokens.get(&self.buy().token); | ||
| let sell_token_metadata = tokens.get(&self.sell().token); | ||
|
|
||
| competition::Auction::new( | ||
| None, | ||
| auction_id, | ||
| vec![competition::Order { | ||
| data: std::sync::Arc::new(competition::order::OrderData { | ||
| uid: Default::default(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we want to give solvers optimal information for risk/inventory management we should probably tell them an expiration for the quote. This can then be used for the cache eviction.
Not necessary in the very first step, I think.