celestia_types/
blob.rs

1//! Types related to creation and submission of blobs.
2
3use std::iter;
4#[cfg(feature = "uniffi")]
5use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9mod commitment;
10mod msg_pay_for_blobs;
11
12use crate::consts::appconsts;
13use crate::consts::appconsts::AppVersion;
14#[cfg(feature = "uniffi")]
15use crate::error::UniffiResult;
16use crate::nmt::Namespace;
17use crate::state::{AccAddress, AddressTrait};
18use crate::{bail_validation, Error, Result, Share};
19
20pub use self::commitment::Commitment;
21pub use self::msg_pay_for_blobs::MsgPayForBlobs;
22pub use celestia_proto::celestia::blob::v1::MsgPayForBlobs as RawMsgPayForBlobs;
23pub use celestia_proto::proto::blob::v1::BlobProto as RawBlob;
24pub use celestia_proto::proto::blob::v1::BlobTx as RawBlobTx;
25#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))]
26use wasm_bindgen::prelude::*;
27
28/// Arbitrary data that can be stored in the network within certain [`Namespace`].
29// NOTE: We don't use the `serde(try_from)` pattern for this type
30// becase JSON representation needs to have `commitment` field but
31// Protobuf definition doesn't.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(try_from = "custom_serde::SerdeBlob", into = "custom_serde::SerdeBlob")]
34#[cfg_attr(
35    all(feature = "wasm-bindgen", target_arch = "wasm32"),
36    wasm_bindgen(getter_with_clone, inspectable)
37)]
38#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
39pub struct Blob {
40    /// A [`Namespace`] the [`Blob`] belongs to.
41    pub namespace: Namespace,
42    /// Data stored within the [`Blob`].
43    pub data: Vec<u8>,
44    /// Version indicating the format in which [`Share`]s should be created from this [`Blob`].
45    pub share_version: u8,
46    /// A [`Commitment`] computed from the [`Blob`]s data.
47    pub commitment: Commitment,
48    /// Index of the blob's first share in the EDS. Only set for blobs retrieved from chain.
49    pub index: Option<u64>,
50    /// A signer of the blob, i.e. address of the account which submitted the blob.
51    ///
52    /// Must be present in `share_version 1` and absent otherwise.
53    pub signer: Option<AccAddress>,
54}
55
56/// Params defines the parameters for the blob module.
57#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
58#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
59#[cfg_attr(
60    all(feature = "wasm-bindgen", target_arch = "wasm32"),
61    wasm_bindgen(getter_with_clone, inspectable)
62)]
63pub struct BlobParams {
64    /// Gas cost per blob byte
65    pub gas_per_blob_byte: u32,
66    /// Max square size
67    pub gov_max_square_size: u64,
68}
69
70impl Blob {
71    /// Create a new blob with the given data within the [`Namespace`], with optional signer.
72    ///
73    /// # Notes
74    ///
75    /// If present onchain, `signer` was verified by consensus node on blob submission.
76    ///
77    /// # Errors
78    ///
79    /// This function propagates any error from the [`Commitment`] creation.
80    /// To use `signer = Some(address)`, [`AppVersion`] must be at least [`AppVersion::V3`].
81    ///
82    /// # Example
83    ///
84    /// ```
85    /// use celestia_types::{AppVersion, Blob, nmt::Namespace, state::AccAddress};
86    ///
87    /// let my_namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
88    /// let signer: AccAddress = "celestia1377k5an3f94v6wyaceu0cf4nq6gk2jtpc46g7h"
89    ///     .parse()
90    ///     .unwrap();
91    /// let blob = Blob::new(
92    ///     my_namespace,
93    ///     b"some data to store on blockchain".to_vec(),
94    ///     Some(signer),
95    ///     AppVersion::V5,
96    /// )
97    /// .expect("Failed to create a blob");
98    ///
99    /// assert_eq!(
100    ///     serde_json::to_string_pretty(&blob).unwrap(),
101    ///     indoc::indoc! {r#"{
102    ///       "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQIDBAU=",
103    ///       "data": "c29tZSBkYXRhIHRvIHN0b3JlIG9uIGJsb2NrY2hhaW4=",
104    ///       "share_version": 1,
105    ///       "commitment": "AUpLKHYnlrfK0cX6DcGyryFMld1nJia+cjkCwXFTFgA=",
106    ///       "index": -1,
107    ///       "signer": "j71qdnFJas04ncZ4/CazBpFlSWE="
108    ///     }"#},
109    /// );
110    /// ```
111    pub fn new(
112        namespace: Namespace,
113        data: Vec<u8>,
114        signer: Option<AccAddress>,
115        app_version: AppVersion,
116    ) -> Result<Blob> {
117        let share_version = if signer.is_none() {
118            appconsts::SHARE_VERSION_ZERO
119        } else {
120            appconsts::SHARE_VERSION_ONE
121        };
122
123        let commitment = Commitment::from_blob(
124            namespace,
125            &data[..],
126            share_version,
127            signer.as_ref(),
128            app_version,
129        )?;
130
131        Ok(Blob {
132            namespace,
133            data,
134            share_version,
135            commitment,
136            index: None,
137            signer,
138        })
139    }
140
141    /// Creates a `Blob` from [`RawBlob`] and an [`AppVersion`].
142    pub fn from_raw(raw: RawBlob, app_version: AppVersion) -> Result<Blob> {
143        let namespace = Namespace::new(raw.namespace_version as u8, &raw.namespace_id)?;
144        let share_version =
145            u8::try_from(raw.share_version).map_err(|_| Error::UnsupportedShareVersion(u8::MAX))?;
146        let signer = raw.signer.try_into().map(AccAddress::new).ok();
147        let commitment = Commitment::from_blob(
148            namespace,
149            &raw.data[..],
150            share_version,
151            signer.as_ref(),
152            app_version,
153        )?;
154
155        Ok(Blob {
156            namespace,
157            data: raw.data,
158            share_version,
159            commitment,
160            index: None,
161            signer,
162        })
163    }
164
165    /// Validate [`Blob`]s data with the [`Commitment`] it has.
166    ///
167    /// # Errors
168    ///
169    /// If validation fails, this function will return an error with a reason of failure.
170    ///
171    /// # Example
172    ///
173    /// ```
174    /// use celestia_types::Blob;
175    /// # use celestia_types::consts::appconsts::AppVersion;
176    /// # use celestia_types::nmt::Namespace;
177    /// #
178    /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
179    ///
180    /// let mut blob = Blob::new(namespace, b"foo".to_vec(), None, AppVersion::V2).unwrap();
181    ///
182    /// assert!(blob.validate(AppVersion::V2).is_ok());
183    ///
184    /// let other_blob = Blob::new(namespace, b"bar".to_vec(), None, AppVersion::V2).unwrap();
185    /// blob.commitment = other_blob.commitment;
186    ///
187    /// assert!(blob.validate(AppVersion::V2).is_err());
188    /// ```
189    pub fn validate(&self, app_version: AppVersion) -> Result<()> {
190        let computed_commitment = Commitment::from_blob(
191            self.namespace,
192            &self.data,
193            self.share_version,
194            self.signer.as_ref(),
195            app_version,
196        )?;
197
198        if self.commitment != computed_commitment {
199            bail_validation!("blob commitment != localy computed commitment")
200        }
201
202        Ok(())
203    }
204
205    /// Validate [`Blob`]s data with a [`Commitment`].
206    ///
207    /// # Errors
208    ///
209    /// If validation fails, this function will return an error with a reason of failure.
210    ///
211    /// # Example
212    ///
213    /// ```
214    /// use celestia_types::Blob;
215    /// # use celestia_types::consts::appconsts::AppVersion;
216    /// # use celestia_types::nmt::Namespace;
217    /// #
218    /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
219    /// #
220    /// # let commitment = Blob::new(namespace, b"foo".to_vec(), None, AppVersion::V2)
221    /// #     .unwrap()
222    /// #     .commitment;
223    ///
224    /// let blob = Blob::new(namespace, b"foo".to_vec(), None, AppVersion::V2).unwrap();
225    ///
226    /// assert!(blob.validate_with_commitment(&commitment, AppVersion::V2).is_ok());
227    ///
228    /// let other_commitment = Blob::new(namespace, b"bar".to_vec(), None, AppVersion::V2)
229    ///     .unwrap()
230    ///     .commitment;
231    ///
232    /// assert!(blob.validate_with_commitment(&other_commitment, AppVersion::V2).is_err());
233    /// ```
234    pub fn validate_with_commitment(
235        &self,
236        commitment: &Commitment,
237        app_version: AppVersion,
238    ) -> Result<()> {
239        self.validate(app_version)?;
240
241        if self.commitment != *commitment {
242            bail_validation!("blob commitment != commitment");
243        }
244
245        Ok(())
246    }
247
248    /// Encode the blob into a sequence of shares.
249    ///
250    /// Check the [`Share`] documentation for more information about the share format.
251    ///
252    /// # Errors
253    ///
254    /// This function will return an error if [`InfoByte`] creation fails
255    /// or the data length overflows [`u32`].
256    ///
257    /// # Example
258    ///
259    /// ```
260    /// use celestia_types::Blob;
261    /// # use celestia_types::consts::appconsts::AppVersion;
262    /// # use celestia_types::nmt::Namespace;
263    /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
264    ///
265    /// let blob = Blob::new(namespace, b"foo".to_vec(), None, AppVersion::V2).unwrap();
266    /// let shares = blob.to_shares().unwrap();
267    ///
268    /// assert_eq!(shares.len(), 1);
269    /// ```
270    ///
271    /// [`Share`]: crate::share::Share
272    /// [`InfoByte`]: crate::share::InfoByte
273    pub fn to_shares(&self) -> Result<Vec<Share>> {
274        commitment::split_blob_to_shares(
275            self.namespace,
276            self.share_version,
277            &self.data,
278            self.signer.as_ref(),
279        )
280    }
281
282    /// Reconstructs a blob from shares.
283    ///
284    /// # Errors
285    ///
286    /// This function will return an error if:
287    /// - there is not enough shares to reconstruct the blob
288    /// - blob doesn't start with the first share
289    /// - shares are from any reserved namespace
290    /// - shares for the blob have different namespaces / share version
291    ///
292    /// # Example
293    ///
294    /// ```
295    /// use celestia_types::{AppVersion, Blob};
296    /// # use celestia_types::nmt::Namespace;
297    /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
298    ///
299    /// let blob = Blob::new(namespace, b"foo".to_vec(), None, AppVersion::V2).unwrap();
300    /// let shares = blob.to_shares().unwrap();
301    ///
302    /// let reconstructed = Blob::reconstruct(&shares, AppVersion::V2).unwrap();
303    ///
304    /// assert_eq!(blob, reconstructed);
305    /// ```
306    pub fn reconstruct<'a, I>(shares: I, app_version: AppVersion) -> Result<Self>
307    where
308        I: IntoIterator<Item = &'a Share>,
309    {
310        let mut shares = shares.into_iter();
311        let first_share = shares.next().ok_or(Error::MissingShares)?;
312        let blob_len = first_share
313            .sequence_length()
314            .ok_or(Error::ExpectedShareWithSequenceStart)?;
315        let namespace = first_share.namespace();
316        if namespace.is_reserved() {
317            return Err(Error::UnexpectedReservedNamespace);
318        }
319        let share_version = first_share.info_byte().expect("non parity").version();
320        let signer = first_share.signer();
321
322        let shares_needed = shares_needed_for_blob(blob_len as usize, signer.is_some());
323        let mut data =
324            Vec::with_capacity(shares_needed * appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE);
325        data.extend_from_slice(first_share.payload().expect("non parity"));
326
327        for _ in 1..shares_needed {
328            let share = shares.next().ok_or(Error::MissingShares)?;
329            if share.namespace() != namespace {
330                return Err(Error::BlobSharesMetadataMismatch(format!(
331                    "expected namespace ({:?}) got ({:?})",
332                    namespace,
333                    share.namespace()
334                )));
335            }
336            let version = share.info_byte().expect("non parity").version();
337            if version != share_version {
338                return Err(Error::BlobSharesMetadataMismatch(format!(
339                    "expected share version ({share_version}) got ({version})"
340                )));
341            }
342            if share.sequence_length().is_some() {
343                return Err(Error::UnexpectedSequenceStart);
344            }
345            data.extend_from_slice(share.payload().expect("non parity"));
346        }
347
348        // remove padding
349        data.truncate(blob_len as usize);
350
351        if share_version == appconsts::SHARE_VERSION_ZERO {
352            Self::new(namespace, data, None, app_version)
353        } else if share_version == appconsts::SHARE_VERSION_ONE {
354            // shouldn't happen as we have user namespace, seq start, and share v1
355            let signer = signer.ok_or(Error::MissingSigner)?;
356            Self::new(namespace, data, Some(signer), app_version)
357        } else {
358            Err(Error::UnsupportedShareVersion(share_version))
359        }
360    }
361
362    /// Reconstructs all the blobs from shares.
363    ///
364    /// This function will seek shares that indicate start of the next blob (with
365    /// [`Share::sequence_length`]) and pass them to [`Blob::reconstruct`].
366    /// It will automatically ignore all shares that are within reserved namespaces
367    /// e.g. it is completely fine to pass whole [`ExtendedDataSquare`] to this
368    /// function and get all blobs in the block.
369    ///
370    /// # Errors
371    ///
372    /// This function propagates any errors from [`Blob::reconstruct`].
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use celestia_types::{AppVersion, Blob};
378    /// # use celestia_types::nmt::Namespace;
379    /// # let namespace1 = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
380    /// # let namespace2 = Namespace::new_v0(&[2, 3, 4, 5, 6]).expect("Invalid namespace");
381    ///
382    /// let blobs = vec![
383    ///     Blob::new(namespace1, b"foo".to_vec(), None, AppVersion::V2).unwrap(),
384    ///     Blob::new(namespace2, b"bar".to_vec(), None, AppVersion::V2).unwrap(),
385    /// ];
386    /// let shares: Vec<_> = blobs.iter().flat_map(|blob| blob.to_shares().unwrap()).collect();
387    ///
388    /// let reconstructed = Blob::reconstruct_all(&shares, AppVersion::V2).unwrap();
389    ///
390    /// assert_eq!(blobs, reconstructed);
391    /// ```
392    ///
393    /// [`ExtendedDataSquare`]: crate::ExtendedDataSquare
394    pub fn reconstruct_all<'a, I>(shares: I, app_version: AppVersion) -> Result<Vec<Self>>
395    where
396        I: IntoIterator<Item = &'a Share>,
397    {
398        let mut shares = shares
399            .into_iter()
400            .filter(|shr| !shr.namespace().is_reserved());
401        let mut blobs = Vec::with_capacity(2);
402
403        loop {
404            let mut blob = {
405                // find next share from blobs namespace that is sequence start
406                let Some(start) = shares.find(|&shr| shr.sequence_length().is_some()) else {
407                    break;
408                };
409                iter::once(start).chain(&mut shares)
410            };
411            blobs.push(Blob::reconstruct(&mut blob, app_version)?);
412        }
413
414        Ok(blobs)
415    }
416
417    /// Get the amount of shares needed to encode this blob.
418    ///
419    /// # Example
420    ///
421    /// ```
422    /// use celestia_types::{AppVersion, Blob};
423    /// # use celestia_types::nmt::Namespace;
424    /// # let namespace = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
425    ///
426    /// let blob = Blob::new(namespace, b"foo".to_vec(), None, AppVersion::V3).unwrap();
427    /// let shares_len = blob.shares_len();
428    ///
429    /// let blob_shares = blob.to_shares().unwrap();
430    ///
431    /// assert_eq!(shares_len, blob_shares.len());
432    /// ```
433    pub fn shares_len(&self) -> usize {
434        let Some(without_first_share) = self
435            .data
436            .len()
437            .checked_sub(appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE)
438        else {
439            return 1;
440        };
441        1 + without_first_share.div_ceil(appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE)
442    }
443}
444
445#[cfg(feature = "uniffi")]
446#[uniffi::export]
447impl Blob {
448    /// Create a new blob with the given data within the [`Namespace`].
449    ///
450    /// # Errors
451    ///
452    /// This function propagates any error from the [`Commitment`] creation.
453    // constructor cannot be named `new`, otherwise it doesn't show up in Kotlin ¯\_(ツ)_/¯
454    #[uniffi::constructor(name = "create")]
455    pub fn uniffi_new(
456        namespace: Arc<Namespace>,
457        data: Vec<u8>,
458        app_version: AppVersion,
459    ) -> UniffiResult<Self> {
460        let namespace = Arc::unwrap_or_clone(namespace);
461        Ok(Blob::new(namespace, data, None, app_version)?)
462    }
463
464    /// Create a new blob with the given data within the [`Namespace`] and with given signer.
465    ///
466    /// # Errors
467    ///
468    /// This function propagates any error from the [`Commitment`] creation. Also [`AppVersion`]
469    /// must be at least [`AppVersion::V3`].
470    #[uniffi::constructor(name = "create_with_signer")]
471    pub fn uniffi_new_with_signer(
472        namespace: Arc<Namespace>,
473        data: Vec<u8>,
474        signer: AccAddress,
475        app_version: AppVersion,
476    ) -> UniffiResult<Blob> {
477        let namespace = Arc::unwrap_or_clone(namespace);
478        Ok(Blob::new(namespace, data, Some(signer), app_version)?)
479    }
480
481    /// A [`Namespace`] the [`Blob`] belongs to.
482    #[uniffi::method(name = "namespace")]
483    pub fn get_namespace(&self) -> Namespace {
484        self.namespace
485    }
486
487    /// Data stored within the [`Blob`].
488    #[uniffi::method(name = "data")]
489    pub fn get_data(&self) -> Vec<u8> {
490        self.data.clone()
491    }
492
493    /// Version indicating the format in which [`Share`]s should be created from this [`Blob`].
494    #[uniffi::method(name = "share_version")]
495    pub fn get_share_version(&self) -> u8 {
496        self.share_version
497    }
498
499    /// A [`Commitment`] computed from the [`Blob`]s data.
500    #[uniffi::method(name = "commitment")]
501    pub fn get_commitment(&self) -> Commitment {
502        self.commitment
503    }
504
505    /// Index of the blob's first share in the EDS. Only set for blobs retrieved from chain.
506    #[uniffi::method(name = "index")]
507    pub fn get_index(&self) -> Option<u64> {
508        self.index
509    }
510
511    /// A signer of the blob, i.e. address of the account which submitted the blob.
512    ///
513    /// Must be present in `share_version 1` and absent otherwise.
514    #[uniffi::method(name = "signer")]
515    pub fn get_signer(&self) -> Option<AccAddress> {
516        self.signer.clone()
517    }
518}
519
520impl From<Blob> for RawBlob {
521    fn from(value: Blob) -> RawBlob {
522        RawBlob {
523            namespace_id: value.namespace.id().to_vec(),
524            namespace_version: value.namespace.version() as u32,
525            data: value.data,
526            share_version: value.share_version as u32,
527            signer: value
528                .signer
529                .map(|addr| addr.as_bytes().to_vec())
530                .unwrap_or_default(),
531        }
532    }
533}
534
535#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))]
536#[wasm_bindgen]
537impl Blob {
538    /// Create a new blob with the given data within the [`Namespace`].
539    #[wasm_bindgen(constructor)]
540    pub fn js_new(
541        namespace: &Namespace,
542        data: Vec<u8>,
543        app_version: &appconsts::JsAppVersion,
544    ) -> Result<Blob> {
545        Self::new(*namespace, data, None, (*app_version).into())
546    }
547
548    /// Clone a blob creating a new deep copy of it.
549    #[wasm_bindgen(js_name = clone)]
550    pub fn js_clone(&self) -> Blob {
551        self.clone()
552    }
553}
554
555fn shares_needed_for_blob(blob_len: usize, has_signer: bool) -> usize {
556    let mut first_share_content = appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE;
557    if has_signer {
558        first_share_content -= appconsts::SIGNER_SIZE;
559    }
560
561    let Some(without_first_share) = blob_len.checked_sub(first_share_content) else {
562        return 1;
563    };
564    1 + without_first_share.div_ceil(appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE)
565}
566
567mod custom_serde {
568    use serde::de::Error as _;
569    use serde::ser::Error as _;
570    use serde::{Deserialize, Deserializer, Serialize, Serializer};
571    use tendermint_proto::serializers::bytes::base64string;
572
573    use crate::nmt::Namespace;
574    use crate::state::{AccAddress, AddressTrait};
575    use crate::{Error, Result};
576
577    use super::{commitment, Blob, Commitment};
578
579    mod index_serde {
580        use super::*;
581        /// Serialize [`Option<u64>`] as `i64` with `None` represented as `-1`.
582        pub fn serialize<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
583        where
584            S: Serializer,
585        {
586            let x = value
587                .map(i64::try_from)
588                .transpose()
589                .map_err(S::Error::custom)?
590                .unwrap_or(-1);
591            serializer.serialize_i64(x)
592        }
593
594        /// Deserialize [`Option<u64>`] from `i64` with negative values as `None`.
595        pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
596        where
597            D: Deserializer<'de>,
598        {
599            i64::deserialize(deserializer).map(|val| if val >= 0 { Some(val as u64) } else { None })
600        }
601    }
602
603    mod signer_serde {
604        use super::*;
605
606        /// Serialize signer as optional base64 string
607        pub fn serialize<S>(value: &Option<AccAddress>, serializer: S) -> Result<S::Ok, S::Error>
608        where
609            S: Serializer,
610        {
611            if let Some(ref addr) = value.as_ref().map(|addr| addr.as_bytes()) {
612                base64string::serialize(addr, serializer)
613            } else {
614                serializer.serialize_none()
615            }
616        }
617
618        /// Deserialize signer from optional base64 string
619        pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<AccAddress>, D::Error>
620        where
621            D: Deserializer<'de>,
622        {
623            let bytes: Vec<u8> = base64string::deserialize(deserializer)?;
624            if bytes.is_empty() {
625                Ok(None)
626            } else {
627                let addr = AccAddress::new(bytes.try_into().map_err(D::Error::custom)?);
628                Ok(Some(addr))
629            }
630        }
631    }
632
633    /// This is the copy of the `Blob` struct, to perform additional checks during deserialization
634    #[derive(Serialize, Deserialize)]
635    pub(super) struct SerdeBlob {
636        namespace: Namespace,
637        #[serde(with = "base64string")]
638        data: Vec<u8>,
639        share_version: u8,
640        commitment: Commitment,
641        // NOTE: celestia supports deserializing blobs without index, so we should too
642        #[serde(default, with = "index_serde")]
643        index: Option<u64>,
644        #[serde(default, with = "signer_serde")]
645        signer: Option<AccAddress>,
646    }
647
648    impl From<Blob> for SerdeBlob {
649        fn from(value: Blob) -> Self {
650            Self {
651                namespace: value.namespace,
652                data: value.data,
653                share_version: value.share_version,
654                commitment: value.commitment,
655                index: value.index,
656                signer: value.signer,
657            }
658        }
659    }
660
661    impl TryFrom<SerdeBlob> for Blob {
662        type Error = Error;
663
664        fn try_from(value: SerdeBlob) -> Result<Self> {
665            // we don't need to require app version when deserializing because commitment is provided
666            // user can still verify commitment and app version compatibility using `Blob::validate`
667            commitment::validate_blob(value.share_version, value.signer.is_some(), None)?;
668
669            Ok(Blob {
670                namespace: value.namespace,
671                data: value.data,
672                share_version: value.share_version,
673                commitment: value.commitment,
674                index: value.index,
675                signer: value.signer,
676            })
677        }
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use crate::nmt::{NS_ID_SIZE, NS_SIZE};
685    use crate::test_utils::random_bytes;
686
687    #[cfg(target_arch = "wasm32")]
688    use wasm_bindgen_test::wasm_bindgen_test as test;
689
690    fn sample_blob() -> Blob {
691        serde_json::from_str(
692            r#"{
693              "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAAAAADCBNOWAP3dM=",
694              "data": "8fIMqAB+kQo7+LLmHaDya8oH73hxem6lQWX1",
695              "share_version": 0,
696              "commitment": "D6YGsPWdxR8ju2OcOspnkgPG2abD30pSHxsFdiPqnVk=",
697              "index": -1
698            }"#,
699        )
700        .unwrap()
701    }
702
703    fn sample_blob_with_signer() -> Blob {
704        serde_json::from_str(
705            r#"{
706              "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAALwwSWpxCuQb5+A=",
707              "data": "lQnnMKE=",
708              "share_version": 1,
709              "commitment": "dujykaNN+Ey7ET3QNdPG0g2uveriBvZusA3fLSOdMKU=",
710              "index": -1,
711              "signer": "Yjc3XldhbdYke5i8aSlggYxCCLE="
712            }"#,
713        )
714        .unwrap()
715    }
716
717    #[test]
718    fn create_new_blob_unsigned() {
719        use crate::consts::appconsts;
720
721        let ns = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
722        let blob = Blob::new(
723            ns,
724            b"some data to store on blockchain".to_vec(),
725            None,
726            AppVersion::V2,
727        )
728        .expect("Failed to create blob");
729
730        let shares = blob.to_shares().expect("to_shares failed");
731        assert!(!shares.is_empty(), "expected at least one share");
732
733        let first = &shares[0];
734
735        // first share should be a sequence start
736        assert!(
737            first.sequence_length().is_some(),
738            "first share must be a sequence start"
739        );
740
741        // check share version and signer
742        let version = first.info_byte().expect("non parity share").version();
743        assert_eq!(
744            version,
745            appconsts::SHARE_VERSION_ZERO,
746            "unexpected share version for unsigned blob"
747        );
748        assert_eq!(
749            first.signer(),
750            None,
751            "unsigned blob must not carry a signer"
752        );
753    }
754
755    #[test]
756    fn create_new_blob_signed() {
757        use crate::consts::appconsts;
758
759        let ns = Namespace::new_v0(&[1, 2, 3, 4, 5]).expect("Invalid namespace");
760        let signer: AccAddress = "celestia1377k5an3f94v6wyaceu0cf4nq6gk2jtpc46g7h"
761            .parse()
762            .expect("invalid signer");
763
764        // AppVersion must allow signer (>= V3)
765        let blob = Blob::new(
766            ns,
767            b"some data to store on blockchain".to_vec(),
768            Some(signer.clone()),
769            AppVersion::V5,
770        )
771        .expect("Failed to create signed blob");
772
773        let shares = blob.to_shares().expect("to_shares failed");
774        assert!(!shares.is_empty(), "expected at least one share");
775
776        let first = &shares[0];
777
778        // first share should be a sequence start
779        assert!(
780            first.sequence_length().is_some(),
781            "first share must be a sequence start"
782        );
783
784        // check share version and signer
785        let version = first.info_byte().expect("non parity share").version();
786        assert_eq!(
787            version,
788            appconsts::SHARE_VERSION_ONE,
789            "unexpected share version for signed blob"
790        );
791        assert_eq!(
792            first.signer(),
793            Some(signer),
794            "first share must carry the expected signer"
795        );
796    }
797
798    #[test]
799    fn create_from_raw() {
800        let expected = sample_blob();
801        let raw = RawBlob::from(expected.clone());
802        let created = Blob::from_raw(raw, AppVersion::V2).unwrap();
803
804        assert_eq!(created, expected);
805    }
806
807    #[test]
808    fn create_from_raw_with_signer() {
809        let expected = sample_blob_with_signer();
810
811        let raw = RawBlob::from(expected.clone());
812
813        Blob::from_raw(raw.clone(), AppVersion::V2).unwrap_err();
814        let created = Blob::from_raw(raw, AppVersion::V3).unwrap();
815
816        assert_eq!(created, expected);
817    }
818
819    #[test]
820    fn validate_blob() {
821        sample_blob().validate(AppVersion::V2).unwrap();
822    }
823
824    #[test]
825    fn validate_blob_with_signer() {
826        sample_blob_with_signer()
827            .validate(AppVersion::V2)
828            .unwrap_err();
829        sample_blob_with_signer().validate(AppVersion::V3).unwrap();
830    }
831
832    #[test]
833    fn validate_blob_commitment_mismatch() {
834        let mut blob = sample_blob();
835        blob.commitment = Commitment::new([7; 32]);
836
837        blob.validate(AppVersion::V2).unwrap_err();
838    }
839
840    #[test]
841    fn deserialize_blob_with_missing_index() {
842        serde_json::from_str::<Blob>(
843            r#"{
844              "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAAAAADCBNOWAP3dM=",
845              "data": "8fIMqAB+kQo7+LLmHaDya8oH73hxem6lQWX1",
846              "share_version": 0,
847              "commitment": "D6YGsPWdxR8ju2OcOspnkgPG2abD30pSHxsFdiPqnVk="
848            }"#,
849        )
850        .unwrap();
851    }
852
853    #[test]
854    fn deserialize_blob_with_share_version_and_signer_mismatch() {
855        // signer in v0
856        serde_json::from_str::<Blob>(
857            r#"{
858              "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAALwwSWpxCuQb5+A=",
859              "data": "lQnnMKE=",
860              "share_version": 0,
861              "commitment": "dujykaNN+Ey7ET3QNdPG0g2uveriBvZusA3fLSOdMKU=",
862              "signer": "Yjc3XldhbdYke5i8aSlggYxCCLE="
863            }"#,
864        )
865        .unwrap_err();
866
867        // no signer in v1
868        serde_json::from_str::<Blob>(
869            r#"{
870              "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAALwwSWpxCuQb5+A=",
871              "data": "lQnnMKE=",
872              "share_version": 1,
873              "commitment": "dujykaNN+Ey7ET3QNdPG0g2uveriBvZusA3fLSOdMKU=",
874            }"#,
875        )
876        .unwrap_err();
877    }
878
879    #[test]
880    fn reconstruct() {
881        for _ in 0..10 {
882            let len = rand::random::<usize>() % (1024 * 1024) + 1;
883            let data = random_bytes(len);
884            let ns = Namespace::const_v0(rand::random());
885            let blob = Blob::new(ns, data, None, AppVersion::V2).unwrap();
886
887            let shares = blob.to_shares().unwrap();
888            assert_eq!(blob, Blob::reconstruct(&shares, AppVersion::V2).unwrap());
889        }
890    }
891
892    #[test]
893    fn reconstruct_with_signer() {
894        for _ in 0..10 {
895            let len = rand::random::<usize>() % (1024 * 1024) + 1;
896            let data = random_bytes(len);
897            let ns = Namespace::const_v0(rand::random());
898            let signer = rand::random::<[u8; 20]>().into();
899
900            let blob = Blob::new(ns, data, Some(signer), AppVersion::V3).unwrap();
901            let shares = blob.to_shares().unwrap();
902
903            Blob::reconstruct(&shares, AppVersion::V2).unwrap_err();
904            assert_eq!(blob, Blob::reconstruct(&shares, AppVersion::V3).unwrap());
905        }
906    }
907
908    #[test]
909    fn reconstruct_empty() {
910        assert!(matches!(
911            Blob::reconstruct(&Vec::<Share>::new(), AppVersion::V2),
912            Err(Error::MissingShares)
913        ));
914    }
915
916    #[test]
917    fn reconstruct_not_sequence_start() {
918        let len = rand::random::<usize>() % (1024 * 1024) + 1;
919        let data = random_bytes(len);
920        let ns = Namespace::const_v0(rand::random());
921        let mut shares = Blob::new(ns, data, None, AppVersion::V2)
922            .unwrap()
923            .to_shares()
924            .unwrap();
925
926        // modify info byte to remove sequence start bit
927        shares[0].as_mut()[NS_SIZE] &= 0b11111110;
928
929        assert!(matches!(
930            Blob::reconstruct(&shares, AppVersion::V2),
931            Err(Error::ExpectedShareWithSequenceStart)
932        ));
933    }
934
935    #[test]
936    fn reconstruct_reserved_namespace() {
937        for ns in (0..255).flat_map(|n| {
938            let mut v0 = [0; NS_ID_SIZE];
939            *v0.last_mut().unwrap() = n;
940            let mut v255 = [0xff; NS_ID_SIZE];
941            *v255.last_mut().unwrap() = n;
942
943            [Namespace::new_v0(&v0), Namespace::new_v255(&v255)]
944        }) {
945            let len = (rand::random::<usize>() % 1023 + 1) * 2;
946            let data = random_bytes(len);
947            let shares = Blob::new(ns.unwrap(), data, None, AppVersion::V2)
948                .unwrap()
949                .to_shares()
950                .unwrap();
951
952            assert!(matches!(
953                Blob::reconstruct(&shares, AppVersion::V2),
954                Err(Error::UnexpectedReservedNamespace)
955            ));
956        }
957    }
958
959    #[test]
960    fn reconstruct_not_enough_shares() {
961        let len = rand::random::<usize>() % 1024 * 1024 + 2048;
962        let data = random_bytes(len);
963        let ns = Namespace::const_v0(rand::random());
964        let shares = Blob::new(ns, data, None, AppVersion::V2)
965            .unwrap()
966            .to_shares()
967            .unwrap();
968
969        assert!(matches!(
970            // minimum for len is 4 so 3 will break stuff
971            Blob::reconstruct(&shares[..2], AppVersion::V2),
972            Err(Error::MissingShares)
973        ));
974    }
975
976    #[test]
977    fn reconstruct_inconsistent_share_version() {
978        let len = rand::random::<usize>() % (1024 * 1024) + 512;
979        let data = random_bytes(len);
980        let ns = Namespace::const_v0(rand::random());
981        let mut shares = Blob::new(ns, data, None, AppVersion::V2)
982            .unwrap()
983            .to_shares()
984            .unwrap();
985
986        // change share version in second share
987        shares[1].as_mut()[NS_SIZE] = 0b11111110;
988
989        assert!(matches!(
990            Blob::reconstruct(&shares, AppVersion::V2),
991            Err(Error::BlobSharesMetadataMismatch(..))
992        ));
993    }
994
995    #[test]
996    fn reconstruct_inconsistent_namespace() {
997        let len = rand::random::<usize>() % (1024 * 1024) + 512;
998        let data = random_bytes(len);
999        let ns = Namespace::const_v0(rand::random());
1000        let ns2 = Namespace::const_v0(rand::random());
1001        let mut shares = Blob::new(ns, data, None, AppVersion::V2)
1002            .unwrap()
1003            .to_shares()
1004            .unwrap();
1005
1006        // change namespace in second share
1007        shares[1].as_mut()[..NS_SIZE].copy_from_slice(ns2.as_bytes());
1008
1009        assert!(matches!(
1010            Blob::reconstruct(&shares, AppVersion::V2),
1011            Err(Error::BlobSharesMetadataMismatch(..))
1012        ));
1013    }
1014
1015    #[test]
1016    fn reconstruct_unexpected_sequence_start() {
1017        let len = rand::random::<usize>() % (1024 * 1024) + 512;
1018        let data = random_bytes(len);
1019        let ns = Namespace::const_v0(rand::random());
1020        let mut shares = Blob::new(ns, data, None, AppVersion::V2)
1021            .unwrap()
1022            .to_shares()
1023            .unwrap();
1024
1025        // modify info byte to add sequence start bit
1026        shares[1].as_mut()[NS_SIZE] |= 0b00000001;
1027
1028        assert!(matches!(
1029            Blob::reconstruct(&shares, AppVersion::V2),
1030            Err(Error::UnexpectedSequenceStart)
1031        ));
1032    }
1033
1034    #[test]
1035    fn reconstruct_all() {
1036        let blobs: Vec<_> = (0..rand::random::<usize>() % 16 + 3)
1037            .map(|_| {
1038                let len = rand::random::<usize>() % (1024 * 1024) + 512;
1039                let data = random_bytes(len);
1040                let ns = Namespace::const_v0(rand::random());
1041                Blob::new(ns, data, None, AppVersion::V2).unwrap()
1042            })
1043            .collect();
1044
1045        let shares: Vec<_> = blobs
1046            .iter()
1047            .flat_map(|blob| blob.to_shares().unwrap())
1048            .collect();
1049        let reconstructed = Blob::reconstruct_all(&shares, AppVersion::V2).unwrap();
1050
1051        assert_eq!(blobs, reconstructed);
1052    }
1053}