1use std::fmt::Debug;
5
6use anchor_lang::{
7 prelude::Pubkey,
8 solana_program::hash::{HASH_BYTES, Hash},
9};
10use data_anchor_blober::hash_blob;
11use itertools::Itertools;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use crate::{
16 blob::{BlobProof, BlobProofError},
17 blober_account_state::{
18 self, BloberAccountStateError, BloberAccountStateProof, BloberAccountStateResult,
19 get_blober_hash, merge_all_hashes,
20 },
21};
22
23#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
35pub struct CompoundInclusionProof {
36 pub blob_proofs: Vec<BlobProof>,
37 pub blober_pubkey: Pubkey,
38 pub blober_account_state_proof: BloberAccountStateProof,
39}
40
41#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct ProofBlob<A: AsRef<[u8]> = Vec<u8>> {
45 pub blob: Pubkey,
46 pub data: Option<A>,
47}
48
49impl ProofBlob<Vec<u8>> {
50 pub fn empty(blob: Pubkey) -> Self {
51 Self { blob, data: None }
52 }
53
54 pub fn hash_blob(&self) -> [u8; HASH_BYTES] {
55 hash_blob(&self.blob, self.data.as_ref().map_or(&[], AsRef::as_ref))
56 }
57}
58
59impl<A: AsRef<[u8]>> ProofBlob<A> {
60 pub fn blob_size(&self) -> Option<usize> {
61 let blob = self.data.as_ref()?;
62 Some(blob.as_ref().len())
63 }
64}
65
66impl<A: AsRef<[u8]>> Debug for ProofBlob<A> {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.debug_struct("Blob")
69 .field("blob", &self.blob)
70 .field("blob_size", &self.blob_size())
71 .finish()
72 }
73}
74
75#[derive(Debug, Clone, Error)]
77pub enum CompoundInclusionProofError {
78 #[error("The number of blobs does not match the number of proofs")]
79 InvalidNumberOfBlobs,
80 #[error(
81 "The number of blob accounts does not match the number of proofs, some blobs are missing"
82 )]
83 MissingBlobs,
84 #[error("The inclusion proof is not for the blober account")]
85 IncludedAccountNotBlober,
86 #[error(
87 "The proof is for a different blockhash than the one provided, expected {expected:?}, found {found:?}"
88 )]
89 BlockHashMismatch { expected: Hash, found: Hash },
90 #[error(
91 "Blob {index} does not match the provided hash, expected {expected:?}, found {found:?}"
92 )]
93 BlobHashMismatch {
94 index: usize,
95 expected: Hash,
96 found: Hash,
97 },
98 #[error(
99 "Blob {index} does not match the provided blob size, expected {expected}, found {found}"
100 )]
101 BlobSizeMismatch {
102 index: usize,
103 expected: usize,
104 found: usize,
105 },
106 #[error("Blob {index} has invalid blob account data: 0x{}", hex::encode(.bytes))]
107 InvalidBlobAccountData { index: usize, bytes: Vec<u8> },
108 #[error("The computed accounts delta hash does not match the provided value")]
109 AccountsDeltaHashMismatch,
110 #[error(transparent)]
111 BloberAccountState(#[from] blober_account_state::BloberAccountStateError),
112 #[error(transparent)]
113 Blob(#[from] BlobProofError),
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct VerifyArgs {
118 pub blober: Pubkey,
119 pub blober_state: Vec<u8>,
120 pub blobs: Vec<ProofBlob<Vec<u8>>>,
121}
122
123impl VerifyArgs {
124 pub fn hash_blobs(&self) -> [u8; HASH_BYTES] {
125 merge_all_hashes(self.blobs.iter().map(ProofBlob::hash_blob))
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct VerifyArgsCommitment {
131 pub blober_hash: [u8; HASH_BYTES],
132}
133
134impl TryFrom<VerifyArgs> for VerifyArgsCommitment {
135 type Error = BloberAccountStateError;
136
137 fn try_from(args: VerifyArgs) -> Result<Self, Self::Error> {
138 Ok(Self {
139 blober_hash: get_blober_hash(&args.blober_state)?,
140 })
141 }
142}
143
144impl TryFrom<&VerifyArgs> for VerifyArgsCommitment {
145 type Error = BloberAccountStateError;
146
147 fn try_from(args: &VerifyArgs) -> Result<Self, Self::Error> {
148 Ok(Self {
149 blober_hash: get_blober_hash(&args.blober_state)?,
150 })
151 }
152}
153
154impl VerifyArgs {
155 pub fn into_commitment(&self) -> BloberAccountStateResult<VerifyArgsCommitment> {
156 VerifyArgsCommitment::try_from(self)
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct CompoundInclusionProofCommitment {
162 pub blober_initial_hash: [u8; HASH_BYTES],
163}
164
165impl From<CompoundInclusionProof> for CompoundInclusionProofCommitment {
166 fn from(proof: CompoundInclusionProof) -> Self {
167 Self {
168 blober_initial_hash: proof.blober_account_state_proof.initial_hash,
169 }
170 }
171}
172
173impl From<&CompoundInclusionProof> for CompoundInclusionProofCommitment {
174 fn from(proof: &CompoundInclusionProof) -> Self {
175 Self {
176 blober_initial_hash: proof.blober_account_state_proof.initial_hash,
177 }
178 }
179}
180
181impl CompoundInclusionProof {
182 pub fn new(
184 blob_proofs: Vec<BlobProof>,
185 blober_pubkey: Pubkey,
186 blober_account_state_proof: BloberAccountStateProof,
187 ) -> Self {
188 Self {
189 blob_proofs,
190 blober_pubkey,
191 blober_account_state_proof,
192 }
193 }
194
195 pub fn into_commitment(&self) -> CompoundInclusionProofCommitment {
196 CompoundInclusionProofCommitment::from(self)
197 }
198
199 pub fn target_slot(&self) -> u64 {
200 self.blober_account_state_proof.target_slot()
201 }
202
203 pub fn hash_proofs(&self) -> [u8; HASH_BYTES] {
204 merge_all_hashes(self.blob_proofs.iter().map(BlobProof::hash_proof))
205 }
206
207 #[tracing::instrument(skip_all, err(Debug), fields(blober = %blober))]
209 pub fn verify(
210 &self,
211 blober: Pubkey,
212 blober_state: &[u8],
213 blobs: &[ProofBlob<impl AsRef<[u8]>>],
214 ) -> Result<(), CompoundInclusionProofError> {
215 if blobs.len() != self.blob_proofs.len() {
216 return Err(CompoundInclusionProofError::InvalidNumberOfBlobs);
217 }
218 let blob_count = self.blober_account_state_proof.blobs().count();
219 if blob_count != self.blob_proofs.len() {
220 return Err(CompoundInclusionProofError::MissingBlobs);
221 }
222 if self.blober_pubkey != blober {
223 return Err(CompoundInclusionProofError::IncludedAccountNotBlober);
224 }
225
226 let blob_accounts = self.blober_account_state_proof.blobs().collect::<Vec<_>>();
227
228 for (index, ((blob, blob_proof), blob_account)) in blobs
229 .iter()
230 .zip_eq(&self.blob_proofs)
231 .zip_eq(blob_accounts)
232 .enumerate()
233 {
234 let digest = blob_account.verify(blob)?;
235
236 if digest != blob_proof.digest {
237 return Err(CompoundInclusionProofError::BlobHashMismatch {
238 index,
239 expected: Hash::new_from_array(blob_proof.digest),
240 found: Hash::new_from_array(digest),
241 });
242 }
243
244 if let Some(data) = &blob.data {
245 blob_proof.verify(data.as_ref())?;
246 }
247 }
248
249 self.blober_account_state_proof.verify(blober_state)?;
250
251 Ok(())
252 }
253}
254
255#[cfg(test)]
256mod tests {
257
258 use std::collections::BTreeMap;
259
260 use anchor_lang::{AnchorSerialize, Discriminator, solana_program::clock::Slot};
261 use arbtest::arbtest;
262 use blober_account_state::{BlobAccount, merge_all_hashes};
263 use data_anchor_blober::{
264 BLOB_DATA_END, BLOB_DATA_START, CHUNK_SIZE, initial_hash,
265 state::{blob::Blob, blober::Blober},
266 };
267 use solana_signer::Signer;
268
269 use super::*;
270 use crate::testing::{ArbAccount, ArbKeypair};
271
272 fn roundtrip_serialization(proof: CompoundInclusionProof) {
273 let serialized_json = serde_json::to_string(&proof).unwrap();
274 let deserialized_json: CompoundInclusionProof =
275 serde_json::from_str(&serialized_json).unwrap();
276 assert_eq!(proof, deserialized_json);
277
278 let serialized_bincode = bincode::serialize(&proof).unwrap();
279 let deserialized_bincode: CompoundInclusionProof =
280 bincode::deserialize(&serialized_bincode).unwrap();
281 assert_eq!(proof, deserialized_bincode);
282 }
283
284 #[test]
285 fn inclusion_construction_no_changes() {
286 let slot = 1;
287 let blober = Pubkey::new_unique();
288 let blober_account_state_proof =
289 BloberAccountStateProof::new(initial_hash(), slot, Default::default());
290 let compound_inclusion_proof =
291 CompoundInclusionProof::new(Vec::new(), blober, blober_account_state_proof);
292 let blober_state = Blober {
293 caller: Pubkey::new_unique(),
294 namespace: "test".to_string(),
295 hash: initial_hash(),
296 slot: 1,
297 };
298 let state_bytes = [
299 Blober::DISCRIMINATOR,
300 blober_state.try_to_vec().unwrap().as_ref(),
301 ]
302 .concat();
303 let uploads: Vec<ProofBlob<Vec<u8>>> = Vec::new();
304 let verification = compound_inclusion_proof.verify(blober, &state_bytes, &uploads);
305 assert!(
306 verification.is_ok(),
307 "Expected verification to succeed, but it failed: {verification:?}",
308 );
309 }
310
311 #[test]
312 fn inclusion_construction_single_blob() {
313 arbtest(|u| {
314 let blob: &[u8] = u.arbitrary()?;
316 if blob.is_empty() {
317 return Ok(());
319 } else if blob.len() > u16::MAX as usize {
320 return Ok(());
322 }
323 let mut chunks = blob
324 .chunks(CHUNK_SIZE as usize)
325 .enumerate()
326 .map(|(i, chunk)| (i as u16, chunk))
327 .collect::<Vec<_>>();
328 for _ in 0..10 {
330 let a = u.choose_index(chunks.len())?;
331 let b = u.choose_index(chunks.len())?;
332 chunks.swap(a, b);
333 }
334
335 let blober = u.arbitrary::<ArbKeypair>()?.pubkey();
336
337 let mut unmodified = true;
338
339 let mut blob_account: (ArbKeypair, ArbAccount) = u.arbitrary()?;
340
341 blob_account.1.data = if u.ratio(1, 10)? {
343 unmodified = false;
344 u.arbitrary::<[u8; BLOB_DATA_END]>()?.to_vec()
345 } else {
346 let mut blob_pda = Blob::new(0, 0, blob.len() as u32, 0);
347 for (chunk_index, chunk_data) in &chunks {
348 blob_pda.insert(0, *chunk_index, chunk_data);
349 }
350 [Blob::DISCRIMINATOR.to_vec(), blob_pda.try_to_vec().unwrap()]
351 .into_iter()
352 .flatten()
353 .collect()
354 };
355
356 let blob_proof = BlobProof::new(&chunks);
357
358 let mut slot = u.arbitrary()?;
360 if slot == 0 {
361 slot = 1;
363 }
364 let mut source_accounts: Vec<_> = vec![BlobAccount::new(
365 blob_account.0.pubkey(),
366 blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END].to_vec(),
367 )];
368
369 if u.ratio(1, 10)? {
370 source_accounts.push(BlobAccount::new(
372 u.arbitrary::<ArbKeypair>()?.pubkey(),
373 u.arbitrary()?,
374 ));
375 unmodified = false;
376 }
377
378 let blober_account_state_proof = BloberAccountStateProof::new(
379 initial_hash(),
380 slot,
381 [(slot + 1, source_accounts.clone())].into_iter().collect(),
382 );
383
384 let mut blober_data = Blober {
386 caller: data_anchor_blober::id(),
387 hash: initial_hash(),
388 slot: 0,
389 namespace: "".to_string(),
390 };
391 if u.ratio(1, 10)? {
392 let new_slot = u.arbitrary()?;
393 if new_slot >= slot && new_slot != 0 {
394 unmodified = new_slot == slot;
395 slot = new_slot;
396 }
397 }
398
399 if u.ratio(9, 10)? {
400 blober_data.store_hash(&source_accounts[0].hash_blob(), slot + 1);
401 } else {
402 unmodified = false;
404 }
405
406 let writable_blob_account = blob_account.0.pubkey();
408
409 let blob_proofs = if u.ratio(1, 10)? {
411 unmodified = false;
413 Vec::new()
414 } else if u.ratio(1, 10)? {
415 unmodified = false;
417 vec![blob_proof.clone(), blob_proof]
418 } else {
419 vec![blob_proof]
420 };
421
422 let compound_inclusion_proof =
423 CompoundInclusionProof::new(blob_proofs, blober, blober_account_state_proof);
424
425 let blobs = if u.ratio(1, 10)? {
426 unmodified = false;
428 Vec::new()
429 } else if u.ratio(1, 10)? {
430 unmodified = false;
432 vec![blob.to_vec(), blob.to_vec()]
433 } else if u.ratio(1, 10)? {
434 let mut new_blob = Vec::new();
436 while new_blob.len() < blob.len() {
437 new_blob.push(u.arbitrary()?);
438 }
439 unmodified = unmodified && new_blob == blob;
440 vec![new_blob]
441 } else if u.ratio(1, 10)? {
442 let mut new_blob = Vec::new();
444 while new_blob.len() == blob.len() {
445 new_blob = u.arbitrary()?;
446 }
447 unmodified = unmodified && new_blob == blob;
448 vec![new_blob]
449 } else {
450 vec![blob.to_vec()]
451 };
452
453 let blobs = blobs
454 .into_iter()
455 .map(|data| ProofBlob {
456 blob: writable_blob_account,
457 data: Some(data),
458 })
459 .collect::<Vec<_>>();
460
461 dbg!(&compound_inclusion_proof);
462 let blober_state = [
463 Blober::DISCRIMINATOR,
464 blober_data.try_to_vec().unwrap().as_ref(),
465 ]
466 .concat();
467 if unmodified {
468 compound_inclusion_proof
469 .verify(blober, &blober_state, &blobs)
470 .unwrap();
471 let empty_blobs: Vec<_> = blobs
473 .into_iter()
474 .map(|b| ProofBlob::empty(b.blob))
475 .collect();
476 compound_inclusion_proof
477 .verify(blober, &blober_state, &empty_blobs)
478 .unwrap();
479 roundtrip_serialization(compound_inclusion_proof);
480 } else {
481 compound_inclusion_proof
482 .verify(blober, &blober_state, &blobs)
483 .unwrap_err();
484 roundtrip_serialization(compound_inclusion_proof);
485 }
486
487 Ok(())
488 })
489 .size_max(100_000_000);
490 }
491
492 #[test]
493 fn inclusion_construction_multiple_slots_multiple_blobs() {
494 arbtest(|u| {
495 let slots: u64 = u.int_in_range(1..=20)?;
496
497 let mut blobs =
498 BTreeMap::<Slot, Vec<(ProofBlob<Vec<u8>>, BlobProof, BlobAccount)>>::new();
499
500 let mut unmodified = true;
501
502 for slot in 1..=slots {
503 let blob_count: u64 = u.int_in_range(0..=5)?;
504 let mut slot_blobs = Vec::with_capacity(blob_count as usize);
505
506 for _ in 0..blob_count {
507 let mut blob = vec![0u8; u.int_in_range(0..=u16::MAX)? as usize];
508 u.fill_buffer(&mut blob)?;
509
510 if blob.is_empty() {
511 continue;
513 }
514
515 let mut chunks = blob
516 .chunks(CHUNK_SIZE as usize)
517 .enumerate()
518 .map(|(i, chunk)| (i as u16, chunk))
519 .collect::<Vec<_>>();
520
521 for _ in 0..10 {
523 let a = u.choose_index(chunks.len())?;
524 let b = u.choose_index(chunks.len())?;
525 chunks.swap(a, b);
526 }
527
528 let blob_address = u.arbitrary::<ArbKeypair>()?.pubkey();
529 let mut blob_state = Blob::new(slot, 0, blob.len() as u32, 0);
530 for (chunk_index, chunk_data) in &chunks {
531 blob_state.insert(slot, *chunk_index, chunk_data);
532 }
533
534 let proof_blob = if u.ratio(1, 10)? {
535 let modified_blob = u.arbitrary::<Vec<u8>>()?;
536 if modified_blob != blob {
537 unmodified = false;
538 }
539 ProofBlob {
540 blob: blob_address,
541 data: Some(modified_blob),
542 }
543 } else {
544 ProofBlob {
545 blob: blob_address,
546 data: Some(blob.clone()),
547 }
548 };
549
550 let blob_proof = if u.ratio(1, 10)? {
551 let mut new_chunks = chunks.clone();
552 for _ in 0..10 {
553 let a = u.choose_index(chunks.len())?;
554 let b = u.choose_index(chunks.len())?;
555 new_chunks.swap(a, b);
556 }
557 if new_chunks != chunks {
558 unmodified = false;
559 }
560
561 BlobProof::new(&new_chunks)
562 } else {
563 BlobProof::new(&chunks)
564 };
565
566 let blob_account_state = [
567 Blob::DISCRIMINATOR.to_vec(),
568 blob_state.try_to_vec().unwrap(),
569 ]
570 .concat()[BLOB_DATA_START..BLOB_DATA_END]
571 .to_vec();
572 let blob_account = if u.ratio(1, 10)? {
573 let new_key = u.arbitrary::<ArbKeypair>()?.pubkey();
574 let new_blob_account_state = u.arbitrary::<Vec<u8>>()?;
575
576 if new_key != blob_address || new_blob_account_state != blob_account_state {
577 unmodified = false;
578 }
579
580 BlobAccount::new(new_key, new_blob_account_state)
581 } else {
582 BlobAccount::new(blob_address, blob_account_state)
583 };
584
585 slot_blobs.push((proof_blob, blob_proof, blob_account));
586 }
587
588 blobs.insert(slot + 1, slot_blobs);
590 }
591
592 let blober_pubkey = u.arbitrary::<ArbKeypair>()?.pubkey();
593
594 let mut blob_accounts = if u.ratio(1, 10)? {
595 let mut blob_accounts_map = BTreeMap::new();
597 for (slot, blob_data) in blobs.iter() {
598 if u.ratio(1, 10)? && !blob_data.is_empty() {
599 unmodified = false;
601 continue;
602 }
603
604 let mut slot_blob_accounts = Vec::new();
605
606 for (_, _, account) in blob_data {
607 if u.ratio(1, 10)? {
608 unmodified = false;
610 continue;
611 } else {
612 slot_blob_accounts.push(account.clone());
613 }
614 }
615
616 if u.ratio(1, 10)? {
617 unmodified = false;
619 let insert_index = u.choose_index(slot_blob_accounts.len())?;
620 slot_blob_accounts.insert(
621 insert_index,
622 BlobAccount::new(u.arbitrary::<ArbKeypair>()?.pubkey(), u.arbitrary()?),
623 );
624 }
625
626 if !slot_blob_accounts.is_empty() {
627 blob_accounts_map.insert(*slot, slot_blob_accounts);
628 }
629 }
630
631 blob_accounts_map
632 } else {
633 blobs
634 .iter()
635 .map(|(slot, accounts)| {
636 (
637 *slot,
638 accounts
639 .iter()
640 .map(|(_, _, account)| account.clone())
641 .collect(),
642 )
643 })
644 .collect()
645 };
646
647 blob_accounts.retain(|_, accounts| !accounts.is_empty());
648
649 let blober_account_state_proof =
650 BloberAccountStateProof::new(initial_hash(), 1, blob_accounts);
651
652 let blob_proofs = if u.ratio(1, 10)? {
653 let mut blob_proofs = Vec::new();
654 for slot_blobs in blobs.values() {
655 for (_, proof, _) in slot_blobs {
656 if u.ratio(1, 10)? {
657 unmodified = false;
659 continue;
660 }
661 blob_proofs.push(proof.clone());
662 }
663 }
664 blob_proofs
665 } else {
666 blobs
667 .values()
668 .flat_map(|blobs| {
669 blobs
670 .iter()
671 .map(|(_, proof, _)| proof.clone())
672 .collect_vec()
673 })
674 .collect_vec()
675 };
676
677 let compound_inclusion_proof =
678 CompoundInclusionProof::new(blob_proofs, blober_pubkey, blober_account_state_proof);
679
680 let caller = u.arbitrary::<ArbKeypair>()?.pubkey();
681 let namespace = u.arbitrary::<String>()?;
682
683 let hash = if u.ratio(1, 10)? {
684 let mut hashes = vec![initial_hash()];
685 for slot_blobs in blobs.values() {
686 for (_, _, account) in slot_blobs {
687 if u.ratio(1, 10)? {
688 unmodified = false;
690 continue;
691 }
692 hashes.push(account.hash_blob());
693 }
694 }
695 merge_all_hashes(hashes.into_iter())
696 } else {
697 merge_all_hashes(
698 std::iter::once(initial_hash()).chain(blobs.values().flat_map(|slot_blobs| {
699 slot_blobs.iter().map(|(_, _, account)| account.hash_blob())
700 })),
701 )
702 };
703
704 let expected_slot = blobs
705 .iter()
706 .filter_map(|(slot, blobs)| (!blobs.is_empty()).then_some(slot))
707 .max()
708 .cloned()
709 .unwrap_or(1);
710 let slot = if u.ratio(1, 10)? {
711 let new_slot = u.arbitrary::<Slot>()?;
712
713 if new_slot != expected_slot {
714 unmodified = false;
715 }
716
717 new_slot
718 } else {
719 expected_slot
720 };
721
722 let blober = Blober {
723 caller,
724 namespace,
725 hash,
726 slot,
727 };
728
729 let blober_state =
730 [Blober::DISCRIMINATOR, blober.try_to_vec().unwrap().as_ref()].concat();
731 let blobs = blobs
732 .values()
733 .flat_map(|blobs| blobs.iter().map(|(blob, _, _)| blob.clone()).collect_vec())
734 .collect_vec();
735
736 dbg!(&compound_inclusion_proof);
737 dbg!(&blober_pubkey);
738 dbg!(&blober.slot);
739 dbg!(&blobs);
740
741 let verification_result =
742 compound_inclusion_proof.verify(blober_pubkey, &blober_state, &blobs);
743
744 if unmodified {
745 verification_result.unwrap();
746 } else {
747 verification_result.unwrap_err();
748 }
749
750 roundtrip_serialization(compound_inclusion_proof);
751
752 Ok(())
753 })
754 .size_max(100_000_000);
755 }
756}