Repository: deltadevsde/prism
Pull request #314: feat: graceful shutdown on prover, and modularized prover components
Author: login: distractedm1nd
Created: 2025-05-23T13:41:03Z | Merged: 2025-05-30T09:03:37Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced modular components for the prover node, including separate modules for syncing, sequencing, and proof generation.
  - Added support for coordinated cancellation and graceful shutdown via cancellation tokens.
  - Expanded configuration options with distinct sections for syncer, sequencer, prover engine, and webserver.

- **Refactor**
  - Refactored the prover architecture for improved modularity and maintainability.
  - Updated internal APIs to encapsulate components and expose new getter methods.

- **Bug Fixes**
  - Improved handling of shutdown signals for safer and cleaner exits.

- **Tests**
  - Updated test configurations and APIs to align with the new modular structure and cancellation support.

- **Chores**
  - Added the `tokio-util` dependency across relevant packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index d6db96c5..e523c031 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7002,6 +7002,7 @@ dependencies = [
  "prism-telemetry-registry",
  "serde",
  "tokio",
+ "tokio-util",
  "toml 0.8.22",
  "tracing",
 ]
@@ -7133,6 +7134,7 @@ dependencies = [
  "serde",
  "sp1-sdk",
  "tokio",
+ "tokio-util",
  "tower-http",
  "tracing",
  "utoipa",
@@ -7229,6 +7231,7 @@ dependencies = [
  "rand 0.8.5",
  "tempfile",
  "tokio",
+ "tokio-util",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ae0f43ef..52385d10 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -85,6 +85,7 @@ redb = "2.4.0"
 async-trait = "0.1.86"
 futures = "0.3.31"
 tokio = { version = "1.16.1", default-features = false }
+tokio-util = "0.7"
 
 # errors
 thiserror = "1.0.62"
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index dc7feaba..e54feea3 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -20,6 +20,7 @@ lumina-node = { workspace = true }
 async-trait = { workspace = true }
 serde = { workspace = true }
 tokio = { workspace = true }
+tokio-util = { workspace = true }
 clap = { workspace = true }
 config = { workspace = true }
 dotenvy = { workspace = true }
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index ef6775fd..6cd8a80d 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -17,6 +17,7 @@ use node_types::NodeType;
 use prism_lightclient::{LightClient, events::EventChannel};
 use prism_prover::Prover;
 use std::sync::Arc;
+use tokio_util::sync::CancellationToken;
 use tracing::{error, info};
 
 pub const SIGNING_KEY_ID: &str = "prism";
@@ -59,6 +60,7 @@ async fn main() -> std::io::Result<()> {
 
     let celestia_config = config.network.celestia_config.clone().unwrap_or_default();
     let start_height = celestia_config.start_height;
+    let cancellation_token = CancellationToken::new();
 
     // Use the metrics registry to record metrics
     if let Some(metrics) = get_metrics() {
@@ -116,17 +118,23 @@ async fn main() -> std::io::Result<()> {
             // When SP1_PROVER is set to mock, disable recursive proofs
             let recursive_proofs = std::env::var("SP1_PROVER").map_or(true, |val| val != "mock");
             let prover_cfg = prism_prover::Config {
-                prover: true,
-                batcher: true,
+                syncer: prism_prover::SyncerConfig {
+                    verifying_key,
+                    start_height,
+                    max_epochless_gap: config.max_epochless_gap,
+                    prover_enabled: true,
+                },
+                sequencer: prism_prover::SequencerConfig {
+                    signing_key,
+                    batcher_enabled: true,
+                },
+                prover_engine: prism_prover::ProverEngineConfig {
+                    recursive_proofs,
+                },
                 webserver: webserver_config.clone().unwrap_or_default(),
-                signing_key,
-                verifying_key,
-                start_height,
-                max_epochless_gap: config.max_epochless_gap,
-                recursive_proofs,
             };
 
-            Arc::new(Prover::new(db, da, &prover_cfg).map_err(|e| {
+            Arc::new(Prover::new(db, da, &prover_cfg, cancellation_token.clone()).map_err(|e| {
                 error!("error initializing prover: {}", e);
                 Error::other(e.to_string())
             })?)
@@ -153,23 +161,49 @@ async fn main() -> std::io::Result<()> {
             // When SP1_PROVER is set to mock, disable recursive proofs
             let recursive_proofs = std::env::var("SP1_PROVER").map_or(true, |val| val != "mock");
             let prover_cfg = prism_prover::Config {
-                prover: false,
-                batcher: true,
+                syncer: prism_prover::SyncerConfig {
+                    verifying_key,
+                    start_height,
+                    max_epochless_gap: config.max_epochless_gap,
+                    prover_enabled: false, // FullNode doesn't generate proofs
+                },
+                sequencer: prism_prover::SequencerConfig {
+                    signing_key,
+                    batcher_enabled: true,
+                },
+                prover_engine: prism_prover::ProverEngineConfig {
+                    recursive_proofs,
+                },
                 webserver: webserver_config.unwrap_or_default(),
-                signing_key,
-                verifying_key,
-                start_height,
-                max_epochless_gap: config.max_epochless_gap,
-                recursive_proofs,
             };
 
-            Arc::new(Prover::new(db, da, &prover_cfg).map_err(|e| {
+            Arc::new(Prover::new(db, da, &prover_cfg, cancellation_token.clone()).map_err(|e| {
                 error!("error initializing prover: {}", e);
                 Error::other(e.to_string())
             })?)
         }
     };
 
+    // Setup signal handling for graceful shutdown
+    let cancellation_for_signal = cancellation_token.clone();
+    tokio::spawn(async move {
+        use tokio::signal::unix::{signal, SignalKind};
+        
+        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
+        let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
+        
+        tokio::select! {
+            _ = sigint.recv() => {
+                info!("Received SIGINT, initiating graceful shutdown");
+            },
+            _ = sigterm.recv() => {
+                info!("Received SIGTERM, initiating graceful shutdown");
+            }
+        }
+        
+        cancellation_for_signal.cancel();
+    });
+
     let result = node.start().await.map_err(|e| Error::other(e.to_string()));
 
     shutdown_telemetry(telemetry_config, meter_provider, log_provider);
diff --git a/crates/node_types/prover/Cargo.toml b/crates/node_types/prover/Cargo.toml
index 59356aa0..67224314 100644
--- a/crates/node_types/prover/Cargo.toml
+++ b/crates/node_types/prover/Cargo.toml
@@ -19,6 +19,7 @@ utoipa-swagger-ui = { workspace = true }
 utoipa-axum = { workspace = true }
 serde = { workspace = true }
 tokio = { workspace = true }
+tokio-util = { workspace = true }
 tracing = { workspace = true }
 anyhow = { workspace = true }
 jmt = { workspace = true }
diff --git a/crates/node_types/prover/src/lib.rs b/crates/node_types/prover/src/lib.rs
index b563d58b..252ed42a 100644
--- a/crates/node_types/prover/src/lib.rs
+++ b/crates/node_types/prover/src/lib.rs
@@ -1,7 +1,11 @@
 pub mod prover;
 pub mod webserver;
+pub mod prover_engine;
+pub mod sequencer;
+pub mod syncer;
 
-pub use prover::{Config, Prover};
+pub use prover::{Config, Prover, SyncerConfig, SequencerConfig, ProverEngineConfig};
+pub use webserver::WebServerConfig;
 
 #[macro_use]
 extern crate tracing;
diff --git a/crates/node_types/prover/src/prover/mod.rs b/crates/node_types/prover/src/prover/mod.rs
index 66ae7a3e..7cebe34d 100644
--- a/crates/node_types/prover/src/prover/mod.rs
+++ b/crates/node_types/prover/src/prover/mod.rs
@@ -1,95 +1,87 @@
 mod timer;
 
-use anyhow::{Context, Result, anyhow, bail};
+use anyhow::{Context, Result, anyhow};
 use async_trait::async_trait;
-use jmt::KeyHash;
 use prism_common::{
-    account::Account,
     api::{
         PendingTransaction, PendingTransactionImpl, PrismApi, PrismApiError,
         types::{AccountResponse, CommitmentResponse, HashedMerkleProof},
     },
-    digest::Digest,
     transaction::Transaction,
 };
-use prism_errors::DataAvailabilityError;
 use prism_keys::{CryptoAlgorithm, SigningKey, VerifyingKey};
 use prism_storage::database::Database;
-use prism_tree::{
-    AccountResponse::*,
-    hasher::TreeHasher,
-    key_directory_tree::KeyDirectoryTree,
-    proofs::{Batch, Proof},
-    snarkable_tree::SnarkableTree,
-};
-use std::{self, collections::VecDeque, sync::Arc};
+use prism_tree::AccountResponse::*;
+use std::sync::Arc;
 use timer::ProverTokioTimer;
-use tokio::{
-    sync::{RwLock, broadcast},
-    task::JoinSet,
-};
-
-use crate::webserver::{WebServer, WebServerConfig};
-use prism_common::operation::Operation;
-use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
-use prism_telemetry_registry::metrics_registry::get_metrics;
-use sp1_sdk::{
-    EnvProver, HashableKey as _, ProverClient, SP1Proof, SP1ProofWithPublicValues, SP1ProvingKey,
-    SP1Stdin, SP1VerifyingKey,
+use tokio::{sync::RwLock, task::JoinSet};
+use tokio_util::sync::CancellationToken;
+
+use crate::{
+    prover_engine::ProverEngine,
+    sequencer::Sequencer,
+    syncer::Syncer,
+    webserver::{WebServer, WebServerConfig},
 };
+use prism_da::DataAvailabilityLayer;
 
 /// Maximum number of DA heights the prover will wait before posting a gapfiller proof
 pub const DEFAULT_MAX_EPOCHLESS_GAP: u64 = 300;
-pub const BASE_PRISM_ELF: &[u8] =
-    include_bytes!("../../../../../elf/base-riscv32im-succinct-zkvm-elf");
-pub const RECURSIVE_PRISM_ELF: &[u8] =
-    include_bytes!("../../../../../elf/recursive-riscv32im-succinct-zkvm-elf");
 
 #[derive(Clone)]
-pub struct Config {
-    /// Enables generating [`FinalizedEpoch`]s and posting them to the DA
-    /// layer. When deactivated, the node will simply sync historical and
-    /// incoming [`FinalizedEpoch`]s.
-    pub prover: bool,
-
-    /// Enables accepting incoming transactions from the webserver and posting batches to the DA layer.
-    /// When deactivated, the node will reject incoming transactions.
-    pub batcher: bool,
-
-    /// Configuration for the webserver.
-    pub webserver: WebServerConfig,
-
-    /// Key used to sign new [`FinalizedEpochs`].
-    pub signing_key: SigningKey,
-
+pub struct SyncerConfig {
     /// Key used to verify incoming [`FinalizedEpochs`].
-    /// This is not necessarily the counterpart to signing_key, as fullnodes must use the [`verifying_key`] of the prover.
     pub verifying_key: VerifyingKey,
-
     /// DA layer height the prover should start syncing transactions from.
     pub start_height: u64,
-    ///
     /// Maximum DA height gap between two epochs; If exceeded, the prover will
     /// repost the last epoch to aid LN syncing.
     pub max_epochless_gap: u64,
+    /// Enables generating [`FinalizedEpoch`]s and posting them to the DA layer.
+    pub prover_enabled: bool,
+}
+
+#[derive(Clone)]
+pub struct SequencerConfig {
+    /// Key used to sign new [`FinalizedEpochs`].
+    pub signing_key: SigningKey,
+    /// Enables accepting incoming transactions from the webserver and posting batches to the DA layer.
+    pub batcher_enabled: bool,
+}
 
-    /// Whether recursive proofs should be enabled - defaults to false, unless SP1_PROVER env var is set to "mock"
+#[derive(Clone)]
+pub struct ProverEngineConfig {
+    /// Whether recursive proofs should be enabled
     pub recursive_proofs: bool,
 }
 
+#[derive(Clone)]
+pub struct Config {
+    pub syncer: SyncerConfig,
+    pub sequencer: SequencerConfig,
+    pub prover_engine: ProverEngineConfig,
+    pub webserver: WebServerConfig,
+}
+
 impl Default for Config {
     fn default() -> Self {
         let signing_key = SigningKey::new_ed25519();
 
         Config {
-            prover: true,
-            batcher: true,
+            syncer: SyncerConfig {
+                verifying_key: signing_key.verifying_key(),
+                start_height: 1,
+                max_epochless_gap: DEFAULT_MAX_EPOCHLESS_GAP,
+                prover_enabled: true,
+            },
+            sequencer: SequencerConfig {
+                signing_key,
+                batcher_enabled: true,
+            },
+            prover_engine: ProverEngineConfig {
+                recursive_proofs: false,
+            },
             webserver: WebServerConfig::default(),
-            signing_key: signing_key.clone(),
-            verifying_key: signing_key.verifying_key(),
-            start_height: 1,
-            max_epochless_gap: DEFAULT_MAX_EPOCHLESS_GAP,
-            recursive_proofs: false,
         }
     }
 }
@@ -107,39 +99,21 @@ impl Config {
         let signing_key =
             SigningKey::new_with_algorithm(algorithm).context("Failed to create signing key")?;
 
-        Ok(Config {
-            signing_key: signing_key.clone(),
-            verifying_key: signing_key.verifying_key(),
-            recursive_proofs: false,
-            ..Config::default()
-        })
+        let mut config = Config::default();
+        config.syncer.verifying_key = signing_key.verifying_key();
+        config.sequencer.signing_key = signing_key;
+        Ok(config)
     }
 }
 
 #[allow(dead_code)]
 pub struct Prover {
-    pub db: Arc<dyn Database>,
-    pub da: Arc<dyn DataAvailabilityLayer>,
-
     pub cfg: Config,
-
-    /// [`pending_transactions`] is a buffer for transactions that have not yet been
-    /// posted to the DA layer.
-    pub pending_transactions: Arc<RwLock<Vec<Transaction>>>,
-
-    /// [`tree`] is the representation of the JMT, prism's state tree. It is accessed via the [`db`].
-    tree: Arc<RwLock<KeyDirectoryTree<Box<dyn Database>>>>,
-
-    base_prover_client: Arc<RwLock<EnvProver>>,
-    base_proving_key: SP1ProvingKey,
-    base_verifying_key: SP1VerifyingKey,
-
-    recursive_prover_client: Arc<RwLock<EnvProver>>,
-    recursive_proving_key: SP1ProvingKey,
-    recursive_verifying_key: SP1VerifyingKey,
-
-    /// The DA height of the latest epoch.
+    prover_engine: Arc<ProverEngine>,
+    sequencer: Arc<Sequencer>,
+    syncer: Arc<Syncer>,
     latest_epoch_da_height: Arc<RwLock<u64>>,
+    cancellation_token: CancellationToken,
 }
 
 #[allow(dead_code)]
@@ -148,526 +122,139 @@ impl Prover {
         db: Arc<Box<dyn Database>>,
         da: Arc<dyn DataAvailabilityLayer>,
         cfg: &Config,
+        cancellation_token: CancellationToken,
     ) -> Result<Prover> {
-        let saved_epoch = match db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => {
-                debug!("no existing epoch state found, starting at epoch 0");
-                0
-            }
-        };
+        let latest_epoch_da_height = Arc::new(RwLock::new(0));
 
-        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(db.clone(), saved_epoch)));
+        let prover_engine = Arc::new(ProverEngine::new(&cfg.prover_engine)?);
 
-        // Create separate prover clients for base and recursive proofs
-        let base_prover_client = ProverClient::from_env();
-        let recursive_prover_client = ProverClient::from_env();
+        let sequencer = Arc::new(Sequencer::new(
+            db.clone(),
+            da.clone(),
+            &cfg.sequencer,
+            latest_epoch_da_height.clone(),
+        )?);
 
-        // Setup keys for both provers
-        let (base_pk, base_vk) = base_prover_client.setup(BASE_PRISM_ELF);
-        let (recursive_pk, recursive_vk) = recursive_prover_client.setup(RECURSIVE_PRISM_ELF);
+        let syncer = Arc::new(Syncer::new(
+            da,
+            db,
+            &cfg.syncer,
+            latest_epoch_da_height.clone(),
+            sequencer.clone(),
+            prover_engine.clone(),
+        ));
 
         Ok(Prover {
-            db: db.clone(),
-            da,
             cfg: cfg.clone(),
-            base_proving_key: base_pk,
-            base_verifying_key: base_vk,
-            recursive_proving_key: recursive_pk,
-            recursive_verifying_key: recursive_vk,
-            base_prover_client: Arc::new(RwLock::new(base_prover_client)),
-            recursive_prover_client: Arc::new(RwLock::new(recursive_prover_client)),
-            tree,
-            pending_transactions: Arc::new(RwLock::new(Vec::new())),
-            latest_epoch_da_height: Arc::new(RwLock::new(0)),
+            prover_engine,
+            sequencer,
+            syncer,
+            latest_epoch_da_height,
+            cancellation_token,
         })
     }
 
-    pub async fn run(self: Arc<Self>) -> Result<()> {
-        self.da
-            .start()
-            .await
-            .map_err(|e| DataAvailabilityError::InitializationError(e.to_string()))
-            .context("Failed to start DataAvailabilityLayer")?;
-
-        let main_loop = self.clone().main_loop();
-
-        let mut futures = JoinSet::new();
-        futures.spawn(main_loop);
-
-        if self.cfg.batcher {
-            let batch_poster = self.clone().post_batch_loop();
-            futures.spawn(batch_poster);
-        }
-
-        let ws = WebServer::new(self.cfg.webserver.clone(), self.clone());
-        if self.cfg.webserver.enabled {
-            futures.spawn(async move { ws.start().await });
-        }
-
-        if let Some(result) = futures.join_next().await {
-            error!("Service exited unexpectedly: {:?}", result);
-            Err(anyhow!("Service exited unexpectedly"))?
-        }
-        error!("All services have ended unexpectedly.");
-        Err(anyhow!("All services have ended unexpectedly"))?
-    }
-
-    async fn main_loop(self: Arc<Self>) -> Result<()> {
-        let mut height_rx = self.da.subscribe_to_heights();
-        let historical_sync_height = height_rx.recv().await?;
-
-        let start_height = match self.db.get_last_synced_height() {
-            Ok(height) => height,
-            Err(_) => {
-                debug!("no existing sync height found, setting sync height to start_height");
-                self.db.set_last_synced_height(&self.cfg.start_height)?;
-                self.cfg.start_height
-            }
-        };
-
-        self.sync_loop(start_height, historical_sync_height, height_rx).await
-    }
-
-    async fn sync_loop(
-        &self,
-        start_height: u64,
-        end_height: u64,
-        mut incoming_heights: broadcast::Receiver<u64>,
-    ) -> Result<()> {
-        // TODO: Should be persisted in database for crash recovery
-        let mut buffered_transactions: VecDeque<Transaction> = VecDeque::new();
-        let mut current_height = start_height;
-
-        while current_height <= end_height {
-            self.process_da_height(current_height, &mut buffered_transactions, false).await?;
-            // TODO: Race between set_epoch and set_last_synced_height
-            self.db.set_last_synced_height(&current_height)?;
-            current_height += 1;
-        }
-
-        info!(
-            "finished historical sync from height {} to {}",
-            start_height, end_height
-        );
-
-        loop {
-            let height = incoming_heights.recv().await?;
-            if height != current_height {
-                return Err(anyhow!(
-                    "heights are not sequential: expected {}, got {}",
-                    current_height,
-                    height
-                ));
-            }
-            self.process_da_height(height, &mut buffered_transactions, true).await?;
-            current_height += 1;
-            // TODO: Race between set_epoch and set_last_synced_height - updating these should be a single atomic transaction
-            self.db.set_last_synced_height(&current_height)?;
-        }
-    }
-
-    async fn process_da_height(
-        &self,
-        height: u64,
-        buffered_transactions: &mut VecDeque<Transaction>,
-        is_real_time: bool,
-    ) -> Result<()> {
-        let next_epoch_height = match self.db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => 0,
-        };
-
-        let transactions = self.da.get_transactions(height).await?;
-        let epoch_result = self.da.get_finalized_epoch(height).await?;
-
-        debug!(
-            "processing {} height {}, next_epoch_height: {}",
-            if is_real_time { "new" } else { "old" },
-            height,
-            next_epoch_height
-        );
-
-        if let Some(epoch) = epoch_result {
-            // run all buffered transactions from the last celestia blocks and increment current_epoch
-            self.process_epoch(epoch, buffered_transactions).await?;
-        } else {
-            debug!("No transactions to process at height {}", height);
-        }
-
-        if is_real_time && !buffered_transactions.is_empty() && self.cfg.prover {
-            let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-            self.finalize_new_epoch(next_epoch_height, all_transactions).await?;
-        }
-
-        // If there are new transactions at this height, add them to the queue to
-        // be included in the next finalized epoch.
-        if !transactions.is_empty() {
-            buffered_transactions.extend(transactions);
-            return Ok(());
-        }
-
-        // post gap filler proof if max gap has been reached
-        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
-        if latest_epoch_height != 0
-            && height.saturating_sub(latest_epoch_height) >= self.cfg.max_epochless_gap
-        {
-            self.finalize_new_epoch(next_epoch_height, Vec::new()).await?;
-        }
-
-        if let Some(metrics) = get_metrics() {
-            metrics.record_celestia_synced_height(height, vec![]);
-            metrics.record_current_epoch(next_epoch_height, vec![]);
-        }
-
-        Ok(())
+    pub fn get_db(&self) -> Arc<Box<dyn Database>> {
+        self.sequencer.get_db()
     }
 
-    async fn process_epoch(
+    pub async fn execute_block(
         &self,
-        epoch: FinalizedEpoch,
-        buffered_transactions: &mut VecDeque<Transaction>,
-    ) -> Result<()> {
-        let current_epoch = match self.db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => 0,
-        };
-
-        // If prover is enabled and is actively producing new epochs, it has
-        // likely already ran all of the transactions in the found epoch, so no
-        // further processing is needed
-        if epoch.height < current_epoch {
-            debug!("epoch {} already processed internally", current_epoch);
-            return Ok(());
-        }
-
-        // TODO: Issue #144
-        epoch
-            .verify_signature(self.cfg.verifying_key.clone())
-            .with_context(|| format!("Invalid signature in epoch {}", epoch.height))?;
-        trace!("valid signature for epoch {}", epoch.height);
-
-        let prev_commitment = if epoch.height == 0 {
-            self.get_commitment_from_tree().await?
-        } else {
-            self.db.get_epoch(&epoch.height.saturating_sub(1))?.current_commitment
-        };
-
-        if epoch.height != current_epoch {
-            return Err(anyhow!(
-                "epoch height mismatch: expected {}, got {}",
-                current_epoch,
-                epoch.height
-            ));
-        }
-
-        if epoch.prev_commitment != prev_commitment {
-            return Err(anyhow!(
-                "previous commitment mismatch at epoch {}",
-                current_epoch
-            ));
-        }
-
-        let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-        if !all_transactions.is_empty() {
-            self.execute_block(all_transactions).await?;
-        }
-
-        let new_commitment = self.get_commitment_from_tree().await?;
-        if epoch.current_commitment != new_commitment {
-            return Err(anyhow!(
-                "new commitment mismatch at epoch {}",
-                current_epoch
-            ));
-        }
-
-        // distinguish between base and recursive proofs for client and verifying key
-        let client = if epoch.height == 0 || !self.cfg.recursive_proofs {
-            self.base_prover_client.read().await
-        } else {
-            self.recursive_prover_client.read().await
-        };
-
-        let verifying_key = if epoch.height == 0 || !self.cfg.recursive_proofs {
-            &self.base_verifying_key
-        } else {
-            &self.recursive_verifying_key
-        };
-
-        match client.verify(&epoch.proof, verifying_key) {
-            Ok(_) => info!(
-                "zkSNARK for epoch {} was validated successfully",
-                epoch.height
-            ),
-            Err(err) => panic!(
-                "failed to validate epoch at height {}: {:?}",
-                epoch.height, err
-            ),
-        }
-
-        debug!(
-            "processed epoch {}. new commitment: {:?}",
-            current_epoch, new_commitment
-        );
-
-        self.db.add_epoch(&epoch)?;
-
-        Ok(())
-    }
-
-    // should only be called for testing and historical sync, it does not
-    // generate the Batch object for proof generation.
-    async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
-        debug!("executing block with {} transactions", transactions.len());
-
-        let mut proofs = Vec::new();
-
-        for transaction in transactions {
-            match self.process_transaction(transaction.clone()).await {
-                Ok(proof) => proofs.push(proof),
-                Err(e) => {
-                    // Log the error and continue with the next transaction
-                    warn!(
-                        "Failed to process transaction: {:?}. Error: {}",
-                        transaction, e
-                    );
-                }
-            }
-        }
-
-        Ok(proofs)
+        transactions: Vec<Transaction>,
+    ) -> Result<Vec<prism_tree::proofs::Proof>> {
+        self.sequencer.execute_block(transactions).await
     }
 
-    /// Finalizes a new epoch by processing the given transactions, proving the epoch,
-    /// and submitting it to the data availability layer.
-    /// Returns the height on the DA layer where the epoch was submitted.
-    async fn finalize_new_epoch(
+    pub async fn finalize_new_epoch(
         &self,
         epoch_height: u64,
         transactions: Vec<Transaction>,
     ) -> Result<u64> {
-        let mut tree = self.tree.write().await;
-        let batch = tree.process_batch(transactions)?;
-        batch.verify()?;
-
-        let finalized_epoch = self.prove_epoch(epoch_height, &batch).await?;
-
-        let da_height = self.da.submit_finalized_epoch(finalized_epoch.clone()).await?;
-        let mut latest_da_height = self.latest_epoch_da_height.write().await;
-        *latest_da_height = da_height;
-
-        // only save the epoch locally if it was successfully submitted
-        self.db.add_epoch(&finalized_epoch)?;
-
-        info!("finalized new epoch at height {}", epoch_height);
+        self.sequencer.finalize_new_epoch(epoch_height, transactions, &self.prover_engine).await
+    }
 
-        Ok(da_height)
+    pub async fn validate_and_queue_update(&self, transaction: Transaction) -> Result<()> {
+        self.sequencer.validate_and_queue_update(transaction).await
     }
 
-    async fn prove_with_base_prover(
-        &self,
-        epoch_height: u64,
-        batch: &Batch,
-    ) -> Result<(
-        SP1ProofWithPublicValues,
-        SP1ProofWithPublicValues,
-        tokio::sync::RwLockReadGuard<'_, EnvProver>,
-        &SP1VerifyingKey,
-    )> {
-        let mut stdin = SP1Stdin::new();
-        stdin.write(batch);
-
-        let client = self.base_prover_client.read().await;
-        info!("generating proof for epoch {}", epoch_height);
-
-        let proof = client.prove(&self.base_proving_key, &stdin).groth16().run()?;
-        info!(
-            "successfully generated base proof for epoch {}",
-            epoch_height
-        );
-
-        let compressed_proof = client.prove(&self.base_proving_key, &stdin).compressed().run()?;
-        info!(
-            "successfully generated compressed proof for epoch {}",
-            epoch_height
-        );
-
-        Ok((proof, compressed_proof, client, &self.base_verifying_key))
+    pub fn get_pending_transactions(&self) -> Arc<RwLock<Vec<Transaction>>> {
+        self.sequencer.get_pending_transactions()
     }
 
-    async fn prove_with_recursive_prover(
+    pub async fn process_transaction(
         &self,
-        epoch_height: u64,
-        batch: &Batch,
-    ) -> Result<(
-        SP1ProofWithPublicValues,
-        SP1ProofWithPublicValues,
-        tokio::sync::RwLockReadGuard<'_, EnvProver>,
-        &SP1VerifyingKey,
-    )> {
-        let prev_epoch = match self.db.get_latest_epoch() {
-            Ok(epoch) => epoch,
-            Err(_) => {
-                return Err(anyhow!(
-                    "Previous epoch not found for recursive verification at height {}",
-                    epoch_height - 1
-                ));
-            }
-        };
-
-        let vk_to_use = if prev_epoch.height == 0 {
-            self.base_verifying_key.clone()
-        } else {
-            self.recursive_verifying_key.clone()
-        };
-
-        let mut stdin = SP1Stdin::new();
-        // Write recursive inputs
-        let compressed_proof = match prev_epoch.compressed_proof.proof {
-            SP1Proof::Compressed(proof) => proof,
-            _ => return Err(anyhow!("Invalid proof type: expected compressed proof")),
-        };
-        stdin.write_proof(*compressed_proof, vk_to_use.clone().vk);
-        stdin.write_vec(prev_epoch.public_values.to_vec());
-        stdin.write(&vk_to_use.hash_u32());
-        stdin.write(batch);
-
-        let client = self.recursive_prover_client.read().await;
-        info!(
-            "generating recursive proof for epoch at height {}",
-            epoch_height
-        );
-
-        let proof = client.prove(&self.recursive_proving_key, &stdin).groth16().run()?;
-        info!(
-            "successfully generated recursive proof for epoch {}",
-            epoch_height
-        );
-        let compressed_proof =
-            client.prove(&self.recursive_proving_key, &stdin).compressed().run()?;
-        info!(
-            "successfully generated recursive compressed proof for epoch {}",
-            epoch_height
-        );
-
-        Ok((
-            proof,
-            compressed_proof,
-            client,
-            &self.recursive_verifying_key,
-        ))
+        transaction: Transaction,
+    ) -> Result<prism_tree::proofs::Proof> {
+        self.sequencer.process_transaction(transaction).await
     }
 
-    async fn prove_epoch(&self, epoch_height: u64, batch: &Batch) -> Result<FinalizedEpoch> {
-        let (proof, compressed_proof, client, verifying_key) =
-            if epoch_height == 0 || !self.cfg.recursive_proofs {
-                self.prove_with_base_prover(epoch_height, batch).await?
-            } else {
-                self.prove_with_recursive_prover(epoch_height, batch).await?
-            };
-
-        client.verify(&proof, verifying_key)?;
-        info!("verified proof for epoch {}", epoch_height);
-
-        let public_values = proof.public_values.to_vec();
-
-        let mut epoch_json = FinalizedEpoch {
-            height: epoch_height,
-            prev_commitment: batch.prev_root,
-            current_commitment: batch.new_root,
-            proof,
-            compressed_proof,
-            public_values,
-            signature: None,
-        };
-
-        epoch_json.insert_signature(&self.cfg.signing_key)?;
-        Ok(epoch_json)
+    pub fn get_da(&self) -> Arc<dyn DataAvailabilityLayer> {
+        self.syncer.get_da()
     }
 
-    async fn post_batch_loop(self: Arc<Self>) -> Result<()> {
-        let mut height_rx = self.da.subscribe_to_heights();
 
-        loop {
-            let height = height_rx.recv().await?;
-            trace!("received height {}", height);
+    pub async fn run(self: Arc<Self>) -> Result<()> {
+        let mut futures = JoinSet::new();
 
-            // Get pending transactions
-            let pending_transactions = {
-                let mut ops = self.pending_transactions.write().await;
-                std::mem::take(&mut *ops)
-            };
+        // Start Syncer (includes DA startup and main sync loop)
+        let syncer = self.syncer.clone();
+        let cancel_token = self.cancellation_token.clone();
+        futures.spawn(async move { syncer.start(cancel_token).await });
 
-            let tx_count = pending_transactions.len();
+        // Start Sequencer (batch poster if enabled)
+        let sequencer = self.sequencer.clone();
+        let cancel_token = self.cancellation_token.clone();
+        futures.spawn(async move { sequencer.start(cancel_token).await });
 
-            // If there are pending transactions, submit them
-            if !pending_transactions.clone().is_empty() {
-                match self.da.submit_transactions(pending_transactions).await {
-                    Ok(submitted_height) => {
-                        info!(
-                            "post_batch_loop: submitted {} transactions at height {}",
-                            tx_count, submitted_height
-                        );
+        // Start WebServer if enabled
+        if self.cfg.webserver.enabled {
+            let ws = WebServer::new(self.cfg.webserver.clone(), self.clone());
+            futures.spawn(async move { ws.start().await });
+        }
+
+        // Wait for any service to exit
+        let exit_result = if let Some(result) = futures.join_next().await {
+            match result {
+                Ok(service_result) => match service_result {
+                    Ok(_) => {
+                        info!("Service exited gracefully, shutting down other components");
+                        self.cancellation_token.cancel();
+                        Ok(())
                     }
-                    Err(e) => {
-                        error!("post_batch_loop: Failed to submit transactions: {}", e);
+                    Err(service_error) => {
+                        error!(
+                            "Service exited with error: {:?}, shutting down other components",
+                            service_error
+                        );
+                        self.cancellation_token.cancel();
+                        Err(service_error)
                     }
+                },
+                Err(join_error) => {
+                    error!(
+                        "Task join error: {:?}, shutting down other components",
+                        join_error
+                    );
+                    self.cancellation_token.cancel();
+                    Err(anyhow!("Task join error: {}", join_error))
                 }
-            } else {
-                debug!(
-                    "post_batch_loop: No pending transactions to submit at height {}",
-                    height
-                );
-            }
-        }
-    }
-
-    async fn get_commitment_from_tree(&self) -> Result<Digest> {
-        let tree = self.tree.read().await;
-        tree.get_commitment().context("Failed to get commitment")
-    }
-
-    async fn get_account_from_tree(&self, id: &str) -> Result<prism_tree::AccountResponse> {
-        let tree = self.tree.read().await;
-        let key_hash = KeyHash::with::<TreeHasher>(id);
-
-        tree.get(key_hash)
-    }
-
-    /// Updates the state from an already verified pending transaction.
-    async fn process_transaction(&self, transaction: Transaction) -> Result<Proof> {
-        let mut tree = self.tree.write().await;
-        tree.process_transaction(transaction)
-    }
-
-    /// Adds an transaction to be posted to the DA layer and applied in the next epoch.
-    pub async fn validate_and_queue_update(&self, transaction: Transaction) -> Result<()> {
-        if !self.cfg.batcher {
-            bail!("Batcher is disabled, cannot queue transactions");
-        }
-
-        // validate against existing account if necessary, including signature checks
-        match transaction.operation {
-            Operation::RegisterService { .. } | Operation::CreateAccount { .. } => {
-                Account::default().process_transaction(&transaction)?;
             }
-            Operation::AddKey { .. }
-            | Operation::RevokeKey { .. }
-            | Operation::AddData { .. }
-            | Operation::SetData { .. } => {
-                let account_response = self.get_account_from_tree(&transaction.id).await?;
-
-                let Found(mut account, _) = account_response else {
-                    bail!("Account not found for id: {}", transaction.id)
-                };
+        } else {
+            error!("No futures in join set, shutting down");
+            Ok(())
+        };
 
-                account.process_transaction(&transaction)?;
+        // Wait for all other components to finish gracefully
+        while let Some(result) = futures.join_next().await {
+            match result {
+                Ok(Ok(_)) => debug!("Component shut down gracefully"),
+                Ok(Err(e)) => warn!("Component shut down with error: {:?}", e),
+                Err(e) => warn!("Component join error during shutdown: {:?}", e),
             }
-        };
+        }
 
-        let mut pending = self.pending_transactions.write().await;
-        pending.push(transaction);
-        Ok(())
+        info!("Prover shutdown complete");
+        exit_result
     }
 }
 
@@ -676,7 +263,7 @@ impl PrismApi for Prover {
     type Timer = ProverTokioTimer;
 
     async fn get_account(&self, id: &str) -> Result<AccountResponse, PrismApiError> {
-        let acc_response = match self.get_account_from_tree(id).await? {
+        let acc_response = match self.sequencer.get_account(id).await? {
             Found(account, inclusion_proof) => {
                 let hashed_inclusion_proof = inclusion_proof.hashed();
                 AccountResponse {
@@ -702,7 +289,7 @@ impl PrismApi for Prover {
     }
 
     async fn get_commitment(&self) -> Result<CommitmentResponse, PrismApiError> {
-        let commitment = self.get_commitment_from_tree().await?;
+        let commitment = self.sequencer.get_commitment().await?;
         Ok(CommitmentResponse { commitment })
     }
 
@@ -710,7 +297,7 @@ impl PrismApi for Prover {
         &self,
         transaction: Transaction,
     ) -> Result<impl PendingTransaction<Timer = Self::Timer>, PrismApiError> {
-        self.validate_and_queue_update(transaction.clone()).await?;
+        self.sequencer.validate_and_queue_update(transaction.clone()).await?;
         Ok(PendingTransactionImpl::new(self, transaction))
     }
 }
diff --git a/crates/node_types/prover/src/prover/tests.rs b/crates/node_types/prover/src/prover/tests.rs
index e568813b..64c19acf 100644
--- a/crates/node_types/prover/src/prover/tests.rs
+++ b/crates/node_types/prover/src/prover/tests.rs
@@ -1,8 +1,10 @@
 use super::*;
 use prism_common::test_transaction_builder::TestTransactionBuilder;
 use prism_keys::{CryptoAlgorithm, SigningKey, VerifyingKey};
+use prism_tree::proofs::Proof;
 use std::{self, sync::Arc, time::Duration};
 use tokio::spawn;
+use tokio_util::sync::CancellationToken;
 
 use prism_da::memory::InMemoryDataAvailabilityLayer;
 use prism_storage::inmemory::InMemoryDatabase;
@@ -12,11 +14,9 @@ async fn create_test_prover(algorithm: CryptoAlgorithm) -> Arc<Prover> {
     let (da_layer, _rx, _brx) = InMemoryDataAvailabilityLayer::new(1);
     let da_layer = Arc::new(da_layer);
     let db: Arc<Box<dyn Database>> = Arc::new(Box::new(InMemoryDatabase::new()));
-    let cfg = Config {
-        max_epochless_gap: 5,
-        ..Config::default_with_key_algorithm(algorithm).unwrap()
-    };
-    Arc::new(Prover::new(db.clone(), da_layer, &cfg).unwrap())
+    let mut cfg = Config::default_with_key_algorithm(algorithm).unwrap();
+    cfg.syncer.max_epochless_gap = 5;
+    Arc::new(Prover::new(db.clone(), da_layer, &cfg, CancellationToken::new()).unwrap())
 }
 
 fn create_mock_transactions(algorithm: CryptoAlgorithm, service_id: String) -> Vec<Transaction> {
@@ -44,7 +44,7 @@ async fn test_posts_epoch_after_max_gap(algorithm: CryptoAlgorithm) {
         prover_handle.run().await.unwrap();
     });
 
-    let mut rx = prover.da.subscribe_to_heights();
+    let mut rx = prover.get_da().subscribe_to_heights();
 
     // Wait for initial blocks to be produced
     loop {
@@ -55,7 +55,7 @@ async fn test_posts_epoch_after_max_gap(algorithm: CryptoAlgorithm) {
     }
 
     // Ensure no gap proof has been created
-    assert!(prover.da.get_finalized_epoch(0).await.unwrap().is_none());
+    assert!(prover.get_da().get_finalized_epoch(0).await.unwrap().is_none());
 
     // Create and submit transactions
     let test_transactions = create_mock_transactions(algorithm, "test_service".to_string());
@@ -76,7 +76,7 @@ async fn test_posts_epoch_after_max_gap(algorithm: CryptoAlgorithm) {
             break;
         }
     }
-    let initial_epoch = prover.da.get_finalized_epoch(initial_epoch_height).await.unwrap().unwrap();
+    let initial_epoch = prover.get_da().get_finalized_epoch(initial_epoch_height).await.unwrap().unwrap();
 
     // Wait for gap length
     loop {
@@ -96,7 +96,7 @@ async fn test_posts_epoch_after_max_gap(algorithm: CryptoAlgorithm) {
         }
     }
     // Verify gap proof contents
-    let gap_proof = prover.da.get_finalized_epoch(current_epoch_height).await.unwrap().unwrap();
+    let gap_proof = prover.get_da().get_finalized_epoch(current_epoch_height).await.unwrap().unwrap();
     assert_eq!(
         gap_proof.height,
         initial_epoch.height + 1,
@@ -119,7 +119,8 @@ async fn test_validate_and_queue_update(algorithm: CryptoAlgorithm) {
 
     prover.clone().validate_and_queue_update(transaction.clone()).await.unwrap();
 
-    let pending_transactions = prover.pending_transactions.read().await;
+    let pending_tx_arc = prover.get_pending_transactions();
+    let pending_transactions = pending_tx_arc.read().await;
     assert_eq!(pending_transactions.len(), 2);
 }
 
@@ -212,7 +213,7 @@ async fn test_restart_sync_from_scratch(algorithm: CryptoAlgorithm) {
     let db1: Arc<Box<dyn Database>> = Arc::new(Box::new(InMemoryDatabase::new()));
     let db2: Arc<Box<dyn Database>> = Arc::new(Box::new(InMemoryDatabase::new()));
     let cfg = Config::default_with_key_algorithm(algorithm).unwrap();
-    let prover = Arc::new(Prover::new(db1.clone(), da_layer.clone(), &cfg).unwrap());
+    let prover = Arc::new(Prover::new(db1.clone(), da_layer.clone(), &cfg, CancellationToken::new()).unwrap());
 
     let runner = prover.clone();
     spawn(async move {
@@ -222,7 +223,7 @@ async fn test_restart_sync_from_scratch(algorithm: CryptoAlgorithm) {
     let transactions = create_mock_transactions(algorithm, "test_service".to_string());
 
     for transaction in transactions {
-        prover.clone().validate_and_queue_update(transaction).await.unwrap();
+        prover.validate_and_queue_update(transaction).await.unwrap();
         while let Ok(new_block) = brx.recv().await {
             if new_block.epoch.is_some() {
                 break;
@@ -230,14 +231,14 @@ async fn test_restart_sync_from_scratch(algorithm: CryptoAlgorithm) {
         }
     }
 
-    assert_eq!(prover.clone().db.get_latest_epoch_height().unwrap(), 3);
+    assert_eq!(prover.get_db().get_latest_epoch_height().unwrap(), 3);
 
-    let prover2 = Arc::new(Prover::new(db2.clone(), da_layer.clone(), &cfg).unwrap());
+    let prover2 = Arc::new(Prover::new(db2.clone(), da_layer.clone(), &cfg, CancellationToken::new()).unwrap());
     let runner = prover2.clone();
     spawn(async move { runner.run().await.unwrap() });
 
     loop {
-        let epoch = prover2.clone().db.get_latest_epoch_height();
+        let epoch = prover2.get_db().get_latest_epoch_height();
         if epoch.is_ok() && epoch.unwrap() == 3 {
             assert_eq!(
                 prover.get_commitment().await.unwrap(),
@@ -254,7 +255,7 @@ async fn test_load_persisted_state(algorithm: CryptoAlgorithm) {
     let da_layer = Arc::new(da_layer);
     let db: Arc<Box<dyn Database>> = Arc::new(Box::new(InMemoryDatabase::new()));
     let cfg = Config::default_with_key_algorithm(algorithm).unwrap();
-    let prover = Arc::new(Prover::new(db.clone(), da_layer.clone(), &cfg).unwrap());
+    let prover = Arc::new(Prover::new(db.clone(), da_layer.clone(), &cfg, CancellationToken::new()).unwrap());
 
     let runner = prover.clone();
     spawn(async move {
@@ -264,7 +265,7 @@ async fn test_load_persisted_state(algorithm: CryptoAlgorithm) {
     let transactions = create_mock_transactions(algorithm, "test_service".to_string());
 
     for transaction in transactions {
-        prover.clone().validate_and_queue_update(transaction).await.unwrap();
+        prover.validate_and_queue_update(transaction).await.unwrap();
         while let Ok(new_block) = brx.recv().await {
             if new_block.epoch.is_some() {
                 break;
@@ -272,12 +273,12 @@ async fn test_load_persisted_state(algorithm: CryptoAlgorithm) {
         }
     }
 
-    assert_eq!(prover.clone().db.get_latest_epoch_height().unwrap(), 3);
+    assert_eq!(prover.get_db().get_latest_epoch_height().unwrap(), 3);
 
-    let prover2 = Arc::new(Prover::new(db.clone(), da_layer.clone(), &cfg).unwrap());
+    let prover2 = Arc::new(Prover::new(db.clone(), da_layer.clone(), &cfg, CancellationToken::new()).unwrap());
     let runner = prover2.clone();
     spawn(async move { runner.run().await.unwrap() });
-    let epoch = prover2.clone().db.get_latest_epoch_height().unwrap();
+    let epoch = prover2.get_db().get_latest_epoch_height().unwrap();
     assert_eq!(epoch, 3);
     assert_eq!(
         prover.get_commitment().await.unwrap(),
diff --git a/crates/node_types/prover/src/prover_engine.rs b/crates/node_types/prover/src/prover_engine.rs
new file mode 100644
index 00000000..621ae7fc
--- /dev/null
+++ b/crates/node_types/prover/src/prover_engine.rs
@@ -0,0 +1,185 @@
+use anyhow::{Result, anyhow};
+use prism_storage::database::Database;
+use prism_tree::proofs::Batch;
+use sp1_sdk::{
+    EnvProver, HashableKey as _, ProverClient, SP1Proof, SP1ProofWithPublicValues, SP1ProvingKey,
+    SP1Stdin, SP1VerifyingKey,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+pub const BASE_PRISM_ELF: &[u8] =
+    include_bytes!("../../../../elf/base-riscv32im-succinct-zkvm-elf");
+pub const RECURSIVE_PRISM_ELF: &[u8] =
+    include_bytes!("../../../../elf/recursive-riscv32im-succinct-zkvm-elf");
+
+#[derive(Clone)]
+pub struct ProverEngine {
+    base_prover_client: Arc<RwLock<EnvProver>>,
+    base_proving_key: SP1ProvingKey,
+    base_verifying_key: SP1VerifyingKey,
+
+    recursive_prover_client: Arc<RwLock<EnvProver>>,
+    recursive_proving_key: SP1ProvingKey,
+    recursive_verifying_key: SP1VerifyingKey,
+
+    recursive_proofs_enabled: bool,
+}
+
+impl ProverEngine {
+    pub fn new(config: &crate::prover::ProverEngineConfig) -> Result<Self> {
+        let base_prover_client = ProverClient::from_env();
+        let recursive_prover_client = ProverClient::from_env();
+
+        let (base_pk, base_vk) = base_prover_client.setup(BASE_PRISM_ELF);
+        let (recursive_pk, recursive_vk) = recursive_prover_client.setup(RECURSIVE_PRISM_ELF);
+
+        Ok(ProverEngine {
+            base_proving_key: base_pk,
+            base_verifying_key: base_vk,
+            recursive_proving_key: recursive_pk,
+            recursive_verifying_key: recursive_vk,
+            base_prover_client: Arc::new(RwLock::new(base_prover_client)),
+            recursive_prover_client: Arc::new(RwLock::new(recursive_prover_client)),
+            recursive_proofs_enabled: config.recursive_proofs,
+        })
+    }
+
+    pub async fn prove_epoch(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+        db: &Arc<Box<dyn Database>>,
+    ) -> Result<(SP1ProofWithPublicValues, SP1ProofWithPublicValues)> {
+        let (proof, compressed_proof, client, verifying_key) =
+            if epoch_height == 0 || !self.recursive_proofs_enabled {
+                self.prove_with_base_prover(epoch_height, batch).await?
+            } else {
+                self.prove_with_recursive_prover(epoch_height, batch, db).await?
+            };
+
+        client.verify(&proof, verifying_key)?;
+        info!("verified proof for epoch {}", epoch_height);
+
+        Ok((proof, compressed_proof))
+    }
+
+    async fn prove_with_base_prover(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+    ) -> Result<(
+        SP1ProofWithPublicValues,
+        SP1ProofWithPublicValues,
+        tokio::sync::RwLockReadGuard<'_, EnvProver>,
+        &SP1VerifyingKey,
+    )> {
+        let mut stdin = SP1Stdin::new();
+        stdin.write(batch);
+
+        let client = self.base_prover_client.read().await;
+        info!("generating proof for epoch {}", epoch_height);
+
+        let proof = client.prove(&self.base_proving_key, &stdin).groth16().run()?;
+        info!(
+            "successfully generated base proof for epoch {}",
+            epoch_height
+        );
+
+        let compressed_proof = client.prove(&self.base_proving_key, &stdin).compressed().run()?;
+        info!(
+            "successfully generated compressed proof for epoch {}",
+            epoch_height
+        );
+
+        Ok((proof, compressed_proof, client, &self.base_verifying_key))
+    }
+
+    async fn prove_with_recursive_prover(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+        db: &Arc<Box<dyn Database>>,
+    ) -> Result<(
+        SP1ProofWithPublicValues,
+        SP1ProofWithPublicValues,
+        tokio::sync::RwLockReadGuard<'_, EnvProver>,
+        &SP1VerifyingKey,
+    )> {
+        let prev_epoch = match db.get_latest_epoch() {
+            Ok(epoch) => epoch,
+            Err(_) => {
+                return Err(anyhow!(
+                    "Previous epoch not found for recursive verification at height {}",
+                    epoch_height - 1
+                ));
+            }
+        };
+
+        let vk_to_use = if prev_epoch.height == 0 {
+            self.base_verifying_key.clone()
+        } else {
+            self.recursive_verifying_key.clone()
+        };
+
+        let mut stdin = SP1Stdin::new();
+        let compressed_proof = match prev_epoch.compressed_proof.proof {
+            SP1Proof::Compressed(proof) => proof,
+            _ => return Err(anyhow!("Invalid proof type: expected compressed proof")),
+        };
+        stdin.write_proof(*compressed_proof, vk_to_use.clone().vk);
+        stdin.write_vec(prev_epoch.public_values.to_vec());
+        stdin.write(&vk_to_use.hash_u32());
+        stdin.write(batch);
+
+        let client = self.recursive_prover_client.read().await;
+        info!(
+            "generating recursive proof for epoch at height {}",
+            epoch_height
+        );
+
+        let proof = client.prove(&self.recursive_proving_key, &stdin).groth16().run()?;
+        info!(
+            "successfully generated recursive proof for epoch {}",
+            epoch_height
+        );
+        let compressed_proof =
+            client.prove(&self.recursive_proving_key, &stdin).compressed().run()?;
+        info!(
+            "successfully generated recursive compressed proof for epoch {}",
+            epoch_height
+        );
+
+        Ok((
+            proof,
+            compressed_proof,
+            client,
+            &self.recursive_verifying_key,
+        ))
+    }
+
+    pub fn get_verifying_key(&self, epoch_height: u64) -> &SP1VerifyingKey {
+        if epoch_height == 0 || !self.recursive_proofs_enabled {
+            &self.base_verifying_key
+        } else {
+            &self.recursive_verifying_key
+        }
+    }
+
+    pub async fn verify_epoch_proof(
+        &self,
+        epoch_height: u64,
+        proof: &SP1ProofWithPublicValues,
+    ) -> Result<()> {
+        let client = if epoch_height == 0 || !self.recursive_proofs_enabled {
+            self.base_prover_client.read().await
+        } else {
+            self.recursive_prover_client.read().await
+        };
+
+        let verifying_key = self.get_verifying_key(epoch_height);
+
+        client.verify(proof, verifying_key)?;
+        Ok(())
+    }
+}
diff --git a/crates/node_types/prover/src/sequencer.rs b/crates/node_types/prover/src/sequencer.rs
new file mode 100644
index 00000000..97c3eb3c
--- /dev/null
+++ b/crates/node_types/prover/src/sequencer.rs
@@ -0,0 +1,223 @@
+use anyhow::{Context, Result, bail};
+use jmt::KeyHash;
+use prism_common::{
+    account::Account, digest::Digest, operation::Operation, transaction::Transaction,
+};
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::SigningKey;
+use prism_storage::database::Database;
+use prism_tree::{
+    AccountResponse::*, hasher::TreeHasher, key_directory_tree::KeyDirectoryTree, proofs::Proof,
+    snarkable_tree::SnarkableTree,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tokio_util::sync::CancellationToken;
+
+use crate::prover_engine::ProverEngine;
+
+#[derive(Clone)]
+pub struct Sequencer {
+    db: Arc<Box<dyn Database>>,
+    da: Arc<dyn DataAvailabilityLayer>,
+    tree: Arc<RwLock<KeyDirectoryTree<Box<dyn Database>>>>,
+    pending_transactions: Arc<RwLock<Vec<Transaction>>>,
+    signing_key: SigningKey,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    batcher_enabled: bool,
+}
+
+impl Sequencer {
+    pub fn new(
+        db: Arc<Box<dyn Database>>,
+        da: Arc<dyn DataAvailabilityLayer>,
+        config: &crate::prover::SequencerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+    ) -> Result<Self> {
+        let saved_epoch = match db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => {
+                debug!("no existing epoch state found, starting at epoch 0");
+                0
+            }
+        };
+
+        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(db.clone(), saved_epoch)));
+
+        Ok(Sequencer {
+            db,
+            da,
+            tree,
+            pending_transactions: Arc::new(RwLock::new(Vec::new())),
+            signing_key: config.signing_key.clone(),
+            latest_epoch_da_height,
+            batcher_enabled: config.batcher_enabled,
+        })
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        if self.batcher_enabled {
+            self.run_batch_poster(cancellation_token).await
+        } else {
+            // Sequencer without batcher doesn't need a running loop
+            // Just wait for cancellation
+            cancellation_token.cancelled().await;
+            info!("Sequencer: Gracefully stopped (batcher disabled)");
+            Ok(())
+        }
+    }
+
+    async fn run_batch_poster(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+
+        loop {
+            tokio::select! {
+                height_result = height_rx.recv() => {
+                    let height = height_result?;
+                    trace!("received height {}", height);
+
+                    let pending_transactions = {
+                        let mut ops = self.pending_transactions.write().await;
+                        std::mem::take(&mut *ops)
+                    };
+
+                    let tx_count = pending_transactions.len();
+
+                    if tx_count > 0 {
+                        match self.da.submit_transactions(pending_transactions).await {
+                            Ok(submitted_height) => {
+                                info!(
+                                    "post_batch_loop: submitted {} transactions at height {}",
+                                    tx_count, submitted_height
+                                );
+                            }
+                            Err(e) => {
+                                error!("post_batch_loop: Failed to submit transactions: {}", e);
+                            }
+                        }
+                    } else {
+                        debug!(
+                            "post_batch_loop: No pending transactions to submit at height {}",
+                            height
+                        );
+                    }
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Sequencer: Gracefully stopping batch poster");
+                    return Ok(());
+                }
+            }
+        }
+    }
+
+    pub async fn finalize_new_epoch(
+        &self,
+        epoch_height: u64,
+        transactions: Vec<Transaction>,
+        prover_engine: &Arc<ProverEngine>,
+    ) -> Result<u64> {
+        let mut tree = self.tree.write().await;
+        let batch = tree.process_batch(transactions)?;
+        batch.verify()?;
+
+        let (proof, compressed_proof) =
+            prover_engine.prove_epoch(epoch_height, &batch, &self.db).await?;
+
+        let public_values = proof.public_values.to_vec();
+
+        let mut epoch_json = FinalizedEpoch {
+            height: epoch_height,
+            prev_commitment: batch.prev_root,
+            current_commitment: batch.new_root,
+            proof,
+            compressed_proof,
+            public_values,
+            signature: None,
+        };
+
+        epoch_json.insert_signature(&self.signing_key)?;
+
+        let da_height = self.da.submit_finalized_epoch(epoch_json.clone()).await?;
+        let mut latest_da_height = self.latest_epoch_da_height.write().await;
+        *latest_da_height = da_height;
+
+        self.db.add_epoch(&epoch_json)?;
+
+        info!("finalized new epoch at height {}", epoch_height);
+
+        Ok(da_height)
+    }
+
+    pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
+        debug!("executing block with {} transactions", transactions.len());
+
+        let mut proofs = Vec::new();
+
+        for transaction in transactions {
+            match self.process_transaction(transaction.clone()).await {
+                Ok(proof) => proofs.push(proof),
+                Err(e) => {
+                    warn!(
+                        "Failed to process transaction: {:?}. Error: {}",
+                        transaction, e
+                    );
+                }
+            }
+        }
+
+        Ok(proofs)
+    }
+
+    pub async fn validate_and_queue_update(&self, transaction: Transaction) -> Result<()> {
+        if !self.batcher_enabled {
+            bail!("Batcher is disabled, cannot queue transactions");
+        }
+
+        match transaction.operation {
+            Operation::RegisterService { .. } | Operation::CreateAccount { .. } => {
+                Account::default().process_transaction(&transaction)?;
+            }
+            Operation::AddKey { .. }
+            | Operation::RevokeKey { .. }
+            | Operation::AddData { .. }
+            | Operation::SetData { .. } => {
+                let account_response = self.get_account(&transaction.id).await?;
+
+                let Found(mut account, _) = account_response else {
+                    bail!("Account not found for id: {}", transaction.id)
+                };
+
+                account.process_transaction(&transaction)?;
+            }
+        };
+
+        let mut pending = self.pending_transactions.write().await;
+        pending.push(transaction);
+        Ok(())
+    }
+
+    pub async fn get_commitment(&self) -> Result<Digest> {
+        let tree = self.tree.read().await;
+        tree.get_commitment().context("Failed to get commitment")
+    }
+
+    pub async fn get_account(&self, id: &str) -> Result<prism_tree::AccountResponse> {
+        let tree = self.tree.read().await;
+        let key_hash = KeyHash::with::<TreeHasher>(id);
+
+        tree.get(key_hash)
+    }
+
+    pub fn get_pending_transactions(&self) -> Arc<RwLock<Vec<Transaction>>> {
+        self.pending_transactions.clone()
+    }
+
+    pub fn get_db(&self) -> Arc<Box<dyn Database>> {
+        self.db.clone()
+    }
+
+    pub async fn process_transaction(&self, transaction: Transaction) -> Result<Proof> {
+        let mut tree = self.tree.write().await;
+        tree.process_transaction(transaction)
+    }
+}
diff --git a/crates/node_types/prover/src/syncer.rs b/crates/node_types/prover/src/syncer.rs
new file mode 100644
index 00000000..95943604
--- /dev/null
+++ b/crates/node_types/prover/src/syncer.rs
@@ -0,0 +1,270 @@
+use anyhow::{Context, Result, anyhow};
+use prism_common::transaction::Transaction;
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::VerifyingKey;
+use prism_storage::database::Database;
+use prism_telemetry_registry::metrics_registry::get_metrics;
+use std::{collections::VecDeque, sync::Arc};
+use tokio::sync::{RwLock, broadcast};
+use tokio_util::sync::CancellationToken;
+
+use crate::{prover_engine::ProverEngine, sequencer::Sequencer};
+
+#[derive(Clone)]
+pub struct Syncer {
+    da: Arc<dyn DataAvailabilityLayer>,
+    db: Arc<Box<dyn Database>>,
+    verifying_key: VerifyingKey,
+    max_epochless_gap: u64,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    start_height: u64,
+    sequencer: Arc<Sequencer>,
+    prover_engine: Arc<ProverEngine>,
+    is_prover_enabled: bool,
+}
+
+impl Syncer {
+    pub fn new(
+        da: Arc<dyn DataAvailabilityLayer>,
+        db: Arc<Box<dyn Database>>,
+        config: &crate::prover::SyncerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+        sequencer: Arc<Sequencer>,
+        prover_engine: Arc<ProverEngine>,
+    ) -> Self {
+        Self {
+            da,
+            db,
+            verifying_key: config.verifying_key.clone(),
+            max_epochless_gap: config.max_epochless_gap,
+            latest_epoch_da_height,
+            start_height: config.start_height,
+            sequencer,
+            prover_engine,
+            is_prover_enabled: config.prover_enabled,
+        }
+    }
+
+    pub fn get_da(&self) -> Arc<dyn DataAvailabilityLayer> {
+        self.da.clone()
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        self.da.start().await?;
+        self.run_main_loop(cancellation_token).await
+    }
+
+    async fn run_main_loop(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+        let historical_sync_height = height_rx.recv().await?;
+
+        let sync_start_height = match self.db.get_last_synced_height() {
+            Ok(height) => height,
+            Err(_) => {
+                debug!("no existing sync height found, setting sync height to start_height");
+                self.db.set_last_synced_height(&self.start_height)?;
+                self.start_height
+            }
+        };
+
+        self.sync_loop(
+            sync_start_height,
+            historical_sync_height,
+            height_rx,
+            cancellation_token,
+        )
+        .await
+    }
+
+    async fn sync_loop(
+        &self,
+        start_height: u64,
+        end_height: u64,
+        mut incoming_heights: broadcast::Receiver<u64>,
+        cancellation_token: CancellationToken,
+    ) -> Result<()> {
+        let mut buffered_transactions: VecDeque<Transaction> = VecDeque::new();
+        let mut current_height = start_height;
+
+        while current_height <= end_height {
+            tokio::select! {
+                result = self.process_da_height(
+                    current_height,
+                    &mut buffered_transactions,
+                    false,
+                ) => {
+                    result?;
+                    self.db.set_last_synced_height(&current_height)?;
+                    current_height += 1;
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Syncer: Gracefully stopping during historical sync at height {}", current_height);
+                    return Ok(());
+                }
+            }
+        }
+
+        info!(
+            "finished historical sync from height {} to {}",
+            start_height, end_height
+        );
+
+        loop {
+            tokio::select! {
+                height_result = incoming_heights.recv() => {
+                    let height = height_result?;
+                    if height != current_height {
+                        return Err(anyhow!(
+                            "heights are not sequential: expected {}, got {}",
+                            current_height,
+                            height
+                        ));
+                    }
+                    self.process_da_height(
+                        height,
+                        &mut buffered_transactions,
+                        true,
+                    ).await?;
+                    current_height += 1;
+                    self.db.set_last_synced_height(&current_height)?;
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Syncer: Gracefully stopping during real-time sync at height {}", current_height);
+                    return Ok(());
+                }
+            }
+        }
+    }
+
+    async fn process_da_height(
+        &self,
+        height: u64,
+        buffered_transactions: &mut VecDeque<Transaction>,
+        is_real_time: bool,
+    ) -> Result<()> {
+        let next_epoch_height = match self.db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => 0,
+        };
+
+        let transactions = self.da.get_transactions(height).await?;
+        let epoch_result = self.da.get_finalized_epoch(height).await?;
+
+        debug!(
+            "processing {} height {}, next_epoch_height: {}",
+            if is_real_time { "new" } else { "old" },
+            height,
+            next_epoch_height
+        );
+
+        if let Some(epoch) = epoch_result {
+            self.process_epoch(epoch, buffered_transactions).await?;
+        } else {
+            debug!("No transactions to process at height {}", height);
+        }
+
+        if is_real_time && !buffered_transactions.is_empty() && self.is_prover_enabled {
+            let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
+            self.sequencer
+                .finalize_new_epoch(next_epoch_height, all_transactions, &self.prover_engine)
+                .await?;
+        }
+
+        if !transactions.is_empty() {
+            buffered_transactions.extend(transactions);
+            return Ok(());
+        }
+
+        // Create a gap epoch if necessary
+        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
+        if latest_epoch_height != 0
+            && height.saturating_sub(latest_epoch_height) >= self.max_epochless_gap
+        {
+            self.sequencer
+                .finalize_new_epoch(next_epoch_height, Vec::new(), &self.prover_engine)
+                .await?;
+        }
+
+        if let Some(metrics) = get_metrics() {
+            metrics.record_celestia_synced_height(height, vec![]);
+            metrics.record_current_epoch(next_epoch_height, vec![]);
+        }
+
+        Ok(())
+    }
+
+    async fn process_epoch(
+        &self,
+        epoch: FinalizedEpoch,
+        buffered_transactions: &mut VecDeque<Transaction>,
+    ) -> Result<()> {
+        let current_epoch = match self.db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => 0,
+        };
+
+        if epoch.height < current_epoch {
+            debug!("epoch {} already processed internally", current_epoch);
+            return Ok(());
+        }
+
+        epoch
+            .verify_signature(self.verifying_key.clone())
+            .with_context(|| format!("Invalid signature in epoch {}", epoch.height))?;
+        trace!("valid signature for epoch {}", epoch.height);
+
+        let prev_commitment = if epoch.height == 0 {
+            self.sequencer.get_commitment().await?
+        } else {
+            self.db.get_epoch(&epoch.height.saturating_sub(1))?.current_commitment
+        };
+
+        if epoch.height != current_epoch {
+            return Err(anyhow!(
+                "epoch height mismatch: expected {}, got {}",
+                current_epoch,
+                epoch.height
+            ));
+        }
+
+        if epoch.prev_commitment != prev_commitment {
+            return Err(anyhow!(
+                "previous commitment mismatch at epoch {}",
+                current_epoch
+            ));
+        }
+
+        let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
+        if !all_transactions.is_empty() {
+            self.sequencer.execute_block(all_transactions).await?;
+        }
+
+        let new_commitment = self.sequencer.get_commitment().await?;
+        if epoch.current_commitment != new_commitment {
+            return Err(anyhow!(
+                "new commitment mismatch at epoch {}",
+                current_epoch
+            ));
+        }
+
+        match self.prover_engine.verify_epoch_proof(epoch.height, &epoch.proof).await {
+            Ok(_) => info!(
+                "zkSNARK for epoch {} was validated successfully",
+                epoch.height
+            ),
+            Err(err) => panic!(
+                "failed to validate epoch at height {}: {:?}",
+                epoch.height, err
+            ),
+        }
+
+        debug!(
+            "processed epoch {}. new commitment: {:?}",
+            current_epoch, new_commitment
+        );
+
+        self.db.add_epoch(&epoch)?;
+
+        Ok(())
+    }
+}
diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml
index 3e302f27..5f3bdb03 100644
--- a/crates/tests/Cargo.toml
+++ b/crates/tests/Cargo.toml
@@ -20,6 +20,7 @@ prism-storage.workspace = true
 prism-prover = { workspace = true }
 prism-keys = { workspace = true }
 prism-lightclient = { workspace = true }
+tokio-util = { workspace = true }
 prism-da.workspace = true
 rand.workspace = true
 tokio.workspace = true
diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs
index 36fe50dd..fa52d803 100644
--- a/crates/tests/src/lib.rs
+++ b/crates/tests/src/lib.rs
@@ -75,15 +75,27 @@ async fn test_light_client_prover_talking() -> Result<()> {
     let pubkey = signing_key.verifying_key();
 
     let prover_cfg = prism_prover::Config {
-        signing_key,
-        recursive_proofs: false,
-        ..prism_prover::Config::default()
+        syncer: prism_prover::SyncerConfig {
+            verifying_key: pubkey.clone(),
+            start_height: 0,
+            max_epochless_gap: 300,
+            prover_enabled: true,
+        },
+        sequencer: prism_prover::SequencerConfig {
+            signing_key,
+            batcher_enabled: true,
+        },
+        prover_engine: prism_prover::ProverEngineConfig {
+            recursive_proofs: false,
+        },
+        webserver: prism_prover::WebServerConfig::default(),
     };
 
     let prover = Arc::new(Prover::new(
         db.clone(),
         bridge_da_layer.clone(),
         &prover_cfg,
+        tokio_util::sync::CancellationToken::new(),
     )?);
 
     let event_channel = EventChannel::new();


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 9**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>crates/cli/src/main.rs (1)</summary><blockquote>

`207-212`: _🛠️ Refactor suggestion_

**Consider waiting for graceful shutdown to complete.**

The code initiates cancellation but doesn't wait for components to finish shutting down before cleaning up telemetry. This could lead to incomplete shutdown or telemetry data loss.

```diff
-let result = node.start().await.map_err(|e| Error::other(e.to_string()));
+let result = tokio::select! {
+    res = node.start() => res.map_err(|e| Error::other(e.to_string())),
+    _ = cancellation_token.cancelled() => {
+        info!("Shutdown signal received, waiting for graceful shutdown...");
+        // Give components time to finish their shutdown procedures
+        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+        Ok(())
+    }
+};

 shutdown_telemetry(telemetry_config, meter_provider, log_provider);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>crates/tests/src/lib.rs (1)</summary><blockquote>

`98-99`: **Consider implementing graceful shutdown for cleaner test execution.**

The test creates a `CancellationToken` but never uses it. Since you've added the infrastructure for graceful shutdown, consider implementing it to ensure clean test teardown.

After line 227, add:

```diff
 // Wait for transaction generator to complete
 tx_handle.await?;

-// We could add code to gracefully shut down the prover and light client here
-// but for test purposes, we'll just return
+// Gracefully shut down the prover and light client
+tokio_util::sync::CancellationToken::new().cancel();
+// Give components time to shut down cleanly
+tokio::time::sleep(Duration::from_secs(1)).await;
```

Note: You'll need to pass the same cancellation token to both the prover and potentially create a mechanism for the light client to respect it as well.


Also applies to: 229-231

</blockquote></details>
<details>
<summary>crates/node_types/prover/src/prover/tests.rs (1)</summary><blockquote>

`216-216`: **Consider reusing cancellation tokens in tests for better resource cleanup.**

While creating new cancellation tokens for each prover instance works, you could improve test cleanup by sharing tokens and canceling them at test end.

Consider creating a shared cancellation token at the beginning of each test and canceling it at the end:

```diff
 async fn test_restart_sync_from_scratch(algorithm: CryptoAlgorithm) {
+    let cancellation_token = CancellationToken::new();
     let (da_layer, _rx, mut brx) = InMemoryDataAvailabilityLayer::new(1);
     // ...
-    let prover = Arc::new(Prover::new(db1.clone(), da_layer.clone(), &cfg, CancellationToken::new()).unwrap());
+    let prover = Arc::new(Prover::new(db1.clone(), da_layer.clone(), &cfg, cancellation_token.clone()).unwrap());
     // ...
-    let prover2 = Arc::new(Prover::new(db2.clone(), da_layer.clone(), &cfg, CancellationToken::new()).unwrap());
+    let prover2 = Arc::new(Prover::new(db2.clone(), da_layer.clone(), &cfg, cancellation_token.clone()).unwrap());
     // ...
+    // At test end:
+    cancellation_token.cancel();
 }
```


Also applies to: 236-236, 258-258, 278-278

</blockquote></details>
<details>
<summary>crates/node_types/prover/src/prover_engine.rs (2)</summary><blockquote>

`31-32`: **Consider using separate configurations for base and recursive provers.**

Both prover clients are initialized using `ProverClient::from_env()`, which means they share the same environment configuration. Consider allowing separate configurations for base and recursive provers, as they might have different resource requirements.

---

`61-63`: **Proof verification uses the same system that generated it.**

The proof is verified using the same client and verifying key that generated it. While this is good for sanity checking, consider adding an option for independent verification.

</blockquote></details>
<details>
<summary>crates/node_types/prover/src/prover/mod.rs (2)</summary><blockquote>

`14-14`: **Avoid wildcard imports for better code clarity.**

Instead of using wildcard imports, explicitly import the variants you need. This improves code readability and makes it easier to track dependencies.

Based on the usage in the file, apply this diff:

```diff
-use prism_tree::AccountResponse::*;
+use prism_tree::AccountResponse::{Found, NotFound};
```

---

`196-213`: **Consider adding service identifiers for better error tracking.**

The current implementation doesn't identify which service (Syncer, Sequencer, or WebServer) exits or fails. For better operational monitoring, consider wrapping the service results with identifiers.

Example approach using a wrapper enum:

```rust
enum ServiceResult {
    Syncer(Result<()>),
    Sequencer(Result<()>),
    WebServer(Result<()>),
}

// Then in the spawn calls:
futures.spawn(async move { 
    ServiceResult::Syncer(syncer.start(cancel_token).await) 
});
```

This would allow you to log which specific service exited or failed.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 20fc82baa3796b991a098d019127bb6b6402feda and 6b332100b6da696a58993b3cf30fa000f9795a7b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `Cargo.toml` (1 hunks)
* `crates/cli/Cargo.toml` (1 hunks)
* `crates/cli/src/main.rs` (4 hunks)
* `crates/node_types/prover/Cargo.toml` (1 hunks)
* `crates/node_types/prover/src/lib.rs` (1 hunks)
* `crates/node_types/prover/src/prover/mod.rs` (5 hunks)
* `crates/node_types/prover/src/prover/tests.rs` (11 hunks)
* `crates/node_types/prover/src/prover_engine.rs` (1 hunks)
* `crates/node_types/prover/src/sequencer.rs` (1 hunks)
* `crates/node_types/prover/src/syncer.rs` (1 hunks)
* `crates/tests/Cargo.toml` (1 hunks)
* `crates/tests/src/lib.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>crates/node_types/prover/src/prover_engine.rs (3)</summary><blockquote>

<details>
<summary>crates/node_types/prover/src/prover/mod.rs (1)</summary>

* `new` (121-155)

</details>
<details>
<summary>crates/node_types/prover/src/sequencer.rs (1)</summary>

* `new` (37-65)

</details>
<details>
<summary>crates/node_types/prover/src/syncer.rs (1)</summary>

* `new` (27-46)

</details>

</blockquote></details>
<details>
<summary>crates/node_types/prover/src/syncer.rs (3)</summary><blockquote>

<details>
<summary>crates/telemetry/src/metrics_registry.rs (1)</summary>

* `get_metrics` (100-108)

</details>
<details>
<summary>crates/node_types/prover/src/prover/mod.rs (1)</summary>

* `new` (121-155)

</details>
<details>
<summary>crates/node_types/prover/src/sequencer.rs (2)</summary>

* `new` (37-65)
* `start` (67-77)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: integration-test
* GitHub Check: unused dependencies
* GitHub Check: unit-test
* GitHub Check: build-and-push-image

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>Cargo.toml (1)</summary>

`88-88`: <details>
<summary>✅ Verification successful</summary>

**Verify tokio-util version and approve the dependency addition.**

The addition of `tokio-util` to support `CancellationToken` functionality for graceful shutdown is appropriate. However, please verify that version 0.7 is current and secure.



---

🌐 Web query:

```web
What is the latest version of tokio-util crate and are there any known security vulnerabilities in version 0.7?
```

💡 Result:

As of May 23, 2025, the latest version of the `tokio-util` crate is 0.7.15, released on March 13, 2025. ([lib.rs](https://lib.rs/crates/tokio-util?utm_source=openai))

Regarding version 0.7, there are no known security vulnerabilities associated with it. The crate has undergone multiple audits, with the most recent review indicating it is safe to deploy. ([lib.rs](https://lib.rs/crates/tokio-util/audit?utm_source=openai))

It's important to note that while `tokio-util` is a utility crate for the Tokio runtime, it does not directly implement networking features. Therefore, any security vulnerabilities in networking components would likely be found in the core `tokio` crate or other related crates.

For example, a known vulnerability in the `tokio` crate (CVE-2023-22466) affected versions 1.7.0 through 1.18.3, where configuring a Windows named pipe server could unintentionally allow remote clients if not properly configured. This issue was addressed in versions 1.18.4, 1.20.3, and 1.23.1 of the `tokio` crate. ([osv.dev](https://osv.dev/vulnerability/CVE-2023-22466?utm_source=openai))

To ensure your project remains secure, it's advisable to keep all dependencies, including `tokio` and `tokio-util`, up to date and monitor their respective changelogs for any security advisories.

Citations:

- 1: https://lib.rs/crates/tokio-util?utm_source=openai
- 2: https://lib.rs/crates/tokio-util/audit?utm_source=openai
- 3: https://osv.dev/vulnerability/CVE-2023-22466?utm_source=openai

---



</details>

**Approve addition of `tokio-util = "0.7"`**

Tokio-util’s 0.7 series is still current (latest patch is 0.7.15) and no known security vulnerabilities affect it. The workspace dependency can be approved.

• Location: `Cargo.toml`, line 88

</details>
<details>
<summary>crates/node_types/prover/Cargo.toml (1)</summary>

`22-22`: **LGTM! Consistent workspace dependency usage.**

The addition of `tokio-util` as a workspace dependency follows the correct pattern and supports the modular prover architecture with cancellation token functionality.

</details>
<details>
<summary>crates/cli/Cargo.toml (1)</summary>

`23-23`: **LGTM! Consistent workspace dependency pattern.**

The workspace dependency usage is consistent across the project and enables the CLI to utilize cancellation tokens for coordinated shutdown management.

</details>
<details>
<summary>crates/tests/Cargo.toml (1)</summary>

`23-23`: **LGTM! Enables test compatibility with new prover API.**

The workspace dependency addition ensures tests can work with the updated prover constructor that requires cancellation tokens.

</details>
<details>
<summary>crates/node_types/prover/src/lib.rs (2)</summary>

`3-5`: **LGTM! Well-structured modular architecture.**

The addition of dedicated modules for `prover_engine`, `sequencer`, and `syncer` provides clear separation of concerns and aligns with the modular refactor objectives.

---

`7-8`: **LGTM! Clean public API with appropriate re-exports.**

The re-exports provide a clean public interface for the modular configuration structs and maintain backward compatibility while exposing the new architecture.

</details>
<details>
<summary>crates/tests/src/lib.rs (1)</summary>

`78-92`: **Configuration refactor looks good!**

The nested configuration structure with `syncer`, `sequencer`, `prover_engine`, and `webserver` properly reflects the modular architecture.

</details>
<details>
<summary>crates/cli/src/main.rs (1)</summary>

`121-135`: **Configuration refactor implemented correctly!**

The nested configuration structure properly separates concerns between `syncer`, `sequencer`, `prover_engine`, and `webserver` components, aligning well with the modular architecture.



Also applies to: 164-178

</details>
<details>
<summary>crates/node_types/prover/src/prover/tests.rs (2)</summary>

`13-20`: **Test setup correctly updated for new API!**

The configuration structure and addition of `CancellationToken` properly reflect the new modular architecture.

---

`47-48`: **Getter methods correctly replace direct field access.**

The transition from direct field access to getter methods (`get_da()`, `get_db()`, `get_pending_transactions()`) properly encapsulates the internal state and aligns with the refactored API.



Also applies to: 58-59, 79-80, 99-100, 122-124, 234-235, 241-242, 276-277, 281-282

</details>
<details>
<summary>crates/node_types/prover/src/prover_engine.rs (1)</summary>

`83-93`: <details>
<summary>✅ Verification successful</summary>

**Optimize proof generation to avoid duplicate computation.**

The method generates groth16 and compressed proofs separately, which duplicates the proving computation. Check if the SP1 SDK supports generating both proof types in a single pass to improve performance.

---

🌐 Web query:

```web
Does SP1 SDK support generating multiple proof types (groth16 and compressed) in a single prove() call?
```

💡 Result:

## SP1 SDK: Generating Multiple Proof Types in a Single prove() Call

**Summary:**  
The SP1 SDK offers various proof types—Core, Compressed, Groth16, and PLONK—each selectable via a builder pattern on the `ProverClient`. However, generating multiple proof types (e.g., Groth16 and Compressed) in a single `prove()` call is **not supported**: each `prove()` call produces a single proof of a specified type, and you must invoke `prove()` separately for each proof format you require.

---

**Details and Evidence**

- The SP1 `ProverClient` supports configuring the proof type *before* invoking `.prove()`, using options like `.compressed()`, `.groth16()`, or `.plonk()`. This builder pattern is explicitly documented, showing one proof type per method chain and `.run()` execution—for example:

  ```rust
  client.prove(&pk, &stdin).compressed().run().unwrap();
  client.prove(&pk, &stdin).groth16().run().unwrap();
  ```

  Each call generates a proof of a single selected type[6].

- Direct support for outputting multiple proof types in one operation is not documented. Instead, to obtain more than one proof type (such as both Groth16 and Compressed), you must call `.prove()` separately for each format after configuring the desired proof type on the builder[6].

- This is reinforced in SP1 documentation and usage guides, which present proof generation as a process producing one proof artifact per invocation[1][6].

---

**How to Generate Multiple Proof Types**

To get multiple proof types for the same computation:

1. Set up your program's proving and verification keys.
2. Call `prove()` for each desired proof type, for example:
   - To generate a compressed proof:
     ```rust
     client.prove(&pk, &stdin).compressed().run().unwrap();
     ```
   - To generate a Groth16 proof:
     ```rust
     client.prove(&pk, &stdin).groth16().run().unwrap();
     ```

Each call will execute the proof generation pipeline for the respective proof type and produce its output independently[6].

---

**Conclusion**

- The SP1 SDK does **not** support generating multiple proof types (Groth16, Compressed, etc.) within a single `prove()` call.
- To obtain multiple types, you must call `prove()` separately for each desired proof type, after configuring the builder accordingly[6].

If you require both Groth16 and compressed proofs, invoke two separate `prove()` calls—one with `.groth16()` and one with `.compressed()`[6].

Citations:

- 1: https://docs.succinct.xyz/docs/sp1/generating-proofs/basics
- 2: https://blog.succinct.xyz/sp1-testnet/
- 3: https://docs.alignedlayer.com/guides/4_generating_proofs
- 4: https://github.com/aerius-labs/sp1-ethereum-storage-proofs
- 5: https://docs.rs/sp1-prover
- 6: https://docs.succinct.xyz/docs/sp1/generating-proofs/proof-types
- 7: https://docs.succinct.xyz/docs/sp1/writing-programs/proof-aggregation
- 8: https://github.com/succinctlabs/sp1-contract-call
- 9: https://docs.succinct.xyz/docs/sp1/security/security-model
- 10: https://arxiv.org/html/2504.14809

---



</details>

**No change needed: SP1 SDK requires separate prove() calls for each proof type.**

After verifying the SP1 SDK documentation, it’s confirmed that each `prove()` invocation produces only one proof format. Generating both Groth16 and Compressed proofs in a single call is not supported, so the two separate calls in `prove_with_base_prover` are necessary.

</details>
<details>
<summary>crates/node_types/prover/src/syncer.rs (1)</summary>

`1-10`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify logging macro imports.**

The code uses `debug!`, `info!`, and other logging macros but the imports don't show where these come from. Ensure the logging macros are properly imported.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check if logging macros are properly imported in the crate
# Look for macro imports or crate-level imports
rg -A 5 "use.*log::|#\[macro_use\].*log" crates/node_types/prover/src/
```

Length of output: 72

---



</details>

**Ensure logging macros are imported**

It looks like `syncer.rs` (and no other file in `crates/node_types/prover/src/`) explicitly brings in `debug!`/`info!` macros from either the `log` or `tracing` crates. Without a `#[macro_use] extern crate log;` (in `lib.rs` or `main.rs`) or explicit `use log::{debug, info};` (or `use tracing::{debug, info};`), those macros won’t resolve at compile time.

Please verify:
- `crates/node_types/prover/src/lib.rs` (or `main.rs`) contains:
  - `#[macro_use] extern crate log;`  
    –or–  
  - `use log::{debug, info, warn, error};`
- Or, if you’re using `tracing`, that you have:
  - `use tracing::{debug, info, warn, error};`

If neither is present, add the appropriate import so that `debug!` and `info!` in `syncer.rs` compile correctly.

</details>
<details>
<summary>crates/node_types/prover/src/prover/mod.rs (3)</summary>

`32-65`: **Well-structured configuration design!**

The modular configuration approach with separate structs for each component (Syncer, Sequencer, ProverEngine) provides excellent separation of concerns and makes the system more maintainable.

---

`109-117`: **Excellent modular architecture!**

The refactored Prover struct with Arc-wrapped components and a cancellation token provides a clean separation of concerns and enables safe concurrent access.

---

`121-155`: **Correct component initialization order!**

The constructor properly initializes components in dependency order (ProverEngine → Sequencer → Syncer) and correctly distributes shared state. Good use of error context for debugging.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:51:11Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (4)</summary><blockquote>

<details>
<summary>crates/node_types/prover/src/syncer.rs (2)</summary><blockquote>

`166-186`: _🛠️ Refactor suggestion_

**Inconsistent epoch finalization conditions.**

The epoch finalization logic has different conditions in two places:
- Lines 166-171: Requires real-time, non-empty transactions, AND prover enabled
- Lines 178-186: Only checks gap threshold, ignoring transaction state and prover flag

This could lead to unexpected behavior where gap epochs are created even when the prover is disabled.



Consider aligning the conditions:
```diff
-        // Create a gap epoch if necessary
-        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
-        if latest_epoch_height != 0
-            && height.saturating_sub(latest_epoch_height) >= self.max_epochless_gap
-        {
-            self.sequencer
-                .finalize_new_epoch(next_epoch_height, Vec::new(), &self.prover_engine)
-                .await?;
-        }
+        // Create a gap epoch if necessary
+        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
+        if self.is_prover_enabled
+            && latest_epoch_height != 0
+            && height.saturating_sub(latest_epoch_height) >= self.max_epochless_gap
+        {
+            self.sequencer
+                .finalize_new_epoch(next_epoch_height, Vec::new(), &self.prover_engine)
+                .await?;
+        }
```

---

`250-259`: _⚠️ Potential issue_

**Replace panic with proper error handling.**

Using `panic!` for proof verification failure will crash the entire process. This is too severe for a verification failure that could be handled gracefully.



```diff
-        match self.prover_engine.verify_epoch_proof(epoch.height, &epoch.proof).await {
-            Ok(_) => info!(
-                "zkSNARK for epoch {} was validated successfully",
-                epoch.height
-            ),
-            Err(err) => panic!(
-                "failed to validate epoch at height {}: {:?}",
-                epoch.height, err
-            ),
-        }
+        self.prover_engine.verify_epoch_proof(epoch.height, &epoch.proof).await
+            .with_context(|| format!("Failed to validate zkSNARK for epoch {}", epoch.height))?;
+        
+        info!("zkSNARK for epoch {} was validated successfully", epoch.height);
```

</blockquote></details>
<details>
<summary>crates/node_types/prover/src/sequencer.rs (1)</summary><blockquote>

`151-169`: _🛠️ Refactor suggestion_

**Track and return failed transactions.**

Failed transactions are only logged as warnings. The caller has no way to know which transactions failed, leading to potential silent failures.



Consider returning both successful proofs and failed transactions:
```diff
-    pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
+    pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<(Vec<Proof>, Vec<(Transaction, String)>)> {
         debug!("executing block with {} transactions", transactions.len());
 
         let mut proofs = Vec::new();
+        let mut failed_transactions = Vec::new();
 
         for transaction in transactions {
             match self.process_transaction(transaction.clone()).await {
                 Ok(proof) => proofs.push(proof),
                 Err(e) => {
                     warn!(
                         "Failed to process transaction: {:?}. Error: {}",
                         transaction, e
                     );
+                    failed_transactions.push((transaction, e.to_string()));
                 }
             }
         }
 
-        Ok(proofs)
+        Ok((proofs, failed_transactions))
     }
```

Note: This would require updating the caller in `syncer.rs` to handle the new return type.

</blockquote></details>
<details>
<summary>crates/node_types/prover/src/prover/mod.rs (1)</summary><blockquote>

`242-245`: _🛠️ Refactor suggestion_

**Handle the edge case when no services are started.**

When no futures are in the join set (e.g., all services disabled), the code returns `Ok()`. This scenario should either be an error or have a more descriptive log message.



Apply this diff to improve the handling:
```diff
         } else {
-            error!("No futures in join set, shutting down");
-            Ok(())
+            warn!("No services were started (all components may be disabled)");
+            Err(anyhow!("No services to run - check configuration"))
         };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6b332100b6da696a58993b3cf30fa000f9795a7b and ee1cddb7a8b4c069efc82ab0cde4dffb08f232ba.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `Cargo.toml` (1 hunks)
* `crates/cli/Cargo.toml` (1 hunks)
* `crates/cli/src/main.rs` (4 hunks)
* `crates/node_types/prover/Cargo.toml` (1 hunks)
* `crates/node_types/prover/src/lib.rs` (1 hunks)
* `crates/node_types/prover/src/prover/mod.rs` (5 hunks)
* `crates/node_types/prover/src/prover/tests.rs` (11 hunks)
* `crates/node_types/prover/src/prover_engine.rs` (1 hunks)
* `crates/node_types/prover/src/sequencer.rs` (1 hunks)
* `crates/node_types/prover/src/syncer.rs` (1 hunks)
* `crates/tests/Cargo.toml` (1 hunks)
* `crates/tests/src/lib.rs` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* crates/tests/Cargo.toml

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* crates/node_types/prover/Cargo.toml
* crates/cli/Cargo.toml
* Cargo.toml
* crates/node_types/prover/src/lib.rs
* crates/node_types/prover/src/prover/tests.rs
* crates/tests/src/lib.rs
* crates/node_types/prover/src/prover_engine.rs
* crates/cli/src/main.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: build-and-push-image
* GitHub Check: unused dependencies
* GitHub Check: integration-test
* GitHub Check: unit-test

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>crates/node_types/prover/src/syncer.rs (2)</summary>

`13-46`: **Well-structured component initialization!**

The `Syncer` struct and constructor are properly designed with clear separation of concerns and dependency injection.

---

`79-137`: **Excellent implementation of graceful shutdown and sequential processing!**

The sync loop properly handles cancellation tokens and validates sequential height processing, which ensures data integrity.

</details>
<details>
<summary>crates/node_types/prover/src/sequencer.rs (2)</summary>

`19-56`: **Clean component structure with proper state initialization!**

The `Sequencer` struct and constructor properly handle state recovery from the database and initialize all required components.

---

`171-197`: **Robust transaction validation logic!**

The method properly validates different operation types and ensures transactions are valid before queuing, preventing invalid transactions from entering the system.

</details>
<details>
<summary>crates/node_types/prover/src/prover/mod.rs (2)</summary>

`32-64`: **Excellent modularization of configuration!**

The split into focused configuration structs (`SyncerConfig`, `SequencerConfig`, `ProverEngineConfig`) improves code organization and makes the configuration more maintainable.

---

`110-155`: **Clean delegation pattern with proper dependency injection!**

The refactored `Prover` struct effectively delegates responsibilities to specialized components while maintaining a clean interface.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T09:01:55Z
user: login: sebasti810
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T09:02:55Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T09:03:29Z

Review comments:
diff_hunk: @@ -153,23 +161,49 @@ async fn main() -> std::io::Result<()> {
             // When SP1_PROVER is set to mock, disable recursive proofs
             let recursive_proofs = std::env::var("SP1_PROVER").map_or(true, |val| val != "mock");
             let prover_cfg = prism_prover::Config {
-                prover: false,
-                batcher: true,
+                syncer: prism_prover::SyncerConfig {
+                    verifying_key,
+                    start_height,
+                    max_epochless_gap: config.max_epochless_gap,
+                    prover_enabled: false, // FullNode doesn't generate proofs
+                },
+                sequencer: prism_prover::SequencerConfig {
+                    signing_key,
+                    batcher_enabled: true,
+                },
+                prover_engine: prism_prover::ProverEngineConfig {
+                    recursive_proofs,
+                },
                 webserver: webserver_config.unwrap_or_default(),
-                signing_key,
-                verifying_key,
-                start_height,
-                max_epochless_gap: config.max_epochless_gap,
-                recursive_proofs,
             };
 
-            Arc::new(Prover::new(db, da, &prover_cfg).map_err(|e| {
+            Arc::new(Prover::new(db, da, &prover_cfg, cancellation_token.clone()).map_err(|e| {
                 error!("error initializing prover: {}", e);
                 Error::other(e.to_string())
             })?)
         }
     };
 
+    // Setup signal handling for graceful shutdown
+    let cancellation_for_signal = cancellation_token.clone();
+    tokio::spawn(async move {
+        use tokio::signal::unix::{signal, SignalKind};
+        
+        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
+        let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
+        
+        tokio::select! {
+            _ = sigint.recv() => {
+                info!("Received SIGINT, initiating graceful shutdown");
+            },
+            _ = sigterm.recv() => {
+                info!("Received SIGTERM, initiating graceful shutdown");
+            }
+        }
+        
+        cancellation_for_signal.cancel();
+    });
+
path: crates/cli/src/main.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Signal handling is Unix-specific and will fail on Windows.**

The current implementation uses Unix-specific signal handling which will cause compilation errors on Windows platforms.

Consider using a cross-platform approach:

```diff
-// Setup signal handling for graceful shutdown
-let cancellation_for_signal = cancellation_token.clone();
-tokio::spawn(async move {
-    use tokio::signal::unix::{signal, SignalKind};
-    
-    let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
-    let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
-    
-    tokio::select! {
-        _ = sigint.recv() => {
-            info!("Received SIGINT, initiating graceful shutdown");
-        },
-        _ = sigterm.recv() => {
-            info!("Received SIGTERM, initiating graceful shutdown");
-        }
-    }
-    
-    cancellation_for_signal.cancel();
-});
+// Setup signal handling for graceful shutdown
+let cancellation_for_signal = cancellation_token.clone();
+tokio::spawn(async move {
+    #[cfg(unix)]
+    {
+        use tokio::signal::unix::{signal, SignalKind};
+        
+        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
+        let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
+        
+        tokio::select! {
+            _ = sigint.recv() => {
+                info!("Received SIGINT, initiating graceful shutdown");
+            },
+            _ = sigterm.recv() => {
+                info!("Received SIGTERM, initiating graceful shutdown");
+            }
+        }
+    }
+    
+    #[cfg(windows)]
+    {
+        tokio::signal::ctrl_c().await.expect("Failed to register Ctrl+C handler");
+        info!("Received Ctrl+C, initiating graceful shutdown");
+    }
+    
+    cancellation_for_signal.cancel();
+});
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    // Setup signal handling for graceful shutdown
    let cancellation_for_signal = cancellation_token.clone();
    tokio::spawn(async move {
        #[cfg(unix)]
        {
            use tokio::signal::unix::{signal, SignalKind};
            
            let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
            let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
            
            tokio::select! {
                _ = sigint.recv() => {
                    info!("Received SIGINT, initiating graceful shutdown");
                },
                _ = sigterm.recv() => {
                    info!("Received SIGTERM, initiating graceful shutdown");
                }
            }
        }
        
        #[cfg(windows)]
        {
            tokio::signal::ctrl_c().await.expect("Failed to register Ctrl+C handler");
            info!("Received Ctrl+C, initiating graceful shutdown");
        }
        
        cancellation_for_signal.cancel();
    });
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/cli/src/main.rs around lines 187 to 206, the signal handling code uses
Unix-specific APIs that will not compile on Windows. Replace the current
Unix-only signal handling with a cross-platform approach by using the
tokio::signal::ctrl_c() function for handling interrupt signals on all
platforms. Adjust the code to listen for ctrl_c() and trigger the cancellation
token accordingly, removing the Unix-specific signal imports and handlers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,185 @@
+use anyhow::{Result, anyhow};
+use prism_storage::database::Database;
+use prism_tree::proofs::Batch;
+use sp1_sdk::{
+    EnvProver, HashableKey as _, ProverClient, SP1Proof, SP1ProofWithPublicValues, SP1ProvingKey,
+    SP1Stdin, SP1VerifyingKey,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+pub const BASE_PRISM_ELF: &[u8] =
+    include_bytes!("../../../../elf/base-riscv32im-succinct-zkvm-elf");
+pub const RECURSIVE_PRISM_ELF: &[u8] =
+    include_bytes!("../../../../elf/recursive-riscv32im-succinct-zkvm-elf");
+
+#[derive(Clone)]
+pub struct ProverEngine {
+    base_prover_client: Arc<RwLock<EnvProver>>,
+    base_proving_key: SP1ProvingKey,
+    base_verifying_key: SP1VerifyingKey,
+
+    recursive_prover_client: Arc<RwLock<EnvProver>>,
+    recursive_proving_key: SP1ProvingKey,
+    recursive_verifying_key: SP1VerifyingKey,
+
+    recursive_proofs_enabled: bool,
+}
+
+impl ProverEngine {
+    pub fn new(config: &crate::prover::ProverEngineConfig) -> Result<Self> {
+        let base_prover_client = ProverClient::from_env();
+        let recursive_prover_client = ProverClient::from_env();
+
+        let (base_pk, base_vk) = base_prover_client.setup(BASE_PRISM_ELF);
+        let (recursive_pk, recursive_vk) = recursive_prover_client.setup(RECURSIVE_PRISM_ELF);
+
+        Ok(ProverEngine {
+            base_proving_key: base_pk,
+            base_verifying_key: base_vk,
+            recursive_proving_key: recursive_pk,
+            recursive_verifying_key: recursive_vk,
+            base_prover_client: Arc::new(RwLock::new(base_prover_client)),
+            recursive_prover_client: Arc::new(RwLock::new(recursive_prover_client)),
+            recursive_proofs_enabled: config.recursive_proofs,
+        })
+    }
+
+    pub async fn prove_epoch(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+        db: &Arc<Box<dyn Database>>,
+    ) -> Result<(SP1ProofWithPublicValues, SP1ProofWithPublicValues)> {
+        let (proof, compressed_proof, client, verifying_key) =
+            if epoch_height == 0 || !self.recursive_proofs_enabled {
+                self.prove_with_base_prover(epoch_height, batch).await?
+            } else {
+                self.prove_with_recursive_prover(epoch_height, batch, db).await?
+            };
+
+        client.verify(&proof, verifying_key)?;
+        info!("verified proof for epoch {}", epoch_height);
+
+        Ok((proof, compressed_proof))
+    }
+
+    async fn prove_with_base_prover(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+    ) -> Result<(
+        SP1ProofWithPublicValues,
+        SP1ProofWithPublicValues,
+        tokio::sync::RwLockReadGuard<'_, EnvProver>,
+        &SP1VerifyingKey,
+    )> {
+        let mut stdin = SP1Stdin::new();
+        stdin.write(batch);
+
+        let client = self.base_prover_client.read().await;
+        info!("generating proof for epoch {}", epoch_height);
+
+        let proof = client.prove(&self.base_proving_key, &stdin).groth16().run()?;
+        info!(
+            "successfully generated base proof for epoch {}",
+            epoch_height
+        );
+
+        let compressed_proof = client.prove(&self.base_proving_key, &stdin).compressed().run()?;
+        info!(
+            "successfully generated compressed proof for epoch {}",
+            epoch_height
+        );
+
+        Ok((proof, compressed_proof, client, &self.base_verifying_key))
+    }
+
+    async fn prove_with_recursive_prover(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+        db: &Arc<Box<dyn Database>>,
+    ) -> Result<(
+        SP1ProofWithPublicValues,
+        SP1ProofWithPublicValues,
+        tokio::sync::RwLockReadGuard<'_, EnvProver>,
+        &SP1VerifyingKey,
+    )> {
+        let prev_epoch = match db.get_latest_epoch() {
+            Ok(epoch) => epoch,
+            Err(_) => {
+                return Err(anyhow!(
+                    "Previous epoch not found for recursive verification at height {}",
+                    epoch_height - 1
+                ));
+            }
+        };
+
+        let vk_to_use = if prev_epoch.height == 0 {
+            self.base_verifying_key.clone()
+        } else {
+            self.recursive_verifying_key.clone()
+        };
+
+        let mut stdin = SP1Stdin::new();
+        let compressed_proof = match prev_epoch.compressed_proof.proof {
+            SP1Proof::Compressed(proof) => proof,
+            _ => return Err(anyhow!("Invalid proof type: expected compressed proof")),
+        };
path: crates/node_types/prover/src/prover_engine.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add comprehensive proof type handling.**

The code assumes the proof is always compressed but only provides a generic error for other types. Consider handling all proof types explicitly.

```diff
-let compressed_proof = match prev_epoch.compressed_proof.proof {
-    SP1Proof::Compressed(proof) => proof,
-    _ => return Err(anyhow!("Invalid proof type: expected compressed proof")),
-};
+let compressed_proof = match prev_epoch.compressed_proof.proof {
+    SP1Proof::Compressed(proof) => proof,
+    SP1Proof::Groth16(_) => return Err(anyhow!("Invalid proof type: expected compressed proof but got Groth16")),
+    SP1Proof::Plonk(_) => return Err(anyhow!("Invalid proof type: expected compressed proof but got Plonk")),
+    _ => return Err(anyhow!("Invalid proof type: expected compressed proof but got unknown type")),
+};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        let compressed_proof = match prev_epoch.compressed_proof.proof {
            SP1Proof::Compressed(proof) => proof,
            SP1Proof::Groth16(_) => return Err(anyhow!(
                "Invalid proof type: expected compressed proof but got Groth16"
            )),
            SP1Proof::Plonk(_) => return Err(anyhow!(
                "Invalid proof type: expected compressed proof but got Plonk"
            )),
            _ => return Err(anyhow!(
                "Invalid proof type: expected compressed proof but got unknown type"
            )),
        };
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/prover_engine.rs around lines 126 to 129, the
match on prev_epoch.compressed_proof.proof only handles the Compressed variant
and returns a generic error for others. Update the match statement to explicitly
handle all possible SP1Proof variants, providing specific logic or error
messages for each type instead of a generic error, ensuring comprehensive proof
type handling.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,185 @@
+use anyhow::{Result, anyhow};
+use prism_storage::database::Database;
+use prism_tree::proofs::Batch;
+use sp1_sdk::{
+    EnvProver, HashableKey as _, ProverClient, SP1Proof, SP1ProofWithPublicValues, SP1ProvingKey,
+    SP1Stdin, SP1VerifyingKey,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+pub const BASE_PRISM_ELF: &[u8] =
+    include_bytes!("../../../../elf/base-riscv32im-succinct-zkvm-elf");
+pub const RECURSIVE_PRISM_ELF: &[u8] =
+    include_bytes!("../../../../elf/recursive-riscv32im-succinct-zkvm-elf");
+
+#[derive(Clone)]
+pub struct ProverEngine {
+    base_prover_client: Arc<RwLock<EnvProver>>,
+    base_proving_key: SP1ProvingKey,
+    base_verifying_key: SP1VerifyingKey,
+
+    recursive_prover_client: Arc<RwLock<EnvProver>>,
+    recursive_proving_key: SP1ProvingKey,
+    recursive_verifying_key: SP1VerifyingKey,
+
+    recursive_proofs_enabled: bool,
+}
+
+impl ProverEngine {
+    pub fn new(config: &crate::prover::ProverEngineConfig) -> Result<Self> {
+        let base_prover_client = ProverClient::from_env();
+        let recursive_prover_client = ProverClient::from_env();
+
+        let (base_pk, base_vk) = base_prover_client.setup(BASE_PRISM_ELF);
+        let (recursive_pk, recursive_vk) = recursive_prover_client.setup(RECURSIVE_PRISM_ELF);
+
+        Ok(ProverEngine {
+            base_proving_key: base_pk,
+            base_verifying_key: base_vk,
+            recursive_proving_key: recursive_pk,
+            recursive_verifying_key: recursive_vk,
+            base_prover_client: Arc::new(RwLock::new(base_prover_client)),
+            recursive_prover_client: Arc::new(RwLock::new(recursive_prover_client)),
+            recursive_proofs_enabled: config.recursive_proofs,
+        })
+    }
+
+    pub async fn prove_epoch(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+        db: &Arc<Box<dyn Database>>,
+    ) -> Result<(SP1ProofWithPublicValues, SP1ProofWithPublicValues)> {
+        let (proof, compressed_proof, client, verifying_key) =
+            if epoch_height == 0 || !self.recursive_proofs_enabled {
+                self.prove_with_base_prover(epoch_height, batch).await?
+            } else {
+                self.prove_with_recursive_prover(epoch_height, batch, db).await?
+            };
+
+        client.verify(&proof, verifying_key)?;
+        info!("verified proof for epoch {}", epoch_height);
+
+        Ok((proof, compressed_proof))
+    }
+
+    async fn prove_with_base_prover(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+    ) -> Result<(
+        SP1ProofWithPublicValues,
+        SP1ProofWithPublicValues,
+        tokio::sync::RwLockReadGuard<'_, EnvProver>,
+        &SP1VerifyingKey,
+    )> {
+        let mut stdin = SP1Stdin::new();
+        stdin.write(batch);
+
+        let client = self.base_prover_client.read().await;
+        info!("generating proof for epoch {}", epoch_height);
+
+        let proof = client.prove(&self.base_proving_key, &stdin).groth16().run()?;
+        info!(
+            "successfully generated base proof for epoch {}",
+            epoch_height
+        );
+
+        let compressed_proof = client.prove(&self.base_proving_key, &stdin).compressed().run()?;
+        info!(
+            "successfully generated compressed proof for epoch {}",
+            epoch_height
+        );
+
+        Ok((proof, compressed_proof, client, &self.base_verifying_key))
+    }
+
+    async fn prove_with_recursive_prover(
+        &self,
+        epoch_height: u64,
+        batch: &Batch,
+        db: &Arc<Box<dyn Database>>,
+    ) -> Result<(
+        SP1ProofWithPublicValues,
+        SP1ProofWithPublicValues,
+        tokio::sync::RwLockReadGuard<'_, EnvProver>,
+        &SP1VerifyingKey,
+    )> {
+        let prev_epoch = match db.get_latest_epoch() {
+            Ok(epoch) => epoch,
+            Err(_) => {
+                return Err(anyhow!(
+                    "Previous epoch not found for recursive verification at height {}",
+                    epoch_height - 1
+                ));
+            }
+        };
path: crates/node_types/prover/src/prover_engine.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling for missing previous epoch.**

The error message suggests this is always an error condition, but missing previous epochs might be normal during startup or recovery. Consider differentiating between expected and unexpected missing epochs.

```diff
-let prev_epoch = match db.get_latest_epoch() {
-    Ok(epoch) => epoch,
-    Err(_) => {
-        return Err(anyhow!(
-            "Previous epoch not found for recursive verification at height {}",
-            epoch_height - 1
-        ));
-    }
-};
+let prev_epoch = db.get_latest_epoch()
+    .with_context(|| format!(
+        "Cannot generate recursive proof for epoch {} without previous epoch {}. \
+         This is expected for the first epoch after startup.",
+        epoch_height, epoch_height - 1
+    ))?;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        let prev_epoch = db.get_latest_epoch()
            .with_context(|| format!(
                "Cannot generate recursive proof for epoch {} without previous epoch {}. \
                 This is expected for the first epoch after startup.",
                epoch_height, epoch_height - 1
            ))?;
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/prover_engine.rs around lines 109 to 117, the
current error handling treats a missing previous epoch as a hard error, but this
might be normal during startup or recovery. Modify the code to distinguish
between expected and unexpected missing epochs by checking the error type or
condition returned by db.get_latest_epoch(). For expected missing epochs, handle
gracefully (e.g., return a specific result or log a warning), and only return an
error for truly unexpected failures.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,264 @@
+use anyhow::{Context, Result, anyhow};
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::VerifyingKey;
+use prism_storage::database::Database;
+use prism_telemetry_registry::metrics_registry::get_metrics;
+use prism_common::transaction::Transaction;
+use std::{collections::VecDeque, sync::Arc};
+use tokio::sync::{broadcast, RwLock};
+use tokio_util::sync::CancellationToken;
+
+use crate::{prover_engine::ProverEngine, sequencer::Sequencer};
+
+#[derive(Clone)]
+pub struct Syncer {
+    da: Arc<dyn DataAvailabilityLayer>,
+    db: Arc<Box<dyn Database>>,
+    verifying_key: VerifyingKey,
+    max_epochless_gap: u64,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    start_height: u64,
+    sequencer: Arc<Sequencer>,
+    prover_engine: Arc<ProverEngine>,
+    is_prover_enabled: bool,
+}
+
+impl Syncer {
+    pub fn new(
+        da: Arc<dyn DataAvailabilityLayer>,
+        db: Arc<Box<dyn Database>>,
+        config: &crate::prover::SyncerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+        sequencer: Arc<Sequencer>,
+        prover_engine: Arc<ProverEngine>,
+    ) -> Self {
+        Self {
+            da,
+            db,
+            verifying_key: config.verifying_key.clone(),
+            max_epochless_gap: config.max_epochless_gap,
+            latest_epoch_da_height,
+            start_height: config.start_height,
+            sequencer,
+            prover_engine,
+            is_prover_enabled: config.prover_enabled,
+        }
+    }
+
+    pub fn get_da(&self) -> Arc<dyn DataAvailabilityLayer> {
+        self.da.clone()
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        self.da.start().await?;
+        self.run_main_loop(cancellation_token).await
+    }
+
+    async fn run_main_loop(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+        let historical_sync_height = height_rx.recv().await?;
+
+        let sync_start_height = match self.db.get_last_synced_height() {
+            Ok(height) => height,
+            Err(_) => {
+                debug!("no existing sync height found, setting sync height to start_height");
+                self.db.set_last_synced_height(&self.start_height)?;
+                self.start_height
+            }
+        };
+
+        self.sync_loop(
+            sync_start_height,
+            historical_sync_height,
+            height_rx,
+            cancellation_token,
+        ).await
+    }
+
+    async fn sync_loop(
+        &self,
+        start_height: u64,
+        end_height: u64,
+        mut incoming_heights: broadcast::Receiver<u64>,
+        cancellation_token: CancellationToken,
+    ) -> Result<()> {
+        let mut buffered_transactions: VecDeque<Transaction> = VecDeque::new();
+        let mut current_height = start_height;
+
+        while current_height <= end_height {
+            tokio::select! {
+                result = self.process_da_height(
+                    current_height,
+                    &mut buffered_transactions,
+                    false,
+                ) => {
+                    result?;
+                    self.db.set_last_synced_height(&current_height)?;
+                    current_height += 1;
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Syncer: Gracefully stopping during historical sync at height {}", current_height);
+                    return Ok(());
+                }
+            }
+        }
+
+        info!(
+            "finished historical sync from height {} to {}",
+            start_height, end_height
+        );
+
+        loop {
+            tokio::select! {
+                height_result = incoming_heights.recv() => {
+                    let height = height_result?;
+                    if height != current_height {
+                        return Err(anyhow!(
+                            "heights are not sequential: expected {}, got {}",
+                            current_height,
+                            height
+                        ));
+                    }
+                    self.process_da_height(
+                        height,
+                        &mut buffered_transactions,
+                        true,
+                    ).await?;
+                    current_height += 1;
+                    self.db.set_last_synced_height(&current_height)?;
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Syncer: Gracefully stopping during real-time sync at height {}", current_height);
+                    return Ok(());
+                }
+            }
+        }
+    }
+
+    async fn process_da_height(
+        &self,
+        height: u64,
+        buffered_transactions: &mut VecDeque<Transaction>,
+        is_real_time: bool,
+    ) -> Result<()> {
+        let next_epoch_height = match self.db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => 0,
+        };
+
+        let transactions = self.da.get_transactions(height).await?;
+        let epoch_result = self.da.get_finalized_epoch(height).await?;
+
+        debug!(
+            "processing {} height {}, next_epoch_height: {}",
+            if is_real_time { "new" } else { "old" },
+            height,
+            next_epoch_height
+        );
+
+        if let Some(epoch) = epoch_result {
+            self.process_epoch(epoch, buffered_transactions).await?;
+        } else {
+            debug!("No transactions to process at height {}", height);
+        }
+
+        if is_real_time && !buffered_transactions.is_empty() && self.is_prover_enabled {
+            let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
+            self.sequencer.finalize_new_epoch(next_epoch_height, all_transactions, &self.prover_engine).await?;
+        }
+
+        if !transactions.is_empty() {
+            buffered_transactions.extend(transactions);
+            return Ok(());
+        }
+
+        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
+        if latest_epoch_height != 0
+            && height.saturating_sub(latest_epoch_height) >= self.max_epochless_gap
+        {
+            self.sequencer.finalize_new_epoch(next_epoch_height, Vec::new(), &self.prover_engine).await?;
+        }
+
+        if let Some(metrics) = get_metrics() {
+            metrics.record_celestia_synced_height(height, vec![]);
+            metrics.record_current_epoch(next_epoch_height, vec![]);
+        }
+
+        Ok(())
+    }
+
+    async fn process_epoch(
+        &self,
+        epoch: FinalizedEpoch,
+        buffered_transactions: &mut VecDeque<Transaction>,
+    ) -> Result<()> {
+        let current_epoch = match self.db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => 0,
+        };
+
+        if epoch.height < current_epoch {
+            debug!("epoch {} already processed internally", current_epoch);
+            return Ok(());
+        }
+
+        epoch
+            .verify_signature(self.verifying_key.clone())
+            .with_context(|| format!("Invalid signature in epoch {}", epoch.height))?;
+        trace!("valid signature for epoch {}", epoch.height);
+
+        let prev_commitment = if epoch.height == 0 {
+            self.sequencer.get_commitment().await?
+        } else {
+            self.db.get_epoch(&epoch.height.saturating_sub(1))?.current_commitment
+        };
+
+        if epoch.height != current_epoch {
+            return Err(anyhow!(
+                "epoch height mismatch: expected {}, got {}",
+                current_epoch,
+                epoch.height
+            ));
+        }
+
+        if epoch.prev_commitment != prev_commitment {
+            return Err(anyhow!(
+                "previous commitment mismatch at epoch {}",
+                current_epoch
+            ));
+        }
+
+        let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
+        if !all_transactions.is_empty() {
+            self.sequencer.execute_block(all_transactions).await?;
+        }
+
+        let new_commitment = self.sequencer.get_commitment().await?;
+        if epoch.current_commitment != new_commitment {
+            return Err(anyhow!(
+                "new commitment mismatch at epoch {}",
+                current_epoch
+            ));
+        }
+
+        match self.prover_engine.verify_epoch_proof(epoch.height, &epoch.proof).await {
+            Ok(_) => info!(
+                "zkSNARK for epoch {} was validated successfully",
+                epoch.height
+            ),
+            Err(err) => panic!(
+                "failed to validate epoch at height {}: {:?}",
+                epoch.height, err
+            ),
+        }
path: crates/node_types/prover/src/syncer.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Replace panic with proper error handling.**

Using `panic!` for proof verification failure will crash the entire process. This is too severe for a verification failure that could be handled gracefully.

```diff
-match self.prover_engine.verify_epoch_proof(epoch.height, &epoch.proof).await {
-    Ok(_) => info!(
-        "zkSNARK for epoch {} was validated successfully",
-        epoch.height
-    ),
-    Err(err) => panic!(
-        "failed to validate epoch at height {}: {:?}",
-        epoch.height, err
-    ),
-}
+self.prover_engine.verify_epoch_proof(epoch.height, &epoch.proof).await
+    .with_context(|| format!("Failed to validate zkSNARK for epoch {}", epoch.height))?;
+
+info!("zkSNARK for epoch {} was validated successfully", epoch.height);
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/syncer.rs around lines 244 to 253, replace the
panic! call in the Err arm of the match statement with proper error handling.
Instead of crashing the process on verification failure, return a Result with an
appropriate error or log the error and handle it gracefully to allow the program
to continue running safely.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,264 @@
+use anyhow::{Context, Result, anyhow};
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::VerifyingKey;
+use prism_storage::database::Database;
+use prism_telemetry_registry::metrics_registry::get_metrics;
+use prism_common::transaction::Transaction;
+use std::{collections::VecDeque, sync::Arc};
+use tokio::sync::{broadcast, RwLock};
+use tokio_util::sync::CancellationToken;
+
+use crate::{prover_engine::ProverEngine, sequencer::Sequencer};
+
+#[derive(Clone)]
+pub struct Syncer {
+    da: Arc<dyn DataAvailabilityLayer>,
+    db: Arc<Box<dyn Database>>,
+    verifying_key: VerifyingKey,
+    max_epochless_gap: u64,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    start_height: u64,
+    sequencer: Arc<Sequencer>,
+    prover_engine: Arc<ProverEngine>,
+    is_prover_enabled: bool,
+}
+
+impl Syncer {
+    pub fn new(
+        da: Arc<dyn DataAvailabilityLayer>,
+        db: Arc<Box<dyn Database>>,
+        config: &crate::prover::SyncerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+        sequencer: Arc<Sequencer>,
+        prover_engine: Arc<ProverEngine>,
+    ) -> Self {
+        Self {
+            da,
+            db,
+            verifying_key: config.verifying_key.clone(),
+            max_epochless_gap: config.max_epochless_gap,
+            latest_epoch_da_height,
+            start_height: config.start_height,
+            sequencer,
+            prover_engine,
+            is_prover_enabled: config.prover_enabled,
+        }
+    }
+
+    pub fn get_da(&self) -> Arc<dyn DataAvailabilityLayer> {
+        self.da.clone()
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        self.da.start().await?;
+        self.run_main_loop(cancellation_token).await
+    }
+
+    async fn run_main_loop(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+        let historical_sync_height = height_rx.recv().await?;
+
+        let sync_start_height = match self.db.get_last_synced_height() {
+            Ok(height) => height,
+            Err(_) => {
+                debug!("no existing sync height found, setting sync height to start_height");
+                self.db.set_last_synced_height(&self.start_height)?;
+                self.start_height
+            }
+        };
+
+        self.sync_loop(
+            sync_start_height,
+            historical_sync_height,
+            height_rx,
+            cancellation_token,
+        ).await
+    }
+
+    async fn sync_loop(
+        &self,
+        start_height: u64,
+        end_height: u64,
+        mut incoming_heights: broadcast::Receiver<u64>,
+        cancellation_token: CancellationToken,
+    ) -> Result<()> {
+        let mut buffered_transactions: VecDeque<Transaction> = VecDeque::new();
+        let mut current_height = start_height;
+
+        while current_height <= end_height {
+            tokio::select! {
+                result = self.process_da_height(
+                    current_height,
+                    &mut buffered_transactions,
+                    false,
+                ) => {
+                    result?;
+                    self.db.set_last_synced_height(&current_height)?;
+                    current_height += 1;
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Syncer: Gracefully stopping during historical sync at height {}", current_height);
+                    return Ok(());
+                }
+            }
+        }
+
+        info!(
+            "finished historical sync from height {} to {}",
+            start_height, end_height
+        );
+
+        loop {
+            tokio::select! {
+                height_result = incoming_heights.recv() => {
+                    let height = height_result?;
+                    if height != current_height {
+                        return Err(anyhow!(
+                            "heights are not sequential: expected {}, got {}",
+                            current_height,
+                            height
+                        ));
+                    }
+                    self.process_da_height(
+                        height,
+                        &mut buffered_transactions,
+                        true,
+                    ).await?;
+                    current_height += 1;
+                    self.db.set_last_synced_height(&current_height)?;
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Syncer: Gracefully stopping during real-time sync at height {}", current_height);
+                    return Ok(());
+                }
+            }
+        }
+    }
+
+    async fn process_da_height(
+        &self,
+        height: u64,
+        buffered_transactions: &mut VecDeque<Transaction>,
+        is_real_time: bool,
+    ) -> Result<()> {
+        let next_epoch_height = match self.db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => 0,
+        };
+
+        let transactions = self.da.get_transactions(height).await?;
+        let epoch_result = self.da.get_finalized_epoch(height).await?;
+
+        debug!(
+            "processing {} height {}, next_epoch_height: {}",
+            if is_real_time { "new" } else { "old" },
+            height,
+            next_epoch_height
+        );
+
+        if let Some(epoch) = epoch_result {
+            self.process_epoch(epoch, buffered_transactions).await?;
+        } else {
+            debug!("No transactions to process at height {}", height);
+        }
+
+        if is_real_time && !buffered_transactions.is_empty() && self.is_prover_enabled {
+            let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
+            self.sequencer.finalize_new_epoch(next_epoch_height, all_transactions, &self.prover_engine).await?;
+        }
+
+        if !transactions.is_empty() {
+            buffered_transactions.extend(transactions);
+            return Ok(());
+        }
+
+        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
+        if latest_epoch_height != 0
+            && height.saturating_sub(latest_epoch_height) >= self.max_epochless_gap
+        {
+            self.sequencer.finalize_new_epoch(next_epoch_height, Vec::new(), &self.prover_engine).await?;
+        }
path: crates/node_types/prover/src/syncer.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Inconsistent epoch finalization conditions.**

The epoch finalization logic is inconsistent:
- Lines 165-168: Requires non-empty transactions AND prover enabled
- Lines 175-180: Only checks the gap threshold, ignoring transaction state and prover flag

This could lead to unexpected behavior where epochs are finalized differently based on the trigger.

Consider aligning the conditions:

```diff
-if is_real_time && !buffered_transactions.is_empty() && self.is_prover_enabled {
-    let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-    self.sequencer.finalize_new_epoch(next_epoch_height, all_transactions, &self.prover_engine).await?;
-}
+let should_finalize = is_real_time && self.is_prover_enabled && 
+    (!buffered_transactions.is_empty() || 
+     (latest_epoch_height != 0 && height.saturating_sub(latest_epoch_height) >= self.max_epochless_gap));
+
+if should_finalize {
+    let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
+    self.sequencer.finalize_new_epoch(next_epoch_height, all_transactions, &self.prover_engine).await?;
+}
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/syncer.rs around lines 165 to 180, the
conditions for finalizing a new epoch are inconsistent: the first block requires
non-empty transactions and prover enabled, while the second only checks the gap
threshold without considering transactions or prover status. To fix this, align
the conditions by including checks for prover enabled and transaction state in
both places, ensuring consistent criteria for epoch finalization throughout the
code.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+use anyhow::{Context, Result, bail};
+use jmt::KeyHash;
+use prism_common::{
+    account::Account,
+    digest::Digest,
+    operation::Operation,
+    transaction::Transaction,
+};
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::SigningKey;
+use prism_storage::database::Database;
+use prism_tree::{
+    AccountResponse::*,
+    hasher::TreeHasher,
+    key_directory_tree::KeyDirectoryTree,
+    proofs::Proof,
+    snarkable_tree::SnarkableTree,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tokio_util::sync::CancellationToken;
+
+use crate::prover_engine::ProverEngine;
+
+#[derive(Clone)]
+pub struct Sequencer {
+    db: Arc<Box<dyn Database>>,
+    da: Arc<dyn DataAvailabilityLayer>,
+    tree: Arc<RwLock<KeyDirectoryTree<Box<dyn Database>>>>,
+    pending_transactions: Arc<RwLock<Vec<Transaction>>>,
+    signing_key: SigningKey,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    batcher_enabled: bool,
+}
+
+impl Sequencer {
+    pub fn new(
+        db: Arc<Box<dyn Database>>,
+        da: Arc<dyn DataAvailabilityLayer>,
+        config: &crate::prover::SequencerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+    ) -> Result<Self> {
+        let saved_epoch = match db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => {
+                debug!("no existing epoch state found, starting at epoch 0");
+                0
+            }
+        };
+
+        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(
+            db.clone(),
+            saved_epoch,
+        )));
+
+        Ok(Sequencer {
+            db,
+            da,
+            tree,
+            pending_transactions: Arc::new(RwLock::new(Vec::new())),
+            signing_key: config.signing_key.clone(),
+            latest_epoch_da_height,
+            batcher_enabled: config.batcher_enabled,
+        })
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        if self.batcher_enabled {
+            self.run_batch_poster(cancellation_token).await
+        } else {
+            // Sequencer without batcher doesn't need a running loop
+            // Just wait for cancellation
+            cancellation_token.cancelled().await;
+            info!("Sequencer: Gracefully stopped (batcher disabled)");
+            Ok(())
+        }
+    }
+
+    async fn run_batch_poster(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+
+        loop {
+            tokio::select! {
+                height_result = height_rx.recv() => {
+                    let height = height_result?;
+                    trace!("received height {}", height);
+
+                    let pending_transactions = {
+                        let mut ops = self.pending_transactions.write().await;
+                        std::mem::take(&mut *ops)
+                    };
+
+                    let tx_count = pending_transactions.len();
+
+                    if !pending_transactions.clone().is_empty() {
+                        match self.da.submit_transactions(pending_transactions).await {
+                            Ok(submitted_height) => {
+                                info!(
+                                    "post_batch_loop: submitted {} transactions at height {}",
+                                    tx_count, submitted_height
+                                );
+                            }
+                            Err(e) => {
+                                error!("post_batch_loop: Failed to submit transactions: {}", e);
+                            }
+                        }
+                    } else {
+                        debug!(
+                            "post_batch_loop: No pending transactions to submit at height {}",
+                            height
+                        );
+                    }
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Sequencer: Gracefully stopping batch poster");
+                    return Ok(());
+                }
+            }
+        }
+    }
+
+    pub async fn finalize_new_epoch(
+        &self,
+        epoch_height: u64,
+        transactions: Vec<Transaction>,
+        prover_engine: &Arc<ProverEngine>,
+    ) -> Result<u64> {
+        let mut tree = self.tree.write().await;
+        let batch = tree.process_batch(transactions)?;
+        batch.verify()?;
+
+        let (proof, compressed_proof) = prover_engine.prove_epoch(epoch_height, &batch, &self.db).await?;
+
+        let public_values = proof.public_values.to_vec();
+
+        let mut epoch_json = FinalizedEpoch {
+            height: epoch_height,
+            prev_commitment: batch.prev_root,
+            current_commitment: batch.new_root,
+            proof,
+            compressed_proof,
+            public_values,
+            signature: None,
+        };
+
+        epoch_json.insert_signature(&self.signing_key)?;
+
+        let da_height = self.da.submit_finalized_epoch(epoch_json.clone()).await?;
+        let mut latest_da_height = self.latest_epoch_da_height.write().await;
+        *latest_da_height = da_height;
+
+        self.db.add_epoch(&epoch_json)?;
+
+        info!("finalized new epoch at height {}", epoch_height);
+
+        Ok(da_height)
+    }
+
+    pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
+        debug!("executing block with {} transactions", transactions.len());
+
+        let mut proofs = Vec::new();
+
+        for transaction in transactions {
+            match self.process_transaction_internal(transaction.clone()).await {
+                Ok(proof) => proofs.push(proof),
+                Err(e) => {
+                    warn!(
+                        "Failed to process transaction: {:?}. Error: {}",
+                        transaction, e
+                    );
+                }
+            }
+        }
+
+        Ok(proofs)
+    }
+
+    async fn process_transaction_internal(&self, transaction: Transaction) -> Result<Proof> {
+        let mut tree = self.tree.write().await;
+        tree.process_transaction(transaction)
+    }
path: crates/node_types/prover/src/sequencer.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove duplicate transaction processing methods.**

Both `process_transaction_internal` and `process_transaction` do the same thing. This duplication is confusing and unnecessary.

Either:
1. Make `process_transaction_internal` public and remove `process_transaction`, or
2. Have `process_transaction` call `process_transaction_internal`:

```diff
 pub async fn process_transaction(&self, transaction: Transaction) -> Result<Proof> {
-    let mut tree = self.tree.write().await;
-    tree.process_transaction(transaction)
+    self.process_transaction_internal(transaction).await
 }
```


Also applies to: 232-235

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/sequencer.rs around lines 179 to 182 and 232 to
235, there are duplicate methods process_transaction_internal and
process_transaction that perform the same logic. To fix this, either make
process_transaction_internal public and remove process_transaction entirely, or
refactor process_transaction to simply call process_transaction_internal to
avoid code duplication and confusion.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+use anyhow::{Context, Result, bail};
+use jmt::KeyHash;
+use prism_common::{
+    account::Account,
+    digest::Digest,
+    operation::Operation,
+    transaction::Transaction,
+};
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::SigningKey;
+use prism_storage::database::Database;
+use prism_tree::{
+    AccountResponse::*,
+    hasher::TreeHasher,
+    key_directory_tree::KeyDirectoryTree,
+    proofs::Proof,
+    snarkable_tree::SnarkableTree,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tokio_util::sync::CancellationToken;
+
+use crate::prover_engine::ProverEngine;
+
+#[derive(Clone)]
+pub struct Sequencer {
+    db: Arc<Box<dyn Database>>,
+    da: Arc<dyn DataAvailabilityLayer>,
+    tree: Arc<RwLock<KeyDirectoryTree<Box<dyn Database>>>>,
+    pending_transactions: Arc<RwLock<Vec<Transaction>>>,
+    signing_key: SigningKey,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    batcher_enabled: bool,
+}
+
+impl Sequencer {
+    pub fn new(
+        db: Arc<Box<dyn Database>>,
+        da: Arc<dyn DataAvailabilityLayer>,
+        config: &crate::prover::SequencerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+    ) -> Result<Self> {
+        let saved_epoch = match db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => {
+                debug!("no existing epoch state found, starting at epoch 0");
+                0
+            }
+        };
+
+        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(
+            db.clone(),
+            saved_epoch,
+        )));
+
+        Ok(Sequencer {
+            db,
+            da,
+            tree,
+            pending_transactions: Arc::new(RwLock::new(Vec::new())),
+            signing_key: config.signing_key.clone(),
+            latest_epoch_da_height,
+            batcher_enabled: config.batcher_enabled,
+        })
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        if self.batcher_enabled {
+            self.run_batch_poster(cancellation_token).await
+        } else {
+            // Sequencer without batcher doesn't need a running loop
+            // Just wait for cancellation
+            cancellation_token.cancelled().await;
+            info!("Sequencer: Gracefully stopped (batcher disabled)");
+            Ok(())
+        }
+    }
+
+    async fn run_batch_poster(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+
+        loop {
+            tokio::select! {
+                height_result = height_rx.recv() => {
+                    let height = height_result?;
+                    trace!("received height {}", height);
+
+                    let pending_transactions = {
+                        let mut ops = self.pending_transactions.write().await;
+                        std::mem::take(&mut *ops)
+                    };
+
+                    let tx_count = pending_transactions.len();
+
+                    if !pending_transactions.clone().is_empty() {
+                        match self.da.submit_transactions(pending_transactions).await {
+                            Ok(submitted_height) => {
+                                info!(
+                                    "post_batch_loop: submitted {} transactions at height {}",
+                                    tx_count, submitted_height
+                                );
+                            }
+                            Err(e) => {
+                                error!("post_batch_loop: Failed to submit transactions: {}", e);
+                            }
+                        }
+                    } else {
+                        debug!(
+                            "post_batch_loop: No pending transactions to submit at height {}",
+                            height
+                        );
+                    }
+                },
+                _ = cancellation_token.cancelled() => {
+                    info!("Sequencer: Gracefully stopping batch poster");
+                    return Ok(());
+                }
+            }
+        }
+    }
+
+    pub async fn finalize_new_epoch(
+        &self,
+        epoch_height: u64,
+        transactions: Vec<Transaction>,
+        prover_engine: &Arc<ProverEngine>,
+    ) -> Result<u64> {
+        let mut tree = self.tree.write().await;
+        let batch = tree.process_batch(transactions)?;
+        batch.verify()?;
+
+        let (proof, compressed_proof) = prover_engine.prove_epoch(epoch_height, &batch, &self.db).await?;
+
+        let public_values = proof.public_values.to_vec();
+
+        let mut epoch_json = FinalizedEpoch {
+            height: epoch_height,
+            prev_commitment: batch.prev_root,
+            current_commitment: batch.new_root,
+            proof,
+            compressed_proof,
+            public_values,
+            signature: None,
+        };
+
+        epoch_json.insert_signature(&self.signing_key)?;
+
+        let da_height = self.da.submit_finalized_epoch(epoch_json.clone()).await?;
+        let mut latest_da_height = self.latest_epoch_da_height.write().await;
+        *latest_da_height = da_height;
+
+        self.db.add_epoch(&epoch_json)?;
+
+        info!("finalized new epoch at height {}", epoch_height);
+
+        Ok(da_height)
+    }
+
+    pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
+        debug!("executing block with {} transactions", transactions.len());
+
+        let mut proofs = Vec::new();
+
+        for transaction in transactions {
+            match self.process_transaction_internal(transaction.clone()).await {
+                Ok(proof) => proofs.push(proof),
+                Err(e) => {
+                    warn!(
+                        "Failed to process transaction: {:?}. Error: {}",
+                        transaction, e
+                    );
+                }
+            }
+        }
+
+        Ok(proofs)
+    }
path: crates/node_types/prover/src/sequencer.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Track and return failed transactions.**

Failed transactions are only logged as warnings. The caller has no way to know which transactions failed, leading to potential silent failures.

Consider returning a result that includes both successful proofs and failed transactions:

```diff
-pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
+pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<(Vec<Proof>, Vec<(Transaction, String)>)> {
     debug!("executing block with {} transactions", transactions.len());
 
     let mut proofs = Vec::new();
+    let mut failed_transactions = Vec::new();
 
     for transaction in transactions {
         match self.process_transaction_internal(transaction.clone()).await {
             Ok(proof) => proofs.push(proof),
             Err(e) => {
                 warn!(
                     "Failed to process transaction: {:?}. Error: {}",
                     transaction, e
                 );
+                failed_transactions.push((transaction, e.to_string()));
             }
         }
     }
 
-    Ok(proofs)
+    Ok((proofs, failed_transactions))
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    pub async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<(Vec<Proof>, Vec<(Transaction, String)>)> {
        debug!("executing block with {} transactions", transactions.len());

        let mut proofs = Vec::new();
        let mut failed_transactions = Vec::new();

        for transaction in transactions {
            match self.process_transaction_internal(transaction.clone()).await {
                Ok(proof) => proofs.push(proof),
                Err(e) => {
                    warn!(
                        "Failed to process transaction: {:?}. Error: {}",
                        transaction, e
                    );
                    failed_transactions.push((transaction, e.to_string()));
                }
            }
        }

        Ok((proofs, failed_transactions))
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/sequencer.rs around lines 159 to 177, the
execute_block function currently logs failed transactions but does not return
any information about them, causing silent failures. Modify the function to
return a result type that includes both the vector of successful proofs and a
collection of failed transactions or their errors. This can be done by defining
a custom return type or using a tuple to hold both successes and failures, and
updating the function logic to accumulate failed transactions alongside
successful proofs before returning them.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,236 @@
+use anyhow::{Context, Result, bail};
+use jmt::KeyHash;
+use prism_common::{
+    account::Account,
+    digest::Digest,
+    operation::Operation,
+    transaction::Transaction,
+};
+use prism_da::{DataAvailabilityLayer, FinalizedEpoch};
+use prism_keys::SigningKey;
+use prism_storage::database::Database;
+use prism_tree::{
+    AccountResponse::*,
+    hasher::TreeHasher,
+    key_directory_tree::KeyDirectoryTree,
+    proofs::Proof,
+    snarkable_tree::SnarkableTree,
+};
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tokio_util::sync::CancellationToken;
+
+use crate::prover_engine::ProverEngine;
+
+#[derive(Clone)]
+pub struct Sequencer {
+    db: Arc<Box<dyn Database>>,
+    da: Arc<dyn DataAvailabilityLayer>,
+    tree: Arc<RwLock<KeyDirectoryTree<Box<dyn Database>>>>,
+    pending_transactions: Arc<RwLock<Vec<Transaction>>>,
+    signing_key: SigningKey,
+    latest_epoch_da_height: Arc<RwLock<u64>>,
+    batcher_enabled: bool,
+}
+
+impl Sequencer {
+    pub fn new(
+        db: Arc<Box<dyn Database>>,
+        da: Arc<dyn DataAvailabilityLayer>,
+        config: &crate::prover::SequencerConfig,
+        latest_epoch_da_height: Arc<RwLock<u64>>,
+    ) -> Result<Self> {
+        let saved_epoch = match db.get_latest_epoch_height() {
+            Ok(height) => height + 1,
+            Err(_) => {
+                debug!("no existing epoch state found, starting at epoch 0");
+                0
+            }
+        };
+
+        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(
+            db.clone(),
+            saved_epoch,
+        )));
+
+        Ok(Sequencer {
+            db,
+            da,
+            tree,
+            pending_transactions: Arc::new(RwLock::new(Vec::new())),
+            signing_key: config.signing_key.clone(),
+            latest_epoch_da_height,
+            batcher_enabled: config.batcher_enabled,
+        })
+    }
+
+    pub async fn start(&self, cancellation_token: CancellationToken) -> Result<()> {
+        if self.batcher_enabled {
+            self.run_batch_poster(cancellation_token).await
+        } else {
+            // Sequencer without batcher doesn't need a running loop
+            // Just wait for cancellation
+            cancellation_token.cancelled().await;
+            info!("Sequencer: Gracefully stopped (batcher disabled)");
+            Ok(())
+        }
+    }
+
+    async fn run_batch_poster(&self, cancellation_token: CancellationToken) -> Result<()> {
+        let mut height_rx = self.da.subscribe_to_heights();
+
+        loop {
+            tokio::select! {
+                height_result = height_rx.recv() => {
+                    let height = height_result?;
+                    trace!("received height {}", height);
+
+                    let pending_transactions = {
+                        let mut ops = self.pending_transactions.write().await;
+                        std::mem::take(&mut *ops)
+                    };
+
+                    let tx_count = pending_transactions.len();
+
+                    if !pending_transactions.clone().is_empty() {
+                        match self.da.submit_transactions(pending_transactions).await {
+                            Ok(submitted_height) => {
+                                info!(
+                                    "post_batch_loop: submitted {} transactions at height {}",
+                                    tx_count, submitted_height
+                                );
+                            }
+                            Err(e) => {
+                                error!("post_batch_loop: Failed to submit transactions: {}", e);
+                            }
+                        }
+                    } else {
path: crates/node_types/prover/src/sequencer.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Optimize transaction handling to avoid unnecessary cloning.**

The code clones `pending_transactions` before checking if it's empty, which is inefficient for large transaction batches.

```diff
-let pending_transactions = {
-    let mut ops = self.pending_transactions.write().await;
-    std::mem::take(&mut *ops)
-};
-
-let tx_count = pending_transactions.len();
-
-if !pending_transactions.clone().is_empty() {
-    match self.da.submit_transactions(pending_transactions).await {
+let pending_transactions = {
+    let mut ops = self.pending_transactions.write().await;
+    if ops.is_empty() {
+        None
+    } else {
+        Some(std::mem::take(&mut *ops))
+    }
+};
+
+if let Some(transactions) = pending_transactions {
+    let tx_count = transactions.len();
+    match self.da.submit_transactions(transactions).await {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
                    let pending_transactions = {
                        let mut ops = self.pending_transactions.write().await;
                        if ops.is_empty() {
                            None
                        } else {
                            Some(std::mem::take(&mut *ops))
                        }
                    };

                    if let Some(transactions) = pending_transactions {
                        let tx_count = transactions.len();
                        match self.da.submit_transactions(transactions).await {
                            Ok(submitted_height) => {
                                info!(
                                    "post_batch_loop: submitted {} transactions at height {}",
                                    tx_count, submitted_height
                                );
                            }
                            Err(e) => {
                                error!("post_batch_loop: Failed to submit transactions: {}", e);
                            }
                        }
                    } else {
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/sequencer.rs around lines 88 to 107, the code
clones the pending_transactions vector just to check if it is empty, which is
inefficient. Instead, check if pending_transactions is empty directly without
cloning it. Use the original pending_transactions variable for the emptiness
check and subsequent processing to avoid unnecessary cloning and improve
performance.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,526 +122,135 @@ impl Prover {
         db: Arc<Box<dyn Database>>,
         da: Arc<dyn DataAvailabilityLayer>,
         cfg: &Config,
+        cancellation_token: CancellationToken,
     ) -> Result<Prover> {
-        let saved_epoch = match db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => {
-                debug!("no existing epoch state found, starting at epoch 0");
-                0
-            }
-        };
+        let latest_epoch_da_height = Arc::new(RwLock::new(0));
 
-        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(db.clone(), saved_epoch)));
+        let prover_engine = Arc::new(ProverEngine::new(&cfg.prover_engine)?);
 
-        // Create separate prover clients for base and recursive proofs
-        let base_prover_client = ProverClient::from_env();
-        let recursive_prover_client = ProverClient::from_env();
+        let sequencer = Arc::new(Sequencer::new(
+            db.clone(),
+            da.clone(),
+            &cfg.sequencer,
+            latest_epoch_da_height.clone(),
+        )?);
 
-        // Setup keys for both provers
-        let (base_pk, base_vk) = base_prover_client.setup(BASE_PRISM_ELF);
-        let (recursive_pk, recursive_vk) = recursive_prover_client.setup(RECURSIVE_PRISM_ELF);
+        let syncer = Arc::new(Syncer::new(
+            da,
+            db,
+            &cfg.syncer,
+            latest_epoch_da_height.clone(),
+            sequencer.clone(),
+            prover_engine.clone(),
+        ));
 
         Ok(Prover {
-            db: db.clone(),
-            da,
             cfg: cfg.clone(),
-            base_proving_key: base_pk,
-            base_verifying_key: base_vk,
-            recursive_proving_key: recursive_pk,
-            recursive_verifying_key: recursive_vk,
-            base_prover_client: Arc::new(RwLock::new(base_prover_client)),
-            recursive_prover_client: Arc::new(RwLock::new(recursive_prover_client)),
-            tree,
-            pending_transactions: Arc::new(RwLock::new(Vec::new())),
-            latest_epoch_da_height: Arc::new(RwLock::new(0)),
+            prover_engine,
+            sequencer,
+            syncer,
+            latest_epoch_da_height,
+            cancellation_token,
         })
     }
 
-    pub async fn run(self: Arc<Self>) -> Result<()> {
-        self.da
-            .start()
-            .await
-            .map_err(|e| DataAvailabilityError::InitializationError(e.to_string()))
-            .context("Failed to start DataAvailabilityLayer")?;
-
-        let main_loop = self.clone().main_loop();
-
-        let mut futures = JoinSet::new();
-        futures.spawn(main_loop);
-
-        if self.cfg.batcher {
-            let batch_poster = self.clone().post_batch_loop();
-            futures.spawn(batch_poster);
-        }
-
-        let ws = WebServer::new(self.cfg.webserver.clone(), self.clone());
-        if self.cfg.webserver.enabled {
-            futures.spawn(async move { ws.start().await });
-        }
-
-        if let Some(result) = futures.join_next().await {
-            error!("Service exited unexpectedly: {:?}", result);
-            Err(anyhow!("Service exited unexpectedly"))?
-        }
-        error!("All services have ended unexpectedly.");
-        Err(anyhow!("All services have ended unexpectedly"))?
-    }
-
-    async fn main_loop(self: Arc<Self>) -> Result<()> {
-        let mut height_rx = self.da.subscribe_to_heights();
-        let historical_sync_height = height_rx.recv().await?;
-
-        let start_height = match self.db.get_last_synced_height() {
-            Ok(height) => height,
-            Err(_) => {
-                debug!("no existing sync height found, setting sync height to start_height");
-                self.db.set_last_synced_height(&self.cfg.start_height)?;
-                self.cfg.start_height
-            }
-        };
-
-        self.sync_loop(start_height, historical_sync_height, height_rx).await
+    pub fn get_db(&self) -> Arc<Box<dyn Database>> {
+        self.sequencer.get_db()
     }
 
-    async fn sync_loop(
+    pub async fn execute_block(
         &self,
-        start_height: u64,
-        end_height: u64,
-        mut incoming_heights: broadcast::Receiver<u64>,
-    ) -> Result<()> {
-        // TODO: Should be persisted in database for crash recovery
-        let mut buffered_transactions: VecDeque<Transaction> = VecDeque::new();
-        let mut current_height = start_height;
-
-        while current_height <= end_height {
-            self.process_da_height(current_height, &mut buffered_transactions, false).await?;
-            // TODO: Race between set_epoch and set_last_synced_height
-            self.db.set_last_synced_height(&current_height)?;
-            current_height += 1;
-        }
-
-        info!(
-            "finished historical sync from height {} to {}",
-            start_height, end_height
-        );
-
-        loop {
-            let height = incoming_heights.recv().await?;
-            if height != current_height {
-                return Err(anyhow!(
-                    "heights are not sequential: expected {}, got {}",
-                    current_height,
-                    height
-                ));
-            }
-            self.process_da_height(height, &mut buffered_transactions, true).await?;
-            current_height += 1;
-            // TODO: Race between set_epoch and set_last_synced_height - updating these should be a single atomic transaction
-            self.db.set_last_synced_height(&current_height)?;
-        }
-    }
-
-    async fn process_da_height(
-        &self,
-        height: u64,
-        buffered_transactions: &mut VecDeque<Transaction>,
-        is_real_time: bool,
-    ) -> Result<()> {
-        let next_epoch_height = match self.db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => 0,
-        };
-
-        let transactions = self.da.get_transactions(height).await?;
-        let epoch_result = self.da.get_finalized_epoch(height).await?;
-
-        debug!(
-            "processing {} height {}, next_epoch_height: {}",
-            if is_real_time { "new" } else { "old" },
-            height,
-            next_epoch_height
-        );
-
-        if let Some(epoch) = epoch_result {
-            // run all buffered transactions from the last celestia blocks and increment current_epoch
-            self.process_epoch(epoch, buffered_transactions).await?;
-        } else {
-            debug!("No transactions to process at height {}", height);
-        }
-
-        if is_real_time && !buffered_transactions.is_empty() && self.cfg.prover {
-            let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-            self.finalize_new_epoch(next_epoch_height, all_transactions).await?;
-        }
-
-        // If there are new transactions at this height, add them to the queue to
-        // be included in the next finalized epoch.
-        if !transactions.is_empty() {
-            buffered_transactions.extend(transactions);
-            return Ok(());
-        }
-
-        // post gap filler proof if max gap has been reached
-        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
-        if latest_epoch_height != 0
-            && height.saturating_sub(latest_epoch_height) >= self.cfg.max_epochless_gap
-        {
-            self.finalize_new_epoch(next_epoch_height, Vec::new()).await?;
-        }
-
-        if let Some(metrics) = get_metrics() {
-            metrics.record_celestia_synced_height(height, vec![]);
-            metrics.record_current_epoch(next_epoch_height, vec![]);
-        }
-
-        Ok(())
-    }
-
-    async fn process_epoch(
-        &self,
-        epoch: FinalizedEpoch,
-        buffered_transactions: &mut VecDeque<Transaction>,
-    ) -> Result<()> {
-        let current_epoch = match self.db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => 0,
-        };
-
-        // If prover is enabled and is actively producing new epochs, it has
-        // likely already ran all of the transactions in the found epoch, so no
-        // further processing is needed
-        if epoch.height < current_epoch {
-            debug!("epoch {} already processed internally", current_epoch);
-            return Ok(());
-        }
-
-        // TODO: Issue #144
-        epoch
-            .verify_signature(self.cfg.verifying_key.clone())
-            .with_context(|| format!("Invalid signature in epoch {}", epoch.height))?;
-        trace!("valid signature for epoch {}", epoch.height);
-
-        let prev_commitment = if epoch.height == 0 {
-            self.get_commitment_from_tree().await?
-        } else {
-            self.db.get_epoch(&epoch.height.saturating_sub(1))?.current_commitment
-        };
-
-        if epoch.height != current_epoch {
-            return Err(anyhow!(
-                "epoch height mismatch: expected {}, got {}",
-                current_epoch,
-                epoch.height
-            ));
-        }
-
-        if epoch.prev_commitment != prev_commitment {
-            return Err(anyhow!(
-                "previous commitment mismatch at epoch {}",
-                current_epoch
-            ));
-        }
-
-        let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-        if !all_transactions.is_empty() {
-            self.execute_block(all_transactions).await?;
-        }
-
-        let new_commitment = self.get_commitment_from_tree().await?;
-        if epoch.current_commitment != new_commitment {
-            return Err(anyhow!(
-                "new commitment mismatch at epoch {}",
-                current_epoch
-            ));
-        }
-
-        // distinguish between base and recursive proofs for client and verifying key
-        let client = if epoch.height == 0 || !self.cfg.recursive_proofs {
-            self.base_prover_client.read().await
-        } else {
-            self.recursive_prover_client.read().await
-        };
-
-        let verifying_key = if epoch.height == 0 || !self.cfg.recursive_proofs {
-            &self.base_verifying_key
-        } else {
-            &self.recursive_verifying_key
-        };
-
-        match client.verify(&epoch.proof, verifying_key) {
-            Ok(_) => info!(
-                "zkSNARK for epoch {} was validated successfully",
-                epoch.height
-            ),
-            Err(err) => panic!(
-                "failed to validate epoch at height {}: {:?}",
-                epoch.height, err
-            ),
-        }
-
-        debug!(
-            "processed epoch {}. new commitment: {:?}",
-            current_epoch, new_commitment
-        );
-
-        self.db.add_epoch(&epoch)?;
-
-        Ok(())
-    }
-
-    // should only be called for testing and historical sync, it does not
-    // generate the Batch object for proof generation.
-    async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
-        debug!("executing block with {} transactions", transactions.len());
-
-        let mut proofs = Vec::new();
-
-        for transaction in transactions {
-            match self.process_transaction(transaction.clone()).await {
-                Ok(proof) => proofs.push(proof),
-                Err(e) => {
-                    // Log the error and continue with the next transaction
-                    warn!(
-                        "Failed to process transaction: {:?}. Error: {}",
-                        transaction, e
-                    );
-                }
-            }
-        }
-
-        Ok(proofs)
+        transactions: Vec<Transaction>,
+    ) -> Result<Vec<prism_tree::proofs::Proof>> {
+        self.sequencer.execute_block(transactions).await
     }
 
-    /// Finalizes a new epoch by processing the given transactions, proving the epoch,
-    /// and submitting it to the data availability layer.
-    /// Returns the height on the DA layer where the epoch was submitted.
-    async fn finalize_new_epoch(
+    pub async fn finalize_new_epoch(
         &self,
         epoch_height: u64,
         transactions: Vec<Transaction>,
     ) -> Result<u64> {
-        let mut tree = self.tree.write().await;
-        let batch = tree.process_batch(transactions)?;
-        batch.verify()?;
-
-        let finalized_epoch = self.prove_epoch(epoch_height, &batch).await?;
-
-        let da_height = self.da.submit_finalized_epoch(finalized_epoch.clone()).await?;
-        let mut latest_da_height = self.latest_epoch_da_height.write().await;
-        *latest_da_height = da_height;
-
-        // only save the epoch locally if it was successfully submitted
-        self.db.add_epoch(&finalized_epoch)?;
-
-        info!("finalized new epoch at height {}", epoch_height);
-
-        Ok(da_height)
+        self.sequencer.finalize_new_epoch(epoch_height, transactions, &self.prover_engine).await
     }
 
-    async fn prove_with_base_prover(
-        &self,
-        epoch_height: u64,
-        batch: &Batch,
-    ) -> Result<(
-        SP1ProofWithPublicValues,
-        SP1ProofWithPublicValues,
-        tokio::sync::RwLockReadGuard<'_, EnvProver>,
-        &SP1VerifyingKey,
-    )> {
-        let mut stdin = SP1Stdin::new();
-        stdin.write(batch);
-
-        let client = self.base_prover_client.read().await;
-        info!("generating proof for epoch {}", epoch_height);
-
-        let proof = client.prove(&self.base_proving_key, &stdin).groth16().run()?;
-        info!(
-            "successfully generated base proof for epoch {}",
-            epoch_height
-        );
-
-        let compressed_proof = client.prove(&self.base_proving_key, &stdin).compressed().run()?;
-        info!(
-            "successfully generated compressed proof for epoch {}",
-            epoch_height
-        );
-
-        Ok((proof, compressed_proof, client, &self.base_verifying_key))
+    pub async fn validate_and_queue_update(&self, transaction: Transaction) -> Result<()> {
+        self.sequencer.validate_and_queue_update(transaction).await
     }
 
-    async fn prove_with_recursive_prover(
-        &self,
-        epoch_height: u64,
-        batch: &Batch,
-    ) -> Result<(
-        SP1ProofWithPublicValues,
-        SP1ProofWithPublicValues,
-        tokio::sync::RwLockReadGuard<'_, EnvProver>,
-        &SP1VerifyingKey,
-    )> {
-        let prev_epoch = match self.db.get_latest_epoch() {
-            Ok(epoch) => epoch,
-            Err(_) => {
-                return Err(anyhow!(
-                    "Previous epoch not found for recursive verification at height {}",
-                    epoch_height - 1
-                ));
-            }
-        };
-
-        let vk_to_use = if prev_epoch.height == 0 {
-            self.base_verifying_key.clone()
-        } else {
-            self.recursive_verifying_key.clone()
-        };
-
-        let mut stdin = SP1Stdin::new();
-        // Write recursive inputs
-        let compressed_proof = match prev_epoch.compressed_proof.proof {
-            SP1Proof::Compressed(proof) => proof,
-            _ => return Err(anyhow!("Invalid proof type: expected compressed proof")),
-        };
-        stdin.write_proof(*compressed_proof, vk_to_use.clone().vk);
-        stdin.write_vec(prev_epoch.public_values.to_vec());
-        stdin.write(&vk_to_use.hash_u32());
-        stdin.write(batch);
-
-        let client = self.recursive_prover_client.read().await;
-        info!(
-            "generating recursive proof for epoch at height {}",
-            epoch_height
-        );
-
-        let proof = client.prove(&self.recursive_proving_key, &stdin).groth16().run()?;
-        info!(
-            "successfully generated recursive proof for epoch {}",
-            epoch_height
-        );
-        let compressed_proof =
-            client.prove(&self.recursive_proving_key, &stdin).compressed().run()?;
-        info!(
-            "successfully generated recursive compressed proof for epoch {}",
-            epoch_height
-        );
-
-        Ok((
-            proof,
-            compressed_proof,
-            client,
-            &self.recursive_verifying_key,
-        ))
+    pub fn get_pending_transactions(&self) -> Arc<RwLock<Vec<Transaction>>> {
+        self.sequencer.get_pending_transactions()
     }
 
-    async fn prove_epoch(&self, epoch_height: u64, batch: &Batch) -> Result<FinalizedEpoch> {
-        let (proof, compressed_proof, client, verifying_key) =
-            if epoch_height == 0 || !self.cfg.recursive_proofs {
-                self.prove_with_base_prover(epoch_height, batch).await?
-            } else {
-                self.prove_with_recursive_prover(epoch_height, batch).await?
-            };
-
-        client.verify(&proof, verifying_key)?;
-        info!("verified proof for epoch {}", epoch_height);
-
-        let public_values = proof.public_values.to_vec();
-
-        let mut epoch_json = FinalizedEpoch {
-            height: epoch_height,
-            prev_commitment: batch.prev_root,
-            current_commitment: batch.new_root,
-            proof,
-            compressed_proof,
-            public_values,
-            signature: None,
-        };
-
-        epoch_json.insert_signature(&self.cfg.signing_key)?;
-        Ok(epoch_json)
+    pub async fn process_transaction(
+        &self,
+        transaction: Transaction,
+    ) -> Result<prism_tree::proofs::Proof> {
+        self.sequencer.process_transaction(transaction).await
     }
 
-    async fn post_batch_loop(self: Arc<Self>) -> Result<()> {
-        let mut height_rx = self.da.subscribe_to_heights();
-
-        loop {
-            let height = height_rx.recv().await?;
-            trace!("received height {}", height);
-
-            // Get pending transactions
-            let pending_transactions = {
-                let mut ops = self.pending_transactions.write().await;
-                std::mem::take(&mut *ops)
-            };
-
-            let tx_count = pending_transactions.len();
-
-            // If there are pending transactions, submit them
-            if !pending_transactions.clone().is_empty() {
-                match self.da.submit_transactions(pending_transactions).await {
-                    Ok(submitted_height) => {
-                        info!(
-                            "post_batch_loop: submitted {} transactions at height {}",
-                            tx_count, submitted_height
-                        );
-                    }
-                    Err(e) => {
-                        error!("post_batch_loop: Failed to submit transactions: {}", e);
-                    }
-                }
-            } else {
-                debug!(
-                    "post_batch_loop: No pending transactions to submit at height {}",
-                    height
-                );
-            }
-        }
+    pub fn get_da(&self) -> Arc<dyn DataAvailabilityLayer> {
+        self.syncer.get_da()
     }
 
-    async fn get_commitment_from_tree(&self) -> Result<Digest> {
-        let tree = self.tree.read().await;
-        tree.get_commitment().context("Failed to get commitment")
-    }
 
-    async fn get_account_from_tree(&self, id: &str) -> Result<prism_tree::AccountResponse> {
-        let tree = self.tree.read().await;
-        let key_hash = KeyHash::with::<TreeHasher>(id);
+    pub async fn run(self: Arc<Self>) -> Result<()> {
+        let mut futures = JoinSet::new();
 
-        tree.get(key_hash)
-    }
+        // Start Syncer (includes DA startup and main sync loop)
+        let syncer = self.syncer.clone();
+        let cancel_token = self.cancellation_token.clone();
+        futures.spawn(async move { syncer.start(cancel_token).await });
 
-    /// Updates the state from an already verified pending transaction.
-    async fn process_transaction(&self, transaction: Transaction) -> Result<Proof> {
-        let mut tree = self.tree.write().await;
-        tree.process_transaction(transaction)
-    }
+        // Start Sequencer (batch poster if enabled)
+        let sequencer = self.sequencer.clone();
+        let cancel_token = self.cancellation_token.clone();
+        futures.spawn(async move { sequencer.start(cancel_token).await });
 
-    /// Adds an transaction to be posted to the DA layer and applied in the next epoch.
-    pub async fn validate_and_queue_update(&self, transaction: Transaction) -> Result<()> {
-        if !self.cfg.batcher {
-            bail!("Batcher is disabled, cannot queue transactions");
+        // Start WebServer if enabled
+        if self.cfg.webserver.enabled {
+            let ws = WebServer::new(self.cfg.webserver.clone(), self.clone());
+            futures.spawn(async move { ws.start().await });
         }
 
-        // validate against existing account if necessary, including signature checks
-        match transaction.operation {
-            Operation::RegisterService { .. } | Operation::CreateAccount { .. } => {
-                Account::default().process_transaction(&transaction)?;
+        // Wait for any service to exit
+        let exit_result = if let Some(result) = futures.join_next().await {
+            match result {
+                Ok(service_result) => {
+                    match service_result {
+                        Ok(_) => {
+                            info!("Service exited gracefully, shutting down other components");
+                            self.cancellation_token.cancel();
+                            Ok(())
+                        },
+                        Err(service_error) => {
+                            error!("Service exited with error: {:?}, shutting down other components", service_error);
+                            self.cancellation_token.cancel();
+                            Err(service_error)
+                        }
+                    }
+                },
+                Err(join_error) => {
+                    error!("Task join error: {:?}, shutting down other components", join_error);
+                    self.cancellation_token.cancel();
+                    Err(anyhow!("Task join error: {}", join_error))
+                }
             }
-            Operation::AddKey { .. }
-            | Operation::RevokeKey { .. }
-            | Operation::AddData { .. }
-            | Operation::SetData { .. } => {
-                let account_response = self.get_account_from_tree(&transaction.id).await?;
-
-                let Found(mut account, _) = account_response else {
-                    bail!("Account not found for id: {}", transaction.id)
-                };
+        } else {
+            error!("No futures in join set, shutting down");
+            Ok(())
+        };
path: crates/node_types/prover/src/prover/mod.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle the edge case when no services are started.**

When no futures are in the join set (e.g., all services disabled), the code returns `Ok()`. This scenario should either be an error or have a more descriptive log message.

Apply this diff to improve the handling:

```diff
        } else {
-            error!("No futures in join set, shutting down");
-            Ok(())
+            warn!("No services were started (all components may be disabled)");
+            Err(anyhow!("No services to run - check configuration"))
        };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        } else {
            warn!("No services were started (all components may be disabled)");
            Err(anyhow!("No services to run - check configuration"))
        };
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/prover/mod.rs around lines 238 to 241, the code
returns Ok(()) when no futures are in the join set, which can be unclear. Update
the code to either return an error indicating no services were started or
enhance the log message to clearly state that no services were enabled and the
system is shutting down. This will improve clarity and handling of this edge
case.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T13:51:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,526 +122,139 @@ impl Prover {
         db: Arc<Box<dyn Database>>,
         da: Arc<dyn DataAvailabilityLayer>,
         cfg: &Config,
+        cancellation_token: CancellationToken,
     ) -> Result<Prover> {
-        let saved_epoch = match db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => {
-                debug!("no existing epoch state found, starting at epoch 0");
-                0
-            }
-        };
+        let latest_epoch_da_height = Arc::new(RwLock::new(0));
 
-        let tree = Arc::new(RwLock::new(KeyDirectoryTree::load(db.clone(), saved_epoch)));
+        let prover_engine = Arc::new(ProverEngine::new(&cfg.prover_engine)?);
 
-        // Create separate prover clients for base and recursive proofs
-        let base_prover_client = ProverClient::from_env();
-        let recursive_prover_client = ProverClient::from_env();
+        let sequencer = Arc::new(Sequencer::new(
+            db.clone(),
+            da.clone(),
+            &cfg.sequencer,
+            latest_epoch_da_height.clone(),
+        )?);
 
-        // Setup keys for both provers
-        let (base_pk, base_vk) = base_prover_client.setup(BASE_PRISM_ELF);
-        let (recursive_pk, recursive_vk) = recursive_prover_client.setup(RECURSIVE_PRISM_ELF);
+        let syncer = Arc::new(Syncer::new(
+            da,
+            db,
+            &cfg.syncer,
+            latest_epoch_da_height.clone(),
+            sequencer.clone(),
+            prover_engine.clone(),
+        ));
 
         Ok(Prover {
-            db: db.clone(),
-            da,
             cfg: cfg.clone(),
-            base_proving_key: base_pk,
-            base_verifying_key: base_vk,
-            recursive_proving_key: recursive_pk,
-            recursive_verifying_key: recursive_vk,
-            base_prover_client: Arc::new(RwLock::new(base_prover_client)),
-            recursive_prover_client: Arc::new(RwLock::new(recursive_prover_client)),
-            tree,
-            pending_transactions: Arc::new(RwLock::new(Vec::new())),
-            latest_epoch_da_height: Arc::new(RwLock::new(0)),
+            prover_engine,
+            sequencer,
+            syncer,
+            latest_epoch_da_height,
+            cancellation_token,
         })
     }
 
-    pub async fn run(self: Arc<Self>) -> Result<()> {
-        self.da
-            .start()
-            .await
-            .map_err(|e| DataAvailabilityError::InitializationError(e.to_string()))
-            .context("Failed to start DataAvailabilityLayer")?;
-
-        let main_loop = self.clone().main_loop();
-
-        let mut futures = JoinSet::new();
-        futures.spawn(main_loop);
-
-        if self.cfg.batcher {
-            let batch_poster = self.clone().post_batch_loop();
-            futures.spawn(batch_poster);
-        }
-
-        let ws = WebServer::new(self.cfg.webserver.clone(), self.clone());
-        if self.cfg.webserver.enabled {
-            futures.spawn(async move { ws.start().await });
-        }
-
-        if let Some(result) = futures.join_next().await {
-            error!("Service exited unexpectedly: {:?}", result);
-            Err(anyhow!("Service exited unexpectedly"))?
-        }
-        error!("All services have ended unexpectedly.");
-        Err(anyhow!("All services have ended unexpectedly"))?
-    }
-
-    async fn main_loop(self: Arc<Self>) -> Result<()> {
-        let mut height_rx = self.da.subscribe_to_heights();
-        let historical_sync_height = height_rx.recv().await?;
-
-        let start_height = match self.db.get_last_synced_height() {
-            Ok(height) => height,
-            Err(_) => {
-                debug!("no existing sync height found, setting sync height to start_height");
-                self.db.set_last_synced_height(&self.cfg.start_height)?;
-                self.cfg.start_height
-            }
-        };
-
-        self.sync_loop(start_height, historical_sync_height, height_rx).await
-    }
-
-    async fn sync_loop(
-        &self,
-        start_height: u64,
-        end_height: u64,
-        mut incoming_heights: broadcast::Receiver<u64>,
-    ) -> Result<()> {
-        // TODO: Should be persisted in database for crash recovery
-        let mut buffered_transactions: VecDeque<Transaction> = VecDeque::new();
-        let mut current_height = start_height;
-
-        while current_height <= end_height {
-            self.process_da_height(current_height, &mut buffered_transactions, false).await?;
-            // TODO: Race between set_epoch and set_last_synced_height
-            self.db.set_last_synced_height(&current_height)?;
-            current_height += 1;
-        }
-
-        info!(
-            "finished historical sync from height {} to {}",
-            start_height, end_height
-        );
-
-        loop {
-            let height = incoming_heights.recv().await?;
-            if height != current_height {
-                return Err(anyhow!(
-                    "heights are not sequential: expected {}, got {}",
-                    current_height,
-                    height
-                ));
-            }
-            self.process_da_height(height, &mut buffered_transactions, true).await?;
-            current_height += 1;
-            // TODO: Race between set_epoch and set_last_synced_height - updating these should be a single atomic transaction
-            self.db.set_last_synced_height(&current_height)?;
-        }
-    }
-
-    async fn process_da_height(
-        &self,
-        height: u64,
-        buffered_transactions: &mut VecDeque<Transaction>,
-        is_real_time: bool,
-    ) -> Result<()> {
-        let next_epoch_height = match self.db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => 0,
-        };
-
-        let transactions = self.da.get_transactions(height).await?;
-        let epoch_result = self.da.get_finalized_epoch(height).await?;
-
-        debug!(
-            "processing {} height {}, next_epoch_height: {}",
-            if is_real_time { "new" } else { "old" },
-            height,
-            next_epoch_height
-        );
-
-        if let Some(epoch) = epoch_result {
-            // run all buffered transactions from the last celestia blocks and increment current_epoch
-            self.process_epoch(epoch, buffered_transactions).await?;
-        } else {
-            debug!("No transactions to process at height {}", height);
-        }
-
-        if is_real_time && !buffered_transactions.is_empty() && self.cfg.prover {
-            let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-            self.finalize_new_epoch(next_epoch_height, all_transactions).await?;
-        }
-
-        // If there are new transactions at this height, add them to the queue to
-        // be included in the next finalized epoch.
-        if !transactions.is_empty() {
-            buffered_transactions.extend(transactions);
-            return Ok(());
-        }
-
-        // post gap filler proof if max gap has been reached
-        let latest_epoch_height = *self.latest_epoch_da_height.read().await;
-        if latest_epoch_height != 0
-            && height.saturating_sub(latest_epoch_height) >= self.cfg.max_epochless_gap
-        {
-            self.finalize_new_epoch(next_epoch_height, Vec::new()).await?;
-        }
-
-        if let Some(metrics) = get_metrics() {
-            metrics.record_celestia_synced_height(height, vec![]);
-            metrics.record_current_epoch(next_epoch_height, vec![]);
-        }
-
-        Ok(())
+    pub fn get_db(&self) -> Arc<Box<dyn Database>> {
+        self.sequencer.get_db()
     }
 
-    async fn process_epoch(
+    pub async fn execute_block(
         &self,
-        epoch: FinalizedEpoch,
-        buffered_transactions: &mut VecDeque<Transaction>,
-    ) -> Result<()> {
-        let current_epoch = match self.db.get_latest_epoch_height() {
-            Ok(height) => height + 1,
-            Err(_) => 0,
-        };
-
-        // If prover is enabled and is actively producing new epochs, it has
-        // likely already ran all of the transactions in the found epoch, so no
-        // further processing is needed
-        if epoch.height < current_epoch {
-            debug!("epoch {} already processed internally", current_epoch);
-            return Ok(());
-        }
-
-        // TODO: Issue #144
-        epoch
-            .verify_signature(self.cfg.verifying_key.clone())
-            .with_context(|| format!("Invalid signature in epoch {}", epoch.height))?;
-        trace!("valid signature for epoch {}", epoch.height);
-
-        let prev_commitment = if epoch.height == 0 {
-            self.get_commitment_from_tree().await?
-        } else {
-            self.db.get_epoch(&epoch.height.saturating_sub(1))?.current_commitment
-        };
-
-        if epoch.height != current_epoch {
-            return Err(anyhow!(
-                "epoch height mismatch: expected {}, got {}",
-                current_epoch,
-                epoch.height
-            ));
-        }
-
-        if epoch.prev_commitment != prev_commitment {
-            return Err(anyhow!(
-                "previous commitment mismatch at epoch {}",
-                current_epoch
-            ));
-        }
-
-        let all_transactions: Vec<Transaction> = buffered_transactions.drain(..).collect();
-        if !all_transactions.is_empty() {
-            self.execute_block(all_transactions).await?;
-        }
-
-        let new_commitment = self.get_commitment_from_tree().await?;
-        if epoch.current_commitment != new_commitment {
-            return Err(anyhow!(
-                "new commitment mismatch at epoch {}",
-                current_epoch
-            ));
-        }
-
-        // distinguish between base and recursive proofs for client and verifying key
-        let client = if epoch.height == 0 || !self.cfg.recursive_proofs {
-            self.base_prover_client.read().await
-        } else {
-            self.recursive_prover_client.read().await
-        };
-
-        let verifying_key = if epoch.height == 0 || !self.cfg.recursive_proofs {
-            &self.base_verifying_key
-        } else {
-            &self.recursive_verifying_key
-        };
-
-        match client.verify(&epoch.proof, verifying_key) {
-            Ok(_) => info!(
-                "zkSNARK for epoch {} was validated successfully",
-                epoch.height
-            ),
-            Err(err) => panic!(
-                "failed to validate epoch at height {}: {:?}",
-                epoch.height, err
-            ),
-        }
-
-        debug!(
-            "processed epoch {}. new commitment: {:?}",
-            current_epoch, new_commitment
-        );
-
-        self.db.add_epoch(&epoch)?;
-
-        Ok(())
-    }
-
-    // should only be called for testing and historical sync, it does not
-    // generate the Batch object for proof generation.
-    async fn execute_block(&self, transactions: Vec<Transaction>) -> Result<Vec<Proof>> {
-        debug!("executing block with {} transactions", transactions.len());
-
-        let mut proofs = Vec::new();
-
-        for transaction in transactions {
-            match self.process_transaction(transaction.clone()).await {
-                Ok(proof) => proofs.push(proof),
-                Err(e) => {
-                    // Log the error and continue with the next transaction
-                    warn!(
-                        "Failed to process transaction: {:?}. Error: {}",
-                        transaction, e
-                    );
-                }
-            }
-        }
-
-        Ok(proofs)
+        transactions: Vec<Transaction>,
+    ) -> Result<Vec<prism_tree::proofs::Proof>> {
+        self.sequencer.execute_block(transactions).await
     }
path: crates/node_types/prover/src/prover/mod.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider updating to handle failed transactions.**

If the `Sequencer::execute_block` method is updated to return failed transactions (as suggested in the sequencer review), this delegation method should be updated accordingly to propagate that information.


Let me verify how `execute_block` is used in the codebase:

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check usage of execute_block method to assess impact of signature change

# Search for execute_block calls
ast-grep --pattern 'execute_block($_)'

# Also search with ripgrep for broader context
rg -A 3 'execute_block\(' --type rust
```

Length of output: 2302

---



</details>

**Update execute_block delegation and its callers to handle failed transactions**

If `Sequencer::execute_block` is changed to return both successful proofs and a list of failed transactions, you’ll need to update all call sites accordingly:

- crates/node_types/prover/src/prover/mod.rs (lines 161–166):  
  • Change the return type from `Result<Vec<Proof>>` to something like  
    `Result<(Vec<Proof>, Vec<(Transaction, String)>)>`  
  • Destructure and propagate both proofs and failed transactions.

- crates/node_types/prover/src/syncer.rs:  
  • Update `self.sequencer.execute_block(all_transactions).await?;` to handle the new tuple return.  
  • Decide how to process or log failures in the syncer.

- crates/node_types/prover/src/prover/tests.rs:  
  • Adjust `let proofs = prover.execute_block(...).await.unwrap();` to unpack the tuple.  
  • Update assertions to check both the number of proofs and any expected failures.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In crates/node_types/prover/src/prover/mod.rs around lines 161 to 166, update
the execute_block method to handle the new return type from
Sequencer::execute_block, which now returns both successful proofs and failed
transactions. Change the return type to Result<(Vec<Proof>, Vec<(Transaction,
String)>)> and adjust the method to destructure and propagate both values.
Additionally, update all callers of this method, including in
crates/node_types/prover/src/syncer.rs and
crates/node_types/prover/src/prover/tests.rs, to handle the tuple return by
unpacking proofs and failures and processing or asserting them accordingly.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T09:01:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-23T13:41:07Z
body: [vc]: #P7nbnr2kemrkx4Xc1jBEcTjwwlx1UdGUrz3w+vVq2PQ=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwcmlzbSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2RlbHRhZGV2cy9wcmlzbS8zb3hhOGpIWTZzUkdaOVRkcHlnN0RqYTdQU1Z0IiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJyb290RGlyZWN0b3J5IjoiZG9jLyIsInByZXZpZXdVcmwiOiJwcmlzbS1naXQtbW9kdWxhci1wcm92ZXItZGVsdGFkZXZzLnZlcmNlbC5hcHAifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **prism** | ⬜️ Ignored ([Inspect](https://vercel.com/deltadevs/prism/3oxa8jHY6sRGZ9Tdpyg7Dja7PSVt)) | [Visit Preview](https://prism-git-modular-prover-deltadevs.vercel.app) |  | May 30, 2025 8:57am |
</details>


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-23T13:41:10Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces a comprehensive modular refactor of the prover node architecture. The monolithic `Prover` is split into three components—`Syncer`, `Sequencer`, and `ProverEngine`—each with its own configuration struct. The system now uses a cancellation token for coordinated shutdown, and configuration handling is updated throughout the CLI, tests, and main application.

## Changes

| File(s)                                                                                  | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Cargo.toml, crates/cli/Cargo.toml, crates/node_types/prover/Cargo.toml, crates/tests/Cargo.toml | Added `tokio-util` as a workspace dependency in all relevant Cargo.toml files.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| crates/cli/src/main.rs                                                                   | Refactored to use a `CancellationToken` for graceful shutdown, updated `Prover::new` calls to include the token, and adopted the new modular configuration struct layout. Added signal handling for SIGINT/SIGTERM to trigger coordinated cancellation.                                                                                                  |
| crates/node_types/prover/src/lib.rs                                                      | Re-exported new configuration types (`SyncerConfig`, `SequencerConfig`, `ProverEngineConfig`), made new modules (`prover_engine`, `sequencer`, `syncer`) public, and re-exported `WebServerConfig`.                                                                                                                                                                                                                                                                                                                                                       |
| crates/node_types/prover/src/prover/mod.rs                                               | Major refactor: split monolithic `Prover` into modular components (`Syncer`, `Sequencer`, `ProverEngine`), replaced direct logic with delegation, updated configuration structures, added cancellation token, and rewrote the service run logic to use concurrent tasks and coordinated shutdown. Updated API trait implementations to delegate to new components. |
| crates/node_types/prover/src/prover/tests.rs                                             | Updated tests to match new `Prover` constructor and API: pass a `CancellationToken`, use getter methods instead of direct field access, and adjust configuration initialization.                                                                                                                             |
| crates/node_types/prover/src/prover_engine.rs                                            | Introduced new `ProverEngine` struct to handle proof generation/verification with base and recursive provers, including key management and async methods for proof operations.                                                                                                                               |
| crates/node_types/prover/src/sequencer.rs                                                | Introduced new `Sequencer` struct for transaction batching, epoch finalization, and DAL/database interaction. Handles transaction queue, batch posting, and exposes utility methods for account and commitment access.                                                                                         |
| crates/node_types/prover/src/syncer.rs                                                   | Introduced new `Syncer` struct to coordinate syncing of epochs and transactions from the DAL to the local system, handling both historical and real-time sync, proof verification, and epoch finalization.                                                                                                    |
| crates/tests/src/lib.rs                                                                  | Updated test configuration for `Prover` to use the new modular config structure and pass a `CancellationToken` to initialization.                                                                                                                                                                                                                                                          |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant CLI
    participant Main
    participant Prover
    participant Syncer
    participant Sequencer
    participant ProverEngine
    participant DAL
    participant DB

    CLI->>Main: Parse command and config
    Main->>Prover: Prover::new(db, da, config, cancellation_token)
    Prover->>Syncer: Syncer::new(...)
    Prover->>Sequencer: Sequencer::new(...)
    Prover->>ProverEngine: ProverEngine::new(...)
    Main->>Prover: Start run loop (with cancellation_token)
    Prover->>Syncer: Start sync (cancellation_token)
    Prover->>Sequencer: Start batch poster (cancellation_token)
    Prover->>WebServer: (if enabled) Start webserver
    Syncer->>DAL: Start and listen for new heights
    loop On new DA height
        Syncer->>DAL: Fetch transactions and epochs
        Syncer->>Sequencer: Buffer/process transactions
        Syncer->>ProverEngine: Verify proofs if epoch finalized
        Syncer->>DB: Store epoch data
    end
    Sequencer->>DAL: Post batches if enabled
    Main->>Prover: On SIGINT/SIGTERM, trigger cancellation
    Prover->>Syncer: Cancel sync
    Prover->>Sequencer: Cancel batch poster
    Prover->>WebServer: Cancel webserver
```

## Possibly related PRs

- [deltadevsde/prism#122](https://github.com/deltadevsde/prism/pull/122): Refactors synchronization logic inside the `Sequencer`, which is now a separate component in this modular architecture.
- [deltadevsde/prism#186](https://github.com/deltadevsde/prism/pull/186): Adds verifying key algorithm field and related key handling changes in configuration and CLI; related at configuration and key management level.
  
## Poem

> In the warren of code, we split and we weave,  
> Prover’s old ways, we gently now leave.  
> Syncer, Sequencer, Engine—each with its part,  
> Modular bunnies, each playing their heart.  
> With tokens for cancel, and configs anew,  
> We hop toward shutdown, as only bunnies do!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=deltadevsde/prism&utm_content=314):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR