Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 6 additions & 7 deletions bin/stress-test/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -653,18 +654,16 @@ pub async fn bench_sync_chain_mmr(
async fn sync_chain_mmr(
api_client: &mut RpcClient<InterceptedService<Channel, OtelInterceptor>>,
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 }
}

Expand Down
36 changes: 0 additions & 36 deletions crates/proto/src/domain/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<proto::rpc::sync_chain_mmr_request::UpperBound> for SyncTarget {
type Error = ConversionError;

fn try_from(
value: proto::rpc::sync_chain_mmr_request::UpperBound,
) -> Result<Self, Self::Error> {
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
// ================================================================================================

Expand Down
15 changes: 1 addition & 14 deletions crates/rpc/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
15 changes: 5 additions & 10 deletions crates/rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
47 changes: 18 additions & 29 deletions crates/store/src/server/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -145,38 +144,28 @@ impl rpc_server::Rpc for StoreApi {
) -> Result<Response<proto::rpc::SyncChainMmrResponse>, 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()),
}))
}

Expand Down
43 changes: 24 additions & 19 deletions crates/store/src/state/sync_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,20 @@ impl State {
pub async fn sync_chain_mmr(
&self,
block_range: RangeInclusive<BlockNumber>,
) -> Result<(MmrDelta, BlockHeader), StateSyncError> {
) -> Result<(BlockHeader, Option<MmrDelta>, Option<MmrProof>), 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))
.await?
.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:
Expand All @@ -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.
Expand Down
42 changes: 14 additions & 28 deletions proto/proto/rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading