use std::{
fs,
io::Read,
path::{Path, PathBuf},
};
use sha2::{Digest, Sha256};
use super::{StorageError, StoragePaths, StorageResult};
#[uniffi::export]
#[allow(clippy::needless_pass_by_value)]
pub fn cache_embedded_groth16_material(paths: &StoragePaths) -> StorageResult<()> {
if has_valid_cached_material(paths)? {
return Ok(());
}
let files = world_id_core::proof::load_embedded_circuit_files()
.map_err(|error| StorageError::CacheDb(error.to_string()))?;
fs::create_dir_all(paths.groth16_dir())
.map_err(|error| StorageError::CacheDb(error.to_string()))?;
write_atomic(&paths.query_zkey_path(), &files.query_zkey)?;
write_atomic(&paths.nullifier_zkey_path(), &files.nullifier_zkey)?;
write_atomic(&paths.query_graph_path(), &files.query_graph)?;
write_atomic(&paths.nullifier_graph_path(), &files.nullifier_graph)?;
Ok(())
}
fn has_valid_cached_material(paths: &StoragePaths) -> StorageResult<bool> {
let entries = [
(
paths.query_zkey_path(),
world_id_core::proof::QUERY_ZKEY_FINGERPRINT,
),
(
paths.nullifier_zkey_path(),
world_id_core::proof::NULLIFIER_ZKEY_FINGERPRINT,
),
(
paths.query_graph_path(),
world_id_core::proof::QUERY_GRAPH_FINGERPRINT,
),
(
paths.nullifier_graph_path(),
world_id_core::proof::NULLIFIER_GRAPH_FINGERPRINT,
),
];
for (path, expected_fingerprint) in entries {
if !path.is_file() {
return Ok(false);
}
let actual_fingerprint = file_sha256_hex(&path)?;
if actual_fingerprint != expected_fingerprint {
return Ok(false);
}
}
Ok(true)
}
fn file_sha256_hex(path: &Path) -> StorageResult<String> {
let mut file = fs::File::open(path)
.map_err(|error| StorageError::CacheDb(error.to_string()))?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 16 * 1024];
loop {
let bytes_read = file
.read(&mut buffer)
.map_err(|error| StorageError::CacheDb(error.to_string()))?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hex::encode(hasher.finalize()))
}
fn write_atomic(path: &Path, bytes: &[u8]) -> StorageResult<()> {
let tmp_path = PathBuf::from(format!("{}.tmp", path.to_string_lossy()));
fs::write(&tmp_path, bytes)
.map_err(|error| StorageError::CacheDb(error.to_string()))?;
fs::rename(&tmp_path, path)
.map_err(|error| StorageError::CacheDb(error.to_string()))
}
#[cfg(test)]
mod tests {
use std::fs;
use super::cache_embedded_groth16_material;
use crate::storage::StoragePaths;
fn temp_root() -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("walletkit-groth16-cache-{}", uuid::Uuid::new_v4()));
path
}
#[test]
fn test_cache_embedded_groth16_material_writes_all_files() {
let root = temp_root();
let paths = StoragePaths::new(&root);
cache_embedded_groth16_material(&paths).expect("cache embedded material");
assert!(paths.groth16_dir().is_dir());
assert!(paths.query_zkey_path().is_file());
assert!(paths.nullifier_zkey_path().is_file());
assert!(paths.query_graph_path().is_file());
assert!(paths.nullifier_graph_path().is_file());
let _ = fs::remove_dir_all(root);
}
#[test]
fn test_cache_embedded_groth16_material_is_idempotent() {
let root = temp_root();
let paths = StoragePaths::new(&root);
cache_embedded_groth16_material(&paths).expect("first cache");
let first_query_len = fs::metadata(paths.query_zkey_path())
.expect("query zkey metadata")
.len();
cache_embedded_groth16_material(&paths).expect("second cache");
let second_query_len = fs::metadata(paths.query_zkey_path())
.expect("query zkey metadata")
.len();
assert_eq!(first_query_len, second_query_len);
assert!(paths.nullifier_zkey_path().is_file());
assert!(paths.query_graph_path().is_file());
assert!(paths.nullifier_graph_path().is_file());
let _ = fs::remove_dir_all(root);
}
}