From d24e0dcc67dfd41bd16e78898d0b236d4da933d1 Mon Sep 17 00:00:00 2001 From: Purushotam Date: Fri, 5 Jun 2026 12:49:39 +0545 Subject: [PATCH] fix(checkpoints): refresh txid during status updates --- backend/.dockerignore | 2 + backend/.env.example | 2 +- backend/Cargo.lock | 1 + .../src/services/checkpoint_service.rs | 25 ++++- backend/model/Cargo.toml | 3 + backend/model/src/checkpoint.rs | 98 +++++++++++++++++++ frontend/.dockerignore | 3 + 7 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 backend/.dockerignore create mode 100644 frontend/.dockerignore diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..54088d5 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,2 @@ +target +.DS_Store diff --git a/backend/.env.example b/backend/.env.example index dc422bb..d616ea2 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1 +1 @@ -STRATA_FULLNODE="https://rpc.testnet-staging.stratabtc.org/" +STRATA_FULLNODE="https://strata-staging.testnet-v2.alpenlabs.io/" diff --git a/backend/Cargo.lock b/backend/Cargo.lock index bdfb192..4e546cd 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2119,6 +2119,7 @@ dependencies = [ "hex", "sea-orm", "serde", + "serde_json", "strata-identifiers", ] diff --git a/backend/bin/checkpoint-explorer/src/services/checkpoint_service.rs b/backend/bin/checkpoint-explorer/src/services/checkpoint_service.rs index 10d36a9..c29767d 100644 --- a/backend/bin/checkpoint-explorer/src/services/checkpoint_service.rs +++ b/backend/bin/checkpoint-explorer/src/services/checkpoint_service.rs @@ -185,6 +185,15 @@ async fn update_checkpoints_status( status: RpcCheckpointConfStatus, ) -> anyhow::Result<()> { let checkpoint_db = CheckpointService::new(&database.db); + let chain_status = fetcher.get_chain_status().await?; + // Highest checkpoint index that can have transitioned out of this local status. + // Pending checkpoints can become confirmed/finalized through the confirmed boundary; + // confirmed checkpoints can become finalized through the finalized boundary. + let transition_boundary_idx = match status { + RpcCheckpointConfStatus::Pending => chain_status.confirmed.epoch(), + RpcCheckpointConfStatus::Confirmed => chain_status.finalized.epoch(), + RpcCheckpointConfStatus::Finalized => return Ok(()), + }; let mut idx: u32 = match status { RpcCheckpointConfStatus::Pending => { @@ -208,7 +217,7 @@ async fn update_checkpoints_status( RpcCheckpointConfStatus::Finalized => return Ok(()), }; - loop { + while idx <= transition_boundary_idx { // This is the stopping condition for the loop. If the checkpoint is not found in the database, // break the loop as we have already updated all the checkpoints. let Some(checkpoint_in_db) = checkpoint_db.get_checkpoint_by_idx(idx).await else { @@ -222,10 +231,16 @@ async fn update_checkpoints_status( }; let new_status = checkpoint_from_rpc.status(); + let new_txid = checkpoint_from_rpc.checkpoint_txid(); + let current_txid = checkpoint_in_db + .l1_reference + .as_ref() + .map(|l1_ref| &l1_ref.txid); - // if there is no change in status, return by doing nothing - if checkpoint_in_db.confirmation_status == Some(new_status) { - return Ok(()); + // This checkpoint is already current, but later rows in the bounded range may have changed. + if checkpoint_in_db.confirmation_status == Some(new_status) && current_txid == new_txid { + idx = idx.saturating_add(1); + continue; } info!(idx, %new_status, "Updating checkpoint status"); @@ -241,4 +256,6 @@ async fn update_checkpoints_status( idx = idx.saturating_add(1); } + + Ok(()) } diff --git a/backend/model/Cargo.toml b/backend/model/Cargo.toml index ce3110e..b6d1259 100644 --- a/backend/model/Cargo.toml +++ b/backend/model/Cargo.toml @@ -12,3 +12,6 @@ serde = { workspace = true } hex = { workspace = true } anyhow = { workspace = true } strata-identifiers = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/backend/model/src/checkpoint.rs b/backend/model/src/checkpoint.rs index bf3df95..63693c3 100644 --- a/backend/model/src/checkpoint.rs +++ b/backend/model/src/checkpoint.rs @@ -129,6 +129,15 @@ impl RpcCheckpointInfo { ConfStatus::Finalized { .. } => RpcCheckpointConfStatus::Finalized, } } + + pub fn checkpoint_txid(&self) -> Option<&Txid> { + match &self.confirmation_status { + ConfStatus::Pending => None, + ConfStatus::Confirmed { l1_reference } | ConfStatus::Finalized { l1_reference } => { + Some(&l1_reference.txid) + } + } + } } impl From for ActiveModel { @@ -188,3 +197,92 @@ impl From for RpcCheckpointInfoCheckpointExp { } } } + +#[cfg(test)] +mod tests { + use super::*; + use sea_orm::ActiveValue::Set; + + fn checkpoint_json(status: serde_json::Value) -> serde_json::Value { + serde_json::json!({ + "idx": 7, + "l1_range": [ + {"height": 70, "blkid": "l1-start"}, + {"height": 79, "blkid": "l1-end"} + ], + "l2_range": [ + {"slot": 700, "blkid": "l2-start"}, + {"slot": 799, "blkid": "l2-end"} + ], + "confirmation_status": status + }) + } + + fn confirmed_status(txid: &str) -> serde_json::Value { + serde_json::json!({ + "status": "confirmed", + "l1_reference": { + "l1_block": {"height": 79, "blkid": "l1-end"}, + "txid": txid, + "wtxid": "wtxid" + } + }) + } + + fn finalized_status(txid: &str) -> serde_json::Value { + serde_json::json!({ + "status": "finalized", + "l1_reference": { + "l1_block": {"height": 79, "blkid": "l1-end"}, + "txid": txid, + "wtxid": "wtxid" + } + }) + } + + #[test] + fn checkpoint_txid_is_none_for_pending() { + let checkpoint: RpcCheckpointInfo = + serde_json::from_value(checkpoint_json(serde_json::json!({"status": "pending"}))) + .expect("pending checkpoint should deserialize"); + + assert_eq!(checkpoint.status(), RpcCheckpointConfStatus::Pending); + assert_eq!(checkpoint.checkpoint_txid(), None); + } + + #[test] + fn checkpoint_txid_is_exposed_for_confirmed_and_finalized() { + let confirmed: RpcCheckpointInfo = + serde_json::from_value(checkpoint_json(confirmed_status("confirmed-txid"))) + .expect("confirmed checkpoint should deserialize"); + let finalized: RpcCheckpointInfo = + serde_json::from_value(checkpoint_json(finalized_status("finalized-txid"))) + .expect("finalized checkpoint should deserialize"); + + assert_eq!(confirmed.status(), RpcCheckpointConfStatus::Confirmed); + assert_eq!( + confirmed.checkpoint_txid().map(String::as_str), + Some("confirmed-txid") + ); + assert_eq!(finalized.status(), RpcCheckpointConfStatus::Finalized); + assert_eq!( + finalized.checkpoint_txid().map(String::as_str), + Some("finalized-txid") + ); + } + + #[test] + fn active_model_sets_checkpoint_txid_for_confirmed_checkpoint() { + let checkpoint: RpcCheckpointInfo = + serde_json::from_value(checkpoint_json(confirmed_status("confirmed-txid"))) + .expect("confirmed checkpoint should deserialize"); + + let active_model: ActiveModel = checkpoint.into(); + + assert_eq!(active_model.status, Set(RpcCheckpointConfStatus::Confirmed)); + assert_eq!( + active_model.checkpoint_txid, + Set(Some("confirmed-txid".to_string())) + ); + } +} diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..0ca39c0 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.DS_Store