diff --git a/CHANGELOG.md b/CHANGELOG.md index 618d093fd..fb0dabe21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - [BREAKING] `BlockRange.block_to` is now required for all RPC endpoints ([#2056](https://github.com/0xMiden/node/pull/2056)). - [BREAKING] Renamed `--url` CLI flags and `*_URL` env vars to `--listen` / `*_LISTEN` across all components. - [BREAKING] Removed `miden-node validator` subcommand and created a separate `miden-validator` binary ([#2053](https://github.com/0xMiden/node/pull/2053)). +- [BREAKING] Simplify `SyncChainMmr` endpoint: the upper end of the block range we're syncing is always the current chain tip on the node. ([#2069](https://github.com/0xMiden/node/pull/2069)). ## v0.14.10 (2026-05-29) diff --git a/bin/stress-test/src/store/mod.rs b/bin/stress-test/src/store/mod.rs index e631bf06e..86e88b407 100644 --- a/bin/stress-test/src/store/mod.rs +++ b/bin/stress-test/src/store/mod.rs @@ -624,10 +624,11 @@ pub async fn bench_sync_chain_mmr( let chain_tip = store_client.clone().status(()).await.unwrap().into_inner().chain_tip; let block_range_size = block_range_size.max(1); + let start_block = chain_tip.saturating_sub(block_range_size); let request = |_| { let mut client = store_client.clone(); - tokio::spawn(async move { sync_chain_mmr(&mut client, chain_tip, block_range_size).await }) + tokio::spawn(async move { sync_chain_mmr(&mut client, start_block).await }) }; let results = stream::iter(0..iterations) @@ -653,18 +654,16 @@ pub async fn bench_sync_chain_mmr( async fn sync_chain_mmr( api_client: &mut RpcClient>, block_from: u32, - block_to: u32, ) -> SyncChainMmrRun { - let sync_request = proto::rpc::SyncChainMmrRequest { - block_from, - upper_bound: Some(proto::rpc::sync_chain_mmr_request::UpperBound::BlockNum(block_to)), - }; + let sync_request = proto::rpc::SyncChainMmrRequest { current_block_height: block_from }; let start = Instant::now(); let response = api_client.sync_chain_mmr(sync_request).await.unwrap(); let elapsed = start.elapsed(); let response = response.into_inner(); - let _mmr_delta = response.mmr_delta.expect("mmr_delta should exist"); + let _mmr_delta = response.latest_committed_mmr_delta.expect("mmr_delta should exist"); + let _mmr_path = response.latest_committed_mmr_path.expect("mmr_path should exist"); + SyncChainMmrRun { duration: elapsed } } diff --git a/crates/proto/src/domain/block.rs b/crates/proto/src/domain/block.rs index e46d549fe..36d37cbb0 100644 --- a/crates/proto/src/domain/block.rs +++ b/crates/proto/src/domain/block.rs @@ -332,42 +332,6 @@ impl From<&FeeParameters> for proto::blockchain::FeeParameters { } } -// SYNC TARGET -// ================================================================================================ - -/// The target block to sync up to in a chain MMR sync request. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SyncTarget { - /// Sync up to a specific block number (inclusive). - BlockNumber(BlockNumber), - /// Sync up to the latest committed block (chain tip). - CommittedChainTip, - /// Sync up to the latest proven block. - ProvenChainTip, -} - -impl TryFrom for SyncTarget { - type Error = ConversionError; - - fn try_from( - value: proto::rpc::sync_chain_mmr_request::UpperBound, - ) -> Result { - use proto::rpc::sync_chain_mmr_request::UpperBound; - - match value { - UpperBound::BlockNum(block_num) => Ok(Self::BlockNumber(block_num.into())), - UpperBound::ChainTip(tip) => match proto::rpc::ChainTip::try_from(tip) { - Ok(proto::rpc::ChainTip::Committed) => Ok(Self::CommittedChainTip), - Ok(proto::rpc::ChainTip::Proven) => Ok(Self::ProvenChainTip), - // These variants should never be encountered. - Ok(proto::rpc::ChainTip::Unspecified) | Err(_) => { - Err(ConversionError::message("unexpected chain tip")) - }, - }, - } - } -} - // BLOCK RANGE // ================================================================================================ diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 303a3868d..012ce6a3e 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -297,20 +297,7 @@ impl api_server::Api for RpcService { let request_ref = request.get_ref(); let span = Span::current(); - span.set_attribute("block_range.from", request_ref.block_from); - match request_ref.upper_bound { - Some(proto::rpc::sync_chain_mmr_request::UpperBound::BlockNum(block_num)) => { - span.set_attribute("block_range.to", block_num); - }, - Some(proto::rpc::sync_chain_mmr_request::UpperBound::ChainTip(chain_tip)) => { - let chain_tip = proto::rpc::ChainTip::try_from(chain_tip) - .unwrap_or(proto::rpc::ChainTip::Unspecified); - span.set_attribute("sync.target", chain_tip.as_str_name()); - }, - None => { - span.set_attribute("sync.target", "CHAIN_TIP_COMMITTED"); - }, - } + span.set_attribute("block_range.from", request_ref.current_block_height); debug!(target: COMPONENT, request = ?request_ref); diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index ba7e40814..897b1ffa8 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -680,22 +680,17 @@ async fn get_limits_endpoint() { } #[tokio::test] -async fn sync_chain_mmr_returns_delta() { +async fn sync_chain_mmr_returns_no_delta_if_already_synced() { let (mut rpc_client, _rpc_addr, store_listener) = start_rpc().await; let (store_runtime, _data_directory, _genesis, _store_addr) = start_store(store_listener).await; - let request = proto::rpc::SyncChainMmrRequest { - block_from: 0, - upper_bound: Some(proto::rpc::sync_chain_mmr_request::UpperBound::ChainTip( - proto::rpc::ChainTip::Committed.into(), - )), - }; + let request = proto::rpc::SyncChainMmrRequest { current_block_height: 0 }; let response = rpc_client.sync_chain_mmr(request).await.expect("sync_chain_mmr should succeed"); let response = response.into_inner(); - let mmr_delta = response.mmr_delta.expect("mmr_delta should exist"); - assert_eq!(mmr_delta.forest, 0); - assert!(mmr_delta.data.is_empty()); + // Chain consists of a genesis block only, there should be no delta. + assert!(response.latest_committed_mmr_delta.is_none()); + assert!(response.latest_committed_mmr_path.is_none()); shutdown_store(store_runtime).await; } diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 116a4e4c1..5f5edcb90 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -5,7 +5,6 @@ use miden_node_proto::decode::{ read_block_range, read_root, }; -use miden_node_proto::domain::block::SyncTarget; use miden_node_proto::generated::store::rpc_server; use miden_node_proto::generated::{self as proto}; use miden_node_utils::limiter::{ @@ -145,38 +144,28 @@ impl rpc_server::Rpc for StoreApi { ) -> Result, Status> { let request = request.into_inner(); - let block_from = BlockNumber::from(request.block_from); - - // Determine upper bound to sync to or default to last committed block. - let sync_target = request - .upper_bound - .map(SyncTarget::try_from) - .transpose() - .map_err(SyncChainMmrError::DeserializationFailed)? - .unwrap_or(SyncTarget::CommittedChainTip); - - let block_to = match sync_target { - SyncTarget::BlockNumber(block_num) => { - block_num.min(self.state.chain_tip(Finality::Committed).await) - }, - SyncTarget::CommittedChainTip => self.state.chain_tip(Finality::Committed).await, - SyncTarget::ProvenChainTip => self.state.chain_tip(Finality::Proven).await, - }; + let client_block_height = BlockNumber::from(request.current_block_height); + let committed_tip = self.state.chain_tip(Finality::Committed).await; + let proven_tip = self.state.chain_tip(Finality::Proven).await; - if block_from > block_to { - Err(SyncChainMmrError::FutureBlock { chain_tip: block_to, block_from })?; + if client_block_height > committed_tip { + Err(SyncChainMmrError::FutureBlock { + chain_tip: committed_tip, + block_from: client_block_height, + })?; } - let block_range = block_from..=block_to; - let (mmr_delta, block_header) = - self.state.sync_chain_mmr(block_range.clone()).await.map_err(internal_error)?; + + let (last_committed_block_header, mmr_delta, mmr_proof) = self + .state + .sync_chain_mmr(client_block_height..=proven_tip) + .await + .map_err(internal_error)?; Ok(Response::new(proto::rpc::SyncChainMmrResponse { - block_range: Some(proto::rpc::BlockRange { - block_from: block_range.start().as_u32(), - block_to: block_range.end().as_u32(), - }), - mmr_delta: Some(mmr_delta.into()), - block_header: Some(block_header.into()), + latest_committed_header: Some(last_committed_block_header.into()), + latest_proven_block_num: Some(proven_tip.into()), + latest_committed_mmr_delta: mmr_delta.map(Into::into), + latest_committed_mmr_path: mmr_proof.map(|p| p.merkle_path().into()), })) } diff --git a/crates/store/src/state/sync_state.rs b/crates/store/src/state/sync_state.rs index c90d095f6..e2ea62681 100644 --- a/crates/store/src/state/sync_state.rs +++ b/crates/store/src/state/sync_state.rs @@ -30,12 +30,12 @@ impl State { pub async fn sync_chain_mmr( &self, block_range: RangeInclusive, - ) -> Result<(MmrDelta, BlockHeader), StateSyncError> { + ) -> Result<(BlockHeader, Option, Option), StateSyncError> { let block_from = *block_range.start(); let block_to = *block_range.end(); - // SAFETY: block_to has been validated to be <= the effective tip (chain tip or latest - // proven block) by the caller, so it must exist in the database. + // SAFETY: block_to has been validated to be <= the chain tip by the caller, + // so it must exist in the database. let block_header = self .db .select_block_header_by_block_num(Some(block_to)) @@ -43,13 +43,7 @@ impl State { .expect("block_to should exist in the database"); if block_from == block_to { - return Ok(( - MmrDelta { - forest: Forest::new(block_from.as_usize()), - data: vec![], - }, - block_header, - )); + return Ok((block_header, None, None)); } // Important notes about the boundary conditions: @@ -64,16 +58,27 @@ impl State { let from_forest = (block_from + 1).as_usize(); let to_forest = block_to.as_usize(); - let mmr_delta = self - .inner - .read() - .await - .blockchain - .as_mmr() - .get_delta(Forest::new(from_forest), Forest::new(to_forest)) - .map_err(StateSyncError::FailedToBuildMmrDelta)?; + let (mmr_delta, mmr_proof) = { + let inner = self.inner.read().await; + + inner.blockchain.num_blocks(); + + let mmr_delta = inner + .blockchain + .as_mmr() + .get_delta(Forest::new(from_forest), Forest::new(to_forest)) + .map_err(StateSyncError::FailedToBuildMmrDelta)?; + + // The MMR at forest N contains proofs for blocks 0..N-1, so we use block_to + 1 to + // include the proof for block_to. + // SAFETY: it is ensured that block_to <= chain_tip, and the blockchain MMR always has + // at least chain_tip + 1 leaves. + let mmr_proof = inner.blockchain.open_at(block_to, block_to + 1)?; + + (mmr_delta, mmr_proof) + }; - Ok((mmr_delta, block_header)) + Ok((block_header, Some(mmr_delta), Some(mmr_proof))) } /// Loads data to synchronize a client's notes. diff --git a/proto/proto/rpc.proto b/proto/proto/rpc.proto index 5b0c1d5b3..5d4efa4d8 100644 --- a/proto/proto/rpc.proto +++ b/proto/proto/rpc.proto @@ -469,41 +469,27 @@ message SyncNotesResponse { // SYNC CHAIN MMR // ================================================================================================ -// The chain tip variant to sync up to. -enum ChainTip { - CHAIN_TIP_UNSPECIFIED = 0; - // Sync up to the latest committed block (chain tip). - CHAIN_TIP_COMMITTED = 1; - // Sync up to the latest proven block. - CHAIN_TIP_PROVEN = 2; -} - // Chain MMR synchronization request. message SyncChainMmrRequest { - // Block number from which to synchronize (inclusive). Set this to the last block + // Block number from which to synchronize. Set this to the last block // already present in the caller's MMR so the delta begins at the next block. - fixed32 block_from = 1; - - // Upper bound for the block range. Determines how far ahead to sync. - oneof upper_bound { - // Sync up to this specific block number (inclusive), clamped to the committed chain tip. - fixed32 block_num = 2; - // Sync up to a chain tip variant (committed or proven). - ChainTip chain_tip = 3; - } - - reserved 4; + fixed32 current_block_height = 1; } // Represents the result of syncing chain MMR. message SyncChainMmrResponse { - // For which block range the MMR delta is returned. - BlockRange block_range = 1; - // Data needed to update the partial MMR from `request.block_range.block_from + 1` to - // `response.block_range.block_to` or the chain tip. - primitives.MmrDelta mmr_delta = 2; - // Block header for `block_range.block_to`. - blockchain.BlockHeader block_header = 3; + // Block header for the committed chain tip. + blockchain.BlockHeader latest_committed_header = 1; + + // Block number of the last proven block. + blockchain.BlockNumber latest_proven_block_num = 2; + + // Data needed to update the partial MMR from `request.current_block_height + 1` to + // the committed chain tip. + primitives.MmrDelta latest_committed_mmr_delta = 3; + + // Merkle path to verify the committed chain tip's inclusion in the MMR. + primitives.MerklePath latest_committed_mmr_path = 4; } // SYNC ACCOUNT STORAGE MAP