use crate::replay::ExecutionRecord;
use crate::types::MeasurementOutcome;
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone)]
pub enum WitnessError {
BrokenChain {
index: usize,
expected: [u8; 32],
found: [u8; 32],
},
InvalidHash { index: usize },
EmptyLog,
}
impl fmt::Display for WitnessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WitnessError::BrokenChain {
index,
expected,
found,
} => write!(
f,
"broken chain at index {}: expected prev_hash {:?}, found {:?}",
index, expected, found
),
WitnessError::InvalidHash { index } => {
write!(f, "invalid self-hash at index {}", index)
}
WitnessError::EmptyLog => write!(f, "cannot verify an empty witness log"),
}
}
}
impl std::error::Error for WitnessError {}
#[derive(Debug, Clone)]
pub struct WitnessEntry {
pub sequence: u64,
pub prev_hash: [u8; 32],
pub execution: ExecutionRecord,
pub result_hash: [u8; 32],
pub entry_hash: [u8; 32],
}
pub struct WitnessLog {
entries: Vec<WitnessEntry>,
}
impl WitnessLog {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn append(
&mut self,
execution: ExecutionRecord,
results: &[MeasurementOutcome],
) -> &WitnessEntry {
let sequence = self.entries.len() as u64;
let prev_hash = self
.entries
.last()
.map(|e| e.entry_hash)
.unwrap_or([0u8; 32]);
let result_hash = hash_measurement_outcomes(results);
let execution_bytes = execution_to_bytes(&execution);
let entry_hash = compute_entry_hash(sequence, &prev_hash, &execution_bytes, &result_hash);
self.entries.push(WitnessEntry {
sequence,
prev_hash,
execution,
result_hash,
entry_hash,
});
self.entries.last().unwrap()
}
pub fn verify_chain(&self) -> Result<(), WitnessError> {
if self.entries.is_empty() {
return Err(WitnessError::EmptyLog);
}
for (i, entry) in self.entries.iter().enumerate() {
let expected_prev = if i == 0 {
[0u8; 32]
} else {
self.entries[i - 1].entry_hash
};
if entry.prev_hash != expected_prev {
return Err(WitnessError::BrokenChain {
index: i,
expected: expected_prev,
found: entry.prev_hash,
});
}
let execution_bytes = execution_to_bytes(&entry.execution);
let recomputed = compute_entry_hash(
entry.sequence,
&entry.prev_hash,
&execution_bytes,
&entry.result_hash,
);
if entry.entry_hash != recomputed {
return Err(WitnessError::InvalidHash { index: i });
}
}
Ok(())
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, index: usize) -> Option<&WitnessEntry> {
self.entries.get(index)
}
pub fn entries(&self) -> &[WitnessEntry] {
&self.entries
}
pub fn to_json(&self) -> String {
let mut buf = String::from("[\n");
for (i, entry) in self.entries.iter().enumerate() {
if i > 0 {
buf.push_str(",\n");
}
buf.push_str(" {\n");
buf.push_str(&format!(" \"sequence\": {},\n", entry.sequence));
buf.push_str(&format!(
" \"prev_hash\": \"{}\",\n",
hex_encode(&entry.prev_hash)
));
buf.push_str(&format!(
" \"circuit_hash\": \"{}\",\n",
hex_encode(&entry.execution.circuit_hash)
));
buf.push_str(&format!(" \"seed\": {},\n", entry.execution.seed));
buf.push_str(&format!(
" \"backend\": \"{}\",\n",
entry.execution.backend
));
buf.push_str(&format!(" \"shots\": {},\n", entry.execution.shots));
buf.push_str(&format!(
" \"software_version\": \"{}\",\n",
entry.execution.software_version
));
buf.push_str(&format!(
" \"timestamp_utc\": {},\n",
entry.execution.timestamp_utc
));
match &entry.execution.noise_config {
Some(nc) => {
buf.push_str(" \"noise_config\": {\n");
buf.push_str(&format!(
" \"depolarizing_rate\": {},\n",
nc.depolarizing_rate
));
buf.push_str(&format!(
" \"bit_flip_rate\": {},\n",
nc.bit_flip_rate
));
buf.push_str(&format!(
" \"phase_flip_rate\": {}\n",
nc.phase_flip_rate
));
buf.push_str(" },\n");
}
None => {
buf.push_str(" \"noise_config\": null,\n");
}
}
buf.push_str(&format!(
" \"result_hash\": \"{}\",\n",
hex_encode(&entry.result_hash)
));
buf.push_str(&format!(
" \"entry_hash\": \"{}\"\n",
hex_encode(&entry.entry_hash)
));
buf.push_str(" }");
}
buf.push_str("\n]");
buf
}
}
impl Default for WitnessLog {
fn default() -> Self {
Self::new()
}
}
fn hash_with_seed(data: &[u8], seed: u64) -> u64 {
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
data.hash(&mut hasher);
hasher.finish()
}
fn hash_to_32(data: &[u8]) -> [u8; 32] {
let mut out = [0u8; 32];
for i in 0u64..4 {
let h = hash_with_seed(data, i);
let start = (i as usize) * 8;
out[start..start + 8].copy_from_slice(&h.to_le_bytes());
}
out
}
fn hash_measurement_outcomes(outcomes: &[MeasurementOutcome]) -> [u8; 32] {
let mut buf = Vec::new();
for m in outcomes {
buf.extend_from_slice(&m.qubit.to_le_bytes());
buf.push(if m.result { 1 } else { 0 });
buf.extend_from_slice(&m.probability.to_le_bytes());
}
hash_to_32(&buf)
}
fn execution_to_bytes(exec: &ExecutionRecord) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&exec.circuit_hash);
buf.extend_from_slice(&exec.seed.to_le_bytes());
buf.extend_from_slice(exec.backend.as_bytes());
buf.extend_from_slice(&exec.shots.to_le_bytes());
buf.extend_from_slice(exec.software_version.as_bytes());
buf.extend_from_slice(&exec.timestamp_utc.to_le_bytes());
if let Some(ref nc) = exec.noise_config {
buf.push(1);
buf.extend_from_slice(&nc.depolarizing_rate.to_le_bytes());
buf.extend_from_slice(&nc.bit_flip_rate.to_le_bytes());
buf.extend_from_slice(&nc.phase_flip_rate.to_le_bytes());
} else {
buf.push(0);
}
buf
}
fn compute_entry_hash(
sequence: u64,
prev_hash: &[u8; 32],
execution_bytes: &[u8],
result_hash: &[u8; 32],
) -> [u8; 32] {
let mut buf = Vec::new();
buf.extend_from_slice(&sequence.to_le_bytes());
buf.extend_from_slice(prev_hash);
buf.extend_from_slice(execution_bytes);
buf.extend_from_slice(result_hash);
hash_to_32(&buf)
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{:02x}", b));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::replay::{NoiseConfig, ReplayEngine};
use crate::types::MeasurementOutcome;
fn make_record(seed: u64) -> ExecutionRecord {
ExecutionRecord {
circuit_hash: [seed as u8; 32],
seed,
backend: "state_vector".to_string(),
noise_config: None,
shots: 1,
software_version: "test".to_string(),
timestamp_utc: 1_700_000_000,
}
}
fn make_outcomes(bits: &[bool]) -> Vec<MeasurementOutcome> {
bits.iter()
.enumerate()
.map(|(i, &b)| MeasurementOutcome {
qubit: i as u32,
result: b,
probability: if b { 0.5 } else { 0.5 },
})
.collect()
}
#[test]
fn empty_log_verification_returns_empty_error() {
let log = WitnessLog::new();
match log.verify_chain() {
Err(WitnessError::EmptyLog) => {} other => panic!("expected EmptyLog, got {:?}", other),
}
}
#[test]
fn empty_log_len_is_zero() {
let log = WitnessLog::new();
assert_eq!(log.len(), 0);
assert!(log.is_empty());
}
#[test]
fn single_entry_has_zero_prev_hash() {
let mut log = WitnessLog::new();
let record = make_record(42);
let outcomes = make_outcomes(&[true, false]);
log.append(record, &outcomes);
let entry = log.get(0).unwrap();
assert_eq!(entry.prev_hash, [0u8; 32]);
assert_eq!(entry.sequence, 0);
}
#[test]
fn single_entry_verifies() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
assert!(log.verify_chain().is_ok());
}
#[test]
fn two_entries_properly_chained() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.append(make_record(2), &make_outcomes(&[false]));
assert_eq!(log.len(), 2);
let first = log.get(0).unwrap();
let second = log.get(1).unwrap();
assert_eq!(second.prev_hash, first.entry_hash);
assert_eq!(second.sequence, 1);
assert!(log.verify_chain().is_ok());
}
#[test]
fn tampering_with_seed_breaks_verification() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.append(make_record(2), &make_outcomes(&[false]));
log.entries[0].execution.seed = 999;
match log.verify_chain() {
Err(WitnessError::InvalidHash { index: 0 }) => {} other => panic!("expected InvalidHash at 0, got {:?}", other),
}
}
#[test]
fn tampering_with_result_hash_breaks_verification() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.entries[0].result_hash = [0xff; 32];
match log.verify_chain() {
Err(WitnessError::InvalidHash { index: 0 }) => {}
other => panic!("expected InvalidHash at 0, got {:?}", other),
}
}
#[test]
fn tampering_with_prev_hash_breaks_verification() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.append(make_record(2), &make_outcomes(&[false]));
log.entries[1].prev_hash = [0xaa; 32];
match log.verify_chain() {
Err(WitnessError::BrokenChain { index: 1, .. }) => {}
other => panic!("expected BrokenChain at 1, got {:?}", other),
}
}
#[test]
fn tampering_with_entry_hash_breaks_verification() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.entries[0].entry_hash = [0xbb; 32];
match log.verify_chain() {
Err(WitnessError::InvalidHash { index: 0 }) => {}
other => panic!("expected InvalidHash at 0, got {:?}", other),
}
}
#[test]
fn tampering_with_sequence_breaks_verification() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.entries[0].execution.backend = "tampered".to_string();
match log.verify_chain() {
Err(WitnessError::InvalidHash { index: 0 }) => {}
other => panic!("expected InvalidHash at 0, got {:?}", other),
}
}
#[test]
fn json_export_contains_all_entries() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.append(make_record(2), &make_outcomes(&[false, true]));
let json = log.to_json();
assert!(json.contains("\"sequence\": 0"));
assert!(json.contains("\"sequence\": 1"));
assert!(json.contains("\"seed\": 1"));
assert!(json.contains("\"seed\": 2"));
assert!(json.contains("\"backend\": \"state_vector\""));
assert!(json.contains("\"entry_hash\""));
assert!(json.contains("\"prev_hash\""));
assert!(json.contains("\"result_hash\""));
assert!(json.contains("\"software_version\": \"test\""));
}
#[test]
fn json_export_with_noise_config() {
let record = ExecutionRecord {
circuit_hash: [0; 32],
seed: 10,
backend: "state_vector".to_string(),
noise_config: Some(NoiseConfig {
depolarizing_rate: 0.01,
bit_flip_rate: 0.005,
phase_flip_rate: 0.002,
}),
shots: 100,
software_version: "test".to_string(),
timestamp_utc: 1_700_000_000,
};
let mut log = WitnessLog::new();
log.append(record, &make_outcomes(&[true]));
let json = log.to_json();
assert!(json.contains("\"depolarizing_rate\": 0.01"));
assert!(json.contains("\"bit_flip_rate\": 0.005"));
assert!(json.contains("\"phase_flip_rate\": 0.002"));
}
#[test]
fn json_export_null_noise() {
let mut log = WitnessLog::new();
log.append(make_record(5), &make_outcomes(&[false]));
let json = log.to_json();
assert!(json.contains("\"noise_config\": null"));
}
#[test]
fn chain_of_100_entries_verifies() {
let mut log = WitnessLog::new();
for i in 0..100u64 {
let outcomes = make_outcomes(&[i % 2 == 0, i % 3 == 0]);
log.append(make_record(i), &outcomes);
}
assert_eq!(log.len(), 100);
assert!(log.verify_chain().is_ok());
for i in 1..100 {
let prev = log.get(i - 1).unwrap();
let curr = log.get(i).unwrap();
assert_eq!(curr.prev_hash, prev.entry_hash);
assert_eq!(curr.sequence, i as u64);
}
}
#[test]
fn tampering_middle_of_long_chain_detected() {
let mut log = WitnessLog::new();
for i in 0..10u64 {
log.append(make_record(i), &make_outcomes(&[true]));
}
log.entries[5].execution.seed = 9999;
match log.verify_chain() {
Err(WitnessError::InvalidHash { index: 5 }) => {}
other => panic!("expected InvalidHash at 5, got {:?}", other),
}
}
#[test]
fn entries_returns_all() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.append(make_record(2), &make_outcomes(&[false]));
log.append(make_record(3), &make_outcomes(&[true, false]));
let entries = log.entries();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].sequence, 0);
assert_eq!(entries[1].sequence, 1);
assert_eq!(entries[2].sequence, 2);
}
#[test]
fn same_inputs_produce_same_hashes() {
let mut log1 = WitnessLog::new();
let mut log2 = WitnessLog::new();
let rec1 = make_record(42);
let rec2 = make_record(42);
let outcomes = make_outcomes(&[true, false]);
log1.append(rec1, &outcomes);
log2.append(rec2, &outcomes);
assert_eq!(
log1.get(0).unwrap().entry_hash,
log2.get(0).unwrap().entry_hash
);
assert_eq!(
log1.get(0).unwrap().result_hash,
log2.get(0).unwrap().result_hash
);
}
#[test]
fn different_results_produce_different_result_hashes() {
let mut log = WitnessLog::new();
log.append(make_record(1), &make_outcomes(&[true]));
log.append(make_record(1), &make_outcomes(&[false]));
assert_ne!(
log.get(0).unwrap().result_hash,
log.get(1).unwrap().result_hash
);
}
#[test]
fn integration_with_replay_engine() {
use crate::circuit::QuantumCircuit;
use crate::simulator::{SimConfig, Simulator};
let mut circuit = QuantumCircuit::new(2);
circuit.h(0).cnot(0, 1).measure(0).measure(1);
let config = SimConfig {
seed: Some(42),
noise: None,
shots: None,
};
let engine = ReplayEngine::new();
let record = engine.record_execution(&circuit, &config, 1);
let result = Simulator::run_with_config(&circuit, &config).unwrap();
let mut log = WitnessLog::new();
log.append(record, &result.measurements);
assert_eq!(log.len(), 1);
assert!(log.verify_chain().is_ok());
let entry = log.get(0).unwrap();
assert_eq!(entry.sequence, 0);
assert_eq!(entry.prev_hash, [0u8; 32]);
}
}