Skip to content

Commit 3b8b773

Browse files
committed
wip: adds Flashbots provider
- adds a FlashbotsProvider for interacting with Flashbots API - adds smoke tests for sim and send bundle Flashbots endpoints - adds a make target for running the Flashbots integration tests
1 parent 3f6d723 commit 3b8b773

File tree

9 files changed

+345
-2
lines changed

9 files changed

+345
-2
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ integration = []
2323
[dependencies]
2424
init4-bin-base = { version = "0.10.1", features = ["perms"] }
2525

26+
signet-bundle = { version = "0.9.0" }
2627
signet-constants = { version = "0.9.0" }
2728
signet-sim = { version = "0.9.0" }
2829
signet-tx-cache = { version = "0.9.0" }
@@ -39,8 +40,9 @@ alloy = { version = "1.0.19", features = [
3940
"rlp",
4041
"node-bindings",
4142
"serde",
42-
"getrandom"
43+
"getrandom",
4344
] }
45+
alloy-transport = "1.0.19"
4446

4547
serde = { version = "1.0.197", features = ["derive"] }
4648

@@ -55,3 +57,4 @@ chrono = "0.4.40"
5557
tokio-stream = "0.1.17"
5658
url = "2.5.4"
5759
tracing = "0.1.41"
60+
async-trait = "0.1.88"

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,8 @@ fmt:
4343
clippy:
4444
$(CARGO) clippy $(CLIPPY_FLAGS)
4545

46+
test-flashbots:
47+
$(CARGO) test --test flashbots_provider_test -- --ignored
48+
4649
tx-submitter:
4750
$(CARGO) run --bin transaction-submitter

