Skip to content

feat: upgrade to comet bft 0.38#1554

Merged
karlem merged 9 commits intomainfrom
upgrade-comet-bft
Mar 25, 2026
Merged

feat: upgrade to comet bft 0.38#1554
karlem merged 9 commits intomainfrom
upgrade-comet-bft

Conversation

@karlem
Copy link
Copy Markdown
Contributor

@karlem karlem commented Mar 23, 2026

Note

High Risk
High risk because it upgrades the Tendermint/CometBFT stack and migrates core ABCI handling from BeginBlock/DeliverTx/EndBlock to FinalizeBlock, impacting block execution, app-hash calculation, and RPC transaction result parsing.

Overview
Upgrades the CometBFT/Tendermint dependency stack to 0.38.x (plus tower-abci 0.16 and prost 0.13), with associated lockfile churn and TLS/websocket crate updates.

Migrates fendermint’s ABCI integration to the v0.38 APIs and ABCI 2.0 flow: replaces per-tx deliver_tx with block-level finalize_block, adds vote-extension hooks, updates server wiring (v038 split/server and listen_tcp), and makes Flush handling non-panicking.

Updates downstream consumers to the new response shapes (tx_result/ExecTxResult), adjusts event attribute indexing helpers, extends genesis consensus params with abci, bumps test/dev CometBFT images to v0.38.x, and improves the actors-custom-car build script to locate Cargo.lock reliably across external CARGO_TARGET_DIR setups.

Written by Cursor Bugbot for commit e94966a. This will update automatically on new commits. Configure here.

@karlem karlem requested a review from a team as a code owner March 23, 2026 18:48
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Duplicate state computation may cause app_hash mismatch
    • finalize_block now stores the exact finalized post-state and app hash for commit, and commit validates executor output against that finalized state before persisting it.
  • ✅ Fixed: Missing failed transaction logging in finalize_block
    • finalize_block now logs deliver_tx failed with code/log/info whenever a transaction result has a non-zero code.

Create PR

Or push these changes by commenting:

@cursor push c598246194
Preview (c598246194)
diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs
--- a/fendermint/app/src/app.rs
+++ b/fendermint/app/src/app.rs
@@ -92,6 +92,11 @@
     state_params: FvmStateParams,
 }
 
+struct FinalizedBlockState {
+    state: SubnetAppState,
+    app_hash: tendermint::hash::AppHash,
+}
+
 impl SubnetAppState {
     pub fn app_hash(&self) -> tendermint::hash::AppHash {
         derive_subnet_app_hash(self)
@@ -165,6 +170,8 @@
     messages_interpreter: Arc<MI>,
 
     light_client_commitments: Arc<tokio::sync::Mutex<Option<LightClientCommitments>>>,
+    /// Post-state computed in `finalize_block`, consumed by `commit`.
+    finalized_block_state: Arc<tokio::sync::Mutex<Option<FinalizedBlockState>>>,
 
     /// Interface to the snapshotter, if enabled.
     snapshots: Option<SnapshotClient>,
@@ -208,6 +215,7 @@
             state_hist_size: config.state_hist_size,
             messages_interpreter: Arc::new(interpreter),
             light_client_commitments: Arc::new(tokio::sync::Mutex::new(None)),
+            finalized_block_state: Arc::new(tokio::sync::Mutex::new(None)),
             snapshots,
             exec_state: Arc::new(tokio::sync::Mutex::new(None)),
             check_state: Arc::new(tokio::sync::Mutex::new(None)),
@@ -420,6 +428,19 @@
         guard.take().expect("exec state empty")
     }
 
+    /// Put the post-state produced during finalization. Has to be empty.
+    async fn put_finalized_block_state(&self, state: FinalizedBlockState) {
+        let mut guard = self.finalized_block_state.lock().await;
+        assert!(guard.is_none(), "finalized block state not empty");
+        *guard = Some(state);
+    }
+
+    /// Take the post-state produced during finalization. Has to be non-empty.
+    async fn take_finalized_block_state(&self) -> FinalizedBlockState {
+        let mut guard = self.finalized_block_state.lock().await;
+        guard.take().expect("finalized block state empty")
+    }
+
     /// Update the execution state using the provided closure.
     ///
     /// Note: Deals with panics in the user provided closure as well.
@@ -880,7 +901,9 @@
         let mut state_params = state.app_state.state_params.clone();
         state_params.timestamp = to_timestamp(request.time);
 
-        let validator = self.get_validator_from_cache(&request.proposer_address).await?;
+        let validator = self
+            .get_validator_from_cache(&request.proposer_address)
+            .await?;
 
         let mut exec_state =
             FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params)
