//! Interactive-worlds conformance behaviors (extensions/interactive-worlds). //! //! The load-bearing assertions are the convergence ones: same op set, //! any arrival order, identical state hash at every checkpoint — with //! no tolerance parameter anywhere. #![cfg(feature = "world")] use ed25519_dalek::SigningKey; use wai::world::sim::{kin_mutation, pack_fx, CAP_SIM_KINEMATIC0}; use wai::world::{ mint_node_id, CausalOp, Fx, Kinematic0, LiveWorld, Replayer, SimContract, WorldContainer, WorldError, WorldState, Xoshiro256pp, NUMERIC_FIXED64, RESOLVE_LWW, }; fn key(seed: u8) -> SigningKey { SigningKey::from_bytes(&[seed; 32]) } fn contract() -> SimContract { SimContract { tick_rate: 60, numeric: NUMERIC_FIXED64.into(), rng_alg: "xoshiro256++".into(), rng_seed: [0x42; 32], sim: CAP_SIM_KINEMATIC0.into(), resolve_default: RESOLVE_LWW.into(), checkpoint_every: 100, } } fn empty_container() -> WorldContainer { let c = contract(); let graph = WorldState::::new().canonical_bytes(&Kinematic0); WorldContainer { contract_bytes: c.to_canonical_json(), contract: c, graph_bytes: graph, assets: vec![], ops: vec![], seal: None, } } fn create_op(k: &SigningKey, lamport: u64, tick: u64, vel: [i32; 3]) -> CausalOp { let target = mint_node_id(&k.verifying_key().to_bytes(), lamport); let payload = pack_fx(&[ Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::from_int(vel[0]), Fx::from_int(vel[1]), Fx::from_int(vel[2]), Fx::ZERO, ]); CausalOp::sign(k, lamport, tick, vec![], target, kin_mutation::CREATE, payload) } /// A two-actor session: creates, impulses, scalar sets, a delete. fn session_ops() -> (Vec, [u8; 16], [u8; 16]) { let a = key(1); let b = key(2); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let n2 = mint_node_id(&b.verifying_key().to_bytes(), 1); let ops = vec![ create_op(&a, 1, 1, [1, 0, 0]), create_op(&b, 1, 1, [0, 1, 0]), CausalOp::sign(&a, 2, 50, vec![], n2, kin_mutation::IMPULSE, pack_fx(&[Fx::from_int(2), Fx::ZERO, Fx::ZERO])), CausalOp::sign(&b, 2, 50, vec![], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::from_int(7)])), CausalOp::sign(&a, 3, 120, vec![], n1, kin_mutation::SET_POS, pack_fx(&[Fx::ZERO, Fx::ZERO, Fx::from_int(5)])), CausalOp::sign(&b, 3, 180, vec![], n2, kin_mutation::DELETE, vec![]), ]; (ops, n1, n2) } fn sealed_container(ops: Vec, final_tick: u64) -> WorldContainer { let mut c = empty_container(); let mut live = LiveWorld::open(Kinematic0, c.clone()).unwrap(); for op in &ops { live.ingest(op.clone()).unwrap(); } let final_hash = live.advance_to(final_tick).unwrap(); c.ops = live.canonical_ops(); c.seal(&key(9), final_tick, final_hash); c } // --------------------------------------------------------------------------- #[test] fn op_wire_round_trip_and_tamper_detection() { let (ops, ..) = session_ops(); for op in &ops { let bytes = op.to_bytes(); let (back, used) = CausalOp::from_bytes(&bytes).unwrap(); assert_eq!(used, bytes.len()); assert_eq!(&back, op); back.verify().unwrap(); // Flip one mutation byte (or the tick for empty mutations): // signature must fail. let mut tampered = back.clone(); if tampered.mutation.is_empty() { tampered.effect_tick += 1; } else { tampered.mutation[0] ^= 1; } assert!(matches!(tampered.verify(), Err(WorldError::BadSignature))); } } #[test] fn node_minting_is_deterministic_and_actor_scoped() { let a = key(1).verifying_key().to_bytes(); let b = key(2).verifying_key().to_bytes(); assert_eq!(mint_node_id(&a, 1), mint_node_id(&a, 1)); assert_ne!(mint_node_id(&a, 1), mint_node_id(&a, 2)); assert_ne!(mint_node_id(&a, 1), mint_node_id(&b, 1)); } #[test] fn container_round_trips_and_ignores_unknown_sections() { let c = sealed_container(session_ops().0, 200); let bytes = c.to_bytes(); let back = WorldContainer::from_bytes(&bytes).unwrap(); assert_eq!(back.contract, c.contract); assert_eq!(back.graph_bytes, c.graph_bytes); assert_eq!(back.ops, c.ops); assert_eq!(back.seal, c.seal); // Append an unknown section kind to the table: reader must ignore it. let mut with_unknown = Vec::new(); with_unknown.extend_from_slice(&bytes[..4]); let n = u16::from_le_bytes(bytes[4..6].try_into().unwrap()); with_unknown.extend_from_slice(&(n + 1).to_le_bytes()); let table_end = 6 + n as usize * 9; with_unknown.extend_from_slice(&bytes[6..table_end]); let payloads_len = bytes.len() - table_end; with_unknown.push(0x7f); // unknown kind with_unknown.extend_from_slice(&(payloads_len as u32).to_le_bytes()); with_unknown.extend_from_slice(&8u32.to_le_bytes()); with_unknown.extend_from_slice(&bytes[table_end..]); with_unknown.extend_from_slice(&[0u8; 8]); let back2 = WorldContainer::from_bytes(&with_unknown).unwrap(); assert_eq!(back2.ops, c.ops); } #[test] fn sealed_replay_is_deterministic() { let c = sealed_container(session_ops().0, 200); let r = Replayer::new(Kinematic0); let cp1 = r.run_sealed(&c).unwrap(); let cp2 = r.run_sealed(&c).unwrap(); assert_eq!(cp1, cp2); // checkpoints at 100, 200 (cadence 100, final 200) assert_eq!(cp1.iter().map(|c| c.tick).collect::>(), vec![100, 200]); } #[test] fn replay_equivalence_under_arrival_order() { let (ops, ..) = session_ops(); let sealed = sealed_container(ops.clone(), 200); let reference = Replayer::new(Kinematic0).run_sealed(&sealed).unwrap(); // Deterministic shuffles via the registered rng — no test-order // nondeterminism allowed even in tests. let mut rng = Xoshiro256pp::from_seed([5u8; 32]); for _ in 0..16 { let mut shuffled = ops.clone(); for i in (1..shuffled.len()).rev() { let j = (rng.next_u64() % (i as u64 + 1)) as usize; shuffled.swap(i, j); } let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); for op in shuffled { live.ingest(op).unwrap(); } for cp in &reference { assert_eq!(live.advance_to(cp.tick).unwrap(), cp.hash, "arrival order changed state"); } } } #[test] fn lww_conflict_resolves_by_canonical_order_not_arrival() { let a = key(1); let b = key(2); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let create = create_op(&a, 1, 1, [0, 0, 0]); // Same tick, same node, same kind: canonical order is // (tick, lamport, actor) — b's op has higher lamport, so b wins // regardless of which arrives first. let set_a = CausalOp::sign(&a, 2, 10, vec![], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::from_int(1)])); let set_b = CausalOp::sign(&b, 3, 10, vec![], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::from_int(2)])); let mut h = Vec::new(); for order in [[&create, &set_a, &set_b], [&create, &set_b, &set_a]] { let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); for op in order { live.ingest(op.clone()).unwrap(); } h.push(live.advance_to(10).unwrap()); } assert_eq!(h[0], h[1]); // And the winner is b (lamport 3 > 2): replay sequentially and check. let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); for op in [&create, &set_b] { live.ingest(op.clone()).unwrap(); } let only_b = live.advance_to(10).unwrap(); assert_eq!(h[0], only_b, "LWW winner must be the canonical-order last writer"); } #[test] fn sum_conflict_is_order_independent_and_both_act() { let a = key(1); let b = key(2); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let create = create_op(&a, 1, 1, [0, 0, 0]); // Equal opposite impulses at the same tick: both act, net zero. let push = CausalOp::sign(&a, 2, 10, vec![], n1, kin_mutation::IMPULSE, pack_fx(&[Fx::from_int(3), Fx::ZERO, Fx::ZERO])); let pull = CausalOp::sign(&b, 2, 10, vec![], n1, kin_mutation::IMPULSE, pack_fx(&[Fx::from_int(-3), Fx::ZERO, Fx::ZERO])); let mut hashes = Vec::new(); for order in [[&create, &push, &pull], [&create, &pull, &push]] { let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); for op in order { live.ingest(op.clone()).unwrap(); } hashes.push(live.advance_to(60).unwrap()); } assert_eq!(hashes[0], hashes[1]); // Net zero: equals the no-impulse world. let mut still = LiveWorld::open(Kinematic0, empty_container()).unwrap(); still.ingest(create.clone()).unwrap(); assert_eq!(hashes[0], still.advance_to(60).unwrap(), "opposite impulses must cancel"); } #[test] fn dep_gating_buffers_until_dep_arrives() { let a = key(1); let create = create_op(&a, 1, 1, [0, 0, 0]); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let dependent = CausalOp::sign( &a, 2, 5, vec![create.id()], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::from_int(9)]), ); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); assert_eq!(live.ingest(dependent.clone()).unwrap(), wai::world::replay::Ingest::Buffered); assert_eq!(live.pending_len(), 1); assert_eq!(live.ingest(create.clone()).unwrap(), wai::world::replay::Ingest::Applied); assert_eq!(live.pending_len(), 0, "buffered op must drain once dep admitted"); // Equals the in-order session. let mut inorder = LiveWorld::open(Kinematic0, empty_container()).unwrap(); inorder.ingest(create).unwrap(); inorder.ingest(dependent).unwrap(); assert_eq!(live.advance_to(10).unwrap(), inorder.advance_to(10).unwrap()); } #[test] fn past_tick_arrival_rederives() { let a = key(1); let create = create_op(&a, 1, 1, [1, 0, 0]); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let late = CausalOp::sign(&a, 2, 30, vec![], n1, kin_mutation::IMPULSE, pack_fx(&[Fx::from_int(1), Fx::ZERO, Fx::ZERO])); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); live.ingest(create.clone()).unwrap(); let _ = live.advance_to(100).unwrap(); // engine is already past tick 30 live.ingest(late.clone()).unwrap(); // late op for tick 30 let rederived = live.advance_to(100).unwrap(); let mut inorder = LiveWorld::open(Kinematic0, empty_container()).unwrap(); inorder.ingest(create).unwrap(); inorder.ingest(late).unwrap(); assert_eq!(rederived, inorder.advance_to(100).unwrap(), "late arrival must converge"); } #[test] fn receipted_checkpoint_seals_history() { let a = key(1); let create = create_op(&a, 1, 1, [0, 0, 0]); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); live.ingest(create).unwrap(); live.advance_to(100).unwrap(); live.seal_checkpoint(100); let intruder = CausalOp::sign(&a, 2, 50, vec![], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::ONE])); assert!(matches!( live.ingest(intruder), Err(WorldError::SealedHistory { effect_tick: 50, sealed_tick: 100 }) )); } #[test] fn duplicate_is_idempotent_equivocation_is_rejected() { let a = key(1); let create = create_op(&a, 1, 1, [0, 0, 0]); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); live.ingest(create.clone()).unwrap(); assert_eq!(live.ingest(create.clone()).unwrap(), wai::world::replay::Ingest::Duplicate); // Same (actor, lamport), different content: equivocation. let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let evil = CausalOp::sign(&a, 1, 2, vec![], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::ONE])); assert!(matches!(live.ingest(evil), Err(WorldError::DuplicateOp(_)))); } #[test] fn seal_verifies_and_tamper_fails() { let c = sealed_container(session_ops().0, 200); let publisher = key(9).verifying_key(); c.verify_seal(&publisher).unwrap(); // Wrong publisher. assert!(matches!(c.verify_seal(&key(8).verifying_key()), Err(WorldError::BadSignature))); // Tampered final hash: signature breaks; and even with a re-signed // seal under a different key, the replayer catches the hash itself. let mut tampered = c.clone(); if let Some(seal) = &mut tampered.seal { seal.final_hash[0] ^= 1; } assert!(matches!(tampered.verify_seal(&publisher), Err(WorldError::BadSignature))); assert!(matches!( Replayer::new(Kinematic0).run_sealed(&tampered), Err(WorldError::HashMismatch { .. }) )); } #[test] fn replay_rejects_non_canonical_or_dep_incomplete_logs() { let mut c = sealed_container(session_ops().0, 200); c.ops.swap(0, 1); assert!(matches!( Replayer::new(Kinematic0).run_sealed(&c), Err(WorldError::NotCanonicalOrder) )); // Dep-incomplete: dependent present, dep removed. let a = key(1); let create = create_op(&a, 1, 1, [0, 0, 0]); let n1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let dependent = CausalOp::sign( &a, 2, 5, vec![create.id()], n1, kin_mutation::SET_SCALAR, pack_fx(&[Fx::ONE]), ); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); live.ingest(create).unwrap(); live.ingest(dependent.clone()).unwrap(); let final_hash = live.advance_to(10).unwrap(); let mut broken = empty_container(); broken.ops = vec![dependent]; // dep stripped broken.seal(&key(9), 10, final_hash); assert!(matches!( Replayer::new(Kinematic0).run_sealed(&broken), Err(WorldError::DepMissing(_)) )); } #[test] fn create_with_unminted_target_is_rejected() { let a = key(1); let bogus_target = [0xee; 16]; let op = CausalOp::sign( &a, 1, 1, vec![], bogus_target, kin_mutation::CREATE, pack_fx(&[Fx::ZERO; 7]), ); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); live.ingest(op).unwrap(); // admission is fine; application fails assert!(matches!(live.advance_to(1), Err(WorldError::BadNodeMint))); } #[test] fn sim_dispatch_routes_by_contract_and_reports_inert() { use wai::world::run_sealed_dispatch; use wai::world::sim::{bal_mutation, BalNode, CAP_SIM_BALLISTIC0}; use wai::world::{Ballistic0, WorldState}; // A ballistic world: ball dropped from y=10, restitution ½, 300 ticks. let mut c = empty_container(); c.contract.sim = CAP_SIM_BALLISTIC0.into(); c.contract_bytes = c.contract.to_canonical_json(); c.graph_bytes = WorldState::::new().canonical_bytes(&Ballistic0); let a = key(1); let target = mint_node_id(&a.verifying_key().to_bytes(), 1); let half = Fx::ONE / Fx::from_int(2); let drop = CausalOp::sign( &a, 1, 1, vec![], target, bal_mutation::CREATE, pack_fx(&[Fx::ZERO, Fx::from_int(10), Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE, half]), ); let mut live = LiveWorld::open(Ballistic0, c.clone()).unwrap(); live.ingest(drop).unwrap(); let final_hash = live.advance_to(300).unwrap(); c.ops = live.canonical_ops(); c.seal(&key(9), 300, final_hash); // Dispatch picks ballistic0 from the contract and replays to the // same checkpoints, twice (bounces are bit-exact, not approximate). let cp1 = run_sealed_dispatch(&c).unwrap(); let cp2 = run_sealed_dispatch(&c).unwrap(); assert_eq!(cp1, cp2); assert_eq!(cp1.last().unwrap().hash, final_hash); // The kinematic engine must NOT accept a ballistic log: its CREATE // payload is 8 Fx, kinematic0's is 7 — a different registration. assert!(Replayer::new(Kinematic0).run_sealed(&c).is_err()); // An unregistered sim capability renders the world inert, cleanly. let mut alien = empty_container(); alien.contract.sim = "wai.world.sim.does_not_exist".into(); alien.contract_bytes = alien.contract.to_canonical_json(); alien.seal(&key(9), 1, [0u8; 32]); assert!(matches!( run_sealed_dispatch(&alien), Err(WorldError::UnknownSimCapability(_)) )); } #[test] fn ballistic_ball_settles_on_the_ground_plane() { use wai::world::sim::{bal_mutation, BalNode, CAP_SIM_BALLISTIC0}; use wai::world::{Ballistic0, WorldState}; let mut c = empty_container(); c.contract.sim = CAP_SIM_BALLISTIC0.into(); c.contract_bytes = c.contract.to_canonical_json(); c.graph_bytes = WorldState::::new().canonical_bytes(&Ballistic0); let a = key(1); let target = mint_node_id(&a.verifying_key().to_bytes(), 1); let drop = CausalOp::sign( &a, 1, 1, vec![], target, bal_mutation::CREATE, pack_fx(&[ Fx::ZERO, Fx::from_int(10), Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE, Fx::ONE / Fx::from_int(2), ]), ); let mut live = LiveWorld::open(Ballistic0, c).unwrap(); live.ingest(drop).unwrap(); // After 20 simulated seconds a restitution-½ ball has long settled: // height ≈ radius, vertical speed ≈ 0. We assert physical bounds, // not exact bits, here — the corpus pins the exact bits. live.advance_to(1200).unwrap(); let ops = live.canonical_ops(); assert_eq!(ops.len(), 1); // Re-derive state to inspect: replay through a fresh engine. let mut fresh = LiveWorld::open(Ballistic0, { let mut c2 = empty_container(); c2.contract.sim = CAP_SIM_BALLISTIC0.into(); c2.contract_bytes = c2.contract.to_canonical_json(); c2.graph_bytes = WorldState::::new().canonical_bytes(&Ballistic0); c2 }) .unwrap(); for op in ops { fresh.ingest(op).unwrap(); } let h1 = fresh.advance_to(1200).unwrap(); let h2 = live.advance_to(1200).unwrap(); assert_eq!(h1, h2, "settled state must be identical across engines"); } // --------------------------------------------------------------------------- // truss0 — the solver-backed registrant // --------------------------------------------------------------------------- mod truss { use super::*; use wai::world::truss::{pack_member, truss_mutation, CAP_SIM_TRUSS0}; use wai::world::{Sim, Truss0, TrussNode, WorldState, Xoshiro256pp}; fn truss_container() -> WorldContainer { let mut c = empty_container(); c.contract.sim = CAP_SIM_TRUSS0.into(); c.contract_bytes = c.contract.to_canonical_json(); c.graph_bytes = WorldState::::new().canonical_bytes(&Truss0); c } fn particle(k: &SigningKey, lamport: u64, tick: u64, pos: [i32; 3], inv_mass: i32) -> CausalOp { let target = mint_node_id(&k.verifying_key().to_bytes(), lamport); CausalOp::sign( k, lamport, tick, vec![], target, truss_mutation::CREATE_PARTICLE, pack_fx(&[ Fx::from_int(pos[0]), Fx::from_int(pos[1]), Fx::from_int(pos[2]), Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::from_int(inv_mass), ]), ) } /// Two free particles 2 units apart, joined by a yield-10% member; /// two actors apply equal-opposite impulses to the two ends at the /// same tick (the SUM rule feeding the solver). fn tug_of_war_ops() -> Vec { let a = key(1); let b = key(2); let pa = mint_node_id(&a.verifying_key().to_bytes(), 1); let pb = mint_node_id(&b.verifying_key().to_bytes(), 1); let tenth = Fx::ONE / Fx::from_int(10); vec![ particle(&a, 1, 1, [-1, 5, 0], 1), particle(&b, 1, 1, [1, 5, 0], 1), CausalOp::sign( &a, 2, 2, vec![], mint_node_id(&a.verifying_key().to_bytes(), 2), truss_mutation::CREATE_MEMBER, pack_member(&pa, &pb, tenth), ), CausalOp::sign(&a, 3, 10, vec![], pa, truss_mutation::IMPULSE, pack_fx(&[Fx::from_int(-20), Fx::ZERO, Fx::ZERO])), CausalOp::sign(&b, 3, 10, vec![], pb, truss_mutation::IMPULSE, pack_fx(&[Fx::from_int(20), Fx::ZERO, Fx::ZERO])), ] } #[test] fn member_constrains_within_yield_and_gravity_acts() { // Anchor at (0,5,0); free mass at (2,5,0); generous yield. The // mass swings under gravity but the solver holds it at rest // distance from the anchor; the member survives. let a = key(1); let anchor = particle(&a, 1, 1, [0, 5, 0], 0); let mass = particle(&a, 2, 1, [2, 5, 0], 1); let anchor_id = mint_node_id(&a.verifying_key().to_bytes(), 1); let mass_id = mint_node_id(&a.verifying_key().to_bytes(), 2); let member = CausalOp::sign( &a, 3, 1, vec![], mint_node_id(&a.verifying_key().to_bytes(), 3), truss_mutation::CREATE_MEMBER, pack_member(&anchor_id, &mass_id, Fx::ONE / Fx::from_int(2)), ); let sim = Truss0; let c = truss_container(); let mut world = WorldState::::new(); let mut rng = Xoshiro256pp::from_seed(c.contract.rng_seed); for op in [&anchor, &mass, &member] { sim.apply(&mut world, op).unwrap(); } for t in 1..=300u64 { sim.step(&mut world, &c.contract, t, &mut rng); } let member_count = world.nodes.values().filter(|n| matches!(n, TrussNode::Member { .. })).count(); assert_eq!(member_count, 1, "within-yield member must survive the swing"); let (apos, mpos) = match (&world.nodes[&anchor_id], &world.nodes[&mass_id]) { (TrussNode::Particle { pos: ap, .. }, TrussNode::Particle { pos: mp, .. }) => (*ap, *mp), _ => panic!("particles missing"), }; assert_eq!(apos[1], Fx::from_int(5), "anchor (inv_mass 0) must not move"); assert!(mpos[1] < Fx::from_int(5), "gravity must pull the mass below its start"); // Constraint held: distance from anchor stays within yield of rest=2. let d = [mpos[0] - apos[0], mpos[1] - apos[1], mpos[2] - apos[2]]; let dist = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt(); assert!(dist > Fx::ONE && dist < Fx::from_int(3), "solver must hold the member near rest length"); } #[test] fn tug_of_war_fractures_the_member_deterministically() { // Direct-state check: after the opposing impulses, the member // is GONE — overload is an emergent solver consequence. let sim = Truss0; let c = truss_container(); let mut world = WorldState::::new(); let mut rng = Xoshiro256pp::from_seed(c.contract.rng_seed); let ops = tug_of_war_ops(); for t in 1..=20u64 { for op in ops.iter().filter(|o| o.effect_tick == t) { sim.apply(&mut world, op).unwrap(); } sim.step(&mut world, &c.contract, t, &mut rng); } let member_count = world.nodes.values().filter(|n| matches!(n, TrussNode::Member { .. })).count(); assert_eq!(member_count, 0, "overloaded member must fracture"); // Convergence: both arrival orders of the conflicting impulses // reach the identical state hash, fracture included. let mut hashes = Vec::new(); for rev in [false, true] { let mut order = tug_of_war_ops(); if rev { order.swap(3, 4); } let mut live = LiveWorld::open(Truss0, truss_container()).unwrap(); for op in order { live.ingest(op).unwrap(); } hashes.push(live.advance_to(120).unwrap()); } assert_eq!(hashes[0], hashes[1], "fracture must be arrival-order independent"); // And it differs from the unloaded world (the impulses mattered). let mut calm = LiveWorld::open(Truss0, truss_container()).unwrap(); for op in tug_of_war_ops().into_iter().take(3) { calm.ingest(op).unwrap(); } assert_ne!(hashes[0], calm.advance_to(120).unwrap()); } #[test] fn truss_dispatches_and_replays_sealed() { let mut c = truss_container(); let mut live = LiveWorld::open(Truss0, c.clone()).unwrap(); for op in tug_of_war_ops() { live.ingest(op).unwrap(); } let final_hash = live.advance_to(120).unwrap(); c.ops = live.canonical_ops(); c.seal(&key(9), 120, final_hash); use wai::world::run_sealed_dispatch; let cp1 = run_sealed_dispatch(&c).unwrap(); let cp2 = run_sealed_dispatch(&c).unwrap(); assert_eq!(cp1, cp2); assert_eq!(cp1.last().unwrap().hash, final_hash); } } #[test] fn effect_tick_zero_is_rejected() { let a = key(1); let op = create_op(&a, 1, 0, [0, 0, 0]); let mut live = LiveWorld::open(Kinematic0, empty_container()).unwrap(); assert!(matches!(live.ingest(op), Err(WorldError::EffectTickZero))); } // --------------------------------------------------------------------------- // truss0 — the solver-backed registrant // --------------------------------------------------------------------------- mod truss0 { use super::*; use wai::world::run_sealed_dispatch; use wai::world::sim::pack_fx; use wai::world::truss::{pack_member, truss_mutation, Truss0, TrussNode, CAP_SIM_TRUSS0}; fn truss_container() -> WorldContainer { let mut c = contract(); c.sim = CAP_SIM_TRUSS0.into(); WorldContainer { contract_bytes: c.to_canonical_json(), contract: c, graph_bytes: WorldState::::new().canonical_bytes(&Truss0), assets: vec![], ops: vec![], seal: None, } } fn particle(k: &SigningKey, lamport: u64, tick: u64, pos: [i32; 3], inv_mass: Fx) -> CausalOp { let target = mint_node_id(&k.verifying_key().to_bytes(), lamport); CausalOp::sign( k, lamport, tick, vec![], target, truss_mutation::CREATE_PARTICLE, pack_fx(&[ Fx::from_int(pos[0]), Fx::from_int(pos[1]), Fx::from_int(pos[2]), Fx::ZERO, Fx::ZERO, Fx::ZERO, inv_mass, ]), ) } fn member(k: &SigningKey, lamport: u64, tick: u64, a: [u8; 16], b: [u8; 16], yield_strain: Fx) -> CausalOp { let target = mint_node_id(&k.verifying_key().to_bytes(), lamport); CausalOp::sign( k, lamport, tick, vec![], target, truss_mutation::CREATE_MEMBER, pack_member(&a, &b, yield_strain), ) } fn pos_of(state: &WorldState, id: &[u8; 16]) -> Option<[f64; 3]> { match state.nodes.get(id) { Some(TrussNode::Particle { pos, .. }) => { Some([pos[0].approx_f64(), pos[1].approx_f64(), pos[2].approx_f64()]) } _ => None, } } #[test] fn pendulum_constraint_holds_through_the_swing() { // Two solver guarantees, sampled mid-swing and at rest: the // CONSTRAINT (the mass stays at rest-length from the anchor at // every sampled tick, the member never fractures under a // static load) and, thanks to the registration's 1/64-per-tick // damping, EQUILIBRIUM — by 15 simulated seconds the mass // hangs directly below the anchor. Trajectory bit-reproducible // throughout. let a = key(1); let anchor_id = mint_node_id(&a.verifying_key().to_bytes(), 1); let mass_id = mint_node_id(&a.verifying_key().to_bytes(), 2); let ops = vec![ particle(&a, 1, 1, [0, 5, 0], Fx::ZERO), // anchor particle(&a, 2, 1, [2, 5, 0], Fx::ONE), // free mass, 2 to the side member(&a, 3, 1, anchor_id, mass_id, Fx::from_int(10)), // rest = 2, generous yield ]; let mut live = LiveWorld::open(Truss0, truss_container()).unwrap(); for op in &ops { live.ingest(op.clone()).unwrap(); } let mut h_final = [0u8; 32]; for t in [60u64, 240, 600, 900] { h_final = live.advance_to(t).unwrap(); assert_eq!(live.state().nodes.len(), 3, "member must survive a swing"); let p = pos_of(live.state(), &mass_id).unwrap(); let dist = (p[0] * p[0] + (p[1] - 5.0) * (p[1] - 5.0) + p[2] * p[2]).sqrt(); assert!((dist - 2.0).abs() < 0.05, "constraint must hold at tick {t}: |r|={dist}"); assert!(p[1] <= 5.001, "the mass cannot rise above its anchor height, y={}", p[1]); } // Damped equilibrium: hanging straight down by t=900. let p = pos_of(live.state(), &mass_id).unwrap(); assert!(p[0].abs() < 0.1, "damping must settle x under the anchor, got {}", p[0]); assert!((p[1] - 3.0).abs() < 0.1, "must hang rest-length below the anchor, got y={}", p[1]); // Determinism: a fresh engine reaches the identical hash. let mut fresh = LiveWorld::open(Truss0, truss_container()).unwrap(); for op in &ops { fresh.ingest(op.clone()).unwrap(); } assert_eq!(fresh.advance_to(900).unwrap(), h_final); } #[test] fn tug_of_war_fractures_the_member_and_converges() { // The cnotes scenario, made conformance: two actors pull the same // member in opposite directions. Both impulses act (SUM rule), // the member sees real tension, exceeds its yield strain, snaps. let a = key(1); let b = key(2); let p1 = mint_node_id(&a.verifying_key().to_bytes(), 1); let p2 = mint_node_id(&b.verifying_key().to_bytes(), 1); let quarter = Fx::ONE / Fx::from_int(4); let setup = vec![ particle(&a, 1, 1, [-1, 5, 0], Fx::ONE), particle(&b, 1, 1, [1, 5, 0], Fx::ONE), member(&a, 2, 1, p1, p2, quarter), // rest = 2, yields at 25% strain ]; let pull_a = CausalOp::sign(&a, 3, 10, vec![], p1, truss_mutation::IMPULSE, pack_fx(&[Fx::from_int(-20), Fx::ZERO, Fx::ZERO])); let pull_b = CausalOp::sign(&b, 3, 10, vec![], p2, truss_mutation::IMPULSE, pack_fx(&[Fx::from_int(20), Fx::ZERO, Fx::ZERO])); let mut hashes = Vec::new(); for order in [[&pull_a, &pull_b], [&pull_b, &pull_a]] { let mut live = LiveWorld::open(Truss0, truss_container()).unwrap(); for op in &setup { live.ingest(op.clone()).unwrap(); } for op in order { live.ingest((*op).clone()).unwrap(); } let h = live.advance_to(120).unwrap(); // The member is gone; the two particles remain. assert_eq!(live.state().nodes.len(), 2, "overloaded member must fracture"); hashes.push(h); } assert_eq!(hashes[0], hashes[1], "fracture must be arrival-order independent"); // Control: gentler opposing pulls stay under yield — member holds. let soft_a = CausalOp::sign(&a, 3, 10, vec![], p1, truss_mutation::IMPULSE, pack_fx(&[Fx::from_int(-5), Fx::ZERO, Fx::ZERO])); let soft_b = CausalOp::sign(&b, 3, 10, vec![], p2, truss_mutation::IMPULSE, pack_fx(&[Fx::from_int(5), Fx::ZERO, Fx::ZERO])); let mut live = LiveWorld::open(Truss0, truss_container()).unwrap(); for op in &setup { live.ingest(op.clone()).unwrap(); } live.ingest(soft_a).unwrap(); live.ingest(soft_b).unwrap(); live.advance_to(120).unwrap(); assert_eq!(live.state().nodes.len(), 3, "a load under yield must NOT fracture"); } #[test] fn truss_dispatches_and_seals() { let a = key(1); let anchor_id = mint_node_id(&a.verifying_key().to_bytes(), 1); let mass_id = mint_node_id(&a.verifying_key().to_bytes(), 2); let ops = vec![ particle(&a, 1, 1, [0, 5, 0], Fx::ZERO), particle(&a, 2, 1, [2, 5, 0], Fx::ONE), member(&a, 3, 1, anchor_id, mass_id, Fx::from_int(10)), ]; let mut c = truss_container(); let mut live = LiveWorld::open(Truss0, c.clone()).unwrap(); for op in &ops { live.ingest(op.clone()).unwrap(); } let final_hash = live.advance_to(200).unwrap(); c.ops = live.canonical_ops(); c.seal(&key(9), 200, final_hash); // Wire round-trip, then dispatch by contract.sim. let back = WorldContainer::from_bytes(&c.to_bytes()).unwrap(); let cps = run_sealed_dispatch(&back).unwrap(); assert_eq!(cps.last().unwrap().hash, final_hash); } } // --------------------------------------------------------------------------- // Session receipts (extension §8, JWP profile) // --------------------------------------------------------------------------- mod session_receipt { use super::*; use wai::world::{world_hash, SessionReceipt}; #[test] fn live_session_seals_to_replay_and_a_verifiable_receipt() { // Play a real two-actor session, seal it to a wai.world.replay, // and build the §8 receipt over the same ops + checkpoints. let (ops, ..) = session_ops(); let final_tick = 200u64; let mut c = empty_container(); let mut live = LiveWorld::open(Kinematic0, c.clone()).unwrap(); for op in &ops { live.ingest(op.clone()).unwrap(); } let final_hash = live.advance_to(final_tick).unwrap(); let canon = live.canonical_ops(); c.ops = canon.clone(); c.seal(&key(9), final_tick, final_hash); // Replay artifact verifies as before. let r = Replayer::new(Kinematic0).run_sealed(&c).unwrap(); // Receipt commits to the SAME ops + checkpoints the replay produced. let wh = world_hash(&c.contract_bytes, &c.graph_bytes); let (checkpoints, work) = wai::world::collect_checkpoints_and_work(&Kinematic0, &c, final_tick).unwrap(); assert_eq!(checkpoints, r, "collect_checkpoints must match run_sealed"); assert!(work > 0, "a session that ran ticks has non-zero node-step work"); let receipt = SessionReceipt::build( &key(9), "did:key:publisher", wh, 0, &canon, &checkpoints, 87_900_000, work, None, ); assert!(receipt.verify()); assert!(receipt.matches_ops(&canon)); assert_eq!(receipt.work_steps, work); // The receipt's last checkpoint hash IS the sealed final hash — // receipt and replay agree on the world's end state. assert_eq!(receipt.checkpoints.last().unwrap().hash, final_hash); // A receipt over a DIFFERENT op log fails the binding. let mut tampered = canon.clone(); tampered.pop(); assert!(!receipt.matches_ops(&tampered)); } #[test] fn world_hash_is_scene_identity_not_session() { // Same scene, two different op logs → same world_hash (it's the // shareable world identity, §7.4), but different receipts. let c = empty_container(); let wh1 = world_hash(&c.contract_bytes, &c.graph_bytes); let wh2 = world_hash(&c.contract_bytes, &c.graph_bytes); assert_eq!(wh1, wh2); // A truss scene has a different identity. let tc = { let mut k = contract(); k.sim = wai::world::sim::CAP_SIM_BALLISTIC0.into(); k.to_canonical_json() }; assert_ne!(wh1, world_hash(&tc, &c.graph_bytes)); } } // --------------------------------------------------------------------------- // Assets (extension §4.1) — content-addressed WAI objects, presentation // only, EXCLUDED from the state hash. // --------------------------------------------------------------------------- mod assets { use super::*; use wai::world::{asset_content_hash, world_hash, AssetEntry}; use wai::{Conditioning, Manifest, ModelRequirement, Wai}; /// A WAI-wrapped asset envelope (here a stand-in payload; the /// gallery generator wraps a real .glb the same way). fn wrap_asset(payload: &[u8]) -> Vec { let manifest = Manifest::new("mesh", "replicate", ModelRequirement::new("wai.mesh.glb"), Conditioning::new("glb")) .with_target(serde_json::json!({})); Wai::new(manifest, payload.to_vec()).to_bytes().unwrap() } #[test] fn assets_do_not_affect_state_or_world_hash() { let ops = session_ops().0; let final_tick = 200u64; // Bare scene. let bare = sealed_container(ops.clone(), final_tick); let bare_hashes = Replayer::new(Kinematic0).run_sealed(&bare).unwrap(); let bare_world = world_hash(&bare.contract_bytes, &bare.graph_bytes); // Same scene + an asset table. let mut with_asset = sealed_container(ops, final_tick); let env = wrap_asset(b"pretend-mesh-bytes"); with_asset.assets.push(AssetEntry { hash: asset_content_hash(&env), role: 0, name: "cube.glb".into(), }); // Re-seal: the seal binds contract/graph/oplog, NOT the asset // table, so the same seal is valid — assets are out of band. let with_hashes = Replayer::new(Kinematic0).run_sealed(&with_asset).unwrap(); assert_eq!(bare_hashes, with_hashes, "assets must not change any state hash"); assert_eq!( bare_world, world_hash(&with_asset.contract_bytes, &with_asset.graph_bytes), "assets must not change the world identity" ); } #[test] fn asset_table_round_trips_and_hash_binds_the_envelope() { let env = wrap_asset(b"glb-payload-v1"); let h = asset_content_hash(&env); let mut c = sealed_container(session_ops().0, 200); c.assets.push(AssetEntry { hash: h, role: 2, name: "tower.glb".into() }); let back = WorldContainer::from_bytes(&c.to_bytes()).unwrap(); assert_eq!(back.assets.len(), 1); assert_eq!(back.assets[0].hash, h); assert_eq!(back.assets[0].name, "tower.glb"); assert_eq!(back.assets[0].role, 2); // The hash binds the exact envelope bytes: any mutation breaks it. let mut tampered = env.clone(); *tampered.last_mut().unwrap() ^= 1; assert_ne!(asset_content_hash(&tampered), h); } }