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