From 9026308e79a110283ddb50832c084f989986dcc9 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 3 Sep 2026 10:10:22 +0000 Subject: [PATCH] Promote placeholder trade rows to real order_uid on placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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. --- .../src/database/onchain_order_events/mod.rs | 48 ++++++- crates/database/src/fast_path.rs | 41 ++++++ crates/database/src/lib.rs | 1 + crates/orderbook/src/database/orders.rs | 121 ++++++++++++++---- crates/orderbook/src/orderbook.rs | 12 +- 5 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 crates/database/src/fast_path.rs diff --git a/crates/autopilot/src/database/onchain_order_events/mod.rs b/crates/autopilot/src/database/onchain_order_events/mod.rs index 268bef5220..f00ca1cd2c 100644 --- a/crates/autopilot/src/database/onchain_order_events/mod.rs +++ b/crates/autopilot/src/database/onchain_order_events/mod.rs @@ -218,6 +218,7 @@ impl OnchainOrderParser { order_placement_events: Vec<(ContractEvent, Log)>, ) -> Result<( Vec, + Vec, Vec>, Vec<(database::events::EventIndex, OnchainOrderPlacement)>, Vec, @@ -265,7 +266,7 @@ impl OnchainOrderParser { .await; 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)| { ( self.custom_onchain_data_parser .customized_event_data_for_event_index( @@ -274,6 +275,7 @@ impl OnchainOrderParser { &custom_data_hashmap, &onchain_order_placement, ), + quote_id, quote, (event_index, onchain_order_placement), order, @@ -318,9 +320,9 @@ impl OnchainOrderParser { .collect(); let invalidation_events = get_invalidation_events(events)?; let invalided_order_uids = extract_invalidated_order_uids(invalidation_events)?; - let (custom_onchain_data, quotes, broadcasted_order_data, mut orders, tx_hashes) = self - .extract_custom_and_general_order_data(order_placement_events) - .await?; + let (custom_onchain_data, quote_ids, quotes, broadcasted_order_data, mut orders, tx_hashes) = + self.extract_custom_and_general_order_data(order_placement_events) + .await?; database::onchain_invalidations::insert_onchain_invalidations( transaction, @@ -364,6 +366,29 @@ impl OnchainOrderParser { .await .context("insert_orders failed")?; + // Promote fast-path quotes for onchain orders (mirrors the trait-based + // path in the orderbook). For each successfully-quoted order tied to a + // fast-path `auction_id`, drop the transient `quotes` row and rewrite + // the placeholder `proposed_trade_executions.order_uid` to the real + // one. + 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")?; + } + for order in &invalided_order_uids { tracing::debug!(?order, "invalidated order"); } @@ -442,6 +467,7 @@ fn extract_invalidated_order_uids( type GeneralOnchainOrderPlacementData = ( EventIndex, + i64, Option, OnchainOrderPlacement, Order, @@ -513,7 +539,14 @@ where None } }; - Ok((event_index, quote, order_data.0, order_data.1, tx_hash)) + Ok(( + event_index, + quote_id, + quote, + order_data.0, + order_data.1, + tx_hash, + )) }, ); let onchain_order_placement_data: Vec> = @@ -1319,9 +1352,10 @@ mod test { metadata: quote.data.metadata.clone().try_into().unwrap(), auction_id: quote.data.auction_id, }; - assert_eq!(result.1, vec![Some(expected_quote)]); + assert_eq!(result.1, vec![0i64]); + assert_eq!(result.2, vec![Some(expected_quote)]); assert_eq!( - result.2, + result.3, vec![( expected_event_index, OnchainOrderPlacement { diff --git a/crates/database/src/fast_path.rs b/crates/database/src/fast_path.rs new file mode 100644 index 0000000000..1029c5726d --- /dev/null +++ b/crates/database/src/fast_path.rs @@ -0,0 +1,41 @@ +//! Database queries for the fast-path settlement feature. +//! +//! Fast-path orders reuse a quote's synthetic solver competition as the +//! actual settlement. This module owns the promotion step that patches +//! the placeholder rows written at quote time to reference the real +//! `order_uid` ([`finalize_quote_competition`]). + +use { + crate::{OrderUid, PgTransaction, auction::AuctionId}, + std::ops::DerefMut, + tracing::instrument, +}; + +/// Because the final order uid is not known when we store the quote +/// competition data we use `0x000...000` as a sentinel value. +/// When an order gets placed referencing a quote competition this function +/// replaces the placeholder value with the now final order uid. +#[instrument(skip_all)] +pub async fn finalize_quote_competition( + ex: &mut PgTransaction<'_>, + auction_id: AuctionId, + order_uid: OrderUid, +) -> Result<(), sqlx::Error> { + const QUERY: &str = r#" +WITH patch_te AS ( + UPDATE proposed_trade_executions + SET order_uid = $1 + WHERE auction_id = $2 AND order_uid = $3 +) +UPDATE competition_auctions +SET order_uids = ARRAY[$1] +WHERE id = $2 +"#; + sqlx::query(QUERY) + .bind(order_uid) + .bind(auction_id) + .bind(OrderUid::default()) + .execute(ex.deref_mut()) + .await?; + Ok(()) +} diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index 585de0278c..1d39f14b77 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -3,6 +3,7 @@ pub mod auction; pub mod byte_array; pub mod ethflow_orders; pub mod events; +pub mod fast_path; pub mod fee_policies; pub mod jit_orders; pub mod last_indexed_blocks; diff --git a/crates/orderbook/src/database/orders.rs b/crates/orderbook/src/database/orders.rs index 5bf1ce2e6d..ac06d31a84 100644 --- a/crates/orderbook/src/database/orders.rs +++ b/crates/orderbook/src/database/orders.rs @@ -5,9 +5,9 @@ use { anyhow::{Context as _, Result}, app_data::AppDataHash, async_trait::async_trait, - bigdecimal::ToPrimitive, chrono::{DateTime, Utc}, database::{ + PgTransaction, byte_array::ByteArray, order_events::{OrderEvent, OrderEventLabel, insert_order_event}, orders::{self, FullOrder, OrderKind as DbOrderKind}, @@ -25,6 +25,7 @@ use { OrderStatus, OrderUid, }, + quote::QuoteId, signature::Signature, time::now_in_epoch_seconds, }, @@ -56,13 +57,22 @@ use { #[cfg_attr(test, mockall::automock)] #[async_trait::async_trait] pub trait OrderStoring: Send + Sync { - async fn insert_order(&self, order: &Order) -> Result<(), InsertionError>; + /// When `quote_id` is `Some`, the transient row is dropped from `quotes` + /// and its `auction_id` (if any) drives the fast-path competition patch. + /// The `order_quotes` row inherits that `auction_id` so there's a single + /// source of truth for the (auction, order) mapping. + async fn insert_order( + &self, + order: &Order, + quote_id: Option, + ) -> Result<(), InsertionError>; async fn cancel_orders(&self, order_uids: Vec, now: DateTime) -> Result<()>; async fn cancel_order(&self, order_uid: &OrderUid, now: DateTime) -> Result<()>; async fn replace_order( &self, old_order: &OrderUid, new_order: &Order, + quote_id: Option, ) -> Result<(), InsertionError>; async fn orders_for_tx(&self, tx_hash: &B256) -> Result>; /// All orders of a single user ordered by creation date descending (newest @@ -114,7 +124,11 @@ async fn cancel_order( Ok(()) } -async fn insert_order(order: &Order, ex: &mut PgConnection) -> Result<(), InsertionError> { +async fn insert_order( + order: &Order, + quote_id: Option, + ex: &mut PgTransaction<'_>, +) -> Result<(), InsertionError> { let order_uid = ByteArray(order.metadata.uid.0); insert_order_event( ex, @@ -192,23 +206,42 @@ async fn insert_order(order: &Order, ex: &mut PgConnection) -> Result<(), Insert .await .map_err(InsertionError::DbError)?; - if let Some(quote) = order.metadata.quote.as_ref() { + // 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, }; database::orders::insert_quote(ex, &db_quote) .await .map_err(InsertionError::DbError)?; + + if let Some(auction_id) = quote.auction_id { + // 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 + // `proposed_trade_executions` rows. + database::fast_path::finalize_quote_competition(ex, auction_id, order_uid) + .await + .map_err(InsertionError::DbError)?; + } } Ok(()) @@ -216,7 +249,11 @@ async fn insert_order(order: &Order, ex: &mut PgConnection) -> Result<(), Insert #[async_trait::async_trait] impl OrderStoring for Postgres { - async fn insert_order(&self, order: &Order) -> Result<(), InsertionError> { + async fn insert_order( + &self, + order: &Order, + quote_id: Option, + ) -> Result<(), InsertionError> { let _timer = super::Metrics::get() .database_queries .with_label_values(&["insert_order"]) @@ -225,7 +262,7 @@ impl OrderStoring for Postgres { let mut connection = self.pool.acquire().await?; let mut ex = connection.begin().await?; - insert_order(order, &mut ex).await?; + insert_order(order, quote_id, &mut ex).await?; Self::insert_order_app_data(order, &mut ex).await?; ex.commit().await?; @@ -263,6 +300,7 @@ impl OrderStoring for Postgres { &self, old_order: &model::order::OrderUid, new_order: &model::order::Order, + quote_id: Option, ) -> anyhow::Result<(), super::orders::InsertionError> { let _timer = super::Metrics::get() .database_queries @@ -281,7 +319,7 @@ impl OrderStoring for Postgres { new_order.metadata.creation_date, ) .await?; - insert_order(&new_order, ex).await?; + insert_order(&new_order, quote_id, ex).await?; Self::insert_order_app_data(&new_order, ex).await?; Ok(()) @@ -935,7 +973,7 @@ mod tests { }, ..Default::default() }; - db.insert_order(&old_order).await.unwrap(); + db.insert_order(&old_order, None).await.unwrap(); let new_order = Order { data: OrderData { @@ -950,7 +988,7 @@ mod tests { }, ..Default::default() }; - db.replace_order(&old_order.metadata.uid, &new_order) + db.replace_order(&old_order.metadata.uid, &new_order, None) .await .unwrap(); @@ -997,7 +1035,7 @@ mod tests { }, ..Default::default() }; - db.insert_order(&old_order).await.unwrap(); + db.insert_order(&old_order, None).await.unwrap(); let new_order = Order { metadata: OrderMetadata { @@ -1008,12 +1046,12 @@ mod tests { }, ..Default::default() }; - db.insert_order(&new_order).await.unwrap(); + db.insert_order(&new_order, None).await.unwrap(); // Attempt to replace an old order with one that already exists should // fail. let err = db - .replace_order(&old_order.metadata.uid, &new_order) + .replace_order(&old_order.metadata.uid, &new_order, None) .await .unwrap_err(); assert!(matches!(err, InsertionError::DuplicatedRecord)); @@ -1046,7 +1084,7 @@ mod tests { signature: Signature::default_with(SigningScheme::PreSign), ..Default::default() }; - db.insert_order(&order).await.unwrap(); + db.insert_order(&order, None).await.unwrap(); let order_status = || async { db.single_order(&order.metadata.uid) @@ -1133,9 +1171,9 @@ mod tests { } }; - db.insert_order(&order(1)).await.unwrap(); - db.insert_order(&order(2)).await.unwrap(); - db.insert_order(&order(3)).await.unwrap(); + db.insert_order(&order(1), None).await.unwrap(); + db.insert_order(&order(2), None).await.unwrap(); + db.insert_order(&order(3), None).await.unwrap(); assert_eq!(order_status(1).await, OrderStatus::Open); assert_eq!(order_status(2).await, OrderStatus::Open); @@ -1178,6 +1216,8 @@ mod tests { }, }; + let quote_id = save_quote_for_test(&db, "e).await; + let uid = OrderUid([0x42; 56]); let order = Order { data: OrderData { @@ -1197,7 +1237,7 @@ mod tests { ..Default::default() }; - db.insert_order(&order).await.unwrap(); + db.insert_order(&order, Some(quote_id)).await.unwrap(); let single_order = db.single_order(&uid).await.unwrap().unwrap(); assert_eq!( @@ -1245,6 +1285,8 @@ mod tests { ..Default::default() }; + let quote_id = save_quote_for_test(&db, "e).await; + let uid = OrderUid([0x42; 56]); let order = Order { data: OrderData { @@ -1260,7 +1302,7 @@ mod tests { ..Default::default() }; - db.insert_order(&order).await.unwrap(); + db.insert_order(&order, Some(quote_id)).await.unwrap(); let single_order = db.single_order(&uid).await.unwrap().unwrap(); @@ -1270,4 +1312,29 @@ mod tests { ); assert_eq!(single_order, order); } + + /// Persists a transient `quotes` row for the given quote and returns its + /// id — mirrors what the quoting flow does before `insert_order` promotes + /// the row into `order_quotes`. + async fn save_quote_for_test(db: &Postgres, quote: &Quote) -> model::quote::QuoteId { + let db_quote = database::quotes::Quote { + id: 0, + sell_token: Default::default(), + buy_token: Default::default(), + sell_amount: u256_to_big_decimal("e.sell_amount), + buy_amount: u256_to_big_decimal("e.buy_amount), + gas_amount: quote.data.fee_parameters.gas_amount, + gas_price: quote.data.fee_parameters.gas_price, + sell_token_price: quote.data.fee_parameters.sell_token_price, + order_kind: DbOrderKind::Sell, + expiration_timestamp: Utc::now(), + quote_kind: Default::default(), + solver: ByteArray(quote.data.solver.0.0), + verified: quote.data.verified, + metadata: quote.data.metadata.clone().try_into().unwrap(), + auction_id: None, + }; + let mut conn = db.pool.acquire().await.unwrap(); + database::quotes::save(&mut conn, &db_quote).await.unwrap() + } } diff --git a/crates/orderbook/src/orderbook.rs b/crates/orderbook/src/orderbook.rs index 21ebc309e9..1f2d1b2555 100644 --- a/crates/orderbook/src/orderbook.rs +++ b/crates/orderbook/src/orderbook.rs @@ -297,13 +297,14 @@ impl Orderbook { .await?; let order_uid = order.metadata.uid; + let quote_id = quote.as_ref().and_then(|q| q.id); // Check if it has to replace an existing order if let Some(old_order) = replaced_order { - self.replace_order(order, old_order).await? + self.replace_order(order, old_order, quote_id).await? } else { self.database - .insert_order(&order) + .insert_order(&order, quote_id) .await .map_err(|err| AddOrderError::from_insertion(err, &order))?; Metrics::on_order_operation(&order, OrderOperation::Created); @@ -443,6 +444,7 @@ impl Orderbook { &self, validated_new_order: Order, old_order: Order, + quote_id: Option, ) -> Result<(), AddOrderError> { validated_new_order .signature @@ -472,7 +474,7 @@ impl Orderbook { } self.database - .replace_order(&old_order.metadata.uid, &validated_new_order) + .replace_order(&old_order.metadata.uid, &validated_new_order, quote_id) .await .map_err(|err| AddOrderError::from_insertion(err, &validated_new_order))?; Metrics::on_order_operation(&old_order, OrderOperation::Cancelled); @@ -794,7 +796,7 @@ mod tests { let old_order = old_order.clone(); move |_| Ok(Some(old_order.clone())) }); - database.expect_replace_order().returning(|_, _| Ok(())); + database.expect_replace_order().returning(|_, _, _| Ok(())); let mut order_validator = MockOrderValidating::new(); order_validator @@ -818,7 +820,7 @@ mod tests { let database = crate::database::Postgres::try_new("postgresql://", Default::default()).unwrap(); database::clear_DANGER(&database.pool).await.unwrap(); - database.insert_order(&old_order).await.unwrap(); + database.insert_order(&old_order, None).await.unwrap(); let database_replica = database.clone(); let app_data = Arc::new(crate::app_data::Registry::new(