open-standards/wai/wai-rs/tests/world_replay.rs
Transaction Science 22928147f3 wai: the parameter-set pin, component identity, pin-aware selection
SPEC §3.1 defines the pin a manifest carries (conditioning.prior, and a
WAI2 rendition's prior): id, sha256, determinism, and the optional form,
tier and supersedes. The digest covers the whole parameter set. A set of
several files is pinned through its listing, the new registered prior
form wai.prior.set. A sink verifies the pin before it decodes, reports a
refusal with one of an ordered set of snake_case reasons, and decodes
only from the set it verified. Tier is a closed vocabulary; supersedes is
informative.

A pin that does not resolve makes its capability unsupported for that
envelope: SPEC §4 dispatch continues at the fallback, §7.1 selection at
the next rendition, and an inert envelope's error names the reasons. A
rendition entry may carry id, sha256 and, on a companion,
companion_of_id. SPEC §7 gains a Digests subsection, and §8 says how a
MINOR revision adds an optional member.

In wai-rs: the manifest types are #[non_exhaustive] with builders; a new
pin module verifies pins in memory and on disk; Wai::dispatch_verified,
WaiMulti::pick and primary check pins and component digests; every
neural decoder reads through ParamSource; a codec-free `ffi` feature
carries the C envelope surface with wai_envelope_select and
wai_neural_decode_ex. wai-web wraps wai::container. New vectors: the
int_mlicpp set-form twin and the pin-conformance corpus with an
independent Python verifier, plus sha256.json sidecars for the vectors
of capabilities that decode against a parameter set.

Squashed from wai/stream-a-parameter-set-identity onto link 12. Conflicts
are resolved to keep both sides; the integration commits that follow
make the two streams agree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 05:51:50 -04:00

929 lines
38 KiB
Rust

//! 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::<wai::world::sim::KinNode>::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<CausalOp>, [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<CausalOp>, 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<_>>(), 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::<BalNode>::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::<BalNode>::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::<BalNode>::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::<TrussNode>::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<CausalOp> {
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::<TrussNode>::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::<TrussNode>::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::<TrussNode>::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<TrussNode>, 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::{collect_checkpoints, 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<u8> {
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);
}
}