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