Skip to content
Open
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
102 changes: 77 additions & 25 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 All @@ -15,7 +20,7 @@ use {
alloy::primitives::U256,
eth_domain_types as eth,
futures::future::try_join_all,
simulator::Simulator,
simulator::{self, Simulator},
std::collections::{BTreeSet, HashMap, HashSet},
tracing::instrument,
};
Expand Down Expand Up @@ -148,28 +153,26 @@ 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));
//
// `Some(..)` means at least one trade strictly requires an access list;
// `None` means it is purely a gas optimization for this settlement, so a
// non-revert fetch failure below can be tolerated.
let partial_access_list: Option<RequiredAccessList> = try_join_all(
solution
.user_trades()
.map(|trade| partial_access_list_for(trade, &solution, eth, simulator)),
)
.await?
.into_iter()
.flatten()
.map(|required| required.0)
.reduce(|acc, list| acc.merge(list))
.map(RequiredAccessList);

// Simulate the settlement and get the access list and gas.
let (access_list, gas) = Self::simulate(
transaction.internalized.clone(),
&partial_access_list,
partial_access_list.as_ref(),
eth,
simulator,
)
Expand Down Expand Up @@ -202,7 +205,7 @@ impl Settlement {
// that the settlement simulates even when internalizations are disabled.
Self::simulate(
transaction.uninternalized.clone(),
&partial_access_list,
partial_access_list.as_ref(),
eth,
simulator,
)
Expand All @@ -223,16 +226,36 @@ impl Settlement {
#[instrument(name = "simulate_settlement", skip_all)]
async fn simulate(
tx: eth::Tx,
partial_access_list: &eth::AccessList,
partial_access_list: Option<&RequiredAccessList>,
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());
let tx = tx.set_access_list(
partial_access_list
.map(|required| required.0.clone())
.unwrap_or_default(),
);

// 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 the
// access-list workaround, a non-revert failure (transport,
// deserialization, ...) shouldn't abort the settlement: we can't
// reason about the outcome, but the on-chain tx would still succeed
// without the access list. Revert errors are propagated either way
// since they signal a real problem with the settlement.
let access_list = match simulator.access_list(&tx).await {
Ok(list) => list,
Err(simulator::Error::Other(err)) if partial_access_list.is_none() => {
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?

);
eth::AccessList::default()
}
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 +376,35 @@ impl Settlement {
}
}

/// Access list entries covering a trade that strictly requires the
/// ETH→contract gas-limit workaround. Distinct from a bare `eth::AccessList`
/// so the settlement-level fallback can tell "no trade required one" (`None`)
/// apart from "trade required one but the list turned out empty" (`Some`).
#[derive(Debug, Clone)]
struct RequiredAccessList(eth::AccessList);

/// Return the partial access list for a single trade. `None` means the trade
/// does not trigger the gas-limit workaround (non-ETH buy or EOA receiver),
/// so its absence must not be treated as "required but empty".
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<Option<RequiredAccessList>, Error> {
if !trade.order().buys_eth() || !trade.order().pays_to_contract(eth).await? {
return Ok(None);
}
let tx = eth::Tx {
from: solution.solver().address(),
to: trade.order().receiver(),
value: 1.into(),
input: Default::default(),
access_list: Default::default(),
};
Ok(Some(RequiredAccessList(simulator.access_list(&tx).await?)))
}

/// Should the interactions be internalized?
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Internalization {
Expand Down
Loading