use std::{
fs::{File, OpenOptions},
io::{Read, Write},
path::Path,
time::Instant,
};
use hyphae_query::{Record, execute};
use hyphae_storage::load_snapshot;
use super::{
ProofAnchor, ProofError, ProvenOperation, ProvenResult, ResultProof, VerificationLimits,
VerificationReport, decode_proof, encode_proof,
};
use crate::decode_document;
pub fn write_result_proof(path: impl AsRef<Path>, proof: &ResultProof) -> Result<(), ProofError> {
let encoded = encode_proof(proof)?;
let mut file = OpenOptions::new().create_new(true).write(true).open(path)?;
file.write_all(&encoded)?;
file.sync_all()?;
Ok(())
}
pub fn read_result_proof(
path: impl AsRef<Path>,
maximum_bytes: u64,
) -> Result<ResultProof, ProofError> {
let mut file = File::open(path)?;
let metadata_length = file.metadata()?.len();
if metadata_length > maximum_bytes {
return Err(ProofError::ProofLimitExceeded {
actual: metadata_length,
maximum: maximum_bytes,
});
}
let capacity = usize::try_from(metadata_length).map_err(|_| ProofError::LengthOverflow)?;
let mut encoded = Vec::with_capacity(capacity);
file.read_to_end(&mut encoded)?;
let actual = u64::try_from(encoded.len()).map_err(|_| ProofError::LengthOverflow)?;
if actual > maximum_bytes {
return Err(ProofError::ProofLimitExceeded {
actual,
maximum: maximum_bytes,
});
}
if actual != metadata_length {
return Err(ProofError::Invalid {
reason: "proof changed while being read",
});
}
decode_proof(&encoded)
}
pub fn verify_result_proof(
proof_path: impl AsRef<Path>,
snapshot_path: impl AsRef<Path>,
expected_anchor_digest: [u8; 32],
limits: &VerificationLimits,
) -> Result<VerificationReport, ProofError> {
let started = Instant::now();
let proof = read_result_proof(proof_path, limits.proof_bytes)?;
check_timeout(started, limits)?;
let anchor_digest = proof.anchor_digest();
if anchor_digest != expected_anchor_digest {
return Err(ProofError::AnchorMismatch);
}
let snapshot = load_snapshot(snapshot_path, &limits.snapshot)?;
check_timeout(started, limits)?;
if ProofAnchor::from_snapshot(&snapshot.info) != *proof.anchor() {
return Err(ProofError::SnapshotAnchorMismatch);
}
let mut records = Vec::with_capacity(snapshot.entries.len());
for entry in snapshot.entries {
check_timeout(started, limits)?;
records.push(Record {
key: entry.key,
value: decode_document(&entry.value)?,
});
}
let verified_result = match (proof.operation(), proof.result()) {
(ProvenOperation::Get { key }, ProvenResult::Get(expected)) => {
let actual = records
.binary_search_by(|record| record.key.as_slice().cmp(key))
.ok()
.map(|index| records[index].clone());
if &actual != expected {
return Err(ProofError::ReexecutionMismatch);
}
ProvenResult::Get(actual)
}
(ProvenOperation::Query(query), ProvenResult::Query(expected)) => {
let elapsed = started.elapsed();
let remaining = limits
.timeout
.checked_sub(elapsed)
.ok_or(ProofError::TimedOut)?;
if remaining.is_zero() {
return Err(ProofError::TimedOut);
}
let query_limits = hyphae_query::ExecutionLimits {
timeout: remaining.min(limits.query.timeout),
..limits.query.clone()
};
let actual = execute(&[records.as_slice()], query, &query_limits)?;
if &actual != expected {
return Err(ProofError::ReexecutionMismatch);
}
ProvenResult::Query(actual)
}
_ => return Err(ProofError::OperationResultMismatch),
};
check_timeout(started, limits)?;
Ok(VerificationReport {
anchor: proof.anchor().clone(),
anchor_digest,
proof_digest: proof.proof_digest(),
result: verified_result,
})
}
fn check_timeout(started: Instant, limits: &VerificationLimits) -> Result<(), ProofError> {
if started.elapsed() >= limits.timeout {
Err(ProofError::TimedOut)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, error::Error, fs, path::PathBuf};
use hyphae_query::{ExecutionLimits, Filter, Query, Record, Value};
use uuid::Uuid;
use super::{VerificationLimits, verify_result_proof, write_result_proof};
use crate::{HyphaeEngine, ProofError, ProvenResult};
struct TestDirectory {
path: PathBuf,
}
impl TestDirectory {
fn create() -> Result<Self, Box<dyn Error>> {
let path = std::env::temp_dir()
.join(format!("hyphae-proof-rehashed-tamper-{}", Uuid::now_v7()));
fs::create_dir_all(&path)?;
Ok(Self { path })
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let _ignored = fs::remove_dir_all(&self.path);
}
}
#[test]
fn self_consistently_rehashed_result_edits_are_rejected() -> Result<(), Box<dyn Error>> {
let temporary = TestDirectory::create()?;
let mut opened = HyphaeEngine::open(temporary.path.join("data"))?;
opened.engine.put_records(
Uuid::now_v7(),
&[record(b"a", 1), record(b"b", 2), record(b"c", 3)],
)?;
let artifact = opened.engine.query_with_proof(
&Query {
filter: Filter::MatchAll,
sort: Vec::new(),
cursor: None,
limit: 3,
aggregation: None,
},
&ExecutionLimits::default(),
)?;
for (name, mutation) in [
("delete", 1_u8),
("insert", 2_u8),
("reorder", 3_u8),
("edit", 4_u8),
] {
let mut forged = artifact.proof.clone();
let ProvenResult::Query(result) = &mut forged.result else {
return Err(ProofError::OperationResultMismatch.into());
};
match mutation {
1 => {
result.rows.remove(0);
}
2 => result.rows.push(result.rows[0].clone()),
3 => result.rows.swap(0, 1),
4 => result.rows[0].value = Value::Integer(999),
_ => return Err(ProofError::OperationResultMismatch.into()),
}
let proof_path = temporary.path.join(format!("{name}.hyproof"));
write_result_proof(&proof_path, &forged)?;
assert!(matches!(
verify_result_proof(
&proof_path,
&artifact.snapshot.path,
artifact.proof.anchor_digest(),
&VerificationLimits::default(),
),
Err(ProofError::ReexecutionMismatch)
));
}
Ok(())
}
fn record(key: &[u8], score: i64) -> Record {
Record::new(
key,
Value::Object(BTreeMap::from([(
"score".to_owned(),
Value::Integer(score),
)])),
)
}
}