data_anchor_proofs/compound/
inclusion.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 itertools::Itertools;
7use serde::{Deserialize, Serialize};
8use solana_sdk::{clock::Slot, hash::HASH_BYTES, pubkey::Pubkey};
9use thiserror::Error;
10
11use crate::{
12    accounts_delta_hash::inclusion::InclusionProof,
13    bank_hash::BankHashProof,
14    blob::{BlobProof, BlobProofError},
15    blober_account_state::{self, BloberAccountStateProof},
16};
17
18/// A proof that a specific Solana block contains blobs, and that there are no other blobs in the block.
19///
20/// This proof consists of four parts:
21/// 1. A list of [blob proofs][`BlobProof`] that prove that the blobs uploaded to the [`blober`] program
22///    hash to the given blob digest.
23/// 2. A [blober account state proof][`BloberAccountStateProof`] that proves that the [`blober`] was
24///    invoked exactly as many times as there are blobs.
25/// 3. An [accounts delta hash proof][`InclusionProof`] that proves that
26///    the accounts_delta_hash *does* include the [`blober`] account.
27/// 4. A [bank hash proof][`BankHashProof`] that proves that the root hash of the accounts_delta_hash
28///    is the same as the root in the bank hash.
29///
30/// The proof can then be verified by supplying the blockhash of the block in which the [`blober`] was
31/// invoked, as well as the blobs of data which were published.
32#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
33pub struct CompoundInclusionProof {
34    slot: Slot,
35    blob_proofs: Vec<BlobProof>,
36    blober_account_state_proof: BloberAccountStateProof,
37    blober_inclusion_proof: InclusionProof,
38    pub bank_hash_proof: BankHashProof,
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.
43pub struct ProofBlob<A: AsRef<[u8]> = Vec<u8>> {
44    pub blob: Pubkey,
45    pub data: Option<A>,
46}
47
48impl ProofBlob<Vec<u8>> {
49    pub fn empty(blob: Pubkey) -> Self {
50        Self { blob, data: None }
51    }
52}
53
54impl<A: AsRef<[u8]>> ProofBlob<A> {
55    pub fn blob_size(&self) -> Option<usize> {
56        let blob = self.data.as_ref()?;
57        Some(blob.as_ref().len())
58    }
59}
60
61impl<A: AsRef<[u8]>> Debug for ProofBlob<A> {
62    #[cfg_attr(test, mutants::skip)]
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Blob")
65            .field("blob", &self.blob)
66            .field("blob_size", &self.blob_size())
67            .finish()
68    }
69}
70
71/// Failures that can occur when verifying a [`CompoundInclusionProof`].
72#[derive(Debug, Clone, Error)]
73pub enum CompoundInclusionProofError {
74    #[error("The number of blobs does not match the number of proofs")]
75    InvalidNumberOfBlobs,
76    #[error(
77        "The number of blob accounts does not match the number of proofs, some blobs are missing"
78    )]
79    MissingBlobs,
80    #[error("The inclusion proof is not for the blober account")]
81    IncludedAccountNotBlober,
82    #[error(
83        "The proof is for a different blockhash than the one provided, expected {expected:?}, found {found:?}"
84    )]
85    BlockHashMismatch {
86        expected: solana_sdk::hash::Hash,
87        found: solana_sdk::hash::Hash,
88    },
89    #[error(
90        "Blob {index} does not match the provided hash, expected {expected:?}, found {found:?}"
91    )]
92    BlobHashMismatch {
93        index: usize,
94        expected: solana_sdk::hash::Hash,
95        found: solana_sdk::hash::Hash,
96    },
97    #[error(
98        "Blob {index} does not match the provided blob size, expected {expected}, found {found}"
99    )]
100    BlobSizeMismatch {
101        index: usize,
102        expected: usize,
103        found: usize,
104    },
105    #[error("Blob {index} has invalid blob account data: 0x{}", hex::encode(.bytes))]
106    InvalidBlobAccountData { index: usize, bytes: Vec<u8> },
107    #[error("The computed accounts delta hash does not match the provided value")]
108    AccountsDeltaHashMismatch,
109    #[error(transparent)]
110    BloberAccountState(#[from] blober_account_state::BloberAccountStateError),
111    #[error(transparent)]
112    Blob(#[from] BlobProofError),
113}
114
115impl CompoundInclusionProof {
116    /// Creates an inclusion proof.
117    pub fn new(
118        slot: Slot,
119        blob_proofs: Vec<BlobProof>,
120        blober_account_state_proof: BloberAccountStateProof,
121        blober_inclusion_proof: InclusionProof,
122        bank_hash_proof: BankHashProof,
123    ) -> Self {
124        Self {
125            slot,
126            blob_proofs,
127            blober_account_state_proof,
128            blober_inclusion_proof,
129            bank_hash_proof,
130        }
131    }
132
133    /// Verifies that a specific Solana block contains the provided blobs, and that no blobs have been excluded.
134    #[tracing::instrument(skip_all, err(Debug), fields(slot = %self.slot, blober = %blober, blockhash = %blockhash))]
135    pub fn verify(
136        &self,
137        blober: Pubkey,
138        blockhash: solana_sdk::hash::Hash,
139        blobs: &[ProofBlob<impl AsRef<[u8]>>],
140    ) -> Result<(), CompoundInclusionProofError> {
141        if blobs.len() != self.blob_proofs.len() {
142            return Err(CompoundInclusionProofError::InvalidNumberOfBlobs);
143        }
144        if self.blober_account_state_proof.blob_accounts.len() != self.blob_proofs.len() {
145            return Err(CompoundInclusionProofError::MissingBlobs);
146        }
147        if self.blober_inclusion_proof.account_pubkey != blober {
148            return Err(CompoundInclusionProofError::IncludedAccountNotBlober);
149        }
150
151        if self.bank_hash_proof.blockhash != blockhash {
152            return Err(CompoundInclusionProofError::BlockHashMismatch {
153                expected: blockhash,
154                found: self.bank_hash_proof.blockhash,
155            });
156        }
157
158        let blob_accounts = &self.blober_account_state_proof.blob_accounts;
159
160        for (index, ((blob, blob_proof), blob_account)) in blobs
161            .iter()
162            .zip_eq(&self.blob_proofs)
163            .zip_eq(blob_accounts)
164            .enumerate()
165        {
166            let (blob_account_digest, blob_account_blob_size) =
167                if blob_account.1.len() >= HASH_BYTES {
168                    blob_account.1.split_at(HASH_BYTES)
169                } else {
170                    return Err(CompoundInclusionProofError::InvalidBlobAccountData {
171                        index,
172                        bytes: blob_account.1.clone(),
173                    });
174                };
175            let blob_account_digest: [u8; 32] = blob_account_digest.try_into().map_err(|_| {
176                CompoundInclusionProofError::InvalidBlobAccountData {
177                    index,
178                    bytes: blob_account.1.clone(),
179                }
180            })?;
181            let blob_account_blob_size: [u8; 4] =
182                blob_account_blob_size.try_into().map_err(|_| {
183                    CompoundInclusionProofError::InvalidBlobAccountData {
184                        index,
185                        bytes: blob_account.1.clone(),
186                    }
187                })?;
188            let blob_account_blob_size = u32::from_le_bytes(blob_account_blob_size) as usize;
189
190            if let Some(blob_size) = blob.blob_size() {
191                if blob_account_blob_size != blob_size {
192                    return Err(CompoundInclusionProofError::BlobSizeMismatch {
193                        index,
194                        expected: blob_account_blob_size,
195                        found: blob_size,
196                    });
197                }
198            }
199
200            if blob_account_digest != blob_proof.digest {
201                return Err(CompoundInclusionProofError::BlobHashMismatch {
202                    index,
203                    expected: solana_sdk::hash::Hash::new_from_array(blob_proof.digest),
204                    found: solana_sdk::hash::Hash::new_from_array(blob_account_digest),
205                });
206            }
207
208            if let Some(data) = &blob.data {
209                blob_proof.verify(data.as_ref())?;
210            }
211        }
212
213        self.blober_account_state_proof
214            .verify(&self.blober_inclusion_proof.account_data.data)?;
215
216        if !self
217            .blober_inclusion_proof
218            .verify(self.bank_hash_proof.accounts_delta_hash)
219        {
220            return Err(CompoundInclusionProofError::AccountsDeltaHashMismatch);
221        }
222
223        Ok(())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use std::collections::HashSet;
230
231    use anchor_lang::{AnchorSerialize, Discriminator};
232    use arbtest::arbtest;
233    use blober_account_state::BlobAccount;
234    use data_anchor_blober::{
235        BLOB_DATA_END, BLOB_DATA_START, CHUNK_SIZE, hash_blob, initial_hash,
236        state::{blob::Blob, blober::Blober},
237    };
238    use solana_sdk::{
239        account::Account, native_token::LAMPORTS_PER_SOL, slot_hashes::SlotHashes, system_program,
240        sysvar, sysvar::SysvarId,
241    };
242
243    use super::*;
244    use crate::{
245        accounts_delta_hash::{
246            AccountMerkleTree,
247            testing::{ArbAccount, ArbKeypair},
248        },
249        bank_hash::BankHashProof,
250        testing::arbitrary_hash,
251    };
252
253    #[test]
254    fn inclusion_construction_single_blob() {
255        arbtest(|u| {
256            // ------------------------- Blob -------------------------
257            let blob: &[u8] = u.arbitrary()?;
258            if blob.is_empty() {
259                // Empty blob, invalid test.
260                return Ok(());
261            } else if blob.len() > u16::MAX as usize {
262                // Blob too large, invalid test.
263                return Ok(());
264            }
265            let mut chunks = blob
266                .chunks(CHUNK_SIZE as usize)
267                .enumerate()
268                .map(|(i, chunk)| (i as u16, chunk))
269                .collect::<Vec<_>>();
270            // Swap a few chunks around to simulate out-of-order submission.
271            for _ in 0..10 {
272                let a = u.choose_index(chunks.len())?;
273                let b = u.choose_index(chunks.len())?;
274                chunks.swap(a, b);
275            }
276
277            let blober = u.arbitrary::<ArbKeypair>()?.pubkey();
278
279            let mut unmodified = true;
280
281            let mut blob_account: (ArbKeypair, ArbAccount) = u.arbitrary()?;
282
283            // 10% chance that there's invalid data, 90% chance that it's the original
284            blob_account.1.data = if u.ratio(1, 10)? {
285                unmodified = false;
286                u.arbitrary::<[u8; BLOB_DATA_END]>()?.to_vec()
287            } else {
288                let mut blob_pda = Blob::new(0, 0, blob.len() as u32, 0);
289                for (chunk_index, chunk_data) in &chunks {
290                    blob_pda.insert(0, *chunk_index, chunk_data);
291                }
292                [Blob::DISCRIMINATOR.to_vec(), blob_pda.try_to_vec().unwrap()]
293                    .into_iter()
294                    .flatten()
295                    .collect()
296            };
297
298            let blob_proof = BlobProof::new(&chunks);
299
300            // ------------------------- Blober account state -------------------------
301            let mut slot = u.arbitrary()?;
302            if slot == 0 {
303                // Slot 0 doesn't work for the contract and will never happen outside of tests.
304                slot = 1;
305            }
306            let mut source_accounts: Vec<_> = vec![BlobAccount(
307                blob_account.0.pubkey(),
308                blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END].to_vec(),
309            )];
310
311            if u.ratio(1, 10)? {
312                // Add an extra source account that hasn't actually called the blober, I.E. false proof.
313                source_accounts.push(BlobAccount(
314                    u.arbitrary::<ArbKeypair>()?.pubkey(),
315                    u.arbitrary()?,
316                ));
317                unmodified = false;
318            }
319
320            let blober_account_state_proof =
321                blober_account_state::BloberAccountStateProof::new(slot, source_accounts.clone());
322
323            // Accounts delta hash, starting with unrelated accounts.
324            let other_accounts: Vec<(ArbKeypair, ArbAccount)> = u.arbitrary()?;
325
326            let mut tree = AccountMerkleTree::builder(
327                [blober, sysvar::slot_hashes::ID]
328                    .into_iter()
329                    .chain(other_accounts.iter().map(|(kp, _)| kp.pubkey()))
330                    .collect(),
331            );
332            for (pubkey, account) in other_accounts.iter() {
333                tree.insert(pubkey.pubkey(), account.clone().into());
334            }
335            // Always include the blober account.
336            let mut blober_data = Blober {
337                caller: data_anchor_blober::id(),
338                hash: initial_hash(),
339                slot: 0,
340            };
341            if u.ratio(1, 10)? {
342                let new_slot = u.arbitrary()?;
343                if new_slot != 0 {
344                    unmodified = new_slot == slot;
345                    slot = new_slot;
346                }
347            }
348
349            if u.ratio(9, 10)? {
350                blober_data.store_hash(
351                    &hash_blob(
352                        &blob_account.0.pubkey().to_bytes().into(),
353                        &blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END],
354                    ),
355                    slot,
356                );
357            } else {
358                // The blober account was not invoked.
359                unmodified = false;
360            }
361            let blober_account = Account {
362                lamports: LAMPORTS_PER_SOL,
363                data: [
364                    Blober::DISCRIMINATOR.to_vec(),
365                    blober_data.try_to_vec().unwrap(),
366                ]
367                .into_iter()
368                .flatten()
369                .collect(),
370                owner: system_program::ID,
371                executable: false,
372                rent_epoch: 0,
373            };
374
375            let (tree, accounts_delta_hash_proof) =
376                if !other_accounts.is_empty() && u.ratio(1, 10)? {
377                    // The blober is never inserted into the tree.
378                    let tree = tree.build();
379                    let false_accounts_delta_hash_proof = tree.unchecked_inclusion_proof(
380                        u.choose_index(other_accounts.len())?,
381                        &blober,
382                        &blober_account,
383                    );
384                    unmodified = false;
385                    (tree, false_accounts_delta_hash_proof)
386                } else if !other_accounts.is_empty() && u.ratio(1, 10)? {
387                    // Valid inclusion proof but for the wrong account.
388                    let keypair = &u.choose(&other_accounts)?.0;
389                    let tree = tree.build();
390                    let accounts_delta_hash_proof = tree.prove_inclusion(keypair.pubkey()).unwrap();
391                    unmodified = keypair.pubkey() == blober;
392                    (tree, accounts_delta_hash_proof)
393                } else {
394                    tree.insert(blober, blober_account);
395                    let tree = tree.build();
396                    let accounts_delta_hash_proof = tree.prove_inclusion(blober).unwrap();
397                    (tree, accounts_delta_hash_proof)
398                };
399
400            // ----------------------- Payer proof -----------------------------------------
401            let writable_blob_account = blob_account.0.pubkey();
402            let read_only_blober_account = data_anchor_blober::id().to_bytes().into();
403
404            // ------------------------- Bank hash -------------------------
405            let parent_bankhash = arbitrary_hash(u)?;
406            let root = tree.root();
407            let signature_count = u.arbitrary()?;
408            let blockhash = arbitrary_hash(u)?;
409
410            let mut bank_hash_proof =
411                BankHashProof::new(parent_bankhash, root, signature_count, blockhash);
412
413            if u.ratio(1, 10)? {
414                // Not testing exhaustively here, just that anything is wrong with the bank hash proof.
415                let new_root = arbitrary_hash(u)?;
416                unmodified = new_root == root;
417                bank_hash_proof.accounts_delta_hash = new_root;
418            }
419
420            // ------------------------- Multi vote proof -------------------------
421            let mut trusted_vote_authorities: Vec<ArbKeypair> = vec![
422                arbitrary::Arbitrary::arbitrary(u)?,
423                arbitrary::Arbitrary::arbitrary(u)?,
424            ];
425            trusted_vote_authorities.sort_by_key(|pk| pk.pubkey());
426
427            let required_votes = 1 + u.choose_index(trusted_vote_authorities.len())?;
428
429            unmodified = unmodified
430                && required_votes <= trusted_vote_authorities.len()
431                && required_votes > 0;
432
433            let proven_slot = u.arbitrary()?;
434            let proven_hash = bank_hash_proof.hash();
435
436            let slot_hashes = u
437                .arbitrary_iter::<(u64, [u8; 32])>()?
438                .map(|tup| Ok((tup?.0, solana_sdk::hash::Hash::new_from_array(tup?.1))))
439                // Include the hash that's being proven.
440                .chain([Ok((proven_slot, proven_hash))].into_iter())
441                .collect::<Result<HashSet<_>, _>>()?
442                .into_iter()
443                .collect::<Vec<_>>();
444            if slot_hashes.is_empty() {
445                return Ok(());
446            }
447
448            let slot_hashes = SlotHashes::new(&slot_hashes);
449
450            let mut slot_hashes_account: Account = u.arbitrary::<ArbAccount>()?.into();
451            slot_hashes_account.data = bincode::serialize(&slot_hashes).unwrap();
452
453            let mut slot_hashes_tree =
454                AccountMerkleTree::builder([read_only_blober_account].into_iter().collect());
455            slot_hashes_tree.insert(SlotHashes::id(), slot_hashes_account);
456
457            // ------------------------- Compound proof -------------------------
458            let blob_proofs = if u.ratio(1, 10)? {
459                // Missing blob proof.
460                unmodified = false;
461                Vec::new()
462            } else if u.ratio(1, 10)? {
463                // Extra blob proof.
464                unmodified = false;
465                vec![blob_proof.clone(), blob_proof]
466            } else {
467                vec![blob_proof]
468            };
469
470            let compound_inclusion_proof = CompoundInclusionProof::new(
471                proven_slot,
472                blob_proofs,
473                blober_account_state_proof,
474                accounts_delta_hash_proof,
475                bank_hash_proof,
476            );
477
478            let blobs = if u.ratio(1, 10)? {
479                // No blobs.
480                unmodified = false;
481                Vec::new()
482            } else if u.ratio(1, 10)? {
483                // An extra blob.
484                unmodified = false;
485                vec![blob.to_vec(), blob.to_vec()]
486            } else if u.ratio(1, 10)? {
487                // A single blob, the right size, but the wrong contents.
488                let mut new_blob = Vec::new();
489                while new_blob.len() < blob.len() {
490                    new_blob.push(u.arbitrary()?);
491                }
492                unmodified = unmodified && new_blob == blob;
493                vec![new_blob]
494            } else if u.ratio(1, 10)? {
495                // A single blob, but the wrong size.
496                let mut new_blob = Vec::new();
497                while new_blob.len() == blob.len() {
498                    new_blob = u.arbitrary()?;
499                }
500                unmodified = unmodified && new_blob == blob;
501                vec![new_blob]
502            } else {
503                vec![blob.to_vec()]
504            };
505
506            let blobs = blobs
507                .into_iter()
508                .map(|data| ProofBlob {
509                    blob: writable_blob_account,
510                    data: Some(data),
511                })
512                .collect::<Vec<_>>();
513
514            dbg!(&compound_inclusion_proof);
515            if unmodified {
516                compound_inclusion_proof
517                    .verify(
518                        blober,
519                        // In real code this value wouldn't come from the proof itself,
520                        // instead it would be sourced from a third-party Solana node.
521                        bank_hash_proof.blockhash,
522                        &blobs,
523                    )
524                    .unwrap();
525                // It should also be possible to verify the proof without the blob data.
526                let empty_blobs: Vec<_> = blobs
527                    .into_iter()
528                    .map(|b| ProofBlob::empty(b.blob))
529                    .collect();
530                compound_inclusion_proof
531                    .verify(blober, bank_hash_proof.blockhash, &empty_blobs)
532                    .unwrap();
533                roundtrip_serialization(compound_inclusion_proof);
534            } else {
535                compound_inclusion_proof
536                    .verify(blober, bank_hash_proof.blockhash, &blobs)
537                    .unwrap_err();
538                roundtrip_serialization(compound_inclusion_proof);
539            }
540
541            Ok(())
542        })
543        .size_max(100_000_000);
544    }
545
546    fn roundtrip_serialization(proof: CompoundInclusionProof) {
547        let serialized_json = serde_json::to_string(&proof).unwrap();
548        let deserialized_json: CompoundInclusionProof =
549            serde_json::from_str(&serialized_json).unwrap();
550        assert_eq!(proof, deserialized_json);
551
552        let serialized_bincode = bincode::serialize(&proof).unwrap();
553        let deserialized_bincode: CompoundInclusionProof =
554            bincode::deserialize(&serialized_bincode).unwrap();
555        assert_eq!(proof, deserialized_bincode);
556
557        let serialized_risc0_zkvm = risc0_zkvm::serde::to_vec(&proof).unwrap();
558        let deserialized_risc0_zkvm: CompoundInclusionProof =
559            risc0_zkvm::serde::from_slice(&serialized_risc0_zkvm).unwrap();
560        assert_eq!(proof, deserialized_risc0_zkvm);
561    }
562}