diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54636984..da8b09bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,6 +197,8 @@ jobs: run: cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime --test scene_fixture_tests --bench scene_fixtures -- -D warnings -D missing_docs - name: cargo clippy (experimental application root, M1) run: cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_shadow --lib --tests -- -D warnings + - name: cargo clippy (experimental application-root commitment profile, M2) + run: cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_profile --lib --tests --benches -- -D warnings - name: cargo clippy (compiler-produced pure evaluation) run: cargo clippy -p warp-core --features trusted_runtime --lib --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_unsigned_addition_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests -- -D warnings -D missing_docs @@ -393,6 +395,10 @@ jobs: cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,application_root_shadow --test scene_fixture_tests cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_shadow --lib application_root cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,application_root_shadow,delta_validate --test application_root_shadow_tests + - name: cargo test (experimental application-root commitment profile, M2) + run: | + cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_profile --lib application_ + cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_profile,delta_validate --test application_root_profile_tests --test trusted_runtime_host_loop_tests --test executable_operation_pipeline_tests - name: cargo test (compiler-produced pure evaluation) run: | cargo test -p warp-core --features trusted_runtime --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_unsigned_addition_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7b59a0..928e9360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -347,6 +347,16 @@ native provider judgments require same-width operands and a dominating no-overflow proof, including legacy Core without a function table. +- Experimental feature `application_root_profile` (M2, off by default, implies + `application_root_shadow`): a worldline registered with + `WorldlineState::with_commitment_profile(CommitmentProfile::ApplicationRadixV0)` + commits `H("echo.state-root.application-radix.v0" || application_root)` as + its `state_root` instead of walking the graph. Replay, readings at earlier + coordinates, forks, initial boundaries, imports and WAL recovery verify + under the worldline's profile; a history replayed or recovered under the + other profile fails. Worldlines default to `LegacyReachableV1`, whose roots, + commit hashes and receipts are unchanged. + - Experimental feature `application_root_shadow` (M1, off by default): an incremental application root over every node, edge and attachment record of a worldline state, kept beside the legacy state root, which it neither diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index ef55c4be..3d7aea44 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -48,6 +48,9 @@ host_test = [] # EXPERIMENTAL (M1): keep an incremental application root beside the legacy # state root, in shadow mode. It changes no root, receipt or wire format. application_root_shadow = [] +# EXPERIMENTAL (M2): let a worldline opt into committing the application root +# as its state root. Legacy worldlines and histories are unchanged. +application_root_profile = ["application_root_shadow"] # TRUSTED HOST ONLY: exposes runtime-owner authority constructors for host # adapters that own scheduler/runtime control. Application crates must not # enable this feature. diff --git a/crates/warp-core/benches/scene_fixtures.rs b/crates/warp-core/benches/scene_fixtures.rs index 041900d2..bf5297a1 100644 --- a/crates/warp-core/benches/scene_fixtures.rs +++ b/crates/warp-core/benches/scene_fixtures.rs @@ -60,18 +60,35 @@ fn main() { ); for tier in [think_append::S, think_append::M] { - run_f5(tier, SEED, ROUNDS); + run_f5(tier, SEED, ROUNDS, "legacy-reachable-v1", think_append::run); + } + // The experimental application-root commitment profile (M2). + #[cfg(feature = "application_root_profile")] + for tier in [think_append::S, think_append::M] { + run_f5(tier, SEED, ROUNDS, "application-radix-v0", |tier, seed| { + think_append::run_with_profile( + tier, + seed, + warp_core::application_root::CommitmentProfile::ApplicationRadixV0, + ) + }); } } -fn run_f5(tier: think_append::Tier, seed: u64, rounds: usize) { +fn run_f5( + tier: think_append::Tier, + seed: u64, + rounds: usize, + profile: &str, + execute: fn(think_append::Tier, u64) -> think_append::Observation, +) { let spec = think_append::workload(tier, seed); let script = think_append::script(tier, seed); let digest = hex32(&think_append::script_digest(&script)); let expect = think_append::expectation(tier, Some(&digest)); let workload_id = spec.workload_id(); println!( - "F5 tier={} seed={seed} workload_id={} expectation_id={} script_digest={digest}", + "F5 tier={} profile={profile} seed={seed} workload_id={} expectation_id={} script_digest={digest}", tier.name, hex32(&workload_id), hex32(&expect.expectation_id(&workload_id)) @@ -96,7 +113,7 @@ fn run_f5(tier: think_append::Tier, seed: u64, rounds: usize) { let mut per_append = Vec::new(); let mut comparable = true; for round in 1..=rounds { - let obs = think_append::run(tier, seed); + let obs = execute(tier, seed); let class = think_append::validate(tier, &script, &expect, &obs); let a = &obs.accounting; println!( @@ -128,7 +145,7 @@ fn run_f5(tier: think_append::Tier, seed: u64, rounds: usize) { let committed = tier.appends as f64; let append_p50 = median_ms(appends); println!( - "F5 tier={} summary valid_rounds={rounds} append_p50_ms={append_p50:.2} \ + "F5 tier={} profile={profile} summary valid_rounds={rounds} append_p50_ms={append_p50:.2} \ per_append_p50_ms={:.4} useful_throughput_appends_per_s={:.0} replay_p50_ms={:.2}", tier.name, median_ms(per_append), @@ -137,7 +154,7 @@ fn run_f5(tier: think_append::Tier, seed: u64, rounds: usize) { ); } else { println!( - "F5 tier={} summary withheld: not every round was valid", + "F5 tier={} profile={profile} summary withheld: not every round was valid", tier.name ); } diff --git a/crates/warp-core/benches/support/fixtures/think_append.rs b/crates/warp-core/benches/support/fixtures/think_append.rs index eb8e2c1e..54a1a3d1 100644 --- a/crates/warp-core/benches/support/fixtures/think_append.rs +++ b/crates/warp-core/benches/support/fixtures/think_append.rs @@ -437,8 +437,26 @@ pub struct Timings { /// Executes the fixture through the trusted host. pub fn run(tier: Tier, seed: u64) -> Observation { + run_from(tier, seed, initial_state(tier)) +} + +/// Executes the fixture on a worldline with commitment profile `profile` +/// (M2, experimental). +#[cfg(feature = "application_root_profile")] +pub fn run_with_profile( + tier: Tier, + seed: u64, + profile: warp_core::application_root::CommitmentProfile, +) -> Observation { + run_from( + tier, + seed, + initial_state(tier).with_commitment_profile(profile), + ) +} + +fn run_from(tier: Tier, seed: u64, initial: WorldlineState) -> Observation { let script = script(tier, seed); - let initial = initial_state(tier); let base = initial.clone(); let worldline_id = WorldlineId::from_bytes(WORLDLINE); let mut runtime = WorldlineRuntime::new(); diff --git a/crates/warp-core/src/application_root.rs b/crates/warp-core/src/application_root.rs index fe34fc34..badc1be8 100644 --- a/crates/warp-core/src/application_root.rs +++ b/crates/warp-core/src/application_root.rs @@ -341,6 +341,51 @@ fn update(subtree: Option<&Arc>, ops: &[(Key, Option)]) -> Optio } } +/// How a worldline's committed `state_root` is computed (M2, experimental). +/// +/// The profile is part of a worldline's registration, like its initial state: +/// it is chosen once, inherited by forks and replay bases, and never inferred +/// from a root's bytes. A history is only verifiable under the profile it was +/// committed with; replaying it under another fails with a root mismatch. +#[cfg(feature = "application_root_profile")] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum CommitmentProfile { + /// The legacy reachable-state root. Every existing history uses it, and + /// its recipe is unchanged. + #[default] + LegacyReachableV1, + /// The experimental application root: every record, reachable or not, + /// in the `echo.application-root.radix.v0` trie, tagged by + /// [`profile_tagged_state_root`] so it never equals a legacy root. + ApplicationRadixV0, +} + +#[cfg(feature = "application_root_profile")] +impl CommitmentProfile { + /// A stable identifier for diagnostics and descriptors. + #[must_use] + pub const fn id(self) -> &'static str { + match self { + Self::LegacyReachableV1 => "echo.state-root.legacy-reachable.v1", + Self::ApplicationRadixV0 => "echo.state-root.application-radix.v0", + } + } +} + +/// The committed `state_root` of an application-profile worldline: +/// `H("echo.state-root.application-radix.v0" || application_root)`. +/// +/// The tag keeps the value domain-separated from every legacy root, so a +/// root is never readable under the wrong profile. +#[cfg(feature = "application_root_profile")] +#[must_use] +pub fn profile_tagged_state_root(application_root: &Hash) -> Hash { + hash_parts(&[ + CommitmentProfile::ApplicationRadixV0.id().as_bytes(), + application_root, + ]) +} + /// A persistent version of the application reading. #[derive(Clone, Debug, Default)] pub(crate) struct ApplicationTrie { diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 6958caff..3a38d02b 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -5917,12 +5917,15 @@ impl SchedulerCoordinator { &admitted_submission_ids(*key, &admitted), ) .map_err(RuntimeError::from)?; + // An application-profile commit already advanced the trie. #[cfg(feature = "application_root_shadow")] - frontier.state_mut().advance_application_root( - application_root_was_current, - outcome.patch.ops(), - outcome.patch.out_slots(), - ); + if !frontier.state().application_root_is_current() { + frontier.state_mut().advance_application_root( + application_root_was_current, + outcome.patch.ops(), + outcome.patch.out_slots(), + ); + } runtime_before .state_journal_mut(key.worldline_id)? .push_touches(touches); @@ -5953,12 +5956,15 @@ impl SchedulerCoordinator { &admitted_submission_ids(*key, &admitted), ) .map_err(RuntimeError::from)?; + // An application-profile commit already advanced the trie. #[cfg(feature = "application_root_shadow")] - frontier.state_mut().advance_application_root( - application_root_was_current, - outcome.patch.ops(), - outcome.patch.out_slots(), - ); + if !frontier.state().application_root_is_current() { + frontier.state_mut().advance_application_root( + application_root_was_current, + outcome.patch.ops(), + outcome.patch.out_slots(), + ); + } runtime_before .state_journal_mut(key.worldline_id)? .push_touches(touches); @@ -8711,6 +8717,113 @@ mod tests { assert_eq!(provenance.len(worldline_id).unwrap(), 1); } + #[cfg(feature = "application_root_profile")] + #[test] + fn application_profile_commits_the_tagged_application_root_and_verifies_only_under_it() { + use crate::application_root::{profile_tagged_state_root, CommitmentProfile}; + + let worldline_id = wl(1); + let base = + WorldlineState::empty().with_commitment_profile(CommitmentProfile::ApplicationRadixV0); + let mut runtime = WorldlineRuntime::new(); + let mut engine = empty_engine(); + engine.register_rule(toy_increment_contract_rule()).unwrap(); + runtime + .register_worldline(worldline_id, base.clone()) + .unwrap(); + register_head( + &mut runtime, + worldline_id, + "default", + None, + true, + InboxPolicy::AcceptAll, + ); + let mut provenance = mirrored_provenance(&runtime); + assert_eq!( + provenance.initial_boundary_hash(worldline_id).unwrap(), + profile_tagged_state_root(&base.application_root()), + "the initial boundary is committed under the profile too" + ); + for index in 0..4_u8 { + runtime + .ingest(IngressEnvelope::local_intent( + IngressTarget::DefaultWriter { worldline_id }, + make_intent_kind("echo.intent/eint-v1"), + toy_increment_intent(&[index]), + )) + .unwrap(); + let records = + SchedulerCoordinator::super_tick(&mut runtime, &mut provenance, &mut engine) + .unwrap(); + let state = runtime.worldlines().get(&worldline_id).unwrap().state(); + let tagged = profile_tagged_state_root(&state.application_root_rebuilt()); + assert_eq!(records[0].state_root, tagged, "commit {index}"); + assert_eq!(state.state_root(), tagged, "commit {index}"); + assert_ne!( + tagged, + crate::snapshot::compute_state_root_for_warp_state( + state.warp_state(), + state.root() + ), + "the profile never reinterprets a legacy root" + ); + } + + let live = runtime.worldlines().get(&worldline_id).unwrap().state(); + let replayed = provenance + .replay_worldline_state(worldline_id, &base) + .unwrap(); + assert_eq!(replayed.state_root(), live.state_root()); + assert_eq!( + replayed.commitment_profile(), + CommitmentProfile::ApplicationRadixV0 + ); + let legacy_base = base.with_commitment_profile(CommitmentProfile::LegacyReachableV1); + assert!( + matches!( + provenance.replay_worldline_state(worldline_id, &legacy_base), + Err(crate::provenance_store::ReplayError::InitialBoundaryHashMismatch { .. }) + ), + "an application-profile history does not verify under the legacy profile" + ); + + let child_worldline_id = wl(43); + runtime + .fork_strand( + &mut provenance, + ForkStrandRequest { + strand_id: make_strand_id("application-profile-fork"), + source_lane_id: worldline_id, + fork_tick: wt(3), + child_worldline_id, + writer_heads: vec![WriterHead::with_routing( + WriterHeadKey { + worldline_id: child_worldline_id, + head_id: make_head_id("application-profile-child"), + }, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )], + retention_posture: test_retention_posture(10), + }, + ) + .unwrap(); + let source = runtime.worldlines().get(&worldline_id).unwrap().state(); + let child = runtime + .worldlines() + .get(&child_worldline_id) + .unwrap() + .state(); + assert_eq!( + child.commitment_profile(), + CommitmentProfile::ApplicationRadixV0 + ); + assert_eq!(child.state_root(), source.state_root()); + } + #[cfg(feature = "application_root_shadow")] #[test] fn application_root_tracks_runtime_commits_forks_and_replay() { diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 3782da48..b5da765c 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -6458,7 +6458,7 @@ pub(crate) fn commit_scheduler_action_batch_to_state_v1( } debug_assert_eq!( state_root_before, - crate::snapshot::compute_state_root_for_warp_state(state.warp_state(), state.root()), + state.recomputed_state_root(), "the caller's state root must be the root of the state being committed" ); let tx_raw = state diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 75bdf9b6..53717d85 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -506,6 +506,13 @@ pub struct Engine { /// outside a runtime batch always starts a fresh log, so a log left behind /// by an aborted commit never leaks into a later patch. runtime_batch_touch_log: bool, + /// During a runtime batch for an application-profile worldline (M2): the + /// worldline's current application trie, to be advanced by the commit. + #[cfg(feature = "application_root_profile")] + runtime_application_trie: Option, + /// The trie the commit advanced, handed back to the worldline state. + #[cfg(feature = "application_root_profile")] + runtime_application_trie_after: Option, /// Working memory for the state root, kept across commits so hashing the /// reachable graph allocates nothing once it has grown to fit. state_root_scratch: crate::snapshot::StateRootScratch, @@ -590,6 +597,11 @@ impl<'a> RuntimeCommitStateGuard<'a> { fn enter(engine: &'a mut Engine, state: &'a mut WorldlineState) -> Self { // Taken before the graph is borrowed mutably, so it keeps whether it // described the pre-commit graph. + #[cfg(feature = "application_root_profile")] + { + engine.runtime_application_trie = state.committing_application_trie(); + engine.runtime_application_trie_after = None; + } #[cfg(feature = "application_root_shadow")] let saved_application_root = state.take_application_root(); let saved_engine_state = @@ -665,6 +677,24 @@ impl<'a> RuntimeCommitStateGuard<'a> { .as_ref("runtime commit guard missing pre-commit state") } + /// The worldline's application trie after a successful commit: the trie + /// the commit advanced for an application-profile worldline, otherwise + /// the saved trie marked stale for the caller to advance. + #[cfg(feature = "application_root_shadow")] + fn application_root_after_success(&mut self) -> crate::worldline_state::ApplicationRootShadow { + let saved = self + .saved_application_root + .take("runtime commit guard missing application root"); + #[cfg(feature = "application_root_profile")] + { + self.engine.runtime_application_trie = None; + if let Some(trie) = self.engine.runtime_application_trie_after.take() { + return saved.advanced_to(trie); + } + } + saved.stale() + } + fn finish_success(&mut self) -> crate::warp_state::WarpStateTouches { // The touch log is the tick's undo record: hand it to the caller. self.engine.runtime_batch_touch_log = false; @@ -713,11 +743,7 @@ impl<'a> RuntimeCommitStateGuard<'a> { // The graph changed: the caller advances the trie from the // commit's write set. #[cfg(feature = "application_root_shadow")] - application_root: Some( - self.saved_application_root - .take("runtime commit guard missing application root") - .stale(), - ), + application_root: Some(self.application_root_after_success()), }); self.engine.current_root = self.saved_root; self.engine.tx_counter = self.saved_tx_counter; @@ -792,10 +818,15 @@ impl<'a> RuntimeCommitStateGuard<'a> { // The graph is restored exactly, so the trie keeps whatever it // described before the commit. #[cfg(feature = "application_root_shadow")] - application_root: Some( + application_root: Some({ + #[cfg(feature = "application_root_profile")] + { + self.engine.runtime_application_trie = None; + self.engine.runtime_application_trie_after = None; + } self.saved_application_root - .take("runtime commit guard missing application root"), - ), + .take("runtime commit guard missing application root") + }), }); self.engine.state = self .saved_engine_state @@ -1010,6 +1041,10 @@ impl Engine { last_execution_footprints: std::sync::Arc::default(), last_aborted_execution_footprints: std::sync::Arc::default(), runtime_batch_touch_log: false, + #[cfg(feature = "application_root_profile")] + runtime_application_trie: None, + #[cfg(feature = "application_root_profile")] + runtime_application_trie_after: None, state_root_scratch: crate::snapshot::StateRootScratch::default(), intent_log: Vec::new(), initial_state, @@ -1213,6 +1248,10 @@ impl Engine { last_execution_footprints: std::sync::Arc::default(), last_aborted_execution_footprints: std::sync::Arc::default(), runtime_batch_touch_log: false, + #[cfg(feature = "application_root_profile")] + runtime_application_trie: None, + #[cfg(feature = "application_root_profile")] + runtime_application_trie_after: None, state_root_scratch: crate::snapshot::StateRootScratch::default(), intent_log: Vec::new(), initial_state, @@ -2130,6 +2169,37 @@ impl Engine { ); let patch_digest = patch.digest(); + // An application-profile worldline (M2) commits its advanced + // application trie, and does not walk the graph. + #[cfg(feature = "application_root_profile")] + let application_state_root = self.runtime_application_trie.take().map(|trie| { + let advanced = trie + .advanced(&self.state, patch.ops(), patch.out_slots()) + .unwrap_or_else(|| crate::application_root::ApplicationTrie::rebuild(&self.state)); + #[cfg(feature = "delta_validate")] + assert_eq!( + advanced.root(), + crate::application_root::ApplicationTrie::rebuild(&self.state).root(), + "the incremental application root must equal its full rebuild" + ); + let root = crate::application_root::profile_tagged_state_root(&advanced.root()); + self.runtime_application_trie_after = Some(advanced); + root + }); + #[cfg(all(feature = "delta_validate", feature = "application_root_profile"))] + let legacy_profile = application_state_root.is_none(); + #[cfg(all(feature = "delta_validate", not(feature = "application_root_profile")))] + let legacy_profile = true; + #[cfg(feature = "application_root_profile")] + let state_root = match application_state_root { + Some(root) => root, + None => crate::snapshot::compute_state_root_with( + &self.state, + &self.current_root, + &mut self.state_root_scratch, + ), + }; + #[cfg(not(feature = "application_root_profile"))] let state_root = crate::snapshot::compute_state_root_with( &self.state, &self.current_root, @@ -2137,7 +2207,7 @@ impl Engine { ); #[cfg(feature = "delta_validate")] - { + if legacy_profile { use crate::snapshot_accum::SnapshotAccumulator; let mut accumulator = SnapshotAccumulator::from_warp_state(&state_before); @@ -2486,7 +2556,9 @@ impl Engine { /// Returns a snapshot view over an external worldline frontier state. #[must_use] pub fn snapshot_for_state(&self, state: &WorldlineState) -> Snapshot { - let state_root = compute_state_root(state.warp_state(), &state.root); + // The worldline's own recipe: the legacy reachable root, or under an + // application profile (M2) the tagged application root. + let state_root = state.recomputed_state_root(); let parents: Vec = state .last_snapshot .as_ref() diff --git a/crates/warp-core/src/playback.rs b/crates/warp-core/src/playback.rs index 2dd39621..c2b07cbd 100644 --- a/crates/warp-core/src/playback.rs +++ b/crates/warp-core/src/playback.rs @@ -35,7 +35,6 @@ use crate::provenance_store::{ advance_replay_state, replay_worldline_state_at_from_provenance, validate_replay_base, ProvenanceStore, ReplayError, }; -use crate::snapshot::compute_state_root_for_warp_state; use crate::worldline::WorldlineId; use crate::worldline_state::WorldlineState; @@ -448,7 +447,7 @@ impl PlaybackCursor { ); assert_eq!( initial_state.state_root(), - compute_state_root_for_warp_state(initial_state.initial_state(), initial_state.root()), + initial_state.state_root_of(initial_state.initial_state()), "playback cursor initial_state must materialize the canonical U0 replay base" ); Self { diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index 1a556259..88a336b0 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -31,7 +31,9 @@ use crate::ident::{Hash, NodeKey, WarpId}; use crate::materialization::FinalizedChannel; use crate::receipt::TickReceipt; use crate::revelation::CausalPosture; -use crate::snapshot::{compute_state_root_for_warp_state, Snapshot}; +#[cfg(test)] +use crate::snapshot::compute_state_root_for_warp_state; +use crate::snapshot::Snapshot; use crate::tick_patch::{TickCommitStatus, WarpTickPatchV1}; use crate::tx::TxId; use crate::worldline_state::WorldlineState; @@ -983,8 +985,7 @@ pub(crate) fn validate_replay_base( } let expected_initial_boundary = provenance.initial_boundary_hash(worldline_id)?; - let actual_initial_boundary = - compute_state_root_for_warp_state(base_state.initial_state(), base_state.root()); + let actual_initial_boundary = base_state.state_root_of(base_state.initial_state()); if actual_initial_boundary != expected_initial_boundary { return Err(ReplayError::InitialBoundaryHashMismatch { expected: expected_initial_boundary, @@ -1059,8 +1060,7 @@ pub(crate) fn advance_replay_state( .apply_to_worldline_state(replayed) .map_err(|source| ReplayError::Apply { tick, source })?; - let actual_state_root = - compute_state_root_for_warp_state(replayed.warp_state(), replayed.root()); + let actual_state_root = replayed.recomputed_state_root(); if actual_state_root != entry.expected.state_root { return Err(ReplayError::StateRootMismatch { tick, @@ -1188,10 +1188,9 @@ fn validate_checkpoint_for_history( }); } - let actual_initial_boundary = compute_state_root_for_warp_state( - checkpoint.state.initial_state(), - checkpoint.state.root(), - ); + let actual_initial_boundary = checkpoint + .state + .state_root_of(checkpoint.state.initial_state()); if actual_initial_boundary != history.initial_boundary_hash { return Err(HistoryError::CheckpointInitialBoundaryHashMismatch { worldline_id, @@ -2071,8 +2070,7 @@ impl ProvenanceService { worldline_id: WorldlineId, state: &WorldlineState, ) -> Result<(), HistoryError> { - let initial_boundary_hash = - compute_state_root_for_warp_state(state.initial_state(), state.root()); + let initial_boundary_hash = state.state_root_of(state.initial_state()); self.store.register_worldline_with_boundary( worldline_id, state.root().warp_id, diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 6fd7cc75..5f6c541a 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -19,7 +19,7 @@ use crate::provenance_store::{ }; use crate::record::{EdgeRecord, NodeRecord}; use crate::revelation::{CausalPosture, RetentionPosture, SourceDisclosurePolicy}; -use crate::snapshot::{compute_commit_hash_v2, compute_state_root_for_warp_state}; +use crate::snapshot::compute_commit_hash_v2; use crate::strand::{ StrandBasisReport, StrandError, StrandId, StrandOverlapRevalidation, StrandRegistry, StrandRevalidationState, @@ -888,10 +888,7 @@ impl SettlementService { continue; } - let actual_state_root = compute_state_root_for_warp_state( - candidate_simulated.warp_state(), - candidate_simulated.root(), - ); + let actual_state_root = candidate_simulated.recomputed_state_root(); if parent_at_anchor && actual_state_root != source_entry.expected.state_root { blocked_reason = Some(ConflictReason::UnsupportedImport); decisions.push(SettlementDecision::ConflictArtifact(conflict_draft( diff --git a/crates/warp-core/src/worldline_state.rs b/crates/warp-core/src/worldline_state.rs index 6746c713..39e6b958 100644 --- a/crates/warp-core/src/worldline_state.rs +++ b/crates/warp-core/src/worldline_state.rs @@ -191,6 +191,10 @@ impl std::fmt::Debug for TestCallCount { pub(crate) struct ApplicationRootShadow { trie: crate::application_root::ApplicationTrie, current: bool, + /// The worldline's commitment profile (M2), carried with the trie so + /// every path that carries the trie carries the profile. + #[cfg(feature = "application_root_profile")] + profile: crate::application_root::CommitmentProfile, } #[cfg(feature = "application_root_shadow")] @@ -216,8 +220,42 @@ impl ApplicationRootShadow { Self { trie: crate::application_root::ApplicationTrie::rebuild(warp_state), current: true, + #[cfg(feature = "application_root_profile")] + profile: crate::application_root::CommitmentProfile::default(), } } + + /// A rebuild of `warp_state` that keeps this shadow's profile. + #[cfg(feature = "application_root_profile")] + fn rebuilt(&self, warp_state: &WarpState) -> Self { + Self { + profile: self.profile, + ..Self::of(warp_state) + } + } + + /// A rebuild of `warp_state`. + #[cfg(not(feature = "application_root_profile"))] + #[allow(clippy::unused_self)] + fn rebuilt(&self, warp_state: &WarpState) -> Self { + Self::of(warp_state) + } + + /// An advanced trie, current, keeping this shadow's profile. + #[cfg_attr(not(feature = "application_root_profile"), allow(clippy::unused_self))] + pub(crate) fn advanced_to(&self, trie: crate::application_root::ApplicationTrie) -> Self { + Self { + trie, + current: true, + #[cfg(feature = "application_root_profile")] + profile: self.profile, + } + } + + #[cfg(feature = "application_root_profile")] + pub(crate) fn current_trie(&self) -> Option<&crate::application_root::ApplicationTrie> { + self.current.then_some(&self.trie) + } } impl Default for WorldlineState { @@ -382,6 +420,12 @@ impl WorldlineState { /// retained root. #[must_use] pub fn state_root(&self) -> Hash { + #[cfg(feature = "application_root_profile")] + if self.application_root.profile + == crate::application_root::CommitmentProfile::ApplicationRadixV0 + { + return crate::application_root::profile_tagged_state_root(&self.application_root()); + } if let Some((root, state_root)) = self.retained_state_root { if root == self.root { debug_assert_eq!( @@ -426,6 +470,89 @@ impl WorldlineState { std::mem::take(&mut self.application_root) } + /// Returns this state with commitment profile `profile` (M2, + /// experimental). Choose it before registering the worldline: it decides + /// what every committed `state_root` of the worldline means. + #[cfg(feature = "application_root_profile")] + #[must_use] + pub fn with_commitment_profile( + mut self, + profile: crate::application_root::CommitmentProfile, + ) -> Self { + self.application_root.profile = profile; + self + } + + /// The worldline's commitment profile (M2, experimental). + #[cfg(feature = "application_root_profile")] + #[must_use] + pub fn commitment_profile(&self) -> crate::application_root::CommitmentProfile { + self.application_root.profile + } + + /// The application trie, when the worldline commits it as its state root + /// and it is current, for the runtime commit guard. + #[cfg(feature = "application_root_profile")] + pub(crate) fn committing_application_trie( + &self, + ) -> Option { + (self.application_root.profile + == crate::application_root::CommitmentProfile::ApplicationRadixV0) + .then(|| { + self.application_root + .current_trie() + .cloned() + .unwrap_or_else(|| { + crate::application_root::ApplicationTrie::rebuild(&self.warp_state) + }) + }) + } + + /// The committed-root recipe of this worldline applied to `warp_state`, + /// for a graph that is not this state's own (an initial boundary or an + /// import candidate). + #[must_use] + pub(crate) fn state_root_of(&self, warp_state: &WarpState) -> Hash { + #[cfg(feature = "application_root_profile")] + if self.application_root.profile + == crate::application_root::CommitmentProfile::ApplicationRadixV0 + { + return crate::application_root::profile_tagged_state_root( + &crate::application_root::ApplicationTrie::rebuild(warp_state).root(), + ); + } + crate::snapshot::compute_state_root_for_warp_state(warp_state, &self.root) + } + + /// This state's committed root, recomputed without the retained legacy + /// state root: the legacy reachable root walked from the graph, or for an + /// application profile the tagged root of the current application trie. + /// + /// The application trie is not rebuilt here when it is current, so replay + /// verifies incrementally; `delta_validate` checks each advance against a + /// full rebuild, and checkpoint and initial-boundary checks use + /// [`Self::state_root_of`], which rebuilds. + #[must_use] + pub(crate) fn recomputed_state_root(&self) -> Hash { + self.state_root_of_own_graph() + } + + #[cfg(not(feature = "application_root_profile"))] + fn state_root_of_own_graph(&self) -> Hash { + crate::snapshot::compute_state_root_for_warp_state(&self.warp_state, &self.root) + } + + #[cfg(feature = "application_root_profile")] + fn state_root_of_own_graph(&self) -> Hash { + if self.application_root.profile + == crate::application_root::CommitmentProfile::ApplicationRadixV0 + { + crate::application_root::profile_tagged_state_root(&self.application_root()) + } else { + crate::snapshot::compute_state_root_for_warp_state(&self.warp_state, &self.root) + } + } + /// Whether the application trie is known to describe the current graph. #[cfg(feature = "application_root_shadow")] pub(crate) fn application_root_is_current(&self) -> bool { @@ -453,11 +580,8 @@ impl WorldlineState { None }; self.application_root = advanced.map_or_else( - || ApplicationRootShadow::of(&self.warp_state), - |trie| ApplicationRootShadow { - trie, - current: true, - }, + || self.application_root.rebuilt(&self.warp_state), + |trie| self.application_root.advanced_to(trie), ); #[cfg(feature = "delta_validate")] assert_eq!( @@ -713,7 +837,7 @@ impl WorldlineState { pub(crate) fn replay_base_from_initial(&self) -> Self { let initial_state = Arc::clone(&self.initial_state); #[cfg(feature = "application_root_shadow")] - let application_root = ApplicationRootShadow::of(&initial_state); + let application_root = self.application_root.rebuilt(&initial_state); Self { warp_state: (*initial_state).clone(), root: self.root, diff --git a/crates/warp-core/tests/application_root_profile_tests.rs b/crates/warp-core/tests/application_root_profile_tests.rs new file mode 100644 index 00000000..643addcf --- /dev/null +++ b/crates/warp-core/tests/application_root_profile_tests.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! The experimental application-root commitment profile (M2) through the F5 +//! scene fixture: the same workload validates under both profiles, the +//! application profile commits a different, tagged root, and replay and +//! readings at earlier coordinates verify under it. + +#[cfg(all( + feature = "application_root_profile", + feature = "native_rule_bootstrap", + feature = "trusted_runtime" +))] +#[path = "../benches/support/action_scene.rs"] +#[allow(dead_code)] +mod action_scene; +#[cfg(all( + feature = "application_root_profile", + feature = "native_rule_bootstrap", + feature = "trusted_runtime" +))] +#[path = "../benches/support/fixtures/mod.rs"] +#[allow(dead_code)] +mod fixtures; + +#[cfg(all( + feature = "application_root_profile", + feature = "native_rule_bootstrap", + feature = "trusted_runtime" +))] +use fixtures::{manifest::hex32, think_append}; +#[cfg(all( + feature = "application_root_profile", + feature = "native_rule_bootstrap", + feature = "trusted_runtime" +))] +use warp_core::application_root::CommitmentProfile; + +#[cfg(all( + feature = "application_root_profile", + feature = "native_rule_bootstrap", + feature = "trusted_runtime" +))] +#[test] +fn f5_validates_under_the_application_profile() { + let tiers: &[think_append::Tier] = if cfg!(debug_assertions) { + &[think_append::S] + } else { + &[think_append::S, think_append::M] + }; + for &tier in tiers { + let script = think_append::script(tier, 7); + let digest = hex32(&think_append::script_digest(&script)); + let expect = think_append::expectation(tier, Some(&digest)); + let legacy = think_append::run(tier, 7); + let application = + think_append::run_with_profile(tier, 7, CommitmentProfile::ApplicationRadixV0); + for (label, obs) in [("legacy", &legacy), ("application", &application)] { + let class = think_append::validate(tier, &script, &expect, obs); + assert!(class.comparable(), "tier {} {label}: {class:?}", tier.name); + assert_eq!( + obs.replay_root, + Some(obs.live_root), + "tier {} {label}", + tier.name + ); + } + assert_ne!( + legacy.live_root, application.live_root, + "tier {}: the profiles commit different roots for the same graph", + tier.name + ); + assert_eq!(application.application_root, Some((true, true))); + } +} diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 3419eeb8..a061f77f 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -117,6 +117,13 @@ fn fixture_host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { fn fixture_host_with_independent_nodes( node_count: usize, +) -> (TrustedRuntimeHost, WriterHeadKey, Vec) { + fixture_host_with_independent_nodes_and_state(node_count, |state| state) +} + +fn fixture_host_with_independent_nodes_and_state( + node_count: usize, + configure: impl FnOnce(WorldlineState) -> WorldlineState, ) -> (TrustedRuntimeHost, WriterHeadKey, Vec) { assert!(node_count > 0, "the fixture must retain one root node"); let warp_id = make_warp_id("operation-fixture"); @@ -144,8 +151,10 @@ fn fixture_host_with_independent_nodes( ))), ); } - let state = WorldlineState::from_root_store(store, root_id) - .expect("the fixture state has one lawful root"); + let state = configure( + WorldlineState::from_root_store(store, root_id) + .expect("the fixture state has one lawful root"), + ); let worldline_id = WorldlineId::from_bytes(digest("operation-fixture-worldline")); let head_key = WriterHeadKey { worldline_id, @@ -4935,6 +4944,74 @@ fn an_action_with_a_forged_origin_keeps_the_whole_frontier_rule() { ); } +/// M2: on an application-profile worldline, Actions commit the tagged +/// application root, the stale-basis exception still corroborates origins +/// (including the genesis boundary), and recovery agrees. +#[cfg(feature = "application_root_profile")] +#[test] +fn application_profile_actions_commit_and_recover() { + use warp_core::application_root::{profile_tagged_state_root, CommitmentProfile}; + + const ACTIONS: usize = 70; + let profiled = |nodes: usize| { + fixture_host_with_independent_nodes_and_state(nodes, |state| { + state.with_commitment_profile(CommitmentProfile::ApplicationRadixV0) + }) + }; + let wal_dir = TempWalDir::new(); + let submissions; + let committed_root; + { + let (mut host, head_key, nodes) = profiled(ACTIONS); + host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("the WAL opens"); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + // Every Action is built at the genesis boundary; those decided after + // the first tick need the scoped exception to commit. + let invocations = nodes + .iter() + .map(|node| action_invocation(&host, &installed, head_key, *node, b"before", b"after")) + .collect::>(); + submissions = invocations + .into_iter() + .map(|invocation| submit_action(&mut host, head_key, invocation)) + .collect::>(); + for _ in 0..ACTIONS.div_ceil(64) { + host.tick_once().expect("the scheduler decides a batch"); + } + assert!(submissions.iter().all(|id| outcome_is_committed(&host, id))); + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the fixture worldline exists") + .state(); + committed_root = state.state_root(); + assert_eq!( + committed_root, + profile_tagged_state_root(&state.application_root_rebuilt()) + ); + } + let (mut recovered, head_key, _) = profiled(ACTIONS); + recovered + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("recovery recomputes validity under the profile and agrees"); + assert!(submissions + .iter() + .all(|id| outcome_is_committed(&recovered, id))); + assert_eq!( + recovered + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the fixture worldline exists") + .state() + .state_root(), + committed_root + ); +} + // --------------------------------------------------------------------------- // Comparison-contract regressions (M0) // --------------------------------------------------------------------------- diff --git a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs index 208a407c..b128f803 100644 --- a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs +++ b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs @@ -4424,3 +4424,93 @@ fn a_legacy_resolved_resend_records_the_retained_envelope() { drop(restarted); fs::remove_dir_all(&fresh_root).expect("WAL cleanup"); } + +/// M2: an application-profile worldline recovers from the filesystem WAL when +/// the host registers it under the same profile, and refuses to recover under +/// the legacy profile. +#[cfg(feature = "application_root_profile")] +#[test] +fn application_profile_history_recovers_only_under_its_profile() { + use warp_core::application_root::CommitmentProfile; + + let profiled_runtime = |profile: CommitmentProfile| { + let mut runtime = WorldlineRuntime::new(); + let worldline_id = WorldlineId::from_bytes([1; 32]); + runtime + .register_worldline( + worldline_id, + WorldlineState::empty().with_commitment_profile(profile), + ) + .expect("worldline should register"); + runtime + .register_writer_head(WriterHead::with_routing( + WriterHeadKey { + worldline_id, + head_id: make_head_id("default"), + }, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )) + .expect("writer head should register"); + (runtime, worldline_id) + }; + let wal_root = temp_runtime_wal_dir("application-profile-recovery"); + let (initial_runtime, worldline_id) = profiled_runtime(CommitmentProfile::ApplicationRadixV0); + let mut host = TrustedRuntimeHost::new(initial_runtime, empty_engine()) + .expect("trusted host should initialize"); + host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&wal_root)) + .expect("host should configure filesystem runtime WAL adapter"); + host.register_contract_package(package()) + .expect("host should install package"); + let submission = host + .app() + .submit_intent_with_runtime_wal_ack(eint_envelope(worldline_id)) + .expect("submission should cross the durable ACK boundary"); + host.stage_installed_contract_submission(submission.submission_id, &admission_ticket(62)) + .expect("trusted host should stage the submission"); + host.run_until_idle(4).expect("the mutation commits"); + let committed_root = host + .runtime() + .worldlines() + .get(&worldline_id) + .expect("worldline exists") + .state() + .state_root(); + drop(host); + + let (recovered_runtime, _) = profiled_runtime(CommitmentProfile::ApplicationRadixV0); + let mut recovered = TrustedRuntimeHost::new(recovered_runtime, empty_engine()) + .expect("reconstructed host should initialize"); + recovered + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&wal_root)) + .expect("the history recovers under its own profile"); + assert_eq!( + recovered + .runtime() + .worldlines() + .get(&worldline_id) + .expect("worldline exists") + .state() + .state_root(), + committed_root + ); + drop(recovered); + + let (legacy_runtime, _) = profiled_runtime(CommitmentProfile::LegacyReachableV1); + let mut legacy = TrustedRuntimeHost::new(legacy_runtime, empty_engine()) + .expect("reconstructed host should initialize"); + let refusal = legacy + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&wal_root)) + .expect_err("an application-profile history does not recover under the legacy profile"); + // Recovery registers the initial boundary under the host's own profile, + // so the mismatch surfaces as the first replayed tick's root. + assert!( + format!("{refusal:?}") + .starts_with("Runtime(Replay(StateRootMismatch { tick: WorldlineTick(0),"), + "the legacy profile is refused at the first replayed root: {refusal:?}" + ); + drop(legacy); + fs::remove_dir_all(&wal_root).expect("WAL fixture cleanup succeeds"); +} diff --git a/docs/spec/merkle-commit.md b/docs/spec/merkle-commit.md index 2f2dda61..b3003eaa 100644 --- a/docs/spec/merkle-commit.md +++ b/docs/spec/merkle-commit.md @@ -29,6 +29,8 @@ The state root is BLAKE3 over canonical encoding of reachable WARP state from a Both canonical snapshots and incremental snapshot accumulation prepend the existing `echo:state_root:v1\0` domain separator before the root binding. Incremental accumulation must produce the same state root as canonical snapshot hashing for the same reachable state. Previously emitted accumulator roots that omitted this prefix do not satisfy that law; this correction does not migrate retained artifacts. +This is the `LegacyReachableV1` commitment profile, which every worldline uses by default. The experimental, off-by-default `application_root_profile` feature lets a worldline be registered with `ApplicationRadixV0`, whose `state_root` is the domain-tagged application root instead; see [Experimental commitment profile](../topics/ApplicationRoot.md#experimental-commitment-profile). Commit hash recipes are the same under either profile. + ## Decision 2: `commit_id` v2 commits to replay, not narration Commit header v2 commits to version, parents, state root, patch digest, and policy id. `patch_digest` is the digest of the replayable tick patch. diff --git a/docs/topics/ApplicationRoot.md b/docs/topics/ApplicationRoot.md index 400d20dc..5a15f9d3 100644 --- a/docs/topics/ApplicationRoot.md +++ b/docs/topics/ApplicationRoot.md @@ -6,7 +6,9 @@ The `application_root_shadow` feature keeps an incremental authenticated application reading beside the legacy state root. It is experimental and off by default. Enabling it changes no state root, receipt, patch, WAL or wire -format. It does not select a new commitment profile for admitted histories. +format. It does not select a new commitment profile for admitted histories; +the separate `application_root_profile` feature does, per worldline, as +described in [Experimental commitment profile](#experimental-commitment-profile). The legacy commitment contract remains in [merkle-commit.md](../spec/merkle-commit.md). @@ -71,5 +73,51 @@ as a valid timing. - `scene_fixture_tests` includes canaries for missing or failed parity and is scheduled with the shadow feature without `delta_validate`. -These witnesses do not establish a production commitment format or recovery -of a new-profile history. WAL and wire formats retain their existing meaning. +These shadow witnesses do not establish a production commitment format. +WAL and wire formats retain their existing meaning. + +## Experimental commitment profile + +The `application_root_profile` feature (experimental, off by default, implies +`application_root_shadow`) lets a worldline commit its application reading +instead of the legacy reachable-state root. `CommitmentProfile` is either +`LegacyReachableV1` (`echo.state-root.legacy-reachable.v1`, the default) or +`ApplicationRadixV0` (`echo.state-root.application-radix.v0`). The profile is +chosen with `WorldlineState::with_commitment_profile` before the worldline is +registered. Nothing infers a profile from a root's bytes. + +An `ApplicationRadixV0` worldline commits +`H("echo.state-root.application-radix.v0" || application_root)` as its +`state_root`. The tag separates this value from every legacy root, so no +historical `state_root` is reinterpreted. Commit hash v2 and v3 recipes are +unchanged; they bind whichever root the profile commits. A +`LegacyReachableV1` worldline's roots, commit hashes and receipts are +unchanged, and with the feature off every path below uses the legacy recipe. + +The profile travels with the retained trie, so rollback markers, replay bases +and checkpoints, forks and the runtime commit guard carry it. The runtime +commit advances the trie from the commit's complete write set and commits its +tagged root without walking the graph. Replay, readings at earlier +coordinates, import candidates, playback cursors, provenance initial +boundaries and checkpoint restores verify under the worldline's own recipe +(`recomputed_state_root`, `state_root_of`). A history replayed or recovered +under the other profile fails as a root mismatch. + +Limits of this profile: + +- It is part of the host's worldline registration and is not written to the + WAL, provenance codecs or WSC. A mismatch is a root mismatch, not a typed + profile error; a durable descriptor would need a codec version and owner + approval of the domain strings. +- It binds only the application reading. It is not a boundary seal over the + history root, and ports and warp-instance headers remain outside the + reading. +- There is no migration from a legacy history and no cross-host exchange + witness. + +Witnesses: the coordinator test that commits, replays and forks under the +profile; `application_root_profile_tests` (F5 validates under both profiles, +which commit different roots); `trusted_runtime_host_loop_tests` (WAL recovery +succeeds only under the registering profile); and +`executable_operation_pipeline_tests` (Actions commit and recover under the +profile, including stale-basis corroboration of the genesis origin).