diff --git a/Cargo.toml b/Cargo.toml
index f3b66054..391a71f5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -33,7 +33,7 @@ signet-tx-cache = { git = "https://github.com/init4tech/signet-sdk", branch = "m
 signet-types = { git = "https://github.com/init4tech/signet-sdk", branch = "main" }
 signet-zenith = { git = "https://github.com/init4tech/signet-sdk", branch = "main" }
 
-trevm = { version = "0.23.4", features = ["concurrent-db", "test-utils"] }
+trevm = { version = "0.23.6", features = ["concurrent-db", "test-utils"] }
 
 alloy = { version = "1.0.5", features = [
     "full",
diff --git a/bin/builder.rs b/bin/builder.rs
index 7ee26fa1..65509ba2 100644
--- a/bin/builder.rs
+++ b/bin/builder.rs
@@ -35,12 +35,17 @@ async fn main() -> eyre::Result<()> {
     let zenith = config.connect_zenith(host_provider.clone());
 
     // Set up the metrics task
-    let metrics = MetricsTask { host_provider };
+    let metrics = MetricsTask { host_provider: host_provider.clone() };
     let (tx_channel, metrics_jh) = metrics.spawn();
 
     // Make a Tx submission task
-    let submit =
-        SubmitTask { zenith, quincey, config: config.clone(), outbound_tx_channel: tx_channel };
+    let submit = SubmitTask {
+        zenith,
+        quincey,
+        config: config.clone(),
+        outbound_tx_channel: tx_channel,
+        host_provider: host_provider.clone(),
+    };
 
     // Set up tx submission
     let (submit_channel, submit_jh) = submit.spawn();
diff --git a/bin/submit_transaction.rs b/bin/submit_transaction.rs
index 71b10f46..071f74bb 100644
--- a/bin/submit_transaction.rs
+++ b/bin/submit_transaction.rs
@@ -1,3 +1,5 @@
+//! A simple transaction submitter that sends a transaction to a recipient address
+//! on a regular interval for the purposes of roughly testing rollup mining.
 use alloy::{
     network::{EthereumWallet, TransactionBuilder},
     primitives::{Address, U256},
@@ -67,18 +69,29 @@ async fn main() {
     }
 }
 
+/// Sends a transaction to the specified recipient address
 async fn send_transaction(provider: &HostProvider, recipient_address: Address) {
     // construct simple transaction to send ETH to a recipient
+    let nonce = match provider.get_transaction_count(provider.default_signer_address()).await {
+        Ok(count) => count,
+        Err(e) => {
+            error!(error = ?e, "failed to get transaction count");
+            return;
+        }
+    };
+
     let tx = TransactionRequest::default()
         .with_from(provider.default_signer_address())
         .with_to(recipient_address)
         .with_value(U256::from(1))
+        .with_nonce(nonce)
         .with_gas_limit(30_000);
 
     // start timer to measure how long it takes to mine the transaction
     let dispatch_start_time: Instant = Instant::now();
 
     // dispatch the transaction
+    debug!(?tx.nonce, "sending transaction with nonce");
     let result = provider.send_transaction(tx).await.unwrap();
 
     // wait for the transaction to mine
@@ -95,10 +108,13 @@ async fn send_transaction(provider: &HostProvider, recipient_address: Address) {
         }
     };
 
-    let hash = receipt.transaction_hash.to_string();
+    record_metrics(dispatch_start_time, receipt);
+}
 
-    // record metrics for how long it took to mine the transaction
+/// Record metrics for how long it took to mine the transaction
+fn record_metrics(dispatch_start_time: Instant, receipt: alloy::rpc::types::TransactionReceipt) {
     let mine_time = dispatch_start_time.elapsed().as_secs();
+    let hash = receipt.transaction_hash.to_string();
     debug!(success = receipt.status(), mine_time, hash, "transaction mined");
     histogram!("txn_submitter.tx_mine_time").record(mine_time as f64);
 }
diff --git a/src/config.rs b/src/config.rs
index f869fad5..49bed7b6 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -3,7 +3,7 @@ use crate::{
     tasks::{
         block::cfg::SignetCfgEnv,
         cache::{BundlePoller, CacheSystem, CacheTask, TxPoller},
-        env::EnvTask,
+        env::{EnvTask, SimEnv},
     },
 };
 use alloy::{
@@ -29,7 +29,6 @@ use init4_bin_base::{
 use signet_zenith::Zenith;
 use std::borrow::Cow;
 use tokio::sync::watch;
-use trevm::revm::context::BlockEnv;
 
 /// Type alias for the provider used to simulate against rollup state.
 pub type RuProvider = RootProvider<Ethereum>;
@@ -247,8 +246,8 @@ impl BuilderConfig {
 
     /// Create an [`EnvTask`] using this config.
     pub fn env_task(&self) -> EnvTask {
-        let provider = self.connect_ru_provider();
-        EnvTask::new(self.clone(), provider)
+        let ru_provider = self.connect_ru_provider();
+        EnvTask::new(self.clone(), ru_provider)
     }
 
     /// Spawn a new [`CacheSystem`] using this config. This contains the
@@ -256,7 +255,7 @@ impl BuilderConfig {
     /// well as the [`SimCache`] and the block env watcher.
     ///
     /// [`SimCache`]: signet_sim::SimCache
-    pub fn spawn_cache_system(&self, block_env: watch::Receiver<Option<BlockEnv>>) -> CacheSystem {
+    pub fn spawn_cache_system(&self, block_env: watch::Receiver<Option<SimEnv>>) -> CacheSystem {
         // Tx Poller pulls transactions from the cache
         let tx_poller = TxPoller::new(self);
         let (tx_receiver, tx_poller) = tx_poller.spawn();
diff --git a/src/tasks/block/sim.rs b/src/tasks/block/sim.rs
index ce7ffb39..20f18667 100644
--- a/src/tasks/block/sim.rs
+++ b/src/tasks/block/sim.rs
@@ -1,7 +1,10 @@
 //! `block.rs` contains the Simulator and everything that wires it into an
 //! actor that handles the simulation of a stream of bundles and transactions
 //! and turns them into valid Pecorino blocks for network submission.
-use crate::config::{BuilderConfig, RuProvider};
+use crate::{
+    config::{BuilderConfig, RuProvider},
+    tasks::env::SimEnv,
+};
 use alloy::{eips::BlockId, network::Ethereum, providers::Provider};
 use init4_bin_base::{
     deps::tracing::{debug, error},
@@ -17,6 +20,7 @@ use tokio::{
     },
     task::JoinHandle,
 };
+use tracing::info;
 use trevm::revm::{
     context::BlockEnv,
     database::{AlloyDB, WrapDatabaseAsync},
@@ -34,9 +38,17 @@ pub struct Simulator {
     pub config: BuilderConfig,
     /// A provider that cannot sign transactions, used for interacting with the rollup.
     pub ru_provider: RuProvider,
-
     /// The block configuration environment on which to simulate
-    pub block_env: watch::Receiver<Option<BlockEnv>>,
+    pub sim_env: watch::Receiver<Option<SimEnv>>,
+}
+
+/// SimResult bundles a BuiltBlock to the BlockEnv it was simulated against.
+#[derive(Debug, Clone)]
+pub struct SimResult {
+    /// The block built with the successfully simulated transactions
+    pub block: BuiltBlock,
+    /// The block environment the transactions were simulated against.
+    pub env: SimEnv,
 }
 
 impl Simulator {
@@ -46,6 +58,7 @@ impl Simulator {
     ///
     /// - `config`: The configuration for the builder.
     /// - `ru_provider`: A provider for interacting with the rollup.
+    /// - `block_env`: A receiver for the block environment to simulate against.
     ///
     /// # Returns
     ///
@@ -53,9 +66,9 @@ impl Simulator {
     pub fn new(
         config: &BuilderConfig,
         ru_provider: RuProvider,
-        block_env: watch::Receiver<Option<BlockEnv>>,
+        sim_env: watch::Receiver<Option<SimEnv>>,
     ) -> Self {
-        Self { config: config.clone(), ru_provider, block_env }
+        Self { config: config.clone(), ru_provider, sim_env }
     }
 
     /// Get the slot calculator.
@@ -65,11 +78,16 @@ impl Simulator {
 
     /// Handles building a single block.
     ///
+    /// Builds a block in the block environment with items from the simulation cache
+    /// against the database state. When the `finish_by` deadline is reached, it
+    /// stops simulating and returns the block.
+    ///
     /// # Arguments
     ///
     /// - `constants`: The system constants for the rollup.
     /// - `sim_items`: The simulation cache containing transactions and bundles.
     /// - `finish_by`: The deadline by which the block must be built.
+    /// - `block_env`: The block environment to simulate against.
     ///
     /// # Returns
     ///
@@ -79,14 +97,17 @@ impl Simulator {
         constants: SignetSystemConstants,
         sim_items: SimCache,
         finish_by: Instant,
-        block: BlockEnv,
+        block_env: BlockEnv,
     ) -> eyre::Result<BuiltBlock> {
+        debug!(block_number = block_env.number, tx_count = sim_items.len(), "starting block build",);
+
         let db = self.create_db().await.unwrap();
+
         let block_build: BlockBuild<_, NoOpInspector> = BlockBuild::new(
             db,
             constants,
             self.config.cfg_env(),
-            block,
+            block_env,
             finish_by,
             self.config.concurrency_limit,
             sim_items,
@@ -94,13 +115,17 @@ impl Simulator {
         );
 
         let built_block = block_build.build().await;
-        debug!(block_number = ?built_block.block_number(), "finished building block");
+        debug!(
+            tx_count = built_block.tx_count(),
+            block_number = built_block.block_number(),
+            "block simulation completed",
+        );
 
         Ok(built_block)
     }
 
-    /// Spawns the simulator task, which handles the setup and sets the deadline
-    /// for the each round of simulation.
+    /// Spawns the simulator task, which ticks along the simulation loop
+    /// as it receives block environments.
     ///
     /// # Arguments
     ///
@@ -115,21 +140,23 @@ impl Simulator {
         self,
         constants: SignetSystemConstants,
         cache: SimCache,
-        submit_sender: mpsc::UnboundedSender<BuiltBlock>,
+        submit_sender: mpsc::UnboundedSender<SimResult>,
     ) -> JoinHandle<()> {
         debug!("starting simulator task");
 
         tokio::spawn(async move { self.run_simulator(constants, cache, submit_sender).await })
     }
 
-    /// Continuously runs the block simulation and submission loop.
+    /// This function runs indefinitely, waiting for the block environment to be set and checking
+    /// if the current slot is valid before building a block and sending it along for to the submit channel.
     ///
-    /// This function clones the simulation cache, calculates a deadline for block building,
-    /// attempts to build a block using the latest cache and constants, and submits the built
-    /// block through the provided channel. If an error occurs during block building or submission,
-    /// it logs the error and continues the loop.
+    /// If it is authorized for the current slot, then the simulator task
+    /// - clones the simulation cache,
+    /// - calculates a deadline for block building,
+    /// - attempts to build a block using the latest cache and constants,
+    /// - then submits the built block through the provided channel.
     ///
-    /// This function runs indefinitely and never returns.
+    /// If an error occurs during block building or submission, it logs the error and continues the loop.
     ///
     /// # Arguments
     ///
@@ -140,26 +167,29 @@ impl Simulator {
         mut self,
         constants: SignetSystemConstants,
         cache: SimCache,
-        submit_sender: mpsc::UnboundedSender<BuiltBlock>,
+        submit_sender: mpsc::UnboundedSender<SimResult>,
     ) {
         loop {
-            let sim_cache = cache.clone();
-            let finish_by = self.calculate_deadline();
-
             // Wait for the block environment to be set
-            if self.block_env.changed().await.is_err() {
-                error!("block_env channel closed");
+            if self.sim_env.changed().await.is_err() {
+                error!("block_env channel closed - shutting down simulator task");
                 return;
             }
+            let Some(sim_env) = self.sim_env.borrow_and_update().clone() else { return };
+            info!(sim_env.block_env.number, "new block environment received");
 
-            // If no env, skip this run
-            let Some(block_env) = self.block_env.borrow_and_update().clone() else { return };
-            debug!(block_env = ?block_env, "building on block env");
-
-            match self.handle_build(constants, sim_cache, finish_by, block_env).await {
+            // Calculate the deadline for this block simulation.
+            // NB: This must happen _after_ taking a reference to the sim cache,
+            // waiting for a new block, and checking current slot authorization.
+            let finish_by = self.calculate_deadline();
+            let sim_cache = cache.clone();
+            match self
+                .handle_build(constants, sim_cache, finish_by, sim_env.block_env.clone())
+                .await
+            {
                 Ok(block) => {
-                    debug!(block = ?block, "built block");
-                    let _ = submit_sender.send(block);
+                    debug!(block = ?block.block_number(), tx_count = block.transactions().len(), "built simulated block");
+                    let _ = submit_sender.send(SimResult { block, env: sim_env });
                 }
                 Err(e) => {
                     error!(err = %e, "failed to build block");
@@ -184,11 +214,10 @@ impl Simulator {
         let remaining = self.slot_calculator().slot_duration() - timepoint;
 
         // We add a 1500 ms buffer to account for sequencer stopping signing.
-
-        let candidate =
+        let deadline =
             Instant::now() + Duration::from_secs(remaining) - Duration::from_millis(1500);
 
-        candidate.max(Instant::now())
+        deadline.max(Instant::now())
     }
 
     /// Creates an `AlloyDB` instance from the rollup provider.
diff --git a/src/tasks/cache/task.rs b/src/tasks/cache/task.rs
index 5c4fea57..c07e2ced 100644
--- a/src/tasks/cache/task.rs
+++ b/src/tasks/cache/task.rs
@@ -1,3 +1,4 @@
+use crate::tasks::env::SimEnv;
 use alloy::consensus::TxEnvelope;
 use init4_bin_base::deps::tracing::{debug, info};
 use signet_sim::SimCache;
@@ -6,7 +7,6 @@ use tokio::{
     sync::{mpsc, watch},
     task::JoinHandle,
 };
-use trevm::revm::context::BlockEnv;
 
 /// Cache task for the block builder.
 ///
@@ -16,8 +16,7 @@ use trevm::revm::context::BlockEnv;
 #[derive(Debug)]
 pub struct CacheTask {
     /// The channel to receive the block environment.
-    env: watch::Receiver<Option<BlockEnv>>,
-
+    env: watch::Receiver<Option<SimEnv>>,
     /// The channel to receive the transaction bundles.
     bundles: mpsc::UnboundedReceiver<TxCacheBundle>,
     /// The channel to receive the transactions.
@@ -27,7 +26,7 @@ pub struct CacheTask {
 impl CacheTask {
     /// Create a new cache task with the given cache and channels.
     pub const fn new(
-        env: watch::Receiver<Option<BlockEnv>>,
+        env: watch::Receiver<Option<SimEnv>>,
         bundles: mpsc::UnboundedReceiver<TxCacheBundle>,
         txns: mpsc::UnboundedReceiver<TxEnvelope>,
     ) -> Self {
@@ -45,10 +44,10 @@ impl CacheTask {
                         break;
                     }
                     if let Some(env) = self.env.borrow_and_update().as_ref() {
-                        basefee = env.basefee;
-                        info!(basefee, number = env.number, timestamp = env.timestamp, "block env changed, clearing cache");
+                        basefee = env.block_env.basefee;
+                        info!(basefee, env.block_env.number, env.block_env.timestamp, "rollup block env changed, clearing cache");
                         cache.clean(
-                            env.number, env.timestamp
+                            env.block_env.number, env.block_env.timestamp
                         );
                     }
                 }
diff --git a/src/tasks/env.rs b/src/tasks/env.rs
index 448d2c8e..7ec8ad99 100644
--- a/src/tasks/env.rs
+++ b/src/tasks/env.rs
@@ -14,17 +14,28 @@ use trevm::revm::{context::BlockEnv, context_interface::block::BlobExcessGasAndP
 /// A task that constructs a BlockEnv for the next block in the rollup chain.
 #[derive(Debug, Clone)]
 pub struct EnvTask {
+    /// Builder configuration values.
     config: BuilderConfig,
-    provider: RuProvider,
+    /// Rollup provider is used to get the latest rollup block header for simulation.
+    ru_provider: RuProvider,
+}
+
+/// Contains a signet BlockEnv and its corresponding host Header.
+#[derive(Debug, Clone)]
+pub struct SimEnv {
+    /// The signet block environment, for rollup block simulation.
+    pub block_env: BlockEnv,
+    /// The header of the previous rollup block.
+    pub prev_header: Header,
 }
 
 impl EnvTask {
-    /// Create a new EnvTask with the given config and provider.
-    pub const fn new(config: BuilderConfig, provider: RuProvider) -> Self {
-        Self { config, provider }
+    /// Create a new [`EnvTask`] with the given config and providers.
+    pub const fn new(config: BuilderConfig, ru_provider: RuProvider) -> Self {
+        Self { config, ru_provider }
     }
 
-    /// Construct a BlockEnv by making calls to the provider.
+    /// Construct a [`BlockEnv`] by from the previous block header.
     fn construct_block_env(&self, previous: &Header) -> BlockEnv {
         BlockEnv {
             number: previous.number + 1,
@@ -44,10 +55,10 @@ impl EnvTask {
         }
     }
 
-    /// Construct the BlockEnv and send it to the sender.
-    async fn task_fut(self, sender: watch::Sender<Option<BlockEnv>>) {
+    /// Returns a sender that sends [`SimEnv`] for communicating the next block environment.
+    async fn task_fut(self, sender: watch::Sender<Option<SimEnv>>) {
         let span = info_span!("EnvTask::task_fut::init");
-        let mut poller = match self.provider.watch_blocks().instrument(span.clone()).await {
+        let mut poller = match self.ru_provider.watch_blocks().instrument(span.clone()).await {
             Ok(poller) => poller,
             Err(err) => {
                 let _span = span.enter();
@@ -63,44 +74,37 @@ impl EnvTask {
         while let Some(blocks) =
             blocks.next().instrument(info_span!("EnvTask::task_fut::stream")).await
         {
-            let Some(block) = blocks.last() else {
+            let Some(block_hash) = blocks.last() else {
                 // This case occurs when there are no changes to the block,
                 // so we do nothing.
-                debug!("empty filter changes");
                 continue;
             };
-            let span = info_span!("EnvTask::task_fut::loop", hash = %block, number = tracing::field::Empty);
+            let span =
+                info_span!("EnvTask::task_fut::loop", %block_hash, number = tracing::field::Empty);
 
-            let previous = match self
-                .provider
-                .get_block((*block).into())
-                .into_future()
-                .instrument(span.clone())
+            // Get the rollup header for rollup block simulation environment configuration
+            let rollup_header = match self
+                .get_latest_rollup_header(&sender, block_hash, &span)
                 .await
             {
-                Ok(Some(block)) => block.header.inner,
-                Ok(None) => {
-                    let _span = span.enter();
-                    let _ = sender.send(None);
-                    debug!("block not found");
-                    // This may mean the chain had a rollback, so the next poll
-                    // should find something.
-                    continue;
-                }
-                Err(err) => {
-                    let _span = span.enter();
-                    let _ = sender.send(None);
-                    error!(%err, "Failed to get latest block");
-                    // Error may be transient, so we should not break the loop.
+                Some(value) => value,
+                None => {
+                    // If we failed to get the rollup header, we skip this iteration.
+                    debug!(%block_hash, "failed to get rollup header - continuint to next block");
                     continue;
                 }
             };
-            span.record("number", previous.number);
-            debug!("retrieved latest block");
+            debug!(rollup_header.number, "pulled rollup block for simulation");
+            span.record("rollup_block_number", rollup_header.number);
+
+            // Construct the block env using the previous block header
+            let signet_env = self.construct_block_env(&rollup_header);
+            debug!(signet_env.number, signet_env.basefee, "constructed signet block env");
 
-            let env = self.construct_block_env(&previous);
-            debug!(?env, "constructed block env");
-            if sender.send(Some(env)).is_err() {
+            if sender
+                .send(Some(SimEnv { block_env: signet_env, prev_header: rollup_header }))
+                .is_err()
+            {
                 // The receiver has been dropped, so we can stop the task.
                 debug!("receiver dropped, stopping task");
                 break;
@@ -108,8 +112,42 @@ impl EnvTask {
         }
     }
 
+    /// Get latest rollup [`Header`] for the given block hash.
+    async fn get_latest_rollup_header(
+        &self,
+        sender: &watch::Sender<Option<SimEnv>>,
+        block: &alloy::primitives::FixedBytes<32>,
+        span: &tracing::Span,
+    ) -> Option<Header> {
+        let previous = match self
+            .ru_provider
+            .get_block((*block).into())
+            .into_future()
+            .instrument(span.clone())
+            .await
+        {
+            Ok(Some(block)) => block.header.inner,
+            Ok(None) => {
+                let _span = span.enter();
+                let _ = sender.send(None);
+                debug!("rollup block not found");
+                // This may mean the chain had a rollback, so the next poll
+                // should find something.
+                return None;
+            }
+            Err(err) => {
+                let _span = span.enter();
+                let _ = sender.send(None);
+                error!(%err, "Failed to get latest block");
+                // Error may be transient, so we should not break the loop.
+                return None;
+            }
+        };
+        Some(previous)
+    }
+
     /// Spawn the task and return a watch::Receiver for the BlockEnv.
-    pub fn spawn(self) -> (watch::Receiver<Option<BlockEnv>>, JoinHandle<()>) {
+    pub fn spawn(self) -> (watch::Receiver<Option<SimEnv>>, JoinHandle<()>) {
         let (sender, receiver) = watch::channel(None);
         let fut = self.task_fut(sender);
         let jh = tokio::spawn(fut);
diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs
index eee57d51..4715d74a 100644
--- a/src/tasks/mod.rs
+++ b/src/tasks/mod.rs
@@ -10,5 +10,5 @@ pub mod metrics;
 /// Tx submission task
 pub mod submit;
 
-/// Constructs the simualtion environment.
+/// Constructs the simulation environment.
 pub mod env;
diff --git a/src/tasks/submit.rs b/src/tasks/submit.rs
index cab53cbe..5eb92d7a 100644
--- a/src/tasks/submit.rs
+++ b/src/tasks/submit.rs
@@ -4,8 +4,8 @@ use crate::{
     utils::extract_signature_components,
 };
 use alloy::{
-    consensus::{SimpleCoder, constants::GWEI_TO_WEI},
-    eips::BlockNumberOrTag,
+    consensus::{Header, SimpleCoder, constants::GWEI_TO_WEI},
+    eips::{BlockId, BlockNumberOrTag},
     network::{TransactionBuilder, TransactionBuilder4844},
     primitives::{Bytes, FixedBytes, TxHash, U256},
     providers::{Provider as _, SendableTx, WalletProvider},
@@ -25,14 +25,12 @@ use signet_zenith::{
     Zenith::{self, IncorrectHostBlock},
 };
 use std::time::{Instant, UNIX_EPOCH};
-use tokio::{sync::mpsc, task::JoinHandle};
+use tokio::{
+    sync::mpsc::{self},
+    task::JoinHandle,
+};
 
-/// Base maximum fee per gas to use as a starting point for retry bumps
-pub const BASE_FEE_PER_GAS: u128 = 10 * GWEI_TO_WEI as u128;
-/// Base max priority fee per gas to use as a starting point for retry bumps
-pub const BASE_MAX_PRIORITY_FEE_PER_GAS: u128 = 2 * GWEI_TO_WEI as u128;
-/// Base maximum fee per blob gas to use as a starting point for retry bumps
-pub const BASE_MAX_FEE_PER_BLOB_GAS: u128 = GWEI_TO_WEI as u128;
+use crate::tasks::block::sim::SimResult;
 
 macro_rules! spawn_provider_send {
     ($provider:expr, $tx:expr) => {
@@ -197,6 +195,8 @@ pub struct SubmitTask {
     pub config: crate::config::BuilderConfig,
     /// Channel over which to send pending transactions
     pub outbound_tx_channel: mpsc::UnboundedSender<TxHash>,
+    /// Host provider for sending transactions and fetching block & header info
+    pub host_provider: HostProvider,
 }
 
 impl SubmitTask {
@@ -209,20 +209,17 @@ impl SubmitTask {
     /// correct height, chain ID, gas limit, and rollup reward address.
     #[instrument(skip_all)]
     async fn construct_sig_request(&self, contents: &BuiltBlock) -> eyre::Result<SignRequest> {
-        let ru_chain_id = U256::from(self.config.ru_chain_id);
-        let next_block_height = self.next_host_block_height().await?;
-
         Ok(SignRequest {
-            host_block_number: U256::from(next_block_height),
+            host_block_number: U256::from(self.next_host_block_height().await?),
             host_chain_id: U256::from(self.config.host_chain_id),
-            ru_chain_id,
+            ru_chain_id: U256::from(self.config.ru_chain_id),
             gas_limit: U256::from(self.config.rollup_block_gas_limit),
             ru_reward_address: self.config.builder_rewards_address,
             contents: *contents.contents_hash(),
         })
     }
 
-    /// Builds blob transaction and encodes the sidecar for it from the provided header and signature values
+    /// Encodes the sidecar and then builds the 4844 blob transaction from the provided header and signature values.
     fn build_blob_tx(
         &self,
         fills: Vec<FillPermit2>,
@@ -259,8 +256,12 @@ impl SubmitTask {
         resp: &SignResponse,
         block: &BuiltBlock,
     ) -> Result<TransactionRequest, eyre::Error> {
+        // Get the latest host block header for gas estimation
+        let host_header = self.latest_host_header().await?;
+
         // Create the transaction request with the signature values
-        let tx: TransactionRequest = self.new_tx_request(retry_count, resp, block).await?;
+        let tx: TransactionRequest =
+            self.new_tx_request(retry_count, resp, block, host_header).await?;
 
         // Simulate the transaction with a call to the host provider and report any errors
         if let Err(err) = self.sim_with_call(&tx).await {
@@ -270,6 +271,21 @@ impl SubmitTask {
         Ok(tx)
     }
 
+    /// Gets the host header from the host provider by fetching the latest block.
+    async fn latest_host_header(&self) -> eyre::Result<Header> {
+        let previous = self
+            .host_provider
+            .get_block(BlockId::Number(BlockNumberOrTag::Latest))
+            .into_future()
+            .await?;
+        debug!(?previous, "got host block for hash");
+
+        match previous {
+            Some(block) => Ok(block.header.inner),
+            None => Err(eyre::eyre!("host block not found")),
+        }
+    }
+
     /// Simulates the transaction with a call to the host provider to check for reverts.
     async fn sim_with_call(&self, tx: &TransactionRequest) -> eyre::Result<()> {
         match self.provider().call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await {
@@ -288,6 +304,7 @@ impl SubmitTask {
         retry_count: usize,
         resp: &SignResponse,
         block: &BuiltBlock,
+        host_header: Header,
     ) -> Result<TransactionRequest, eyre::Error> {
         // manually retrieve nonce
         let nonce =
@@ -297,14 +314,8 @@ impl SubmitTask {
         // Extract the signature components from the response
         let (v, r, s) = extract_signature_components(&resp.sig);
 
-        // Calculate gas limits based on retry attempts
         let (max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas) =
-            calculate_gas_limits(
-                retry_count,
-                BASE_FEE_PER_GAS,
-                BASE_MAX_PRIORITY_FEE_PER_GAS,
-                BASE_MAX_FEE_PER_BLOB_GAS,
-            );
+            calculate_gas(retry_count, host_header);
 
         // Build the block header
         let header: BlockHeader = BlockHeader {
@@ -314,11 +325,11 @@ impl SubmitTask {
             rewardAddress: resp.req.ru_reward_address,
             blockDataHash: *block.contents_hash(),
         };
-        debug!(?header, "built block header");
+        debug!(?header.hostBlockNumber, "built rollup block header");
 
         // Extract fills from the built block
         let fills = self.extract_fills(block);
-        debug!(fill_count = fills.len(), "extracted fills");
+        debug!(fill_count = fills.len(), "extracted fills from rollup block");
 
         // Create a blob transaction with the blob header and signature values and return it
         let tx = self
@@ -397,6 +408,7 @@ impl SubmitTask {
         debug!(
             host_block_number = %sig_request.host_block_number,
             ru_chain_id = %sig_request.ru_chain_id,
+            tx_count = block.tx_count(),
             "constructed signature request for host block"
         );
 
@@ -413,12 +425,12 @@ impl SubmitTask {
     ) -> eyre::Result<ControlFlow> {
         let mut retries = 0;
         let building_start_time = Instant::now();
+
         let (current_slot, start, end) = self.calculate_slot_window();
         debug!(current_slot, start, end, "calculating target slot window");
 
         // Retry loop
         let result = loop {
-            // Log the retry attempt
             let span = debug_span!("SubmitTask::retrying_handle_inbound", retries);
 
             let inbound_result =
@@ -431,6 +443,7 @@ impl SubmitTask {
                             debug!(slot_number, "403 detected - skipping slot");
                             return Ok(ControlFlow::Skip);
                         } else {
+                            // Otherwise, log error and retry
                             error!(error = %err, "error handling inbound block");
                         }
 
@@ -466,9 +479,15 @@ impl SubmitTask {
         };
 
         // This is reached when `Done` or `Skip` is returned
-        histogram!("builder.block_build_time")
-            .record(building_start_time.elapsed().as_millis() as f64);
-        info!(?result, "finished block building");
+        let elapsed = building_start_time.elapsed().as_millis() as f64;
+        histogram!("builder.block_build_time").record(elapsed);
+        info!(
+            ?result,
+            tx_count = block.tx_count(),
+            block_number = block.block_number(),
+            build_time = ?elapsed,
+            "finished block building"
+        );
         Ok(result)
     }
 
@@ -497,69 +516,84 @@ impl SubmitTask {
         block.host_fills().iter().map(FillPermit2::from).collect()
     }
 
-    /// Task future for the submit task
-    /// NB: This task assumes that the simulator will only send it blocks for
-    /// slots that it's assigned.
-    async fn task_future(self, mut inbound: mpsc::UnboundedReceiver<BuiltBlock>) {
-        // Holds a reference to the last block we attempted to submit
-        let mut last_block_attempted: u64 = 0;
-
+    /// Task future for the submit task. This function runs the main loop of the task.
+    async fn task_future(self, mut inbound: mpsc::UnboundedReceiver<SimResult>) {
         loop {
             // Wait to receive a new block
-            let Some(block) = inbound.recv().await else {
-                debug!("upstream task gone");
+            let Some(sim_result) = inbound.recv().await else {
+                debug!("upstream task gone - exiting submit task");
                 break;
             };
-            debug!(block_number = block.block_number(), ?block, "submit channel received block");
-
-            // Only attempt each block number once
-            if block.block_number() == last_block_attempted {
-                debug!("block number is unchanged from last attempt - skipping");
+            debug!(block_number = sim_result.block.block_number(), "submit channel received block");
+
+            // Don't submit empty blocks
+            if sim_result.block.is_empty() {
+                debug!(
+                    block_number = sim_result.block.block_number(),
+                    "received empty block - skipping"
+                );
                 continue;
             }
 
-            // This means we have encountered a new block, so reset the last block attempted
-            last_block_attempted = block.block_number();
-            debug!(last_block_attempted, "resetting last block attempted");
-
-            if self.retrying_handle_inbound(&block, 3).await.is_err() {
-                debug!("error handling inbound block");
+            if let Err(e) = self.retrying_handle_inbound(&sim_result.block, 3).await {
+                error!(error = %e, "error handling inbound block");
                 continue;
-            };
+            }
         }
     }
 
     /// Spawns the in progress block building task
-    pub fn spawn(self) -> (mpsc::UnboundedSender<BuiltBlock>, JoinHandle<()>) {
-        let (sender, inbound) = mpsc::unbounded_channel();
+    pub fn spawn(self) -> (mpsc::UnboundedSender<SimResult>, JoinHandle<()>) {
+        let (sender, inbound) = mpsc::unbounded_channel::<SimResult>();
         let handle = tokio::spawn(self.task_future(inbound));
-
         (sender, handle)
     }
 }
 
-// Returns gas parameters based on retry counts.
-fn calculate_gas_limits(
+/// Calculates gas parameters based on the block environment and retry count.
+fn calculate_gas(retry_count: usize, prev_header: Header) -> (u128, u128, u128) {
+    let fallback_blob_basefee = 500;
+    let fallback_basefee = 7;
+
+    let base_fee_per_gas = match prev_header.base_fee_per_gas {
+        Some(basefee) => basefee,
+        None => fallback_basefee,
+    };
+
+    let parent_blob_basefee = prev_header.excess_blob_gas.unwrap_or(0) as u128;
+    let blob_basefee = if parent_blob_basefee > 0 {
+        // Use the parent blob base fee if available
+        parent_blob_basefee
+    } else {
+        // Fallback to a default value if no blob base fee is set
+        fallback_blob_basefee
+    };
+
+    bump_gas_from_retries(retry_count, base_fee_per_gas, blob_basefee)
+}
+
+/// Bumps the gas parameters based on the retry count, base fee, and blob base fee.
+pub fn bump_gas_from_retries(
     retry_count: usize,
-    base_max_fee_per_gas: u128,
-    base_max_priority_fee_per_gas: u128,
-    base_max_fee_per_blob_gas: u128,
+    basefee: u64,
+    blob_basefee: u128,
 ) -> (u128, u128, u128) {
-    let bump_multiplier = 1150u128.pow(retry_count as u32); // 15% bump
-    let blob_bump_multiplier = 2000u128.pow(retry_count as u32); // 100% bump (double each time) for blob gas
-    let bump_divisor = 1000u128.pow(retry_count as u32);
+    const PRIORITY_FEE_BASE: u64 = 2 * GWEI_TO_WEI;
+    const BASE_MULTIPLIER: u128 = 2;
+    const BLOB_MULTIPLIER: u128 = 2;
+
+    // Increase priority fee by 20% per retry
+    let priority_fee =
+        PRIORITY_FEE_BASE * (12u64.pow(retry_count as u32) / 10u64.pow(retry_count as u32));
 
-    let max_fee_per_gas = base_max_fee_per_gas * bump_multiplier / bump_divisor;
-    let max_priority_fee_per_gas = base_max_priority_fee_per_gas * bump_multiplier / bump_divisor;
-    let max_fee_per_blob_gas = base_max_fee_per_blob_gas * blob_bump_multiplier / bump_divisor;
+    // Max fee includes basefee + priority + headroom (double basefee, etc.)
+    let max_fee_per_gas = (basefee as u128) * BASE_MULTIPLIER + (priority_fee as u128);
+    let max_fee_per_blob_gas = blob_basefee * BLOB_MULTIPLIER * (retry_count as u128 + 1);
 
     debug!(
         retry_count,
-        max_fee_per_gas,
-        max_priority_fee_per_gas,
-        max_fee_per_blob_gas,
-        "calculated bumped gas parameters"
+        max_fee_per_gas, priority_fee, max_fee_per_blob_gas, "calculated bumped gas parameters"
     );
 
-    (max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
+    (max_fee_per_gas, priority_fee as u128, max_fee_per_blob_gas)
 }
diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs
index a88869be..1317a266 100644
--- a/tests/block_builder_test.rs
+++ b/tests/block_builder_test.rs
@@ -8,13 +8,12 @@ use alloy::{
     signers::local::PrivateKeySigner,
 };
 use builder::{
-    tasks::{block::sim::Simulator, cache::CacheTask},
+    tasks::block::sim::Simulator,
     test_utils::{new_signed_tx, setup_logging, setup_test_config, test_block_env},
 };
 use signet_sim::{SimCache, SimItem};
 use signet_types::constants::SignetSystemConstants;
 use std::time::{Duration, Instant};
-use tokio::{sync::mpsc::unbounded_channel, time::timeout};
 
 /// Tests the `handle_build` method of the `Simulator`.
 ///
@@ -71,60 +70,3 @@ async fn test_handle_build() {
     assert!(got.is_ok());
     assert!(got.unwrap().tx_count() == 2);
 }
-
-/// Tests the full block builder loop, including transaction ingestion and block simulation.
-///
-/// This test sets up a simulated environment using Anvil, creates a block builder,
-/// and verifies that the builder can process incoming transactions and produce a block
-/// within a specified timeout.
-#[ignore = "integration test"]
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn test_spawn() {
-    setup_logging();
-
-    // Make a test config
-    let config = setup_test_config().unwrap();
-    let constants = SignetSystemConstants::pecorino();
-
-    // Create an anvil instance for testing
-    let anvil_instance = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn();
-
-    // Create a wallet
-    let keys = anvil_instance.keys();
-    let test_key_0 = PrivateKeySigner::from_signing_key(keys[0].clone().into());
-    let test_key_1 = PrivateKeySigner::from_signing_key(keys[1].clone().into());
-
-    // Plumb inputs for the test setup
-    let (tx_sender, tx_receiver) = unbounded_channel();
-    let (_, bundle_receiver) = unbounded_channel();
-    let (block_sender, mut block_receiver) = unbounded_channel();
-
-    let env_task = config.env_task();
-    let (block_env, _env_jh) = env_task.spawn();
-
-    let cache_task = CacheTask::new(block_env.clone(), bundle_receiver, tx_receiver);
-    let (sim_cache, _cache_jh) = cache_task.spawn();
-
-    // Create a rollup provider
-    let ru_provider = RootProvider::<Ethereum>::new_http(anvil_instance.endpoint_url());
-
-    let sim = Simulator::new(&config, ru_provider.clone(), block_env);
-
-    // Finally, Kick off the block builder task.
-    sim.spawn_simulator_task(constants, sim_cache.clone(), block_sender);
-
-    // Feed in transactions to the tx_sender and wait for the block to be simulated
-    let tx_1 = new_signed_tx(&test_key_0, 0, U256::from(1_f64), 11_000).unwrap();
-    let tx_2 = new_signed_tx(&test_key_1, 0, U256::from(2_f64), 10_000).unwrap();
-    tx_sender.send(tx_1).unwrap();
-    tx_sender.send(tx_2).unwrap();
-
-    // Wait for a block with timeout
-    let result = timeout(Duration::from_secs(5), block_receiver.recv()).await;
-    assert!(result.is_ok(), "Did not receive block within 5 seconds");
-
-    // Assert on the block
-    let block = result.unwrap();
-    assert!(block.is_some(), "Block channel closed without receiving a block");
-    assert!(block.unwrap().tx_count() == 2); // TODO: Why is this failing? I'm seeing EVM errors but haven't tracked them down yet.
-}