//! `wai.world.model` — **replay-equivalence on a signed action trace**: an //! interactive world-model session is carried as a log of signed action ops, which //! replays — under canonical total order, regardless of arrival — to one portable //! session hash on every machine. The renderer (the world model) is declared by id; //! the trace is the conformant artifact. Never skips. #![cfg(feature = "world_model")] use ed25519_dalek::SigningKey; use wai::world_model::{decode_action, hex32, replay, sign_action, ModelHeader}; fn key(s: u8) -> SigningKey { SigningKey::from_bytes(&[s; 32]) } fn header() -> ModelHeader { ModelHeader { model_id: [7u8; 32], seed: 0x00C0FFEE, width: 128, height: 128, fps: 24 } } /// A two-actor arrow-key walk, deliberately out of order. fn trace() -> Vec { let (a, b) = (key(1), key(2)); vec![ sign_action(&a, 1, 1, 1, 0, 0x01, b""), // a: +x sign_action(&b, 1, 1, 0, 1, 0x02, b""), // b: +y sign_action(&a, 2, 3, 0, -1, 0x04, b"jump"), // a later, with payload sign_action(&b, 2, 2, -1, 0, 0x00, b""), // b: -x ] } #[test] fn action_trace_replays_to_a_portable_session_hash() { let h = header(); let ops = trace(); let s = replay(&h, &ops, true).expect("replay verifies + chains"); assert_eq!(s.n_steps, 4); // canonical order is (effect_tick, lamport, actor): ticks 1,1,2,3. assert_eq!(s.actions.iter().map(|a| a.tick).collect::>(), vec![1, 1, 2, 3]); assert_eq!(s.actions[2].payload, b"".to_vec()); // tick-2 op is b's -x (no payload) assert_eq!(s.actions[3].payload, b"jump".to_vec()); // REPLAY-EQUIVALENCE: shuffled arrival → identical session hash, no tolerance. let mut shuffled = ops.clone(); shuffled.reverse(); let s2 = replay(&h, &shuffled, true).expect("replay"); assert_eq!(s.session_hash, s2.session_hash, "session hash must be order-independent"); // The portable, committed session hash (deterministic Ed25519 + canonical order // + BLAKE3 chain): byte-identical on every machine. let got = hex32(&s.session_hash); println!("world.model session_hash = {got}"); assert_eq!(got, "bdabab2b551c4f164d3737f178698e748d58cacd96059b28e63f1a4ff5c8882a", "portable session hash"); // TAMPER-EVIDENCE: change one button bit → a different session hash. let (a, b) = (key(1), key(2)); let tampered = vec![ sign_action(&a, 1, 1, 1, 0, 0x01, b""), sign_action(&b, 1, 1, 0, 1, 0x02, b""), sign_action(&a, 2, 3, 0, -1, 0x05, b"jump"), // 0x04 -> 0x05 sign_action(&b, 2, 2, -1, 0, 0x00, b""), ]; let st = replay(&h, &tampered, true).expect("replay"); assert_ne!(st.session_hash, s.session_hash, "any action edit must change the hash"); // A different declared model_id ⇒ a different session, even with the same trace. let h2 = ModelHeader { model_id: [9u8; 32], ..header() }; assert_ne!(replay(&h2, &ops, true).unwrap().session_hash, s.session_hash, "model_id is bound"); } #[test] fn frames_get_a_signed_energy_accounted_provenance_receipt() { use wai::world_model::frame_receipt; let s = replay(&header(), &trace(), true).unwrap(); // three rendered frames, each with measured microjoules let frames = [([0x11u8; 32], 1_200u64), ([0x22u8; 32], 1_350u64), ([0x33u8; 32], 1_180u64)]; let r = frame_receipt(&key(9), "sink:world-model-runner", s.session_hash, &frames); assert!(r.verify(), "the provenance receipt must verify"); assert_eq!(r.total_joules_micro, 1_200 + 1_350 + 1_180, "joules accounted across frames"); assert_eq!(r.capability, "wai.world.model"); assert_eq!(r.asset_hash, [0x33u8; 32], "asset = last rendered frame"); assert_eq!(r.steps[0].input_hash, s.session_hash, "lineage originates at the session"); } #[test] fn decode_round_trips_the_action_body() { let op = sign_action(&key(3), 5, 42, -7, 9, 0xABCD, b"look"); let act = decode_action(&op).expect("decode"); assert_eq!((act.tick, act.dx, act.dy, act.buttons, act.payload), (42, -7, 9, 0xABCD, b"look".to_vec())); assert!(op.verify().is_ok(), "the action op is a valid signed CausalOp"); }