nitro_da_proofs/compound/
completeness.rs1use 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#[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#[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 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 #[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 excluded != &blober {
70 return Err(CompoundCompletenessProofError::ExcludedAccountNotBlober);
71 }
72 } 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 ..Default::default()
124 },
125 ));
126 false
127 } else {
128 true
129 };
130 solana_accounts.sort_by_key(|(pubkey, _)| *pubkey);
131
132 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 .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 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 if not_blober != blober {
208 dbg!(&tree, ¬_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 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 bank_hash_proof.blockhash,
231 )
232 .unwrap();
233 roundtrip_serialization(proof);
234 };
235 } else {
236 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}