Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 14 additions & 21 deletions README.md
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Drive by changes: ppdated the README to match latest env config changes re: signet system constants usage, Flashbots updates, chain name, etc...

Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ When the deadline is reached, the simulator is stopped, and all open simulation

### ✨ Submit Task

If Flashbots endpoint has been configured the Flashbots submit task will prepare a Flashbots bundle out of that Signet block, and then submits that bundle to the Flashbots endpoint.

If a Flashbots endpoint has _not_ been configured, the Builder uses the [builder helper contract] and to craft a rollup block transaction and submits that to the default mempool. This mode of operation is only for testing on private networks and should not be used in production, since it can leak sensitive transaction data from the Signet block.
The Flashbots submit task prepares a Flashbots bundle out of the Signet block and its host transactions and then submits that bundle to the Flashbots endpoint. It sends the hash of the rollup block transaction for to the Metrics task for further tracking.

If the block received from simulation is empty, the submit task will ignore it.

Expand All @@ -88,32 +86,29 @@ Finally, if it's non-empty, the submit task attempts to get a signature for the
The Builder is configured via environment variables. The following values are supported for configuration.

Key | Required | Description
----------------------------- | -------- | ----------------------------------------------------------------------------------------
`HOST_CHAIN_ID` | Yes | Host-chain ID (e.g. `3151908`)
`RU_CHAIN_ID` | Yes | Rollup-chain ID (e.g. `14174`)
----------------------------- | -------- | ------------------------------------------------------------------------------
`RUST_LOG` | No | The log level of the builder
`CHAIN_NAME` | No | The chain name ("pecorino", or the corresponding name)
`HOST_RPC_URL` | Yes | RPC endpoint for the host chain
`ROLLUP_RPC_URL` | Yes | RPC endpoint for the rollup chain
`TX_POOL_URL` | Yes | Transaction pool URL (must end with `/`)
`TX_BROADCAST_URLS` | No | Additional endpoints for blob txs (comma-separated, slash required)
`FLASHBOTS_ENDPOINT` | No | Flashbots API to submit blocks to. Defaults to the BuilderHelper submit task if not set.
`ZENITH_ADDRESS` | Yes | Zenith contract address
`BUILDER_HELPER_ADDRESS` | Yes | Builder helper contract address
`QUINCEY_URL` | Yes | Remote sequencer signing endpoint
`BUILDER_PORT` | Yes | HTTP port for the Builder (default: `8080`)
`SEQUENCER_KEY` | Yes | AWS KMS key ID _or_ local private key for sequencer signing
`SEQUENCER_KEY` | No | AWS Key ID _OR_ local private key for the Sequencer; set IFF using local Sequencer signing instead of remote (via `QUINCEY_URL`) Quincey signing
`TX_POOL_URL` | Yes | Transaction pool URL (must end with `/`)
`FLASHBOTS_ENDPOINT` | No | Flashbots API to submit blocks to
`ROLLUP_BLOCK_GAS_LIMIT` | No | Override for rollup block gas limit
`MAX_HOST_GAS_COEFFICIENT` | No | Optional maximum host gas coefficient, as a percentage, to use when building blocks
`BUILDER_KEY` | Yes | AWS KMS key ID _or_ local private key for builder signing
`AWS_ACCESS_KEY_ID` | No | AWS secret access key ID (required if not using `BUILDER_KEY`)
`AWS_SECRET_ACCESS_KEY` | No | AWS secret access key (required if not using `BUILDER_KEY`)
`AWS_DEFAULT_REGION` | No | AWS region for the KMS key in question (required if not using `BUILDER_KEY`)
`BUILDER_PORT` | Yes | HTTP port for the Builder (default: `8080`)
`BUILDER_REWARDS_ADDRESS` | Yes | Address receiving builder rewards
`ROLLUP_BLOCK_GAS_LIMIT` | No | Override for block gas limit
`CONCURRENCY_LIMIT` | No | Max concurrent tasks the simulator uses
`CONCURRENCY_LIMIT` | No | Optional max number of concurrent tasks the simulator uses. Defaults to a system call to determine optimal parallelism
`OAUTH_CLIENT_ID` | Yes | Oauth client ID for the builder
`OAUTH_CLIENT_SECRET` | Yes | Oauth client secret for the builder
`OAUTH_AUTHENTICATE_URL` | Yes | Oauth authenticate URL for the builder for performing OAuth logins
`OAUTH_TOKEN_URL` | Yes | Oauth token URL for the builder to get an Oauth2 access token
`AUTH_TOKEN_REFRESH_INTERVAL` | Yes | The OAuth token refresh interval in seconds.
`CHAIN_NAME` | No | The chain name ("pecorino", or the corresponding name)
`SLOT_OFFSET` | No | Slot timing offset in seconds. Required if `CHAIN_NAME` is not present
`SLOT_DURATION` | No | Slot duration in seconds. Required if `CHAIN_NAME` is not present
`START_TIMESTAMP` | No | UNIX timestamp for slot 0\. Required if `CHAIN_NAME` is not present

--------------------------------------------------------------------------------

Expand Down Expand Up @@ -188,5 +183,3 @@ The previous header's basefee is tracked through the build loop and used for gas
## 🪪 License

This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).

[builder helper contract]: https://github.com/init4tech/helper-contracts/blob/main/src/BuilderHelper.sol
132 changes: 94 additions & 38 deletions src/tasks/submit/flashbots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use init4_bin_base::{deps::metrics::counter, utils::signer::LocalOrAws};
use tokio::{sync::mpsc, task::JoinHandle};
use tracing::{Instrument, debug, debug_span};

