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    #[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
43pub type BlobProofResult<T = ()> = Result<T, BlobProofError>;
44
45impl BlobProof {
46    /// Creates a new proof for the given blob. The blob must be at least one byte in size.
47    pub fn new<A: AsRef<[u8]>>(chunks: &[(u16, A)]) -> Self {
48        let digest = compute_blob_digest(chunks);
49        let chunk_order = chunks.iter().map(|(i, _)| *i).collect();
50        Self {
51            digest,
52            chunk_order,
53        }
54    }
55
56    pub fn hash_proof(&self) -> [u8; HASH_BYTES] {
57        let order_bytes: Vec<_> = self
58            .chunk_order
59            .iter()
60            .flat_map(|&i| i.to_le_bytes())
61            .collect();
62        hash::hashv(&[&self.digest, &order_bytes]).to_bytes()
63    }
64
65    /// Verifies that the given blob matches the proof.
66    pub fn verify(&self, blob: &[u8]) -> BlobProofResult {
67        let chunks = self
68            .chunk_order
69            .iter()
70            .map(|&i| {
71                let start_offset = i as usize * CHUNK_SIZE as usize;
72                let end_offset = min(start_offset + CHUNK_SIZE as usize, blob.len());
73
74                match blob.get(start_offset..end_offset) {
75                    Some(chunk) => Ok((i, chunk)),
76                    None => Err(BlobProofError::InvalidStructure),
77                }
78            })
79            .collect::<BlobProofResult<Vec<_>>>()?;
80
81        let digest = compute_blob_digest(&chunks);
82
83        if self.digest == digest {
84            Ok(())
85        } else {
86            Err(BlobProofError::DigestMismatch {
87                expected: self.digest,
88                found: digest,
89            })
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use arbtest::arbtest;
97    use data_anchor_blober::CHUNK_SIZE;
98
99    use super::*;
100
101    #[test]
102    fn empty_blob() {
103        BlobProof::new::<&[u8]>(&[]).verify(&[]).unwrap();
104    }
105
106    #[test]
107    fn proof() {
108        arbtest(|u| {
109            let data = u.arbitrary::<Vec<u8>>()?;
110            if data.is_empty() {
111                // Empty blob, invalid test.
112                return Ok(());
113            }
114            let mut chunks = data
115                .chunks(CHUNK_SIZE as usize)
116                .enumerate()
117                .map(|(i, c)| (i as u16, c))
118                .collect::<Vec<_>>();
119            for _ in 0..u.arbitrary_len::<usize>()? {
120                let a = u.choose_index(chunks.len())?;
121                let b = u.choose_index(chunks.len())?;
122                chunks.swap(a, b);
123            }
124            let proof = BlobProof::new(&chunks);
125            proof.verify(&data).unwrap();
126            Ok(())
127        })
128        .size_max(100_000_000);
129    }
130
131    #[test]
132    fn false_proof() {
133        arbtest(|u| {
134            let mut data = u.arbitrary::<Vec<u8>>()?;
135            if data.is_empty() {
136                // Empty blob, invalid test.
137                return Ok(());
138            }
139            let chunks = data
140                .chunks(CHUNK_SIZE as usize)
141                .enumerate()
142                .map(|(i, c)| (i as u16, c))
143                .collect::<Vec<_>>();
144
145            let proof = BlobProof::new(&chunks);
146            // Swap the 0th byte with some other byte, which should change the digest.
147            let other = 1 + u.choose_index(data.len() - 1)?;
148            let before = data.clone();
149            data.swap(0, other);
150            if data == before {
151                // No change, invalid test.
152                return Ok(());
153            }
154            proof.verify(&data).unwrap_err();
155            Ok(())
156        })
157        .size_max(100_000_000);
158    }
159}