celestia_types/
blob.rs

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