1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
//! Superposition during migration — entity on two nodes simultaneously.
//!
//! Wraps `MigrationState` with observational semantics. During migration,
//! an entity exists in superposition until routing "collapses" it to the
//! target node.
use crate::adapter::net::compute::MigrationPhase;
use crate::adapter::net::state::causal::CausalLink;
use super::chain::ContinuityProof;
/// Observational phase of an entity during migration.
///
/// Maps to `MigrationPhase` but with physical semantics:
/// the entity's "wavefunction" spreads, superposes, and collapses.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuperpositionPhase {
/// Entity exists only on source (pre-snapshot).
Localized,
/// Snapshot taken, target restoring. Entity still authoritative on source.
Spreading,
/// Both nodes may hold the entity. Superposed state.
Superposed,
/// Target caught up. Ready to collapse to single location.
ReadyToCollapse,
/// Routing switched. Target is canonical. Source draining.
Collapsed,
/// Source cleaned up. Back to single location on target.
Resolved,
}
impl SuperpositionPhase {
/// Map from MigrationPhase to SuperpositionPhase.
pub fn from_migration(phase: MigrationPhase) -> Self {
match phase {
MigrationPhase::Snapshot => Self::Localized,
MigrationPhase::Transfer => Self::Spreading,
MigrationPhase::Restore => Self::Spreading,
MigrationPhase::Replay => Self::Superposed,
MigrationPhase::Cutover => Self::Collapsed,
MigrationPhase::Complete => Self::Resolved,
}
}
/// Whether the entity currently exists on multiple nodes.
pub fn is_superposed(self) -> bool {
matches!(self, Self::Superposed | Self::ReadyToCollapse)
}
/// Whether routing has collapsed to a single node.
pub fn is_collapsed(self) -> bool {
matches!(self, Self::Collapsed | Self::Resolved)
}
}
/// Tracks an entity's superposition state during migration.
///
/// Provides observational semantics on top of the mechanical
/// `MigrationState` from L5.
pub struct SuperpositionState {
/// Entity being migrated.
origin_hash: u64,
/// Source node's chain head at snapshot time.
source_head: CausalLink,
/// Target node's chain head (advances during replay).
target_head: CausalLink,
/// Current superposition phase.
phase: SuperpositionPhase,
/// Events observed by source since snapshot.
source_observed_since: u64,
/// Events replayed on target.
target_replayed_through: u64,
}
impl SuperpositionState {
/// Create a new superposition state when migration begins.
pub fn new(origin_hash: u64, source_head: CausalLink) -> Self {
Self {
origin_hash,
source_head,
target_head: source_head, // target starts from same point
phase: SuperpositionPhase::Localized,
source_observed_since: 0,
target_replayed_through: source_head.sequence,
}
}
/// Advance the phase based on migration progress.
pub fn advance(&mut self, migration_phase: MigrationPhase) {
self.phase = SuperpositionPhase::from_migration(migration_phase);
}
/// Record that source has processed more events since snapshot.
pub fn source_advanced(&mut self, new_head: CausalLink) {
self.source_head = new_head;
self.source_observed_since = new_head
.sequence
.saturating_sub(self.target_replayed_through);
}
/// Record that target has replayed events.
pub fn target_replayed(&mut self, new_head: CausalLink) {
self.target_head = new_head;
self.target_replayed_through = new_head.sequence;
// Check if target has caught up to source. Pre-fix this
// only transitioned from `Superposed`. If `target_replayed`
// arrived while still in `Spreading` (target catches up
// before `advance(Replay)` flips the phase to Superposed),
// the catch-up was observed but the phase was stuck —
// `target_replayed` is wire-driven, not re-invoked on phase
// change, so `ReadyToCollapse` never fired and the
// migration stalled.
//
// Transition from either `Spreading` or `Superposed` on
// catch-up. The other phases (`Localized` before the
// migration starts, `ReadyToCollapse` / `Collapsed` /
// `Resolved` after) shouldn't react.
if self.target_replayed_through >= self.source_head.sequence
&& matches!(
self.phase,
SuperpositionPhase::Superposed | SuperpositionPhase::Spreading
)
{
self.phase = SuperpositionPhase::ReadyToCollapse;
}
}
/// Whether the target has caught up to the source.
pub fn target_caught_up(&self) -> bool {
self.target_replayed_through >= self.source_head.sequence
}
/// Collapse the superposition (routing switches to target).
pub fn collapse(&mut self) {
self.phase = SuperpositionPhase::Collapsed;
}
/// Mark migration as fully resolved.
pub fn resolve(&mut self) {
self.phase = SuperpositionPhase::Resolved;
}
/// Generate a continuity proof spanning the migration.
///
/// Proves that the chain is intact from the source's snapshot point
/// through the target's current head.
///
/// **Hash convention.** `ContinuityProof::verify_against`
/// computes `compute_parent_hash(event.link, event.payload)` for
/// the event at `from_seq` / `to_seq` — i.e. the *forward* hash
/// of the event AT that sequence. A `CausalLink`'s `parent_hash`
/// field is the forward hash of the *previous* event (event at
/// `sequence - 1`). So when we use `head.parent_hash` as the
/// proof's hash, we must point `from_seq` / `to_seq` at
/// `head.sequence - 1` — that's the event whose forward hash
/// equals `head.parent_hash`.
///
/// The match-on-min/max pattern below picks the head whose seq
/// matches the from/to anchor, so that proofs spanning a
/// target-behind-source case never mix identities (using
/// target's seq with source's parent_hash would produce a proof
/// that could never verify).
///
/// Note: head events with `sequence == 0` (genesis) have no
/// previous event, so the proof anchors at seq=0 and the hash is
/// the link's `parent_hash` (typically zero / a genesis sentinel).
/// `verify_against` will fail for such proofs unless the verifier
/// holds the genesis event — by design, since you can't prove
/// continuity of a genesis-only chain.
pub fn continuity_proof(&self) -> ContinuityProof {
let (lo_head, hi_head) = if self.source_head.sequence <= self.target_head.sequence {
(&self.source_head, &self.target_head)
} else {
(&self.target_head, &self.source_head)
};
ContinuityProof {
origin_hash: self.origin_hash,
// Anchor at `head.sequence - 1` — that's the event whose
// forward hash equals `head.parent_hash`. saturating_sub
// for genesis (seq==0).
from_seq: lo_head.sequence.saturating_sub(1),
to_seq: hi_head.sequence.saturating_sub(1),
from_hash: lo_head.parent_hash,
to_hash: hi_head.parent_hash,
}
}
/// Get the current phase.
#[inline]
pub fn phase(&self) -> SuperpositionPhase {
self.phase
}
/// Get the origin hash.
#[inline]
pub fn origin_hash(&self) -> u64 {
self.origin_hash
}
/// Get the source head.
#[inline]
pub fn source_head(&self) -> &CausalLink {
&self.source_head
}
/// Get the target head.
#[inline]
pub fn target_head(&self) -> &CausalLink {
&self.target_head
}
/// Events the target still needs to replay.
pub fn replay_gap(&self) -> u64 {
self.source_head
.sequence
.saturating_sub(self.target_replayed_through)
}
}
impl std::fmt::Debug for SuperpositionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SuperpositionState")
.field("origin", &format!("{:#x}", self.origin_hash))
.field("phase", &self.phase)
.field("source_seq", &self.source_head.sequence)
.field("target_seq", &self.target_head.sequence)
.field("replay_gap", &self.replay_gap())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_link(origin: u64, seq: u64) -> CausalLink {
CausalLink {
origin_hash: origin,
horizon_encoded: 0,
sequence: seq,
parent_hash: seq * 1000, // deterministic for testing
}
}
#[test]
fn test_lifecycle() {
let source_head = make_link(0xAAAA, 100);
let mut state = SuperpositionState::new(0xAAAA, source_head);
assert_eq!(state.phase(), SuperpositionPhase::Localized);
assert!(!state.phase().is_superposed());
// Migration starts
state.advance(MigrationPhase::Transfer);
assert_eq!(state.phase(), SuperpositionPhase::Spreading);
state.advance(MigrationPhase::Replay);
assert_eq!(state.phase(), SuperpositionPhase::Superposed);
assert!(state.phase().is_superposed());
// Source advances while target replays
state.source_advanced(make_link(0xAAAA, 105));
assert_eq!(state.replay_gap(), 5);
assert!(!state.target_caught_up());
// Target catches up
state.target_replayed(make_link(0xAAAA, 105));
assert!(state.target_caught_up());
assert_eq!(state.phase(), SuperpositionPhase::ReadyToCollapse);
// Collapse
state.collapse();
assert!(state.phase().is_collapsed());
state.resolve();
assert_eq!(state.phase(), SuperpositionPhase::Resolved);
}
/// Pin: `target_replayed` must transition from `Spreading`
/// to `ReadyToCollapse` when the target catches up before
/// the migration phase has been advanced to Replay. Pre-fix
/// the transition only fired from `Superposed`; if the
/// target catch-up signal arrived while still in
/// `Spreading` (target replayed faster than the
/// orchestrator's phase advance), the migration stalled —
/// `target_replayed` is wire-driven and isn't re-invoked
/// when `advance(Replay)` later flips the phase.
#[test]
fn target_replayed_can_transition_from_spreading() {
let source_head = make_link(0xAAAA, 100);
let mut state = SuperpositionState::new(0xAAAA, source_head);
// Migration starts; phase is Spreading (Transfer).
state.advance(MigrationPhase::Transfer);
assert_eq!(state.phase(), SuperpositionPhase::Spreading);
// Target catches up before the orchestrator advances
// to Replay.
state.target_replayed(make_link(0xAAAA, 100));
assert!(state.target_caught_up());
// Pre-fix this stayed at `Spreading`. Post-fix it
// transitions directly to `ReadyToCollapse`.
assert_eq!(
state.phase(),
SuperpositionPhase::ReadyToCollapse,
"target_replayed must transition Spreading → ReadyToCollapse \
when target catches up; pre-fix this stuck at Spreading"
);
}
#[test]
fn test_continuity_proof() {
let source_head = make_link(0xAAAA, 100);
let mut state = SuperpositionState::new(0xAAAA, source_head);
state.source_advanced(make_link(0xAAAA, 110));
state.target_replayed(make_link(0xAAAA, 105));
let proof = state.continuity_proof();
assert_eq!(proof.origin_hash, 0xAAAA);
// The proof anchors at `head.sequence - 1` so
// the verifier (`compute_parent_hash` of the event AT that
// seq) sees the same hash bytes that `head.parent_hash`
// carries. Pre-fix the seqs were 105 / 110 and the hashes
// were head.parent_hash — a mismatch the verifier would
// always reject.
assert_eq!(proof.from_seq, 104);
assert_eq!(proof.to_seq, 109);
}
/// Regression for BUG_AUDIT_2026_04_30_CORE.md #99: the proof
/// produced by `SuperpositionState::continuity_proof` must
/// actually verify against an `EntityLog` that contains the
/// matching events. Pre-fix the seqs and hashes pointed to
/// different events so verification always failed; pre-fix
/// also mixed identities when target's seq < source's seq.
///
/// We pin the round-trip:
/// 1. Build a chain of events in an EntityLog (using the
/// same `CausalChainBuilder` pattern the chain.rs tests
/// use, so parent_hash linkage is structurally correct).
/// 2. Build a SuperpositionState whose head links carry the
/// same `parent_hash` values the chain produced.
/// 3. Generate the proof and verify it against the log.
/// 4. Pre-fix: verify_against fails with `HashMismatch`.
/// Post-fix: Ok.
#[test]
fn continuity_proof_round_trips_through_entity_log() {
use crate::adapter::net::identity::EntityKeypair;
use crate::adapter::net::state::causal::{compute_parent_hash, CausalChainBuilder};
use crate::adapter::net::state::EntityLog;
use bytes::Bytes;
let kp = EntityKeypair::generate();
let origin = kp.origin_hash();
let mut log = EntityLog::new(kp.entity_id().clone());
let mut builder = CausalChainBuilder::new(origin);
// Build a 5-event chain. Each event's parent_hash is the
// forward hash of the prior event (link + payload).
for i in 0..5usize {
let event = builder
.append(Bytes::from(format!("event-{}", i)), 0)
.unwrap();
log.append(event).unwrap();
}
// Source's head is event[3]; target replayed up to event[2].
let events: Vec<_> = log.range(1, 5).into_iter().cloned().collect();
assert_eq!(
events.len(),
5,
"got {} events: {:?}",
events.len(),
events.iter().map(|e| e.link.sequence).collect::<Vec<_>>()
);
let source_head_link = events[3].link;
let target_head_link = events[2].link;
let mut state = SuperpositionState::new(origin, source_head_link);
state.target_replayed(target_head_link);
let proof = state.continuity_proof();
// The proof's anchors are seq-1 of each head, since
// head.parent_hash = forward hash of (head.sequence - 1).
assert_eq!(proof.origin_hash, origin);
let lo_seq = source_head_link.sequence.min(target_head_link.sequence);
let hi_seq = source_head_link.sequence.max(target_head_link.sequence);
assert_eq!(proof.from_seq, lo_seq.saturating_sub(1));
assert_eq!(proof.to_seq, hi_seq.saturating_sub(1));
// Round-trip: verify against the log.
// Pre-fix: this fails with HashMismatch because the proof's
// hashes are head.parent_hash but its from_seq/to_seq pointed
// at the heads themselves, so the verifier hashed event at
// from_seq and got a different value than head.parent_hash
// (which is the hash of the event at from_seq - 1).
proof
.verify_against(&log)
.expect("post-fix proof must verify against the log it was derived from");
// Sanity-check the hash bytes: from_hash should equal
// compute_parent_hash of event at from_seq.
let event_at_from = log
.range(proof.from_seq, proof.from_seq)
.into_iter()
.next()
.expect("event at from_seq must exist");
assert_eq!(
proof.from_hash,
compute_parent_hash(&event_at_from.link, &event_at_from.payload),
"from_hash must match the forward hash of event at from_seq"
);
}
/// CR-33: pin the genesis-edge documented limitation. A head
/// with `sequence == 0` (the genesis event) produces a proof
/// where `from_seq == 0` and `from_hash == parent_hash`
/// (typically zero / a genesis sentinel). `verify_against`
/// will fail unless the verifier holds the genesis event
/// — by design, since you can't prove continuity of a
/// genesis-only chain.
///
/// More subtly: a head with `sequence == 1` ALSO produces
/// `from_seq == 0` (via `saturating_sub(1)`). After ANY
/// snapshot-prune that removes the genesis event, the
/// resulting proof is unverifiable for the seq-1 head until
/// the head advances past the prune anchor.
///
/// This test pins both the genesis edge AND the seq-1 edge
/// so a future maintainer touching the `saturating_sub(1)`
/// either preserves the documented behavior or updates this
/// test to reflect a new contract.
#[test]
fn cr33_continuity_proof_at_genesis_and_seq_one_edge_cases() {
// Edge 1: head at exactly seq=0 (genesis-only chain).
let genesis_head = make_link(0xCAFE, 0);
let state = SuperpositionState::new(0xCAFE, genesis_head);
let proof = state.continuity_proof();
assert_eq!(
proof.from_seq, 0,
"genesis head: saturating_sub(1) yields 0 (CR-33 documented)"
);
assert_eq!(proof.to_seq, 0, "genesis head: from_seq == to_seq == 0");
// The proof's hash is whatever genesis's parent_hash carries
// — typically 0 (genesis sentinel). `verify_against` against
// a genesis-only log MIGHT succeed if event[0]'s forward
// hash happens to equal `genesis_head.parent_hash`, but
// typically does not — by design.
// Edge 2: head at seq=1 (one event past genesis). Same
// saturating_sub(1) collapse as genesis.
let seq1_head = make_link(0xBEEF, 1);
let state = SuperpositionState::new(0xBEEF, seq1_head);
let proof = state.continuity_proof();
assert_eq!(
proof.from_seq, 0,
"CR-33: head at seq=1 produces from_seq=0 — same as genesis. \
After ANY snapshot-prune that removes seq=0, the resulting \
proof becomes unverifiable. Documented limitation: heads \
must advance past the prune anchor before producing \
verifiable proofs."
);
assert_eq!(proof.to_seq, 0);
}
#[test]
fn test_auto_ready_to_collapse() {
let source_head = make_link(0xAAAA, 50);
let mut state = SuperpositionState::new(0xAAAA, source_head);
state.advance(MigrationPhase::Replay);
assert_eq!(state.phase(), SuperpositionPhase::Superposed);
// Target catches up while in Superposed phase
state.target_replayed(make_link(0xAAAA, 50));
assert_eq!(state.phase(), SuperpositionPhase::ReadyToCollapse);
}
#[test]
fn test_replay_gap() {
let source_head = make_link(0xAAAA, 100);
let mut state = SuperpositionState::new(0xAAAA, source_head);
assert_eq!(state.replay_gap(), 0); // target starts at same point
state.source_advanced(make_link(0xAAAA, 120));
assert_eq!(state.replay_gap(), 20);
state.target_replayed(make_link(0xAAAA, 110));
assert_eq!(state.replay_gap(), 10);
}
}