data_anchor_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 data_anchor_blober::{CHUNK_SIZE, compute_blob_digest};
6use serde::{Deserialize, Serialize};
7use solana_sdk::hash::{HASH_BYTES, Hash};
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("digest", &Hash::new_from_array(self.digest))
26            .field("chunk_order", &self.chunk_order)
27            .finish()
28    }
29}
30
31/// Failures that can occur when verifying a [`BlobProof`].
32#[derive(Debug, Clone, Error, PartialEq, Eq)]
33pub enum BlobProofError {
34    #[error("Invalid structure when checking blob against stored chunks.")]
35    InvalidStructure,
36    #[error("Digest does not match the expected value. Expected {expected:?}, found {found:?}")]
37    DigestMismatch {
38        expected: [u8; HASH_BYTES],
39        found: [u8; HASH_BYTES],
40    },
41}
42
43impl BlobProof {
44    /// Creates a new proof for the given blob. The blob must be at least one byte in size.
45    pub fn new<A: AsRef<[u8]>>(chunks: &[(u16, A)]) -> Self {
46        let digest = compute_blob_digest(chunks);
47        let chunk_order = chunks.iter().map(|(i, _)| *i).collect();
48        Self {
49            digest,
50            chunk_order,
51        }
52    }
53
54    /// Verifies that the given blob matches the proof.
55    pub fn verify(&self, blob: &[u8]) -> Result<(), BlobProofError> {
56        let chunk_size = CHUNK_SIZE as usize;
57        let chunks = self
58            .chunk_order
59            .iter()
60            .map(|&i| {
61                let start_offset = i as usize * chunk_size;
62                let end_offset = min(start_offset + chunk_size, blob.len());
63
64                match blob.get(start_offset..end_offset) {
65                    Some(chunk) => Ok((i, chunk)),
66                    None => Err(BlobProofError::InvalidStructure),
67                }
68            })
69            .collect::<Result<Vec<_>, BlobProofError>>()?;
70
71        let digest = compute_blob_digest(&chunks);
72
73        if self.digest == digest {
74            Ok(())
75        } else {
76            Err(BlobProofError::DigestMismatch {
77                expected: self.digest,
78                found: digest,
79            })
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use arbtest::arbtest;
87    use data_anchor_blober::CHUNK_SIZE;
88
89    use super::*;
90
91    #[test]
92    fn empty_blob() {
93        BlobProof::new::<&[u8]>(&[]).verify(&[]).unwrap();
94    }
95
96    #[test]
97    fn proof() {
98        arbtest(|u| {
99            let data = u.arbitrary::<Vec<u8>>()?;
100            if data.is_empty() {
101                // Empty blob, invalid test.
102                return Ok(());
103            }
104            let mut chunks = data
105                .chunks(CHUNK_SIZE as usize)
106                .enumerate()
107                .map(|(i, c)| (i as u16, c))
108                .collect::<Vec<_>>();
109            for _ in 0..u.arbitrary_len::<usize>()? {
110                let a = u.choose_index(chunks.len())?;
111                let b = u.choose_index(chunks.len())?;
112                chunks.swap(a, b);
113            }
114            let proof = BlobProof::new(&chunks);
115            proof.verify(&data).unwrap();
116            Ok(())
117        })
118        .size_max(100_000_000);
119    }
120
121    #[test]
122    fn false_proof() {
123        arbtest(|u| {
124            let mut data = u.arbitrary::<Vec<u8>>()?;
125            if data.is_empty() {
126                // Empty blob, invalid test.
127                return Ok(());
128            }
129            let chunks = data
130                .chunks(CHUNK_SIZE as usize)
131                .enumerate()
132                .map(|(i, c)| (i as u16, c))
133                .collect::<Vec<_>>();
134
135            let proof = BlobProof::new(&chunks);
136            // Swap the 0th byte with some other byte, which should change the digest.
137            let other = 1 + u.choose_index(data.len() - 1)?;
138            let before = data.clone();
139            data.swap(0, other);
140            if data == before {
141                // No change, invalid test.
142                return Ok(());
143            }
144            proof.verify(&data).unwrap_err();
145            Ok(())
146        })
147        .size_max(100_000_000);
148    }
149}