Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion crates/driver/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,20 @@ paths:
$ref: "#/components/schemas/DateTime"
required: true
- in: query
name: fastPath
name: enableFastPath
description: Hint that this quote is intended for fast-path execution.
schema:
type: boolean
required: false
- in: query
name: auctionId
description: >-
Real auction id the orderbook allocated from the shared auctions
sequence, under which a fast-path solution is cached for a later
`/settle`. Only sent for fast-path quotes.
schema:
type: integer
required: false
responses:
"200":
description: Quote successfully created.
Expand Down Expand Up @@ -439,6 +448,17 @@ components:
supportsFastPath:
type: boolean
description: Whether the quoting solver supports fast-path (out-of-competition) execution.
solutionId:
type: integer
description: >-
For fast-path quotes: id of the cached solution, to later submit it
via `/settle`. Present only when the solution was successfully cached.
auctionId:
type: integer
description: >-
For fast-path quotes: the auction id the solution was cached under,
to later submit it via `/settle`. Present only when the solution was
successfully cached.
required:
- clearingPrices
- solver
Expand Down
30 changes: 30 additions & 0 deletions crates/driver/src/domain/competition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +67 to +69

Copy link
Copy Markdown
Contributor

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.


/// 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
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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(
Expand Down Expand Up @@ -308,6 +328,7 @@ impl Competition {
simulator,
mempools,
settlements: Default::default(),
quote_solutions: Default::default(),
risk_detector,
fetcher,
order_sorting_strategies,
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
That being said using a map makes more sense here since those solutions will on average not be used in order.

/// 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.
Expand Down
62 changes: 46 additions & 16 deletions crates/driver/src/domain/quote.rs
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::{
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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,
Expand All @@ -63,6 +68,8 @@ impl Quote {
_ => None,
})
.collect(),
solution_id: None,
auction_id: None,
})
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we should stop looking at solver.fast_path_enabled(). We already checked above that fast path support is enabled.


let liquidity = match solver.liquidity() {
solver::Liquidity::Fetch => {
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have to pass auction_id if self already contains it?

.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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 HashMap<(auction_id, solution_id), Quote> or something like that.

}
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(),
Expand Down
6 changes: 6 additions & 0 deletions crates/driver/src/infra/api/routes/quote/dto/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ impl Order {
},
deadline: self.deadline,
enable_fast_path: self.enable_fast_path,
auction_id: self.auction_id,
}
}
}
Expand All @@ -32,6 +33,11 @@ pub struct Order {
deadline: chrono::DateTime<chrono::Utc>,
#[serde(default)]
enable_fast_path: bool,
/// Real auction id the orderbook allocated from the shared `auctions`
/// sequence, under which a fast-path solution is cached for a later
/// `/settle`. Only present for fast-path quotes.
#[serde(default)]
auction_id: Option<i64>,
}

#[derive(Debug, Deserialize)]
Expand Down
8 changes: 8 additions & 0 deletions crates/driver/src/infra/api/routes/quote/dto/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ impl Quote {
tx_origin: quote.tx_origin,
jit_orders: quote.jit_orders.into_iter().map(Into::into).collect(),
supports_fast_path,
solution_id: quote.solution_id,
auction_id: quote.auction_id,
}
}
}
Expand All @@ -45,6 +47,12 @@ pub struct Quote {
jit_orders: Vec<JitOrder>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
supports_fast_path: bool,
/// For fast-path quotes: the cached solution id and the auction id it was
/// cached under, to later submit it via `/settle`.
#[serde(skip_serializing_if = "Option::is_none")]
solution_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
auction_id: Option<i64>,
}

#[serde_as]
Expand Down
2 changes: 1 addition & 1 deletion crates/driver/src/infra/api/routes/quote/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async fn route(
state.solver(),
state.liquidity(),
state.tokens(),
&state.competition().risk_detector,
state.competition(),
)
.await;
observe::quoted(state.solver().name(), &order, &quote);
Expand Down
45 changes: 45 additions & 0 deletions crates/driver/src/tests/cases/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,51 @@ async fn with_jit_order() {
quote.ok().amount().interactions().jit_order();
}

/// A fast-path quote caches its solution in the driver, echoing the solution id
/// and the orderbook-provided auction id so the autopilot can later settle it.
#[tokio::test]
#[ignore]
async fn fast_path_caching() {
const AUCTION_ID: i64 = 42;

let test = tests::setup()
.pool(ab_pool())
.order(ab_order())
.solution(ab_solution())
.solvers(vec![tests::setup::test_solver().fast_path_enabled()])
.auction_id(AUCTION_ID)
.quote()
.quote_fast_path()
.done()
.await;

let quote = test.quote().await.ok();

// Both ids are echoed only when the solution was cached: `auction_id` must be
// the id the request carried (the one the orderbook allocated from the shared
// sequence), and `solution_id` is present (its value is process-global, so
// not asserted). The accessors panic if the fields are absent.
quote.solution_id();
assert_eq!(quote.auction_id(), AUCTION_ID);
}

/// A regular quote (no `enableFastPath`), even from a fast-path-capable solver,
/// does not cache a settlement and carries no settle info.
#[tokio::test]
#[ignore]
async fn regular_quote_has_no_settle_info() {
let test = tests::setup()
.pool(ab_pool())
.order(ab_order())
.solution(ab_solution())
.solvers(vec![tests::setup::test_solver().fast_path_enabled()])
.quote()
.done()
.await;

test.quote().await.ok().no_fast_path_settle_info();
}

/// Test that quote haircut correctly reduces the executed amount for quotes
/// when configured. The haircut should make quotes more conservative without
/// affecting the ability to place and execute orders.
Expand Down
13 changes: 11 additions & 2 deletions crates/driver/src/tests/setup/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ pub fn quote_req(test: &Test) -> serde_json::Value {
}

let quote = test.quoted_orders.first().unwrap();
json!({
let mut req = json!({
"sellToken": test.blockchain.get_token(quote.order.sell_token).encode_hex_with_prefix(),
"buyToken": test.blockchain.get_token(quote.order.buy_token).encode_hex_with_prefix(),
"amount": match quote.order.side {
Expand All @@ -195,7 +195,14 @@ pub fn quote_req(test: &Test) -> serde_json::Value {
order::Side::Buy => "buy",
},
"deadline": test.deadline,
})
});
if test.quote_fast_path {
// Mirrors what the orderbook does for fast-path quotes: request fast-path
// and hand over the real auction id it allocated from the shared sequence.
req["enableFastPath"] = json!(true);
req["auctionId"] = json!(test.auction_id);
}
req
}

/// Create the config file for the driver to use.
Expand Down Expand Up @@ -322,6 +329,7 @@ async fn create_config_file(
merge-solutions = {}
haircut-bps = {}
max-solutions-to-propose = {}
fast-path-enabled = {}
"#,
solver.name,
addr,
Expand All @@ -338,6 +346,7 @@ async fn create_config_file(
solver.merge_solutions,
solver.haircut_bps,
solver.max_solutions_to_propose,
solver.fast_path_enabled,
)
.unwrap();
if !solver.submission_accounts.is_empty() {
Expand Down
Loading
Loading