data_anchor_proofs/
blob.rs1use 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#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
15pub struct BlobProof {
16 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#[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 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 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 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 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 let other = 1 + u.choose_index(data.len() - 1)?;
147 let before = data.clone();
148 data.swap(0, other);
149 if data == before {
150 return Ok(());
152 }
153 proof.verify(&data).unwrap_err();
154 Ok(())
155 })
156 .size_max(100_000_000);
157 }
158}