From 7922749c74f43afa407396c9bccb390fb2cc5b8e Mon Sep 17 00:00:00 2001 From: Dusan Stanivukovic Date: Wed, 1 Nov 2023 10:36:46 +0100 Subject: [PATCH] Colocation Notify - failed simulation (#2013) # Description Implements (2) from https://github.com/cowprotocol/services/issues/1891 # Changes - [ ] Simulation failure error (together with tx data) propagated from the `driver` to the `solvers`. Unfortunately, not all data can (and should) be populated to support the legacy version. `block_number` is one of those that is needed but it is not provided by simulators. This is the never ending issue we generally have with doing simulations on a specific block number. The easiest fix would be to add get_block_number() call to each web3 simulation (enso and tenderly seem to give block_number in their responses), but it wouldn't be accurate. At the moment I decided to skip it, since, after all, solvers can pretty much figure out the block number on their own, there are usually 1 or 2 candidates... --- Cargo.lock | 1 + .../src/domain/competition/solution/mod.rs | 4 +- .../domain/competition/solution/settlement.rs | 2 +- crates/driver/src/infra/notify/mod.rs | 43 +++++++++---------- .../driver/src/infra/notify/notification.rs | 12 +++--- crates/driver/src/infra/simulator/mod.rs | 8 ++-- .../src/infra/solver/dto/notification.rs | 22 ++++++++++ crates/shared/src/http_solver/model.rs | 2 + crates/solvers/Cargo.toml | 1 + .../src/api/routes/notify/dto/notification.rs | 22 ++++++++++ crates/solvers/src/boundary/legacy.rs | 21 +++++++++ crates/solvers/src/domain/eth/mod.rs | 34 +++++++++++++++ crates/solvers/src/domain/notification.rs | 11 ++--- crates/solvers/src/util/bytes.rs | 30 +++++++++++++ crates/solvers/src/util/mod.rs | 1 + 15 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 crates/solvers/src/util/bytes.rs diff --git a/Cargo.lock b/Cargo.lock index 15fcc9b1ae..7a9de100fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4172,6 +4172,7 @@ dependencies = [ "tower", "tower-http", "tracing", + "web3", ] [[package]] diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index ec37e22376..bdf06c1f89 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -324,9 +324,9 @@ pub enum Error { #[error(transparent)] Execution(#[from] trade::ExecutionError), #[error( - "invalid internalization: solution attempts to internalize tokens which are not trusted" + "non bufferable tokens used: solution attempts to internalize tokens which are not trusted" )] - UntrustedInternalization(BTreeSet), + NonBufferableTokensUsed(BTreeSet), #[error("invalid internalization: uninternalized solution fails to simulate")] FailingInternalization, #[error("insufficient solver account Ether balance, required {0:?}")] diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 10c0e17a60..b0dec87f58 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -113,7 +113,7 @@ impl Settlement { .map(|asset| asset.token) .collect::>(); if !untrusted_tokens.is_empty() { - return Err(Error::UntrustedInternalization(untrusted_tokens)); + return Err(Error::NonBufferableTokensUsed(untrusted_tokens)); } // Encode the solution into a settlement. diff --git a/crates/driver/src/infra/notify/mod.rs b/crates/driver/src/infra/notify/mod.rs index d89400b33e..a218903b2e 100644 --- a/crates/driver/src/infra/notify/mod.rs +++ b/crates/driver/src/infra/notify/mod.rs @@ -6,8 +6,10 @@ use { mod notification; pub use notification::{Kind, Notification, ScoreKind, Settlement}; - -use crate::domain::{competition::score, eth, mempools::Error}; +use { + super::simulator, + crate::domain::{competition::score, eth, mempools::Error}, +}; pub fn solver_timeout(solver: &Solver, auction_id: Option) { solver.notify(auction_id, None, notification::Kind::Timeout); @@ -59,29 +61,26 @@ pub fn encoding_failed( solution_id: solution::Id, err: &solution::Error, ) { - match err { - solution::Error::UntrustedInternalization(tokens) => { - solver.notify( - auction_id, - Some(solution_id), - notification::Kind::NonBufferableTokensUsed(tokens.clone()), - ); + let notification = match err { + solution::Error::NonBufferableTokensUsed(tokens) => { + notification::Kind::NonBufferableTokensUsed(tokens.clone()) } solution::Error::SolverAccountInsufficientBalance(required) => { - solver.notify( - auction_id, - Some(solution_id), - notification::Kind::SolverAccountInsufficientBalance(*required), - ); + notification::Kind::SolverAccountInsufficientBalance(*required) } - solution::Error::Blockchain(_) => (), - solution::Error::Boundary(_) => (), - solution::Error::Simulation(_) => (), // todo, - solution::Error::AssetFlow(_) => (), - solution::Error::Execution(_) => (), - solution::Error::FailingInternalization => (), - solution::Error::DifferentSolvers => (), - } + solution::Error::Blockchain(_) => return, + solution::Error::Boundary(_) => return, + solution::Error::Simulation(simulator::Error::WithTx(error)) => { + notification::Kind::SimulationFailed(error.tx.clone()) + } + solution::Error::Simulation(simulator::Error::Basic(_)) => return, + solution::Error::AssetFlow(_) => return, + solution::Error::Execution(_) => return, + solution::Error::FailingInternalization => return, + solution::Error::DifferentSolvers => return, + }; + + solver.notify(auction_id, Some(solution_id), notification); } pub fn executed( diff --git a/crates/driver/src/infra/notify/notification.rs b/crates/driver/src/infra/notify/notification.rs index f31df7e304..5b7eab0b5f 100644 --- a/crates/driver/src/infra/notify/notification.rs +++ b/crates/driver/src/infra/notify/notification.rs @@ -6,6 +6,11 @@ use { std::collections::BTreeSet, }; +type RequiredEther = Ether; +type TokensUsed = BTreeSet; +type TransactionHash = eth::TxId; +type Transaction = eth::Tx; + /// A notification sent to solvers in case of important events in the driver. #[derive(Debug)] pub struct Notification { @@ -14,9 +19,6 @@ pub struct Notification { pub kind: Kind, } -pub type RequiredEther = Ether; -pub type TokensUsed = BTreeSet; - #[derive(Debug)] pub enum Kind { /// Solver engine timed out. @@ -25,6 +27,8 @@ pub enum Kind { EmptySolution, /// Solution received from solver engine don't have unique id. DuplicatedSolutionId, + /// Failed simulation during competition. + SimulationFailed(Transaction), /// No valid score could be computed for the solution. ScoringFailed(ScoreKind), /// Solution aimed to internalize tokens that are not considered safe to @@ -60,8 +64,6 @@ pub enum ScoreKind { ObjectiveValueNonPositive, } -type TransactionHash = eth::TxId; - #[derive(Debug)] pub enum Settlement { /// Winning solver settled successfully transaction onchain. diff --git a/crates/driver/src/infra/simulator/mod.rs b/crates/driver/src/infra/simulator/mod.rs index d9f3354e17..52af8c662b 100644 --- a/crates/driver/src/infra/simulator/mod.rs +++ b/crates/driver/src/infra/simulator/mod.rs @@ -142,17 +142,17 @@ pub enum SimulatorError { #[derive(Debug, thiserror::Error)] #[error("err: {err:?}, tx: {tx:?}")] pub struct WithTxError { - err: SimulatorError, - tx: eth::Tx, + pub err: SimulatorError, + pub tx: eth::Tx, } #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("basic: {0:?}")] + #[error(transparent)] Basic(#[from] SimulatorError), /// If a transaction reverted, forward that transaction together with the /// error. - #[error("with tx: {0:?}")] + #[error(transparent)] WithTx(#[from] WithTxError), } diff --git a/crates/driver/src/infra/solver/dto/notification.rs b/crates/driver/src/infra/solver/dto/notification.rs index 65e676446e..9ac6edc11f 100644 --- a/crates/driver/src/infra/solver/dto/notification.rs +++ b/crates/driver/src/infra/solver/dto/notification.rs @@ -10,6 +10,7 @@ use { serde::Serialize, serde_with::serde_as, std::collections::BTreeSet, + web3::types::AccessList, }; impl Notification { @@ -24,6 +25,13 @@ impl Notification { kind: match kind { notify::Kind::Timeout => Kind::Timeout, notify::Kind::EmptySolution => Kind::EmptySolution, + notify::Kind::SimulationFailed(tx) => Kind::SimulationFailed(Tx { + from: tx.from.into(), + to: tx.to.into(), + input: tx.input.into(), + value: tx.value.into(), + access_list: tx.access_list.into(), + }), notify::Kind::ScoringFailed(notify::ScoreKind::ZeroScore) => { Kind::ScoringFailed(ScoreKind::ZeroScore) } @@ -82,6 +90,7 @@ pub enum Kind { Timeout, EmptySolution, DuplicatedSolutionId, + SimulationFailed(Tx), ScoringFailed(ScoreKind), NonBufferableTokensUsed { tokens: BTreeSet, @@ -93,6 +102,19 @@ pub enum Kind { Settled(Settlement), } +#[serde_as] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Tx { + pub from: eth::H160, + pub to: eth::H160, + #[serde_as(as = "serialize::Hex")] + pub input: Vec, + #[serde_as(as = "serialize::U256")] + pub value: eth::U256, + pub access_list: AccessList, +} + #[serde_as] #[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] diff --git a/crates/shared/src/http_solver/model.rs b/crates/shared/src/http_solver/model.rs index 8bfac6b336..1d78c8a1d1 100644 --- a/crates/shared/src/http_solver/model.rs +++ b/crates/shared/src/http_solver/model.rs @@ -509,6 +509,7 @@ pub struct SimulatedTransaction { /// on pub tx_index: u64, /// Is transaction simulated with internalized interactions or without + /// TODO: remove field once the colocation is enabled. pub internalization: InternalizationStrategy, /// Which storage the settlement tries to access. Contains `None` if some /// error happened while estimating the access list. @@ -536,6 +537,7 @@ pub enum InternalizationStrategy { EncodeAllInteractions, #[serde(rename = "Enabled")] SkipInternalizableInteraction, + Unknown, } #[cfg(test)] diff --git a/crates/solvers/Cargo.toml b/crates/solvers/Cargo.toml index 9168f30f00..c34b389d50 100644 --- a/crates/solvers/Cargo.toml +++ b/crates/solvers/Cargo.toml @@ -33,6 +33,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "tim toml = "0.7" tower = "0.4" tower-http = { version = "0.4", features = ["trace"] } +web3 = "0.19" # TODO Once solvers are ported and E2E tests set up, slowly migrate code and # remove/re-evaluate these dependencies. diff --git a/crates/solvers/src/api/routes/notify/dto/notification.rs b/crates/solvers/src/api/routes/notify/dto/notification.rs index e7727be7f4..67eb75d11a 100644 --- a/crates/solvers/src/api/routes/notify/dto/notification.rs +++ b/crates/solvers/src/api/routes/notify/dto/notification.rs @@ -7,6 +7,7 @@ use { serde::Deserialize, serde_with::{serde_as, DisplayFromStr}, std::collections::BTreeSet, + web3::types::AccessList, }; impl Notification { @@ -21,6 +22,13 @@ impl Notification { kind: match &self.kind { Kind::Timeout => notification::Kind::Timeout, Kind::EmptySolution => notification::Kind::EmptySolution, + Kind::SimulationFailed(tx) => notification::Kind::SimulationFailed(eth::Tx { + from: tx.from.into(), + to: tx.to.into(), + input: tx.input.clone().into(), + value: tx.value.into(), + access_list: tx.access_list.clone(), + }), Kind::ScoringFailed(ScoreKind::ObjectiveValueNonPositive) => { notification::Kind::ScoringFailed( notification::ScoreKind::ObjectiveValueNonPositive, @@ -89,6 +97,7 @@ pub enum Kind { Timeout, EmptySolution, DuplicatedSolutionId, + SimulationFailed(Tx), ScoringFailed(ScoreKind), NonBufferableTokensUsed { tokens: BTreeSet, @@ -100,6 +109,19 @@ pub enum Kind { Settled(Settlement), } +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Tx { + from: H160, + to: H160, + #[serde_as(as = "serialize::Hex")] + input: Vec, + #[serde_as(as = "serialize::U256")] + value: U256, + access_list: AccessList, +} + #[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] diff --git a/crates/solvers/src/boundary/legacy.rs b/crates/solvers/src/boundary/legacy.rs index 9dcaf2a3d7..da3fed4163 100644 --- a/crates/solvers/src/boundary/legacy.rs +++ b/crates/solvers/src/boundary/legacy.rs @@ -23,16 +23,19 @@ use { BatchAuctionModel, ConcentratedPoolParameters, ConstantProductPoolParameters, + InternalizationStrategy, MetadataModel, OrderModel, Score, SettledBatchAuctionModel, + SimulatedTransaction, SolverRejectionReason, SolverRunError, StablePoolParameters, SubmissionResult, TokenAmount, TokenInfoModel, + TransactionWithError, WeightedPoolTokenData, WeightedProductPoolParameters, }, @@ -572,6 +575,8 @@ fn to_domain_solution( }) } +const UNKNOWN_BLOCK_NUMBER: u64 = 0; + fn to_boundary_auction_result(notification: ¬ification::Notification) -> (i64, AuctionResult) { let auction_id = match notification.auction_id { auction::Id::Solve(id) => id, @@ -583,6 +588,22 @@ fn to_boundary_auction_result(notification: ¬ification::Notification) -> (i64 AuctionResult::Rejected(SolverRejectionReason::RunError(SolverRunError::Timeout)) } Kind::EmptySolution => AuctionResult::Rejected(SolverRejectionReason::NoUserOrders), + Kind::SimulationFailed(tx) => AuctionResult::Rejected( + SolverRejectionReason::SimulationFailure(TransactionWithError { + error: "".to_string(), + transaction: SimulatedTransaction { + from: tx.from.into(), + to: tx.to.into(), + data: tx.input.clone().into(), + internalization: InternalizationStrategy::Unknown, + block_number: UNKNOWN_BLOCK_NUMBER, // todo #2018 + tx_index: Default::default(), + access_list: Default::default(), + max_fee_per_gas: Default::default(), + max_priority_fee_per_gas: Default::default(), + }, + }), + ), Kind::ScoringFailed(ScoreKind::ObjectiveValueNonPositive) => { AuctionResult::Rejected(SolverRejectionReason::ObjectiveValueNonPositive) } diff --git a/crates/solvers/src/domain/eth/mod.rs b/crates/solvers/src/domain/eth/mod.rs index b16d39e838..82023089dd 100644 --- a/crates/solvers/src/domain/eth/mod.rs +++ b/crates/solvers/src/domain/eth/mod.rs @@ -1,3 +1,5 @@ +use {crate::util::bytes::Bytes, web3::types::AccessList}; + mod chain; pub use { @@ -37,9 +39,41 @@ pub struct Asset { #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Ether(pub U256); +impl From for Ether { + fn from(value: U256) -> Self { + Self(value) + } +} + /// Gas amount. #[derive(Clone, Copy, Debug, Default)] pub struct Gas(pub U256); /// A 256-bit rational type. pub type Rational = num::rational::Ratio; + +/// An address. Can be an EOA or a smart contract address. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Address(pub H160); + +impl From for Address { + fn from(value: H160) -> Self { + Self(value) + } +} + +impl From
for H160 { + fn from(value: Address) -> Self { + value.0 + } +} + +/// An onchain transaction. +#[derive(Debug, Clone)] +pub struct Tx { + pub from: Address, + pub to: Address, + pub value: Ether, + pub input: Bytes>, + pub access_list: AccessList, +} diff --git a/crates/solvers/src/domain/notification.rs b/crates/solvers/src/domain/notification.rs index 2da4aa9a14..d9404b1108 100644 --- a/crates/solvers/src/domain/notification.rs +++ b/crates/solvers/src/domain/notification.rs @@ -7,6 +7,11 @@ use { std::collections::BTreeSet, }; +type RequiredEther = Ether; +type TokensUsed = BTreeSet; +type TransactionHash = eth::H256; +type Transaction = eth::Tx; + /// The notification about important events happened in driver, that solvers /// need to know about. #[derive(Debug)] @@ -16,23 +21,19 @@ pub struct Notification { pub kind: Kind, } -pub type RequiredEther = Ether; -pub type TokensUsed = BTreeSet; - /// All types of notifications solvers can be informed about. #[derive(Debug)] pub enum Kind { Timeout, EmptySolution, DuplicatedSolutionId, + SimulationFailed(Transaction), ScoringFailed(ScoreKind), NonBufferableTokensUsed(TokensUsed), SolverAccountInsufficientBalance(RequiredEther), Settled(Settlement), } -pub type TransactionHash = eth::H256; - /// The result of winning solver trying to settle the transaction onchain. #[derive(Debug)] pub enum Settlement { diff --git a/crates/solvers/src/util/bytes.rs b/crates/solvers/src/util/bytes.rs new file mode 100644 index 0000000000..1ff531c545 --- /dev/null +++ b/crates/solvers/src/util/bytes.rs @@ -0,0 +1,30 @@ +/// A thin wrapper around a collection of bytes. Provides hex debug formatting. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct Bytes(pub T); + +impl std::fmt::Debug for Bytes +where + T: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x{}", hex::encode(&self.0)) + } +} + +impl From for Bytes { + fn from(value: T) -> Self { + Self(value) + } +} + +impl From>> for Vec { + fn from(value: Bytes>) -> Self { + value.0 + } +} + +impl From> for [u8; N] { + fn from(value: Bytes<[u8; N]>) -> Self { + value.0 + } +} diff --git a/crates/solvers/src/util/mod.rs b/crates/solvers/src/util/mod.rs index 96d4562cc3..bb03d5273e 100644 --- a/crates/solvers/src/util/mod.rs +++ b/crates/solvers/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod bytes; pub mod conv; pub mod fmt; pub mod http;