(fast4) inject final order_uid in proposed_trade_executions - #4859
(fast4) inject final order_uid in proposed_trade_executions#4859MartinquaXD wants to merge 1 commit into
Conversation
|
Claude encountered an error after 4m 57s —— View job PR Review: inject final
|
| // delete the transient `quotes` row so every order is tied to exactly one | ||
| // quote — the data is then moved directly into permanent `order_quotes` | ||
| // table. | ||
| let quote = match quote_id { | ||
| Some(id) => database::quotes::delete_and_return_row(ex, id) | ||
| .await | ||
| .map_err(InsertionError::DbError)?, | ||
| None => None, | ||
| }; | ||
|
|
||
| if let Some(quote) = quote { | ||
| let db_quote = database::orders::Quote { | ||
| order_uid, | ||
| // safe to unwrap as these values were converted from f64 previously | ||
| gas_amount: quote.gas_amount.to_f64().unwrap(), | ||
| gas_price: quote.gas_price.to_f64().unwrap(), | ||
| sell_token_price: quote.sell_token_price.to_f64().unwrap(), | ||
| sell_amount: u256_to_big_decimal("e.sell_amount), | ||
| buy_amount: u256_to_big_decimal("e.buy_amount), | ||
| solver: ByteArray(quote.solver.0.0), | ||
| gas_amount: quote.gas_amount, | ||
| gas_price: quote.gas_price, | ||
| sell_token_price: quote.sell_token_price, | ||
| sell_amount: quote.sell_amount, | ||
| buy_amount: quote.buy_amount, | ||
| solver: quote.solver, | ||
| verified: quote.verified, | ||
| metadata: quote.metadata.clone(), | ||
| auction_id: None, | ||
| metadata: quote.metadata, | ||
| auction_id: quote.auction_id, | ||
| }; |
There was a problem hiding this comment.
Behavior change: the order_quotes record can now be silently dropped.
Previously the order_quotes row was always inserted from the in-memory order.metadata.quote whenever a quote existed. Now it is sourced from delete_and_return_row(quote_id), so if the transient quotes row is gone by the time insert_order runs, delete_and_return_row returns None and no order_quotes row is written at all for the order.
That row can legitimately be missing before this call:
- Two orders that resolve to the same quote (e.g.
find_quotematches by parameters, not just id — identical orders placed concurrently both find quote rowX). The firstinsert_orderdeletesXand commits; the second getsNone→ loses its quote record. remove_expired_quotesracing an order that references a just-expired quote.
The old in-memory path was immune to both. Losing order_quotes affects downstream fee-policy / reward attribution for that order.
Since model::order::OrderQuote doesn't carry auction_id, the cleanest fix is probably to keep inserting order_quotes from order.metadata.quote as before, and use delete_and_return_row only to (a) source auction_id and (b) drive the fast-path competition patch + cleanup — so a missing transient row degrades to "no fast-path patch" rather than "no quote record". At minimum, worth confirming this drop is intentional.
| // the quote is associated with a auction competition indicating | ||
| // that this is going to be used for a fast path execution. | ||
| // not that we know the final order uid we can patch up the |
There was a problem hiding this comment.
Comment typos:
| // the quote is associated with a auction competition indicating | |
| // that this is going to be used for a fast path execution. | |
| // not that we know the final order uid we can patch up the | |
| // the quote is associated with an auction competition indicating | |
| // that this is going to be used for a fast path execution. | |
| // now that we know the final order uid we can patch up the |
| for (quote_id, quote, order) in izip!("e_ids, "es, &orders) { | ||
| let Some(quote) = quote else { | ||
| continue; | ||
| }; | ||
| let Some(auction_id) = quote.auction_id else { | ||
| continue; | ||
| }; | ||
| // The order_quotes row already carries the auction_id (populated | ||
| // inline above by `insert_quotes`), so all that's left is to drop | ||
| // the transient `quotes` row and patch competition tables. | ||
| database::quotes::delete_and_return_row(transaction, *quote_id) | ||
| .await | ||
| .context("failed to delete promoted onchain quote")?; | ||
| database::fast_path::finalize_quote_competition(transaction, auction_id, order.uid) | ||
| .await | ||
| .context("failed to patch competition rows for onchain order")?; | ||
| } |
There was a problem hiding this comment.
This loop patches competition_auctions.order_uids for every order that has a Some quote with an auction_id, regardless of whether the onchain placement actually succeeded. convert_onchain_order_placement returns quote: Ok(..) even for orders it flags as outside_market_price (it only increments a metric), so a failed/invalid fast-path placement would still overwrite the competition row with ARRAY[order_uid]. If ethflow + fast-path can co-occur, consider gating this on placement success (as the insert_quotes comment above at L346-348 implies quotes are only meaningful for orders that make it into an auction).
|
|
Adds a `database::fast_path` module with `finalize_quote_competition`, which patches the placeholder `order_uid = 0x00…00` rows written at quote time to the real uid once the order is placed. Threads `quote_id: Option<QuoteId>` through `OrderStoring::insert_order` and `replace_order`. When set, the orderbook drops the transient `quotes` row, promotes it into `order_quotes` (carrying `auction_id` forward), and calls `finalize_quote_competition`. The autopilot's onchain-order parser mirrors the same promotion path so ethflow orders backed by a fast-path quote finalise their competition too.
a117f40 to
9026308
Compare
|
|
||
| let data_tuple = onchain_order_data.into_iter().map( | ||
| |(event_index, quote, onchain_order_placement, order, tx_hash)| { | ||
| |(event_index, quote_id, quote, onchain_order_placement, order, tx_hash)| { |
There was a problem hiding this comment.
with this amount of parameters, I think it's time we make this a function
| Vec<W>, | ||
| Vec<i64>, | ||
| Vec<Option<database::orders::Quote>>, | ||
| Vec<(database::events::EventIndex, OnchainOrderPlacement)>, | ||
| Vec<Order>, | ||
| Vec<TxHash>, |
There was a problem hiding this comment.
IMO we're at a point this should be a struct
fleupold
left a comment
There was a problem hiding this comment.
Looks good, just unsure about what this new "a quote cannot be reused" logic might break.
| order_placement_events: Vec<(ContractEvent, Log)>, | ||
| ) -> Result<( | ||
| Vec<W>, | ||
| Vec<i64>, |
There was a problem hiding this comment.
This return type looks pretty complicated. Can we use a QuoteId alias instead of i64 here or refactor it to return a named struct?
| // delete the transient `quotes` row so every order is tied to exactly one | ||
| // quote — the data is then moved directly into permanent `order_quotes` | ||
| // table. | ||
| let quote = match quote_id { |
There was a problem hiding this comment.
Can you add an e2e test what happens now when someone tries to use the same quote id twice? I feel like there are cases where we might end up writing an order to the DB without quote information (which may break a bunch of assumption down the line especially around the way fees are charged). I think this can happen if an quote is "found" via search (e.g. an order that doesn't use a quote id) and later or around the same time the order which generated the original quote gets placed specifying the quote id.
I feel like delete_and_return_row should not use fetch_optional but hard fail if the specified quote_id wasn't found on disk.
This way we reject orders that use a quote id which was already used.
There was a problem hiding this comment.
I think the other race could be if the quote stream is still open, it may override a finalized auction if a new quote arrives after the order has been placed.
| .await | ||
| .context("insert_orders failed")?; | ||
|
|
||
| // Promote fast-path quotes for onchain orders (mirrors the trait-based |
There was a problem hiding this comment.
Aren't we missing applying the validFrom here as well? Probably a sign that the min_fast_path_exclusivity should live only in the autopilot and apply to orders that don't explicitly set a validFrom. Maybe we can "gate" the feature differently in the API?
Description
So far we only have dummy order_uids in the
proposed_trade_executionstable. This PR checks if an order was placed against a fast path quote and then updates the respectiveproposed_trade_executionsrows with the now knownorder_uid.Changes
One important implementation detail is that fast path quotes should be associated with order 1:1 while the current logic theoretically allows placing multiple orders against the same reference quote.
While this was initially introduced as an optimization IIRC it does not make a ton of sense anymore so this PR deletes a
quotesrow when it gets promoted to a persistentorder_quotesrow.How to test
updated existing unit tests