The interactive world-model field (Genie / Oasis / Matrix / Hunyuan-World, and
fal's WMA transport) streams PIXELS: a GPU runs the model and ships generated video
to a thin client. Nothing about that session is reproducible, auditable, or
energy-accounted — the generated world is ephemeral.
wai.world.model flips the wire, the way wai.world.{scene,replay,live} already does
for deterministic worlds: the canonical artifact is the ACTION TRACE, not the
pixels. Each control is a signed CausalOp (the world spec already says "an input
event is an op emitted by one actor"); the trace replays — under canonical total
order, any arrival — to one portable SESSION HASH on every machine
(replay-equivalence). A declared model_id pins which world model + seed turns the
trace into frames; the model is sink-supplied presentation. The generated frames are
vouched for by a signed, joules-accounted provenance receipt (AssetReceipt).
So a session is portable, signed, metered, and replayable even when the renderer is
a float model — the three things the whole field lacks. When the renderer is an
INTEGER-exact world model (the int_* family, Phase 2) the frames hash-converge too:
a generative interactive world you can put in a receipt.
- world_model.rs (feature world_model = ["world","provenance"]): ModelHeader (model
+ seed + geometry, bound into the hash), action encode/decode over CausalOp,
replay() → ModelSession{session_hash, actions} (canonical order, sig-verify,
BLAKE3 hash-chain), frame_receipt() → AssetReceipt over per-frame steps + joules.
- capability wai.world.model registered (Lossless tier, StandardDefined — the trace
is exact; the renderer is declared). SPEC row added.
- tests/world_model.rs never-skips: replay-equivalence (shuffled arrival → identical
session hash bdabab2b…), tamper-evidence, model_id binding, action round-trip,
signed energy-accounted provenance receipt.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
90 lines
4.1 KiB
Rust
90 lines
4.1 KiB
Rust
//! `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<wai::world::CausalOp> {
|
|
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<_>>(), 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");
|
|
}
|