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 hash_blob, initial_hash,
236 state::{blob::Blob, blober::Blober},
237 BLOB_DATA_END, BLOB_DATA_START, CHUNK_SIZE,
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 testing::{ArbAccount, ArbKeypair},
248 AccountMerkleTree,
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, solana_sdk::hash::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}