-
Notifications
You must be signed in to change notification settings - Fork 161
Propose multiple winning solutions in the driver #4267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3fdb455
7e93ab0
0f06e79
90355b7
ee4422d
73c8c97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -283,7 +283,7 @@ impl Competition { | |
| } | ||
|
|
||
| /// Solve an auction as part of this competition. | ||
| pub async fn solve(&self, request: Request<Body>) -> Result<Option<Solved>, Error> { | ||
| pub async fn solve(&self, request: Request<Body>) -> Result<Vec<Solved>, Error> { | ||
| let start = Instant::now(); | ||
| let timer = ::observe::metrics::metrics() | ||
| .on_auction_overhead_start("driver", "pre_processing_total"); | ||
|
|
@@ -344,7 +344,7 @@ impl Competition { | |
|
|
||
| if auction.orders.is_empty() { | ||
| tracing::info!("no orders left after pre-processing; skipping solving"); | ||
| return Ok(None); | ||
| return Ok(vec![]); | ||
| } | ||
|
|
||
| let auction = &auction; | ||
|
|
@@ -498,59 +498,78 @@ impl Competition { | |
| observe::score(settlement, score); | ||
| } | ||
|
|
||
| // Pick the best-scoring settlement. | ||
| let (mut score, settlement) = scores | ||
| // Build scored settlements sorted best-first. | ||
| let scored: Vec<(Solved, Settlement)> = scores | ||
| .into_iter() | ||
| .max_by_key(|(score, _)| score.to_owned()) | ||
| .sorted_by(|(a, _), (b, _)| b.cmp(a)) | ||
| .map(|(score, settlement)| { | ||
| ( | ||
| Solved { | ||
| id: settlement.solution().clone(), | ||
| score, | ||
| trades: settlement.orders(), | ||
| prices: settlement.prices(), | ||
| gas: Some(settlement.gas.estimate), | ||
| }, | ||
| settlement, | ||
| ) | ||
| let solved = Solved { | ||
| id: settlement.solution().clone(), | ||
| score, | ||
| trades: settlement.orders(), | ||
| prices: settlement.prices(), | ||
| gas: Some(settlement.gas.estimate), | ||
| }; | ||
| (solved, settlement) | ||
| }) | ||
| .unzip(); | ||
| .collect(); | ||
|
|
||
| let Some(settlement) = settlement else { | ||
| // Don't wait for the deadline because we can't produce a solution anyway. | ||
| return Ok(score); | ||
| }; | ||
| let solution_id = settlement.solution().get(); | ||
| if scored.is_empty() { | ||
| return Ok(vec![]); | ||
| } | ||
|
|
||
| let max_to_propose = self.solver.max_solutions_to_propose(); | ||
| let scored: Vec<(Solved, Settlement)> = scored.into_iter().take(max_to_propose).collect(); | ||
|
|
||
| // Cache all settlements so they can be revealed/settled later. | ||
| // Use a multiple of max_to_propose so solutions from previous | ||
| // overlapping auctions survive until their /settle completes. | ||
| { | ||
| let mut lock = self.settlements.lock().unwrap(); | ||
| lock.push_front(settlement.clone()); | ||
|
|
||
| /// Number of solutions that may be cached at most. | ||
| const MAX_SOLUTION_STORAGE: usize = 5; | ||
| lock.truncate(MAX_SOLUTION_STORAGE); | ||
| for (_, settlement) in &scored { | ||
| lock.push_front(settlement.clone()); | ||
| } | ||
|
Comment on lines
+529
to
+531
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i wonder if the following would be faster:
|
||
| lock.truncate(max_to_propose * 5); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think a name for this magic number would be helpful. Maybe |
||
| } | ||
|
|
||
| // Re-simulate the solution on every new block until the deadline ends to make | ||
| // sure we actually submit a working solution close to when the winner | ||
| // gets picked by the procotol. | ||
| // Re-simulate all solutions on every new block until the deadline ends to | ||
| // make sure we only propose solutions that are still working when the | ||
| // winner gets picked by the protocol. | ||
| let mut voided: HashSet<u64> = HashSet::new(); | ||
| if let Ok(remaining) = deadline.remaining() { | ||
| let score_ref = &mut score; | ||
| let has_haircut = settlement.has_haircut(); | ||
| let voided_ref = &mut voided; | ||
| let scored_ref = &scored; | ||
| let simulate_on_new_blocks = async move { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would it make sense to extract this into a function? the nesting level gets deep and it would become easier to read |
||
| let mut stream = | ||
| ethrpc::block_stream::into_stream(self.eth.current_block().clone()); | ||
| while let Some(block) = stream.next().await { | ||
| if let Err(simulator::Error::Revert(err)) = | ||
| self.simulate_settlement(&settlement).await | ||
| { | ||
| let active: Vec<_> = scored_ref | ||
| .iter() | ||
| .filter(|(solved, _)| !voided_ref.contains(&solved.id.get())) | ||
| .collect(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't have to
Comment on lines
+546
to
+549
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't you permanently remove solutions from scored to avoid this? |
||
|
|
||
| let results: Vec<_> = | ||
| futures::future::join_all(active.iter().map(|(solved, settlement)| { | ||
| let solution_id = solved.id.get(); | ||
| async move { | ||
| let result = self.simulate_settlement(settlement).await; | ||
| (solution_id, settlement, result) | ||
| } | ||
| })) | ||
| .await; | ||
|
|
||
| for (solution_id, settlement, result) in results { | ||
| let err = match result { | ||
| Err(simulator::Error::Revert(err)) => err, | ||
| _ => continue, | ||
| }; | ||
| let has_haircut = settlement.has_haircut(); | ||
| observe::winner_voided(self.solver.name(), block, &err, has_haircut); | ||
| *score_ref = None; | ||
| voided_ref.insert(solution_id); | ||
| self.settlements | ||
| .lock() | ||
| .unwrap() | ||
| .retain(|s| s.solution().get() != solution_id); | ||
|
Comment on lines
+569
to
+572
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels a bit weird to lock settlements multiple times in a loop after all the solutions already got invalidated. let voided_solutions =
futures::future::join_all(active.map(|(solved, settlement)| {
let solution_id = solved.id.get();
async move {
let result = self.simulate_settlement(settlement).await;
let err = match result {
Err(simulator::Error::Revert(err)) => err,
_ => return None,
};
let has_haircut = settlement.has_haircut();
observe::winner_voided(self.solver.name(), block, &err, has_haircut);
self.settlements
.lock()
.unwrap()
.retain(|s| s.solution().get() != solution_id);
if !has_haircut {
notify::simulation_failed(
&self.solver,
auction.id(),
settlement.solution(),
&simulator::Error::Revert(err),
true,
);
}
Some(solution_id)
}
}))
.await;
voided_ref.extend(voided_solutions.into_iter().flatten()); |
||
| // Only notify solver if solution doesn't have haircut | ||
| if !has_haircut { | ||
| notify::simulation_failed( | ||
| &self.solver, | ||
|
|
@@ -560,14 +579,20 @@ impl Competition { | |
| true, | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
| if voided_ref.len() == scored_ref.len() { | ||
| return; // all solutions voided, no point waiting for more blocks | ||
| } | ||
| } | ||
| }; | ||
| let _ = tokio::time::timeout(remaining, simulate_on_new_blocks).await; | ||
| } | ||
|
|
||
| Ok(score) | ||
| Ok(scored | ||
| .into_iter() | ||
| .filter(|(solved, _)| !voided.contains(&solved.id.get())) | ||
| .map(|(solved, _)| solved) | ||
| .collect()) | ||
| } | ||
|
|
||
| // Oders already need to be sorted from most relevant to least relevant so that | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Take it or leave it: Cover the error conditions in the function doc too The error message is great though, this would just help when hovering over |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the reason to switch away from
max_by_keyto avoid cloning the score? Otherwise I findmax_by_keyeasier to understand by just reading the code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
max_by_keygive 1 element (the max), but now we actually want a vector of N best solutions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, right. Then
sorted_by_keywould be easier to read, no?