Skip to content

Commit 61e1846

Browse files
committed
refactor(competition): introduce BidPayload for auction bids and update related structures
1 parent 80d6b8f commit 61e1846

15 files changed

Lines changed: 455 additions & 248 deletions

File tree

crates/autopilot/src/domain/competition/bid.rs

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,20 @@ use {
99
pub type Scored = state::Scored<Score>;
1010
pub type Ranked = state::Ranked<Score>;
1111

12-
/// A solver's auction bid, which includes solution and corresponding driver
13-
/// data, progressing through the winner selection process.
14-
///
15-
/// It uses the type-state pattern to enforce correct state
16-
/// transitions at compile time. The state parameter tracks progression through
17-
/// three phases:
18-
///
19-
/// 1. **Unscored**: Initial state when the solution is received from the driver
20-
/// 2. **Scored**: After computing surplus and fees for the solution
21-
/// 3. **Ranked**: After winner selection determines if this is a winner
12+
/// Payload carried by [`Bid`]: the solution plus its originating driver.
13+
/// Accessible directly through the bid via [`winner_selection::Bid`]'s
14+
/// `Deref` impl, so `bid.solution()` and `bid.driver()` keep working.
2215
#[derive(Clone)]
23-
pub struct Bid<State = Ranked> {
16+
pub struct BidPayload {
2417
solution: Solution,
2518
driver: Arc<infra::Driver>,
26-
state: State,
2719
}
2820

29-
impl<T> Bid<T> {
21+
impl BidPayload {
22+
pub fn new(solution: Solution, driver: Arc<infra::Driver>) -> Self {
23+
Self { solution, driver }
24+
}
25+
3026
pub fn solution(&self) -> &Solution {
3127
&self.solution
3228
}
@@ -36,29 +32,7 @@ impl<T> Bid<T> {
3632
}
3733
}
3834

39-
impl<State> state::HasState for Bid<State> {
40-
type Next<NewState> = Bid<NewState>;
41-
type State = State;
42-
43-
fn with_state<NewState>(self, state: NewState) -> Self::Next<NewState> {
44-
Bid {
45-
solution: self.solution,
46-
driver: self.driver,
47-
state,
48-
}
49-
}
50-
51-
fn state(&self) -> &Self::State {
52-
&self.state
53-
}
54-
}
55-
56-
impl Bid<Unscored> {
57-
pub fn new(solution: Solution, driver: Arc<infra::Driver>) -> Self {
58-
Self {
59-
solution,
60-
driver,
61-
state: Unscored,
62-
}
63-
}
64-
}
35+
/// A solver's auction bid in the typestate pipeline `Unscored -> Scored ->
36+
/// Ranked`. State transitions are enforced at compile time via
37+
/// [`winner_selection::Bid`].
38+
pub type Bid<State = Ranked> = ::winner_selection::Bid<BidPayload, State>;

crates/autopilot/src/domain/competition/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use {
1111
mod bid;
1212
pub mod winner_selection;
1313

14-
pub use bid::{Bid, RankType, Ranked, Scored, Unscored};
14+
pub use bid::{Bid, BidPayload, RankType, Ranked, Scored, Unscored};
1515

1616
type SolutionId = u64;
1717

crates/autopilot/src/domain/competition/winner_selection.rs

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use {
3131
competition::{Bid, RankType, Ranked, Score, Solution, TradedOrder, Unscored},
3232
fee,
3333
},
34+
::observe::metrics,
3435
::winner_selection::state::{HasState, RankedItem, ScoredItem, UnscoredItem},
3536
eth_domain_types::{self as eth, Address, WrappedNativeToken},
3637
std::collections::HashMap,
@@ -66,34 +67,39 @@ impl Arbitrator {
6667
(bid, solution)
6768
})
6869
.collect();
69-
let (ws_ranking, mut by_key) = self.0.arbitrate_paired(paired, &auction.into());
70+
let rejoined = self.0.arbitrate_paired_and_rejoin(paired, &auction.into());
71+
72+
// An orphan means two input bids shared a `SolutionKey`. Autopilot
73+
// runs one process per chain; panicking here takes auctioning down
74+
// until restart. Warn and bump the counter so oncall can alert.
75+
if rejoined.orphans > 0 {
76+
tracing::warn!(
77+
orphans = rejoined.orphans,
78+
"ranked solutions had no matching bid; SolutionKey collision suspected",
79+
);
80+
Metrics::get()
81+
.orphan_solutions
82+
.inc_by(rejoined.orphans as u64);
83+
}
84+
debug_assert!(rejoined.orphans == 0, "expected no orphans");
7085

71-
let reference_scores = self
72-
.0
73-
.compute_reference_scores(&ws_ranking)
86+
let reference_scores = rejoined
87+
.reference_scores
7488
.into_iter()
7589
.map(|(solver, score)| (solver, Score(eth::Ether(score))))
7690
.collect();
77-
78-
let filtered_out = ws_ranking
91+
let filtered_out = rejoined
7992
.filtered_out
8093
.into_iter()
81-
.map(|ws_solution| {
82-
let bid = by_key
83-
.remove(&winsel::SolutionKey::from(&ws_solution))
84-
.expect("every filtered-out solution has a matching bid");
94+
.map(|(bid, ws_solution)| {
8595
bid.with_score(Score(eth::Ether(ws_solution.score())))
8696
.with_rank(RankType::FilteredOut)
8797
})
8898
.collect();
89-
90-
let ranked = ws_ranking
99+
let ranked = rejoined
91100
.ranked
92101
.into_iter()
93-
.map(|ws_solution| {
94-
let bid = by_key
95-
.remove(&winsel::SolutionKey::from(&ws_solution))
96-
.expect("every ranked solution has a matching bid");
102+
.map(|(bid, ws_solution)| {
97103
bid.with_score(Score(eth::Ether(ws_solution.score())))
98104
.with_rank(ws_solution.state().rank_type)
99105
})
@@ -107,6 +113,21 @@ impl Arbitrator {
107113
}
108114
}
109115

116+
#[derive(prometheus_metric_storage::MetricStorage)]
117+
#[metric(subsystem = "winner_selection")]
118+
struct Metrics {
119+
/// Arbitrator-returned solutions whose `SolutionKey` had no matching
120+
/// bid in the rejoin step. Non-zero indicates a `SolutionKey` collision
121+
/// in the input set or an arbitrator invariant violation.
122+
orphan_solutions: prometheus::IntCounter,
123+
}
124+
125+
impl Metrics {
126+
fn get() -> &'static Self {
127+
Metrics::instance(metrics::get_storage_registry()).unwrap()
128+
}
129+
}
130+
110131
impl From<&domain::Auction> for winsel::AuctionContext {
111132
fn from(auction: &domain::Auction) -> Self {
112133
Self {
@@ -260,7 +281,7 @@ mod tests {
260281
Price,
261282
order::{self, AppDataHash},
262283
},
263-
competition::{Bid, Solution, TradedOrder, Unscored},
284+
competition::{Bid, BidPayload, Solution, TradedOrder, Unscored},
264285
},
265286
infra::Driver,
266287
},
@@ -1194,7 +1215,7 @@ mod tests {
11941215
.await
11951216
.unwrap();
11961217

1197-
Bid::new(solution, std::sync::Arc::new(driver))
1218+
Bid::new(BidPayload::new(solution, std::sync::Arc::new(driver)))
11981219
}
11991220

12001221
fn amount(value: u128) -> String {

crates/autopilot/src/run_loop.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,10 @@ impl RunLoop {
643643
.filter_map(|solution| match solution {
644644
Ok(solution) => {
645645
Metrics::solution_ok(&driver);
646-
Some(competition::Bid::new(solution, driver.clone()))
646+
Some(competition::Bid::new(competition::BidPayload::new(
647+
solution,
648+
driver.clone(),
649+
)))
647650
}
648651
Err(err) => {
649652
Metrics::solution_err(&driver, &err);

crates/autopilot/src/shadow.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use {
1111
crate::{
1212
domain::{
1313
self,
14-
competition::{Bid, Score, Unscored, winner_selection},
14+
competition::{Bid, BidPayload, Score, Unscored, winner_selection},
1515
},
1616
infra::{
1717
self,
@@ -259,7 +259,7 @@ impl RunLoop {
259259

260260
solutions
261261
.into_iter()
262-
.map(|s| Bid::new(s, Arc::clone(&driver)))
262+
.map(|s| Bid::new(BidPayload::new(s, Arc::clone(&driver))))
263263
.collect()
264264
}
265265

crates/driver/src/domain/competition/mod.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -562,18 +562,14 @@ impl Competition {
562562
lock.truncate(max_to_propose * MAX_CONCURRENT_AUCTIONS);
563563
}
564564

565-
// Shadow-mode submission to the pod network. Failures must never affect
566-
// the response we return to the autopilot.
567-
if let (Some(pm), Some(auction_id), Some((best, _))) =
568-
(self.solver.pod_manager(), auction.id, scored.first())
569-
{
570-
pm.spawn(
571-
auction_id,
572-
auction.clone(),
573-
deadline,
574-
best.clone(),
575-
self.solver.clone(),
576-
);
565+
// Shadow-mode submission to pod. Failures and cost must not affect
566+
// the response we return to autopilot. `spawn` does its serialization
567+
// synchronously from borrowed inputs, so no deep `Auction` clone
568+
// happens here. Send the full scored vec so pod and autopilot rank
569+
// the same set.
570+
if let (Some(pm), Some(auction_id)) = (self.solver.pod_manager(), auction.id) {
571+
let solveds: Vec<Solved> = scored.iter().map(|(s, _)| s).cloned().collect();
572+
pm.spawn(auction_id, deadline, solveds, auction, self.solver.clone());
577573
}
578574

579575
// Re-simulate the solution on every new block until the deadline ends to make

crates/driver/src/domain/competition/solver_winner_selection.rs

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
pub use winner_selection::Unscored;
22
use {
3-
crate::domain::competition::order::FeePolicy,
4-
eth_domain_types::{self as eth, Address, Ether, WrappedNativeToken},
3+
crate::{domain::competition::order::FeePolicy, infra::api::routes::solve::dto},
4+
::observe::metrics,
5+
eth_domain_types::{self as eth, Ether, WrappedNativeToken},
56
winner_selection::{
67
self as winsel,
78
OrderUid,
8-
state::{self, HasState, RankedItem, ScoredItem, UnscoredItem},
9+
state::{HasState, RankedItem, ScoredItem, UnscoredItem},
910
},
1011
};
1112

@@ -37,30 +38,69 @@ impl SolverArbitrator {
3738
&self,
3839
bids: Vec<Bid<Unscored>>,
3940
auction: &crate::domain::competition::Auction,
41+
) -> Vec<Bid> {
42+
self.arbitrate_with_context(bids, &auction.into())
43+
}
44+
45+
/// Same as [`Self::arbitrate`] but takes a precomputed
46+
/// [`winsel::AuctionContext`]. Use this when the caller has already
47+
/// built the context in the foreground, to avoid the per-call
48+
/// conversion from `Auction`.
49+
pub fn arbitrate_with_context(
50+
&self,
51+
bids: Vec<Bid<Unscored>>,
52+
context: &winsel::AuctionContext,
4053
) -> Vec<Bid> {
4154
let paired = bids
4255
.into_iter()
4356
.map(|bid| {
44-
let solution: winsel::Solution<winsel::Unscored> = bid.solution().into();
57+
let solution: winsel::Solution<winsel::Unscored> = bid.payload().into();
4558
(bid, solution)
4659
})
4760
.collect();
48-
let (ws_ranking, mut by_key) = self.0.arbitrate_paired(paired, &auction.into());
61+
let rejoined = self.0.arbitrate_paired_and_rejoin(paired, context);
62+
63+
// An orphan means two input bids shared a `SolutionKey`. Pod is
64+
// open so untrusted input can engineer such collisions. Don't panic
65+
// (it would kill the spawned pod task); warn and bump the counter
66+
// so oncall can alert.
67+
if rejoined.orphans > 0 {
68+
tracing::warn!(
69+
orphans = rejoined.orphans,
70+
"ranked solutions had no matching bid; SolutionKey collision suspected",
71+
);
72+
Metrics::get()
73+
.orphan_solutions
74+
.inc_by(rejoined.orphans as u64);
75+
}
76+
debug_assert!(rejoined.orphans == 0, "expected no orphans");
4977

50-
ws_ranking
78+
rejoined
5179
.ranked
5280
.into_iter()
53-
.map(|ws_solution| {
54-
let bid = by_key
55-
.remove(&winsel::SolutionKey::from(&ws_solution))
56-
.expect("every ranked solution has a matching bid");
81+
.map(|(bid, ws_solution)| {
5782
bid.with_score(Score(eth::Ether(ws_solution.score())))
5883
.with_rank(ws_solution.state().rank_type)
5984
})
6085
.collect()
6186
}
6287
}
6388

89+
#[derive(prometheus_metric_storage::MetricStorage)]
90+
#[metric(subsystem = "winner_selection")]
91+
struct Metrics {
92+
/// Arbitrator-returned solutions whose `SolutionKey` had no matching
93+
/// bid in the rejoin step. Non-zero indicates a `SolutionKey` collision
94+
/// in the input set or an arbitrator invariant violation.
95+
orphan_solutions: prometheus::IntCounter,
96+
}
97+
98+
impl Metrics {
99+
fn get() -> &'static Self {
100+
Metrics::instance(metrics::get_storage_registry()).unwrap()
101+
}
102+
}
103+
64104
impl From<&crate::domain::competition::Auction> for winsel::AuctionContext {
65105
fn from(auction: &crate::domain::competition::Auction) -> Self {
66106
Self {
@@ -156,53 +196,7 @@ impl From<&crate::infra::api::routes::solve::dto::solve_response::Solution>
156196
pub type Scored = winsel::state::Scored<Score>;
157197
pub type Ranked = winsel::state::Ranked<Score>;
158198

159-
/// A solver's auction bid, which includes solution and corresponding driver
160-
/// data, progressing through the winner selection process.
161-
///
162-
/// It uses the type-state pattern to enforce correct state
163-
/// transitions at compile time. The state parameter tracks progression through
164-
/// three phases:
165-
///
166-
/// 1. **Unscored**: Initial state when the solution is received from the driver
167-
/// 2. **Scored**: After computing surplus and fees for the solution
168-
/// 3. **Ranked**: After winner selection determines if this is a winner
169-
#[derive(Clone)]
170-
pub struct Bid<State = Ranked> {
171-
solution: crate::infra::api::routes::solve::dto::solve_response::Solution,
172-
state: State,
173-
}
174-
175-
impl<T> Bid<T> {
176-
pub fn solution(&self) -> &crate::infra::api::routes::solve::dto::solve_response::Solution {
177-
&self.solution
178-
}
179-
180-
pub fn submission_address(&self) -> &Address {
181-
&self.solution.submission_address
182-
}
183-
}
184-
185-
impl<State> state::HasState for Bid<State> {
186-
type Next<NewState> = Bid<NewState>;
187-
type State = State;
188-
189-
fn with_state<NewState>(self, state: NewState) -> Self::Next<NewState> {
190-
Bid {
191-
solution: self.solution,
192-
state,
193-
}
194-
}
195-
196-
fn state(&self) -> &Self::State {
197-
&self.state
198-
}
199-
}
200-
201-
impl Bid<Unscored> {
202-
pub fn new(solution: crate::infra::api::routes::solve::dto::solve_response::Solution) -> Self {
203-
Self {
204-
solution,
205-
state: Unscored,
206-
}
207-
}
208-
}
199+
/// A solver's auction bid in the typestate pipeline `Unscored -> Scored ->
200+
/// Ranked`. State transitions are enforced at compile time via
201+
/// [`winsel::Bid`].
202+
pub type Bid<State = Ranked> = winsel::Bid<dto::solve_response::Solution, State>;

0 commit comments

Comments
 (0)