Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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
77 changes: 57 additions & 20 deletions crates/driver/src/domain/competition/solution/settlement.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use {
super::{Error, Solution, encoding, trade::ClearingPrices},
super::{
Error,
Solution,
encoding,
trade::{self, ClearingPrices},
},
crate::{
domain::{
self,
Expand Down Expand Up @@ -148,23 +153,19 @@ 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 {
if !trade.order().buys_eth() || !trade.order().pays_to_contract(eth).await? {
return Ok(Default::default());
}
let tx = eth::Tx {
from: solution.solver().address(),
to: trade.order().receiver(),
value: 1.into(),
input: Default::default(),
access_list: Default::default(),
};
Result::<_, Error>::Ok(simulator.access_list(&tx).await?)
}))
.await?;
let partial_access_list = partial_access_lists
.into_iter()
.fold(eth::AccessList::default(), |acc, list| acc.merge(list));
//
// A non-empty partial access list also signals that at least one trade
// strictly requires an access list. Conversely, an empty partial list
// means the access list is only a gas optimization for this settlement,
// so a fetch failure below can be tolerated.
let partial_access_list = try_join_all(
solution
.user_trades()
.map(|trade| partial_access_list_for(trade, &solution, eth, simulator)),
)
.await?
.into_iter()
.fold(eth::AccessList::default(), |acc, list| acc.merge(list));

// Simulate the settlement and get the access list and gas.
let (access_list, gas) = Self::simulate(
Expand Down Expand Up @@ -231,8 +232,22 @@ impl Settlement {
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. If the partial list is empty, no trade
// strictly requires the access-list workaround, so 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 partial_access_list.is_empty() => {
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 Expand Up @@ -353,6 +368,28 @@ impl Settlement {
}
}

/// Return the partial access list entries needed to forward 1 wei of ETH to a
/// smart-contract receiver. Returns an empty access list when the trade does
/// not trigger the gas-limit workaround (non-ETH buy or EOA receiver).
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.

I wonder if a new type makes sense here: RequiredAccessList.
This function would then return Result<Option<RequiredAccessList>> to indicate on a type level whether we can continue after non-revert errors.

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.

Makes sense. Done in 5fa6473

async fn partial_access_list_for(
Comment on lines +379 to +389
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.

"gas-limit workaround" is a bit abstract here, could you cover what it exactly is?

My understanding it that without the access list the TX would not pass below the gas limit?

trade: &trade::Fulfillment,
solution: &Solution,
eth: &Ethereum,
simulator: &Simulator,
) -> Result<eth::AccessList, Error> {
if !trade.order().buys_eth() || !trade.order().pays_to_contract(eth).await? {
return Ok(eth::AccessList::default());
}
let tx = eth::Tx {
from: solution.solver().address(),
to: trade.order().receiver(),
value: 1.into(),
input: Default::default(),
access_list: Default::default(),
};
Ok(simulator.access_list(&tx).await?)
}

/// Should the interactions be internalized?
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Internalization {
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 LoggingJson<T>(pub T);

impl<S, T> FromRequest<S> for LoggingJson<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 LoggingQuery<T>(pub T);

impl<S, T> FromRequestParts<S> for LoggingQuery<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
6 changes: 3 additions & 3 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::LoggingQuery},
observe,
},
tracing::Instrument,
Expand All @@ -16,10 +16,10 @@ 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>,
LoggingQuery(order): LoggingQuery<dto::Order>,
) -> Result<axum::Json<dto::Quote>, (axum::http::StatusCode, axum::Json<Error>)> {
let handle_request = async {
let order = order.0.into_domain();
let order = order.into_domain();
observe::quoting(&order);
let quote = order
.quote(
Expand Down
4 changes: 2 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::LoggingJson},
observe,
},
},
Expand All @@ -17,7 +17,7 @@ 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>,
LoggingJson(req): LoggingJson<dto::RevealRequest>,
) -> Result<axum::Json<dto::RevealResponse>, (axum::http::StatusCode, axum::Json<Error>)> {
let auction_id =
auction::Id::try_from(req.auction_id).map_err(api::routes::AuctionError::from)?;
Expand Down
4 changes: 2 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::LoggingJson},
observe,
},
},
Expand All @@ -17,7 +17,7 @@ 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>,
LoggingJson(req): LoggingJson<dto::SettleRequest>,
) -> Result<(), (axum::http::StatusCode, axum::Json<Error>)> {
let auction_id =
auction::Id::try_from(req.auction_id).map_err(api::routes::AuctionError::from)?;
Expand Down
Loading