/// Handles construction, simulation, and submission of rollup blocks to the
/// Flashbots network.
/// Handles preparation and submission of simulated rollup blocks to the
/// Flashbots relay as MEV bundles.
#[derive(Debug)]
pub struct FlashbotsTask {
/// Builder configuration for the task.
Expand All @@ -31,12 +31,14 @@ pub struct FlashbotsTask {
/// The key used to sign requests to the Flashbots relay.
signer: LocalOrAws,
/// Channel for sending hashes of outbound transactions.
_outbound: mpsc::UnboundedSender<TxHash>,
outbound: mpsc::UnboundedSender<TxHash>,
}

impl FlashbotsTask {
/// Returns a new `FlashbotsTask` instance that receives `SimResult` types from the given
/// channel and handles their preparation, submission to the Flashbots network.
/// Creates a new `FlashbotsTask` instance with initialized providers and connections.
///
/// Sets up Quincey for block signing, host provider for transaction submission,
/// Flashbots provider for bundle submission, and Zenith instance for contract interactions.
pub async fn new(
config: BuilderConfig,
outbound: mpsc::UnboundedSender<TxHash>,
Expand All @@ -50,28 +52,56 @@ impl FlashbotsTask {

let zenith = config.connect_zenith(host_provider);

Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, _outbound: outbound })
Ok(Self { config, quincey, zenith, flashbots, signer: builder_key, outbound })
}

/// Returns a reference to the inner `HostProvider`
pub fn host_provider(&self) -> HostProvider {
self.zenith.provider().clone()
}

/// Returns a reference to the inner `FlashbotsProvider`
pub const fn flashbots(&self) -> &FlashbotsProvider {
&self.flashbots
}

/// Prepares a MEV bundle with the configured submit call
/// 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<MevSendBundle> {
// This function is left for forwards compatibility when we want to add
// different bundle preparation methods in the future.
self.prepare_bundle_helper(sim_result).await
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
async fn prepare_bundle(&self, sim_result: &SimResult) -> eyre::Result<MevSendBundle> {
// 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 bundle_body = self.build_bundle_body(sim_result, tx_bytes);

// Create the MEV bundle (valid only in the specific host block)
Ok(MevSendBundle::new(
sim_result.host_block_number(),
Some(sim_result.host_block_number()),
ProtocolVersion::V0_1,
bundle_body,
))
}

/// Prepares a BundleHelper call containing the rollup block and corresponding fills into a MEV bundle.
async fn prepare_bundle_helper(&self, sim_result: &SimResult) -> eyre::Result<MevSendBundle> {
/// 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.
async fn prepare_signed_transaction(
&self,
sim_result: &SimResult,
) -> eyre::Result<alloy::consensus::TxEnvelope> {
let prep = SubmitPrep::new(
&sim_result.block,
self.host_provider(),
Expand All @@ -80,34 +110,48 @@ impl FlashbotsTask {
);

let tx = prep.prep_transaction(sim_result.prev_host()).await?;

let sendable = self.host_provider().fill(tx.into_request()).await?;

let tx_bytes = sendable
.as_envelope()
.ok_or_eyre("failed to get envelope from filled tx")?
.encoded_2718()
.into();
sendable.as_envelope().ok_or_eyre("failed to get envelope from filled tx").cloned()
}

/// Tracks the outbound transaction hash and increments submission metrics.
///
/// Sends the transaction hash to the outbound channel for monitoring.
/// Logs a debug message if the channel is closed.
fn track_outbound_tx(&self, envelope: &alloy::consensus::TxEnvelope) {
counter!("signet.builder.flashbots.").increment(1);
let hash = *envelope.tx_hash();
if self.outbound.send(hash).is_err() {
debug!("outbound channel closed, could not track tx hash");
}
}

let bundle_body = sim_result
/// 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<BundleItem> {
sim_result
.block
.host_transactions()
.iter()
.cloned()
.chain(std::iter::once(tx_bytes))
.map(|tx| BundleItem::Tx { tx, can_revert: false })
.collect::<Vec<_>>();

// Only valid in the specific host block
Ok(MevSendBundle::new(
sim_result.host_block_number(),
Some(sim_result.host_block_number()),
ProtocolVersion::V0_1,
bundle_body,
))
.collect()
}

/// Task future that runs the Flashbots submission loop.
/// Main task loop that processes simulation results and submits bundles to Flashbots.
///
/// Receives `SimResult`s from the inbound channel, prepares MEV bundles, and submits
/// them to the Flashbots relay. Skips empty blocks and continues processing on errors.
async fn task_future(self, mut inbound: mpsc::UnboundedReceiver<SimResult>) {
debug!("starting flashbots task");

Expand Down Expand Up @@ -172,7 +216,19 @@ impl FlashbotsTask {
}
}

/// Spawns the Flashbots task that handles incoming `SimResult`s.
/// Returns a clone of the host provider for transaction operations.
fn host_provider(&self) -> HostProvider {
self.zenith.provider().clone()
}

/// Returns a reference to the Flashbots provider.
const fn flashbots(&self) -> &FlashbotsProvider {
&self.flashbots
}

/// Spawns the Flashbots task in a new Tokio task.
///
/// Returns a channel sender for submitting `SimResult`s and a join handle for the task.
pub fn spawn(self) -> (mpsc::UnboundedSender<SimResult>, JoinHandle<()>) {
let (sender, inbound) = mpsc::unbounded_channel::<SimResult>();
let handle = tokio::spawn(self.task_future(inbound));
Expand Down
Loading