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))]
59pub struct BlobParams {
60 pub gas_per_blob_byte: u32,
62 pub gov_max_square_size: u64,
64}
65
66impl Blob {
67 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[uniffi::method(name = "namespace")]
441 pub fn get_namespace(&self) -> Namespace {
442 self.namespace
443 }
444
445 #[uniffi::method(name = "data")]
447 pub fn get_data(&self) -> Vec<u8> {
448 self.data.clone()
449 }
450
451 #[uniffi::method(name = "share_version")]
453 pub fn get_share_version(&self) -> u8 {
454 self.share_version
455 }
456
457 #[uniffi::method(name = "commitment")]
459 pub fn get_commitment(&self) -> Commitment {
460 self.commitment
461 }
462
463 #[uniffi::method(name = "index")]
465 pub fn get_index(&self) -> Option<u64> {
466 self.index
467 }
468
469 #[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 #[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 #[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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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}