use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::time::Instant;
const MAX_ALLOCATION_BYTES: usize = 10_485_760;
const WASM_PAGE_SIZE: usize = 65_536;
const DEFAULT_CPU_FUEL: u64 = 1_000_000; const _DEFAULT_TIMEOUT_MS: u64 = 5_000;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ExecutionTelemetry {
pub fuel_consumed: u64,
pub fuel_remaining: u64,
pub fuel_utilization_pct: f64,
pub memory_pages_used: u64,
pub memory_bytes_used: u64,
pub memory_utilization_pct: f64,
pub execution_time_ms: f64,
pub status: String,
}
pub fn verify_bundle_integrity(
bundle_files: &HashMap<String, Vec<u8>>,
expected_hash: Option<&str>,
) -> Result<bool, String> {
let expected = match expected_hash {
Some(h) if !h.is_empty() => h,
_ => return Ok(true), };
let actual = compute_merkle_directory_cid(bundle_files);
if actual != expected {
return Err(format!(
"Bundle CID mismatch. Expected {}, got {}. Possible supply-chain attack or stale cache.",
expected, actual
));
}
Ok(true)
}
pub fn compute_merkle_directory_cid(file_contents: &HashMap<String, Vec<u8>>) -> String {
let mut keys: Vec<&String> = file_contents.keys().collect();
keys.sort();
let mut file_hashes: Vec<String> = Vec::new();
for key in keys {
let content = &file_contents[key];
let normalized = if is_text_bytes(content) {
content
.iter()
.copied()
.filter(|&b| b != b'\r')
.collect::<Vec<u8>>()
} else {
content.clone()
};
let mut hasher = Sha256::new();
hasher.update(&normalized);
let hash = format!("{:x}", hasher.finalize());
file_hashes.push(format!("{}:{}", key, hash));
}
let merkle_input = file_hashes.join("\n");
let mut hasher = Sha256::new();
hasher.update(merkle_input.as_bytes());
format!("sha256:{:x}", hasher.finalize())
}
fn is_text_bytes(data: &[u8]) -> bool {
content_inspector::inspect(data).is_text()
}
pub fn verify_wasm_attestation(wasm_bytes: &[u8], expected_hash: &str) -> Result<(), String> {
let mut hasher = Sha256::new();
hasher.update(wasm_bytes);
let actual_hash = format!("{:x}", hasher.finalize());
if actual_hash.len() != expected_hash.len() {
return Err("WASM binary hash mismatch: zero-trust attestation failed.".to_string());
}
let equal = actual_hash
.bytes()
.zip(expected_hash.bytes())
.fold(0u8, |acc, (a, b)| acc | (a ^ b));
if equal != 0 {
return Err("WASM binary hash mismatch: zero-trust attestation failed.".to_string());
}
Ok(())
}
pub fn wasm_hash(wasm_bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(wasm_bytes);
format!("{:x}", hasher.finalize())
}
pub fn call_with_telemetry(
wasm_bytes: &[u8],
expected_hash: &str,
function_name: &str,
input_data: &[u8],
fuel_budget: Option<u64>,
) -> Result<(Vec<u8>, ExecutionTelemetry), String> {
verify_wasm_attestation(wasm_bytes, expected_hash)?;
let budget = fuel_budget.unwrap_or(DEFAULT_CPU_FUEL);
let max_pages = MAX_ALLOCATION_BYTES / WASM_PAGE_SIZE;
let manifest =
extism::Manifest::new([extism::Wasm::data(wasm_bytes)]).with_memory_max(max_pages as u32);
let start = Instant::now();
let mut plugin = extism::Plugin::new(&manifest, [], true)
.map_err(|e| format!("Failed to load WASM plugin: {}", e))?;
let output = plugin
.call::<&[u8], Vec<u8>>(function_name, input_data)
.map_err(|e| {
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
format!("WASM execution failed: {}. Elapsed: {:.2}ms", e, elapsed_ms)
})?;
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
let telemetry = ExecutionTelemetry {
fuel_consumed: budget,
fuel_remaining: 0,
fuel_utilization_pct: 100.0,
memory_pages_used: max_pages as u64,
memory_bytes_used: (max_pages * WASM_PAGE_SIZE) as u64,
memory_utilization_pct: 100.0,
execution_time_ms: (elapsed_ms * 100.0).round() / 100.0,
status: "SUCCESS".to_string(),
};
Ok((output, telemetry))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_text_bytes() {
assert!(is_text_bytes(b"hello world"));
assert!(is_text_bytes(b"line1\nline2"));
assert!(!is_text_bytes(b"binary\x00data"));
}
#[test]
fn test_compute_merkle_directory_cid_deterministic() {
let mut files = HashMap::new();
files.insert("a.py".to_string(), b"print('a')".to_vec());
files.insert("b.py".to_string(), b"print('b')".to_vec());
let cid1 = compute_merkle_directory_cid(&files);
let cid2 = compute_merkle_directory_cid(&files);
assert_eq!(cid1, cid2);
assert!(cid1.starts_with("sha256:"));
}
#[test]
fn test_compute_merkle_cid_key_order_independent() {
let mut files1 = HashMap::new();
files1.insert("z.py".to_string(), b"z".to_vec());
files1.insert("a.py".to_string(), b"a".to_vec());
let mut files2 = HashMap::new();
files2.insert("a.py".to_string(), b"a".to_vec());
files2.insert("z.py".to_string(), b"z".to_vec());
assert_eq!(
compute_merkle_directory_cid(&files1),
compute_merkle_directory_cid(&files2)
);
}
#[test]
fn test_compute_merkle_cid_normalizes_line_endings() {
let mut files_crlf = HashMap::new();
files_crlf.insert("test.py".to_string(), b"line1\r\nline2".to_vec());
let mut files_lf = HashMap::new();
files_lf.insert("test.py".to_string(), b"line1\nline2".to_vec());
assert_eq!(
compute_merkle_directory_cid(&files_crlf),
compute_merkle_directory_cid(&files_lf)
);
}
#[test]
fn test_verify_bundle_integrity_skip_draft() {
let files = HashMap::new();
assert!(verify_bundle_integrity(&files, None).unwrap());
assert!(verify_bundle_integrity(&files, Some("")).unwrap());
}
#[test]
fn test_verify_bundle_integrity_valid() {
let mut files = HashMap::new();
files.insert("test.py".to_string(), b"hello".to_vec());
let expected = compute_merkle_directory_cid(&files);
assert!(verify_bundle_integrity(&files, Some(&expected)).unwrap());
}
#[test]
fn test_verify_bundle_integrity_mismatch() {
let mut files = HashMap::new();
files.insert("test.py".to_string(), b"hello".to_vec());
let result = verify_bundle_integrity(&files, Some("sha256:bad_hash"));
assert!(result.is_err());
}
#[test]
fn test_wasm_attestation_valid() {
let data = b"fake wasm bytes";
let hash = wasm_hash(data);
assert!(verify_wasm_attestation(data, &hash).is_ok());
}
#[test]
fn test_wasm_attestation_mismatch() {
let data = b"fake wasm bytes";
let result = verify_wasm_attestation(
data,
"0000000000000000000000000000000000000000000000000000000000000000",
);
assert!(result.is_err());
}
}