From 18e15411cda06db0156974c658a95b426c653053 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 5 Feb 2026 15:10:05 +0100 Subject: [PATCH 1/6] feat(submit): pylon blob preloading integration --- Cargo.lock | 4 +- Cargo.toml | 6 +-- bin/builder.rs | 21 ++++++-- src/config.rs | 14 ++++- src/tasks/submit/flashbots.rs | 97 ++++++++++++++++------------------- src/tasks/submit/mod.rs | 4 ++ src/tasks/submit/prep.rs | 41 ++++++++------- src/tasks/submit/pylon.rs | 66 ++++++++++++++++++++++++ src/test_utils.rs | 1 + 9 files changed, 171 insertions(+), 83 deletions(-) create mode 100644 src/tasks/submit/pylon.rs diff --git a/Cargo.lock b/Cargo.lock index d362d6e4..14cdfaaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4944,9 +4944,7 @@ dependencies = [ [[package]] name = "init4-bin-base" -version = "0.18.0-rc.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300b9df7fddc42d13d9d27d8a3def2c8982b1c02771523117984fe96be127843" +version = "0.18.0-rc.7" dependencies = [ "alloy", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index d471c90d..21c22e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ name = "zenith-builder-example" path = "bin/builder.rs" [dependencies] -init4-bin-base = { version = "0.18.0-rc.0", features = ["perms", "aws"] } +init4-bin-base = { version = "0.18.0-rc.7", features = ["perms", "aws", "pylon"] } signet-constants = { version = "0.16.0-rc.8" } signet-sim = { version = "0.16.0-rc.8" } @@ -63,7 +63,7 @@ alloy-hardforks = "0.4.0" alloy-chains = "0.2" # comment / uncomment for local dev -# [patch.crates-io] +[patch.crates-io] # signet-constants = { path = "../signet-sdk/crates/constants" } # signet-types = { path = "../signet-sdk/crates/types" } # signet-zenith = { path = "../signet-sdk/crates/zenith" } @@ -73,4 +73,4 @@ alloy-chains = "0.2" # signet-journal = { path = "../signet-sdk/crates/journal" } # signet-tx-cache = { path = "../signet-sdk/crates/tx-cache" } # signet-bundle = { path = "../signet-sdk/crates/bundle" } -# init4-bin-base = { path = "../bin-base" } +init4-bin-base = { path = "../bin-base" } diff --git a/bin/builder.rs b/bin/builder.rs index adf26997..1f268ba3 100644 --- a/bin/builder.rs +++ b/bin/builder.rs @@ -1,8 +1,11 @@ use builder::{ service::serve_builder, tasks::{ - block::sim::SimulatorTask, cache::CacheTasks, env::EnvTask, metrics::MetricsTask, - submit::FlashbotsTask, + block::sim::SimulatorTask, + cache::CacheTasks, + env::EnvTask, + metrics::MetricsTask, + submit::{FlashbotsTask, PylonTask}, }, }; use init4_bin_base::deps::tracing::{info, info_span}; @@ -32,10 +35,17 @@ async fn main() -> eyre::Result<()> { let (block_env, env_jh) = env_task.spawn(); let (tx_channel, metrics_jh) = metrics_task.spawn(); + // Set up the pylon task + let pylon_client = builder::config().connect_pylon(); + let pylon_task = PylonTask::new(pylon_client); + let (pylon_sender, pylon_jh) = pylon_task.spawn(); + // Set up the cache, submit, and simulator tasks let cache_tasks = CacheTasks::new(block_env.clone()); - let (submit_task, simulator_task) = - tokio::try_join!(FlashbotsTask::new(tx_channel.clone()), SimulatorTask::new(block_env),)?; + let (submit_task, simulator_task) = tokio::try_join!( + FlashbotsTask::new(tx_channel.clone(), pylon_sender), + SimulatorTask::new(block_env), + )?; // Spawn the cache, submit, and simulator tasks let cache_system = cache_tasks.spawn(); @@ -65,6 +75,9 @@ async fn main() -> eyre::Result<()> { _ = submit_jh => { info!("submit finished"); }, + _ = pylon_jh => { + info!("pylon task finished"); + }, _ = metrics_jh => { info!("metrics finished"); }, diff --git a/src/config.rs b/src/config.rs index 1296ccc0..79c125a5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ use alloy::{ }; use eyre::Result; use init4_bin_base::{ - perms::{Authenticator, OAuthConfig, SharedToken}, + perms::{Authenticator, OAuthConfig, SharedToken, pylon}, utils::{ calc::SlotCalculator, from_env::FromEnv, @@ -25,6 +25,9 @@ use signet_zenith::Zenith; use std::borrow::Cow; use tokio::join; +/// Pylon client type for blob sidecar submission. +pub type PylonClient = pylon::PylonClient; + /// Type alias for the provider used to simulate against rollup state. pub type RuProvider = RootProvider; @@ -168,6 +171,10 @@ pub struct BuilderConfig { /// The signet system constants. pub constants: SignetSystemConstants, + + /// URL for the Pylon blob server API. + #[from_env(var = "PYLON_URL", desc = "URL for the Pylon blob server API")] + pub pylon_url: url::Url, } impl BuilderConfig { @@ -285,4 +292,9 @@ impl BuilderConfig { ((gas_limit as u128 * (self.max_host_gas_coefficient.unwrap_or(80) as u128)) / 100u128) as u64 } + + /// Connect to the Pylon blob server. + pub fn connect_pylon(&self) -> PylonClient { + PylonClient::new(self.pylon_url.clone(), self.oauth_token()) + } } diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index 6b3d998c..12bdfc79 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -7,7 +7,7 @@ use crate::{ }; use alloy::{ consensus::TxEnvelope, - eips::Encodable2718, + eips::{Encodable2718, eip7594::BlobTransactionSidecarEip7594}, primitives::{Bytes, TxHash}, providers::ext::MevApi, rpc::types::mev::EthSendBundle, @@ -32,12 +32,17 @@ pub struct FlashbotsTask { signer: LocalOrAws, /// Channel for sending hashes of outbound transactions. outbound: mpsc::UnboundedSender, + /// Channel for sending sidecars to the Pylon task. + pylon_sender: mpsc::UnboundedSender<(TxHash, BlobTransactionSidecarEip7594)>, } impl FlashbotsTask { /// Returns a new `FlashbotsTask` instance that receives `SimResult` types from the given /// channel and handles their preparation, submission to the Flashbots network. - pub async fn new(outbound: mpsc::UnboundedSender) -> eyre::Result { + pub async fn new( + outbound: mpsc::UnboundedSender, + pylon_sender: mpsc::UnboundedSender<(TxHash, BlobTransactionSidecarEip7594)>, + ) -> eyre::Result { let config = crate::config(); let (quincey, host_provider, flashbots, builder_key) = tokio::try_join!( @@ -49,54 +54,28 @@ impl FlashbotsTask { let zenith = config.connect_zenith(host_provider); - Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound }) + Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound, pylon_sender }) } - /// Prepares a MEV bundle from a simulation result. - /// - /// This function serves as an entry point for bundle preparation and is left - /// for forward compatibility when adding different bundle preparation methods. - pub async fn prepare(&self, sim_result: &SimResult) -> eyre::Result { - // This function is left for forwards compatibility when we want to add - // different bundle preparation methods in the future. - self.prepare_bundle(sim_result).await - } - - /// Prepares a MEV bundle containing the host transactions and the rollup block. - /// - /// This method orchestrates the bundle preparation by: - /// 1. Preparing and signing the submission transaction - /// 2. Tracking the transaction hash for monitoring - /// 3. Encoding the transaction for bundle inclusion - /// 4. Constructing the complete bundle body - #[instrument(skip_all, level = "debug")] - async fn prepare_bundle(&self, sim_result: &SimResult) -> eyre::Result { - // Prepare and sign the transaction - let block_tx = self.prepare_signed_transaction(sim_result).await?; - - // Track the outbound transaction - self.track_outbound_tx(&block_tx); - - // Encode the transaction - let tx_bytes = block_tx.encoded_2718().into(); - - // Build the bundle body with the block_tx bytes as the last transaction in the bundle. + /// Builds a MEV bundle from a signed transaction envelope and simulation result. + fn build_bundle(&self, envelope: &TxEnvelope, sim_result: &SimResult) -> EthSendBundle { + let tx_bytes: Bytes = envelope.encoded_2718().into(); let txs = self.build_bundle_body(sim_result, tx_bytes); - // Create the MEV bundle (valid only in the specific host block) - Ok(EthSendBundle { - txs, - block_number: sim_result.host_block_number(), - ..Default::default() - }) + EthSendBundle { txs, block_number: sim_result.host_block_number(), ..Default::default() } } /// Prepares and signs the submission transaction for the rollup block. /// /// Creates a `SubmitPrep` instance to build the transaction, then fills /// and signs it using the host provider. + /// + /// Returns the signed transaction envelope and the sidecar (for forwarding to Pylon). #[instrument(skip_all, level = "debug")] - async fn prepare_signed_transaction(&self, sim_result: &SimResult) -> eyre::Result { + async fn prepare_signed_transaction( + &self, + sim_result: &SimResult, + ) -> eyre::Result<(TxEnvelope, alloy::eips::eip7594::BlobTransactionSidecarEip7594)> { let prep = SubmitPrep::new( &sim_result.block, self.host_provider(), @@ -104,7 +83,7 @@ impl FlashbotsTask { self.config.clone(), ); - let tx = prep.prep_transaction(sim_result.prev_host()).await?; + let (tx, sidecar) = prep.prep_transaction(sim_result.prev_host()).await?; let sendable = self .host_provider() @@ -115,7 +94,7 @@ impl FlashbotsTask { let tx_envelope = sendable.try_into_envelope()?; debug!(tx_hash = ?tx_envelope.hash(), "prepared signed rollup block transaction envelope"); - Ok(tx_envelope) + Ok((tx_envelope, sidecar)) } /// Tracks the outbound transaction hash and increments submission metrics. @@ -174,17 +153,23 @@ impl FlashbotsTask { } span_debug!(span, "flashbots task received block"); - // Prepare a MEV bundle with the configured call type from the sim result - let result = self.prepare(&sim_result).instrument(span.clone()).await; + // Prepare and sign the transaction + let (envelope, sidecar) = + match self.prepare_signed_transaction(&sim_result).instrument(span.clone()).await { + Ok(result) => result, + Err(error) => { + counter!("signet.builder.flashbots.bundle_prep_failures").increment(1); + span_debug!(span, %error, "bundle preparation failed"); + continue; + } + }; + + // Extract tx_hash and track the outbound transaction + let tx_hash = *envelope.tx_hash(); + self.track_outbound_tx(&envelope); - let bundle = match result { - Ok(bundle) => bundle, - Err(error) => { - counter!("signet.builder.flashbots.bundle_prep_failures").increment(1); - span_debug!(span, %error, "bundle preparation failed"); - continue; - } - }; + // Build the bundle + let bundle = self.build_bundle(&envelope, &sim_result); // Make a child span to cover submission, or use the current span // if debug is not enabled. @@ -193,9 +178,10 @@ impl FlashbotsTask { // Send the bundle to Flashbots, instrumenting the send future so // all events inside the async send are attributed to the submit - // span. + // span. Only send sidecar to Pylon if Flashbots accepts. let flashbots = self.flashbots().to_owned(); let signer = self.signer.clone(); + let pylon_sender = self.pylon_sender.clone(); tokio::spawn( async move { @@ -209,6 +195,11 @@ impl FlashbotsTask { hash = resp.map(|r| r.bundle_hash.to_string()), "Submitted MEV bundle to Flashbots, received OK response" ); + + // Only send sidecar to Pylon after Flashbots accepts + if let Err(e) = pylon_sender.send((tx_hash, sidecar)) { + debug!("pylon channel closed: {}", e); + } } Err(err) => { counter!("signet.builder.flashbots.submission_failures").increment(1); @@ -216,7 +207,7 @@ impl FlashbotsTask { } } } - .instrument(submit_span.clone()), + .instrument(submit_span), ); } } diff --git a/src/tasks/submit/mod.rs b/src/tasks/submit/mod.rs index b9a7d4c5..b8b8e7ae 100644 --- a/src/tasks/submit/mod.rs +++ b/src/tasks/submit/mod.rs @@ -5,5 +5,9 @@ pub use flashbots::FlashbotsTask; mod prep; pub use prep::{Bumpable, SubmitPrep}; +/// Submission logic for Pylon blob server +pub mod pylon; +pub use pylon::PylonTask; + mod sim_err; pub use sim_err::{SimErrorResp, SimRevertKind}; diff --git a/src/tasks/submit/prep.rs b/src/tasks/submit/prep.rs index 4e262af0..1732a569 100644 --- a/src/tasks/submit/prep.rs +++ b/src/tasks/submit/prep.rs @@ -56,6 +56,12 @@ impl<'a> SubmitPrep<'a> { } } + /// Encodes the rollup block into an EIP-7594 sidecar. + #[instrument(skip(self), level = "debug")] + fn build_sidecar(&self) -> eyre::Result { + self.block.encode_blob::().build_7594().map_err(Into::into) + } + /// Construct a quincey signature request for the block. fn sig_request(&self) -> &SignRequest { self.sig_request.get_or_init(|| { @@ -103,12 +109,6 @@ impl<'a> SubmitPrep<'a> { self.quincey_resp().await.map(|resp| &resp.sig).map(utils::extract_signature_components) } - /// Encodes the rollup block into an EIP-7594 sidecar. - #[instrument(skip(self), level = "debug")] - async fn build_sidecar(&self) -> eyre::Result { - self.block.encode_blob::().build_7594().map_err(Into::into) - } - /// Build a signature and header input for the host chain transaction. async fn build_input(&self) -> eyre::Result { let (v, r, s) = self.quincey_signature().await?; @@ -125,30 +125,33 @@ impl<'a> SubmitPrep<'a> { Ok(call.abi_encode().into()) } - /// Create a new transaction request for the host chain. - async fn new_tx_request(&self) -> eyre::Result { + /// Prepares a transaction for submission to the host chain. + /// + /// Returns the bumpable transaction request and the sidecar, which can be + /// forwarded to other tasks (e.g., Pylon) for further processing. + pub async fn prep_transaction( + self, + prev_host: &Header, + ) -> eyre::Result<(Bumpable, BlobTransactionSidecarEip7594)> { let nonce_fut = self .provider .get_transaction_count(self.provider.default_signer_address()) .into_future() .map(|res| res.map_err(Into::into)); - let (nonce, sidecar, input) = - try_join!(nonce_fut, self.build_sidecar(), self.build_input())?; + let (nonce, input) = try_join!(nonce_fut, self.build_input())?; + + // Build the sidecar once + let sidecar = self.build_sidecar()?; - let tx = TransactionRequest::default() - .with_blob_sidecar(sidecar) + // Clone for the transaction request, keep original for return + let req = TransactionRequest::default() + .with_blob_sidecar(sidecar.clone()) .with_input(input) .with_to(self.config.constants.host_zenith()) .with_nonce(nonce); - Ok(tx) - } - - /// Prepares a transaction for submission to the host chain. - pub async fn prep_transaction(self, prev_host: &Header) -> eyre::Result { - let req = self.new_tx_request().in_current_span().await?; - Ok(Bumpable::new(req, prev_host)) + Ok((Bumpable::new(req, prev_host), sidecar)) } } diff --git a/src/tasks/submit/pylon.rs b/src/tasks/submit/pylon.rs new file mode 100644 index 00000000..e04cd991 --- /dev/null +++ b/src/tasks/submit/pylon.rs @@ -0,0 +1,66 @@ +//! Pylon Task handles submitting blob sidecars to the Pylon blob server +//! for data availability. + +use crate::config::PylonClient; +use alloy::{eips::eip7594::BlobTransactionSidecarEip7594, primitives::TxHash}; +use init4_bin_base::deps::metrics::counter; +use tokio::{sync::mpsc, task::JoinHandle}; +use tracing::{debug, error}; + +/// Task that submits blob sidecars to the Pylon blob server. +#[derive(Debug)] +pub struct PylonTask { + /// Pylon client for posting sidecars. + client: PylonClient, +} + +impl PylonTask { + /// Create a new PylonTask with the given client. + pub const fn new(client: PylonClient) -> Self { + Self { client } + } + + /// Main task loop that processes sidecar submissions to Pylon. + /// + /// Receives `(TxHash, BlobTransactionSidecarEip7594)` tuples from the + /// inbound channel and submits them to the Pylon blob server. + async fn task_future( + self, + mut inbound: mpsc::UnboundedReceiver<(TxHash, BlobTransactionSidecarEip7594)>, + ) { + debug!("starting pylon task"); + + loop { + let Some((tx_hash, sidecar)) = inbound.recv().await else { + debug!("upstream task gone - exiting pylon task"); + break; + }; + + let client = self.client.clone(); + + tokio::spawn(async move { + match client.post_sidecar(tx_hash, sidecar).await { + Ok(()) => { + counter!("signet.builder.pylon.posted").increment(1); + debug!(%tx_hash, "posted sidecar to pylon"); + } + Err(err) => { + counter!("signet.builder.pylon.failures").increment(1); + error!(%tx_hash, %err, "pylon submission failed"); + } + } + }); + } + } + + /// Spawns the Pylon task in a new Tokio task. + /// + /// Returns a sender for submitting sidecars and a join handle for the task. + pub fn spawn( + self, + ) -> (mpsc::UnboundedSender<(TxHash, BlobTransactionSidecarEip7594)>, JoinHandle<()>) { + let (sender, receiver) = mpsc::unbounded_channel(); + let handle = tokio::spawn(self.task_future(receiver)); + (sender, handle) + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs index cb9d3e75..20119f41 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -56,6 +56,7 @@ pub fn setup_test_config() -> &'static BuilderConfig { block_query_cutoff_buffer: 3000, max_host_gas_coefficient: Some(80), constants: SignetSystemConstants::parmigiana(), + pylon_url: "http://localhost:8081".parse().unwrap(), } }) } From 1716278ba666a02b70ccf1581331372bff7cc098 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 5 Feb 2026 16:07:36 +0100 Subject: [PATCH 2/6] chore: revert --- bin/builder.rs | 21 ++------ src/config.rs | 14 +---- src/tasks/submit/flashbots.rs | 97 +++++++++++++++++++---------------- src/tasks/submit/mod.rs | 4 -- src/tasks/submit/prep.rs | 41 +++++++-------- src/tasks/submit/pylon.rs | 66 ------------------------ src/test_utils.rs | 1 - 7 files changed, 77 insertions(+), 167 deletions(-) delete mode 100644 src/tasks/submit/pylon.rs diff --git a/bin/builder.rs b/bin/builder.rs index 1f268ba3..adf26997 100644 --- a/bin/builder.rs +++ b/bin/builder.rs @@ -1,11 +1,8 @@ use builder::{ service::serve_builder, tasks::{ - block::sim::SimulatorTask, - cache::CacheTasks, - env::EnvTask, - metrics::MetricsTask, - submit::{FlashbotsTask, PylonTask}, + block::sim::SimulatorTask, cache::CacheTasks, env::EnvTask, metrics::MetricsTask, + submit::FlashbotsTask, }, }; use init4_bin_base::deps::tracing::{info, info_span}; @@ -35,17 +32,10 @@ async fn main() -> eyre::Result<()> { let (block_env, env_jh) = env_task.spawn(); let (tx_channel, metrics_jh) = metrics_task.spawn(); - // Set up the pylon task - let pylon_client = builder::config().connect_pylon(); - let pylon_task = PylonTask::new(pylon_client); - let (pylon_sender, pylon_jh) = pylon_task.spawn(); - // Set up the cache, submit, and simulator tasks let cache_tasks = CacheTasks::new(block_env.clone()); - let (submit_task, simulator_task) = tokio::try_join!( - FlashbotsTask::new(tx_channel.clone(), pylon_sender), - SimulatorTask::new(block_env), - )?; + let (submit_task, simulator_task) = + tokio::try_join!(FlashbotsTask::new(tx_channel.clone()), SimulatorTask::new(block_env),)?; // Spawn the cache, submit, and simulator tasks let cache_system = cache_tasks.spawn(); @@ -75,9 +65,6 @@ async fn main() -> eyre::Result<()> { _ = submit_jh => { info!("submit finished"); }, - _ = pylon_jh => { - info!("pylon task finished"); - }, _ = metrics_jh => { info!("metrics finished"); }, diff --git a/src/config.rs b/src/config.rs index 79c125a5..1296ccc0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ use alloy::{ }; use eyre::Result; use init4_bin_base::{ - perms::{Authenticator, OAuthConfig, SharedToken, pylon}, + perms::{Authenticator, OAuthConfig, SharedToken}, utils::{ calc::SlotCalculator, from_env::FromEnv, @@ -25,9 +25,6 @@ use signet_zenith::Zenith; use std::borrow::Cow; use tokio::join; -/// Pylon client type for blob sidecar submission. -pub type PylonClient = pylon::PylonClient; - /// Type alias for the provider used to simulate against rollup state. pub type RuProvider = RootProvider; @@ -171,10 +168,6 @@ pub struct BuilderConfig { /// The signet system constants. pub constants: SignetSystemConstants, - - /// URL for the Pylon blob server API. - #[from_env(var = "PYLON_URL", desc = "URL for the Pylon blob server API")] - pub pylon_url: url::Url, } impl BuilderConfig { @@ -292,9 +285,4 @@ impl BuilderConfig { ((gas_limit as u128 * (self.max_host_gas_coefficient.unwrap_or(80) as u128)) / 100u128) as u64 } - - /// Connect to the Pylon blob server. - pub fn connect_pylon(&self) -> PylonClient { - PylonClient::new(self.pylon_url.clone(), self.oauth_token()) - } } diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index 12bdfc79..6b3d998c 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -7,7 +7,7 @@ use crate::{ }; use alloy::{ consensus::TxEnvelope, - eips::{Encodable2718, eip7594::BlobTransactionSidecarEip7594}, + eips::Encodable2718, primitives::{Bytes, TxHash}, providers::ext::MevApi, rpc::types::mev::EthSendBundle, @@ -32,17 +32,12 @@ pub struct FlashbotsTask { signer: LocalOrAws, /// Channel for sending hashes of outbound transactions. outbound: mpsc::UnboundedSender, - /// Channel for sending sidecars to the Pylon task. - pylon_sender: mpsc::UnboundedSender<(TxHash, BlobTransactionSidecarEip7594)>, } impl FlashbotsTask { /// Returns a new `FlashbotsTask` instance that receives `SimResult` types from the given /// channel and handles their preparation, submission to the Flashbots network. - pub async fn new( - outbound: mpsc::UnboundedSender, - pylon_sender: mpsc::UnboundedSender<(TxHash, BlobTransactionSidecarEip7594)>, - ) -> eyre::Result { + pub async fn new(outbound: mpsc::UnboundedSender) -> eyre::Result { let config = crate::config(); let (quincey, host_provider, flashbots, builder_key) = tokio::try_join!( @@ -54,28 +49,54 @@ impl FlashbotsTask { let zenith = config.connect_zenith(host_provider); - Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound, pylon_sender }) + Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound }) } - /// Builds a MEV bundle from a signed transaction envelope and simulation result. - fn build_bundle(&self, envelope: &TxEnvelope, sim_result: &SimResult) -> EthSendBundle { - let tx_bytes: Bytes = envelope.encoded_2718().into(); + /// Prepares a MEV bundle from a simulation result. + /// + /// This function serves as an entry point for bundle preparation and is left + /// for forward compatibility when adding different bundle preparation methods. + pub async fn prepare(&self, sim_result: &SimResult) -> eyre::Result { + // This function is left for forwards compatibility when we want to add + // different bundle preparation methods in the future. + self.prepare_bundle(sim_result).await + } + + /// Prepares a MEV bundle containing the host transactions and the rollup block. + /// + /// This method orchestrates the bundle preparation by: + /// 1. Preparing and signing the submission transaction + /// 2. Tracking the transaction hash for monitoring + /// 3. Encoding the transaction for bundle inclusion + /// 4. Constructing the complete bundle body + #[instrument(skip_all, level = "debug")] + async fn prepare_bundle(&self, sim_result: &SimResult) -> eyre::Result { + // Prepare and sign the transaction + let block_tx = self.prepare_signed_transaction(sim_result).await?; + + // Track the outbound transaction + self.track_outbound_tx(&block_tx); + + // Encode the transaction + let tx_bytes = block_tx.encoded_2718().into(); + + // Build the bundle body with the block_tx bytes as the last transaction in the bundle. let txs = self.build_bundle_body(sim_result, tx_bytes); - EthSendBundle { txs, block_number: sim_result.host_block_number(), ..Default::default() } + // Create the MEV bundle (valid only in the specific host block) + Ok(EthSendBundle { + txs, + block_number: sim_result.host_block_number(), + ..Default::default() + }) } /// Prepares and signs the submission transaction for the rollup block. /// /// Creates a `SubmitPrep` instance to build the transaction, then fills /// and signs it using the host provider. - /// - /// Returns the signed transaction envelope and the sidecar (for forwarding to Pylon). #[instrument(skip_all, level = "debug")] - async fn prepare_signed_transaction( - &self, - sim_result: &SimResult, - ) -> eyre::Result<(TxEnvelope, alloy::eips::eip7594::BlobTransactionSidecarEip7594)> { + async fn prepare_signed_transaction(&self, sim_result: &SimResult) -> eyre::Result { let prep = SubmitPrep::new( &sim_result.block, self.host_provider(), @@ -83,7 +104,7 @@ impl FlashbotsTask { self.config.clone(), ); - let (tx, sidecar) = prep.prep_transaction(sim_result.prev_host()).await?; + let tx = prep.prep_transaction(sim_result.prev_host()).await?; let sendable = self .host_provider() @@ -94,7 +115,7 @@ impl FlashbotsTask { let tx_envelope = sendable.try_into_envelope()?; debug!(tx_hash = ?tx_envelope.hash(), "prepared signed rollup block transaction envelope"); - Ok((tx_envelope, sidecar)) + Ok(tx_envelope) } /// Tracks the outbound transaction hash and increments submission metrics. @@ -153,23 +174,17 @@ impl FlashbotsTask { } span_debug!(span, "flashbots task received block"); - // Prepare and sign the transaction - let (envelope, sidecar) = - match self.prepare_signed_transaction(&sim_result).instrument(span.clone()).await { - Ok(result) => result, - Err(error) => { - counter!("signet.builder.flashbots.bundle_prep_failures").increment(1); - span_debug!(span, %error, "bundle preparation failed"); - continue; - } - }; - - // Extract tx_hash and track the outbound transaction - let tx_hash = *envelope.tx_hash(); - self.track_outbound_tx(&envelope); + // Prepare a MEV bundle with the configured call type from the sim result + let result = self.prepare(&sim_result).instrument(span.clone()).await; - // Build the bundle - let bundle = self.build_bundle(&envelope, &sim_result); + let bundle = match result { + Ok(bundle) => bundle, + Err(error) => { + counter!("signet.builder.flashbots.bundle_prep_failures").increment(1); + span_debug!(span, %error, "bundle preparation failed"); + continue; + } + }; // Make a child span to cover submission, or use the current span // if debug is not enabled. @@ -178,10 +193,9 @@ impl FlashbotsTask { // Send the bundle to Flashbots, instrumenting the send future so // all events inside the async send are attributed to the submit - // span. Only send sidecar to Pylon if Flashbots accepts. + // span. let flashbots = self.flashbots().to_owned(); let signer = self.signer.clone(); - let pylon_sender = self.pylon_sender.clone(); tokio::spawn( async move { @@ -195,11 +209,6 @@ impl FlashbotsTask { hash = resp.map(|r| r.bundle_hash.to_string()), "Submitted MEV bundle to Flashbots, received OK response" ); - - // Only send sidecar to Pylon after Flashbots accepts - if let Err(e) = pylon_sender.send((tx_hash, sidecar)) { - debug!("pylon channel closed: {}", e); - } } Err(err) => { counter!("signet.builder.flashbots.submission_failures").increment(1); @@ -207,7 +216,7 @@ impl FlashbotsTask { } } } - .instrument(submit_span), + .instrument(submit_span.clone()), ); } } diff --git a/src/tasks/submit/mod.rs b/src/tasks/submit/mod.rs index b8b8e7ae..b9a7d4c5 100644 --- a/src/tasks/submit/mod.rs +++ b/src/tasks/submit/mod.rs @@ -5,9 +5,5 @@ pub use flashbots::FlashbotsTask; mod prep; pub use prep::{Bumpable, SubmitPrep}; -/// Submission logic for Pylon blob server -pub mod pylon; -pub use pylon::PylonTask; - mod sim_err; pub use sim_err::{SimErrorResp, SimRevertKind}; diff --git a/src/tasks/submit/prep.rs b/src/tasks/submit/prep.rs index 1732a569..4e262af0 100644 --- a/src/tasks/submit/prep.rs +++ b/src/tasks/submit/prep.rs @@ -56,12 +56,6 @@ impl<'a> SubmitPrep<'a> { } } - /// Encodes the rollup block into an EIP-7594 sidecar. - #[instrument(skip(self), level = "debug")] - fn build_sidecar(&self) -> eyre::Result { - self.block.encode_blob::().build_7594().map_err(Into::into) - } - /// Construct a quincey signature request for the block. fn sig_request(&self) -> &SignRequest { self.sig_request.get_or_init(|| { @@ -109,6 +103,12 @@ impl<'a> SubmitPrep<'a> { self.quincey_resp().await.map(|resp| &resp.sig).map(utils::extract_signature_components) } + /// Encodes the rollup block into an EIP-7594 sidecar. + #[instrument(skip(self), level = "debug")] + async fn build_sidecar(&self) -> eyre::Result { + self.block.encode_blob::().build_7594().map_err(Into::into) + } + /// Build a signature and header input for the host chain transaction. async fn build_input(&self) -> eyre::Result { let (v, r, s) = self.quincey_signature().await?; @@ -125,33 +125,30 @@ impl<'a> SubmitPrep<'a> { Ok(call.abi_encode().into()) } - /// Prepares a transaction for submission to the host chain. - /// - /// Returns the bumpable transaction request and the sidecar, which can be - /// forwarded to other tasks (e.g., Pylon) for further processing. - pub async fn prep_transaction( - self, - prev_host: &Header, - ) -> eyre::Result<(Bumpable, BlobTransactionSidecarEip7594)> { + /// Create a new transaction request for the host chain. + async fn new_tx_request(&self) -> eyre::Result { let nonce_fut = self .provider .get_transaction_count(self.provider.default_signer_address()) .into_future() .map(|res| res.map_err(Into::into)); - let (nonce, input) = try_join!(nonce_fut, self.build_input())?; - - // Build the sidecar once - let sidecar = self.build_sidecar()?; + let (nonce, sidecar, input) = + try_join!(nonce_fut, self.build_sidecar(), self.build_input())?; - // Clone for the transaction request, keep original for return - let req = TransactionRequest::default() - .with_blob_sidecar(sidecar.clone()) + let tx = TransactionRequest::default() + .with_blob_sidecar(sidecar) .with_input(input) .with_to(self.config.constants.host_zenith()) .with_nonce(nonce); - Ok((Bumpable::new(req, prev_host), sidecar)) + Ok(tx) + } + + /// Prepares a transaction for submission to the host chain. + pub async fn prep_transaction(self, prev_host: &Header) -> eyre::Result { + let req = self.new_tx_request().in_current_span().await?; + Ok(Bumpable::new(req, prev_host)) } } diff --git a/src/tasks/submit/pylon.rs b/src/tasks/submit/pylon.rs deleted file mode 100644 index e04cd991..00000000 --- a/src/tasks/submit/pylon.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Pylon Task handles submitting blob sidecars to the Pylon blob server -//! for data availability. - -use crate::config::PylonClient; -use alloy::{eips::eip7594::BlobTransactionSidecarEip7594, primitives::TxHash}; -use init4_bin_base::deps::metrics::counter; -use tokio::{sync::mpsc, task::JoinHandle}; -use tracing::{debug, error}; - -/// Task that submits blob sidecars to the Pylon blob server. -#[derive(Debug)] -pub struct PylonTask { - /// Pylon client for posting sidecars. - client: PylonClient, -} - -impl PylonTask { - /// Create a new PylonTask with the given client. - pub const fn new(client: PylonClient) -> Self { - Self { client } - } - - /// Main task loop that processes sidecar submissions to Pylon. - /// - /// Receives `(TxHash, BlobTransactionSidecarEip7594)` tuples from the - /// inbound channel and submits them to the Pylon blob server. - async fn task_future( - self, - mut inbound: mpsc::UnboundedReceiver<(TxHash, BlobTransactionSidecarEip7594)>, - ) { - debug!("starting pylon task"); - - loop { - let Some((tx_hash, sidecar)) = inbound.recv().await else { - debug!("upstream task gone - exiting pylon task"); - break; - }; - - let client = self.client.clone(); - - tokio::spawn(async move { - match client.post_sidecar(tx_hash, sidecar).await { - Ok(()) => { - counter!("signet.builder.pylon.posted").increment(1); - debug!(%tx_hash, "posted sidecar to pylon"); - } - Err(err) => { - counter!("signet.builder.pylon.failures").increment(1); - error!(%tx_hash, %err, "pylon submission failed"); - } - } - }); - } - } - - /// Spawns the Pylon task in a new Tokio task. - /// - /// Returns a sender for submitting sidecars and a join handle for the task. - pub fn spawn( - self, - ) -> (mpsc::UnboundedSender<(TxHash, BlobTransactionSidecarEip7594)>, JoinHandle<()>) { - let (sender, receiver) = mpsc::unbounded_channel(); - let handle = tokio::spawn(self.task_future(receiver)); - (sender, handle) - } -} diff --git a/src/test_utils.rs b/src/test_utils.rs index 20119f41..cb9d3e75 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -56,7 +56,6 @@ pub fn setup_test_config() -> &'static BuilderConfig { block_query_cutoff_buffer: 3000, max_host_gas_coefficient: Some(80), constants: SignetSystemConstants::parmigiana(), - pylon_url: "http://localhost:8081".parse().unwrap(), } }) } From 6763b18f9f14b0256035f85c4fac502d035ff6e7 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 5 Feb 2026 16:36:59 +0100 Subject: [PATCH 3/6] chore: slim down to fire and forget --- src/config.rs | 14 ++++++++- src/tasks/submit/flashbots.rs | 59 +++++++++++++++++++++++++++++------ src/test_utils.rs | 1 + 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1296ccc0..79c125a5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ use alloy::{ }; use eyre::Result; use init4_bin_base::{ - perms::{Authenticator, OAuthConfig, SharedToken}, + perms::{Authenticator, OAuthConfig, SharedToken, pylon}, utils::{ calc::SlotCalculator, from_env::FromEnv, @@ -25,6 +25,9 @@ use signet_zenith::Zenith; use std::borrow::Cow; use tokio::join; +/// Pylon client type for blob sidecar submission. +pub type PylonClient = pylon::PylonClient; + /// Type alias for the provider used to simulate against rollup state. pub type RuProvider = RootProvider; @@ -168,6 +171,10 @@ pub struct BuilderConfig { /// The signet system constants. pub constants: SignetSystemConstants, + + /// URL for the Pylon blob server API. + #[from_env(var = "PYLON_URL", desc = "URL for the Pylon blob server API")] + pub pylon_url: url::Url, } impl BuilderConfig { @@ -285,4 +292,9 @@ impl BuilderConfig { ((gas_limit as u128 * (self.max_host_gas_coefficient.unwrap_or(80) as u128)) / 100u128) as u64 } + + /// Connect to the Pylon blob server. + pub fn connect_pylon(&self) -> PylonClient { + PylonClient::new(self.pylon_url.clone(), self.oauth_token()) + } } diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index 6b3d998c..a8610baa 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -1,13 +1,13 @@ //! Flashbots Task receives simulated blocks from an upstream channel and //! submits them to the Flashbots relay as bundles. use crate::{ - config::{BuilderConfig, FlashbotsProvider, HostProvider, ZenithInstance}, + config::{BuilderConfig, FlashbotsProvider, HostProvider, PylonClient, ZenithInstance}, quincey::Quincey, tasks::{block::sim::SimResult, submit::SubmitPrep}, }; use alloy::{ consensus::TxEnvelope, - eips::Encodable2718, + eips::{Encodable2718, eip7594::BlobTransactionSidecarEip7594}, primitives::{Bytes, TxHash}, providers::ext::MevApi, rpc::types::mev::EthSendBundle, @@ -32,6 +32,8 @@ pub struct FlashbotsTask { signer: LocalOrAws, /// Channel for sending hashes of outbound transactions. outbound: mpsc::UnboundedSender, + /// Pylon client for blob sidecar submission. + pylon: PylonClient, } impl FlashbotsTask { @@ -48,15 +50,21 @@ impl FlashbotsTask { )?; let zenith = config.connect_zenith(host_provider); + let pylon = config.connect_pylon(); - Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound }) + Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound, pylon }) } /// Prepares a MEV bundle from a simulation result. /// /// This function serves as an entry point for bundle preparation and is left /// for forward compatibility when adding different bundle preparation methods. - pub async fn prepare(&self, sim_result: &SimResult) -> eyre::Result { + /// + /// Returns the bundle, tx hash, and the blob sidecar for Pylon submission. + pub async fn prepare( + &self, + sim_result: &SimResult, + ) -> eyre::Result<(EthSendBundle, TxHash, BlobTransactionSidecarEip7594)> { // This function is left for forwards compatibility when we want to add // different bundle preparation methods in the future. self.prepare_bundle(sim_result).await @@ -69,14 +77,30 @@ impl FlashbotsTask { /// 2. Tracking the transaction hash for monitoring /// 3. Encoding the transaction for bundle inclusion /// 4. Constructing the complete bundle body + /// + /// Returns the bundle, tx hash, and the blob sidecar for Pylon submission. #[instrument(skip_all, level = "debug")] - async fn prepare_bundle(&self, sim_result: &SimResult) -> eyre::Result { + async fn prepare_bundle( + &self, + sim_result: &SimResult, + ) -> eyre::Result<(EthSendBundle, TxHash, BlobTransactionSidecarEip7594)> { // Prepare and sign the transaction let block_tx = self.prepare_signed_transaction(sim_result).await?; // Track the outbound transaction self.track_outbound_tx(&block_tx); + // Get tx hash for Pylon submission + let tx_hash = *block_tx.tx_hash(); + + // Clone the sidecar from the envelope for Pylon submission + // We always build EIP-7594 sidecars, so this is guaranteed to exist + let sidecar = block_tx + .as_eip4844() + .and_then(|tx| tx.tx().sidecar()) + .and_then(|s| s.clone().into_eip7594()) + .expect("sidecar is guaranteed to exist for blob transactions"); + // Encode the transaction let tx_bytes = block_tx.encoded_2718().into(); @@ -84,11 +108,13 @@ impl FlashbotsTask { let txs = self.build_bundle_body(sim_result, tx_bytes); // Create the MEV bundle (valid only in the specific host block) - Ok(EthSendBundle { + let bundle = EthSendBundle { txs, block_number: sim_result.host_block_number(), ..Default::default() - }) + }; + + Ok((bundle, tx_hash, sidecar)) } /// Prepares and signs the submission transaction for the rollup block. @@ -177,8 +203,8 @@ impl FlashbotsTask { // Prepare a MEV bundle with the configured call type from the sim result let result = self.prepare(&sim_result).instrument(span.clone()).await; - let bundle = match result { - Ok(bundle) => bundle, + let (bundle, tx_hash, sidecar) = match result { + Ok(result) => result, Err(error) => { counter!("signet.builder.flashbots.bundle_prep_failures").increment(1); span_debug!(span, %error, "bundle preparation failed"); @@ -193,9 +219,10 @@ impl FlashbotsTask { // Send the bundle to Flashbots, instrumenting the send future so // all events inside the async send are attributed to the submit - // span. + // span. If Flashbots accepts, submit sidecar to Pylon. let flashbots = self.flashbots().to_owned(); let signer = self.signer.clone(); + let pylon = self.pylon.clone(); tokio::spawn( async move { @@ -209,6 +236,18 @@ impl FlashbotsTask { hash = resp.map(|r| r.bundle_hash.to_string()), "Submitted MEV bundle to Flashbots, received OK response" ); + + // Fire and forget pylon submission + match pylon.post_sidecar(tx_hash, sidecar).await { + Ok(()) => { + counter!("signet.builder.pylon.posted").increment(1); + debug!(%tx_hash, "posted sidecar to pylon"); + } + Err(err) => { + counter!("signet.builder.pylon.failures").increment(1); + error!(%tx_hash, %err, "pylon submission failed"); + } + } } Err(err) => { counter!("signet.builder.flashbots.submission_failures").increment(1); diff --git a/src/test_utils.rs b/src/test_utils.rs index cb9d3e75..20119f41 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -56,6 +56,7 @@ pub fn setup_test_config() -> &'static BuilderConfig { block_query_cutoff_buffer: 3000, max_host_gas_coefficient: Some(80), constants: SignetSystemConstants::parmigiana(), + pylon_url: "http://localhost:8081".parse().unwrap(), } }) } From 7bc6d07ad79a7cd460f68c09b9d6ff51cb566062 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 6 Feb 2026 14:38:39 +0100 Subject: [PATCH 4/6] chore: simplify --- Cargo.lock | 18 +++++------ src/tasks/submit/flashbots.rs | 58 ++++++++++++----------------------- 2 files changed, 28 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14cdfaaf..357073d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3456,7 +3456,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3736,7 +3736,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4726,7 +4726,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.57.0", ] [[package]] @@ -4944,7 +4944,7 @@ dependencies = [ [[package]] name = "init4-bin-base" -version = "0.18.0-rc.7" +version = "0.18.0-rc.8" dependencies = [ "alloy", "async-trait", @@ -5968,7 +5968,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7068,7 +7068,7 @@ dependencies = [ "once_cell", "socket2 0.6.2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -10410,7 +10410,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -11527,7 +11527,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -12681,7 +12681,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index a8610baa..3ea76830 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -7,7 +7,7 @@ use crate::{ }; use alloy::{ consensus::TxEnvelope, - eips::{Encodable2718, eip7594::BlobTransactionSidecarEip7594}, + eips::Encodable2718, primitives::{Bytes, TxHash}, providers::ext::MevApi, rpc::types::mev::EthSendBundle, @@ -59,12 +59,7 @@ impl FlashbotsTask { /// /// This function serves as an entry point for bundle preparation and is left /// for forward compatibility when adding different bundle preparation methods. - /// - /// Returns the bundle, tx hash, and the blob sidecar for Pylon submission. - pub async fn prepare( - &self, - sim_result: &SimResult, - ) -> eyre::Result<(EthSendBundle, TxHash, BlobTransactionSidecarEip7594)> { + pub async fn prepare(&self, sim_result: &SimResult) -> eyre::Result { // This function is left for forwards compatibility when we want to add // different bundle preparation methods in the future. self.prepare_bundle(sim_result).await @@ -77,30 +72,14 @@ impl FlashbotsTask { /// 2. Tracking the transaction hash for monitoring /// 3. Encoding the transaction for bundle inclusion /// 4. Constructing the complete bundle body - /// - /// Returns the bundle, tx hash, and the blob sidecar for Pylon submission. #[instrument(skip_all, level = "debug")] - async fn prepare_bundle( - &self, - sim_result: &SimResult, - ) -> eyre::Result<(EthSendBundle, TxHash, BlobTransactionSidecarEip7594)> { + async fn prepare_bundle(&self, sim_result: &SimResult) -> eyre::Result { // Prepare and sign the transaction let block_tx = self.prepare_signed_transaction(sim_result).await?; // Track the outbound transaction self.track_outbound_tx(&block_tx); - // Get tx hash for Pylon submission - let tx_hash = *block_tx.tx_hash(); - - // Clone the sidecar from the envelope for Pylon submission - // We always build EIP-7594 sidecars, so this is guaranteed to exist - let sidecar = block_tx - .as_eip4844() - .and_then(|tx| tx.tx().sidecar()) - .and_then(|s| s.clone().into_eip7594()) - .expect("sidecar is guaranteed to exist for blob transactions"); - // Encode the transaction let tx_bytes = block_tx.encoded_2718().into(); @@ -108,15 +87,12 @@ impl FlashbotsTask { let txs = self.build_bundle_body(sim_result, tx_bytes); // Create the MEV bundle (valid only in the specific host block) - let bundle = EthSendBundle { + Ok(EthSendBundle { txs, block_number: sim_result.host_block_number(), ..Default::default() - }; - - Ok((bundle, tx_hash, sidecar)) + }) } - /// Prepares and signs the submission transaction for the rollup block. /// /// Creates a `SubmitPrep` instance to build the transaction, then fills @@ -202,9 +178,8 @@ impl FlashbotsTask { // Prepare a MEV bundle with the configured call type from the sim result let result = self.prepare(&sim_result).instrument(span.clone()).await; - - let (bundle, tx_hash, sidecar) = match result { - Ok(result) => result, + let bundle = match result { + Ok(bundle) => bundle, Err(error) => { counter!("signet.builder.flashbots.bundle_prep_failures").increment(1); span_debug!(span, %error, "bundle preparation failed"); @@ -212,6 +187,10 @@ impl FlashbotsTask { } }; + // Due to the way the bundle is built, the block transaction is the last transaction in the bundle. + // We'll use this to forward the tx to pylon, which will preload the sidecar. + let block_tx = bundle.txs.last().unwrap().clone(); + // Make a child span to cover submission, or use the current span // if debug is not enabled. let _guard = span.enter(); @@ -219,7 +198,7 @@ impl FlashbotsTask { // Send the bundle to Flashbots, instrumenting the send future so // all events inside the async send are attributed to the submit - // span. If Flashbots accepts, submit sidecar to Pylon. + // span. If Flashbots accepts, submit envelope to Pylon. let flashbots = self.flashbots().to_owned(); let signer = self.signer.clone(); let pylon = self.pylon.clone(); @@ -237,15 +216,16 @@ impl FlashbotsTask { "Submitted MEV bundle to Flashbots, received OK response" ); - // Fire and forget pylon submission - match pylon.post_sidecar(tx_hash, sidecar).await { + match pylon.post_sidecar(block_tx).await { Ok(()) => { - counter!("signet.builder.pylon.posted").increment(1); - debug!(%tx_hash, "posted sidecar to pylon"); + counter!("signet.builder.pylon.sidecars_submitted") + .increment(1); + debug!("posted sidecar to pylon"); } Err(err) => { - counter!("signet.builder.pylon.failures").increment(1); - error!(%tx_hash, %err, "pylon submission failed"); + counter!("signet.builder.pylon.submission_failures") + .increment(1); + error!(%err, "pylon submission failed"); } } } From ac8b10d0e066d1ff0d3463081b5bfbf5d94e6a4b Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 6 Feb 2026 14:39:48 +0100 Subject: [PATCH 5/6] chore: docs --- src/tasks/submit/flashbots.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index 3ea76830..af9cf10e 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -187,7 +187,7 @@ impl FlashbotsTask { } }; - // Due to the way the bundle is built, the block transaction is the last transaction in the bundle. + // Due to the way the bundle is built, the block transaction is the last transaction in the bundle, and will always exist. // We'll use this to forward the tx to pylon, which will preload the sidecar. let block_tx = bundle.txs.last().unwrap().clone(); From 452582a9c112a3210fefecc68aa51f156ef85918 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 6 Feb 2026 14:43:10 +0100 Subject: [PATCH 6/6] chore: move build_bundle_body to `SimResult` --- src/tasks/block/sim.rs | 17 ++++++++++++++++- src/tasks/submit/flashbots.rs | 27 ++------------------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/tasks/block/sim.rs b/src/tasks/block/sim.rs index 3963dc43..ea3cfe3e 100644 --- a/src/tasks/block/sim.rs +++ b/src/tasks/block/sim.rs @@ -5,7 +5,7 @@ use crate::{ config::{BuilderConfig, HostProvider, RuProvider}, tasks::env::SimEnv, }; -use alloy::consensus::Header; +use alloy::{consensus::Header, eips::Encodable2718, primitives::Bytes}; use init4_bin_base::{ deps::metrics::{counter, histogram}, utils::calc::SlotCalculator, @@ -62,6 +62,21 @@ impl SimResult { pub fn clone_span(&self) -> Span { self.sim_env.clone_span() } + + /// Constructs the MEV bundle body from host transactions and the submission transaction. + /// + /// Combines all host transactions from the rollup block with the prepared rollup block + /// submission transaction, wrapping each as a non-revertible bundle item. + /// + /// The rollup block transaction is always included and placed last in the bundle. + pub fn build_bundle_body(&self, block_tx_bytes: Bytes) -> Vec { + self.block + .host_transactions() + .iter() + .map(|tx| tx.encoded_2718().into()) + .chain(std::iter::once(block_tx_bytes)) + .collect() + } } /// A task that builds blocks based on incoming [`SimEnv`]s and a simulation diff --git a/src/tasks/submit/flashbots.rs b/src/tasks/submit/flashbots.rs index af9cf10e..caee876c 100644 --- a/src/tasks/submit/flashbots.rs +++ b/src/tasks/submit/flashbots.rs @@ -6,10 +6,7 @@ use crate::{ tasks::{block::sim::SimResult, submit::SubmitPrep}, }; use alloy::{ - consensus::TxEnvelope, - eips::Encodable2718, - primitives::{Bytes, TxHash}, - providers::ext::MevApi, + consensus::TxEnvelope, eips::Encodable2718, primitives::TxHash, providers::ext::MevApi, rpc::types::mev::EthSendBundle, }; use init4_bin_base::{deps::metrics::counter, utils::signer::LocalOrAws}; @@ -84,7 +81,7 @@ impl FlashbotsTask { let tx_bytes = block_tx.encoded_2718().into(); // Build the bundle body with the block_tx bytes as the last transaction in the bundle. - let txs = self.build_bundle_body(sim_result, tx_bytes); + let txs = sim_result.build_bundle_body(tx_bytes); // Create the MEV bundle (valid only in the specific host block) Ok(EthSendBundle { @@ -132,26 +129,6 @@ impl FlashbotsTask { } } - /// Constructs the MEV bundle body from host transactions and the submission transaction. - /// - /// Combines all host transactions from the rollup block with the prepared rollup block - /// submission transaction, wrapping each as a non-revertible bundle item. - /// - /// The rollup block transaction is placed last in the bundle. - fn build_bundle_body( - &self, - sim_result: &SimResult, - tx_bytes: alloy::primitives::Bytes, - ) -> Vec { - sim_result - .block - .host_transactions() - .iter() - .map(|tx| tx.encoded_2718().into()) - .chain(std::iter::once(tx_bytes)) - .collect() - } - /// Main task loop that processes simulation results and submits bundles to Flashbots. /// /// Receives `SimResult`s from the inbound channel, prepares MEV bundles, and submits