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