From b98fb968c6613a4d044920281721a618ee12f19a Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Tue, 12 May 2026 14:02:05 +0200 Subject: [PATCH 1/2] feat(rpc): simplify SyncChainMmr endpoint This endpoint is now always syncing up to the committed tip of the chain. Some extra data is also returned, like the latest proven block number and the MMR path of the committed tip. --- bin/stress-test/src/store/mod.rs | 13 ++++---- crates/proto/src/domain/block.rs | 36 --------------------- crates/rpc/src/server/api.rs | 15 +-------- crates/rpc/src/tests.rs | 15 +++------ crates/store/src/server/rpc_api.rs | 47 +++++++++++----------------- crates/store/src/state/sync_state.rs | 43 ++++++++++++++----------- proto/proto/rpc.proto | 42 +++++++++---------------- 7 files changed, 68 insertions(+), 143 deletions(-) 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 From 0b1d30b6769c678b78c1b4d8d18f8024e7bd6c43 Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Tue, 12 May 2026 16:32:24 +0200 Subject: [PATCH 2/2] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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)