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