src/tasks/submit/flashbots/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Signet's Flashbots Block Submitter
2+
3+
/// handles rollup block submission to the Flashbots network
4+
/// via the provider
5+
pub mod submitter;
6+
pub use submitter::FlashbotsSubmitter;
7+
8+
/// implements a bundle provider API for building Flashbots
9+
/// compatible MEV bundles
10+
pub mod provider;
11+
pub use provider::FlashbotsProvider;
12+
13+
/// handles the lifecyle of receiving, preparing, and submitting
14+
/// a rollup block to the Flashbots network.
15+
pub mod task;
16+
pub use task::FlashbotsTask;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//! A generic Flashbots bundle API wrapper.
2+
use alloy::network::{Ethereum, Network};
3+
use alloy::primitives::Address;
4+
use alloy::providers::{
5+
Provider, SendableTx,
6+
fillers::{FillerControlFlow, TxFiller},
7+
};
8+
use alloy::rpc::types::eth::TransactionRequest;
9+
use alloy::rpc::types::mev::{EthBundleHash, MevSendBundle, ProtocolVersion};
10+
use alloy_transport::TransportResult;
11+
use eyre::Context as _;
12+
use std::ops::Deref;
13+
14+
use crate::tasks::block::sim::SimResult;
15+
use signet_types::SignedFill;
16+
17+
/// A wrapper over a `Provider` that adds Flashbots MEV bundle helpers.
18+
#[derive(Debug, Clone)]
19+
pub struct FlashbotsProvider<P> {
20+
inner: P,
21+
/// The base URL for the Flashbots API.
22+
pub relay_url: url::Url,
23+
}
24+
25+
impl<P: Provider<Ethereum>> FlashbotsProvider<P> {
26+
/// Wraps a provider with the URL and returns a new `FlashbotsProvider`.
27+
pub fn new(inner: P, relay_url: url::Url) -> Self {
28+
Self { inner, relay_url }
29+
}
30+
31+
/// Consume self and return the inner provider.
32+
pub fn into_inner(self) -> P {
33+
self.inner
34+
}
35+
36+
/// Borrow the inner provider.
37+
pub const fn inner(&self) -> &P {
38+
&self.inner
39+
}
40+
}
41+
42+
impl<P> Deref for FlashbotsProvider<P> {
43+
type Target = P;
44+
fn deref(&self) -> &Self::Target {
45+
&self.inner
46+
}
47+
}
48+
49+
impl<P> FlashbotsProvider<P>
50+
where
51+
P: Provider<Ethereum> + Clone + Send + Sync + 'static,
52+
{
53+
/// Convert a SignedFill to a TransactionRequest calling the Orders contract.
54+
///
55+
/// This prepares the calldata for RollupOrders::fillPermit2(outputs, permit2) and sets
56+
/// `to` to the given Orders contract address. The returned request is unsigned.
57+
pub fn fill_to_tx_request(fill: &SignedFill, orders_contract: Address) -> TransactionRequest {
58+
fill.to_fill_tx(orders_contract)
59+
}
60+
61+
/// Construct a new empty bundle template for the given block number.
62+
pub fn empty_bundle(&self, target_block: u64) -> MevSendBundle {
63+
MevSendBundle::new(target_block, Some(target_block), ProtocolVersion::V0_1, vec![])
64+
}
65+
66+
/// Prepares a bundle transaction from the simulation result.
67+
pub fn prepare_bundle(&self, sim_result: &SimResult, target_block: u64) -> MevSendBundle {
68+
let bundle_body = Vec::new();
69+
70+
// Populate the bundle body with the simulation result.
71+
72+
// TODO: Push host fills into the Flashbots bundle body.
73+
let _host_fills = sim_result.block.host_fills();
74+
// _host_fills.iter().map(|f| f.to_fill_tx(todo!()));
75+
76+
// TODO: Add the rollup block blob transaction to the Flashbots bundle body.
77+
// let blob_tx = ...;
78+
let _ = &sim_result; // keep param used until wired
79+
80+
// Create the bundle from the target block and bundle body
81+
MevSendBundle::new(target_block, Some(target_block), ProtocolVersion::V0_1, bundle_body)
82+
}
83+
84+
/// Submit the prepared Flashbots bundle to the relay via `mev_sendBundle`.
85+
pub async fn send_bundle(&self, bundle: MevSendBundle) -> eyre::Result<EthBundleHash> {
86+
// NOTE: The Flashbots relay expects a single parameter which is the bundle object.
87+
// Alloy's `raw_request` accepts any serializable params; wrapping in a 1-tuple is fine.
88+
let hash: EthBundleHash = self
89+
.inner
90+
.raw_request("mev_sendBundle".into(), (bundle,))
91+
.await
92+
.wrap_err("flashbots mev_sendBundle RPC failed")?;
93+
Ok(hash)
94+
}
95+
96+
/// Simulate a bundle via `mev_simBundle`.
97+
pub async fn simulate_bundle(&self, bundle: &MevSendBundle) -> eyre::Result<()> {
98+
// We ignore the response (likely a JSON object with sim traces) for now and just ensure success.
99+
let _resp: serde_json::Value = self
100+
.inner
101+
.raw_request("mev_simBundle".into(), (bundle.clone(),))
102+
.await
103+
.wrap_err("flashbots mev_simBundle RPC failed")?;
104+
Ok(())
105+
}
106+
107+
/// Check the status of a previously submitted bundle.
108+
pub async fn bundle_status(&self, _hash: EthBundleHash) -> eyre::Result<()> {
109+
eyre::bail!("FlashbotsProvider::bundle_status unimplemented")
110+
}
111+
}
112+
113+
impl<N, P> TxFiller<N> for FlashbotsProvider<P>
114+
where
115+
N: Network,
116+
P: TxFiller<N> + Provider<N> + Clone + Send + Sync + core::fmt::Debug + 'static,
117+
{
118+
type Fillable = <P as TxFiller<N>>::Fillable;
119+
120+
fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow {
121+
TxFiller::<N>::status(&self.inner, tx)
122+
}
123+
124+
fn fill_sync(&self, tx: &mut SendableTx<N>) {
125+
TxFiller::<N>::fill_sync(&self.inner, tx)
126+
}
127+
128+
fn prepare<Prov: Provider<N>>(
129+
&self,
130+
provider: &Prov,
131+
tx: &N::TransactionRequest,
132+
) -> impl core::future::Future<Output = TransportResult<Self::Fillable>> + Send {
133+
TxFiller::<N>::prepare(&self.inner, provider, tx)
134+
}
135+
136+
fn fill(
137+
&self,
138+
fillable: Self::Fillable,
139+
tx: SendableTx<N>,
140+
) -> impl core::future::Future<Output = TransportResult<SendableTx<N>>> + Send {
141+
TxFiller::<N>::fill(&self.inner, fillable, tx)
142+
}
143+
}

