Skip to content

Propose multiple winning solutions in the driver#4267

Open
fafk wants to merge 6 commits intomainfrom
many-winners-driver
Open

Propose multiple winning solutions in the driver#4267
fafk wants to merge 6 commits intomainfrom
many-winners-driver

Conversation

@fafk
Copy link
Copy Markdown
Contributor

@fafk fafk commented Mar 17, 2026

Description

The driver currently proposes only the single highest-scoring solution to the autopilot. With EIP-7702 parallel submission in place, the autopilot's combinatorial auction can now benefit from receiving all valid solutions from a driver to find the optimal set of winners.

Changes

[x] Driver's solve() now returns Vec instead of Option with all valid solutions sorted best-first
[x] Block re-simulation loop now monitors all proposed solutions individually, voiding only those that revert
[x] New per-solver config flag propose-all-solutions (default: false) keeps existing behavior until EIP-7702 infrastructure is ready

How to test

cargo nextest run -p driver --test-threads 1 --run-ignored ignored-only -E 'test(multiple_solutions)'         

To enable in production, add to the solver config: propose-all-solutions = true (requires submission-accounts to also be configured along with the forwarder contract).

@fafk fafk requested a review from a team as a code owner March 17, 2026 14:11
@fafk fafk changed the title Many winners driver Propose multiple winning solutions in the driver Mar 17, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for multiple solution proposals from the driver, which is a key feature for enabling EIP-7702 parallel submissions. The changes to Competition::solve to handle multiple solutions, including sorting, caching, and re-simulation, are well-implemented. The new configuration flag propose-all-solutions and associated validation are also correctly added. I've found one issue related to configuration validation for EIP-7702 setup that should be addressed to prevent runtime failures from misconfigurations.

Comment thread crates/driver/src/infra/solver/eip7702.rs Outdated
@fafk fafk requested a review from MartinquaXD March 17, 2026 14:28
@github-actions
Copy link
Copy Markdown

This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed.

@github-actions github-actions bot added the stale label Mar 25, 2026
Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment on lines +537 to +539
for (_, settlement) in &scored {
lock.push_front(settlement.clone());
}
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 the following would be faster:

  • keeping scored as a vecdeque from the start
  • extending scored with settlements
  • replacing settlements with scored

Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/infra/config/file/mod.rs Outdated
Comment thread crates/driver/src/tests/setup/mod.rs Outdated
@github-actions github-actions bot removed the stale label Mar 26, 2026
for (_, settlement) in &scored {
lock.push_front(settlement.clone());
}
const MAX_SOLUTION_STORAGE: usize = 5;
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.

But scored can hold more than 5 items, right? Should we truncate scored to MAX_SOLUTION_STORAGE before caching, so what is stored and what is returned are always in sync.

Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/infra/observe/mod.rs Outdated
Comment thread crates/driver/src/tests/cases/multiple_solutions.rs
Comment thread crates/driver/src/tests/cases/multiple_solutions.rs
Comment thread crates/driver/src/tests/setup/driver.rs
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 4, 2026

This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed.

@github-actions github-actions bot added the stale label Apr 4, 2026
@jmg-duarte jmg-duarte removed the stale label Apr 6, 2026
let mut scored: Vec<(Option<Solved>, Settlement)> = scores
.into_iter()
.max_by_key(|(score, _)| score.to_owned())
.sorted_by(|(a, _), (b, _)| b.cmp(a))
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.

Is the reason to switch away from max_by_key to avoid cloning the score? Otherwise I find max_by_key easier to understand by just reading the code.

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.

max_by_key give 1 element (the max), but now we actually want a vector of N best solutions.

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.

Ahh, right. Then sorted_by_key would be easier to read, no?

            .sorted_by_key(|(score, _)| std::cmp::Reverse(*score))

Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/infra/config/file/mod.rs Outdated
{
let mut lock = self.settlements.lock().unwrap();
for (_, settlement) in &scored {
lock.push_front(settlement.clone());
}
const MAX_SOLUTION_STORAGE: usize = 5;
lock.truncate(MAX_SOLUTION_STORAGE);
lock.truncate(max_to_propose * 5);
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.

nit: I think a name for this magic number would be helpful. Maybe MAX_CONCURRENT_AUCTIONS?

let active: Vec<_> = scored_ref
.iter()
.filter(|(solved, _)| !voided_ref.contains(&solved.id.get()))
.collect();
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.

You don't have to .collect() here for join_all to work.

Comment on lines +569 to +572
self.settlements
.lock()
.unwrap()
.retain(|s| s.solution().get() != solution_id);
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.

Feels a bit weird to lock settlements multiple times in a loop after all the solutions already got invalidated.
WDYT about doing most of this error handling in the future itself and only return Option<SolutionId> from the future. Then you can add all of them to the voided_ref at once and lock self.settlements as the simulation results trickle in.

                    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());

/// discarded and only valid ones are returned.
#[tokio::test]
#[ignore]
async fn all_proposed_with_invalid() {
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.

nit: awkward name - why not only_proposes_valid_solutions?

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.

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

let has_haircut = settlement.has_haircut();
let voided_ref = &mut voided;
let scored_ref = &scored;
let simulate_on_new_blocks = async move {
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.

would it make sense to extract this into a function? the nesting level gets deep and it would become easier to read

Comment on lines +546 to +549
let active: Vec<_> = scored_ref
.iter()
.filter(|(solved, _)| !voided_ref.contains(&solved.id.get()))
.collect();
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.

Couldn't you permanently remove solutions from scored to avoid this?
Or can it be that a solution that fails on block N, can pass again on N+1?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants