data_anchor_proofs/compound/
completeness.rs

1//! This proof module contains the logic for verifying "completeness" in the sense that there are
2//! no blobs in a specific Solana block.
3
4use serde::{Deserialize, Serialize};
5use solana_sdk::{clock::Slot, hash::Hash, pubkey::Pubkey};
6use thiserror::Error;
7
8use crate::{
9    accounts_delta_hash::exclusion::{ExclusionProof, ExclusionProofError},
10    bank_hash::BankHashProof,
11};
12
13/// A proof that there are no blobs in a specific Solana block.
14///
15/// This proof consists of two parts:
16/// 1. An [accounts delta hash proof][`ExclusionProof`] that proves that
17///    the accounts_delta_hash does *not* include the [`blober`] account.
18/// 2. A [bank hash proof][`BankHashProof`] that proves that the root hash of the accounts_delta_hash
19///    is the same as the root in the bank hash.
20///
21/// The proof can then be verified by supplying the blockhash of the block in which the [`blober`]
22/// was invoked.
23#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
24pub struct CompoundCompletenessProof {
25    slot: Slot,
26    blober_exclusion_proof: ExclusionProof,
27    pub bank_hash_proof: BankHashProof,
28}
29
30/// Failures that can occur when verifying a [`CompoundCompletenessProof`].
31#[derive(Debug, Clone, Error)]
32pub enum CompoundCompletenessProofError {
33    #[error("The exclusion proof is not for the blober account")]
34    ExcludedAccountNotBlober,
35    #[error(
36        "The proof is for a different blockhash than the one provided, expected {expected:?}, found {found:?}"
37    )]
38    BlockHashMismatch { expected: Hash, found: Hash },
39    #[error(transparent)]
40    AccountsDeltaHash(#[from] ExclusionProofError),
41}
42
43impl CompoundCompletenessProof {
44    /// Creates a completeness proof.
45    pub fn new(
46        slot: Slot,
47        blober_exclusion_proof: ExclusionProof,
48        bank_hash_proof: BankHashProof,
49    ) -> Self {
50        Self {
51            slot,
52            blober_exclusion_proof,
53            bank_hash_proof,
54        }
55    }
56
57    /// Verifies that there are no blobs in a specific Solana block.
58    #[tracing::instrument(skip_all, err(Debug), fields(slot = %self.slot, blober = %blober, blockhash = %blockhash))]
59    pub fn verify(
60        &self,
61        blober: Pubkey,
62        blockhash: Hash,
63    ) -> Result<(), CompoundCompletenessProofError> {
64        if let Some(excluded) = self.blober_exclusion_proof.excluded() {
65            // If the exclusion proof is for a specific account, it should be for the blober account.
66            if excluded != &blober {
67                return Err(CompoundCompletenessProofError::ExcludedAccountNotBlober);
68            }
69        } // If it's for the empty case (no accounts updated), there's nothing to check.
70
71        if self.bank_hash_proof.blockhash != blockhash {
72            return Err(CompoundCompletenessProofError::BlockHashMismatch {
73                expected: blockhash,
74                found: self.bank_hash_proof.blockhash,
75            });
76        }
77
78        self.blober_exclusion_proof
79            .verify(self.bank_hash_proof.accounts_delta_hash)?;
80
81        Ok(())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use std::collections::HashSet;
88
89    use arbtest::arbtest;
90    use solana_sdk::{
91        account::Account,
92        hash::Hash,
93        slot_hashes::SlotHashes,
94        sysvar::{self, SysvarId},
95    };
96
97    use super::*;
98    use crate::{
99        accounts_delta_hash::{
100            AccountMerkleTree,
101            exclusion::{ExclusionProof, left::ExclusionLeftProof},
102            testing::{ArbAccount, ArbKeypair, UnwrapOrArbitrary, choose_or_generate},
103        },
104        testing::arbitrary_hash,
105    };
106
107    #[test]
108    fn completeness_construction() {
109        arbtest(|u| {
110            let accounts: Vec<(ArbKeypair, ArbAccount)> = u.arbitrary()?;
111            let (leftmost_index, leftmost) = choose_or_generate(u, &accounts)?;
112
113            let blober = u.arbitrary::<ArbKeypair>()?.pubkey();
114
115            let mut solana_accounts = accounts
116                .into_iter()
117                .map(|(keypair, account)| (keypair.pubkey(), account.into()))
118                .collect::<Vec<_>>();
119            let is_excluded = if u.ratio(1, 2)? {
120                solana_accounts.push((
121                    blober,
122                    Account {
123                        // The actual contents of the blober doesn't matter for this proof - if it's
124                        // not excluded, the proof is invalid.
125                        ..Default::default()
126                    },
127                ));
128                false
129            } else {
130                true
131            };
132            solana_accounts.sort_by_key(|(pubkey, _)| *pubkey);
133
134            // Used later in the test, but must be marked as an important pubkey in advance for that to work.
135            let not_blober = u.arbitrary::<ArbKeypair>()?.pubkey();
136            let mut tree = AccountMerkleTree::builder([blober, not_blober].into_iter().collect());
137            for (pubkey, account) in solana_accounts.iter() {
138                tree.insert(*pubkey, account.clone());
139            }
140            let tree = tree.build();
141
142            let parent_bankhash = arbitrary_hash(u)?;
143            let signature_count = u.arbitrary()?;
144            let blockhash = arbitrary_hash(u)?;
145            let root = tree.root();
146            let bank_hash_proof =
147                BankHashProof::new(parent_bankhash, root, signature_count, blockhash);
148
149            let mut trusted_vote_authorities: Vec<ArbKeypair> = vec![
150                arbitrary::Arbitrary::arbitrary(u)?,
151                arbitrary::Arbitrary::arbitrary(u)?,
152            ];
153            trusted_vote_authorities.sort_by_key(|pk| pk.pubkey());
154
155            let required_votes = 1 + u.choose_index(trusted_vote_authorities.len())?;
156
157            let votes_valid =
158                required_votes <= trusted_vote_authorities.len() && required_votes > 0;
159
160            let proven_slot = u.arbitrary()?;
161            let proven_hash = bank_hash_proof.hash();
162
163            let slot_hashes = u
164                .arbitrary_iter::<(u64, [u8; 32])>()?
165                .map(|tup| Ok((tup?.0, Hash::new_from_array(tup?.1))))
166                // Include the hash that's being proven.
167                .chain([Ok((proven_slot, proven_hash))].into_iter())
168                .collect::<Result<HashSet<_>, _>>()?
169                .into_iter()
170                .collect::<Vec<_>>();
171            if slot_hashes.is_empty() {
172                return Ok(());
173            }
174
175            let slot_hashes = SlotHashes::new(&slot_hashes);
176
177            let mut slot_hashes_account: Account = u.arbitrary::<ArbAccount>()?.into();
178            slot_hashes_account.data = bincode::serialize(&slot_hashes).unwrap();
179
180            let mut slot_hashes_tree =
181                AccountMerkleTree::builder([sysvar::slot_hashes::ID].into_iter().collect());
182            slot_hashes_tree.insert(SlotHashes::id(), slot_hashes_account);
183
184            if is_excluded {
185                let exclusion_proof = tree.prove_exclusion(blober).unwrap();
186                let proof = CompoundCompletenessProof::new(
187                    proven_slot,
188                    exclusion_proof.clone(),
189                    bank_hash_proof,
190                );
191                if u.ratio(1, 5)? {
192                    // Wrong accounts_delta_hash, but account *is* actually excluded.
193                    let accounts_delta_hash = arbitrary_hash(u)?;
194                    let bank_hash_proof = BankHashProof::new(
195                        parent_bankhash,
196                        accounts_delta_hash,
197                        signature_count,
198                        blockhash,
199                    );
200                    let proof = CompoundCompletenessProof::new(
201                        proven_slot,
202                        exclusion_proof,
203                        bank_hash_proof,
204                    );
205                    proof.verify(blober, bank_hash_proof.blockhash).unwrap_err();
206                    roundtrip_serialization(proof);
207                } else if !solana_accounts.is_empty() && u.ratio(1, 5)? {
208                    // The excluded account is not the blober account.
209                    if not_blober != blober {
210                        dbg!(&tree, &not_blober.to_string());
211                        if let Some(exclusion_proof) = tree.prove_exclusion(not_blober) {
212                            let proof = CompoundCompletenessProof::new(
213                                proven_slot,
214                                exclusion_proof,
215                                bank_hash_proof,
216                            );
217                            proof.verify(blober, bank_hash_proof.blockhash).unwrap_err();
218                            roundtrip_serialization(proof);
219                        }
220                    }
221                } else if !votes_valid {
222                    // Something is wrong with the multi vote proof.
223                    proof.verify(blober, bank_hash_proof.blockhash).unwrap_err();
224                    roundtrip_serialization(proof);
225                } else {
226                    dbg!(&proof);
227                    proof
228                        .verify(
229                            blober,
230                            // In real code this value wouldn't come from the proof itself,
231                            // instead it would be sourced from a third-party Solana node.
232                            bank_hash_proof.blockhash,
233                        )
234                        .unwrap();
235                    roundtrip_serialization(proof);
236                };
237            } else {
238                // It doesn't really matter which false exclusion proof is used here, it could be
239                // exhaustive but it's not worth the readability of the test.
240                let false_exclusion_proof = ExclusionProof::ExclusionLeft(ExclusionLeftProof {
241                    excluded: blober,
242                    leftmost: tree.unchecked_inclusion_proof(
243                        leftmost_index.unwrap_or_arbitrary(u)?,
244                        &leftmost.0.pubkey(),
245                        &leftmost.1.clone().into(),
246                    ),
247                });
248                let proof = CompoundCompletenessProof::new(
249                    proven_slot,
250                    false_exclusion_proof,
251                    bank_hash_proof,
252                );
253                dbg!(&solana_accounts, &proof);
254                proof.verify(blober, bank_hash_proof.blockhash).unwrap_err();
255                roundtrip_serialization(proof);
256            }
257
258            Ok(())
259        })
260        .size_max(100_000_000);
261    }
262
263    fn roundtrip_serialization(proof: CompoundCompletenessProof) {
264        let serialized_json = serde_json::to_string(&proof).unwrap();
265        let deserialized_json: CompoundCompletenessProof =
266            serde_json::from_str(&serialized_json).unwrap();
267        assert_eq!(proof, deserialized_json);
268
269        let serialized_bincode = bincode::serialize(&proof).unwrap();
270        let deserialized_bincode: CompoundCompletenessProof =
271            bincode::deserialize(&serialized_bincode).unwrap();
272        assert_eq!(proof, deserialized_bincode);
273
274        let serialized_risc0_zkvm = risc0_zkvm::serde::to_vec(&proof).unwrap();
275        let deserialized_risc0_zkvm: CompoundCompletenessProof =
276            risc0_zkvm::serde::from_slice(&serialized_risc0_zkvm).unwrap();
277        assert_eq!(proof, deserialized_risc0_zkvm);
278    }
279}