1use std::{
4 fs::{File, OpenOptions},
5 io::{Read, Write},
6 path::Path,
7 time::Instant,
8};
9
10use hyphae_query::{Record, execute};
11use hyphae_storage::load_snapshot;
12
13use super::{
14 ProofAnchor, ProofError, ProvenOperation, ProvenResult, ResultProof, VerificationLimits,
15 VerificationReport, decode_proof, encode_proof,
16};
17use crate::decode_document;
18
19pub fn write_result_proof(path: impl AsRef<Path>, proof: &ResultProof) -> Result<(), ProofError> {
27 let encoded = encode_proof(proof)?;
28 let mut file = OpenOptions::new().create_new(true).write(true).open(path)?;
29 file.write_all(&encoded)?;
30 file.sync_all()?;
31 Ok(())
32}
33
34pub fn read_result_proof(
41 path: impl AsRef<Path>,
42 maximum_bytes: u64,
43) -> Result<ResultProof, ProofError> {
44 let mut file = File::open(path)?;
45 let metadata_length = file.metadata()?.len();
46 if metadata_length > maximum_bytes {
47 return Err(ProofError::ProofLimitExceeded {
48 actual: metadata_length,
49 maximum: maximum_bytes,
50 });
51 }
52 let capacity = usize::try_from(metadata_length).map_err(|_| ProofError::LengthOverflow)?;
53 let mut encoded = Vec::with_capacity(capacity);
54 file.read_to_end(&mut encoded)?;
55 let actual = u64::try_from(encoded.len()).map_err(|_| ProofError::LengthOverflow)?;
56 if actual > maximum_bytes {
57 return Err(ProofError::ProofLimitExceeded {
58 actual,
59 maximum: maximum_bytes,
60 });
61 }
62 if actual != metadata_length {
63 return Err(ProofError::Invalid {
64 reason: "proof changed while being read",
65 });
66 }
67 decode_proof(&encoded)
68}
69
70pub fn verify_result_proof(
79 proof_path: impl AsRef<Path>,
80 snapshot_path: impl AsRef<Path>,
81 expected_anchor_digest: [u8; 32],
82 limits: &VerificationLimits,
83) -> Result<VerificationReport, ProofError> {
84 let started = Instant::now();
85 let proof = read_result_proof(proof_path, limits.proof_bytes)?;
86 check_timeout(started, limits)?;
87
88 let anchor_digest = proof.anchor_digest();
89 if anchor_digest != expected_anchor_digest {
90 return Err(ProofError::AnchorMismatch);
91 }
92
93 let snapshot = load_snapshot(snapshot_path, &limits.snapshot)?;
94 check_timeout(started, limits)?;
95 if ProofAnchor::from_snapshot(&snapshot.info) != *proof.anchor() {
96 return Err(ProofError::SnapshotAnchorMismatch);
97 }
98
99 let mut records = Vec::with_capacity(snapshot.entries.len());
100 for entry in snapshot.entries {
101 check_timeout(started, limits)?;
102 records.push(Record {
103 key: entry.key,
104 value: decode_document(&entry.value)?,
105 });
106 }
107
108 let verified_result = match (proof.operation(), proof.result()) {
109 (ProvenOperation::Get { key }, ProvenResult::Get(expected)) => {
110 let actual = records
111 .binary_search_by(|record| record.key.as_slice().cmp(key))
112 .ok()
113 .map(|index| records[index].clone());
114 if &actual != expected {
115 return Err(ProofError::ReexecutionMismatch);
116 }
117 ProvenResult::Get(actual)
118 }
119 (ProvenOperation::Query(query), ProvenResult::Query(expected)) => {
120 let elapsed = started.elapsed();
121 let remaining = limits
122 .timeout
123 .checked_sub(elapsed)
124 .ok_or(ProofError::TimedOut)?;
125 if remaining.is_zero() {
126 return Err(ProofError::TimedOut);
127 }
128 let query_limits = hyphae_query::ExecutionLimits {
129 timeout: remaining.min(limits.query.timeout),
130 ..limits.query.clone()
131 };
132 let actual = execute(&[records.as_slice()], query, &query_limits)?;
133 if &actual != expected {
134 return Err(ProofError::ReexecutionMismatch);
135 }
136 ProvenResult::Query(actual)
137 }
138 _ => return Err(ProofError::OperationResultMismatch),
139 };
140 check_timeout(started, limits)?;
141
142 Ok(VerificationReport {
143 anchor: proof.anchor().clone(),
144 anchor_digest,
145 proof_digest: proof.proof_digest(),
146 result: verified_result,
147 })
148}
149
150fn check_timeout(started: Instant, limits: &VerificationLimits) -> Result<(), ProofError> {
151 if started.elapsed() >= limits.timeout {
152 Err(ProofError::TimedOut)
153 } else {
154 Ok(())
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use std::{collections::BTreeMap, error::Error, fs, path::PathBuf};
161
162 use hyphae_query::{ExecutionLimits, Filter, Query, Record, Value};
163 use uuid::Uuid;
164
165 use super::{VerificationLimits, verify_result_proof, write_result_proof};
166 use crate::{HyphaeEngine, ProofError, ProvenResult};
167
168 struct TestDirectory {
169 path: PathBuf,
170 }
171
172 impl TestDirectory {
173 fn create() -> Result<Self, Box<dyn Error>> {
174 let path = std::env::temp_dir()
175 .join(format!("hyphae-proof-rehashed-tamper-{}", Uuid::now_v7()));
176 fs::create_dir_all(&path)?;
177 Ok(Self { path })
178 }
179 }
180
181 impl Drop for TestDirectory {
182 fn drop(&mut self) {
183 let _ignored = fs::remove_dir_all(&self.path);
184 }
185 }
186
187 #[test]
188 fn self_consistently_rehashed_result_edits_are_rejected() -> Result<(), Box<dyn Error>> {
189 let temporary = TestDirectory::create()?;
190 let mut opened = HyphaeEngine::open(temporary.path.join("data"))?;
191 opened.engine.put_records(
192 Uuid::now_v7(),
193 &[record(b"a", 1), record(b"b", 2), record(b"c", 3)],
194 )?;
195 let artifact = opened.engine.query_with_proof(
196 &Query {
197 filter: Filter::MatchAll,
198 sort: Vec::new(),
199 cursor: None,
200 limit: 3,
201 aggregation: None,
202 },
203 &ExecutionLimits::default(),
204 )?;
205
206 for (name, mutation) in [
207 ("delete", 1_u8),
208 ("insert", 2_u8),
209 ("reorder", 3_u8),
210 ("edit", 4_u8),
211 ] {
212 let mut forged = artifact.proof.clone();
213 let ProvenResult::Query(result) = &mut forged.result else {
214 return Err(ProofError::OperationResultMismatch.into());
215 };
216 match mutation {
217 1 => {
218 result.rows.remove(0);
219 }
220 2 => result.rows.push(result.rows[0].clone()),
221 3 => result.rows.swap(0, 1),
222 4 => result.rows[0].value = Value::Integer(999),
223 _ => return Err(ProofError::OperationResultMismatch.into()),
224 }
225 let proof_path = temporary.path.join(format!("{name}.hyproof"));
226 write_result_proof(&proof_path, &forged)?;
227 assert!(matches!(
228 verify_result_proof(
229 &proof_path,
230 &artifact.snapshot.path,
231 artifact.proof.anchor_digest(),
232 &VerificationLimits::default(),
233 ),
234 Err(ProofError::ReexecutionMismatch)
235 ));
236 }
237 Ok(())
238 }
239
240 fn record(key: &[u8], score: i64) -> Record {
241 Record::new(
242 key,
243 Value::Object(BTreeMap::from([(
244 "score".to_owned(),
245 Value::Integer(score),
246 )])),
247 )
248 }
249}