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