From 6a99c5b2476448d7556a4226e32e05d4ee329454 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 7 Oct 2026 20:57:41 -0700 Subject: [PATCH 1/2] Fix incremental snapshot state-root domain separation (#771) --- CHANGELOG.md | 2 ++ crates/warp-core/src/snapshot_accum.rs | 12 ++++++++++++ docs/spec/merkle-commit.md | 2 ++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bfbeb22..dc133dc3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ ### Fixed +- Incremental snapshot state roots now include the existing v1 domain separator and agree with canonical snapshot hashing. Previously emitted incorrect accumulator roots are not migrated. + - The SPDX checker preserves Markdown frontmatter when it checks or repairs license headers. - Action WAL recovery reuses ordered verified replay cursors instead of retaining a full state for each basis tick. It preserves basis, Tick, result, obstruction and conflict checks, including delayed stale bases. diff --git a/crates/warp-core/src/snapshot_accum.rs b/crates/warp-core/src/snapshot_accum.rs index c71b58a9a..860d6410f 100644 --- a/crates/warp-core/src/snapshot_accum.rs +++ b/crates/warp-core/src/snapshot_accum.rs @@ -810,6 +810,7 @@ impl SnapshotAccumulator { use blake3::Hasher; let mut hasher = Hasher::new(); + hasher.update(crate::domain::STATE_ROOT_V1); // Root binding hasher.update(&root.warp_id.0); @@ -984,6 +985,17 @@ fn hash_attachment_value(hasher: &mut blake3::Hasher, value: &AttachmentValue) { mod tests { use super::*; + #[test] + fn accumulator_root_matches_the_canonical_snapshot_root() { + let state = crate::WorldlineState::empty(); + let accumulator = SnapshotAccumulator::from_warp_state(state.warp_state()); + let actual = accumulator.build(state.root(), [0; 32], 0); + assert_eq!( + actual.state_root, + crate::snapshot::compute_state_root(state.warp_state(), state.root()) + ); + } + #[test] fn test_empty_accumulator() { let acc = SnapshotAccumulator::new(); diff --git a/docs/spec/merkle-commit.md b/docs/spec/merkle-commit.md index 3fcbe1296..a33cc2f68 100644 --- a/docs/spec/merkle-commit.md +++ b/docs/spec/merkle-commit.md @@ -27,6 +27,8 @@ access to the original process. The state root is BLAKE3 over canonical encoding of reachable WARP state from a root `NodeKey`. Reachability follows outbound skeleton edges and descended attachment portals from reachable node/edge slots. +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. + ## 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. From e8ecf92174eefd18f54f6f14c3d42864d4e3be7c Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 7 Oct 2026 21:08:33 -0700 Subject: [PATCH 2/2] Test incremental root parity with reachable descended content --- crates/warp-core/src/snapshot_accum.rs | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/crates/warp-core/src/snapshot_accum.rs b/crates/warp-core/src/snapshot_accum.rs index 860d6410f..9afcd2ff0 100644 --- a/crates/warp-core/src/snapshot_accum.rs +++ b/crates/warp-core/src/snapshot_accum.rs @@ -996,6 +996,86 @@ mod tests { ); } + #[test] + fn accumulator_root_matches_canonical_reachable_content() { + use crate::attachment::AtomPayload; + use crate::graph::GraphStore; + use crate::warp_state::WarpState; + + let warp = make_warp_id("parity-root"); + let descended = make_warp_id("parity-descended"); + let root = make_node_id("root"); + let child = make_node_id("child"); + let inner = make_node_id("inner"); + let edge = make_edge_id("root-child"); + let root_key = NodeKey { + warp_id: warp, + local_id: root, + }; + let mut store = GraphStore::new(warp); + for node in [root, child] { + store.insert_node( + node, + NodeRecord { + ty: make_type_id("Node"), + }, + ); + } + store.insert_edge( + root, + EdgeRecord { + id: edge, + from: root, + to: child, + ty: make_type_id("Link"), + }, + ); + store.set_node_attachment( + root, + Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("Payload"), + bytes::Bytes::from_static(b"parity"), + ))), + ); + store.set_node_attachment(child, Some(AttachmentValue::Descend(descended))); + let parent = AttachmentKey { + owner: AttachmentOwner::Node(NodeKey { + warp_id: warp, + local_id: child, + }), + plane: AttachmentPlane::Alpha, + }; + let mut inner_store = GraphStore::new(descended); + inner_store.insert_node( + inner, + NodeRecord { + ty: make_type_id("Inner"), + }, + ); + let mut state = WarpState::new(); + state.upsert_instance( + WarpInstance { + warp_id: warp, + root_node: root, + parent: None, + }, + store, + ); + state.upsert_instance( + WarpInstance { + warp_id: descended, + root_node: inner, + parent: Some(parent), + }, + inner_store, + ); + let accumulator = SnapshotAccumulator::from_warp_state(&state); + assert_eq!( + accumulator.build(&root_key, [0; 32], 0).state_root, + crate::snapshot::compute_state_root(&state, &root_key) + ); + } + #[test] fn test_empty_accumulator() { let acc = SnapshotAccumulator::new();