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