Skip to main content

miden_client/sync/
block_header.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::block::{BlockHeader, BlockNumber};
6use miden_protocol::crypto::merkle::MerklePath;
7use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, MmrPeaks, PartialMmr};
8#[cfg(feature = "testing")]
9use miden_protocol::transaction::TransactionKernel;
10use tracing::warn;
11
12use crate::rpc::NodeRpcClient;
13use crate::store::{BlockRelevance, StoreError};
14use crate::{Client, ClientError};
15
16/// Network information management methods.
17impl<AUTH> Client<AUTH> {
18    /// Retrieves a block header by its block number from the store.
19    ///
20    /// Returns `None` if the block header is not found in the store.
21    pub async fn get_block_header_by_num(
22        &self,
23        block_num: BlockNumber,
24    ) -> Result<Option<(BlockHeader, BlockRelevance)>, ClientError> {
25        self.store.get_block_header_by_num(block_num).await.map_err(Into::into)
26    }
27
28    /// Ensures that the genesis block is available. If the genesis commitment is already
29    /// cached in the RPC client, returns early. Otherwise, fetches the genesis block from
30    /// the node, stores it, and sets the commitment in the RPC client.
31    pub async fn ensure_genesis_in_place(&mut self) -> Result<(), ClientError> {
32        if self.rpc_api.has_genesis_commitment().is_some() {
33            return Ok(());
34        }
35
36        let (genesis, _) = self
37            .rpc_api
38            .get_block_header_by_number(Some(BlockNumber::GENESIS), false)
39            .await?;
40
41        let blank_mmr_peaks = MmrPeaks::new(Forest::empty(), vec![])
42            .expect("Blank MmrPeaks should not fail to instantiate");
43        self.store.insert_block_header(&genesis, blank_mmr_peaks, false).await?;
44        self.rpc_api.set_genesis_commitment(genesis.commitment()).await?;
45        Ok(())
46    }
47
48    /// Seeds the local client state needed to create accounts and execute programs without a node.
49    ///
50    /// This stores default RPC limits and inserts a synthetic genesis header if one is not
51    /// already present in the store. The synthetic header is only intended for local-only
52    /// execution and debugging.
53    #[cfg(feature = "testing")]
54    pub async fn prepare_offline_bootstrap(&mut self) -> Result<(), ClientError> {
55        let limits = self.store.get_rpc_limits().await?.unwrap_or_default();
56        self.store.set_rpc_limits(limits).await?;
57        self.rpc_api.set_rpc_limits(limits).await;
58
59        if let Some((genesis, _)) = self.store.get_block_header_by_num(BlockNumber::GENESIS).await?
60        {
61            self.rpc_api.set_genesis_commitment(genesis.commitment()).await?;
62            return Ok(());
63        }
64
65        let genesis = synthetic_offline_genesis_header();
66        let blank_mmr_peaks = MmrPeaks::new(Forest::empty(), vec![])
67            .expect("Blank MmrPeaks should not fail to instantiate");
68        self.store.insert_block_header(&genesis, blank_mmr_peaks, false).await?;
69        self.rpc_api.set_genesis_commitment(genesis.commitment()).await?;
70        Ok(())
71    }
72
73    /// Fetches from the store the current view of the chain's [`PartialMmr`].
74    pub async fn get_current_partial_mmr(&self) -> Result<PartialMmr, ClientError> {
75        self.store.get_current_partial_mmr().await.map_err(Into::into)
76    }
77
78    // HELPERS
79    // --------------------------------------------------------------------------------------------
80
81    /// Retrieves and stores a [`BlockHeader`] by number, and stores its authentication data as
82    /// well.
83    ///
84    /// If the store already contains MMR data for the requested block number, the request isn't
85    /// done and the stored block header is returned.
86    pub(crate) async fn get_and_store_authenticated_block(
87        &self,
88        block_num: BlockNumber,
89        current_partial_mmr: &mut PartialMmr,
90    ) -> Result<BlockHeader, ClientError> {
91        if current_partial_mmr.is_tracked(block_num.as_usize()) {
92            warn!("Current partial MMR already contains the requested data");
93            let (block_header, _) = self
94                .store
95                .get_block_header_by_num(block_num)
96                .await?
97                .expect("Block header should be tracked");
98            return Ok(block_header);
99        }
100
101        // Fetch the block header and MMR proof from the node
102        let (block_header, path_nodes) =
103            fetch_block_header(self.rpc_api.clone(), block_num, current_partial_mmr).await?;
104        let tracked_nodes = authenticated_block_nodes(&block_header, path_nodes);
105
106        // Insert header and MMR nodes
107        self.store
108            .insert_block_header(&block_header, current_partial_mmr.peaks(), true)
109            .await?;
110        self.store.insert_partial_blockchain_nodes(&tracked_nodes).await?;
111
112        Ok(block_header)
113    }
114}
115
116#[cfg(feature = "testing")]
117fn synthetic_offline_genesis_header() -> BlockHeader {
118    BlockHeader::mock(BlockNumber::GENESIS, None, None, &[], TransactionKernel.to_commitment())
119}
120
121// UTILS
122// --------------------------------------------------------------------------------------------
123
124/// Returns a merkle path nodes for a specific block adjusted for a defined forest size.
125/// This function trims the merkle path to include only the nodes that are relevant for
126/// the MMR forest.
127///
128/// # Parameters
129/// - `merkle_path`: Original merkle path.
130/// - `block_num`: The block number for which the path is computed.
131/// - `forest`: The target size of the forest.
132pub(crate) fn adjust_merkle_path_for_forest(
133    merkle_path: &MerklePath,
134    block_num: BlockNumber,
135    forest: Forest,
136) -> Vec<(InOrderIndex, Word)> {
137    let expected_path_len = forest
138        .leaf_to_corresponding_tree(block_num.as_usize())
139        .expect("forest includes block number") as usize;
140
141    let mut idx = InOrderIndex::from_leaf_pos(block_num.as_usize());
142    let mut path_nodes = Vec::with_capacity(expected_path_len);
143
144    for node in merkle_path.nodes().iter().take(expected_path_len) {
145        path_nodes.push((idx.sibling(), *node));
146        idx = idx.parent();
147    }
148
149    path_nodes
150}
151
152fn authenticated_block_nodes(
153    block_header: &BlockHeader,
154    mut path_nodes: Vec<(InOrderIndex, Word)>,
155) -> Vec<(InOrderIndex, Word)> {
156    let mut nodes = Vec::with_capacity(path_nodes.len() + 1);
157    nodes.push((
158        InOrderIndex::from_leaf_pos(block_header.block_num().as_usize()),
159        block_header.commitment(),
160    ));
161    nodes.append(&mut path_nodes);
162    nodes
163}
164
165pub(crate) async fn fetch_block_header(
166    rpc_api: Arc<dyn NodeRpcClient>,
167    block_num: BlockNumber,
168    current_partial_mmr: &mut PartialMmr,
169) -> Result<(BlockHeader, Vec<(InOrderIndex, Word)>), ClientError> {
170    let (block_header, mmr_proof) = rpc_api.get_block_header_with_proof(block_num).await?;
171
172    // Trim merkle path to keep nodes relevant to our current PartialMmr since the node's MMR
173    // might be of a forest arbitrarily higher
174    let path_nodes = adjust_merkle_path_for_forest(
175        mmr_proof.merkle_path(),
176        block_num,
177        current_partial_mmr.forest(),
178    );
179
180    let merkle_path = MerklePath::new(path_nodes.iter().map(|(_, n)| *n).collect());
181
182    current_partial_mmr
183        .track(block_num.as_usize(), block_header.commitment(), &merkle_path)
184        .map_err(StoreError::MmrError)?;
185
186    Ok((block_header, path_nodes))
187}
188
189#[cfg(test)]
190mod tests {
191    use miden_protocol::block::account_tree::AccountTree;
192    use miden_protocol::block::{BlockHeader, BlockNumber};
193    use miden_protocol::crypto::merkle::MerklePath;
194    use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, Mmr, PartialMmr};
195    use miden_protocol::crypto::merkle::smt::Smt;
196    use miden_protocol::transaction::TransactionKernel;
197    use miden_protocol::{Felt, Word};
198
199    #[cfg(feature = "testing")]
200    use super::synthetic_offline_genesis_header;
201    use super::{adjust_merkle_path_for_forest, authenticated_block_nodes};
202
203    fn word(n: u64) -> Word {
204        Word::new([Felt::new(n), Felt::new(0), Felt::new(0), Felt::new(0)])
205    }
206
207    #[test]
208    fn adjust_merkle_path_truncates_to_forest_bounds() {
209        let forest = Forest::new(5);
210        // Forest 5 <=> block 4 is rightmost leaf
211        let block_num = BlockNumber::from(4u32);
212        let path = MerklePath::new(vec![word(1), word(2), word(3)]);
213
214        let adjusted = adjust_merkle_path_for_forest(&path, block_num, forest);
215        // Block 4 conforms a single leaf tree so it should be empty
216        assert!(adjusted.is_empty());
217    }
218
219    #[test]
220    fn adjust_merkle_path_keeps_proof_valid_for_smaller_forest() {
221        // Build a proof in a larger forest and ensure truncation does not keep siblings from a
222        // different tree in the smaller forest, which would invalidate the proof.
223        let mut mmr = Mmr::new();
224        for value in 0u64..8 {
225            mmr.add(word(value));
226        }
227
228        let large_forest = Forest::new(8);
229        let small_forest = Forest::new(5);
230        let leaf_pos = 4usize;
231        let block_num = BlockNumber::from(u32::try_from(leaf_pos).unwrap());
232
233        let proof = mmr.open_at(leaf_pos, large_forest).expect("valid proof");
234        let adjusted_nodes =
235            adjust_merkle_path_for_forest(proof.merkle_path(), block_num, small_forest);
236        let adjusted_path = MerklePath::new(adjusted_nodes.iter().map(|(_, n)| *n).collect());
237
238        let peaks = mmr.peaks_at(small_forest).unwrap();
239        let mut partial = PartialMmr::from_peaks(peaks);
240        let leaf = mmr.get(leaf_pos).expect("leaf exists");
241
242        partial
243            .track(leaf_pos, leaf, &adjusted_path)
244            .expect("adjusted path should verify against smaller forest peaks");
245    }
246
247    #[test]
248    fn adjust_merkle_path_correct_indices() {
249        // Forest 6 has trees of size 2 and 4
250        let forest = Forest::new(6);
251        // Block 1 is on tree with size 4 (merkle path should have 2 nodes)
252        let block_num = BlockNumber::from(1u32);
253        let nodes = vec![word(10), word(11), word(12), word(13)];
254        let path = MerklePath::new(nodes.clone());
255
256        let adjusted = adjust_merkle_path_for_forest(&path, block_num, forest);
257
258        assert_eq!(adjusted.len(), 2);
259        assert_eq!(adjusted[0].1, nodes[0]);
260        assert_eq!(adjusted[1].1, nodes[1]);
261
262        let mut idx = InOrderIndex::from_leaf_pos(1);
263        let expected0 = idx.sibling();
264        idx = idx.parent();
265        let expected1 = idx.sibling();
266
267        assert_eq!(adjusted[0].0, expected0);
268        assert_eq!(adjusted[1].0, expected1);
269    }
270
271    #[test]
272    fn adjust_path_limit_correct_when_siblings_in_bounds() {
273        // Ensure the expected depth limit matters even when the next sibling
274        // is "in-bounds" (but not part of the leaf's subtree for that forest)
275        let large_leaves = 8usize;
276        let small_leaves = 7usize;
277        let leaf_pos = 2usize;
278        let mut mmr = Mmr::new();
279        for value in 0u64..large_leaves as u64 {
280            mmr.add(word(value));
281        }
282
283        let small_forest = Forest::new(small_leaves);
284        let proof = mmr.open_at(leaf_pos, Forest::new(large_leaves)).expect("valid proof");
285        let expected_depth =
286            small_forest.leaf_to_corresponding_tree(leaf_pos).expect("leaf is in forest") as usize;
287
288        // Confirm the next sibling after the expected depth is still in bounds, which would
289        // create an overlong path without the depth cap.
290        let mut idx = InOrderIndex::from_leaf_pos(leaf_pos);
291        for _ in 0..expected_depth {
292            idx = idx.parent();
293        }
294        let next_sibling = idx.sibling();
295        let rightmost = InOrderIndex::from_leaf_pos(small_leaves - 1);
296        assert!(next_sibling <= rightmost);
297        assert!(proof.merkle_path().depth() as usize > expected_depth);
298
299        let adjusted = adjust_merkle_path_for_forest(
300            proof.merkle_path(),
301            BlockNumber::from(u32::try_from(leaf_pos).unwrap()),
302            small_forest,
303        );
304        assert_eq!(adjusted.len(), expected_depth);
305    }
306
307    #[test]
308    fn authenticated_block_nodes_include_leaf_commitment() {
309        let block_header = BlockHeader::mock(4, None, None, &[], TransactionKernel.to_commitment());
310        let path_nodes = vec![
311            (InOrderIndex::from_leaf_pos(4).sibling(), word(10)),
312            (InOrderIndex::from_leaf_pos(4).parent().sibling(), word(11)),
313        ];
314
315        let nodes = authenticated_block_nodes(&block_header, path_nodes.clone());
316
317        assert_eq!(nodes[0], (InOrderIndex::from_leaf_pos(4), block_header.commitment()));
318        assert_eq!(&nodes[1..], path_nodes.as_slice());
319    }
320
321    #[test]
322    #[cfg(feature = "testing")]
323    fn synthetic_offline_genesis_header_uses_mock_genesis() {
324        let genesis = synthetic_offline_genesis_header();
325
326        assert_eq!(genesis.block_num(), BlockNumber::GENESIS);
327        assert_eq!(genesis.account_root(), AccountTree::<Smt>::default().root());
328        assert_eq!(genesis.tx_kernel_commitment(), TransactionKernel.to_commitment());
329    }
330}