src/tasks/submit/flashbots/task.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! Flashbots Task receives simulated blocks from an upstream channel and
2+
//! submits them to the Flashbots relay as bundles.
3+
use crate::{
4+
quincey::Quincey,
5+
tasks::{block::sim::SimResult, submit::flashbots::FlashbotsProvider},
6+
};
7+
use alloy::{network::Ethereum, primitives::TxHash, providers::Provider};
8+
use init4_bin_base::deps::tracing::{debug, error};
9+
use signet_constants::SignetSystemConstants;
10+
use tokio::{sync::mpsc, task::JoinHandle};
11+
12+
/// Handles construction, simulation, and submission of rollup blocks to the
13+
/// Flashbots network.
14+
#[derive(Debug)]
15+
pub struct FlashbotsTask<P> {
16+
/// Builder configuration for the task.
17+
pub config: crate::config::BuilderConfig,
18+
/// System constants for the rollup and host chain.
19+
pub constants: SignetSystemConstants,
20+
/// Quincey instance for block signing.
21+
pub quincey: Quincey,
22+
/// Provider for interacting with Flashbots API.
23+
pub provider: P,
24+
/// Channel for sending hashes of outbound transactions.
25+
pub outbound: mpsc::UnboundedSender<TxHash>,
26+
}
27+
28+
impl<P> FlashbotsTask<P>
29+
where
30+
P: Provider<Ethereum> + Clone + Send + Sync + 'static,
31+
{
32+
/// Returns a new FlashbotsTask instance that receives `SimResult` types from the given
33+
/// channel and handles their preparation, submission to the Flashbots network.
34+
pub fn new(
35+
config: crate::config::BuilderConfig,
36+
constants: SignetSystemConstants,
37+
quincey: Quincey,
38+
provider: P,
39+
outbound: mpsc::UnboundedSender<TxHash>,
40+
) -> FlashbotsTask<P> {
41+
Self { config, constants, quincey, provider, outbound }
42+
}
43+
44+
/// Task future that runs the Flashbots submission loop.
45+
async fn task_future(self, mut inbound: mpsc::UnboundedReceiver<SimResult>) {
46+
debug!("starting flashbots task");
47+
48+
// Wrap the underlying host provider with Flashbots helpers.
49+
let flashbots =
50+
FlashbotsProvider::new(self.provider, self.config.flashbots_endpoint.into());
51+
52+
loop {
53+
let Some(sim_result) = inbound.recv().await else {
54+
debug!("upstream task gone - exiting flashbots task");
55+
break;
56+
};
57+
debug!(?sim_result.env.block_env.number, "simulation block received");
58+
59+
let current_block: u64 = sim_result.env.block_env.number.to::<u64>();
60+
let target_block = current_block + 1;
61+
62+
// TODO: populate and prepare bundle from `SimResult` once `BundleProvider` is wired.
63+
let bundle = flashbots.prepare_bundle(&sim_result, target_block);
64+
65+
// simulate then send the bundle
66+
let sim_bundle_result = flashbots.simulate_bundle(&bundle).await;
67+
match sim_bundle_result {
68+
Ok(_) => {
69+
debug!("bundle simulation successful, ready to send");
70+
match flashbots.send_bundle(bundle).await {
71+
Ok(bundle_hash) => {
72+
debug!(?bundle_hash, "bundle successfully sent to Flashbots");
73+
}
74+
Err(err) => {
75+
error!(?err, "failed to send bundle to Flashbots");
76+
continue;
77+
}
78+
}
79+
}
80+
Err(err) => {
81+
error!(?err, "bundle simulation failed");
82+
continue;
83+
}
84+
}
85+
}
86+
}
87+
88+
/// Spawns the Flashbots task that handles incoming `SimResult`s.
89+
pub fn spawn(self) -> (mpsc::UnboundedSender<SimResult>, JoinHandle<()>) {
90+
let (sender, inbound) = mpsc::unbounded_channel::<SimResult>();
91+
let handle = tokio::spawn(self.task_future(inbound));
92+
(sender, handle)
93+
}
94+
}

