Skip to content

feat: fan out bundle submission to multiple MEV relay endpoints#257

Merged
dylanlott merged 1 commit intomainfrom
dylan/fan-out-bundle-submission
Apr 2, 2026
Merged

feat: fan out bundle submission to multiple MEV relay endpoints#257
dylanlott merged 1 commit intomainfrom
dylan/fan-out-bundle-submission

Conversation

@dylanlott
Copy link
Copy Markdown
Contributor

@dylanlott dylanlott commented Apr 1, 2026

Description

This PR adds fan-out bundle submission to multiple MEV relay endpoints.

Replaces the FLASHBOTS_ENDPOINT with a SUBMIT_ENDPOINTS env variable, which is a comma-separated list of URLs.

Related Issue

Closes ENG-2114

Testing

  • make fmt passes
  • make clippy passes
  • make test passes
  • New tests added (if applicable)

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.
Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@dylanlott dylanlott self-assigned this Apr 1, 2026
let signer = self.signer.clone();
let pylon = self.pylon.clone();

tokio::spawn(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +233 to +238
// 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}"));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if we use Vec<url::Url> above we don't need to do this ourselves

Copy link
Copy Markdown
Member

@Evalir Evalir left a comment

Choose a reason for hiding this comment

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

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))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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_failures is 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_successes and relay_failures lose partial results on timeout.
    • relay_submissions is keeping track of successes, not attempts.
    • deadline_missed is 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_BUFFER config but we're hardcoding the timeout to one second. Should we adjust this to actually respect the buffer?

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_relay fn that independently wraps each future with a timeout. This function would be called using a FuturesUnordered to collect all the relay send futures, so they would still have the same timeout, but each result can be processed independently. It could return some RelayOutcome enum or similar we can use for tallying metrics.

Copy link
Copy Markdown
Contributor

@Fraser999 Fraser999 left a comment

Choose a reason for hiding this comment

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

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 {
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.

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)

Copy link
Copy Markdown
Member

@Evalir Evalir left a comment

Choose a reason for hiding this comment

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

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

@Evalir Evalir marked this pull request as ready for review April 2, 2026 02:44
@Evalir Evalir requested a review from prestwich as a code owner April 2, 2026 02:44
@dylanlott dylanlott merged commit 71d1b0e into main Apr 2, 2026
6 checks passed
@dylanlott dylanlott deleted the dylan/fan-out-bundle-submission branch April 2, 2026 22:29
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.

3 participants