diff --git a/CHANGELOG.md b/CHANGELOG.md index d577f88fd..9407e2797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - [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] Removed `miden-node ntx-builder` subcommand and created a separate `miden-ntx-builder` binary ([#2067](https://github.com/0xMiden/node/pull/2067)). +- [BREAKING] Changed `SyncChainMmr` endpoint: the upper end of the block range we're syncing is now the chain tip with the requested finality level. Validator signature is also returned ([#2075](https://github.com/0xMiden/node/pull/2075)). ## v0.14.10 (2026-05-29) diff --git a/bin/stress-test/src/main.rs b/bin/stress-test/src/main.rs index 9e02f4dd5..b8bbed365 100644 --- a/bin/stress-test/src/main.rs +++ b/bin/stress-test/src/main.rs @@ -95,11 +95,7 @@ pub enum Endpoint { block_range: u32, }, #[command(name = "sync-chain-mmr")] - SyncChainMmr { - /// Block range size for each request (number of blocks to query). - #[arg(short, long, value_name = "BLOCK_RANGE", default_value = "1000")] - block_range: u32, - }, + SyncChainMmr, #[command(name = "load-state")] LoadState, #[command(name = "get-account")] @@ -158,8 +154,8 @@ async fn main() { ) .await; }, - Endpoint::SyncChainMmr { block_range } => { - bench_sync_chain_mmr(data_directory, iterations, concurrency, block_range).await; + Endpoint::SyncChainMmr => { + bench_sync_chain_mmr(data_directory, iterations, concurrency).await; }, Endpoint::LoadState => { load_state(&data_directory).await; diff --git a/bin/stress-test/src/store/mod.rs b/bin/stress-test/src/store/mod.rs index e631bf06e..bd6fbaabd 100644 --- a/bin/stress-test/src/store/mod.rs +++ b/bin/stress-test/src/store/mod.rs @@ -612,22 +612,16 @@ async fn sync_transactions_paginated( /// - `iterations`: number of requests to send. /// - `concurrency`: number of requests to send in parallel. /// - `block_range_size`: number of blocks to include per request. -pub async fn bench_sync_chain_mmr( - data_directory: PathBuf, - iterations: usize, - concurrency: usize, - block_range_size: u32, -) { +pub async fn bench_sync_chain_mmr(data_directory: PathBuf, iterations: usize, concurrency: usize) { let (store_client, _) = start_store(data_directory).await; wait_for_store(&store_client).await.unwrap(); let chain_tip = store_client.clone().status(()).await.unwrap().into_inner().chain_tip; - let block_range_size = block_range_size.max(1); 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, chain_tip).await }) }; let results = stream::iter(0..iterations) @@ -652,12 +646,11 @@ pub async fn bench_sync_chain_mmr( /// - the response. async fn sync_chain_mmr( api_client: &mut RpcClient>, - block_from: u32, - block_to: u32, + current_block_height: u32, ) -> SyncChainMmrRun { let sync_request = proto::rpc::SyncChainMmrRequest { - block_from, - upper_bound: Some(proto::rpc::sync_chain_mmr_request::UpperBound::BlockNum(block_to)), + current_block_height, + finality_level: proto::rpc::FinalityLevel::Committed.into(), }; let start = Instant::now(); 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..08474bb68 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -297,20 +297,8 @@ 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("current_block_height", request_ref.current_block_height); + span.set_attribute("finality_level", request_ref.finality_level().as_str_name()); debug!(target: COMPONENT, request = ?request_ref); diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index ba7e40814..01a02faa5 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -685,10 +685,8 @@ async fn sync_chain_mmr_returns_delta() { 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(), - )), + current_block_height: 0, + finality_level: proto::rpc::FinalityLevel::Committed.into(), }; let response = rpc_client.sync_chain_mmr(request).await.expect("sync_chain_mmr should succeed"); let response = response.into_inner(); diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 7653e2537..4833a23a7 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use anyhow::Context; use diesel::{Connection, SqliteConnection}; +use miden_crypto::dsa::ecdsa_k256_keccak::Signature; use miden_node_proto::domain::account::AccountInfo; use miden_node_proto::{BlockProofRequest, generated as proto}; use miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES; @@ -350,6 +351,19 @@ impl Db { .await } + /// Search for a [`BlockHeader`] and its [`Signature`] from the database by its `block_num`. + #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] + pub async fn select_block_header_and_signature_by_block_num( + &self, + block_number: BlockNumber, + ) -> Result> { + self.transact("block headers and signature by block number", move |conn| { + let val = queries::select_block_header_and_signature_by_block_num(conn, block_number)?; + Ok(val) + }) + .await + } + /// Loads multiple block headers from the DB. #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] pub async fn select_block_headers( diff --git a/crates/store/src/db/models/queries/block_headers.rs b/crates/store/src/db/models/queries/block_headers.rs index 1f2348d1a..d5f9896d5 100644 --- a/crates/store/src/db/models/queries/block_headers.rs +++ b/crates/store/src/db/models/queries/block_headers.rs @@ -63,6 +63,38 @@ pub(crate) fn select_block_header_by_block_num( row.map(std::convert::TryInto::try_into).transpose() } +/// Select a [`BlockHeader`] and its [`Signature`] from the DB by its `block_num` using the given +/// [`SqliteConnection`]. +/// +/// # Returns +/// +/// When `block_num` is [None], the latest block header and its signature is returned. Otherwise, +/// the block with the given block height is returned. +/// +/// ```sql +/// -- with argument +/// SELECT block_num, block_header, signature +/// FROM block_headers +/// WHERE block_num = ?1 +/// +/// -- without argument +/// SELECT block_num, block_header, signature +/// FROM block_headers +/// ORDER BY block_num DESC +/// LIMIT 1 +/// ``` +pub(crate) fn select_block_header_and_signature_by_block_num( + conn: &mut SqliteConnection, + block_number: BlockNumber, +) -> Result, DatabaseError> { + let sel = SelectDsl::select(schema::block_headers::table, BlockHeaderRawRow::as_select()); + let row = sel + .filter(schema::block_headers::block_num.eq(block_number.to_raw_sql())) + .get_result::(conn) + .optional()?; + row.map(std::convert::TryInto::try_into).transpose() +} + /// Select block headers for the given block numbers. /// /// # Parameters @@ -175,6 +207,7 @@ pub struct BlockHeaderRawRow { pub signature: Vec, pub commitment: Vec, } + impl TryInto for BlockHeaderRawRow { type Error = DatabaseError; fn try_into(self) -> Result { diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index 30dc17176..5b6669643 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -276,7 +276,7 @@ pub enum SyncChainMmrError { #[error("start block is not known")] FutureBlock { chain_tip: BlockNumber, - block_from: BlockNumber, + current_block_height: BlockNumber, }, #[error("malformed block number")] DeserializationFailed(#[source] ConversionError), diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 116a4e4c1..3e41c7d36 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,29 +144,24 @@ impl rpc_server::Rpc for StoreApi { ) -> Result, Status> { let request = request.into_inner(); - let block_from = BlockNumber::from(request.block_from); + let current_block_height = BlockNumber::from(request.current_block_height); - // 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) + // Determine finality level of the tip to sync to or default to the committed tip. + let sync_target = match request.finality_level() { + proto::rpc::FinalityLevel::Committed | proto::rpc::FinalityLevel::Unspecified => { + 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, + proto::rpc::FinalityLevel::Proven => self.state.chain_tip(Finality::Proven).await, }; - if block_from > block_to { - Err(SyncChainMmrError::FutureBlock { chain_tip: block_to, block_from })?; + if current_block_height > sync_target { + Err(SyncChainMmrError::FutureBlock { + chain_tip: sync_target, + current_block_height, + })?; } - let block_range = block_from..=block_to; - let (mmr_delta, block_header) = + let block_range = current_block_height..=sync_target; + let (mmr_delta, block_header, block_signature) = self.state.sync_chain_mmr(block_range.clone()).await.map_err(internal_error)?; Ok(Response::new(proto::rpc::SyncChainMmrResponse { @@ -177,6 +171,7 @@ impl rpc_server::Rpc for StoreApi { }), mmr_delta: Some(mmr_delta.into()), block_header: Some(block_header.into()), + block_signature: Some(block_signature.into()), })) } diff --git a/crates/store/src/state/sync_state.rs b/crates/store/src/state/sync_state.rs index c0cddc5e4..b978c17fa 100644 --- a/crates/store/src/state/sync_state.rs +++ b/crates/store/src/state/sync_state.rs @@ -1,5 +1,6 @@ use std::ops::RangeInclusive; +use miden_crypto::dsa::ecdsa_k256_keccak::Signature; use miden_protocol::account::AccountId; use miden_protocol::block::{BlockHeader, BlockNumber}; use miden_protocol::crypto::merkle::mmr::{Forest, MmrDelta, MmrProof}; @@ -30,15 +31,15 @@ impl State { pub async fn sync_chain_mmr( &self, block_range: RangeInclusive, - ) -> Result<(MmrDelta, BlockHeader), StateSyncError> { + ) -> Result<(MmrDelta, BlockHeader, Signature), 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. - let block_header = self + let (block_header, signature) = self .db - .select_block_header_by_block_num(Some(block_to)) + .select_block_header_and_signature_by_block_num(block_to) .await? .expect("block_to should exist in the database"); @@ -49,6 +50,7 @@ impl State { data: vec![], }, block_header, + signature, )); } @@ -73,7 +75,7 @@ impl State { .get_delta(Forest::new(from_forest), Forest::new(to_forest)) .map_err(StateSyncError::FailedToBuildMmrDelta)?; - Ok((mmr_delta, block_header)) + Ok((mmr_delta, block_header, signature)) } /// Loads data to synchronize a client's notes. diff --git a/proto/proto/rpc.proto b/proto/proto/rpc.proto index 5b0c1d5b3..4fcec1c58 100644 --- a/proto/proto/rpc.proto +++ b/proto/proto/rpc.proto @@ -469,41 +469,39 @@ message SyncNotesResponse { // SYNC CHAIN MMR // ================================================================================================ -// The chain tip variant to sync up to. -enum ChainTip { - CHAIN_TIP_UNSPECIFIED = 0; +// Finality level we'd like to sync up to. +enum FinalityLevel { + FINALITY_LEVEL_UNSPECIFIED = 0; // Sync up to the latest committed block (chain tip). - CHAIN_TIP_COMMITTED = 1; + FINALITY_LEVEL_COMMITTED = 1; // Sync up to the latest proven block. - CHAIN_TIP_PROVEN = 2; + FINALITY_LEVEL_PROVEN = 2; } // Chain MMR synchronization request. message SyncChainMmrRequest { // Block number from which to synchronize (inclusive). 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; - } + fixed32 current_block_height = 1; - reserved 4; + // Sync target: either the committed or the proven tip. + FinalityLevel finality_level = 2; } // 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. + + // Data needed to update the partial MMR from `request.block_range.current_block_height + 1` to + // the sync target. primitives.MmrDelta mmr_delta = 2; - // Block header for `block_range.block_to`. + + // Block header for the sync target. blockchain.BlockHeader block_header = 3; + + // Validator signature for the sync target. + blockchain.BlockSignature block_signature = 4; } // SYNC ACCOUNT STORAGE MAP