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    #[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/// Failures that can occur when verifying a [`CompoundInclusionProof`].
77#[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    /// Creates an inclusion proof.
184    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    /// Verifies that a specific Solana block contains the provided blobs, and that no blobs have been excluded.
209    #[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            // ------------------------- Blob -------------------------
316            let blob: &[u8] = u.arbitrary()?;
317            if blob.is_empty() {
318                // Empty blob, invalid test.
319                return Ok(());
320            } else if blob.len() > u16::MAX as usize {
321                // Blob too large, invalid test.
322                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            // Swap a few chunks around to simulate out-of-order submission.
330            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            // 10% chance that there's invalid data, 90% chance that it's the original
343            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            // ------------------------- Blober account state -------------------------
360            let mut slot = u.arbitrary()?;
361            if slot == 0 {
362                // Slot 0 doesn't work for the contract and will never happen outside of tests.
363                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                // Add an extra source account that hasn't actually called the blober, I.E. false proof.
372                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            // Always include the blober account.
386            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                // The blober account was not invoked.
404                unmodified = false;
405            }
406
407            // ----------------------- Payer proof -----------------------------------------
408            let writable_blob_account = blob_account.0.pubkey();
409
410            // ------------------------- Compound proof -------------------------
411            let blob_proofs = if u.ratio(1, 10)? {
412                // Missing blob proof.
413                unmodified = false;
414                Vec::new()
415            } else if u.ratio(1, 10)? {
416                // Extra blob proof.
417                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                // No blobs.
428                unmodified = false;
429                Vec::new()
430            } else if u.ratio(1, 10)? {
431                // An extra blob.
432                unmodified = false;
433                vec![blob.to_vec(), blob.to_vec()]
434            } else if u.ratio(1, 10)? {
435                // A single blob, the right size, but the wrong contents.
436                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                // A single blob, but the wrong size.
444                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                // It should also be possible to verify the proof without the blob data.
473                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                        // Empty blob, skip.
513                        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                    // Swap a few chunks around to simulate out-of-order submission.
523                    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                // We want to start insertions at slot 2
590                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                // Add an extra blob account that hasn't actually called the blober, I.E. false proof.
597                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                        // Skip this slot.
601                        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                            // Skip this account.
610                            unmodified = false;
611                            continue;
612                        } else {
613                            slot_blob_accounts.push(account.clone());
614                        }
615                    }
616
617                    if u.ratio(1, 10)? {
618                        // Add an extra account that hasn't called the blober.
619                        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                            // Skip this proof.
659                            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                            // Skip this account.
690                            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}