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