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