@@ -898,7 +921,10 @@
         let mut tx_results = Vec::with_capacity(request.txs.len());
         for tx in request.txs {
             let msg = tx.to_vec();
-            let result = self.messages_interpreter.apply_message(&mut exec_state, msg).await;
+            let result = self
+                .messages_interpreter
+                .apply_message(&mut exec_state, msg)
+                .await;
             let deliver_tx = match result {
                 Ok(ApplyMessageResponse {
                     applied_message,
@@ -913,6 +939,15 @@
                 Err(ApplyMessageError::Other(e)) => Err(e).context("failed to apply message")?,
             };
 
+            if deliver_tx.code != 0.into() {
+                tracing::info!(
+                    code = ?deliver_tx.code,
+                    log = %deliver_tx.log,
+                    info = %deliver_tx.info,
+                    "deliver_tx failed"
+                );
+            }
+
             tx_results.push(tendermint::abci::types::ExecTxResult {
                 code: deliver_tx.code,
                 data: deliver_tx.data,
@@ -969,6 +1004,11 @@
         projected_state.state_commitments = light_client_commitments;
         let app_hash = projected_state.app_hash();
 
+        self.put_finalized_block_state(FinalizedBlockState {
+            state: projected_state,
+            app_hash: app_hash.clone(),
+        })
+        .await;
         self.put_exec_state(exec_state).await;
 
         Ok(response::FinalizeBlock {
@@ -983,11 +1023,12 @@
     /// Commit the current state at the current height.
     async fn commit(&self) -> AbciResult<response::Commit> {
         let exec_state = self.take_exec_state().await;
+        let finalized = self.take_finalized_block_state().await;
+        let exec_block_height: BlockHeight = exec_state.block_height().try_into()?;
+        let exec_timestamp = exec_state.timestamp();
 
         // Commit the execution state to the datastore.
-        let mut state = self.committed_state()?;
-        state.app_state.block_height = exec_state.block_height().try_into()?;
-        state.app_state.state_params.timestamp = exec_state.timestamp();
+        let state = finalized.state;
 
         let (
             state_root,
@@ -1000,17 +1041,21 @@
             _,
         ) = exec_state.commit().context("failed to commit FVM")?;
 
-        state.app_state.state_params.state_root = state_root;
-        state.app_state.state_params.app_version = app_version;
-        state.app_state.state_params.base_fee = base_fee;
-        state.app_state.state_params.circ_supply = circ_supply;
-        state.app_state.state_params.power_scale = power_scale;
+        if state.app_state.block_height != exec_block_height
+            || state.app_state.state_params.timestamp != exec_timestamp
+            || state.app_state.state_params.state_root != state_root
+            || state.app_state.state_params.app_version != app_version
+            || state.app_state.state_params.base_fee != base_fee
+            || state.app_state.state_params.circ_supply != circ_supply
+            || state.app_state.state_params.power_scale != power_scale
+        {
+            return Err(anyhow!("finalized state does not match committed execution state").into());
+        }
 
         let mut c = self.light_client_commitments.lock().await;
-        // because of the take, no need to *c = None
-        state.state_commitments = c.take();
+        let _ = c.take();
 
-        let app_hash = state.app_hash();
+        let app_hash = finalized.app_hash;
         let block_height = state.app_state.block_height;
 
         // Tell CometBFT how much of the block history it can forget.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we even need this example?

Copy link
Copy Markdown
Contributor

@cryptoAtwill cryptoAtwill left a comment

Choose a reason for hiding this comment

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

Looks like only updating ABCI calls, if e2e tests are working, should be good.

Copy link
Copy Markdown

@sergefdrv sergefdrv left a comment

Choose a reason for hiding this comment

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

Looks good 👍

@karlem karlem merged commit 8fd1750 into main Mar 25, 2026
18 checks passed
@karlem karlem deleted the upgrade-comet-bft branch March 25, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants