Skip to main content

miden_crypto/merkle/mmr/
proof.rs

1/// The representation of a single Merkle path.
2use alloc::vec::Vec;
3
4use super::{super::MerklePath, MmrError, forest::Forest};
5use crate::Word;
6
7// MMR PROOF
8// ================================================================================================
9
10#[derive(Debug, Clone, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12pub struct MmrPath {
13    /// The state of the MMR when the MMR path was created.
14    forest: Forest,
15
16    /// The position of the leaf value within the MMR.
17    position: usize,
18
19    /// The Merkle opening, starting from the value's sibling up to and excluding the root of the
20    /// responsible tree.
21    merkle_path: MerklePath,
22}
23
24impl MmrPath {
25    /// Creates a new `MmrPath` with the given forest, position, and merkle path.
26    pub fn new(forest: Forest, position: usize, merkle_path: MerklePath) -> Self {
27        Self { forest, position, merkle_path }
28    }
29
30    /// Returns the state of the MMR when the MMR path was created.
31    pub fn forest(&self) -> Forest {
32        self.forest
33    }
34
35    /// Returns the position of the leaf value within the MMR.
36    pub fn position(&self) -> usize {
37        self.position
38    }
39
40    /// Returns the Merkle opening, starting from the value's sibling up to and excluding the root
41    /// of the responsible tree.
42    pub fn merkle_path(&self) -> &MerklePath {
43        &self.merkle_path
44    }
45
46    /// Converts the leaf global position into a local position that can be used to verify the
47    /// Merkle path.
48    pub fn relative_pos(&self) -> usize {
49        self.forest
50            .leaf_relative_position(self.position)
51            .expect("position must be part of the forest")
52    }
53
54    /// Returns index of the MMR peak against which the Merkle path in this proof can be verified.
55    pub fn peak_index(&self) -> usize {
56        self.forest.tree_index(self.position)
57    }
58
59    /// Returns a new [MmrPath] adjusted for a smaller target forest.
60    ///
61    /// This is useful when receiving authenticated data from a larger MMR and needing to adjust
62    /// the path for a smaller MMR. The path is trimmed to include only the nodes relevant
63    /// for the target forest.
64    ///
65    /// # Errors
66    /// Returns an error if:
67    /// - The target forest does not include this path's position
68    /// - The target forest is larger than the current forest
69    pub fn with_forest(&self, target_forest: Forest) -> Result<MmrPath, MmrError> {
70        // Validate target forest includes the position
71        if target_forest.num_leaves() <= self.position {
72            return Err(MmrError::PositionNotFound(self.position));
73        }
74
75        // Validate target forest is not larger than current forest
76        if target_forest > self.forest {
77            return Err(MmrError::ForestOutOfBounds(
78                target_forest.num_leaves(),
79                self.forest.num_leaves(),
80            ));
81        }
82
83        // Get expected path length for the target forest
84        let target_path_len = target_forest
85            .leaf_to_corresponding_tree(self.position)
86            .expect("position is in target forest") as usize;
87
88        // Trim the merkle path to the target length
89        let trimmed_nodes: Vec<_> =
90            self.merkle_path.nodes().iter().take(target_path_len).copied().collect();
91        let trimmed_path = MerklePath::new(trimmed_nodes);
92
93        Ok(MmrPath::new(target_forest, self.position, trimmed_path))
94    }
95}
96
97#[derive(Debug, Clone, PartialEq)]
98#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
99pub struct MmrProof {
100    /// The Merkle path data describing how to authenticate the leaf.
101    path: MmrPath,
102
103    /// The leaf value that was opened.
104    leaf: Word,
105}
106
107impl MmrProof {
108    /// Creates a new `MmrProof` with the given path and leaf.
109    pub fn new(path: MmrPath, leaf: Word) -> Self {
110        Self { path, leaf }
111    }
112
113    /// Returns the Merkle path data describing how to authenticate the leaf.
114    pub fn path(&self) -> &MmrPath {
115        &self.path
116    }
117
118    /// Returns the leaf value that was opened.
119    pub fn leaf(&self) -> Word {
120        self.leaf
121    }
122
123    /// Returns the state of the MMR when the proof was created.
124    pub fn forest(&self) -> Forest {
125        self.path.forest()
126    }
127
128    /// Returns the position of the leaf value within the MMR.
129    pub fn position(&self) -> usize {
130        self.path.position()
131    }
132
133    /// Returns the Merkle opening, starting from the value's sibling up to and excluding the root
134    /// of the responsible tree.
135    pub fn merkle_path(&self) -> &MerklePath {
136        self.path.merkle_path()
137    }
138
139    /// Converts the leaf global position into a local position that can be used to verify the
140    /// merkle_path.
141    pub fn relative_pos(&self) -> usize {
142        self.path.relative_pos()
143    }
144
145    /// Returns index of the MMR peak against which the Merkle path in this proof can be verified.
146    pub fn peak_index(&self) -> usize {
147        self.path.peak_index()
148    }
149
150    /// Returns a new [MmrProof] adjusted for a smaller target forest.
151    ///
152    /// This is useful when receiving authenticated data from a larger MMR and needing to adjust
153    /// the proof for a smaller MMR. The path is trimmed to include only the nodes relevant
154    /// for the target forest.
155    ///
156    /// # Errors
157    /// Returns an error if:
158    /// - The target forest does not include this proof's position
159    /// - The target forest is larger than the current forest
160    pub fn with_forest(&self, target_forest: Forest) -> Result<MmrProof, MmrError> {
161        let adjusted_path = self.path.with_forest(target_forest)?;
162        Ok(MmrProof::new(adjusted_path, self.leaf))
163    }
164}
165
166// TESTS
167// ================================================================================================
168
169#[cfg(test)]
170mod tests {
171    use super::{MerklePath, MmrPath, MmrProof};
172    use crate::{
173        Word,
174        merkle::{
175            int_to_node,
176            mmr::{Mmr, forest::Forest},
177        },
178    };
179
180    #[test]
181    fn test_peak_index() {
182        // --- single peak forest ---------------------------------------------
183        let forest = Forest::new(11);
184
185        // the first 4 leaves belong to peak 0
186        for position in 0..8 {
187            let proof = make_dummy_proof(forest, position);
188            assert_eq!(proof.peak_index(), 0);
189        }
190
191        // --- forest with non-consecutive peaks ------------------------------
192        let forest = Forest::new(11);
193
194        // the first 8 leaves belong to peak 0
195        for position in 0..8 {
196            let proof = make_dummy_proof(forest, position);
197            assert_eq!(proof.peak_index(), 0);
198        }
199
200        // the next 2 leaves belong to peak 1
201        for position in 8..10 {
202            let proof = make_dummy_proof(forest, position);
203            assert_eq!(proof.peak_index(), 1);
204        }
205
206        // the last leaf is the peak 2
207        let proof = make_dummy_proof(forest, 10);
208        assert_eq!(proof.peak_index(), 2);
209
210        // --- forest with consecutive peaks ----------------------------------
211        let forest = Forest::new(7);
212
213        // the first 4 leaves belong to peak 0
214        for position in 0..4 {
215            let proof = make_dummy_proof(forest, position);
216            assert_eq!(proof.peak_index(), 0);
217        }
218
219        // the next 2 leaves belong to peak 1
220        for position in 4..6 {
221            let proof = make_dummy_proof(forest, position);
222            assert_eq!(proof.peak_index(), 1);
223        }
224
225        // the last leaf is the peak 2
226        let proof = make_dummy_proof(forest, 6);
227        assert_eq!(proof.peak_index(), 2);
228    }
229
230    fn make_dummy_proof(forest: Forest, position: usize) -> MmrProof {
231        let path = MmrPath::new(forest, position, MerklePath::default());
232        MmrProof::new(path, Word::empty())
233    }
234
235    #[test]
236    fn test_mmr_proof_with_forest() {
237        // Create an MMR with 5 leaves
238        let mut small_mmr = Mmr::new();
239        for i in 0..5 {
240            small_mmr.add(int_to_node(i));
241        }
242        let small_forest = small_mmr.forest();
243
244        // Clone and add 5 more leaves to create larger MMR
245        let mut large_mmr = small_mmr.clone();
246        for i in 5..10 {
247            large_mmr.add(int_to_node(i));
248        }
249
250        // Get proof for position 2 from the larger MMR
251        let large_proof = large_mmr.open(2).unwrap();
252        let small_path_len = small_forest.leaf_to_corresponding_tree(2).unwrap() as u8;
253
254        // Sanity check: larger MMR should have a longer path (otherwise we're not testing trimming)
255        assert!(large_proof.merkle_path().depth() > small_path_len);
256
257        // Adjust proof to smaller forest (should remove 1 node from the path)
258        let adjusted_proof = large_proof.with_forest(small_forest).unwrap();
259        assert_eq!(large_proof.merkle_path().depth() - adjusted_proof.merkle_path().depth(), 1);
260
261        // Verify the adjusted proof is valid in the smaller MMR
262        let peak_idx = adjusted_proof.peak_index();
263        let relative_pos = adjusted_proof.relative_pos();
264        let computed_root = adjusted_proof
265            .merkle_path()
266            .compute_root(relative_pos as u64, adjusted_proof.leaf())
267            .unwrap();
268        assert_eq!(computed_root, small_mmr.peaks().peaks()[peak_idx]);
269    }
270
271    #[test]
272    fn test_mmr_path_with_forest_errors() {
273        // Create a MMR with 7 leaves
274        let mut mmr = Mmr::new();
275        for i in 0..7 {
276            mmr.add(int_to_node(i));
277        }
278        let proof = mmr.open(2).unwrap();
279        let path = proof.path();
280
281        // Error: target forest doesn't include position
282        let small_forest = Forest::new(2);
283        assert!(path.with_forest(small_forest).is_err());
284
285        // Error: target forest is larger than current
286        let large_forest = Forest::new(15);
287        assert!(path.with_forest(large_forest).is_err());
288
289        // Same forest should work
290        assert!(path.with_forest(mmr.forest()).is_ok());
291    }
292}