1use std::fmt::Debug;
5
6use itertools::Itertools;
7use serde::{Deserialize, Serialize};
8use solana_sdk::{clock::Slot, hash::HASH_BYTES, pubkey::Pubkey};
9use thiserror::Error;
10
11use crate::{
12 accounts_delta_hash::inclusion::InclusionProof,
13 bank_hash::BankHashProof,
14 blob::{BlobProof, BlobProofError},
15 blober_account_state::{self, BloberAccountStateProof},
16};
17
18#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
33pub struct CompoundInclusionProof {
34 slot: Slot,
35 blob_proofs: Vec<BlobProof>,
36 blober_account_state_proof: BloberAccountStateProof,
37 blober_inclusion_proof: InclusionProof,
38 pub bank_hash_proof: BankHashProof,
39}
40
41pub struct ProofBlob<A: AsRef<[u8]> = Vec<u8>> {
44 pub blob: Pubkey,
45 pub data: Option<A>,
46}
47
48impl ProofBlob<Vec<u8>> {
49 pub fn empty(blob: Pubkey) -> Self {
50 Self { blob, data: None }
51 }
52}
53
54impl<A: AsRef<[u8]>> ProofBlob<A> {
55 pub fn blob_size(&self) -> Option<usize> {
56 let blob = self.data.as_ref()?;
57 Some(blob.as_ref().len())
58 }
59}
60
61impl<A: AsRef<[u8]>> Debug for ProofBlob<A> {
62 #[cfg_attr(test, mutants::skip)]
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("Blob")
65 .field("blob", &self.blob)
66 .field("blob_size", &self.blob_size())
67 .finish()
68 }
69}
70
71#[derive(Debug, Clone, Error)]
73pub enum CompoundInclusionProofError {
74 #[error("The number of blobs does not match the number of proofs")]
75 InvalidNumberOfBlobs,
76 #[error(
77 "The number of blob accounts does not match the number of proofs, some blobs are missing"
78 )]
79 MissingBlobs,
80 #[error("The inclusion proof is not for the blober account")]
81 IncludedAccountNotBlober,
82 #[error(
83 "The proof is for a different blockhash than the one provided, expected {expected:?}, found {found:?}"
84 )]
85 BlockHashMismatch {
86 expected: solana_sdk::hash::Hash,
87 found: solana_sdk::hash::Hash,
88 },
89 #[error(
90 "Blob {index} does not match the provided hash, expected {expected:?}, found {found:?}"
91 )]
92 BlobHashMismatch {
93 index: usize,
94 expected: solana_sdk::hash::Hash,
95 found: solana_sdk::hash::Hash,
96 },
97 #[error(
98 "Blob {index} does not match the provided blob size, expected {expected}, found {found}"
99 )]
100 BlobSizeMismatch {
101 index: usize,
102 expected: usize,
103 found: usize,
104 },
105 #[error("Blob {index} has invalid blob account data: 0x{}", hex::encode(.bytes))]
106 InvalidBlobAccountData { index: usize, bytes: Vec<u8> },
107 #[error("The computed accounts delta hash does not match the provided value")]
108 AccountsDeltaHashMismatch,
109 #[error(transparent)]
110 BloberAccountState(#[from] blober_account_state::BloberAccountStateError),
111 #[error(transparent)]
112 Blob(#[from] BlobProofError),
113}
114
115impl CompoundInclusionProof {
116 pub fn new(
118 slot: Slot,
119 blob_proofs: Vec<BlobProof>,
120 blober_account_state_proof: BloberAccountStateProof,
121 blober_inclusion_proof: InclusionProof,
122 bank_hash_proof: BankHashProof,
123 ) -> Self {
124 Self {
125 slot,
126 blob_proofs,
127 blober_account_state_proof,
128 blober_inclusion_proof,
129 bank_hash_proof,
130 }
131 }
132
133 #[tracing::instrument(skip_all, err(Debug), fields(slot = %self.slot, blober = %blober, blockhash = %blockhash))]
135 pub fn verify(
136 &self,
137 blober: Pubkey,
138 blockhash: solana_sdk::hash::Hash,
139 blobs: &[ProofBlob<impl AsRef<[u8]>>],
140 ) -> Result<(), CompoundInclusionProofError> {
141 if blobs.len() != self.blob_proofs.len() {
142 return Err(CompoundInclusionProofError::InvalidNumberOfBlobs);
143 }
144 if self.blober_account_state_proof.blob_accounts.len() != self.blob_proofs.len() {
145 return Err(CompoundInclusionProofError::MissingBlobs);
146 }
147 if self.blober_inclusion_proof.account_pubkey != blober {
148 return Err(CompoundInclusionProofError::IncludedAccountNotBlober);
149 }
150
151 if self.bank_hash_proof.blockhash != blockhash {
152 return Err(CompoundInclusionProofError::BlockHashMismatch {
153 expected: blockhash,
154 found: self.bank_hash_proof.blockhash,
155 });
156 }
157
158 let blob_accounts = &self.blober_account_state_proof.blob_accounts;
159
160 for (index, ((blob, blob_proof), blob_account)) in blobs
161 .iter()
162 .zip_eq(&self.blob_proofs)
163 .zip_eq(blob_accounts)
164 .enumerate()
165 {
166 let (blob_account_digest, blob_account_blob_size) =
167 if blob_account.1.len() >= HASH_BYTES {
168 blob_account.1.split_at(HASH_BYTES)
169 } else {
170 return Err(CompoundInclusionProofError::InvalidBlobAccountData {
171 index,
172 bytes: blob_account.1.clone(),
173 });
174 };
175 let blob_account_digest: [u8; 32] = blob_account_digest.try_into().map_err(|_| {
176 CompoundInclusionProofError::InvalidBlobAccountData {
177 index,
178 bytes: blob_account.1.clone(),
179 }
180 })?;
181 let blob_account_blob_size: [u8; 4] =
182 blob_account_blob_size.try_into().map_err(|_| {
183 CompoundInclusionProofError::InvalidBlobAccountData {
184 index,
185 bytes: blob_account.1.clone(),
186 }
187 })?;
188 let blob_account_blob_size = u32::from_le_bytes(blob_account_blob_size) as usize;
189
190 if let Some(blob_size) = blob.blob_size() {
191 if blob_account_blob_size != blob_size {
192 return Err(CompoundInclusionProofError::BlobSizeMismatch {
193 index,
194 expected: blob_account_blob_size,
195 found: blob_size,
196 });
197 }
198 }
199
200 if blob_account_digest != blob_proof.digest {
201 return Err(CompoundInclusionProofError::BlobHashMismatch {
202 index,
203 expected: solana_sdk::hash::Hash::new_from_array(blob_proof.digest),
204 found: solana_sdk::hash::Hash::new_from_array(blob_account_digest),
205 });
206 }
207
208 if let Some(data) = &blob.data {
209 blob_proof.verify(data.as_ref())?;
210 }
211 }
212
213 self.blober_account_state_proof
214 .verify(&self.blober_inclusion_proof.account_data.data)?;
215
216 if !self
217 .blober_inclusion_proof
218 .verify(self.bank_hash_proof.accounts_delta_hash)
219 {
220 return Err(CompoundInclusionProofError::AccountsDeltaHashMismatch);
221 }
222
223 Ok(())
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use std::collections::HashSet;
230
231 use anchor_lang::{AnchorSerialize, Discriminator};
232 use arbtest::arbtest;
233 use blober_account_state::BlobAccount;
234 use data_anchor_blober::{
235 BLOB_DATA_END, BLOB_DATA_START, CHUNK_SIZE, hash_blob, initial_hash,
236 state::{blob::Blob, blober::Blober},
237 };
238 use solana_sdk::{
239 account::Account, native_token::LAMPORTS_PER_SOL, slot_hashes::SlotHashes, system_program,
240 sysvar, sysvar::SysvarId,
241 };
242
243 use super::*;
244 use crate::{
245 accounts_delta_hash::{
246 AccountMerkleTree,
247 testing::{ArbAccount, ArbKeypair},
248 },
249 bank_hash::BankHashProof,
250 testing::arbitrary_hash,
251 };
252
253 #[test]
254 fn inclusion_construction_single_blob() {
255 arbtest(|u| {
256 let blob: &[u8] = u.arbitrary()?;
258 if blob.is_empty() {
259 return Ok(());
261 } else if blob.len() > u16::MAX as usize {
262 return Ok(());
264 }
265 let mut chunks = blob
266 .chunks(CHUNK_SIZE as usize)
267 .enumerate()
268 .map(|(i, chunk)| (i as u16, chunk))
269 .collect::<Vec<_>>();
270 for _ in 0..10 {
272 let a = u.choose_index(chunks.len())?;
273 let b = u.choose_index(chunks.len())?;
274 chunks.swap(a, b);
275 }
276
277 let blober = u.arbitrary::<ArbKeypair>()?.pubkey();
278
279 let mut unmodified = true;
280
281 let mut blob_account: (ArbKeypair, ArbAccount) = u.arbitrary()?;
282
283 blob_account.1.data = if u.ratio(1, 10)? {
285 unmodified = false;
286 u.arbitrary::<[u8; BLOB_DATA_END]>()?.to_vec()
287 } else {
288 let mut blob_pda = Blob::new(0, 0, blob.len() as u32, 0);
289 for (chunk_index, chunk_data) in &chunks {
290 blob_pda.insert(0, *chunk_index, chunk_data);
291 }
292 [Blob::DISCRIMINATOR.to_vec(), blob_pda.try_to_vec().unwrap()]
293 .into_iter()
294 .flatten()
295 .collect()
296 };
297
298 let blob_proof = BlobProof::new(&chunks);
299
300 let mut slot = u.arbitrary()?;
302 if slot == 0 {
303 slot = 1;
305 }
306 let mut source_accounts: Vec<_> = vec![BlobAccount(
307 blob_account.0.pubkey(),
308 blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END].to_vec(),
309 )];
310
311 if u.ratio(1, 10)? {
312 source_accounts.push(BlobAccount(
314 u.arbitrary::<ArbKeypair>()?.pubkey(),
315 u.arbitrary()?,
316 ));
317 unmodified = false;
318 }
319
320 let blober_account_state_proof =
321 blober_account_state::BloberAccountStateProof::new(slot, source_accounts.clone());
322
323 let other_accounts: Vec<(ArbKeypair, ArbAccount)> = u.arbitrary()?;
325
326 let mut tree = AccountMerkleTree::builder(
327 [blober, sysvar::slot_hashes::ID]
328 .into_iter()
329 .chain(other_accounts.iter().map(|(kp, _)| kp.pubkey()))
330 .collect(),
331 );
332 for (pubkey, account) in other_accounts.iter() {
333 tree.insert(pubkey.pubkey(), account.clone().into());
334 }
335 let mut blober_data = Blober {
337 caller: data_anchor_blober::id(),
338 hash: initial_hash(),
339 slot: 0,
340 };
341 if u.ratio(1, 10)? {
342 let new_slot = u.arbitrary()?;
343 if new_slot != 0 {
344 unmodified = new_slot == slot;
345 slot = new_slot;
346 }
347 }
348
349 if u.ratio(9, 10)? {
350 blober_data.store_hash(
351 &hash_blob(
352 &blob_account.0.pubkey().to_bytes().into(),
353 &blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END],
354 ),
355 slot,
356 );
357 } else {
358 unmodified = false;
360 }
361 let blober_account = Account {
362 lamports: LAMPORTS_PER_SOL,
363 data: [
364 Blober::DISCRIMINATOR.to_vec(),
365 blober_data.try_to_vec().unwrap(),
366 ]
367 .into_iter()
368 .flatten()
369 .collect(),
370 owner: system_program::ID,
371 executable: false,
372 rent_epoch: 0,
373 };
374
375 let (tree, accounts_delta_hash_proof) =
376 if !other_accounts.is_empty() && u.ratio(1, 10)? {
377 let tree = tree.build();
379 let false_accounts_delta_hash_proof = tree.unchecked_inclusion_proof(
380 u.choose_index(other_accounts.len())?,
381 &blober,
382 &blober_account,
383 );
384 unmodified = false;
385 (tree, false_accounts_delta_hash_proof)
386 } else if !other_accounts.is_empty() && u.ratio(1, 10)? {
387 let keypair = &u.choose(&other_accounts)?.0;
389 let tree = tree.build();
390 let accounts_delta_hash_proof = tree.prove_inclusion(keypair.pubkey()).unwrap();
391 unmodified = keypair.pubkey() == blober;
392 (tree, accounts_delta_hash_proof)
393 } else {
394 tree.insert(blober, blober_account);
395 let tree = tree.build();
396 let accounts_delta_hash_proof = tree.prove_inclusion(blober).unwrap();
397 (tree, accounts_delta_hash_proof)
398 };
399
400 let writable_blob_account = blob_account.0.pubkey();
402 let read_only_blober_account = data_anchor_blober::id().to_bytes().into();
403
404 let parent_bankhash = arbitrary_hash(u)?;
406 let root = tree.root();
407 let signature_count = u.arbitrary()?;
408 let blockhash = arbitrary_hash(u)?;
409
410 let mut bank_hash_proof =
411 BankHashProof::new(parent_bankhash, root, signature_count, blockhash);
412
413 if u.ratio(1, 10)? {
414 let new_root = arbitrary_hash(u)?;
416 unmodified = new_root == root;
417 bank_hash_proof.accounts_delta_hash = new_root;
418 }
419
420 let mut trusted_vote_authorities: Vec<ArbKeypair> = vec![
422 arbitrary::Arbitrary::arbitrary(u)?,
423 arbitrary::Arbitrary::arbitrary(u)?,
424 ];
425 trusted_vote_authorities.sort_by_key(|pk| pk.pubkey());
426
427 let required_votes = 1 + u.choose_index(trusted_vote_authorities.len())?;
428
429 unmodified = unmodified
430 && required_votes <= trusted_vote_authorities.len()
431 && required_votes > 0;
432
433 let proven_slot = u.arbitrary()?;
434 let proven_hash = bank_hash_proof.hash();
435
436 let slot_hashes = u
437 .arbitrary_iter::<(u64, [u8; 32])>()?
438 .map(|tup| Ok((tup?.0, solana_sdk::hash::Hash::new_from_array(tup?.1))))
439 .chain([Ok((proven_slot, proven_hash))].into_iter())
441 .collect::<Result<HashSet<_>, _>>()?
442 .into_iter()
443 .collect::<Vec<_>>();
444 if slot_hashes.is_empty() {
445 return Ok(());
446 }
447
448 let slot_hashes = SlotHashes::new(&slot_hashes);
449
450 let mut slot_hashes_account: Account = u.arbitrary::<ArbAccount>()?.into();
451 slot_hashes_account.data = bincode::serialize(&slot_hashes).unwrap();
452
453 let mut slot_hashes_tree =
454 AccountMerkleTree::builder([read_only_blober_account].into_iter().collect());
455 slot_hashes_tree.insert(SlotHashes::id(), slot_hashes_account);
456
457 let blob_proofs = if u.ratio(1, 10)? {
459 unmodified = false;
461 Vec::new()
462 } else if u.ratio(1, 10)? {
463 unmodified = false;
465 vec![blob_proof.clone(), blob_proof]
466 } else {
467 vec![blob_proof]
468 };
469
470 let compound_inclusion_proof = CompoundInclusionProof::new(
471 proven_slot,
472 blob_proofs,
473 blober_account_state_proof,
474 accounts_delta_hash_proof,
475 bank_hash_proof,
476 );
477
478 let blobs = if u.ratio(1, 10)? {
479 unmodified = false;
481 Vec::new()
482 } else if u.ratio(1, 10)? {
483 unmodified = false;
485 vec![blob.to_vec(), blob.to_vec()]
486 } else if u.ratio(1, 10)? {
487 let mut new_blob = Vec::new();
489 while new_blob.len() < blob.len() {
490 new_blob.push(u.arbitrary()?);
491 }
492 unmodified = unmodified && new_blob == blob;
493 vec![new_blob]
494 } else if u.ratio(1, 10)? {
495 let mut new_blob = Vec::new();
497 while new_blob.len() == blob.len() {
498 new_blob = u.arbitrary()?;
499 }
500 unmodified = unmodified && new_blob == blob;
501 vec![new_blob]
502 } else {
503 vec![blob.to_vec()]
504 };
505
506 let blobs = blobs
507 .into_iter()
508 .map(|data| ProofBlob {
509 blob: writable_blob_account,
510 data: Some(data),
511 })
512 .collect::<Vec<_>>();
513
514 dbg!(&compound_inclusion_proof);
515 if unmodified {
516 compound_inclusion_proof
517 .verify(
518 blober,
519 bank_hash_proof.blockhash,
522 &blobs,
523 )
524 .unwrap();
525 let empty_blobs: Vec<_> = blobs
527 .into_iter()
528 .map(|b| ProofBlob::empty(b.blob))
529 .collect();
530 compound_inclusion_proof
531 .verify(blober, bank_hash_proof.blockhash, &empty_blobs)
532 .unwrap();
533 roundtrip_serialization(compound_inclusion_proof);
534 } else {
535 compound_inclusion_proof
536 .verify(blober, bank_hash_proof.blockhash, &blobs)
537 .unwrap_err();
538 roundtrip_serialization(compound_inclusion_proof);
539 }
540
541 Ok(())
542 })
543 .size_max(100_000_000);
544 }
545
546 fn roundtrip_serialization(proof: CompoundInclusionProof) {
547 let serialized_json = serde_json::to_string(&proof).unwrap();
548 let deserialized_json: CompoundInclusionProof =
549 serde_json::from_str(&serialized_json).unwrap();
550 assert_eq!(proof, deserialized_json);
551
552 let serialized_bincode = bincode::serialize(&proof).unwrap();
553 let deserialized_bincode: CompoundInclusionProof =
554 bincode::deserialize(&serialized_bincode).unwrap();
555 assert_eq!(proof, deserialized_bincode);
556
557 let serialized_risc0_zkvm = risc0_zkvm::serde::to_vec(&proof).unwrap();
558 let deserialized_risc0_zkvm: CompoundInclusionProof =
559 risc0_zkvm::serde::from_slice(&serialized_risc0_zkvm).unwrap();
560 assert_eq!(proof, deserialized_risc0_zkvm);
561 }
562}