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) = blob_account.1.split_at(HASH_BYTES);
167 let blob_account_digest: [u8; 32] = blob_account_digest.try_into().map_err(|_| {
168 CompoundInclusionProofError::InvalidBlobAccountData {
169 index,
170 bytes: blob_account.1.clone(),
171 }
172 })?;
173 let blob_account_blob_size: [u8; 4] =
174 blob_account_blob_size.try_into().map_err(|_| {
175 CompoundInclusionProofError::InvalidBlobAccountData {
176 index,
177 bytes: blob_account.1.clone(),
178 }
179 })?;
180 let blob_account_blob_size = u32::from_le_bytes(blob_account_blob_size) as usize;
181
182 if let Some(blob_size) = blob.blob_size() {
183 if blob_account_blob_size != blob_size {
184 return Err(CompoundInclusionProofError::BlobSizeMismatch {
185 index,
186 expected: blob_account_blob_size,
187 found: blob_size,
188 });
189 }
190 }
191
192 if blob_account_digest != blob_proof.digest {
193 return Err(CompoundInclusionProofError::BlobHashMismatch {
194 index,
195 expected: solana_sdk::hash::Hash::new_from_array(blob_proof.digest),
196 found: solana_sdk::hash::Hash::new_from_array(blob_account_digest),
197 });
198 }
199
200 if let Some(data) = &blob.data {
201 blob_proof.verify(data.as_ref())?;
202 }
203 }
204
205 self.blober_account_state_proof
206 .verify(&self.blober_inclusion_proof.account_data.data)?;
207
208 if !self
209 .blober_inclusion_proof
210 .verify(self.bank_hash_proof.accounts_delta_hash)
211 {
212 return Err(CompoundInclusionProofError::AccountsDeltaHashMismatch);
213 }
214
215 Ok(())
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use std::collections::HashSet;
222
223 use anchor_lang::{AnchorSerialize, Discriminator};
224 use arbtest::arbtest;
225 use blober_account_state::BlobAccount;
226 use nitro_da_blober::{
227 BLOB_DATA_END, BLOB_DATA_START, CHUNK_SIZE, hash_blob, initial_hash,
228 state::{blob::Blob, blober::Blober},
229 };
230 use solana_sdk::{
231 account::Account, native_token::LAMPORTS_PER_SOL, slot_hashes::SlotHashes, system_program,
232 sysvar, sysvar::SysvarId,
233 };
234
235 use super::*;
236 use crate::{
237 accounts_delta_hash::{
238 AccountMerkleTree,
239 testing::{ArbAccount, ArbKeypair},
240 },
241 bank_hash::BankHashProof,
242 testing::arbitrary_hash,
243 };
244
245 #[test]
246 fn inclusion_construction_single_blob() {
247 arbtest(|u| {
248 let blob: &[u8] = u.arbitrary()?;
250 if blob.is_empty() {
251 return Ok(());
253 } else if blob.len() > u16::MAX as usize {
254 return Ok(());
256 }
257 let mut chunks = blob
258 .chunks(CHUNK_SIZE as usize)
259 .enumerate()
260 .map(|(i, chunk)| (i as u16, chunk))
261 .collect::<Vec<_>>();
262 for _ in 0..10 {
264 let a = u.choose_index(chunks.len())?;
265 let b = u.choose_index(chunks.len())?;
266 chunks.swap(a, b);
267 }
268
269 let blober = u.arbitrary::<ArbKeypair>()?.pubkey();
270
271 let mut unmodified = true;
272
273 let mut blob_account: (ArbKeypair, ArbAccount) = u.arbitrary()?;
274
275 blob_account.1.data = if u.ratio(1, 10)? {
276 unmodified = false;
277 u.arbitrary::<[u8; BLOB_DATA_END]>()?.to_vec()
278 } else {
279 let mut blob_pda = Blob::new(0, 0, blob.len() as u32, 0);
280 for (chunk_index, chunk_data) in &chunks {
281 blob_pda.insert(0, *chunk_index, chunk_data);
282 }
283 [Blob::DISCRIMINATOR.to_vec(), blob_pda.try_to_vec().unwrap()]
284 .into_iter()
285 .flatten()
286 .collect()
287 };
288
289 let blob_proof = BlobProof::new(&chunks);
290
291 let mut slot = u.arbitrary()?;
293 if slot == 0 {
294 slot = 1;
296 }
297 let mut source_accounts: Vec<_> = vec![BlobAccount(
298 blob_account.0.pubkey(),
299 blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END].to_vec(),
300 )];
301
302 if u.ratio(1, 10)? {
303 source_accounts.push(BlobAccount(
305 u.arbitrary::<ArbKeypair>()?.pubkey(),
306 u.arbitrary()?,
307 ));
308 unmodified = false;
309 }
310
311 let blober_account_state_proof =
312 blober_account_state::BloberAccountStateProof::new(slot, source_accounts.clone());
313
314 let other_accounts: Vec<(ArbKeypair, ArbAccount)> = u.arbitrary()?;
316
317 let mut tree = AccountMerkleTree::builder(
318 [blober, sysvar::slot_hashes::ID]
319 .into_iter()
320 .chain(other_accounts.iter().map(|(kp, _)| kp.pubkey()))
321 .collect(),
322 );
323 for (pubkey, account) in other_accounts.iter() {
324 tree.insert(pubkey.pubkey(), account.clone().into());
325 }
326 let mut blober_data = Blober {
328 caller: nitro_da_blober::id(),
329 hash: initial_hash(),
330 slot: 0,
331 };
332 if u.ratio(1, 10)? {
333 let new_slot = u.arbitrary()?;
334 if new_slot != 0 {
335 unmodified = new_slot == slot;
336 slot = new_slot;
337 }
338 }
339
340 if u.ratio(9, 10)? {
341 blober_data.store_hash(
342 &hash_blob(
343 &blob_account.0.pubkey().to_bytes().into(),
344 &blob_account.1.data[BLOB_DATA_START..BLOB_DATA_END],
345 ),
346 slot,
347 );
348 } else {
349 unmodified = false;
351 }
352 let blober_account = Account {
353 lamports: LAMPORTS_PER_SOL,
354 data: [
355 Blober::DISCRIMINATOR.to_vec(),
356 blober_data.try_to_vec().unwrap(),
357 ]
358 .into_iter()
359 .flatten()
360 .collect(),
361 owner: system_program::ID,
362 executable: false,
363 rent_epoch: 0,
364 };
365
366 let (tree, accounts_delta_hash_proof) =
367 if !other_accounts.is_empty() && u.ratio(1, 10)? {
368 let tree = tree.build();
370 let false_accounts_delta_hash_proof = tree.unchecked_inclusion_proof(
371 u.choose_index(other_accounts.len())?,
372 &blober,
373 &blober_account,
374 );
375 unmodified = false;
376 (tree, false_accounts_delta_hash_proof)
377 } else if !other_accounts.is_empty() && u.ratio(1, 10)? {
378 let keypair = &u.choose(&other_accounts)?.0;
380 let tree = tree.build();
381 let accounts_delta_hash_proof = tree.prove_inclusion(keypair.pubkey()).unwrap();
382 unmodified = keypair.pubkey() == blober;
383 (tree, accounts_delta_hash_proof)
384 } else {
385 tree.insert(blober, blober_account);
386 let tree = tree.build();
387 let accounts_delta_hash_proof = tree.prove_inclusion(blober).unwrap();
388 (tree, accounts_delta_hash_proof)
389 };
390
391 let writable_blob_account = blob_account.0.pubkey();
393 let read_only_blober_account = nitro_da_blober::id().to_bytes().into();
394
395 let parent_bankhash = arbitrary_hash(u)?;
397 let root = tree.root();
398 let signature_count = u.arbitrary()?;
399 let blockhash = arbitrary_hash(u)?;
400
401 let mut bank_hash_proof =
402 BankHashProof::new(parent_bankhash, root, signature_count, blockhash);
403
404 if u.ratio(1, 10)? {
405 let new_root = arbitrary_hash(u)?;
407 unmodified = new_root == root;
408 bank_hash_proof.accounts_delta_hash = new_root;
409 }
410
411 let mut trusted_vote_authorities: Vec<ArbKeypair> = vec![
413 arbitrary::Arbitrary::arbitrary(u)?,
414 arbitrary::Arbitrary::arbitrary(u)?,
415 ];
416 trusted_vote_authorities.sort_by_key(|pk| pk.pubkey());
417
418 let required_votes = 1 + u.choose_index(trusted_vote_authorities.len())?;
419
420 unmodified = unmodified
421 && required_votes <= trusted_vote_authorities.len()
422 && required_votes > 0;
423
424 let proven_slot = u.arbitrary()?;
425 let proven_hash = bank_hash_proof.hash();
426
427 let slot_hashes = u
428 .arbitrary_iter::<(u64, [u8; 32])>()?
429 .map(|tup| Ok((tup?.0, solana_sdk::hash::Hash::new_from_array(tup?.1))))
430 .chain([Ok((proven_slot, proven_hash))].into_iter())
432 .collect::<Result<HashSet<_>, _>>()?
433 .into_iter()
434 .collect::<Vec<_>>();
435 if slot_hashes.is_empty() {
436 return Ok(());
437 }
438
439 let slot_hashes = SlotHashes::new(&slot_hashes);
440
441 let mut slot_hashes_account: Account = u.arbitrary::<ArbAccount>()?.into();
442 slot_hashes_account.data = bincode::serialize(&slot_hashes).unwrap();
443
444 let mut slot_hashes_tree =
445 AccountMerkleTree::builder([read_only_blober_account].into_iter().collect());
446 slot_hashes_tree.insert(SlotHashes::id(), slot_hashes_account);
447
448 let blob_proofs = if u.ratio(1, 10)? {
450 unmodified = false;
452 Vec::new()
453 } else if u.ratio(1, 10)? {
454 unmodified = false;
456 vec![blob_proof.clone(), blob_proof]
457 } else {
458 vec![blob_proof]
459 };
460
461 let compound_inclusion_proof = CompoundInclusionProof::new(
462 proven_slot,
463 blob_proofs,
464 blober_account_state_proof,
465 accounts_delta_hash_proof,
466 bank_hash_proof,
467 );
468
469 let blobs = if u.ratio(1, 10)? {
470 unmodified = false;
472 Vec::new()
473 } else if u.ratio(1, 10)? {
474 unmodified = false;
476 vec![blob.to_vec(), blob.to_vec()]
477 } else if u.ratio(1, 10)? {
478 let mut new_blob = Vec::new();
480 while new_blob.len() < blob.len() {
481 new_blob.push(u.arbitrary()?);
482 }
483 unmodified = unmodified && new_blob == blob;
484 vec![new_blob]
485 } else if u.ratio(1, 10)? {
486 let mut new_blob = Vec::new();
488 while new_blob.len() == blob.len() {
489 new_blob = u.arbitrary()?;
490 }
491 unmodified = unmodified && new_blob == blob;
492 vec![new_blob]
493 } else {
494 vec![blob.to_vec()]
495 };
496
497 let blobs = blobs
498 .into_iter()
499 .map(|data| ProofBlob {
500 blob: writable_blob_account,
501 data: Some(data),
502 })
503 .collect::<Vec<_>>();
504
505 if unmodified {
506 dbg!(&compound_inclusion_proof);
507 compound_inclusion_proof
508 .verify(
509 blober,
510 bank_hash_proof.blockhash,
513 &blobs,
514 )
515 .unwrap();
516 let empty_blobs: Vec<_> = blobs
518 .into_iter()
519 .map(|b| ProofBlob::empty(b.blob))
520 .collect();
521 compound_inclusion_proof
522 .verify(blober, bank_hash_proof.blockhash, &empty_blobs)
523 .unwrap();
524 roundtrip_serialization(compound_inclusion_proof);
525 } else {
526 compound_inclusion_proof
527 .verify(blober, bank_hash_proof.blockhash, &blobs)
528 .unwrap_err();
529 roundtrip_serialization(compound_inclusion_proof);
530 }
531
532 Ok(())
533 })
534 .size_max(100_000_000);
535 }
536
537 fn roundtrip_serialization(proof: CompoundInclusionProof) {
538 let serialized_json = serde_json::to_string(&proof).unwrap();
539 let deserialized_json: CompoundInclusionProof =
540 serde_json::from_str(&serialized_json).unwrap();
541 assert_eq!(proof, deserialized_json);
542
543 let serialized_bincode = bincode::serialize(&proof).unwrap();
544 let deserialized_bincode: CompoundInclusionProof =
545 bincode::deserialize(&serialized_bincode).unwrap();
546 assert_eq!(proof, deserialized_bincode);
547
548 let serialized_risc0_zkvm = risc0_zkvm::serde::to_vec(&proof).unwrap();
549 let deserialized_risc0_zkvm: CompoundInclusionProof =
550 risc0_zkvm::serde::from_slice(&serialized_risc0_zkvm).unwrap();
551 assert_eq!(proof, deserialized_risc0_zkvm);
552 }
553}