Skip to main content

miden_client/sync/
block_header.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3
4use miden_protocol::block::{BlockHeader, BlockNumber, ValidatorConfig};
5use miden_protocol::crypto::hash::rpo::Rpo256;
6use miden_protocol::crypto::merkle::MerklePath;
7use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, PartialMmr};
8use miden_protocol::{Felt, Word};
9use tracing::warn;
10
11use crate::rpc::NodeRpcClient;
12use crate::rpc::domain::note::ResolvedSyncNotesBlock;
13use crate::store::{BlockRelevance, StoreError};
14#[cfg(feature = "testing")]
15use crate::test_utils::mock::MockRpcApi;
16use crate::{CachedPartialMmr, Client, ClientError};
17
18/// Network information management methods.
19impl<AUTH> Client<AUTH> {
20    /// Retrieves a block header by its block number from the store.
21    ///
22    /// Returns `None` if the block header is not found in the store.
23    pub async fn get_block_header_by_num(
24        &self,
25        block_num: BlockNumber,
26    ) -> Result<Option<(BlockHeader, BlockRelevance)>, ClientError> {
27        self.store.get_block_header_by_num(block_num).await.map_err(Into::into)
28    }
29
30    /// Retrieves the block header at the current sync height from the store.
31    pub async fn get_latest_block_header(&self) -> Result<BlockHeader, ClientError> {
32        let sync_height = self.store.get_sync_height().await?;
33        let Some((block_header, _)) = self.get_block_header_by_num(sync_height).await? else {
34            return Err(ClientError::DataStoreError(miden_tx::DataStoreError::BlockNotFound(
35                sync_height,
36            )));
37        };
38        Ok(block_header)
39    }
40
41    /// Retrieves the validator configuration committed by the block header at the current sync
42    /// height.
43    pub async fn get_validator_config(&self) -> Result<ValidatorConfig, ClientError> {
44        Ok(self.get_latest_block_header().await?.validator_config().clone())
45    }
46
47    /// Ensures that the genesis block is available. If the genesis commitment is already cached in
48    /// the RPC client, returns early. Otherwise, fetches the genesis block from the node, stores
49    /// it, and sets the commitment in the RPC client.
50    pub async fn ensure_genesis_in_place(&mut self) -> Result<(), ClientError> {
51        if self.rpc_api.has_genesis_commitment().is_some() {
52            return Ok(());
53        }
54
55        let (genesis, _) = self
56            .rpc_api
57            .get_block_header_by_number(Some(BlockNumber::GENESIS), false)
58            .await?;
59
60        // Genesis is untracked since there are no client notes associated with it, so we fetch no
61        // MMR proof and pass no nodes.
62        self.store.insert_block_header(&genesis, &[], false).await?;
63        self.rpc_api.set_genesis_commitment(genesis.commitment()).await?;
64        Ok(())
65    }
66
67    /// Seeds local state for offline account creation and debugging without a real node.
68    ///
69    /// Applies default RPC limits, then either aligns the RPC genesis with an existing stored
70    /// genesis, or replaces the RPC client with [`MockRpcApi`] and runs
71    /// [`Self::ensure_genesis_in_place`] so genesis comes from the mock chain.
72    #[cfg(feature = "testing")]
73    pub async fn prepare_offline_bootstrap(&mut self) -> Result<(), ClientError> {
74        let limits = self.store.get_rpc_limits().await?.unwrap_or_default();
75        self.store.set_rpc_limits(limits).await?;
76        self.rpc_api.set_rpc_limits(limits).await;
77
78        if let Some((genesis, _)) = self.store.get_block_header_by_num(BlockNumber::GENESIS).await?
79        {
80            self.rpc_api.set_genesis_commitment(genesis.commitment()).await?;
81            return Ok(());
82        }
83
84        let rpc = MockRpcApi::default();
85        self.seed_protocol_config(rpc.protocol_config()).await?;
86        *self.test_rpc_api() = Arc::new(rpc);
87        self.ensure_genesis_in_place().await?;
88        Ok(())
89    }
90
91    /// Returns the cached [`PartialMmr`] if in-memory caching is enabled and its fingerprint
92    /// matches the current store state, otherwise rebuilds from the store.
93    pub async fn get_current_partial_mmr(&self) -> Result<PartialMmr, ClientError> {
94        if self.cache_partial_mmr_in_memory
95            && let Some(ref cached) = self.partial_mmr
96            && cached.store_peaks_hash == self.current_store_peaks_hash().await?
97            && cached.tracked_blocks_hash == self.current_tracked_blocks_hash().await?
98        {
99            return Ok(cached.mmr.clone());
100        }
101        self.store.get_current_partial_mmr().await.map_err(Into::into)
102    }
103
104    /// Stores the MMR in the cache if in-memory caching is enabled, capturing the current store
105    /// fingerprint. Must run after any store mutation that may have changed the sync-height peaks
106    /// or the tracked block set.
107    pub(crate) async fn cache_partial_mmr(&mut self, mmr: PartialMmr) -> Result<(), ClientError> {
108        if !self.cache_partial_mmr_in_memory {
109            return Ok(());
110        }
111
112        let store_peaks_hash = self.current_store_peaks_hash().await?;
113        let tracked_blocks_hash = self.current_tracked_blocks_hash().await?;
114        self.partial_mmr = Some(CachedPartialMmr {
115            store_peaks_hash,
116            tracked_blocks_hash,
117            mmr,
118        });
119        Ok(())
120    }
121
122    /// Hashes the store's peaks at the current sync height. Used as the cache freshness
123    /// fingerprint.
124    async fn current_store_peaks_hash(&self) -> Result<Word, ClientError> {
125        Ok(self.store.get_current_blockchain_peaks().await?.hash_peaks())
126    }
127
128    /// Hashes the store's tracked block numbers (sorted). Used as the cache freshness fingerprint
129    /// to detect tracked-set drift without rebuilding the MMR.
130    async fn current_tracked_blocks_hash(&self) -> Result<Word, ClientError> {
131        // BTreeSet iterates in sorted order, so the hash is deterministic.
132        let tracked = self.store.get_tracked_block_header_numbers().await?;
133        let elements: Vec<Felt> = tracked
134            .iter()
135            .map(|&n| Felt::from(u32::try_from(n).expect("block number fits in u32")))
136            .collect();
137        Ok(Rpo256::hash_elements(&elements))
138    }
139
140    /// Tracks each fetched note block in `partial_mmr` and stores its header together with the
141    /// authentication nodes that tracking produced.
142    pub(crate) async fn insert_note_blocks(
143        &mut self,
144        blocks: &[ResolvedSyncNotesBlock],
145        partial_mmr: &mut PartialMmr,
146    ) -> Result<(), ClientError> {
147        let mut authenticated_blocks = Vec::with_capacity(blocks.len());
148        for block in blocks {
149            let block_num = block.block_header.block_num();
150            // Also skips a block the loop itself just tracked, so one returned twice is stored
151            // once.
152            if partial_mmr.is_tracked(block_num.as_usize()) {
153                continue;
154            }
155
156            let path_nodes = track_block_in_mmr(
157                partial_mmr,
158                block_num,
159                block.block_header.commitment(),
160                &block.mmr_path,
161            )?;
162            authenticated_blocks.push((block.block_header.clone(), path_nodes));
163        }
164
165        for (block_header, path_nodes) in authenticated_blocks {
166            let nodes = authenticated_block_nodes(&block_header, path_nodes);
167            self.store.insert_block_header(&block_header, &nodes, true).await?;
168        }
169
170        Ok(())
171    }
172
173    // HELPERS
174    // --------------------------------------------------------------------------------------------
175
176    /// Retrieves and stores a [`BlockHeader`] by number, and stores its authentication data as
177    /// well.
178    ///
179    /// If the store already contains MMR data for the requested block number, the request isn't
180    /// done and the stored block header is returned.
181    pub(crate) async fn get_and_store_authenticated_block(
182        &self,
183        block_num: BlockNumber,
184        current_partial_mmr: &mut PartialMmr,
185    ) -> Result<BlockHeader, ClientError> {
186        if current_partial_mmr.is_tracked(block_num.as_usize()) {
187            warn!("Current partial MMR already contains the requested data");
188            let (block_header, _) = self
189                .store
190                .get_block_header_by_num(block_num)
191                .await?
192                .expect("Block header should be tracked");
193            return Ok(block_header);
194        }
195
196        // Fetch the block header and MMR proof from the node
197        let (block_header, path_nodes) =
198            fetch_block_header(self.rpc_api.clone(), block_num, current_partial_mmr).await?;
199        let tracked_nodes = authenticated_block_nodes(&block_header, path_nodes);
200
201        // The header and its MMR nodes must be inserted together; a header without its nodes cannot
202        // be authenticated later.
203        self.store.insert_block_header(&block_header, &tracked_nodes, true).await?;
204
205        Ok(block_header)
206    }
207}
208
209// UTILS
210// --------------------------------------------------------------------------------------------
211
212/// Returns a merkle path nodes for a specific block adjusted for a defined forest size. This
213/// function trims the merkle path to include only the nodes that are relevant for the MMR forest.
214///
215/// # Parameters
216/// - `merkle_path`: Original merkle path.
217/// - `block_num`: The block number for which the path is computed.
218/// - `forest`: The target size of the forest.
219pub(crate) fn adjust_merkle_path_for_forest(
220    merkle_path: &MerklePath,
221    block_num: BlockNumber,
222    forest: Forest,
223) -> Vec<(InOrderIndex, Word)> {
224    let expected_path_len = forest
225        .leaf_to_corresponding_tree(block_num.as_usize())
226        .expect("forest includes block number") as usize;
227
228    let mut idx = InOrderIndex::from_leaf_pos(block_num.as_usize());
229    let mut path_nodes = Vec::with_capacity(expected_path_len);
230
231    for node in merkle_path.nodes().iter().take(expected_path_len) {
232        path_nodes.push((idx.sibling(), *node));
233        idx = idx.parent();
234    }
235
236    path_nodes
237}
238
239/// Adjusts a Merkle path for the given forest, then calls [`PartialMmr::track`] to verify and
240/// register the block. Returns the forest-adjusted authentication path nodes for the tracked block.
241pub(crate) fn track_block_in_mmr(
242    partial_mmr: &mut PartialMmr,
243    block_num: BlockNumber,
244    block_commitment: Word,
245    mmr_path: &MerklePath,
246) -> Result<Vec<(InOrderIndex, Word)>, ClientError> {
247    let path_nodes = adjust_merkle_path_for_forest(mmr_path, block_num, partial_mmr.forest());
248    let adjusted_path = MerklePath::new(path_nodes.iter().map(|(_, n)| *n).collect());
249
250    partial_mmr
251        .track(block_num.as_usize(), block_commitment, &adjusted_path)
252        .map_err(StoreError::MmrError)?;
253
254    Ok(path_nodes)
255}
256
257fn authenticated_block_nodes(
258    block_header: &BlockHeader,
259    mut path_nodes: Vec<(InOrderIndex, Word)>,
260) -> Vec<(InOrderIndex, Word)> {
261    let mut nodes = Vec::with_capacity(path_nodes.len() + 1);
262    nodes.push((
263        InOrderIndex::from_leaf_pos(block_header.block_num().as_usize()),
264        block_header.commitment(),
265    ));
266    nodes.append(&mut path_nodes);
267    nodes
268}
269
270pub(crate) async fn fetch_block_header(
271    rpc_api: Arc<dyn NodeRpcClient>,
272    block_num: BlockNumber,
273    current_partial_mmr: &mut PartialMmr,
274) -> Result<(BlockHeader, Vec<(InOrderIndex, Word)>), ClientError> {
275    let (block_header, mmr_proof) = rpc_api.get_block_header_with_proof(block_num).await?;
276
277    let path_nodes = track_block_in_mmr(
278        current_partial_mmr,
279        block_num,
280        block_header.commitment(),
281        mmr_proof.merkle_path(),
282    )?;
283
284    Ok((block_header, path_nodes))
285}
286
287#[cfg(test)]
288mod tests {
289    use miden_protocol::block::{BlockHeader, BlockNumber};
290    use miden_protocol::crypto::merkle::MerklePath;
291    use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, Mmr, PartialMmr};
292    use miden_protocol::{Felt, Word};
293
294    use super::{adjust_merkle_path_for_forest, authenticated_block_nodes};
295
296    fn word(n: u64) -> Word {
297        Word::new([
298            Felt::new(n).expect("test value should fit into the base field"),
299            Felt::new(0).expect("zero is a valid field element"),
300            Felt::new(0).expect("zero is a valid field element"),
301            Felt::new(0).expect("zero is a valid field element"),
302        ])
303    }
304
305    #[test]
306    fn adjust_merkle_path_truncates_to_forest_bounds() {
307        let forest = Forest::new(5).expect("valid forest");
308        // Forest 5 <=> block 4 is rightmost leaf
309        let block_num = BlockNumber::from(4u32);
310        let path = MerklePath::new(vec![word(1), word(2), word(3)]);
311
312        let adjusted = adjust_merkle_path_for_forest(&path, block_num, forest);
313        // Block 4 conforms a single leaf tree so it should be empty
314        assert!(adjusted.is_empty());
315    }
316
317    #[test]
318    #[should_panic(expected = "forest includes block number")]
319    fn adjust_merkle_path_panics_for_block_beyond_forest() {
320        // Forest 5 covers leaves 0..=4, so a claimed commit height of 5 has no corresponding tree
321        // and the depth lookup has nothing to return. Notes whose inclusion proof claims a height
322        // past the synced view must be dropped before reaching this point.
323        let forest = Forest::new(5).expect("valid forest");
324        let block_num = BlockNumber::from(5u32);
325        let path = MerklePath::new(vec![word(1), word(2), word(3)]);
326
327        adjust_merkle_path_for_forest(&path, block_num, forest);
328    }
329
330    #[test]
331    fn adjust_merkle_path_keeps_proof_valid_for_smaller_forest() {
332        // Build a proof in a larger forest and ensure truncation does not keep siblings from a
333        // different tree in the smaller forest, which would invalidate the proof.
334        let mut mmr = Mmr::new();
335        for value in 0u64..8 {
336            mmr.add(word(value)).expect("test MMR append should succeed");
337        }
338
339        let large_forest = Forest::new(8).expect("valid forest");
340        let small_forest = Forest::new(5).expect("valid forest");
341        let leaf_pos = 4usize;
342        let block_num = BlockNumber::from(u32::try_from(leaf_pos).unwrap());
343
344        let proof = mmr.open_at(leaf_pos, large_forest).expect("valid proof");
345        let adjusted_nodes =
346            adjust_merkle_path_for_forest(proof.merkle_path(), block_num, small_forest);
347        let adjusted_path = MerklePath::new(adjusted_nodes.iter().map(|(_, n)| *n).collect());
348
349        let peaks = mmr.peaks_at(small_forest).unwrap();
350        let mut partial = PartialMmr::from_peaks(peaks);
351        let leaf = mmr.get(leaf_pos).expect("leaf exists");
352
353        partial
354            .track(leaf_pos, leaf, &adjusted_path)
355            .expect("adjusted path should verify against smaller forest peaks");
356    }
357
358    #[test]
359    fn adjust_merkle_path_correct_indices() {
360        // Forest 6 has trees of size 2 and 4
361        let forest = Forest::new(6).expect("valid forest");
362        // Block 1 is on tree with size 4 (merkle path should have 2 nodes)
363        let block_num = BlockNumber::from(1u32);
364        let nodes = vec![word(10), word(11), word(12), word(13)];
365        let path = MerklePath::new(nodes.clone());
366
367        let adjusted = adjust_merkle_path_for_forest(&path, block_num, forest);
368
369        assert_eq!(adjusted.len(), 2);
370        assert_eq!(adjusted[0].1, nodes[0]);
371        assert_eq!(adjusted[1].1, nodes[1]);
372
373        let mut idx = InOrderIndex::from_leaf_pos(1);
374        let expected0 = idx.sibling();
375        idx = idx.parent();
376        let expected1 = idx.sibling();
377
378        assert_eq!(adjusted[0].0, expected0);
379        assert_eq!(adjusted[1].0, expected1);
380    }
381
382    #[test]
383    fn adjust_path_limit_correct_when_siblings_in_bounds() {
384        // Ensure the expected depth limit matters even when the next sibling is "in-bounds" (but
385        // not part of the leaf's subtree for that forest)
386        let large_leaves = 8usize;
387        let small_leaves = 7usize;
388        let leaf_pos = 2usize;
389        let mut mmr = Mmr::new();
390        for value in 0u64..large_leaves as u64 {
391            mmr.add(word(value)).expect("test MMR append should succeed");
392        }
393
394        let small_forest = Forest::new(small_leaves).expect("valid forest");
395        let proof = mmr
396            .open_at(leaf_pos, Forest::new(large_leaves).expect("valid forest"))
397            .expect("valid proof");
398        let expected_depth =
399            small_forest.leaf_to_corresponding_tree(leaf_pos).expect("leaf is in forest") as usize;
400
401        // Confirm the next sibling after the expected depth is still in bounds, which would create
402        // an overlong path without the depth cap.
403        let mut idx = InOrderIndex::from_leaf_pos(leaf_pos);
404        for _ in 0..expected_depth {
405            idx = idx.parent();
406        }
407        let next_sibling = idx.sibling();
408        let rightmost = InOrderIndex::from_leaf_pos(small_leaves - 1);
409        assert!(next_sibling <= rightmost);
410        assert!(proof.merkle_path().depth() as usize > expected_depth);
411
412        let adjusted = adjust_merkle_path_for_forest(
413            proof.merkle_path(),
414            BlockNumber::from(u32::try_from(leaf_pos).unwrap()),
415            small_forest,
416        );
417        assert_eq!(adjusted.len(), expected_depth);
418    }
419
420    #[test]
421    fn authenticated_block_nodes_include_leaf_commitment() {
422        let block_header = BlockHeader::mock(4, None, None, &[]);
423        let path_nodes = vec![
424            (InOrderIndex::from_leaf_pos(4).sibling(), word(10)),
425            (InOrderIndex::from_leaf_pos(4).parent().sibling(), word(11)),
426        ];
427
428        let nodes = authenticated_block_nodes(&block_header, path_nodes.clone());
429
430        assert_eq!(nodes[0], (InOrderIndex::from_leaf_pos(4), block_header.commitment()));
431        assert_eq!(&nodes[1..], path_nodes.as_slice());
432    }
433}