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