src/tasks/submit/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ pub use prep::{Bumpable, SubmitPrep};
44
mod sim_err;
55
pub use sim_err::{SimErrorResp, SimRevertKind};
66

7-
mod builder_helper;
7+
pub mod builder_helper;
88
pub use builder_helper::{BuilderHelperTask, ControlFlow};
9+
10+
pub mod flashbots;
11+
pub use flashbots::{FlashbotsSubmitter, FlashbotsTask};

src/tasks/submit/task.rs

Whitespace-only changes.

tests/flashbots_provider_test.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Integration tests for the FlashbotsProvider.
2+
//! These tests require the `FLASHBOTS_ENDPOINT` env var to be set.
3+
#[cfg(test)]
4+
mod tests {
5+
use alloy::{network::Ethereum, providers::ProviderBuilder};
6+
use builder::tasks::submit::flashbots::FlashbotsProvider;
7+
8+
#[cfg(test)]
9+
mod tests {
10+
use super::*;
11+
use alloy::{network::Ethereum, providers::ProviderBuilder};
12+
13+
#[tokio::test]
14+
#[ignore = "integration test"]
15+
async fn smoke_root_provider() {
16+
let url: url::Url = "http://localhost:9062/".parse().unwrap();
17+
let root = alloy::providers::RootProvider::<Ethereum>::new_http(url.clone());
18+
let fill = ProviderBuilder::new_with_network().connect_provider(root);
19+
let fp = FlashbotsProvider::new(fill, url);
20+
21+
assert_eq!(fp.relay_url.as_str(), "http://localhost:9062/");
22+
23+
let bundle = fp.empty_bundle(1);
24+
let dbg_bundle = format!("{:?}", bundle);
25+
assert!(dbg_bundle.contains("1"));
26+
}
27+
28+
#[tokio::test]
29+
#[ignore = "integration test"]
30+
async fn smoke_simulate_bundle() {
31+
let Ok(endpoint) = std::env::var("FLASHBOTS_ENDPOINT") else {
32+
eprintln!("skipping: set FLASHBOTS_ENDPOINT to run integration test");
33+
return;
34+
};
35+
let url: url::Url = endpoint.parse().expect("invalid FLASHBOTS_ENDPOINT");
36+
let root = alloy::providers::RootProvider::<Ethereum>::new_http(url.clone());
37+
let provider = ProviderBuilder::new_with_network().connect_provider(root);
38+
let fp = FlashbotsProvider::new(provider, url);
39+
40+
let bundle = fp.empty_bundle(1);
41+
let res = fp.simulate_bundle(&bundle).await;
42+
43+
if let Err(err) = &res {
44+
eprintln!("simulate error (expected for empty bundle): {err}");
45+
}
46+
47+
if let Err(err) = &res {
48+
let msg = format!("{err}");
49+
assert!(msg.contains("mev_simBundle"));
50+
}
51+
52+
assert!(res.is_ok() || res.is_err());
53+
}
54+
55+
#[tokio::test]
56+
#[ignore = "integration test"]
57+
async fn smoke_send_bundle() {
58+
let Ok(endpoint) = std::env::var("FLASHBOTS_ENDPOINT") else {
59+
eprintln!("skipping: set FLASHBOTS_ENDPOINT to run integration test");
60+
return;
61+
};
62+
let url: url::Url = endpoint.parse().expect("invalid FLASHBOTS_ENDPOINT");
63+
let root = alloy::providers::RootProvider::<Ethereum>::new_http(url.clone());
64+
let provider = ProviderBuilder::new_with_network().connect_provider(root);
65+
let fp = FlashbotsProvider::new(provider, url);
66+
67+
let bundle = fp.empty_bundle(1);
68+
let res = fp.send_bundle(bundle).await;
69+
70+
if let Err(err) = &res {
71+
let msg = format!("{err}");
72+
assert!(msg.contains("mev_sendBundle"));
73+
}
74+
75+
assert!(res.is_ok() || res.is_err());
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)