feat: fan out bundle submission to multiple MEV relay endpoints#257
feat: fan out bundle submission to multiple MEV relay endpoints#257
Conversation
Replaces the current single FLASHBOTS_ENDPOINT with SUBMIT_ENDPOINTS, a comma-separated list of MEV relay/builder RPC URLs. The submit task sends each prepared bundle to all configured endpoints concurrently via join_all with a slot-defined deadline, so that a single slow or unresponsive reply doesn't delay the bundle broadcast.
| let signer = self.signer.clone(); | ||
| let pylon = self.pylon.clone(); | ||
|
|
||
| tokio::spawn( |
There was a problem hiding this comment.
this function has become massive now. we need to slim this down. I'd separate the relay fanout and pylon submission to different fns, that we can then join! here on the spawned task. That way they both happen at the same time and we limit complexity here
| infallible | ||
| )] | ||
| pub flashbots_endpoint: url::Url, | ||
| pub submit_endpoints: Vec<String>, |
There was a problem hiding this comment.
| pub submit_endpoints: Vec<String>, | |
| pub submit_endpoints: Vec<url::Url>, |
since we want this to fail at startup, we should just straight up use the type so it fails on parse instead of having to do it manually ourselves below
There was a problem hiding this comment.
Actually, this might not work directly as url::Url won't be directly convertible. I still think a better idea here is to have a newtype that trims and parses, such as
pub struct SubmitEndpoints(pub Vec<url::Url>);which would then trim and parse the urls.
| // Validate that every submit endpoint is a parseable URL. Fail fast | ||
| // on startup rather than at first block submission. | ||
| for raw in &self.submit_endpoints { | ||
| url::Url::parse(raw) | ||
| .unwrap_or_else(|e| panic!("invalid URL in SUBMIT_ENDPOINTS \"{raw}\": {e}")); | ||
| } |
There was a problem hiding this comment.
if we use Vec<url::Url> above we don't need to do this ourselves
Evalir
left a comment
There was a problem hiding this comment.
The README should also be updated to refer to the new endpoint
|
|
||
| let (mut successes, mut failures) = (0u32, 0u32); | ||
|
|
||
| match tokio::time::timeout(deadline_dur, futures_util::future::join_all(futs)) |
There was a problem hiding this comment.
when it comes to metrics accounting, there's a few issues, although I quite like the metrics we're keeping track of:
- The timeout right now is a "hard" timeout, in the sense that it cancels ongoing work. That in itself is fine, but, it causes metrics to not be collected properly.
- When the timeout is reached,
all_relay_failuresis incremented if the entire set of futures did not succeed, which means we could increment this metric even if we've got partial success, when the only case this should happen is if all errored. - as a result of the above,
relay_successesandrelay_failureslose partial results on timeout. relay_submissionsis keeping track of successes, not attempts.deadline_missedis incremented on both timeout and completed-but-late success, so this is an overloaded metric. We need to set in stone what "missing a deadline" means: is early-submission-but-late-acceptance missing a deadline? or does only late submission count? it's ambiguous, so the metrics could be either correct or misleading depending on this.- we have a
SUBMIT_DEADLINE_BUFFERconfig but we're hardcoding the timeout to one second. Should we adjust this to actually respect the buffer?
- When the timeout is reached,
I think switching to a FuturesUnordered processing style instead of a join_all might make this easier to get right, as we can process results as they come, and send off the futures themselves with match tokio::time::timeout_at(deadline, provider.send_bundle...).
If I had to sketch out how this would look, I would roughly:
- make the entire relay fanout flow a separate function
- have a helper
submit_to_relayfn that independently wraps each future with a timeout. This function would be called using aFuturesUnorderedto collect all the relay send futures, so they would still have the same timeout, but each result can be processed independently. It could return someRelayOutcomeenum or similar we can use for tallying metrics.
Fraser999
left a comment
There was a problem hiding this comment.
We still refer to "flashbots" in .claude/CLAUDE.md, and in the doc comment of src/tasks/submit/mod.rs.
| /// Fans out each prepared bundle to all relays concurrently and submits | ||
| /// the blob sidecar to Pylon regardless of relay outcome. | ||
| #[derive(Debug)] | ||
| pub struct FlashbotsTask { |
There was a problem hiding this comment.
Should this be renamed to SubmitTask, and the file to submit.rs? (Or we could move the code here into tasks/submit/mod.rs if we don't like the repetition in tasks/submit/submit.rs)
Evalir
left a comment
There was a problem hiding this comment.
Approving to unblock—this is critical to get in today to get a new builder version running.
We'll have a quick follow up PR which solves the nits presented

Description
This PR adds fan-out bundle submission to multiple MEV relay endpoints.
Replaces the
FLASHBOTS_ENDPOINTwith aSUBMIT_ENDPOINTSenv variable, which is a comma-separated list of URLs.Related Issue
Closes ENG-2114
Testing
make fmtpassesmake clippypassesmake testpasses