celestia_types/blob/
commitment.rs

1use std::io::Cursor;
2use std::num::NonZeroU64;
3
4use base64::prelude::*;
5use bytes::{Buf, BufMut, BytesMut};
6use celestia_proto::serializers::cow_str::CowStr;
7use nmt_rs::NamespaceMerkleHasher;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use tendermint::crypto::sha256::HASH_SIZE;
10use tendermint::{crypto, merkle};
11#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))]
12use wasm_bindgen::prelude::*;
13
14use crate::consts::appconsts;
15use crate::nmt::{Namespace, NamespacedHashExt, NamespacedSha2Hasher, Nmt, RawNamespacedHash};
16use crate::state::{AccAddress, AddressTrait};
17use crate::{AppVersion, Error, Result};
18use crate::{InfoByte, Share};
19
20/// A merkle hash used to identify the [`Blob`]s data.
21///
22/// In Celestia network, the transaction which pays for the blob's inclusion
23/// is separated from the data itself. The reason for that is to allow verifying
24/// the blockchain's state without the need to pull the actual data which got stored.
25/// To achieve that, the [`MsgPayForBlobs`] transaction only includes the [`Commitment`]s
26/// of the blobs it is paying for, not the data itself.
27///
28/// The algorithm of computing the [`Commitment`] of the [`Blob`]'s [`Share`]s is
29/// designed in a way to allow easy and cheap proving of the [`Share`]s inclusion in the
30/// block. It is computed as a [`merkle hash`] of all the [`Nmt`] subtree roots created from
31/// the blob shares included in the [`ExtendedDataSquare`] rows. Assuming the `s1` and `s2`
32/// are the only shares of some blob posted to the celestia, they'll result in a single subtree
33/// root as shown below:
34///
35/// ```text
36/// NMT:           row root
37///                /     \
38///              o   subtree root
39///             / \      / \
40///           _________________
41/// EDS row: | s | s | s1 | s2 |
42/// ```
43///
44/// Using subtree roots as a base for [`Commitment`] computation allows for much smaller
45/// inclusion proofs than when the [`Share`]s would be used directly, but it imposes some
46/// constraints on how the [`Blob`]s can be placed in the [`ExtendedDataSquare`]. You can
47/// read more about that in the [`share commitment rules`].
48///
49/// [`Blob`]: crate::Blob
50/// [`Share`]: crate::share::Share
51/// [`MsgPayForBlobs`]: celestia_proto::celestia::blob::v1::MsgPayForBlobs
52/// [`merkle hash`]: tendermint::merkle::simple_hash_from_byte_vectors
53/// [`Nmt`]: crate::nmt::Nmt
54/// [`ExtendedDataSquare`]: crate::ExtendedDataSquare
55/// [`share commitment rules`]: https://github.com/celestiaorg/celestia-app/blob/main/specs/src/specs/data_square_layout.md#blob-share-commitment-rules
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
57#[cfg_attr(
58    all(feature = "wasm-bindgen", target_arch = "wasm32"),
59    wasm_bindgen(inspectable)
60)]
61pub struct Commitment {
62    /// Hash of the commitment
63    hash: merkle::Hash,
64}
65
66impl Commitment {
67    /// Create a new commitment with hash
68    pub fn new(hash: merkle::Hash) -> Self {
69        Commitment { hash }
70    }
71
72    /// Generate the share commitment from the given blob data.
73    pub fn from_blob(
74        namespace: Namespace,
75        blob_data: &[u8],
76        share_version: u8,
77        signer: Option<&AccAddress>,
78        app_version: AppVersion,
79    ) -> Result<Commitment> {
80        validate_blob(share_version, signer.is_some(), Some(app_version))?;
81        let shares = split_blob_to_shares(namespace, share_version, blob_data, signer)?;
82        Self::from_shares(namespace, &shares, app_version)
83    }
84
85    /// Generate the commitment from the given shares.
86    pub fn from_shares(
87        namespace: Namespace,
88        mut shares: &[Share],
89        app_version: AppVersion,
90    ) -> Result<Commitment> {
91        // the commitment is the root of a merkle mountain range with max tree size
92        // determined by the number of roots required to create a share commitment
93        // over that blob. The size of the tree is only increased if the number of
94        // subtree roots surpasses a constant threshold.
95        let subtree_root_threshold = appconsts::subtree_root_threshold(app_version);
96        let subtree_width = subtree_width(shares.len() as u64, subtree_root_threshold);
97        let tree_sizes = merkle_mountain_range_sizes(shares.len() as u64, subtree_width);
98
99        let mut leaf_sets: Vec<&[_]> = Vec::with_capacity(tree_sizes.len());
100
101        for size in tree_sizes {
102            let (leafs, rest) = shares.split_at(size as usize);
103            leaf_sets.push(leafs);
104            shares = rest;
105        }
106
107        // create the commitments by pushing each leaf set onto an nmt
108        let mut subtree_roots: Vec<RawNamespacedHash> = Vec::with_capacity(leaf_sets.len());
109        for leaf_set in leaf_sets {
110            // create the nmt
111            let mut tree = Nmt::with_hasher(NamespacedSha2Hasher::with_ignore_max_ns(true));
112            for leaf_share in leaf_set {
113                tree.push_leaf(leaf_share.as_ref(), namespace.into())
114                    .map_err(Error::Nmt)?;
115            }
116            // add the root
117            subtree_roots.push(tree.root().to_array());
118        }
119
120        let hash = merkle::simple_hash_from_byte_vectors::<crypto::default::Sha256>(&subtree_roots);
121
122        Ok(Commitment { hash })
123    }
124
125    /// Hash of the commitment
126    pub fn hash(&self) -> &merkle::Hash {
127        &self.hash
128    }
129}
130
131#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))]
132#[wasm_bindgen]
133impl Commitment {
134    /// Hash of the commitment
135    #[wasm_bindgen(js_name = hash)]
136    pub fn js_hash(&self) -> Vec<u8> {
137        self.hash.to_vec()
138    }
139}
140
141impl From<Commitment> for merkle::Hash {
142    fn from(commitment: Commitment) -> Self {
143        commitment.hash
144    }
145}
146
147impl Serialize for Commitment {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: Serializer,
151    {
152        let s = BASE64_STANDARD.encode(self.hash);
153        serializer.serialize_str(&s)
154    }
155}
156
157impl<'de> Deserialize<'de> for Commitment {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: Deserializer<'de>,
161    {
162        // base64 needs more buffer size than the final output
163        let mut buf = [0u8; HASH_SIZE * 2];
164
165        let s = CowStr::deserialize(deserializer)?;
166
167        let len = BASE64_STANDARD
168            .decode_slice(s, &mut buf)
169            .map_err(|e| serde::de::Error::custom(e.to_string()))?;
170
171        let hash: merkle::Hash = buf[..len]
172            .try_into()
173            .map_err(|_| serde::de::Error::custom("commitment is not a size of a sha256"))?;
174
175        Ok(Commitment { hash })
176    }
177}
178
179/// Check if the combination of share_verison and signer (and app version, in case it's known in
180/// this context) is valid, and return appropriate error otherwise
181pub(crate) fn validate_blob(
182    share_version: u8,
183    has_signer: bool,
184    app_version: Option<AppVersion>,
185) -> Result<()> {
186    if ![appconsts::SHARE_VERSION_ZERO, appconsts::SHARE_VERSION_ONE].contains(&share_version) {
187        return Err(Error::UnsupportedShareVersion(share_version));
188    }
189    if share_version == appconsts::SHARE_VERSION_ZERO && has_signer {
190        return Err(Error::SignerNotSupported);
191    }
192    if share_version == appconsts::SHARE_VERSION_ONE && !has_signer {
193        return Err(Error::MissingSigner);
194    }
195    if app_version
196        .is_some_and(|app| share_version == appconsts::SHARE_VERSION_ONE && app < AppVersion::V3)
197    {
198        return Err(Error::UnsupportedShareVersion(share_version));
199    }
200    Ok(())
201}
202
203/// Splits blob's data to the sequence of shares
204pub(crate) fn split_blob_to_shares(
205    namespace: Namespace,
206    share_version: u8,
207    blob_data: &[u8],
208    signer: Option<&AccAddress>,
209) -> Result<Vec<Share>> {
210    let mut shares = Vec::new();
211    let mut cursor = Cursor::new(blob_data);
212
213    while cursor.has_remaining() {
214        let share = build_sparse_share(namespace, share_version, signer, &mut cursor)?;
215        shares.push(share);
216    }
217    Ok(shares)
218}
219
220/// Build a sparse share from a cursor over data
221fn build_sparse_share(
222    namespace: Namespace,
223    share_version: u8,
224    signer: Option<&AccAddress>,
225    data: &mut Cursor<impl AsRef<[u8]>>,
226) -> Result<Share> {
227    let is_first_share = data.position() == 0;
228    let data_len = cursor_inner_length(data);
229    let mut bytes = BytesMut::with_capacity(appconsts::SHARE_SIZE);
230
231    // Write the namespace
232    bytes.put_slice(namespace.as_bytes());
233    // Write the info byte
234    let info_byte = InfoByte::new(share_version, is_first_share)?;
235    bytes.put_u8(info_byte.as_u8());
236
237    // If this share is first in the sequence, write the bytes len of the sequence
238    if is_first_share {
239        let data_len = data_len
240            .try_into()
241            .map_err(|_| Error::ShareSequenceLenExceeded(data_len))?;
242        bytes.put_u32(data_len);
243        // additionally, if share_version is 1, put the signer after sequence len
244        if share_version == appconsts::SHARE_VERSION_ONE {
245            let signer = signer.as_ref().ok_or(Error::MissingSigner)?;
246            bytes.put_slice(signer.as_bytes());
247        }
248    }
249
250    // Calculate amount of bytes to read
251    let current_size = bytes.len();
252    let available_space = appconsts::SHARE_SIZE - current_size;
253    let read_amount = available_space.min(data.remaining());
254
255    // Resize to share size with 0 padding
256    bytes.resize(appconsts::SHARE_SIZE, 0);
257    // Read the share data
258    data.copy_to_slice(&mut bytes[current_size..current_size + read_amount]);
259
260    Share::from_raw(&bytes)
261}
262
263fn cursor_inner_length(cursor: &Cursor<impl AsRef<[u8]>>) -> usize {
264    cursor.get_ref().as_ref().len()
265}
266
267/// merkle_mountain_range_sizes returns the sizes (number of leaf nodes) of the
268/// trees in a merkle mountain range constructed for a given total_size and
269/// max_tree_size.
270///
271/// https://docs.grin.mw/wiki/chain-state/merkle-mountain-range/
272/// https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md
273fn merkle_mountain_range_sizes(mut total_size: u64, max_tree_size: u64) -> Vec<u64> {
274    let mut tree_sizes = Vec::new();
275
276    while total_size != 0 {
277        if total_size >= max_tree_size {
278            tree_sizes.push(max_tree_size);
279            total_size -= max_tree_size;
280        } else {
281            let tree_size = round_down_to_power_of_2(
282                // unwrap is safe as total_size can't be zero there
283                total_size.try_into().unwrap(),
284            )
285            .expect("Failed to find next power of 2");
286            tree_sizes.push(tree_size);
287            total_size -= tree_size;
288        }
289    }
290
291    tree_sizes
292}
293
294/// blob_min_square_size returns the minimum square size that can contain share_count
295/// number of shares.
296fn blob_min_square_size(share_count: u64) -> u64 {
297    round_up_to_power_of_2((share_count as f64).sqrt().ceil() as u64)
298        .expect("Failed to find minimum blob square size")
299}
300
301/// subtree_width determines the maximum number of leaves per subtree in the share
302/// commitment over a given blob. The input should be the total number of shares
303/// used by that blob. The reasoning behind this algorithm is discussed in depth
304/// in ADR013
305/// (celestia-app/docs/architecture/adr-013-non-interative-default-rules-for-zero-padding).
306fn subtree_width(share_count: u64, subtree_root_threshold: u64) -> u64 {
307    // per ADR013, we use a predetermined threshold to determine width of sub
308    // trees used to create share commitments
309    let mut s = share_count / subtree_root_threshold;
310
311    // round up if the width is not an exact multiple of the threshold
312    if share_count % subtree_root_threshold != 0 {
313        s += 1;
314    }
315
316    // use a power of two equal to or larger than the multiple of the subtree
317    // root threshold
318    s = round_up_to_power_of_2(s).expect("Failed to find next power of 2");
319
320    // use the minimum of the subtree width and the min square size, this
321    // gurarantees that a valid value is returned
322    // return min(s, BlobMinSquareSize(shareCount))
323    s.min(blob_min_square_size(share_count))
324}
325
326/// round_up_to_power_of_2 returns the next power of two that is strictly greater than input.
327fn round_up_to_power_of_2(x: u64) -> Option<u64> {
328    let mut po2 = 1;
329
330    loop {
331        if po2 >= x {
332            return Some(po2);
333        }
334        if let Some(next_po2) = po2.checked_shl(1) {
335            po2 = next_po2;
336        } else {
337            return None;
338        }
339    }
340}
341
342/// round_down_to_power_of_2 returns the next power of two less than or equal to input.
343fn round_down_to_power_of_2(x: NonZeroU64) -> Option<u64> {
344    let x: u64 = x.into();
345
346    match round_up_to_power_of_2(x) {
347        Some(po2) if po2 == x => Some(x),
348        Some(po2) => Some(po2 / 2),
349        _ => None,
350    }
351}
352
353#[cfg(feature = "uniffi")]
354mod uniffi_types {
355    use super::{Commitment as RustCommitment, HASH_SIZE};
356    use uniffi::Record;
357
358    use crate::error::UniffiConversionError;
359
360    #[derive(Record)]
361    pub struct Commitment {
362        pub sha_hash: Vec<u8>,
363    }
364
365    impl From<RustCommitment> for Commitment {
366        fn from(value: RustCommitment) -> Self {
367            Commitment {
368                sha_hash: value.hash.to_vec(),
369            }
370        }
371    }
372
373    impl TryFrom<Commitment> for RustCommitment {
374        type Error = UniffiConversionError;
375
376        fn try_from(value: Commitment) -> Result<Self, Self::Error> {
377            let hash: [u8; HASH_SIZE] = value
378                .sha_hash
379                .try_into()
380                .map_err(|_| UniffiConversionError::InvalidCommitmentLength)?;
381            Ok(RustCommitment { hash })
382        }
383    }
384
385    uniffi::custom_type!(RustCommitment, Commitment);
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[cfg(target_arch = "wasm32")]
393    use wasm_bindgen_test::wasm_bindgen_test as test;
394
395    #[test]
396    fn test_single_sparse_share_v0() {
397        let namespace = Namespace::new(0, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).unwrap();
398        let data = vec![1, 2, 3, 4, 5, 6, 7];
399        let mut cursor = Cursor::new(&data);
400
401        let share = build_sparse_share(namespace, 0, None, &mut cursor).unwrap();
402
403        // check cursor
404        assert!(!cursor.has_remaining());
405
406        // check namespace
407        let (share_ns, share_data) = share.as_ref().split_at(appconsts::NAMESPACE_SIZE);
408        assert_eq!(share_ns, namespace.as_bytes());
409
410        // check data
411        let expected_share_start: &[u8] = &[
412            1, // info byte
413            0, 0, 0, 7, // sequence len
414            1, 2, 3, 4, 5, 6, 7, // data
415        ];
416        let (share_data, share_padding) = share_data.split_at(expected_share_start.len());
417        assert_eq!(share_data, expected_share_start);
418
419        // check padding
420        assert_eq!(
421            share_padding,
422            &vec![0; appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE - data.len()],
423        );
424    }
425
426    #[test]
427    fn test_single_sparse_share_v1() {
428        let namespace = Namespace::new(0, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).unwrap();
429        let data = vec![1, 2, 3, 4, 5, 6, 7];
430        let signer = AccAddress::from([9; appconsts::SIGNER_SIZE]);
431        let mut cursor = Cursor::new(&data);
432
433        let share = build_sparse_share(namespace, 1, Some(&signer), &mut cursor).unwrap();
434
435        // check cursor
436        assert!(!cursor.has_remaining());
437
438        // check namespace
439        let (share_ns, share_data) = share.as_ref().split_at(appconsts::NAMESPACE_SIZE);
440        assert_eq!(share_ns, namespace.as_bytes());
441
442        // check data
443        let expected_share_start: &[u8] = &[
444            0b00000011, // info byte
445            0, 0, 0, 7, // sequence len
446            9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // signer
447            1, 2, 3, 4, 5, 6, 7, // data
448        ];
449        let (share_data, share_padding) = share_data.split_at(expected_share_start.len());
450        assert_eq!(share_data, expected_share_start);
451
452        // check padding
453        assert_eq!(
454            share_padding,
455            &vec![
456                0;
457                appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE - appconsts::SIGNER_SIZE - data.len()
458            ],
459        );
460    }
461
462    #[test]
463    fn test_sparse_share_v0_with_continuation() {
464        let namespace = Namespace::new(0, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).unwrap();
465        let continuation_len = 7;
466        let data = vec![7; appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE + continuation_len];
467        let mut cursor = Cursor::new(&data);
468
469        let first_share = build_sparse_share(namespace, 0, None, &mut cursor).unwrap();
470
471        // check cursor
472        assert_eq!(
473            cursor.position(),
474            appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE as u64
475        );
476
477        // check namespace
478        let (share_ns, share_data) = first_share.as_ref().split_at(appconsts::NAMESPACE_SIZE);
479        assert_eq!(share_ns, namespace.as_bytes());
480
481        // check info byte
482        let (share_info_byte, share_data) = share_data.split_at(appconsts::SHARE_INFO_BYTES);
483        assert_eq!(share_info_byte, &[1]);
484
485        // check sequence len
486        let (share_seq_len, share_data) = share_data.split_at(appconsts::SEQUENCE_LEN_BYTES);
487        assert_eq!(share_seq_len, &(data.len() as u32).to_be_bytes());
488
489        // check data
490        assert_eq!(
491            share_data,
492            &vec![7; appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE]
493        );
494
495        // Continuation share
496        let continuation_share = build_sparse_share(namespace, 0, None, &mut cursor).unwrap();
497
498        // check cursor
499        assert!(!cursor.has_remaining());
500
501        // check namespace
502        let (share_ns, share_data) = continuation_share
503            .as_ref()
504            .split_at(appconsts::NAMESPACE_SIZE);
505        assert_eq!(share_ns, namespace.as_bytes());
506
507        // check data
508        let expected_continuation_share_start: &[u8] = &[
509            0, // info byte
510            7, 7, 7, 7, 7, 7, 7, // data
511        ];
512        let (share_data, share_padding) =
513            share_data.split_at(expected_continuation_share_start.len());
514        assert_eq!(share_data, expected_continuation_share_start);
515
516        // check padding
517        assert_eq!(
518            share_padding,
519            &vec![0; appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE - continuation_len],
520        );
521    }
522
523    #[test]
524    fn test_sparse_share_v0_empty_data() {
525        let namespace = Namespace::new(0, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).unwrap();
526        let data = vec![];
527        let mut cursor = Cursor::new(&data);
528        let expected_share_start: &[u8] = &[
529            1, // info byte
530            0, 0, 0, 0, // sequence len
531        ];
532
533        let share = build_sparse_share(namespace, 0, None, &mut cursor).unwrap();
534
535        // check cursor
536        assert!(!cursor.has_remaining());
537
538        // check namespace
539        let (share_ns, share_data) = share.as_ref().split_at(appconsts::NAMESPACE_SIZE);
540        assert_eq!(share_ns, namespace.as_bytes());
541
542        // check data
543        let (share_start, share_data) = share_data.split_at(expected_share_start.len());
544        assert_eq!(share_start, expected_share_start);
545
546        // check padding
547        assert_eq!(
548            share_data,
549            &vec![0; appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE],
550        );
551    }
552
553    #[test]
554    fn merkle_mountain_ranges() {
555        struct TestCase {
556            total_size: u64,
557            square_size: u64,
558            expected: Vec<u64>,
559        }
560
561        let test_cases = [
562            TestCase {
563                total_size: 11,
564                square_size: 4,
565                expected: vec![4, 4, 2, 1],
566            },
567            TestCase {
568                total_size: 2,
569                square_size: 64,
570                expected: vec![2],
571            },
572            TestCase {
573                total_size: 64,
574                square_size: 8,
575                expected: vec![8, 8, 8, 8, 8, 8, 8, 8],
576            },
577            // Height
578            // 3              x                               x
579            //              /    \                         /    \
580            //             /      \                       /      \
581            //            /        \                     /        \
582            //           /          \                   /          \
583            // 2        x            x                 x            x
584            //        /   \        /   \             /   \        /   \
585            // 1     x     x      x     x           x     x      x     x         x
586            //      / \   / \    / \   / \         / \   / \    / \   / \      /   \
587            // 0   0   1 2   3  4   5 6   7       8   9 10  11 12 13 14  15   16   17    18
588            TestCase {
589                total_size: 19,
590                square_size: 8,
591                expected: vec![8, 8, 2, 1],
592            },
593        ];
594        for case in test_cases {
595            assert_eq!(
596                merkle_mountain_range_sizes(case.total_size, case.square_size),
597                case.expected,
598            );
599        }
600    }
601
602    #[test]
603    fn blob_validation() {
604        let app_signer_allowed = Some(AppVersion::V3);
605        let app_signer_forbidden = Some(AppVersion::V2);
606        let app_unknown = None;
607
608        let share_signer_required = appconsts::SHARE_VERSION_ONE;
609        let share_signer_forbidden = appconsts::SHARE_VERSION_ZERO;
610        let share_version_unsupported = appconsts::MAX_SHARE_VERSION;
611
612        let with_signer = true;
613        let no_signer = false;
614
615        // all good - no signer
616        validate_blob(share_signer_forbidden, no_signer, app_signer_allowed).unwrap();
617        validate_blob(share_signer_forbidden, no_signer, app_signer_forbidden).unwrap();
618        validate_blob(share_signer_forbidden, no_signer, app_unknown).unwrap();
619
620        // all good - with signer
621        validate_blob(share_signer_required, with_signer, app_signer_allowed).unwrap();
622        validate_blob(share_signer_required, with_signer, app_unknown).unwrap();
623
624        // unsupported app version
625        validate_blob(share_version_unsupported, no_signer, app_signer_allowed).unwrap_err();
626
627        // no signer when required
628        validate_blob(share_signer_required, no_signer, app_signer_forbidden).unwrap_err();
629        validate_blob(share_signer_required, no_signer, app_signer_allowed).unwrap_err();
630        validate_blob(share_signer_required, no_signer, app_unknown).unwrap_err();
631
632        // with signer when forbidden
633        validate_blob(share_signer_required, with_signer, app_signer_forbidden).unwrap_err();
634        validate_blob(share_signer_forbidden, with_signer, app_signer_forbidden).unwrap_err();
635        validate_blob(share_signer_forbidden, with_signer, app_signer_allowed).unwrap_err();
636        validate_blob(share_signer_forbidden, with_signer, app_unknown).unwrap_err();
637    }
638}