Skip to main content

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