data_anchor_proofs/
compound.rs

1//! This proof module contains the logic for verifying "inclusion" in the sense that a specific
2//! Solana block contains blobs, and that there are no other blobs in the block.
3
4use 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/// A proof that a specific Solana block contains blobs, and that there are no other blobs in the block.
24///
25/// This proof consists of four parts:
26/// 1. A list of [blob proofs][`BlobProof`] that prove that the blobs uploaded to the [`blober`] program
27///    hash to the given blob digest.
28/// 2. The public key of the [`blober`] PDA that was invoked to commite the blobs to.
29/// 3. A [blober account state proof][`BloberAccountStateProof`] that proves that the [`blober`] was
30///    invoked exactly as many times as there are blobs.
31///
32/// The proof can then be verified by supplying the blockhash of the block in which the [`blober`] was
33/// invoked, as well as the blobs of data which were published.
34#[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/// All data relevant for proving a single blob. If the `chunks` field is `None`, the blob itself will
42/// not be checked, but the rest of the proof will still be verified.
43#[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/// Failures that can occur when verifying a [`CompoundInclusionProof`].
76#[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    /// Creates an inclusion proof.
183    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    /// Verifies that a specific Solana block contains the provided blobs, and that no blobs have been excluded.
208    #[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            // ------------------------- Blob -------------------------
315            let blob: &[u8] = u.arbitrary()?;
316            if blob.is_empty() {
317                // Empty blob, invalid test.
318                return Ok(());
319            } else if blob.len() > u16::MAX as usize {
320                // Blob too large, invalid test.
321                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            // Swap a few chunks around to simulate out-of-order submission.
329            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            // 10% chance that there's invalid data, 90% chance that it's the original
342            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            // ------------------------- Blober account state -------------------------
359            let mut slot = u.arbitrary()?;
360            if slot == 0 {
361                // Slot 0 doesn't work for the contract and will never happen outside of tests.
362                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                // Add an extra source account that hasn't actually called the blober, I.E. false proof.
371                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            // Always include the blober account.
385            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                // The blober account was not invoked.
403                unmodified = false;
404            }
405
406            // ----------------------- Payer proof -----------------------------------------
407            let writable_blob_account = blob_account.0.pubkey();
408
409            // ------------------------- Compound proof -------------------------
410            let blob_proofs = if u.ratio(1, 10)? {
411                // Missing blob proof.
412                unmodified = false;
413                Vec::new()
414            } else if u.ratio(1, 10)? {
415                // Extra blob proof.
416                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                // No blobs.
427                unmodified = false;
428                Vec::new()
429            } else if u.ratio(1, 10)? {
430                // An extra blob.
431                unmodified = false;
432                vec![blob.to_vec(), blob.to_vec()]
433            } else if u.ratio(1, 10)? {
434                // A single blob, the right size, but the wrong contents.
435                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                // A single blob, but the wrong size.
443                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                // It should also be possible to verify the proof without the blob data.
472                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                        // Empty blob, skip.
512                        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                    // Swap a few chunks around to simulate out-of-order submission.
522                    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                // We want to start insertions at slot 2
589                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                // Add an extra blob account that hasn't actually called the blober, I.E. false proof.
596                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                        // Skip this slot.
600                        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                            // Skip this account.
609                            unmodified = false;
610                            continue;
611                        } else {
612                            slot_blob_accounts.push(account.clone());
613                        }
614                    }
615
616                    if u.ratio(1, 10)? {
617                        // Add an extra account that hasn't called the blober.
618                        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                            // Skip this proof.
658                            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                            // Skip this account.
689                            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}