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