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..9afcd2ff0 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,97 @@ 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 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(); 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.