Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion crates/driver/src/domain/competition/pre_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ impl Utilities {
observe::metrics::metrics().on_auction_overhead_start("driver", "parse_dto");
// deserialization takes tens of milliseconds so run it on a blocking task
tokio::task::spawn_blocking(move || {
serde_json::from_slice(&solve_request).context("could not parse solve request")
serde_json::from_slice(&solve_request)
.inspect_err(|err| {
tracing::warn!(?err, "failed to parse /solve request body");
})
.context("could not parse solve request")
})
.await
.context("failed to await blocking task")??
Expand Down
37 changes: 31 additions & 6 deletions crates/driver/src/domain/competition/solution/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,15 @@ impl Settlement {
// The solution is to do access list estimation in two steps: first, simulate
// moving 1 wei into every smart contract to get a partial access list, and then
// use that partial access list to calculate the final access list.
let partial_access_lists = try_join_all(solution.user_trades().map(|trade| async {
//
// The same predicate also tells us whether an access list is strictly
// required for this settlement: the gas-limit workaround only matters
// when at least one trade sends ETH to a smart contract. For any other
// settlement the access list is purely a gas optimization, so a fetch
// failure should not abort submission.
let per_trade_access_lists = try_join_all(solution.user_trades().map(|trade| async {
Comment thread
squadgazzz marked this conversation as resolved.
Outdated
if !trade.order().buys_eth() || !trade.order().pays_to_contract(eth).await? {
return Ok(Default::default());
return Result::<_, Error>::Ok((false, eth::AccessList::default()));
}
let tx = eth::Tx {
from: solution.solver().address(),
Expand All @@ -159,17 +165,20 @@ impl Settlement {
input: Default::default(),
access_list: Default::default(),
};
Result::<_, Error>::Ok(simulator.access_list(&tx).await?)
Ok((true, simulator.access_list(&tx).await?))
}))
.await?;
let partial_access_list = partial_access_lists
let access_list_required = per_trade_access_lists.iter().any(|(r, _)| *r);
let partial_access_list = per_trade_access_lists
.into_iter()
.map(|(_, list)| list)
.fold(eth::AccessList::default(), |acc, list| acc.merge(list));
Comment thread
squadgazzz marked this conversation as resolved.
Outdated

// Simulate the settlement and get the access list and gas.
let (access_list, gas) = Self::simulate(
transaction.internalized.clone(),
&partial_access_list,
access_list_required,
eth,
simulator,
)
Expand Down Expand Up @@ -203,6 +212,7 @@ impl Settlement {
Self::simulate(
transaction.uninternalized.clone(),
&partial_access_list,
access_list_required,
eth,
simulator,
)
Expand All @@ -224,15 +234,30 @@ impl Settlement {
async fn simulate(
tx: eth::Tx,
partial_access_list: &eth::AccessList,
access_list_required: bool,
Comment thread
squadgazzz marked this conversation as resolved.
Outdated
eth: &Ethereum,
simulator: &Simulator,
) -> Result<(eth::AccessList, eth::Gas), Error> {
// Add the partial access list to the settlement tx.
let tx = tx.set_access_list(partial_access_list.to_owned());

// Simulate the full access list, passing the partial access
// list into the simulation.
let access_list = simulator.access_list(&tx).await?;
// list into the simulation. When no trade strictly requires an access
// list (no ETH -> contract transfer), treat the fetch as best-effort:
// a failing RPC would otherwise abort the whole settlement even though
// the on-chain tx would succeed without it.
let access_list = match simulator.access_list(&tx).await {
Ok(list) => list,
Err(err) if !access_list_required => {
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 here we need to do stricter error handling. If the error indicates a revert we should still abort. Only if we can't reason about the outcome of the simulation we should assume things are okay (e.g. deserialization error like in the post-mortem, transport error, something else? 🤷‍♂️).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

tracing::warn!(
?err,
"access list estimation failed; continuing without it (no ETH->contract \
trades)"
Comment on lines +252 to +253
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.

Like the other comment, I also don't understand what "ETH->contract" is supposed to mean, is it a transfer?

);
partial_access_list.clone()
}
Err(err) => return Err(err.into()),
};
let tx = tx.set_access_list(access_list.clone());

// Simulate the settlement using the full access list and get the gas used.
Expand Down
65 changes: 65 additions & 0 deletions crates/driver/src/infra/api/extract.rs
Comment thread
squadgazzz marked this conversation as resolved.
Outdated
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 error handling should be split into a separate PR IMO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done #4358

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Axum extractors that emit a `warn` log when request deserialization
//! fails, then delegate to the stock extractor's rejection so the HTTP
//! response shape is unchanged.

use {
axum::{
extract::{
FromRequest,
FromRequestParts,
Request,
rejection::{JsonRejection, QueryRejection},
},
http::request::Parts,
},
serde::de::DeserializeOwned,
};

pub struct Json<T>(pub T);

impl<S, T> FromRequest<S> for Json<T>
where
S: Send + Sync,
T: DeserializeOwned,
{
type Rejection = JsonRejection;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
match axum::Json::<T>::from_request(req, state).await {
Ok(axum::Json(value)) => Ok(Self(value)),
Err(rejection) => {
tracing::warn!(
err = %rejection,
target = std::any::type_name::<T>(),
"failed to deserialize JSON request body",
);
Err(rejection)
}
}
}
}

pub struct Query<T>(pub T);

impl<S, T> FromRequestParts<S> for Query<T>
where
S: Send + Sync,
T: DeserializeOwned,
{
type Rejection = QueryRejection;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match axum::extract::Query::<T>::from_request_parts(parts, state).await {
Ok(axum::extract::Query(value)) => Ok(Self(value)),
Err(rejection) => {
tracing::warn!(
err = %rejection,
target = std::any::type_name::<T>(),
query = parts.uri.query().unwrap_or_default(),
"failed to deserialize query string",
);
Err(rejection)
}
}
}
}
1 change: 1 addition & 0 deletions crates/driver/src/infra/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use {
};

mod error;
mod extract;
pub mod routes;

pub struct Api {
Expand Down
4 changes: 2 additions & 2 deletions crates/driver/src/infra/api/routes/quote/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
crate::infra::{
api::{Error, State},
api::{Error, State, extract},
observe,
},
tracing::Instrument,
Expand All @@ -16,7 +16,7 @@ pub(in crate::infra::api) fn quote(router: axum::Router<State>) -> axum::Router<

async fn route(
state: axum::extract::State<State>,
order: axum::extract::Query<dto::Order>,
order: extract::Query<dto::Order>,
) -> Result<axum::Json<dto::Quote>, (axum::http::StatusCode, axum::Json<Error>)> {
let handle_request = async {
let order = order.0.into_domain();
Expand Down
5 changes: 3 additions & 2 deletions crates/driver/src/infra/api/routes/reveal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use {
crate::{
domain::competition::auction,
infra::{
api::{self, Error, State},
api::{self, Error, State, extract},
observe,
},
},
Expand All @@ -17,8 +17,9 @@ pub(in crate::infra::api) fn reveal(router: axum::Router<State>) -> axum::Router

async fn route(
state: axum::extract::State<State>,
req: axum::Json<dto::RevealRequest>,
req: extract::Json<dto::RevealRequest>,
) -> Result<axum::Json<dto::RevealResponse>, (axum::http::StatusCode, axum::Json<Error>)> {
let req = req.0;
let auction_id =
auction::Id::try_from(req.auction_id).map_err(api::routes::AuctionError::from)?;
let handle_request = async {
Expand Down
5 changes: 3 additions & 2 deletions crates/driver/src/infra/api/routes/settle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use {
crate::{
domain::competition::auction,
infra::{
api::{self, Error, State},
api::{self, Error, State, extract},
observe,
},
},
Expand All @@ -17,8 +17,9 @@ pub(in crate::infra::api) fn settle(router: axum::Router<State>) -> axum::Router

async fn route(
state: axum::extract::State<State>,
req: axum::Json<dto::SettleRequest>,
req: extract::Json<dto::SettleRequest>,
) -> Result<(), (axum::http::StatusCode, axum::Json<Error>)> {
let req = req.0;
Comment thread
squadgazzz marked this conversation as resolved.
Outdated
let auction_id =
auction::Id::try_from(req.auction_id).map_err(api::routes::AuctionError::from)?;
let solver = state.solver().name().to_string();
Expand Down
Loading