1use 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#[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
45pub 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#[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 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 #[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 let blob: &[u8] = u.arbitrary()?;
259 if blob.is_empty() {
260 return Ok(());
262 } else if blob.len() > u16::MAX as usize {
263 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 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 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 let mut slot = u.arbitrary()?;
303 if slot == 0 {
304 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 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 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 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 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 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 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 let writable_blob_account = blob_account.0.pubkey();
403 let read_only_blober_account = data_anchor_blober::id().to_bytes().into();
404
405 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 let new_root = arbitrary_hash(u)?;
417 unmodified = new_root == root;
418 bank_hash_proof.accounts_delta_hash = new_root;
419 }
420
421 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 .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 let blob_proofs = if u.ratio(1, 10)? {
460 unmodified = false;
462 Vec::new()
463 } else if u.ratio(1, 10)? {
464 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 unmodified = false;
482 Vec::new()
483 } else if u.ratio(1, 10)? {
484 unmodified = false;
486 vec![blob.to_vec(), blob.to_vec()]
487 } else if u.ratio(1, 10)? {
488 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 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 bank_hash_proof.blockhash,
523 &blobs,
524 )
525 .unwrap();
526 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}