Skip to content
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

[ON HOLD] Reject solution containing outside-of-auction order #2712

Closed
wants to merge 4 commits into from
Closed
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
4 changes: 4 additions & 0 deletions crates/autopilot/src/domain/settlement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ impl Settlement {
/// id.
const META_DATA_LEN: usize = 8;

pub fn order_uids(&self) -> impl Iterator<Item = &order::OrderUid> {
self.trades.iter().map(|trade| trade.order_uid())
}

pub fn native_surplus(&self, prices: &auction::Prices) -> Result<eth::Ether, trade::Error> {
self.trades
.iter()
Expand Down
4 changes: 4 additions & 0 deletions crates/autopilot/src/domain/settlement/trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ impl Trade {
}
}

pub fn order_uid(&self) -> &domain::OrderUid {
&self.order_uid
}

/// CIP38 score defined as surplus + protocol fee
///
/// Denominated in NATIVE token
Expand Down
29 changes: 29 additions & 0 deletions crates/autopilot/src/infra/persistence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use {
},
anyhow::Context,
chrono::Utc,
database::byte_array::ByteArray,
number::conversions::big_decimal_to_u256,
primitive_types::H256,
std::{collections::HashMap, sync::Arc},
Expand Down Expand Up @@ -167,6 +168,34 @@ impl Persistence {

Ok(prices)
}

/// Returns true if none of the given orders exist in the database.
pub async fn orders_do_not_exist(
&self,
order_uids: &[domain::OrderUid],
) -> Result<bool, Error> {
let mut ex = self
.postgres
.pool
.begin()
Copy link
Contributor

Choose a reason for hiding this comment

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

There is only a single DB call so we don't need a transaction here.

Suggested change
.begin()
.acquire()

.await
.context("begin")
.map_err(Error::DbError)?;

let order_exist = database::orders::orders_do_not_exist(
&mut ex,
order_uids
.iter()
.map(|uid| ByteArray(uid.0))
.collect::<Vec<_>>()
.as_slice(),
)
.await
.context("fetch")
.map_err(Error::DbError)?;

Ok(order_exist)
}
}

#[derive(Debug, thiserror::Error)]
Expand Down
45 changes: 44 additions & 1 deletion crates/autopilot/src/run_loop.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
crate::{
database::competition::Competition,
domain::{self, auction::order::Class, OrderUid},
domain::{self, auction::order::Class, settlement::Settlement, OrderUid},
infra::{
self,
solvers::dto::{reveal, settle, solve},
Expand Down Expand Up @@ -141,6 +141,23 @@ impl RunLoop {
}
};

// validate solution
let settlement = match Settlement::new(
&revealed.calldata.internalized.clone().into(),
self.eth.contracts().settlement_domain_separator(),
) {
Ok(settlement) => settlement,
Err(err) => {
tracing::error!(driver = %driver.name, ?err, "unexpected calldata format");
return;
}
};

if !self.validate_settlement(&auction, &settlement).await {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think before we enforce any checks like this we should ensure that we await the correct settlement transaction. It's possible that a malicious losing solver would settle a solution with the expected auction_id out of competition.
Currently we just await the next tx that has the expected auction_id attached to its calldata. What we should probably do instead is to make it so that we only await a solution with the expected auction_id that also came from the winning solver.
That way this verification always applies to "real" solution and a malicious solver can't disrupt the main loop.
This disrupting solver would then easily be removed from the next competitions by the circuit breaker. Unfortunately this likely requires a change in the DB because we have a 1:1 mapping between tx_hash and auction_id at the moment whereas it should probably be a 1:1 mapping between tx_hash and (auction_id, solver).

tracing::error!(driver = %driver.name, "invalid settlement");
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we still store all the relevant data for this competition when the solution fails some checks?

}

let order_uids = solution.order_ids().copied().collect();
self.persistence
.store_order_events(order_uids, OrderEventLabel::Considered);
Expand Down Expand Up @@ -348,6 +365,32 @@ impl RunLoop {
.collect()
}

/// Settlement is allowed to contain orders that are part of the Auction or
/// JIT orders.
async fn validate_settlement(
&self,
auction: &domain::Auction,
settlement: &Settlement,
) -> bool {
let auction_orders = auction.orders.iter().map(|o| o.uid).collect::<HashSet<_>>();

// all JIT orders will be non_auction_orders
let non_auction_orders = settlement
.order_uids()
.filter(|order| !auction_orders.contains(order))
.cloned()
.collect::<Vec<_>>();

// make sure all non-auction orders are also non-database
self.persistence
.orders_do_not_exist(&non_auction_orders)
.await
.unwrap_or_else(|err| {
tracing::warn!(?err, "failed to check if all orders exist");
Copy link
Contributor

Choose a reason for hiding this comment

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

These checks are pretty important so they should fail loudly IMO (e.g. tracing::error!)

Suggested change
tracing::warn!(?err, "failed to check if all orders exist");
tracing::warn!(?err, "failed to check if all unexpected orders are JIT orders");

false
})
}

/// Computes a driver's solutions for the solver competition.
async fn solve(
&self,
Expand Down
50 changes: 50 additions & 0 deletions crates/database/src/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,27 @@ pub async fn user_orders_with_quote(
.await
}

pub async fn orders_do_not_exist(
ex: &mut PgConnection,
uids: &[OrderUid],
Copy link
Contributor

Choose a reason for hiding this comment

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

Could take an impl IntoIterator<Item = OrderUid> instead to avoid double allocating the orders vector.

) -> Result<bool, sqlx::Error> {
if uids.is_empty() {
return Ok(true);
}

let mut query_builder =
QueryBuilder::new("SELECT NOT EXISTS (SELECT 1 FROM orders WHERE uid IN (");

let mut separated = query_builder.separated(", ");
for value_type in uids {
separated.push_bind(value_type);
}
separated.push_unseparated(")) ");
Comment on lines +769 to +773
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks other than the usual query builder code. Is the explicit use of .separated() and push_unseparated() here necessary because we push into the inner query here?


let query = query_builder.build_query_scalar();
query.fetch_one(ex).await
}

#[cfg(test)]
mod tests {
use {
Expand Down Expand Up @@ -1891,4 +1912,33 @@ mod tests {
.unwrap();
assert_eq!(full_order.full_app_data, Some(full_app_data));
}

#[tokio::test]
#[ignore]
async fn postgres_all_orders_not_exist() {
let mut db = PgConnection::connect("postgresql://").await.unwrap();
let mut db = db.begin().await.unwrap();
crate::clear_DANGER_(&mut db).await.unwrap();

let order1 = Order {
uid: ByteArray([1; 56]),
..Default::default()
};
let order2 = Order {
uid: ByteArray([2; 56]),
..Default::default()
};

assert!(orders_do_not_exist(&mut db, &[order1.uid, order2.uid])
.await
.unwrap());

insert_order(&mut db, &order2).await.unwrap();

assert!(orders_do_not_exist(&mut db, &[order1.uid]).await.unwrap());
assert!(!orders_do_not_exist(&mut db, &[order2.uid]).await.unwrap());
assert!(!orders_do_not_exist(&mut db, &[order1.uid, order2.uid])
.await
.unwrap());
}
}
Loading