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(
120 namespace: Namespace,
121 data: Vec<u8>,
122 signer: AccAddress,
123 app_version: AppVersion,
124 ) -> Result<Blob> {
125 let signer = Some(signer);
126 let share_version = appconsts::SHARE_VERSION_ONE;
127 let commitment = Commitment::from_blob(
128 namespace,
129 &data[..],
130 share_version,
131 signer.as_ref(),
132 app_version,
133 )?;
134
135 Ok(Blob {
136 namespace,
137 data,
138 share_version,
139 commitment,
140 index: None,
141 signer,
142 })
143 }
144
145 pub fn from_raw(raw: RawBlob, app_version: AppVersion) -> Result<Blob> {
147 let namespace = Namespace::new(raw.namespace_version as u8, &raw.namespace_id)?;
148 let share_version =
149 u8::try_from(raw.share_version).map_err(|_| Error::UnsupportedShareVersion(u8::MAX))?;
150 let signer = raw.signer.try_into().map(AccAddress::new).ok();
151 let commitment = Commitment::from_blob(
152 namespace,
153 &raw.data[..],
154 share_version,
155 signer.as_ref(),
156 app_version,
157 )?;
158
159 Ok(Blob {
160 namespace,
161 data: raw.data,
162 share_version,
163 commitment,
164 index: None,
165 signer,
166 })
167 }
168
169 pub fn validate(&self, app_version: AppVersion) -> Result<()> {
194 let computed_commitment = Commitment::from_blob(
195 self.namespace,
196 &self.data,
197 self.share_version,
198 self.signer.as_ref(),
199 app_version,
200 )?;
201
202 if self.commitment != computed_commitment {
203 bail_validation!("blob commitment != localy computed commitment")
204 }
205
206 Ok(())
207 }
208
209 pub fn to_shares(&self) -> Result<Vec<Share>> {
235 commitment::split_blob_to_shares(
236 self.namespace,
237 self.share_version,
238 &self.data,
239 self.signer.as_ref(),
240 )
241 }
242
243 pub fn reconstruct<'a, I>(shares: I, app_version: AppVersion) -> Result<Self>
268 where
269 I: IntoIterator<Item = &'a Share>,
270 {
271 let mut shares = shares.into_iter();
272 let first_share = shares.next().ok_or(Error::MissingShares)?;
273 let blob_len = first_share
274 .sequence_length()
275 .ok_or(Error::ExpectedShareWithSequenceStart)?;
276 let namespace = first_share.namespace();
277 if namespace.is_reserved() {
278 return Err(Error::UnexpectedReservedNamespace);
279 }
280 let share_version = first_share.info_byte().expect("non parity").version();
281 let signer = first_share.signer();
282
283 let shares_needed = shares_needed_for_blob(blob_len as usize, signer.is_some());
284 let mut data =
285 Vec::with_capacity(shares_needed * appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE);
286 data.extend_from_slice(first_share.payload().expect("non parity"));
287
288 for _ in 1..shares_needed {
289 let share = shares.next().ok_or(Error::MissingShares)?;
290 if share.namespace() != namespace {
291 return Err(Error::BlobSharesMetadataMismatch(format!(
292 "expected namespace ({:?}) got ({:?})",
293 namespace,
294 share.namespace()
295 )));
296 }
297 let version = share.info_byte().expect("non parity").version();
298 if version != share_version {
299 return Err(Error::BlobSharesMetadataMismatch(format!(
300 "expected share version ({share_version}) got ({version})"
301 )));
302 }
303 if share.sequence_length().is_some() {
304 return Err(Error::UnexpectedSequenceStart);
305 }
306 data.extend_from_slice(share.payload().expect("non parity"));
307 }
308
309 data.truncate(blob_len as usize);
311
312 if share_version == appconsts::SHARE_VERSION_ZERO {
313 Self::new(namespace, data, app_version)
314 } else if share_version == appconsts::SHARE_VERSION_ONE {
315 let signer = signer.ok_or(Error::MissingSigner)?;
317 Self::new_with_signer(namespace, data, signer, app_version)
318 } else {
319 Err(Error::UnsupportedShareVersion(share_version))
320 }
321 }
322
323 pub fn reconstruct_all<'a, I>(shares: I, app_version: AppVersion) -> Result<Vec<Self>>
356 where
357 I: IntoIterator<Item = &'a Share>,
358 {
359 let mut shares = shares
360 .into_iter()
361 .filter(|shr| !shr.namespace().is_reserved());
362 let mut blobs = Vec::with_capacity(2);
363
364 loop {
365 let mut blob = {
366 let Some(start) = shares.find(|&shr| shr.sequence_length().is_some()) else {
368 break;
369 };
370 iter::once(start).chain(&mut shares)
371 };
372 blobs.push(Blob::reconstruct(&mut blob, app_version)?);
373 }
374
375 Ok(blobs)
376 }
377
378 pub fn shares_len(&self) -> usize {
395 let Some(without_first_share) = self
396 .data
397 .len()
398 .checked_sub(appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE)
399 else {
400 return 1;
401 };
402 1 + without_first_share.div_ceil(appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE)
403 }
404}
405
406#[cfg(feature = "uniffi")]
407#[uniffi::export]
408impl Blob {
409 #[uniffi::constructor(name = "create")]
416 pub fn uniffi_new(
417 namespace: Arc<Namespace>,
418 data: Vec<u8>,
419 app_version: AppVersion,
420 ) -> UniffiResult<Self> {
421 let namespace = Arc::unwrap_or_clone(namespace);
422 Ok(Blob::new(namespace, data, app_version)?)
423 }
424
425 #[uniffi::constructor(name = "create_with_signer")]
432 pub fn uniffi_new_with_signer(
433 namespace: Arc<Namespace>,
434 data: Vec<u8>,
435 signer: AccAddress,
436 app_version: AppVersion,
437 ) -> UniffiResult<Blob> {
438 let namespace = Arc::unwrap_or_clone(namespace);
439 Ok(Blob::new_with_signer(namespace, data, signer, app_version)?)
440 }
441
442 #[uniffi::method(name = "namespace")]
444 pub fn get_namespace(&self) -> Namespace {
445 self.namespace
446 }
447
448 #[uniffi::method(name = "data")]
450 pub fn get_data(&self) -> Vec<u8> {
451 self.data.clone()
452 }
453
454 #[uniffi::method(name = "share_version")]
456 pub fn get_share_version(&self) -> u8 {
457 self.share_version
458 }
459
460 #[uniffi::method(name = "commitment")]
462 pub fn get_commitment(&self) -> Commitment {
463 self.commitment
464 }
465
466 #[uniffi::method(name = "index")]
468 pub fn get_index(&self) -> Option<u64> {
469 self.index
470 }
471
472 #[uniffi::method(name = "signer")]
476 pub fn get_signer(&self) -> Option<AccAddress> {
477 self.signer.clone()
478 }
479}
480
481impl From<Blob> for RawBlob {
482 fn from(value: Blob) -> RawBlob {
483 RawBlob {
484 namespace_id: value.namespace.id().to_vec(),
485 namespace_version: value.namespace.version() as u32,
486 data: value.data,
487 share_version: value.share_version as u32,
488 signer: value
489 .signer
490 .map(|addr| addr.as_bytes().to_vec())
491 .unwrap_or_default(),
492 }
493 }
494}
495
496#[cfg(all(feature = "wasm-bindgen", target_arch = "wasm32"))]
497#[wasm_bindgen]
498impl Blob {
499 #[wasm_bindgen(constructor)]
501 pub fn js_new(
502 namespace: &Namespace,
503 data: Vec<u8>,
504 app_version: &appconsts::JsAppVersion,
505 ) -> Result<Blob> {
506 Self::new(*namespace, data, (*app_version).into())
507 }
508
509 #[wasm_bindgen(js_name = clone)]
511 pub fn js_clone(&self) -> Blob {
512 self.clone()
513 }
514}
515
516fn shares_needed_for_blob(blob_len: usize, has_signer: bool) -> usize {
517 let mut first_share_content = appconsts::FIRST_SPARSE_SHARE_CONTENT_SIZE;
518 if has_signer {
519 first_share_content -= appconsts::SIGNER_SIZE;
520 }
521
522 let Some(without_first_share) = blob_len.checked_sub(first_share_content) else {
523 return 1;
524 };
525 1 + without_first_share.div_ceil(appconsts::CONTINUATION_SPARSE_SHARE_CONTENT_SIZE)
526}
527
528mod custom_serde {
529 use serde::de::Error as _;
530 use serde::ser::Error as _;
531 use serde::{Deserialize, Deserializer, Serialize, Serializer};
532 use tendermint_proto::serializers::bytes::base64string;
533
534 use crate::nmt::Namespace;
535 use crate::state::{AccAddress, AddressTrait};
536 use crate::{Error, Result};
537
538 use super::{commitment, Blob, Commitment};
539
540 mod index_serde {
541 use super::*;
542 pub fn serialize<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
544 where
545 S: Serializer,
546 {
547 let x = value
548 .map(i64::try_from)
549 .transpose()
550 .map_err(S::Error::custom)?
551 .unwrap_or(-1);
552 serializer.serialize_i64(x)
553 }
554
555 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
557 where
558 D: Deserializer<'de>,
559 {
560 i64::deserialize(deserializer).map(|val| if val >= 0 { Some(val as u64) } else { None })
561 }
562 }
563
564 mod signer_serde {
565 use super::*;
566
567 pub fn serialize<S>(value: &Option<AccAddress>, serializer: S) -> Result<S::Ok, S::Error>
569 where
570 S: Serializer,
571 {
572 if let Some(ref addr) = value.as_ref().map(|addr| addr.as_bytes()) {
573 base64string::serialize(addr, serializer)
574 } else {
575 serializer.serialize_none()
576 }
577 }
578
579 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<AccAddress>, D::Error>
581 where
582 D: Deserializer<'de>,
583 {
584 let bytes: Vec<u8> = base64string::deserialize(deserializer)?;
585 if bytes.is_empty() {
586 Ok(None)
587 } else {
588 let addr = AccAddress::new(bytes.try_into().map_err(D::Error::custom)?);
589 Ok(Some(addr))
590 }
591 }
592 }
593
594 #[derive(Serialize, Deserialize)]
596 pub(super) struct SerdeBlob {
597 namespace: Namespace,
598 #[serde(with = "base64string")]
599 data: Vec<u8>,
600 share_version: u8,
601 commitment: Commitment,
602 #[serde(default, with = "index_serde")]
604 index: Option<u64>,
605 #[serde(default, with = "signer_serde")]
606 signer: Option<AccAddress>,
607 }
608
609 impl From<Blob> for SerdeBlob {
610 fn from(value: Blob) -> Self {
611 Self {
612 namespace: value.namespace,
613 data: value.data,
614 share_version: value.share_version,
615 commitment: value.commitment,
616 index: value.index,
617 signer: value.signer,
618 }
619 }
620 }
621
622 impl TryFrom<SerdeBlob> for Blob {
623 type Error = Error;
624
625 fn try_from(value: SerdeBlob) -> Result<Self> {
626 commitment::validate_blob(value.share_version, value.signer.is_some(), None)?;
629
630 Ok(Blob {
631 namespace: value.namespace,
632 data: value.data,
633 share_version: value.share_version,
634 commitment: value.commitment,
635 index: value.index,
636 signer: value.signer,
637 })
638 }
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::nmt::{NS_ID_SIZE, NS_SIZE};
646 use crate::test_utils::random_bytes;
647
648 #[cfg(target_arch = "wasm32")]
649 use wasm_bindgen_test::wasm_bindgen_test as test;
650
651 fn sample_blob() -> Blob {
652 serde_json::from_str(
653 r#"{
654 "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAAAAADCBNOWAP3dM=",
655 "data": "8fIMqAB+kQo7+LLmHaDya8oH73hxem6lQWX1",
656 "share_version": 0,
657 "commitment": "D6YGsPWdxR8ju2OcOspnkgPG2abD30pSHxsFdiPqnVk=",
658 "index": -1
659 }"#,
660 )
661 .unwrap()
662 }
663
664 fn sample_blob_with_signer() -> Blob {
665 serde_json::from_str(
666 r#"{
667 "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAALwwSWpxCuQb5+A=",
668 "data": "lQnnMKE=",
669 "share_version": 1,
670 "commitment": "dujykaNN+Ey7ET3QNdPG0g2uveriBvZusA3fLSOdMKU=",
671 "index": -1,
672 "signer": "Yjc3XldhbdYke5i8aSlggYxCCLE="
673 }"#,
674 )
675 .unwrap()
676 }
677
678 #[test]
679 fn create_from_raw() {
680 let expected = sample_blob();
681 let raw = RawBlob::from(expected.clone());
682 let created = Blob::from_raw(raw, AppVersion::V2).unwrap();
683
684 assert_eq!(created, expected);
685 }
686
687 #[test]
688 fn create_from_raw_with_signer() {
689 let expected = sample_blob_with_signer();
690
691 let raw = RawBlob::from(expected.clone());
692
693 Blob::from_raw(raw.clone(), AppVersion::V2).unwrap_err();
694 let created = Blob::from_raw(raw, AppVersion::V3).unwrap();
695
696 assert_eq!(created, expected);
697 }
698
699 #[test]
700 fn validate_blob() {
701 sample_blob().validate(AppVersion::V2).unwrap();
702 }
703
704 #[test]
705 fn validate_blob_with_signer() {
706 sample_blob_with_signer()
707 .validate(AppVersion::V2)
708 .unwrap_err();
709 sample_blob_with_signer().validate(AppVersion::V3).unwrap();
710 }
711
712 #[test]
713 fn validate_blob_commitment_mismatch() {
714 let mut blob = sample_blob();
715 blob.commitment = Commitment::new([7; 32]);
716
717 blob.validate(AppVersion::V2).unwrap_err();
718 }
719
720 #[test]
721 fn deserialize_blob_with_missing_index() {
722 serde_json::from_str::<Blob>(
723 r#"{
724 "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAAAAADCBNOWAP3dM=",
725 "data": "8fIMqAB+kQo7+LLmHaDya8oH73hxem6lQWX1",
726 "share_version": 0,
727 "commitment": "D6YGsPWdxR8ju2OcOspnkgPG2abD30pSHxsFdiPqnVk="
728 }"#,
729 )
730 .unwrap();
731 }
732
733 #[test]
734 fn deserialize_blob_with_share_version_and_signer_mismatch() {
735 serde_json::from_str::<Blob>(
737 r#"{
738 "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAALwwSWpxCuQb5+A=",
739 "data": "lQnnMKE=",
740 "share_version": 0,
741 "commitment": "dujykaNN+Ey7ET3QNdPG0g2uveriBvZusA3fLSOdMKU=",
742 "signer": "Yjc3XldhbdYke5i8aSlggYxCCLE="
743 }"#,
744 )
745 .unwrap_err();
746
747 serde_json::from_str::<Blob>(
749 r#"{
750 "namespace": "AAAAAAAAAAAAAAAAAAAAAAAAALwwSWpxCuQb5+A=",
751 "data": "lQnnMKE=",
752 "share_version": 1,
753 "commitment": "dujykaNN+Ey7ET3QNdPG0g2uveriBvZusA3fLSOdMKU=",
754 }"#,
755 )
756 .unwrap_err();
757 }
758
759 #[test]
760 fn reconstruct() {
761 for _ in 0..10 {
762 let len = rand::random::<usize>() % (1024 * 1024) + 1;
763 let data = random_bytes(len);
764 let ns = Namespace::const_v0(rand::random());
765 let blob = Blob::new(ns, data, AppVersion::V2).unwrap();
766
767 let shares = blob.to_shares().unwrap();
768 assert_eq!(blob, Blob::reconstruct(&shares, AppVersion::V2).unwrap());
769 }
770 }
771
772 #[test]
773 fn reconstruct_with_signer() {
774 for _ in 0..10 {
775 let len = rand::random::<usize>() % (1024 * 1024) + 1;
776 let data = random_bytes(len);
777 let ns = Namespace::const_v0(rand::random());
778 let signer = rand::random::<[u8; 20]>().into();
779
780 let blob = Blob::new_with_signer(ns, data, signer, AppVersion::V3).unwrap();
781 let shares = blob.to_shares().unwrap();
782
783 Blob::reconstruct(&shares, AppVersion::V2).unwrap_err();
784 assert_eq!(blob, Blob::reconstruct(&shares, AppVersion::V3).unwrap());
785 }
786 }
787
788 #[test]
789 fn reconstruct_empty() {
790 assert!(matches!(
791 Blob::reconstruct(&Vec::<Share>::new(), AppVersion::V2),
792 Err(Error::MissingShares)
793 ));
794 }
795
796 #[test]
797 fn reconstruct_not_sequence_start() {
798 let len = rand::random::<usize>() % (1024 * 1024) + 1;
799 let data = random_bytes(len);
800 let ns = Namespace::const_v0(rand::random());
801 let mut shares = Blob::new(ns, data, AppVersion::V2)
802 .unwrap()
803 .to_shares()
804 .unwrap();
805
806 shares[0].as_mut()[NS_SIZE] &= 0b11111110;
808
809 assert!(matches!(
810 Blob::reconstruct(&shares, AppVersion::V2),
811 Err(Error::ExpectedShareWithSequenceStart)
812 ));
813 }
814
815 #[test]
816 fn reconstruct_reserved_namespace() {
817 for ns in (0..255).flat_map(|n| {
818 let mut v0 = [0; NS_ID_SIZE];
819 *v0.last_mut().unwrap() = n;
820 let mut v255 = [0xff; NS_ID_SIZE];
821 *v255.last_mut().unwrap() = n;
822
823 [Namespace::new_v0(&v0), Namespace::new_v255(&v255)]
824 }) {
825 let len = (rand::random::<usize>() % 1023 + 1) * 2;
826 let data = random_bytes(len);
827 let shares = Blob::new(ns.unwrap(), data, AppVersion::V2)
828 .unwrap()
829 .to_shares()
830 .unwrap();
831
832 assert!(matches!(
833 Blob::reconstruct(&shares, AppVersion::V2),
834 Err(Error::UnexpectedReservedNamespace)
835 ));
836 }
837 }
838
839 #[test]
840 fn reconstruct_not_enough_shares() {
841 let len = rand::random::<usize>() % 1024 * 1024 + 2048;
842 let data = random_bytes(len);
843 let ns = Namespace::const_v0(rand::random());
844 let shares = Blob::new(ns, data, AppVersion::V2)
845 .unwrap()
846 .to_shares()
847 .unwrap();
848
849 assert!(matches!(
850 Blob::reconstruct(&shares[..2], AppVersion::V2),
852 Err(Error::MissingShares)
853 ));
854 }
855
856 #[test]
857 fn reconstruct_inconsistent_share_version() {
858 let len = rand::random::<usize>() % (1024 * 1024) + 512;
859 let data = random_bytes(len);
860 let ns = Namespace::const_v0(rand::random());
861 let mut shares = Blob::new(ns, data, AppVersion::V2)
862 .unwrap()
863 .to_shares()
864 .unwrap();
865
866 shares[1].as_mut()[NS_SIZE] = 0b11111110;
868
869 assert!(matches!(
870 Blob::reconstruct(&shares, AppVersion::V2),
871 Err(Error::BlobSharesMetadataMismatch(..))
872 ));
873 }
874
875 #[test]
876 fn reconstruct_inconsistent_namespace() {
877 let len = rand::random::<usize>() % (1024 * 1024) + 512;
878 let data = random_bytes(len);
879 let ns = Namespace::const_v0(rand::random());
880 let ns2 = Namespace::const_v0(rand::random());
881 let mut shares = Blob::new(ns, data, AppVersion::V2)
882 .unwrap()
883 .to_shares()
884 .unwrap();
885
886 shares[1].as_mut()[..NS_SIZE].copy_from_slice(ns2.as_bytes());
888
889 assert!(matches!(
890 Blob::reconstruct(&shares, AppVersion::V2),
891 Err(Error::BlobSharesMetadataMismatch(..))
892 ));
893 }
894
895 #[test]
896 fn reconstruct_unexpected_sequence_start() {
897 let len = rand::random::<usize>() % (1024 * 1024) + 512;
898 let data = random_bytes(len);
899 let ns = Namespace::const_v0(rand::random());
900 let mut shares = Blob::new(ns, data, AppVersion::V2)
901 .unwrap()
902 .to_shares()
903 .unwrap();
904
905 shares[1].as_mut()[NS_SIZE] |= 0b00000001;
907
908 assert!(matches!(
909 Blob::reconstruct(&shares, AppVersion::V2),
910 Err(Error::UnexpectedSequenceStart)
911 ));
912 }
913
914 #[test]
915 fn reconstruct_all() {
916 let blobs: Vec<_> = (0..rand::random::<usize>() % 16 + 3)
917 .map(|_| {
918 let len = rand::random::<usize>() % (1024 * 1024) + 512;
919 let data = random_bytes(len);
920 let ns = Namespace::const_v0(rand::random());
921 Blob::new(ns, data, AppVersion::V2).unwrap()
922 })
923 .collect();
924
925 let shares: Vec<_> = blobs
926 .iter()
927 .flat_map(|blob| blob.to_shares().unwrap())
928 .collect();
929 let reconstructed = Blob::reconstruct_all(&shares, AppVersion::V2).unwrap();
930
931 assert_eq!(blobs, reconstructed);
932 }
933}