commit_verify/mpc/
tree.rs

1// Client-side-validation foundation libraries.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 LNP/BP Laboratories,
10//                         Institute for Distributed and Cognitive Systems
11// (InDCS), Switzerland. Copyright (C) 2019-2025 Dr Maxim Orlovsky.
12// All rights under the above copyrights are reserved.
13//
14// Licensed under the Apache License, Version 2.0 (the "License"); you may not
15// use this file except in compliance with the License. You may obtain a copy of
16// the License at
17//
18//        http://www.apache.org/licenses/LICENSE-2.0
19//
20// Unless required by applicable law or agreed to in writing, software
21// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
22// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
23// License for the specific language governing permissions and limitations under
24// the License.
25
26use amplify::confinement::{LargeVec, MediumOrdMap};
27use amplify::num::{u256, u5};
28use amplify::Wrapper;
29
30pub use self::commit::Error;
31use crate::merkle::MerkleHash;
32use crate::mpc::atoms::Leaf;
33use crate::mpc::{
34    Commitment, MerkleBlock, MerkleConcealed, MerkleProof, Message, MessageMap, Method, Proof,
35    ProtocolId,
36};
37use crate::{CommitId, Conceal, LIB_NAME_COMMIT_VERIFY};
38
39/// Number of cofactor variants tried before moving to the next tree depth.
40#[allow(dead_code)]
41const COFACTOR_ATTEMPTS: u16 = 500;
42
43type OrderedMap = MediumOrdMap<u32, (ProtocolId, Message)>;
44
45/// Complete information about LNPBP-4 merkle tree.
46#[derive(Clone, PartialEq, Eq, Hash, Debug)]
47#[derive(StrictDumb, StrictType, StrictEncode, StrictDecode)]
48#[strict_type(lib = LIB_NAME_COMMIT_VERIFY)]
49#[derive(CommitEncode)]
50#[commit_encode(crate = crate, strategy = conceal, id = Commitment)]
51pub struct MerkleTree {
52    /// Method used to construct MPC proof (hash function, merklization).
53    pub(super) method: Method,
54
55    /// Tree depth (up to 32).
56    pub(super) depth: u5,
57
58    /// Entropy used for placeholders.
59    pub(super) entropy: u64,
60
61    /// Cofactor is used as an additive to the modulo divisor to improve packing
62    /// of protocols inside a tree of a given depth.
63    pub(super) cofactor: u16,
64
65    /// Map of the messages by their respective protocol ids
66    pub(super) messages: MessageMap,
67
68    pub(super) map: OrderedMap,
69}
70
71impl Proof for MerkleTree {
72    fn matches(&self, other: &Self) -> bool { self.commit_id() == other.commit_id() }
73}
74
75impl MerkleTree {
76    /// Compute the root of the Merkle tree.
77    pub fn root(&self) -> MerkleHash {
78        let iter = (0..self.width_limit()).map(|pos| {
79            self.map
80                .get(&pos)
81                .map(|(protocol, msg)| Leaf::inhabited(*protocol, *msg))
82                .unwrap_or_else(|| Leaf::entropy(self.entropy, pos))
83        });
84        let leaves = LargeVec::try_from_iter(iter).expect("tree width has u32-bound size");
85        debug_assert_eq!(leaves.len_u32(), self.width_limit());
86        MerkleHash::merklize(&leaves)
87    }
88}
89
90impl Conceal for MerkleTree {
91    type Concealed = MerkleConcealed;
92
93    fn conceal(&self) -> Self::Concealed { MerkleBlock::from(self.clone()).conceal() }
94}
95
96mod commit {
97    use std::collections::BTreeMap;
98
99    use amplify::confinement::Confined;
100
101    use super::*;
102    use crate::mpc::MultiSource;
103    use crate::{TryCommitVerify, UntaggedProtocol};
104
105    /// Errors generated during multi-message commitment process by
106    /// [`MerkleTree::try_commit`]
107    #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error, Debug, Display)]
108    #[display(doc_comments)]
109    pub enum Error {
110        /// can't create commitment for an empty message list and zero tree
111        /// depth.
112        Empty,
113
114        /// number of messages ({0}) for LNPBP-4 commitment which exceeds the
115        /// protocol limit of 2^16
116        TooManyMessages(usize),
117
118        /// the provided number of messages ({0}) can't fit LNPBP-4 commitment
119        /// size limits for a given set of protocol ids.
120        CantFitInMaxSlots(usize),
121    }
122
123    /// # Panics
124    ///
125    /// Panics if the crate is compiled without `rand` feature enabled and the
126    /// MultiSource doesn't contain a static entropy.
127    impl TryCommitVerify<MultiSource, UntaggedProtocol> for MerkleTree {
128        type Error = Error;
129
130        fn try_commit(source: &MultiSource) -> Result<Self, Error> {
131            #[cfg(feature = "rand")]
132            use rand::{rng, RngCore};
133
134            let msg_count = source.messages.len();
135
136            if source.min_depth == u5::ZERO && source.messages.is_empty() {
137                return Err(Error::Empty);
138            }
139            if msg_count > 2usize.pow(u5::MAX.to_u8() as u32) {
140                return Err(Error::TooManyMessages(msg_count));
141            }
142
143            #[cfg(feature = "rand")]
144            let entropy = source.static_entropy.unwrap_or_else(|| rng().next_u64());
145            #[cfg(not(feature = "rand"))]
146            let entropy = source.static_entropy.expect(
147                "use must use `rand` feature for crate commit_verify if you do not provide static \
148                 entropy information in `MultiSource`",
149            );
150
151            let mut map = BTreeMap::<u32, (ProtocolId, Message)>::new();
152
153            let mut depth = source.min_depth;
154            let mut prev_width = 1u32;
155            loop {
156                let width_limit = 2u32.pow(depth.to_u8() as u32);
157                if width_limit as usize >= msg_count {
158                    for cofactor in 0..=(prev_width.min(COFACTOR_ATTEMPTS as u32) as u16) {
159                        map.clear();
160                        if source.messages.iter().all(|(protocol, message)| {
161                            let pos = protocol_id_pos(*protocol, cofactor, depth);
162                            map.insert(pos, (*protocol, *message)).is_none()
163                        }) {
164                            return Ok(MerkleTree {
165                                method: source.method,
166                                depth,
167                                entropy,
168                                cofactor,
169                                messages: source.messages.clone(),
170                                map: Confined::try_from(map).expect("MultiSource type guarantees"),
171                            });
172                        }
173                    }
174                }
175
176                prev_width = width_limit;
177                depth = depth
178                    .checked_add(1)
179                    .ok_or(Error::CantFitInMaxSlots(msg_count))?;
180            }
181        }
182    }
183}
184
185pub(super) fn protocol_id_pos(protocol_id: ProtocolId, cofactor: u16, depth: u5) -> u32 {
186    let width = 2u32.pow(depth.to_u8() as u32);
187    debug_assert_ne!(width, 0);
188    let rem = u256::from_le_bytes((*protocol_id).into_inner()) %
189        u256::from(width.saturating_sub(cofactor as u32).max(1) as u64);
190    rem.low_u64() as u32
191}
192
193impl MerkleTree {
194    /// Computes position for a given `protocol_id` within the tree leaves.
195    pub fn protocol_id_pos(&self, protocol_id: ProtocolId) -> u32 {
196        protocol_id_pos(protocol_id, self.cofactor, self.depth)
197    }
198
199    /// Computes the maximum possible width of the MPC Merkle tree, equal to `2
200    /// ^ depth`.
201    pub fn width_limit(&self) -> u32 { 2u32.pow(self.depth.to_u8() as u32) }
202
203    /// Computes the factored width of the MPC Merkle tree, equal to `2 ^ depth
204    /// - cofactor`.
205    pub fn factored_width(&self) -> u32 { self.width_limit() - self.cofactor as u32 }
206
207    /// Get the depth of the MPC Merkle tree.
208    pub fn depth(&self) -> u5 { self.depth }
209
210    /// Get the cofactor of the MPC Merkle tree.
211    pub fn cofactor(&self) -> u16 { self.cofactor }
212
213    /// Get the value of the entropy used in the MPC Merkle tree construct.
214    pub fn entropy(&self) -> u64 { self.entropy }
215
216    /// Convert this MPC Merkle tree into an iterator over items and proofs of
217    /// their inclusion.
218    pub fn into_proofs(self) -> impl Iterator<Item = (ProtocolId, MerkleProof)> {
219        let block = MerkleBlock::from(self);
220        block.into_known_proofs()
221    }
222}
223
224#[cfg(test)]
225pub(crate) mod test_helpers {
226    #![cfg_attr(coverage_nightly, coverage(off))]
227
228    use std::collections::BTreeMap;
229
230    use amplify::confinement::Confined;
231    use amplify::Bytes32;
232    use rand::random;
233
234    use super::*;
235    use crate::mpc::MultiSource;
236    use crate::TryCommitVerify;
237
238    pub fn make_det_messages(no: u32) -> BTreeMap<ProtocolId, Message> {
239        let mut msgs = BTreeMap::new();
240        for _ in 0..no {
241            let protocol_id = u256::from(no);
242            let msg = random::<u8>();
243            msgs.insert(
244                ProtocolId::from(protocol_id.to_le_bytes()),
245                Message::from_inner(Bytes32::with_fill(msg)),
246            );
247        }
248        msgs
249    }
250
251    pub fn make_random_messages(no: u32) -> BTreeMap<ProtocolId, Message> {
252        let mut msgs = BTreeMap::new();
253        for _ in 0..no {
254            let protocol_id = random::<u128>();
255            let protocol_id = u256::from(protocol_id);
256            let msg = random::<u8>();
257            msgs.insert(
258                ProtocolId::from(protocol_id.to_le_bytes()),
259                Message::from_inner(Bytes32::with_fill(msg)),
260            );
261        }
262        msgs
263    }
264
265    pub fn make_random_tree(msgs: &BTreeMap<ProtocolId, Message>) -> MerkleTree {
266        let src = MultiSource {
267            method: Method::Sha256t,
268            min_depth: u5::ZERO,
269            messages: Confined::try_from_iter(msgs.iter().map(|(a, b)| (*a, *b))).unwrap(),
270            static_entropy: None,
271        };
272        MerkleTree::try_commit(&src).unwrap()
273    }
274}
275
276#[cfg(test)]
277mod test {
278    #![cfg_attr(coverage_nightly, coverage(off))]
279
280    use std::collections::BTreeSet;
281
282    use amplify::num::u5;
283    use amplify::Wrapper;
284    use rand::random;
285    use strict_encoding::{StreamWriter, StrictEncode};
286
287    use crate::mpc::tree::test_helpers::{make_random_messages, make_random_tree};
288    use crate::mpc::MerkleBlock;
289    use crate::{CommitId, Conceal};
290
291    #[test]
292    #[should_panic(expected = "Empty")]
293    fn tree_empty() {
294        let msgs = make_random_messages(0);
295        make_random_tree(&msgs);
296    }
297
298    #[test]
299    fn tree_sizing() {
300        for size in 1..16 {
301            let msgs = make_random_messages(size);
302            make_random_tree(&msgs);
303        }
304        for exp in 5..=8 {
305            let size = 2u32.pow(exp);
306
307            let msgs = make_random_messages(size);
308            make_random_tree(&msgs);
309
310            let msgs = make_random_messages(size - 9);
311            make_random_tree(&msgs);
312
313            let msgs = make_random_messages(size + 13);
314            make_random_tree(&msgs);
315        }
316    }
317
318    #[test]
319    #[ignore = "should be executed alone for performance reasons"]
320    fn tree_huge() {
321        // Tree with 8192 protocol-messages: depth 23, cofactor 103. Serialized length
322        // 1081361 bytes. Takes 71589 msecs to generate
323        // Root is 58755c63bbcb1a648982956c90a471a3fc79b12ae97867828e2f0ce8c9f7e7db.
324        // Takes 560735 msecs to compute
325
326        use std::time::Instant;
327
328        let count = 1_048_576 / 128;
329        let msgs = make_random_messages(count);
330
331        let start = Instant::now();
332        let tree = make_random_tree(&msgs);
333        let elapsed_gen = start.elapsed();
334
335        let mut counter = StreamWriter::counter::<{ usize::MAX }>();
336        tree.strict_write(&mut counter).unwrap();
337        eprintln!(
338            "Tree with {count} protocol-messages: depth {}, cofactor {}, width {}.\nSerialized \
339             length {} bytes.\nTakes {} msecs to generate",
340            tree.depth,
341            tree.cofactor,
342            tree.factored_width(),
343            counter.unconfine().count,
344            elapsed_gen.as_millis(),
345        );
346
347        let start = Instant::now();
348        let root = tree.root();
349        let elapsed_root = start.elapsed();
350        eprintln!("Root is {root}. Takes {} msecs to compute", elapsed_root.as_millis(),);
351    }
352
353    #[test]
354    fn tree_structure() {
355        let msgs = make_random_messages(9);
356        let tree = make_random_tree(&msgs);
357        assert!(tree.depth() > u5::with(3));
358        assert!(tree.factored_width() > 9);
359        let mut set = BTreeSet::<u32>::new();
360        for (pid, msg) in msgs {
361            let pos = tree.protocol_id_pos(pid);
362            assert!(set.insert(pos));
363            assert_eq!(tree.messages.get(&pid), Some(&msg));
364        }
365    }
366
367    #[test]
368    fn tree_conceal() {
369        let msgs = make_random_messages(9);
370        let tree = make_random_tree(&msgs);
371        assert_eq!(tree.conceal(), MerkleBlock::from(tree.clone()).conceal());
372    }
373
374    #[test]
375    fn tree_id() {
376        let msgs = make_random_messages(9);
377        let tree = make_random_tree(&msgs);
378        let id = tree.commit_id();
379        let root = tree.root();
380        assert_ne!(id.into_inner(), root.into_inner());
381    }
382
383    #[test]
384    fn tree_id_entropy() {
385        let msgs = make_random_messages(9);
386        let mut tree = make_random_tree(&msgs);
387        let id1 = tree.commit_id();
388
389        tree.entropy = loop {
390            let entropy = random();
391            if entropy != tree.entropy {
392                break entropy;
393            }
394        };
395        let id2 = tree.commit_id();
396
397        assert_ne!(id1, id2);
398    }
399
400    #[test]
401    #[ignore = "should be executed alone for performance reasons"]
402    fn scalability() {
403        let mut depths = vec![];
404        let mut cofacs = vec![];
405        for _ in 0..10 {
406            let msgs = make_random_messages(500);
407            let tree = make_random_tree(&msgs);
408            depths.push(tree.depth.to_u8());
409            cofacs.push(tree.cofactor);
410        }
411        let davg = depths.iter().map(|v| *v as u32).sum::<u32>() as f32 / 10f32;
412        eprintln!("Depth: avg={davg:.2} {depths:?}");
413        eprintln!("Cofactors: {cofacs:?}");
414        assert!(davg <= 15f32);
415    }
416}