Skip to content

Commit

Permalink
Autopilot can receive multiple solutions (#1909)
Browse files Browse the repository at this point in the history
# Description
Implements #1905

# Changes
<!-- List of detailed changes (how the change is accomplished) -->

- [x] Changes the solve/reveal/settle interface between `autopilot` and
`driver`. Now the driver can send multiple solutions for a single
auction for a single solver. As a consequence, each solution must have
unique `id`, therefore `/reveal` and `/settle` requests must include
`id` of the winning solution so the driver can know which one was
selected as winner.
- [x] A bit of renaming on the `autopilot` side to make things clearer
and consistent.
- [x] Reorganized the code on `driver` side for `/reveal` and `/settle`
(separate `dto` files as we had for `/solve`). This is to be consistent,
but also now when you look into driver api folder it's immediately seen
which endpoints are there.
  • Loading branch information
sunce86 authored Oct 3, 2023
1 parent ebb1778 commit 3b92512
Show file tree
Hide file tree
Showing 20 changed files with 376 additions and 196 deletions.
8 changes: 4 additions & 4 deletions crates/autopilot/src/driver_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ impl Driver {
self.request_response("solve", Some(request)).await
}

pub async fn reveal(&self) -> Result<reveal::Response> {
self.request_response("reveal", Option::<&()>::None).await
pub async fn reveal(&self, request: &reveal::Request) -> Result<reveal::Response> {
self.request_response("reveal", Some(request)).await
}

pub async fn settle(&self) -> Result<settle::Response> {
self.request_response("settle", Option::<&()>::None).await
pub async fn settle(&self, request: &settle::Request) -> Result<settle::Response> {
self.request_response("settle", Some(request)).await
}

async fn request_response<Response>(
Expand Down
43 changes: 38 additions & 5 deletions crates/autopilot/src/driver_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,22 +133,42 @@ pub mod solve {
pub call_data: Vec<u8>,
}

#[serde_as]
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Response {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Solution {
/// Unique ID of the solution, used to identify it in subsequent
/// requests (reveal, settle).
#[serde_as(as = "serde_with::DisplayFromStr")]
pub solution_id: u64,
pub score: U256,
/// Address used by the driver to submit the settlement onchain.
pub submission_address: H160,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Response {
pub solutions: Vec<Solution>,
}
}

pub mod reveal {
use {
model::{bytes_hex, order::OrderUid},
serde::Deserialize,
serde::{Deserialize, Serialize},
serde_with::serde_as,
};

#[serde_as]
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Request {
/// Unique ID of the solution to reveal.
#[serde_as(as = "serde_with::DisplayFromStr")]
pub solution_id: u64,
}

#[serde_as]
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand All @@ -160,15 +180,28 @@ pub mod reveal {
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Response {
pub orders: Vec<OrderUid>,
pub calldata: Calldata,
}
}

pub mod settle {
use {model::bytes_hex, serde::Deserialize, serde_with::serde_as};
use {
model::bytes_hex,
serde::{Deserialize, Serialize},
serde_with::serde_as,
};

#[serde_as]
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Request {
/// Unique ID of the solution to settle.
#[serde_as(as = "serde_with::DisplayFromStr")]
pub solution_id: u64,
}

#[serde_as]
#[derive(Clone, Debug, Default, Deserialize)]
Expand Down
86 changes: 65 additions & 21 deletions crates/autopilot/src/run_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use {
},
driver_api::Driver,
driver_model::{
reveal,
reveal::{self, Request},
settle,
solve::{self, Class},
},
solvable_orders::SolvableOrdersCache,
Expand Down Expand Up @@ -89,7 +90,43 @@ impl RunLoop {

async fn single_run_(&self, id: AuctionId, auction: &Auction) {
tracing::info!("solving");
let mut solutions = self.solve(auction, id).await;
let solutions = self.solve(auction, id).await;

// Validate solutions and filter out invalid ones.
let mut solutions = solutions
.into_iter()
.filter_map(|(index, response)| {
if response.solutions.is_empty() {
tracing::debug!(driver = ?self.drivers[index].url, "driver sent zero solutions");
return None;
}

Some(
response
.solutions
.into_iter()
.filter_map(|solution| {
if solution.score == U256::zero() {
tracing::debug!(
id = ?solution.solution_id,
driver = ?self.drivers[index].url,
"driver sent solution with zero score",
);
None
} else {
Some((index, solution))
}
})
.collect_vec(),
)
})
.flatten()
.collect_vec();

if solutions.is_empty() {
tracing::info!(?id, "no solutions for auction");
return;
}

// Shuffle so that sorting randomly splits ties.
solutions.shuffle(&mut rand::thread_rng());
Expand All @@ -98,13 +135,11 @@ impl RunLoop {

// TODO: Keep going with other solutions until some deadline.
if let Some((index, solution)) = solutions.last() {
// The winner has score 0 so all solutions are empty.
if solution.score == 0.into() {
return;
}

tracing::info!(url = %self.drivers[*index].url, "revealing with driver");
let revealed = match self.reveal(id, &self.drivers[*index]).await {
let revealed = match self
.reveal(id, solution.solution_id, &self.drivers[*index])
.await
{
Ok(result) => result,
Err(err) => {
tracing::warn!(?err, "driver {} failed to reveal", self.drivers[*index].url);
Expand Down Expand Up @@ -305,11 +340,7 @@ impl RunLoop {
results
.into_iter()
.filter_map(|(index, result)| match result {
Ok(result) if result.score >= 0.into() => Some((index, result)),
Ok(result) => {
tracing::warn!("bad score {:?}", result.score);
None
}
Ok(result) => Some((index, result)),
Err(err) => {
tracing::warn!(?err, "driver solve error");
None
Expand All @@ -319,8 +350,16 @@ impl RunLoop {
}

/// Ask the winning solver to reveal their solution.
async fn reveal(&self, id: AuctionId, driver: &Driver) -> Result<reveal::Response> {
let response = driver.reveal().await.context("reveal")?;
async fn reveal(
&self,
id: AuctionId,
solution_id: u64,
driver: &Driver,
) -> Result<reveal::Response> {
let response = driver
.reveal(&Request { solution_id })
.await
.context("reveal")?;
ensure!(
response.calldata.internalized.ends_with(&id.to_be_bytes()),
"reveal auction id missmatch"
Expand All @@ -334,24 +373,29 @@ impl RunLoop {
&self,
id: AuctionId,
driver: &Driver,
solve: &solve::Response,
reveal: &reveal::Response,
solved: &solve::Solution,
revealed: &reveal::Response,
) -> Result<()> {
let events = reveal
let events = revealed
.orders
.iter()
.map(|uid| (*uid, OrderEventLabel::Executing))
.collect_vec();
self.database.store_order_events(&events).await;

driver.settle().await.context("settle")?;
driver
.settle(&settle::Request {
solution_id: solved.solution_id,
})
.await
.context("settle")?;
// TODO: React to deadline expiring.
let transaction = self
.wait_for_settlement_transaction(id, solve.submission_address)
.wait_for_settlement_transaction(id, solved.submission_address)
.await
.context("wait for settlement transaction")?;
if let Some(tx) = transaction {
let events = reveal
let events = revealed
.orders
.iter()
.map(|uid| (*uid, OrderEventLabel::Traded))
Expand Down
27 changes: 23 additions & 4 deletions crates/autopilot/src/shadow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ impl RunLoop {
.await
}

/// Computes a driver's solution in the shadow competition.
/// Computes a driver's solutions in the shadow competition.
async fn participate(
&self,
driver: &Driver,
Expand All @@ -194,9 +194,25 @@ impl RunLoop {
.await
.map_err(|_| Error::Timeout)?
.map_err(Error::Solve)?;
let score = NonZeroU256::new(proposed.score).ok_or(Error::ZeroScore)?;
let (score, solution_id, submission_address) = proposed
.solutions
.into_iter()
.max_by_key(|solution| solution.score)
.map(|solution| {
(
solution.score,
solution.solution_id,
solution.submission_address,
)
})
.ok_or(Error::NoSolutions)?;

let score = NonZeroU256::new(score).ok_or(Error::ZeroScore)?;

let revealed = driver.reveal().await.map_err(Error::Reveal)?;
let revealed = driver
.reveal(&reveal::Request { solution_id })
.await
.map_err(Error::Reveal)?;
if !revealed
.calldata
.internalized
Expand All @@ -207,7 +223,7 @@ impl RunLoop {

Ok(Solution {
score,
account: proposed.submission_address,
account: submission_address,
calldata: revealed.calldata,
})
}
Expand Down Expand Up @@ -237,6 +253,8 @@ impl Participant<'_> {
enum Error {
#[error("the solver timed out")]
Timeout,
#[error("driver did not propose any solutions")]
NoSolutions,
#[error("the proposed a 0-score solution")]
ZeroScore,
#[error("the solver's revealed solution does not match the auction")]
Expand All @@ -251,6 +269,7 @@ impl Error {
fn label(&self) -> &str {
match self {
Error::Timeout => "timeout",
Error::NoSolutions => "no solutions",
Error::ZeroScore => "empty",
Error::Mismatch => "mismatch",
Error::Solve(_) => "error",
Expand Down
65 changes: 0 additions & 65 deletions crates/driver/src/infra/api/routes/reveal.rs

This file was deleted.

4 changes: 4 additions & 0 deletions crates/driver/src/infra/api/routes/reveal/dto/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod revealed;
mod solution;

pub use {revealed::Revealed, solution::Solution};
Loading

0 comments on commit 3b92512

Please sign in to comment.