Finding 4: full-trace evidence — reconstructed and re-derived, not summarized

Removes the deferral. Each sampled execution's FULL trace (every graph edge,
count, weight) and delta are persisted as evidence; the independent attestor
reconstructs them, recomputes the canonical trace hash and delta hash, re-derives
the replay-record leaf, and confirms it is among the retained Merkle leaves the
root is built from. The behavior-fingerprint hash is re-derived from features
(not trusted), and f64 divergence is stored bit-exact for an identical hash.

- trace_model: ExecutionTrace::serialize/deserialize (round-trips canonical_hash;
  total on garbage). Test: full_trace_serialize_roundtrips_canonical_hash.
- world_model: WorldDelta::serialize/deserialize (round-trips hash).
- ci_reports: retains TRACE_EVIDENCE_SAMPLE full traces; writes
  evidence/traces.tsv.
- attestation: depends on trace_model/world_model; reconstructs each trace,
  recomputes the leaf, requires it to match the claimed leaf AND be a retained
  leaf. Negative control: tampered_trace_breaks_attestation (corrupting the full
  trace, leaving the claimed leaf, fails attestation).
- merge-gates: requires evidence/traces.tsv; the separate attest step verifies it.

End-to-end (fast profile): 256/256 full traces reconstructed and re-derived to
retained leaves; recomputed root matches the claim over 6600 leaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:22:55 -07:00
co-authored by Claude Opus 4.8
parent 1e50c80627
commit bea076df43
8 changed files with 481 additions and 19 deletions
+5 -3
View File
@@ -9,7 +9,9 @@ name = "attest"
path = "src/main.rs"
[dependencies]
# Only the shared hashing primitive — NOT ci_reports. The root is recomputed by
# this crate's own code path, so it is an independent check, not a re-export of
# the producer's claim.
# NOT ci_reports. The root, the full-trace hashes, and the leaves are recomputed
# by this crate's own code path an independent check, not a re-export of the
# producer's claim. trace_model/world_model provide the shared trace/delta types
# and hashing the verifier reconstructs from raw evidence.
world_model = { path = "../world_model" }
trace_model = { path = "../trace_model" }
+178 -16
View File
@@ -17,8 +17,10 @@
//! executions performed by a trusted third party — that requires external
//! re-execution / signing infrastructure (see the BLOCKED note in the report).
use std::collections::HashSet;
use std::path::Path;
use world_model::Hasher;
use trace_model::{ExecutionTrace, ReplayRecord};
use world_model::{Hash, Hasher, WorldDelta};
/// Recompute the Merkle root over `leaves` (binary tree, FNV-combined). This is
/// an independent re-implementation of the producer's algorithm; agreement is
@@ -62,6 +64,40 @@ fn claim<'a>(claims: &'a [(String, String)], key: &str) -> Option<&'a str> {
claims.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
/// Reconstruct the leaf of one full-trace evidence record from its raw trace +
/// delta + seeds, independently of any reported digest. Returns the recomputed
/// leaf, or `None` if the record is malformed.
///
/// `line` is `ws ps cs prs future leaf <TAB> <trace> <TAB> <delta>`.
pub fn recompute_trace_leaf(line: &str) -> Option<(u64, u64)> {
let mut parts = line.splitn(3, '\t');
let header = parts.next()?;
let trace_s = parts.next()?;
let delta_s = parts.next()?;
let mut h = header.split_whitespace();
let hx = |s: Option<&str>| -> Option<u64> { u64::from_str_radix(s?.trim(), 16).ok() };
let world_seed = hx(h.next())?;
let program_seed = hx(h.next())?;
let contract_seed = hx(h.next())?;
let perturbation_seed = hx(h.next())?;
let future = hx(h.next())?;
let claimed_leaf = hx(h.next())?;
let trace = ExecutionTrace::deserialize(trace_s)?;
let delta = WorldDelta::deserialize(delta_s)?;
// Recompute the canonical trace hash and delta hash from the FULL structure.
let rr = ReplayRecord {
world_seed,
program_seed,
contract_seed,
perturbation_seed,
trace_hash: trace.canonical_hash(),
delta_hash: delta.hash(),
future_hash: Hash(future),
};
Some((rr.hash().0, claimed_leaf))
}
/// Outcome of an attestation.
#[derive(Clone, Debug)]
pub struct Attestation {
@@ -69,6 +105,8 @@ pub struct Attestation {
pub leaf_count: usize,
pub recomputed_root: u64,
pub claimed_root: Option<u64>,
pub traces_verified: usize,
pub traces_total: usize,
pub checks: Vec<(String, bool)>,
}
@@ -100,12 +138,44 @@ pub fn verify_dir(dir: &Path) -> Result<Attestation, String> {
let nonempty = !leaves.is_empty();
checks.push(("leaves are present (root has leaves)".into(), nonempty));
// Full-trace evidence (finding 4): reconstruct each sampled trace + delta,
// recompute its leaf independently, and confirm it both matches the record's
// claimed leaf AND is one of the retained leaves the root is built from.
let leaf_set: HashSet<u64> = leaves.iter().copied().collect();
let traces_path = dir.join("evidence/traces.tsv");
let traces_txt = std::fs::read_to_string(&traces_path)
.map_err(|e| format!("cannot read {}: {e}", traces_path.display()))?;
let mut traces_total = 0usize;
let mut traces_verified = 0usize;
for line in traces_txt.lines() {
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
traces_total += 1;
match recompute_trace_leaf(line) {
Some((recomputed_leaf, claimed_leaf)) => {
if recomputed_leaf == claimed_leaf && leaf_set.contains(&recomputed_leaf) {
traces_verified += 1;
}
}
None => {}
}
}
let traces_present = traces_total > 0;
checks.push(("full-trace evidence present".into(), traces_present));
checks.push((
"every sampled full trace recomputes to a retained leaf".into(),
traces_present && traces_verified == traces_total,
));
let ok = checks.iter().all(|(_, b)| *b);
Ok(Attestation {
ok,
leaf_count: leaves.len(),
recomputed_root: recomputed,
claimed_root,
traces_verified,
traces_total,
checks,
})
}
@@ -124,46 +194,138 @@ mod tests {
assert_ne!(a, c);
}
fn write_evidence(dir: &Path, leaves: &[u64], claimed_root: u64) {
use trace_model::{
BehaviorFingerprint, CausalEdge, CausalGraph, CausalNode, DivergenceGraph,
DomainAccessGraph, ExecutionTrace, InformationFlowGraph, PerturbationResponse, ReplayRecord,
TemporalGraph,
};
use world_model::{DomainDelta, DomainId, Hash, WorldDelta, HIDDEN_LANES, LANES};
fn sample_trace(seed: u64) -> ExecutionTrace {
let mut read_graph = DomainAccessGraph::default();
read_graph.access_count[(seed % 8) as usize] = 3;
read_graph.edges.push((0, 2, (seed % 7) as u32 + 1));
let mut write_graph = DomainAccessGraph::default();
write_graph.access_count[2] = 4;
let causal_graph = CausalGraph {
edges: vec![CausalEdge {
from: CausalNode { domain: 0, lane: 1, hidden: false, step: 2 },
to: CausalNode { domain: 2, lane: 0, hidden: true, step: 2 },
weight: seed as i64 - 100,
}],
};
ExecutionTrace {
read_graph,
write_graph,
causal_graph,
information_flow: InformationFlowGraph { edges: vec![(0, 2, 9)] },
executor_divergence: DivergenceGraph { executor_count: 2, pairwise: vec![0.0, 0.5, 0.5, 0.0] },
temporal_graph: TemporalGraph { edges: vec![(1, 2, 3)] },
perturbation_response: PerturbationResponse::default(),
behavior_fingerprint: BehaviorFingerprint::from_features(vec![seed as i64, -2, 3]),
}
}
fn sample_delta(seed: u64) -> WorldDelta {
let mut observed = [0i64; LANES];
observed[0] = seed as i64;
WorldDelta {
domain_deltas: vec![DomainDelta { domain: DomainId(2), observed, hidden: [0i64; HIDDEN_LANES] }],
turn_advance: 0,
}
}
fn record(seed: u64) -> (ReplayRecord, ExecutionTrace, WorldDelta) {
let trace = sample_trace(seed);
let delta = sample_delta(seed);
let rr = ReplayRecord {
world_seed: seed,
program_seed: seed ^ 1,
contract_seed: seed ^ 2,
perturbation_seed: seed ^ 3,
trace_hash: trace.canonical_hash(),
delta_hash: delta.hash(),
future_hash: Hash(seed.wrapping_mul(0x9e3779b97f4a7c15)),
};
(rr, trace, delta)
}
/// Write a full evidence dir from real records. If `tamper_trace` is set,
/// that record's serialized trace is corrupted after its leaf was claimed.
fn write_full_evidence(dir: &Path, seeds: &[u64], tamper_trace: Option<usize>) {
let ev = dir.join("evidence");
std::fs::create_dir_all(&ev).unwrap();
let records: Vec<_> = seeds.iter().map(|&s| record(s)).collect();
let leaves: Vec<u64> = records.iter().map(|(rr, _, _)| rr.hash().0).collect();
let mut lt = String::from("leaf\n");
for l in leaves {
for l in &leaves {
lt.push_str(&format!("{:016x}\n", l));
}
std::fs::write(ev.join("leaves.tsv"), lt).unwrap();
let claims = format!(
"# claims\nroot\t{:016x}\nleaf_count\t{}\ntotal_comparisons\t{}\n",
claimed_root,
merkle_root(&leaves),
leaves.len(),
leaves.len()
);
std::fs::write(ev.join("claims.tsv"), claims).unwrap();
let mut traces = String::from("# trace evidence\n");
for (i, (rr, trace, delta)) in records.iter().enumerate() {
let mut trace_s = trace.serialize();
if tamper_trace == Some(i) {
// Corrupt the full trace without changing the claimed leaf.
trace_s = trace_s.replacen("trace-v1 ", "trace-v1 999 ", 1);
}
traces.push_str(&format!(
"{:016x} {:016x} {:016x} {:016x} {:016x} {:016x}\t{}\t{}\n",
rr.world_seed, rr.program_seed, rr.contract_seed, rr.perturbation_seed,
rr.future_hash.0, rr.hash().0, trace_s, delta.serialize(),
));
}
std::fs::write(ev.join("traces.tsv"), traces).unwrap();
}
#[test]
fn honest_evidence_attests() {
let dir = std::env::temp_dir().join("magicka_attest_ok");
let leaves = [10u64, 20, 30, 40, 50];
write_evidence(&dir, &leaves, merkle_root(&leaves));
write_full_evidence(&dir, &[10, 20, 30, 40, 50], None);
let att = verify_dir(&dir).unwrap();
assert!(att.ok, "honest evidence should attest: {:?}", att.checks);
assert_eq!(att.traces_verified, att.traces_total);
assert!(att.traces_total > 0);
}
/// Negative control: a tampered leaf changes the recomputed root, so the
/// claimed root no longer matches and attestation FAILS. Proves the
/// attestation is not vacuous and that the root genuinely binds the leaves.
/// claimed root no longer matches and attestation FAILS.
#[test]
fn tampered_leaf_breaks_attestation() {
let dir = std::env::temp_dir().join("magicka_attest_bad");
let leaves = [10u64, 20, 30, 40, 50];
let claimed = merkle_root(&leaves);
// Write evidence whose leaves were altered after the root was claimed.
let mut tampered = leaves.to_vec();
tampered[2] ^= 0xdead_beef;
write_evidence(&dir, &tampered, claimed);
let dir = std::env::temp_dir().join("magicka_attest_bad_leaf");
write_full_evidence(&dir, &[10, 20, 30, 40, 50], None);
// Flip a leaf in the file after the root was claimed.
let lp = dir.join("evidence/leaves.tsv");
let txt = std::fs::read_to_string(&lp).unwrap();
let mut lines: Vec<String> = txt.lines().map(|s| s.to_string()).collect();
lines[2] = format!("{:016x}", 0xdead_beefu64);
std::fs::write(&lp, lines.join("\n")).unwrap();
let att = verify_dir(&dir).unwrap();
assert!(!att.ok, "tampered leaves must fail attestation");
assert!(!att.ok, "tampered leaf must fail attestation");
assert!(att.checks.iter().any(|(n, ok)| n.contains("root") && !ok));
}
/// Negative control for finding 4: corrupting the FULL TRACE (without
/// touching the claimed leaf) makes the recomputed leaf disagree, so the
/// trace no longer attests. Proves the evidence is the full trace, not a
/// trusted digest.
#[test]
fn tampered_trace_breaks_attestation() {
let dir = std::env::temp_dir().join("magicka_attest_bad_trace");
write_full_evidence(&dir, &[10, 20, 30, 40, 50], Some(2));
let att = verify_dir(&dir).unwrap();
assert!(!att.ok, "tampered full trace must fail attestation");
assert!(att.traces_verified < att.traces_total);
assert!(att.checks.iter().any(|(n, ok)| n.contains("full trace") && !ok));
}
}
+4
View File
@@ -18,6 +18,10 @@ fn main() {
Some(r) => eprintln!(" claimed root: {:016x}", r),
None => eprintln!(" claimed root: <missing>"),
}
eprintln!(
" full traces verified: {}/{} (reconstructed + leaf re-derived)",
att.traces_verified, att.traces_total
);
for (name, ok) in &att.checks {
eprintln!(" [{}] {}", if *ok { "PASS" } else { "FAIL" }, name);
}