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