nitro_da_proofs/
blob.rs

1//! Proof of the contents of a blob uploaded to the blober program.
2
3use std::{cmp::min, fmt::Debug};
4
5use nitro_da_blober::{compute_blob_digest, CHUNK_SIZE};
6use serde::{Deserialize, Serialize};
7use solana_sdk::hash::HASH_BYTES;
8use thiserror::Error;
9
10/// A proof that a specific blob has been uploaded to the blober program. The proof consists of two
11/// parts: The digest of the blob, and the order in which its chunks arrived. The digest is computed
12/// incrementally by hashing the current hash (starting from the default hash) with the chunk index
13/// and data, see [`compute_blob_digest`] for the exact implementation.
14#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
15pub struct BlobProof {
16    /// The SHA-256 hash of the blob.
17    pub digest: [u8; 32],
18    pub chunk_order: Vec<u16>,
19}
20
21impl Debug for BlobProof {
22    #[cfg_attr(test, mutants::skip)]
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("Proof")
25            .field(
26                "digest",
27                &solana_sdk::hash::Hash::new_from_array(self.digest),
28            )
29            .field("chunk_order", &self.chunk_order)
30            .finish()
31    }
32}
33
34/// Failures that can occur when verifying a [`BlobProof`].
35#[derive(Debug, Clone, Error, PartialEq, Eq)]
36pub enum BlobProofError {
37    #[error("Invalid structure when checking blob against stored chunks.")]
38    InvalidStructure,
39    #[error("Digest does not match the expected value. Expected {expected:?}, found {found:?}")]
40    DigestMismatch {
41        expected: [u8; HASH_BYTES],
42        found: [u8; HASH_BYTES],
43    },
44}
45
46impl BlobProof {
47    /// Creates a new proof for the given blob. The blob must be at least one byte in size.
48    pub fn new<A: AsRef<[u8]>>(chunks: &[(u16, A)]) -> Self {
49        let digest = compute_blob_digest(chunks);
50        let chunk_order = chunks.iter().map(|(i, _)| *i).collect();
51        Self {
52            digest,
53            chunk_order,
54        }
55    }
56
57    /// Verifies that the given blob matches the proof.
58    pub fn verify(&self, blob: &[u8]) -> Result<(), BlobProofError> {
59        let chunk_size = CHUNK_SIZE as usize;
60        let chunks = self
61            .chunk_order
62            .iter()
63            .map(|&i| {
64                let start_offset = i as usize * chunk_size;
65                let end_offset = min(start_offset + chunk_size, blob.len());
66
67                match blob.get(start_offset..end_offset) {
68                    Some(chunk) => Ok((i, chunk)),
69                    None => Err(BlobProofError::InvalidStructure),
70                }
71            })
72            .collect::<Result<Vec<_>, BlobProofError>>()?;
73
74        let digest = compute_blob_digest(&chunks);
75
76        if self.digest == digest {
77            Ok(())
78        } else {
79            Err(BlobProofError::DigestMismatch {
80                expected: self.digest,
81                found: digest,
82            })
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use arbtest::arbtest;
90    use nitro_da_blober::CHUNK_SIZE;
91
92    use super::*;
93
94    #[test]
95    fn empty_blob() {
96        BlobProof::new::<&[u8]>(&[]).verify(&[]).unwrap();
97    }
98
99    #[test]
100    fn proof() {
101        arbtest(|u| {
102            let data = u.arbitrary::<Vec<u8>>()?;
103            if data.is_empty() {
104                // Empty blob, invalid test.
105                return Ok(());
106            }
107            let mut chunks = data
108                .chunks(CHUNK_SIZE as usize)
109                .enumerate()
110                .map(|(i, c)| (i as u16, c))
111                .collect::<Vec<_>>();
112            for _ in 0..u.arbitrary_len::<usize>()? {
113                let a = u.choose_index(chunks.len())?;
114                let b = u.choose_index(chunks.len())?;
115                chunks.swap(a, b);
116            }
117            let proof = BlobProof::new(&chunks);
118            proof.verify(&data).unwrap();
119            Ok(())
120        })
121        .size_max(100_000_000);
122    }
123
124    #[test]
125    fn false_proof() {
126        arbtest(|u| {
127            let mut data = u.arbitrary::<Vec<u8>>()?;
128            if data.is_empty() {
129                // Empty blob, invalid test.
130                return Ok(());
131            }
132            let chunks = data
133                .chunks(CHUNK_SIZE as usize)
134                .enumerate()
135                .map(|(i, c)| (i as u16, c))
136                .collect::<Vec<_>>();
137
138            let proof = BlobProof::new(&chunks);
139            // Swap the 0th byte with some other byte, which should change the digest.
140            let other = 1 + u.choose_index(data.len() - 1)?;
141            let before = data.clone();
142            data.swap(0, other);
143            if data == before {
144                // No change, invalid test.
145                return Ok(());
146            }
147            proof.verify(&data).unwrap_err();
148            Ok(())
149        })
150        .size_max(100_000_000);
151    }
152}