|
1 | 1 | pub use winner_selection::Unscored; |
2 | 2 | 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}, |
5 | 6 | winner_selection::{ |
6 | 7 | self as winsel, |
7 | 8 | OrderUid, |
8 | | - state::{self, HasState, RankedItem, ScoredItem, UnscoredItem}, |
| 9 | + state::{HasState, RankedItem, ScoredItem, UnscoredItem}, |
9 | 10 | }, |
10 | 11 | }; |
11 | 12 |
|
@@ -37,30 +38,69 @@ impl SolverArbitrator { |
37 | 38 | &self, |
38 | 39 | bids: Vec<Bid<Unscored>>, |
39 | 40 | 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, |
40 | 53 | ) -> Vec<Bid> { |
41 | 54 | let paired = bids |
42 | 55 | .into_iter() |
43 | 56 | .map(|bid| { |
44 | | - let solution: winsel::Solution<winsel::Unscored> = bid.solution().into(); |
| 57 | + let solution: winsel::Solution<winsel::Unscored> = bid.payload().into(); |
45 | 58 | (bid, solution) |
46 | 59 | }) |
47 | 60 | .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"); |
49 | 77 |
|
50 | | - ws_ranking |
| 78 | + rejoined |
51 | 79 | .ranked |
52 | 80 | .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)| { |
57 | 82 | bid.with_score(Score(eth::Ether(ws_solution.score()))) |
58 | 83 | .with_rank(ws_solution.state().rank_type) |
59 | 84 | }) |
60 | 85 | .collect() |
61 | 86 | } |
62 | 87 | } |
63 | 88 |
|
| 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 | + |
64 | 104 | impl From<&crate::domain::competition::Auction> for winsel::AuctionContext { |
65 | 105 | fn from(auction: &crate::domain::competition::Auction) -> Self { |
66 | 106 | Self { |
@@ -156,53 +196,7 @@ impl From<&crate::infra::api::routes::solve::dto::solve_response::Solution> |
156 | 196 | pub type Scored = winsel::state::Scored<Score>; |
157 | 197 | pub type Ranked = winsel::state::Ranked<Score>; |
158 | 198 |
|
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