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 #[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#[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 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 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 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 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 let other = 1 + u.choose_index(data.len() - 1)?;
148 let before = data.clone();
149 data.swap(0, other);
150 if data == before {
151 return Ok(());
153 }
154 proof.verify(&data).unwrap_err();
155 Ok(())
156 })
157 .size_max(100_000_000);
158 }
159}