1use 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#[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 pub namespace: Namespace,
42 pub data: Vec<u8>,
44 pub share_version: u8,
46 pub commitment: Commitment,
48 pub index: Option<u64>,
50 pub signer: Option<AccAddress>,
54}
55
56#[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 pub gas_per_blob_byte: u32,
66 pub gov_max_square_size: u64,
68}
69
70impl Blob {
71 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[uniffi::method(name = "namespace")]
491 pub fn get_namespace(&self) -> Namespace {
492 self.namespace
493 }
494
495 #[uniffi::method(name = "data")]
497 pub fn get_data(&self) -> Vec<u8> {
498 self.data.clone()
499 }
500
501 #[uniffi::method(name = "share_version")]
503 pub fn get_share_version(&self) -> u8 {
504 self.share_version
505 }
506
507 #[uniffi::method(name = "commitment")]
509 pub fn get_commitment(&self) -> Commitment {
510 self.commitment
511 }
512
513 #[uniffi::method(name = "index")]
515 pub fn get_index(&self) -> Option<u64> {
516 self.index
517 }
518
519 #[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 #[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 #[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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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}