9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
1487 lines
63 KiB
Python
1487 lines
63 KiB
Python
"""Bounded transition-system explorer for NIP-RS manual-unread candidates.
|
|
|
|
BFS over canonical global states. At each depth, enabled transitions are
|
|
local actions and message deliveries, interleaved — not phased.
|
|
|
|
Universe: 2 upgraded devices + 1 legacy device, 2 contexts.
|
|
Transitions: mark_unread, mark_read (with frontier advance),
|
|
advance_frontier, compact, reinstall, deliver (including
|
|
duplicate/replay). Legacy rewrite semantics are exercised through the
|
|
deliver path (legacy_sanitize_and_publish), not as a separate
|
|
transition — a legacy device never mutates its own state outside
|
|
delivery, so a dedicated no-op transition added nothing (see NOTE.md).
|
|
|
|
Invariants:
|
|
I1 merge_reg_b associative/commutative/idempotent
|
|
I2 convergence: all delivery orders -> identical override verdict
|
|
I3 no frontier regression
|
|
I4 concurrent set/clear winner stable (order-independent, ancestor-independent)
|
|
I5 compaction: no loss of live set, no resurrection of dead clear,
|
|
survives merge with stale pre-compaction state
|
|
I5c directed deep-history: compact -> new local actions (counter reuse)
|
|
-> delayed stale delivery does not resurrect a dead override or
|
|
lose a genuinely-live one. Scope: the compacting device's own
|
|
pre-compaction ancestor (or an exact copy of it) replayed back to
|
|
that same device.
|
|
I5d cross-device compaction transparency (requalified, NOT
|
|
zero-divergence): a tombstone's counter ceiling can one-shot
|
|
suppress an unrelated device's concurrent fresh set with no
|
|
resurrection, and the suppression is always recoverable by one
|
|
more local action. Proven suppress-only direction via a bounded
|
|
witness cube plus a structural monotonicity argument.
|
|
I6 replay harmless
|
|
I7 legacy rewrite: B sibling keys survive / A overrides erased (witness)
|
|
I8 bounded key growth per context
|
|
I9 DeviceA post-receive counter absorption
|
|
"""
|
|
from itertools import permutations
|
|
from copy import deepcopy
|
|
from model import (
|
|
RegB, merge_reg_b, override_set_b, compact_b,
|
|
RegA, merge_reg_a,
|
|
DeviceB, DeviceA,
|
|
SET, CLEAR,
|
|
legacy_prune, legacy_trim, legacy_sanitize_blob,
|
|
escape_context_key, unescape_context_key, ESCAPE_PREFIX,
|
|
)
|
|
|
|
CONTEXTS = ("c0", "c1")
|
|
FRONTIER_VALS = (10, 20)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I1: algebraic properties
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_merge_algebra_b():
|
|
vals = [0, 1, 2, 3]
|
|
regs = [RegB(s, c, b) for s in vals for c in vals for b in vals]
|
|
violations = []
|
|
for a in regs:
|
|
if merge_reg_b(a, a) != a:
|
|
violations.append(("idempotent", a))
|
|
for a in regs:
|
|
for b in regs:
|
|
if merge_reg_b(a, b) != merge_reg_b(b, a):
|
|
violations.append(("commutative", a, b))
|
|
for a in regs:
|
|
for b in regs:
|
|
for c in regs:
|
|
if merge_reg_b(merge_reg_b(a, b), c) != merge_reg_b(a, merge_reg_b(b, c)):
|
|
violations.append(("associative", a, b, c))
|
|
return violations
|
|
|
|
|
|
def test_merge_algebra_a():
|
|
vals = [0, 1, 2]
|
|
tiebreaks = ["a", "b"]
|
|
ops = [SET, CLEAR]
|
|
baselines = [0, 10]
|
|
regs = [RegA(ct, t, o, bl)
|
|
for ct in vals for t in tiebreaks for o in ops for bl in baselines]
|
|
violations = []
|
|
for tie_op in [CLEAR, SET]:
|
|
for a in regs:
|
|
if merge_reg_a(a, a, tie_op) != a:
|
|
violations.append(("idempotent", tie_op, a))
|
|
for a in regs:
|
|
for b in regs:
|
|
if merge_reg_a(a, b, tie_op) != merge_reg_a(b, a, tie_op):
|
|
violations.append(("commutative", tie_op, a, b))
|
|
for a in regs:
|
|
for b in regs:
|
|
for c in regs:
|
|
ab_c = merge_reg_a(merge_reg_a(a, b, tie_op), c, tie_op)
|
|
a_bc = merge_reg_a(a, merge_reg_a(b, c, tie_op), tie_op)
|
|
if ab_c != a_bc:
|
|
violations.append(("associative", tie_op, a, b, c))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BFS state explorer — Candidate B
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def next_frontier(device, ctx):
|
|
cur = device.effective_frontier(ctx)
|
|
for fv in FRONTIER_VALS:
|
|
if fv > cur:
|
|
return fv
|
|
return None
|
|
|
|
|
|
def enabled_transitions(devices, tie_policy):
|
|
"""Generate (kind, args) tuples for all enabled transitions."""
|
|
trans = []
|
|
for di, d in enumerate(devices):
|
|
for ctx in CONTEXTS:
|
|
if not d.is_legacy:
|
|
trans.append(("mark_unread", di, ctx))
|
|
fv = next_frontier(d, ctx)
|
|
if fv is not None:
|
|
trans.append(("mark_read", di, ctx, fv))
|
|
if ctx in d.overrides:
|
|
trans.append(("compact", di, ctx))
|
|
fv = next_frontier(d, ctx)
|
|
if fv is not None:
|
|
trans.append(("advance", di, ctx, fv))
|
|
if not d.is_legacy:
|
|
trans.append(("reinstall", di))
|
|
for si in range(len(devices)):
|
|
for di in range(len(devices)):
|
|
if si != di:
|
|
trans.append(("deliver", si, di))
|
|
return trans
|
|
|
|
|
|
def apply_transition(devices, t, tie_policy):
|
|
kind = t[0]
|
|
if kind == "mark_unread":
|
|
devices[t[1]].do_mark_unread(t[2])
|
|
elif kind == "mark_read":
|
|
devices[t[1]].do_mark_read(t[2], t[3])
|
|
elif kind == "advance":
|
|
devices[t[1]].do_advance_frontier(t[2], t[3])
|
|
elif kind == "compact":
|
|
devices[t[1]].do_compact(t[2], tie_policy)
|
|
elif kind == "reinstall":
|
|
devices[t[1]].do_reinstall()
|
|
elif kind == "deliver":
|
|
src = devices[t[1]]
|
|
dst = devices[t[2]]
|
|
if src.is_legacy:
|
|
blob = src.legacy_sanitize_and_publish(tie_policy)
|
|
else:
|
|
blob = src.publish_blob(tie_policy)
|
|
dst.receive_merge(blob)
|
|
|
|
|
|
def state_sig(devices, tie_policy):
|
|
return tuple(d.state_key(CONTEXTS, tie_policy) for d in devices)
|
|
|
|
|
|
def check_convergence(devices, tie_policy, trace, violations):
|
|
"""Publish all blobs, deliver in every order, check upgraded devices
|
|
converge on override_is_set for each context.
|
|
|
|
Tests with latest_ts=5 (below all frontiers) so the override is the
|
|
sole unread source — no masking by natural unread.
|
|
"""
|
|
blobs = []
|
|
for d in devices:
|
|
if d.is_legacy:
|
|
blobs.append(d.legacy_sanitize_and_publish(tie_policy))
|
|
else:
|
|
blobs.append(d.publish_blob(tie_policy))
|
|
|
|
verdicts_per_order = []
|
|
for perm in permutations(range(len(blobs))):
|
|
receivers = deepcopy(devices)
|
|
for idx in perm:
|
|
for r in receivers:
|
|
r.receive_merge(blobs[idx])
|
|
per_device = []
|
|
for r in receivers:
|
|
if not r.is_legacy:
|
|
per_device.append(
|
|
tuple(r.override_is_set(ctx, tie_policy) for ctx in CONTEXTS)
|
|
)
|
|
verdicts_per_order.append(tuple(per_device))
|
|
|
|
if len(set(verdicts_per_order)) > 1:
|
|
violations.append(("I2-convergence", trace, set(verdicts_per_order)))
|
|
|
|
|
|
def check_compaction_safety(devices, tie_policy, trace, violations):
|
|
"""For each upgraded device with overrides:
|
|
1. Check override_is_set directly (not via verdict/latest_ts).
|
|
2. Compact and verify override_is_set unchanged.
|
|
3. Merge compacted state with stale pre-compaction state in both orders.
|
|
Verify no resurrection and no loss.
|
|
"""
|
|
for di, d in enumerate(devices):
|
|
if d.is_legacy:
|
|
continue
|
|
for ctx in CONTEXTS:
|
|
reg = d.overrides.get(ctx)
|
|
if reg is None:
|
|
continue
|
|
front = d.effective_frontier(ctx)
|
|
ov_before = d._override_set(reg, front, tie_policy)
|
|
compacted = d._compact(reg, front, tie_policy)
|
|
ov_after = d._override_set(compacted, front, tie_policy) if compacted else False
|
|
|
|
if ov_before and not ov_after:
|
|
violations.append((
|
|
"I5-compaction-lost-set", trace, di, ctx,
|
|
reg, compacted, front, tie_policy
|
|
))
|
|
if not ov_before and ov_after:
|
|
violations.append((
|
|
"I5-compaction-resurrection", trace, di, ctx,
|
|
reg, compacted, front, tie_policy
|
|
))
|
|
|
|
if compacted is not None:
|
|
for merged in [merge_reg_b(compacted, reg), merge_reg_b(reg, compacted)]:
|
|
ov_merged = d._override_set(merged, front, tie_policy)
|
|
if not ov_before and ov_merged:
|
|
violations.append((
|
|
"I5-compaction-merge-resurrection", trace, di, ctx,
|
|
reg, compacted, merged
|
|
))
|
|
|
|
|
|
def explore_b(max_depth=4, tie_policy=CLEAR, device_cls=DeviceB):
|
|
"""BFS over all reachable global states up to max_depth.
|
|
|
|
Returns (states_explored, violations).
|
|
Accepts device_cls for mutation testing via subclassing.
|
|
"""
|
|
def make_devices():
|
|
return [
|
|
device_cls("d0", is_legacy=False),
|
|
device_cls("d1", is_legacy=False),
|
|
device_cls("d2", is_legacy=True),
|
|
]
|
|
|
|
violations = []
|
|
seen = set()
|
|
states_explored = 0
|
|
queue = [(make_devices(), [])]
|
|
|
|
while queue:
|
|
devices, trace = queue.pop(0)
|
|
sig = state_sig(devices, tie_policy)
|
|
if sig in seen:
|
|
continue
|
|
seen.add(sig)
|
|
states_explored += 1
|
|
|
|
for di, d in enumerate(devices):
|
|
for ctx in CONTEXTS:
|
|
prev_front = d.effective_frontier(ctx)
|
|
if prev_front < 0:
|
|
violations.append(("I3-frontier-negative", trace, di, ctx))
|
|
|
|
if len(trace) >= max_depth:
|
|
check_convergence(devices, tie_policy, trace, violations)
|
|
check_compaction_safety(devices, tie_policy, trace, violations)
|
|
continue
|
|
|
|
for t in enabled_transitions(devices, tie_policy):
|
|
new_devs = deepcopy(devices)
|
|
fronts_before = {
|
|
(di, ctx): d.effective_frontier(ctx)
|
|
for di, d in enumerate(new_devs) for ctx in CONTEXTS
|
|
}
|
|
apply_transition(new_devs, t, tie_policy)
|
|
|
|
# Reinstall intentionally wipes local state; frontier regression
|
|
# is only invalid during merge/delivery/compaction/advance.
|
|
if t[0] != "reinstall":
|
|
for (di, ctx), fb in fronts_before.items():
|
|
fa = new_devs[di].effective_frontier(ctx)
|
|
if fa < fb:
|
|
violations.append(("I3-frontier-regression", trace + [t], di, ctx, fb, fa))
|
|
|
|
queue.append((new_devs, trace + [t]))
|
|
|
|
return states_explored, violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I4: concurrent set/clear winner stable
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_concurrent_stability(device_cls=DeviceB):
|
|
"""Two devices concurrently set and clear from every possible ancestor state.
|
|
The winner must be the same regardless of delivery order AND ancestor state."""
|
|
violations = []
|
|
for tie_policy in [CLEAR, SET]:
|
|
for pre_s, pre_c in [(0, 0), (1, 0), (0, 1), (2, 1), (1, 2), (1, 1)]:
|
|
for front in [0, 10]:
|
|
for ctx in CONTEXTS:
|
|
ancestor = RegB(s=pre_s, c=pre_c, b=front)
|
|
|
|
d0 = device_cls("d0")
|
|
d0.frontier[ctx] = front
|
|
d0.overrides[ctx] = deepcopy(ancestor)
|
|
d1 = device_cls("d1")
|
|
d1.frontier[ctx] = front
|
|
d1.overrides[ctx] = deepcopy(ancestor)
|
|
|
|
d0.do_mark_unread(ctx)
|
|
d1.do_mark_read(ctx, front + 10)
|
|
|
|
blob0 = d0.publish_blob(tie_policy)
|
|
blob1 = d1.publish_blob(tie_policy)
|
|
|
|
verdicts = set()
|
|
for first, second in [(blob0, blob1), (blob1, blob0)]:
|
|
r = device_cls("recv")
|
|
r.frontier[ctx] = front
|
|
r.overrides[ctx] = deepcopy(ancestor)
|
|
r.receive_merge(first)
|
|
r.receive_merge(second)
|
|
verdicts.add(r.override_is_set(ctx, tie_policy))
|
|
|
|
if len(verdicts) > 1:
|
|
violations.append((
|
|
"I4-unstable", tie_policy, ctx,
|
|
pre_s, pre_c, front
|
|
))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I5: direct compaction register-level check (all register values x policies)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_compaction_register_exhaustive():
|
|
"""Exhaustive check over bounded register cube and frontier values.
|
|
Tests override_is_set directly — no latest_ts masking."""
|
|
violations = []
|
|
vals = [0, 1, 2, 3]
|
|
frontiers = [0, 10, 20]
|
|
for tie_policy in [CLEAR, SET]:
|
|
for s in vals:
|
|
for c in vals:
|
|
for b in frontiers:
|
|
for fv in frontiers:
|
|
reg = RegB(s=s, c=c, b=b)
|
|
ov_before = override_set_b(reg, fv, tie_policy)
|
|
compacted = compact_b(reg, fv, tie_policy)
|
|
ov_after = (override_set_b(compacted, fv, tie_policy)
|
|
if compacted else False)
|
|
|
|
if ov_before and not ov_after:
|
|
violations.append((
|
|
"loss", tie_policy, reg, fv, compacted
|
|
))
|
|
if not ov_before and ov_after:
|
|
violations.append((
|
|
"resurrection", tie_policy, reg, fv, compacted
|
|
))
|
|
|
|
if compacted is not None:
|
|
merged_fwd = merge_reg_b(compacted, reg)
|
|
merged_rev = merge_reg_b(reg, compacted)
|
|
for label, merged in [("fwd", merged_fwd), ("rev", merged_rev)]:
|
|
ov_merged = override_set_b(merged, fv, tie_policy)
|
|
if not ov_before and ov_merged:
|
|
violations.append((
|
|
f"merge-resurrection-{label}",
|
|
tie_policy, reg, fv, compacted, merged
|
|
))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I5c: directed deep-history — compact -> new actions (counter reuse) ->
|
|
# delayed stale delivery (including split across two slots)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _apply_action_seq(dev, ctx, seq, ts):
|
|
for a in seq:
|
|
if a == "set":
|
|
dev.do_mark_unread(ctx)
|
|
else:
|
|
dev.do_mark_read(ctx, ts)
|
|
|
|
|
|
def _ancestor_ctx_dict(ctx, reg):
|
|
return {f"ov_s:{ctx}": reg.s, f"ov_c:{ctx}": reg.c, f"ov_b:{ctx}": reg.b}
|
|
|
|
|
|
_DEEP_HISTORY_ACTION_SEQS = [
|
|
(), ("set",), ("clear",), ("set", "clear"), ("clear", "set"),
|
|
("set", "set"), ("clear", "clear"),
|
|
]
|
|
# One delivery shape: single unsplit blob. The prior "split_fwd"/"split_rev"
|
|
# shapes are no longer distinct — with the atomic-grouping rule a single-
|
|
# context blob's compliant split puts the whole group in one slot and the
|
|
# other empty, making split_fwd and split_rev semantically identical to
|
|
# single. Keeping only one shape avoids 2/3 duplicate executions (2,016 →
|
|
# 672 meaningful points) while losing zero register-level coverage.
|
|
_DEEP_HISTORY_DELIVERY_SHAPES = ("single",)
|
|
|
|
|
|
def test_deep_history_compaction(device_cls=DeviceB):
|
|
"""Directed check over the exact shape a depth-4 BFS structurally
|
|
cannot reach (~9 transitions): compact -> new local set/clear actions
|
|
(counter reuse against the tombstone floor) -> delayed delivery of
|
|
the pre-compaction stale ancestor, including split across 2 slots.
|
|
|
|
Oracle: compaction is a storage optimization and must never change
|
|
the semantic outcome. A reference device that never compacts, given
|
|
the identical ancestor / frontier advance / action sequence / late
|
|
ancestor delivery, must reach the same override_is_set verdict as
|
|
the compacting device. This directly targets Thufir's counterexample
|
|
(RegB(3,0,10) -> None under delete-on-dominance -> counter reuse ->
|
|
RegB(3,2,20) resurrection) and requires the tombstone floor from
|
|
compact_b to hold under it.
|
|
|
|
Returns (cube_size, violations).
|
|
"""
|
|
violations = []
|
|
cube_size = 0
|
|
ctx = "c0"
|
|
stale_vals = (0, 1, 2, 3)
|
|
baselines = (0, 10)
|
|
post_frontiers = (10, 20)
|
|
|
|
for s0 in stale_vals:
|
|
for c0 in stale_vals:
|
|
for b0 in baselines:
|
|
for f1 in post_frontiers:
|
|
if f1 <= b0:
|
|
continue # not a dominance/compaction scenario
|
|
ancestor = RegB(s=s0, c=c0, b=b0)
|
|
ancestor_blob = _ancestor_ctx_dict(ctx, ancestor)
|
|
for seq in _DEEP_HISTORY_ACTION_SEQS:
|
|
for tie_policy in (CLEAR, SET):
|
|
for shape in _DEEP_HISTORY_DELIVERY_SHAPES:
|
|
cube_size += 1
|
|
|
|
dev = device_cls("d0")
|
|
dev.frontier[ctx] = b0
|
|
dev.overrides[ctx] = ancestor
|
|
dev.do_advance_frontier(ctx, f1)
|
|
dev.do_compact(ctx, tie_policy)
|
|
_apply_action_seq(dev, ctx, seq, f1)
|
|
|
|
if shape == "single":
|
|
dev.receive_merge({"contexts": dict(ancestor_blob)})
|
|
ov_after = dev.override_is_set(ctx, tie_policy)
|
|
|
|
ref = device_cls("ref")
|
|
ref.frontier[ctx] = b0
|
|
ref.overrides[ctx] = ancestor
|
|
ref.do_advance_frontier(ctx, f1)
|
|
_apply_action_seq(ref, ctx, seq, f1)
|
|
ref.receive_merge({"contexts": dict(ancestor_blob)})
|
|
ov_ref = ref.override_is_set(ctx, tie_policy)
|
|
|
|
if ov_after != ov_ref:
|
|
violations.append((
|
|
"I5c-deep-history-divergence", tie_policy, shape,
|
|
ancestor, f1, seq,
|
|
f"compacted_path={ov_after}", f"reference={ov_ref}",
|
|
))
|
|
return cube_size, violations
|
|
|
|
|
|
def test_tombstone_stale_merge_direct():
|
|
"""Tombstone floor merged directly with its own pre-compaction stale
|
|
ancestor (no intervening local actions) must not resurrect and must
|
|
not exceed the ancestor's own verdict."""
|
|
violations = []
|
|
vals = (0, 1, 2, 3)
|
|
frontiers = (0, 10, 20)
|
|
for tie_policy in (CLEAR, SET):
|
|
for s in vals:
|
|
for c in vals:
|
|
for b in frontiers:
|
|
for fv in frontiers:
|
|
if fv <= b:
|
|
continue
|
|
reg = RegB(s=s, c=c, b=b)
|
|
compacted = compact_b(reg, fv, tie_policy)
|
|
if compacted is None:
|
|
continue # virgin register: nothing to tombstone
|
|
ov_before = override_set_b(reg, fv, tie_policy)
|
|
for merged in (merge_reg_b(compacted, reg), merge_reg_b(reg, compacted)):
|
|
ov_merged = override_set_b(merged, fv, tie_policy)
|
|
if not ov_before and ov_merged:
|
|
violations.append((
|
|
"tombstone-stale-merge-resurrection",
|
|
tie_policy, reg, fv, compacted, merged,
|
|
))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I5d: cross-device compaction transparency (requalified — suppress-only,
|
|
# NOT zero-divergence) + re-mark recovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_tombstone_merge_monotonic():
|
|
"""Structural lemma: merging in ANY tombstone RegB(0, k, 0) is a
|
|
monotonically non-increasing function of the ceiling k in
|
|
override_set_b's boolean output, for a fixed receiving register and
|
|
frontier. A tombstone only ever adds to C (its S and B are both 0,
|
|
so max() with any receiving register leaves that register's own S
|
|
and B untouched) — raising C can only weaken S's relative standing,
|
|
never strengthen it. This is what makes resurrection structurally
|
|
impossible and suppression the only possible direction, independent
|
|
of any bounded cube.
|
|
"""
|
|
violations = []
|
|
vals = (0, 1, 2, 3)
|
|
baselines = (0, 10, 20)
|
|
ceilings = (0, 1, 2, 3, 4, 5)
|
|
for tie_policy in (CLEAR, SET):
|
|
for s in vals:
|
|
for c in vals:
|
|
for b in baselines:
|
|
for fv in baselines:
|
|
x_reg = RegB(s=s, c=c, b=b)
|
|
prev = None
|
|
for k in ceilings:
|
|
merged = merge_reg_b(x_reg, RegB(s=0, c=k, b=0))
|
|
cur = override_set_b(merged, fv, tie_policy)
|
|
if prev is not None and cur and not prev:
|
|
violations.append((
|
|
"I5d-non-monotonic-ceiling", tie_policy,
|
|
x_reg, fv, k, merged,
|
|
))
|
|
prev = cur
|
|
return violations
|
|
|
|
|
|
def test_cross_device_compaction_suppression(device_cls=DeviceB):
|
|
"""Compaction is NOT semantically transparent cross-device (I5c only
|
|
covers the same-device replay shape). A tombstone re-encodes
|
|
baseline-dominated death — frontier-relative, doesn't transfer
|
|
across devices — as a clear-counter ceiling — globally comparable —
|
|
so it can one-shot suppress an unrelated device's concurrent fresh
|
|
set whose own counters don't exceed that ceiling.
|
|
|
|
Witness (Paul's report, illustrative — the cube below tests nearby
|
|
parameter values `f_x` in `(5, 15, 25, 35)`, not the literal
|
|
`f_x=30` used in the original report; the shape is the same):
|
|
Y: mark_unread -> RegB(1,0,0); frontier->10 (dead) -> compact ->
|
|
tombstone RegB(0,1,0)
|
|
X: offline, fresh mark_unread at frontier 30 -> RegB(1,0,30), LIVE
|
|
X merges Y's tombstone -> RegB(1,1,30) -> suppressed (clear-wins)
|
|
Control (Y publishes the uncompacted RegB(1,0,0) instead): X stays
|
|
RegB(1,0,30), LIVE — the divergence is caused by compaction, not
|
|
by the merge itself.
|
|
|
|
Proves over a bounded cube, both tie policies: every divergence
|
|
between "X merges Y's tombstone" and "X merges Y's uncompacted
|
|
ancestor" is a suppression (never a resurrection — that would
|
|
contradict test_tombstone_merge_monotonic), and every suppression
|
|
recovers with one more local mark-unread, stable under tombstone
|
|
replay.
|
|
|
|
Returns (cube_size, suppress_count, violations).
|
|
"""
|
|
violations = []
|
|
cube_size = 0
|
|
suppress_count = 0
|
|
dead_vals = (0, 1, 2, 3)
|
|
dead_baselines = (0, 10)
|
|
dead_post_frontiers = (10, 20)
|
|
fresh_frontiers = (5, 15, 25, 35)
|
|
|
|
for tie_policy in (CLEAR, SET):
|
|
for s_y in dead_vals:
|
|
for c_y in dead_vals:
|
|
for b_y in dead_baselines:
|
|
for f_y in dead_post_frontiers:
|
|
if f_y <= b_y:
|
|
continue
|
|
ancestor = RegB(s=s_y, c=c_y, b=b_y)
|
|
tomb = compact_b(ancestor, f_y, tie_policy)
|
|
if tomb is None or tomb == ancestor:
|
|
continue # virgin, or was live (not compacted)
|
|
|
|
for f_x in fresh_frontiers:
|
|
cube_size += 1
|
|
x_reg = RegB(s=1, c=0, b=f_x)
|
|
x_before = override_set_b(x_reg, f_x, tie_policy)
|
|
if not x_before:
|
|
violations.append((
|
|
"I5d-setup-not-live", tie_policy, x_reg, f_x,
|
|
))
|
|
continue
|
|
|
|
ov_tomb = override_set_b(
|
|
merge_reg_b(x_reg, tomb), f_x, tie_policy
|
|
)
|
|
ov_ancestor = override_set_b(
|
|
merge_reg_b(x_reg, ancestor), f_x, tie_policy
|
|
)
|
|
|
|
if ov_tomb == ov_ancestor:
|
|
continue
|
|
if ov_tomb and not ov_ancestor:
|
|
violations.append((
|
|
"I5d-resurrection-vs-ancestor", tie_policy,
|
|
ancestor, tomb, x_reg, f_x,
|
|
))
|
|
continue
|
|
|
|
suppress_count += 1
|
|
dev = device_cls("x")
|
|
dev.frontier["c0"] = f_x
|
|
dev.overrides["c0"] = merge_reg_b(x_reg, tomb)
|
|
dev.do_mark_unread("c0")
|
|
if not dev.override_is_set("c0", tie_policy):
|
|
violations.append((
|
|
"I5d-recovery-failed", tie_policy,
|
|
ancestor, tomb, x_reg, f_x, dev.overrides["c0"],
|
|
))
|
|
continue
|
|
dev.receive_merge({"contexts": {
|
|
"ov_s:c0": tomb.s, "ov_c:c0": tomb.c, "ov_b:c0": tomb.b,
|
|
}})
|
|
if not dev.override_is_set("c0", tie_policy):
|
|
violations.append((
|
|
"I5d-recovery-not-replay-stable", tie_policy,
|
|
ancestor, tomb, x_reg, f_x, dev.overrides["c0"],
|
|
))
|
|
|
|
return cube_size, suppress_count, violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# New invariant: published-state merge closure (Paul's fix-scope item 2,
|
|
# generalizing Thufir's pass-1/2 CRITICAL — dead+dead merge resurrection)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _dead_register_points(tie_policy):
|
|
"""Bounded cube of (label, reg, frontier) points independently
|
|
verified DEAD (inactive) under `tie_policy` by the real
|
|
`override_set_b` predicate — the death cause (baseline dominance,
|
|
clear-count dominance, or clear-wins tie) is whatever the predicate
|
|
actually computes for that point, not asserted by construction.
|
|
"""
|
|
vals = (0, 1, 2, 3)
|
|
baselines = (0, 10, 50)
|
|
frontiers = (0, 20, 60, 100)
|
|
points = []
|
|
for s in vals:
|
|
for c in vals:
|
|
if s == 0 and c == 0:
|
|
continue # virgin: not a "dead override" case
|
|
for b in baselines:
|
|
for fv in frontiers:
|
|
reg = RegB(s=s, c=c, b=b)
|
|
if override_set_b(reg, fv, tie_policy):
|
|
continue # live: out of scope for this invariant
|
|
points.append((f"s={s}c={c}b={b}fv={fv}", reg, fv))
|
|
return points
|
|
|
|
|
|
def test_published_merge_closure(device_cls=DeviceB):
|
|
"""Over reachable *published* states: joining any two individually-
|
|
inactive published states must remain inactive.
|
|
|
|
This targets Thufir's pass-1/2 CRITICAL directly: a dead register's
|
|
death cause is frontier-relative (baseline dominance) or
|
|
device-local-history-relative (clear-count dominance), but the
|
|
componentwise-max join recombines each register's `S`/`C`/`B`
|
|
independent of the history that produced them, so two individually-
|
|
dead registers could — before canonical publication — recombine
|
|
into a live join. Canonicalizing every override to `RegB(0,
|
|
max(S,C), 0)` before serialization (this round's CRITICAL fix)
|
|
folds every dead cause into a single globally-comparable `C`
|
|
ceiling with `S=0`, which per `test_tombstone_merge_monotonic` can
|
|
only ever raise a receiver's `C` — never resurrect.
|
|
|
|
Checked two ways:
|
|
- Directed case: Thufir's exact witness pair — `RegB(3,2,0)`
|
|
inactive via baseline dominance at frontier 50, `RegB(1,2,100)`
|
|
inactive via clear dominance at frontier 100 — whose raw
|
|
componentwise join is `RegB(3,2,100)`, live (`S=3>C=2`,
|
|
`frontier(100) not> B(100)`). Both tie policies.
|
|
- General search: every pairwise join of a bounded cube of
|
|
independently-dead `(reg, frontier)` points (see
|
|
`_dead_register_points`), delivered to a fresh receiver in both
|
|
direct orders and via a one-hop relay that itself republishes
|
|
(re-canonicalizes) what it received before forwarding — covering
|
|
delayed/multi-hop delivery, not just direct pairwise merge.
|
|
|
|
Returns (cube_size, violations).
|
|
"""
|
|
violations = []
|
|
cube_size = 0
|
|
|
|
def _check_pair(tie_policy, label_a, blob_a, label_b, blob_b, tag):
|
|
nonlocal cube_size
|
|
cube_size += 1
|
|
for first, second in [(blob_a, blob_b), (blob_b, blob_a)]:
|
|
recv = device_cls("recv")
|
|
recv.receive_merge(first)
|
|
recv.receive_merge(second)
|
|
if recv.override_is_set("c0", tie_policy):
|
|
violations.append((
|
|
tag, tie_policy, label_a, label_b, recv.overrides.get("c0"),
|
|
))
|
|
# Multi-hop: a relay receives blob_a alone, republishes
|
|
# (re-canonicalizes) before forwarding, then the receiver gets
|
|
# the relayed form plus blob_b directly, in both orders.
|
|
relay = device_cls("relay")
|
|
relay.receive_merge(blob_a)
|
|
relayed = relay.publish_blob(tie_policy)
|
|
for first, second in [(relayed, blob_b), (blob_b, relayed)]:
|
|
recv = device_cls("recv_hop")
|
|
recv.receive_merge(first)
|
|
recv.receive_merge(second)
|
|
if recv.override_is_set("c0", tie_policy):
|
|
violations.append((
|
|
tag + "-multihop", tie_policy, label_a, label_b,
|
|
recv.overrides.get("c0"),
|
|
))
|
|
|
|
# --- Directed case: Thufir's exact witness pair. ---
|
|
for tie_policy in (CLEAR, SET):
|
|
reg_a, front_a = RegB(s=3, c=2, b=0), 50
|
|
reg_b, front_b = RegB(s=1, c=2, b=100), 100
|
|
assert not override_set_b(reg_a, front_a, tie_policy)
|
|
assert not override_set_b(reg_b, front_b, tie_policy)
|
|
|
|
dev_a = device_cls("a")
|
|
dev_a.frontier["c0"] = front_a
|
|
dev_a.overrides["c0"] = reg_a
|
|
dev_b = device_cls("b")
|
|
dev_b.frontier["c0"] = front_b
|
|
dev_b.overrides["c0"] = reg_b
|
|
|
|
_check_pair(
|
|
tie_policy, f"thufir-witness-A={reg_a}@{front_a}",
|
|
dev_a.publish_blob(tie_policy),
|
|
f"thufir-witness-B={reg_b}@{front_b}",
|
|
dev_b.publish_blob(tie_policy),
|
|
"merge-closure-thufir-witness",
|
|
)
|
|
|
|
# --- General search over a bounded cube of dead published states. ---
|
|
for tie_policy in (CLEAR, SET):
|
|
points = _dead_register_points(tie_policy)
|
|
for label_a, reg_a, front_a in points:
|
|
dev_a = device_cls("a")
|
|
dev_a.frontier["c0"] = front_a
|
|
dev_a.overrides["c0"] = reg_a
|
|
blob_a = dev_a.publish_blob(tie_policy)
|
|
for label_b, reg_b, front_b in points:
|
|
dev_b = device_cls("b")
|
|
dev_b.frontier["c0"] = front_b
|
|
dev_b.overrides["c0"] = reg_b
|
|
blob_b = dev_b.publish_blob(tie_policy)
|
|
_check_pair(
|
|
tie_policy, label_a, blob_a, label_b, blob_b,
|
|
"merge-closure-cube",
|
|
)
|
|
|
|
return cube_size, violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I6: replay harmless
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_replay_harmless(device_cls=DeviceB):
|
|
violations = []
|
|
for tie_policy in [CLEAR, SET]:
|
|
for ctx in CONTEXTS:
|
|
d = device_cls("d0")
|
|
d.frontier[ctx] = 10
|
|
d.do_mark_unread(ctx)
|
|
blob = d.publish_blob(tie_policy)
|
|
state_before = (
|
|
dict(d.frontier),
|
|
{k: v for k, v in d.overrides.items()},
|
|
)
|
|
d.receive_merge(blob)
|
|
d.receive_merge(blob)
|
|
d.receive_merge(blob)
|
|
state_after = (
|
|
dict(d.frontier),
|
|
{k: v for k, v in d.overrides.items()},
|
|
)
|
|
if state_before != state_after:
|
|
violations.append(("I6-replay", tie_policy, ctx))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I7: legacy rewrite
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_legacy_rewrite_b():
|
|
"""B's sibling keys survive legacy sanitization (round-trip)."""
|
|
violations = []
|
|
for ctx in CONTEXTS:
|
|
d = DeviceB("d0")
|
|
d.frontier[ctx] = 10
|
|
d.do_mark_unread(ctx)
|
|
blob = d.publish_blob()
|
|
sanitized = legacy_sanitize_blob(blob)
|
|
|
|
recv_orig = DeviceB("recv1")
|
|
recv_orig.receive_merge(blob)
|
|
recv_san = DeviceB("recv2")
|
|
recv_san.receive_merge(sanitized)
|
|
|
|
for c in CONTEXTS:
|
|
if recv_orig.overrides.get(c) != recv_san.overrides.get(c):
|
|
violations.append(("I7-B-sanitize-mutated", c,
|
|
recv_orig.overrides.get(c),
|
|
recv_san.overrides.get(c)))
|
|
return violations
|
|
|
|
|
|
def test_legacy_erasure_a():
|
|
"""A's top-level overrides field is erased by legacy rewrite. Expected witness."""
|
|
d = DeviceA("d0")
|
|
d.frontier["c0"] = 10
|
|
d.do_mark_unread("c0")
|
|
blob = d.publish_blob()
|
|
assert "overrides" in blob
|
|
legacy_blob = {"v": 1, "client_id": "legacy", "contexts": dict(blob["contexts"])}
|
|
return "overrides" not in legacy_blob
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I8: bounded key growth
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_bounded_growth():
|
|
"""I8: bounded key growth, canonical wire shape. A live override
|
|
(last action = mark_unread, still within baseline) publishes
|
|
exactly 3 keys/ctx; a dead override (mark_read past baseline, or
|
|
C > S under clear-wins) canonicalizes to exactly 1 key/ctx
|
|
(`ov_c:` tombstone) at publish time — never 0 (virgin-only) or 3
|
|
(dead-but-uncompacted, which the pre-fix serializer allowed).
|
|
"""
|
|
violations = []
|
|
for ctx in CONTEXTS:
|
|
# Live: 100 set/clear round-trips, ending on a fresh mark_unread
|
|
# so S > C (live under both tie policies) at publish time.
|
|
d = DeviceB("d0")
|
|
d.frontier[ctx] = 10
|
|
for _ in range(100):
|
|
d.do_mark_unread(ctx)
|
|
d.do_mark_read(ctx, d.effective_frontier(ctx) + 1)
|
|
d.do_mark_unread(ctx)
|
|
blob = d.publish_blob(CLEAR)
|
|
ov_keys = [k for k in blob["contexts"] if k.startswith("ov_")]
|
|
if ov_keys != [f"ov_s:{ctx}", f"ov_c:{ctx}", f"ov_b:{ctx}"]:
|
|
violations.append(("I8-growth-live", ctx, ov_keys))
|
|
|
|
# Dead: advance the frontier past baseline B — override_set_b's
|
|
# baseline-dominance clause forces S dead regardless of S vs C.
|
|
d.do_advance_frontier(ctx, d.effective_frontier(ctx) + 100)
|
|
tomb_blob = d.publish_blob(CLEAR)
|
|
tomb_keys = [k for k in tomb_blob["contexts"] if k.startswith("ov_")]
|
|
if tomb_keys != [f"ov_c:{ctx}"]:
|
|
violations.append(("I8-growth-tombstone", ctx, tomb_keys))
|
|
return violations
|
|
|
|
|
|
def test_wire_shape_exact():
|
|
"""Exact wire-shape regression (Paul's fix-scope item 4): a live
|
|
override serializes to exactly 3 `ov_*` keys, a dead override to
|
|
exactly 1 (`ov_c:` only, zero-valued `ov_s`/`ov_b` omitted), and a
|
|
virgin override to exactly 0. Checked directly against
|
|
`publish_blob`'s output, independent of `do_compact`.
|
|
"""
|
|
violations = []
|
|
for tie_policy in (CLEAR, SET):
|
|
# Live.
|
|
d_live = DeviceB("d0")
|
|
d_live.frontier["c0"] = 10
|
|
d_live.do_mark_unread("c0")
|
|
live_blob = d_live.publish_blob(tie_policy)
|
|
live_keys = sorted(k for k in live_blob["contexts"] if k.startswith("ov_"))
|
|
if live_keys != ["ov_b:c0", "ov_c:c0", "ov_s:c0"]:
|
|
violations.append(("wire-shape-live", tie_policy, live_keys))
|
|
|
|
# Dead (clear-wins only: S==C>0 is dead under CLEAR, live under
|
|
# SET — use baseline dominance instead so it's dead under both).
|
|
d_dead = DeviceB("d0")
|
|
d_dead.frontier["c0"] = 10
|
|
d_dead.do_mark_unread("c0")
|
|
d_dead.do_advance_frontier("c0", 100)
|
|
dead_blob = d_dead.publish_blob(tie_policy)
|
|
dead_keys = sorted(k for k in dead_blob["contexts"] if k.startswith("ov_"))
|
|
if dead_keys != ["ov_c:c0"]:
|
|
violations.append(("wire-shape-tombstone", tie_policy, dead_keys))
|
|
if dead_blob["contexts"]["ov_c:c0"] != 1:
|
|
violations.append((
|
|
"wire-shape-tombstone-ceiling", tie_policy,
|
|
dead_blob["contexts"]["ov_c:c0"],
|
|
))
|
|
|
|
# Virgin: no override ever set for this context.
|
|
d_virgin = DeviceB("d0")
|
|
d_virgin.frontier["c0"] = 10
|
|
d_virgin.overrides["c0"] = RegB(s=0, c=0, b=0)
|
|
virgin_blob = d_virgin.publish_blob(tie_policy)
|
|
virgin_keys = [k for k in virgin_blob["contexts"] if k.startswith("ov_")]
|
|
if virgin_keys:
|
|
violations.append(("wire-shape-virgin", tie_policy, virgin_keys))
|
|
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I9: DeviceA counter absorption
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_a_counter_absorption():
|
|
"""After receiving a blob with counter=10, a local action must use counter>10."""
|
|
d0 = DeviceA("d0")
|
|
d0.frontier["c0"] = 10
|
|
d0.counter = 10
|
|
d0.do_mark_unread("c0")
|
|
blob0 = d0.publish_blob()
|
|
|
|
d1 = DeviceA("d1")
|
|
d1.frontier["c0"] = 10
|
|
d1.receive_merge(blob0)
|
|
assert d1.counter >= 10, f"counter not absorbed: {d1.counter}"
|
|
|
|
d1.do_mark_read("c0", 20)
|
|
reg = d1.overrides.get("c0")
|
|
assert reg is not None and reg.counter > 10, \
|
|
f"post-receive clear at counter {reg.counter} would lose to set at 10"
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Identity-free (B): reinstall convergence
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_b_identity_free(device_cls=DeviceB):
|
|
violations = []
|
|
for tie_policy in [CLEAR, SET]:
|
|
for ctx in CONTEXTS:
|
|
d = device_cls("d0")
|
|
d.frontier[ctx] = 10
|
|
d.do_mark_unread(ctx)
|
|
blob1 = d.publish_blob(tie_policy)
|
|
|
|
d_re = device_cls("d0_reinstalled")
|
|
d_re.receive_merge(blob1)
|
|
d_re.do_mark_read(ctx, 20)
|
|
blob2 = d_re.publish_blob(tie_policy)
|
|
|
|
verdicts = set()
|
|
for first, second in [(blob1, blob2), (blob2, blob1)]:
|
|
recv = device_cls("recv")
|
|
recv.receive_merge(first)
|
|
recv.receive_merge(second)
|
|
verdicts.add(recv.override_is_set(ctx, tie_policy))
|
|
if len(verdicts) > 1:
|
|
violations.append(("identity-free", tie_policy, ctx))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Legacy prune/trim interaction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_legacy_prune_interaction():
|
|
"""ov_ keys survive prune; msg:ov_ nested keys would be pruned (state loss)."""
|
|
base = {"c0": 50, "msg:m1": 30, "thread:t1": 40}
|
|
ov = {"ov_s:c0": 1, "ov_c:c0": 0, "ov_b:c0": 10}
|
|
all_keys = {**base, **ov}
|
|
pruned = legacy_prune(all_keys, horizon=35)
|
|
ov_survived = all(k in pruned for k in ov)
|
|
msg_pruned = "msg:m1" not in pruned
|
|
|
|
nested = {"msg:ov_s:c0": 1, "msg:ov_c:c0": 0, "msg:ov_b:c0": 10}
|
|
pruned_nested = legacy_prune({**base, **nested}, horizon=35)
|
|
nested_lost = any(k not in pruned_nested for k in nested)
|
|
return ov_survived, msg_pruned, nested_lost
|
|
|
|
|
|
def test_legacy_trim_interaction():
|
|
"""Excess override keys block legacy publish when budget exceeded."""
|
|
contexts = {"c0": 50}
|
|
for i in range(1000):
|
|
contexts[f"ov_s:c{i}"] = 1
|
|
contexts[f"ov_c:c{i}"] = 0
|
|
contexts[f"ov_b:c{i}"] = 10
|
|
_, fits = legacy_trim(contexts, "client1", max_bytes=32768)
|
|
return not fits
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multi-slot union
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_multi_slot_union(device_cls=DeviceB):
|
|
"""Split a published blob across 2 slots using the atomic-grouping rule,
|
|
deliver each slot separately, verify convergence with delivering the full blob.
|
|
|
|
Production: mergeReadStateEvents merges per-slot blobs with per-context
|
|
max(). Override sibling keys are individual context entries, so they
|
|
follow the same merge path. The atomic-grouping rule requires that all
|
|
`ov_*` sibling keys for a context travel with that context's frontier
|
|
key in the same slot — `split_blob_into_slots` enforces this.
|
|
"""
|
|
violations = []
|
|
for tie_policy in [CLEAR, SET]:
|
|
dev = device_cls("d0")
|
|
dev.frontier["c0"] = 10
|
|
dev.frontier["c1"] = 20
|
|
dev.do_mark_unread("c0")
|
|
dev.do_mark_read("c1", 30)
|
|
|
|
full_blob = dev.publish_blob(tie_policy)
|
|
slots = dev.split_blob_into_slots(tie_policy, n_slots=2)
|
|
slot0, slot1 = slots[0], slots[1]
|
|
|
|
recv_full = device_cls("recv_full")
|
|
recv_full.receive_merge(full_blob)
|
|
|
|
for first, second in [(slot0, slot1), (slot1, slot0)]:
|
|
recv_split = device_cls("recv_split")
|
|
recv_split.receive_merge(first)
|
|
recv_split.receive_merge(second)
|
|
|
|
for ctx in CONTEXTS:
|
|
ov_full = recv_full.override_is_set(ctx, tie_policy)
|
|
ov_split = recv_split.override_is_set(ctx, tie_policy)
|
|
f_full = recv_full.effective_frontier(ctx)
|
|
f_split = recv_split.effective_frontier(ctx)
|
|
if ov_full != ov_split:
|
|
violations.append(("multi-slot-override", tie_policy, ctx))
|
|
if f_full != f_split:
|
|
violations.append(("multi-slot-frontier", tie_policy, ctx))
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Interleaved-delivery grouping: Thufir's CRITICAL transport counterexample
|
|
#
|
|
# Without the atomic-grouping rule a compliant publisher would still split
|
|
# ov_s:/ov_c:/ov_b: across slots as independent entries. M8's per-entry
|
|
# split places frontier+ov_s: in slot 0 and ov_b:+ov_c: in slot 1. An
|
|
# observer holding only slot 0 reconstructs RegB(1,0,0), judges it
|
|
# baseline-dead (B=0 ≤ frontier=10), and canonically publishes tombstone
|
|
# RegB(0,1,0). After all slots and that transient tombstone are eventually
|
|
# merged the result is RegB(1,1,10) — dead under clear-wins —
|
|
# permanently suppressing a live override.
|
|
#
|
|
# The atomic-grouping rule closes this: every ov_* entry for a context
|
|
# travels with the context's frontier entry, so a receiver always sees
|
|
# either the complete register or nothing. This test exercises both:
|
|
# - The PASS path: grouped slots → no false tombstone possible.
|
|
# - The FAIL path (M8): per-entry split → Thufir's exact witness reproduced.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_interleaved_delivery_grouping(device_cls=DeviceB):
|
|
"""Exercise receive-one-slot → canonical re-publish → receive-rest →
|
|
re-publish permutations, both slot orders, including delayed delivery
|
|
of both transient re-publications to a third observer.
|
|
|
|
Protocol sequence (exact, per Paul's brief):
|
|
1. partial_obs receives first_slot → publishes transient_1.
|
|
2. partial_obs receives second_slot → publishes transient_2.
|
|
3. Third-party finals receive BOTH source slots AND both transient
|
|
publications in relevant interleaving orders.
|
|
Oracle: after eventual delivery of ALL blobs (both source slots +
|
|
both transients), every observer's override matches source liveness.
|
|
|
|
With the atomic-grouping rule (default DeviceB):
|
|
- The compliant split puts the full register in one slot, the other
|
|
is empty. partial_obs after step 1 holds either the complete
|
|
register (live → transient_1 is live) or nothing (transient_1 is
|
|
empty/virgin). Either way, step 2 delivers the remaining (possibly
|
|
empty) slot. Final merge of all blobs = source liveness. PASS.
|
|
With per-entry splitting (M8):
|
|
- slot 0 carries frontier + ov_s: (partial → RegB(1,0,0), dead).
|
|
transient_1 is tombstone RegB(0,1,0). After step 2 partial_obs
|
|
holds full register but transient_1 tombstone is already in
|
|
circulation. Finals that receive transient_1 get
|
|
RegB(1,1,10) — dead under clear-wins. FAIL (Thufir's witness).
|
|
"""
|
|
violations = []
|
|
|
|
# Source: live override RegB(1,0,10) at frontier=10 — Thufir's witness.
|
|
src_s, src_c, src_b, src_front = 1, 0, 10, 10
|
|
|
|
for tie_policy in (CLEAR, SET):
|
|
src = device_cls("src")
|
|
src.frontier["c0"] = src_front
|
|
src.overrides["c0"] = RegB(s=src_s, c=src_c, b=src_b)
|
|
|
|
# Confirm source is actually live.
|
|
assert src.override_is_set("c0", tie_policy), (
|
|
f"test precondition: source must be live under {tie_policy}"
|
|
)
|
|
|
|
# Produce the source's two slots via the (possibly mutated) split.
|
|
slots = src.split_blob_into_slots(tie_policy, n_slots=2)
|
|
slot0, slot1 = slots[0], slots[1]
|
|
src_live = src.override_is_set("c0", tie_policy)
|
|
|
|
for first_slot, second_slot in [(slot0, slot1), (slot1, slot0)]:
|
|
# Step 1: partial_obs receives first slot, canonically re-publishes.
|
|
partial_obs = device_cls("partial_obs")
|
|
partial_obs.receive_merge(first_slot)
|
|
transient_1 = partial_obs.publish_blob(tie_policy)
|
|
|
|
# Step 2: partial_obs receives second slot, publishes again.
|
|
partial_obs.receive_merge(second_slot)
|
|
transient_2 = partial_obs.publish_blob(tie_policy)
|
|
|
|
# Step 3: third-party finals receive BOTH source slots AND both
|
|
# transient publications, in several representative interleaving
|
|
# orders. All must agree with source liveness.
|
|
# Representative orders: transient_1 before both slots (most
|
|
# dangerous under M8), transient_1 after both slots, and
|
|
# interleaved. We check 3 explicit orders rather than all 4!
|
|
# permutations (24) for speed; M8's canonical false-clear path
|
|
# (transient_1 first, then second_slot only) is order 1.
|
|
check_orders = [
|
|
# Most dangerous: transient_1 arrives first, before any source
|
|
[transient_1, slot0, slot1, transient_2],
|
|
# Normal: both source slots first, then both transients
|
|
[slot0, slot1, transient_1, transient_2],
|
|
# Interleaved: first source, transient_1, second source, transient_2
|
|
[first_slot, transient_1, second_slot, transient_2],
|
|
]
|
|
|
|
for order in check_orders:
|
|
final = device_cls("final")
|
|
for blob in order:
|
|
final.receive_merge(blob)
|
|
final_live = final.override_is_set("c0", tie_policy)
|
|
if final_live != src_live:
|
|
t1_reg = partial_obs.overrides.get("c0")
|
|
violations.append((
|
|
"interleaved-delivery-false-clear",
|
|
tie_policy,
|
|
f"slot_order=(first={list(first_slot['contexts'].keys())[:2]}...)",
|
|
f"delivery_order={[list(b['contexts'].keys())[:2] for b in order]}",
|
|
f"transient_1_reg={t1_reg}",
|
|
f"final_reg={final.overrides.get('c0')}",
|
|
f"expected_live={src_live} got_live={final_live}",
|
|
))
|
|
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Escaped-context slot-grouping regression
|
|
#
|
|
# Thufir's CRITICAL (round 2): a raw context ID that starts with a
|
|
# reserved prefix (e.g. "ov_s:evil") escapes to "esc:ov_s:evil" as its
|
|
# frontier wire key. Before the fix, split_blob_into_slots grouped the
|
|
# frontier by its wire key ("esc:ov_s:evil") but the ov_* siblings by
|
|
# the raw suffix ("ov_s:evil") — two identities for one logical context.
|
|
# The frontier and its siblings landed in different slots.
|
|
#
|
|
# Across publication cycles the replaceable slot d-tag coordinates
|
|
# update slot-by-slot. A relay can therefore serve: new frontier slot
|
|
# (just published, carries esc:ov_s:evil=10) + stale override slot
|
|
# (old coordinate, carries ov_s/ov_c/ov_b at b=0 from the old pub).
|
|
# The reconstructed register is RegB(s=1, c=0, b=0) at frontier=10 —
|
|
# baseline-dead. Canonical re-publication emits tombstone RegB(0,1,0).
|
|
# Eventually both current slots + the transient merge to RegB(1,1,10) —
|
|
# dead under clear-wins — permanently suppressing a live override.
|
|
#
|
|
# The fix: derive the frontier's group identity via unescape_context_key
|
|
# so it joins the same group as its ov_* siblings. This test exercises
|
|
# both directions: M9 (reverts to escaped-key grouping) must reproduce
|
|
# the witness, and the correct model must pass.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_escaped_context_slot_grouping(device_cls=DeviceB):
|
|
"""Regression for escaped-context identity mismatch in split_blob_into_slots.
|
|
|
|
Scenario:
|
|
1. Source has a live override on raw context "ov_s:evil" (escapes to
|
|
"esc:ov_s:evil" as frontier wire key) — Thufir's exact escaped witness.
|
|
2. Source publishes twice: first at frontier=0/b=0, then after advancing
|
|
frontier to 10 and re-marking unread (b=10). Each publication produces
|
|
2 slots. Simulates a relay retaining a stale old-cycle slot under its
|
|
old replaceable coordinate while only the new-cycle slot for the OTHER
|
|
half has been updated — the old/new slot-coordinate mixture.
|
|
3. An observer receives: new-cycle frontier-bearing slot (frontier=10,
|
|
esc:ov_s:evil=10) + stale old-cycle override slot (ov_s/ov_c/ov_b from
|
|
first pub where b=0).
|
|
4. Observer canonically re-publishes (mandatory, per NIP-RS spec).
|
|
5. A third-party final observer receives both current-cycle source slots
|
|
plus the transient re-publication.
|
|
6. Oracle: final observer must see the override as live.
|
|
|
|
With the unescape-before-group fix: frontier + ov_* siblings always land
|
|
in the same slot → no partial register → no false tombstone. PASS.
|
|
With M9 (escaped-key grouping): frontier in one slot, siblings in another
|
|
→ partial reconstruction → false tombstone → final merge dead. FAIL.
|
|
"""
|
|
violations = []
|
|
raw_ctx = "ov_s:evil"
|
|
|
|
for tie_policy in (CLEAR, SET):
|
|
# --- Publication cycle 1: initial state, frontier=0 ---
|
|
src_old = device_cls("src")
|
|
src_old.frontier[raw_ctx] = 0
|
|
src_old.do_mark_unread(raw_ctx) # RegB(s=1, c=0, b=0)
|
|
old_slots = src_old.split_blob_into_slots(tie_policy, n_slots=2)
|
|
# old_slots[0] is the "stale old-coordinate slot" a relay may retain.
|
|
|
|
# --- Publication cycle 2: frontier advances, re-mark-unread ---
|
|
src_new = device_cls("src")
|
|
src_new.frontier[raw_ctx] = 10
|
|
src_new.do_mark_unread(raw_ctx) # RegB(s=1, c=0, b=10) — live at frontier=10
|
|
|
|
assert src_new.override_is_set(raw_ctx, tie_policy), (
|
|
f"test precondition: source must be live under {tie_policy}"
|
|
)
|
|
|
|
new_slots = src_new.split_blob_into_slots(tie_policy, n_slots=2)
|
|
|
|
# --- Full delivery: both new slots → both current-cycle slot arrive ---
|
|
recv_full = device_cls("recv_full")
|
|
recv_full.receive_merge(new_slots[0])
|
|
recv_full.receive_merge(new_slots[1])
|
|
if not recv_full.override_is_set(raw_ctx, tie_policy):
|
|
violations.append((
|
|
"escaped-ctx-full-delivery-dead", tie_policy,
|
|
f"full={recv_full.overrides.get(raw_ctx)}",
|
|
))
|
|
|
|
# --- Mixture: new frontier-bearing slot + stale old override slot ---
|
|
# Identify which new slot carries the frontier and which carries ov_*,
|
|
# then pair the frontier slot with the old-cycle override slot.
|
|
wire_frontier = escape_context_key(raw_ctx)
|
|
|
|
new_frontier_slot_idx = 0 if wire_frontier in new_slots[0]["contexts"] else 1
|
|
new_frontier_slot = new_slots[new_frontier_slot_idx]
|
|
old_override_slot = old_slots[1 - new_frontier_slot_idx] # opposite slot
|
|
|
|
# Check whether the frontier and ov_* siblings are co-located in new_slots.
|
|
ov_s_key = f"ov_s:{raw_ctx}"
|
|
frontier_and_ov_same_slot = (
|
|
wire_frontier in new_slots[new_frontier_slot_idx]["contexts"] and
|
|
ov_s_key in new_slots[new_frontier_slot_idx]["contexts"]
|
|
)
|
|
|
|
if frontier_and_ov_same_slot:
|
|
# Correct grouping: old override slot has nothing relevant, mixture
|
|
# is safe by construction — the stale slot is just an empty dict.
|
|
# Verify anyway for defense-in-depth.
|
|
obs = device_cls("obs")
|
|
obs.receive_merge(new_frontier_slot)
|
|
obs.receive_merge(old_override_slot)
|
|
transient = obs.publish_blob(tie_policy)
|
|
|
|
final = device_cls("final")
|
|
final.receive_merge(new_slots[0])
|
|
final.receive_merge(new_slots[1])
|
|
final.receive_merge(transient)
|
|
if not final.override_is_set(raw_ctx, tie_policy):
|
|
violations.append((
|
|
"escaped-ctx-grouped-mixture-dead", tie_policy,
|
|
f"transient={obs.overrides.get(raw_ctx)}",
|
|
f"final={final.overrides.get(raw_ctx)}",
|
|
))
|
|
else:
|
|
# Mismatched grouping (M9 path): frontier and siblings split.
|
|
# The mixture produces a partial register → false tombstone.
|
|
obs = device_cls("obs")
|
|
obs.receive_merge(new_frontier_slot) # gets frontier=10, no ov_*
|
|
obs.receive_merge(old_override_slot) # gets ov_s/ov_c/ov_b at b=0
|
|
transient = obs.publish_blob(tie_policy)
|
|
|
|
# Final observer gets everything: both new slots + transient.
|
|
for order in [(new_slots[0], new_slots[1]), (new_slots[1], new_slots[0])]:
|
|
final = device_cls("final")
|
|
final.receive_merge(order[0])
|
|
final.receive_merge(order[1])
|
|
final.receive_merge(transient)
|
|
if not final.override_is_set(raw_ctx, tie_policy):
|
|
violations.append((
|
|
"escaped-ctx-mixture-false-clear", tie_policy,
|
|
f"obs_reg={obs.overrides.get(raw_ctx)}",
|
|
f"transient_reg={transient['contexts']}",
|
|
f"final_reg={final.overrides.get(raw_ctx)}",
|
|
))
|
|
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Reserved key namespace: adversarial prefix collision
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_reserved_namespace_collision():
|
|
"""A genuine user context whose raw ID begins with the reserved `ov_`
|
|
stem (e.g. a pre-existing legacy context literally named `ov_s:evil`)
|
|
must round-trip as frontier state, not be misparsed as a control key
|
|
for a different context, and must not collide with a real override's
|
|
sibling keys in the same flattened contexts map.
|
|
|
|
Exercises: escape on publish, unescape on receive, and a live
|
|
override on a DIFFERENT context in the same blob to prove no
|
|
control-key collision occurs.
|
|
"""
|
|
violations = []
|
|
adversarial_raw = "ov_s:evil" # would misparse as ov_s: control for ctx "evil"
|
|
real_ctx = "c0"
|
|
|
|
# Escaping must be a no-op for every context ID Buzz actually
|
|
# generates, and must trigger for the adversarial one.
|
|
for benign in ("b68cd7cb-6f8d-4641-b743-a7349eb4114b",
|
|
"msg:" + "a" * 64, "thread:" + "b" * 64):
|
|
if escape_context_key(benign) != benign:
|
|
violations.append(("namespace-benign-escaped", benign))
|
|
if escape_context_key(adversarial_raw) == adversarial_raw:
|
|
violations.append(("namespace-adversarial-not-escaped", adversarial_raw))
|
|
if not escape_context_key(adversarial_raw).startswith(ESCAPE_PREFIX):
|
|
violations.append(("namespace-adversarial-missing-marker", adversarial_raw))
|
|
|
|
dev = DeviceB("d0")
|
|
dev.frontier[adversarial_raw] = 42
|
|
dev.frontier[real_ctx] = 5
|
|
dev.do_mark_unread(real_ctx)
|
|
blob = dev.publish_blob()
|
|
|
|
wire_key = escape_context_key(adversarial_raw)
|
|
if wire_key not in blob["contexts"]:
|
|
violations.append(("namespace-wire-key-missing", wire_key, blob["contexts"]))
|
|
if blob["contexts"].get(wire_key) != 42:
|
|
violations.append(("namespace-value-corrupted", wire_key, blob["contexts"].get(wire_key)))
|
|
|
|
recv = DeviceB("recv")
|
|
recv.receive_merge(blob)
|
|
if recv.effective_frontier(adversarial_raw) != 42:
|
|
violations.append((
|
|
"namespace-roundtrip-failed", adversarial_raw,
|
|
recv.effective_frontier(adversarial_raw),
|
|
))
|
|
if adversarial_raw in recv.overrides:
|
|
violations.append(("namespace-misparsed-as-override", adversarial_raw))
|
|
if recv.overrides.get(real_ctx) is None or recv.overrides[real_ctx].s == 0:
|
|
violations.append(("namespace-real-override-corrupted", real_ctx, recv.overrides.get(real_ctx)))
|
|
|
|
return violations
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run all
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def run_all():
|
|
print("=" * 60)
|
|
print("NIP-RS manual-unread exhaustive model")
|
|
print("=" * 60)
|
|
total_violations = 0
|
|
|
|
def report(name, violations):
|
|
nonlocal total_violations
|
|
n = len(violations) if isinstance(violations, list) else 0
|
|
total_violations += n
|
|
status = "PASS" if n == 0 else f"FAIL ({n})"
|
|
print(f" {name}: {status}")
|
|
if n > 0:
|
|
for v in violations[:3]:
|
|
print(f" {v}")
|
|
|
|
print("\n--- I1: merge algebra (B) ---")
|
|
report("assoc/commut/idempot", test_merge_algebra_b())
|
|
|
|
print("\n--- I1: merge algebra (A) ---")
|
|
report("assoc/commut/idempot", test_merge_algebra_a())
|
|
|
|
print("\n--- I2+I3+I5: BFS explorer (B, clear-wins) ---")
|
|
n, v = explore_b(max_depth=4, tie_policy=CLEAR)
|
|
print(f" states explored: {n}")
|
|
report("convergence+frontier+compaction", v)
|
|
|
|
print("\n--- I2+I3+I5: BFS explorer (B, set-wins) ---")
|
|
n, v = explore_b(max_depth=4, tie_policy=SET)
|
|
print(f" states explored: {n}")
|
|
report("convergence+frontier+compaction", v)
|
|
|
|
print("\n--- I4: concurrent set/clear stability ---")
|
|
report("stable winner", test_concurrent_stability())
|
|
|
|
print("\n--- I5: compaction register-level exhaustive ---")
|
|
report("all register values x policies", test_compaction_register_exhaustive())
|
|
|
|
print("\n--- I5c: directed deep-history (compact -> reuse -> stale delivery) ---")
|
|
cube_size, deep_v = test_deep_history_compaction()
|
|
print(f" parameter cube size: {cube_size}")
|
|
report("no divergence from never-compact reference", deep_v)
|
|
|
|
print("\n--- I5c: tombstone + stale-ancestor merge (direct) ---")
|
|
report("no resurrection", test_tombstone_stale_merge_direct())
|
|
|
|
print("\n--- I5d: tombstone-merge monotonicity (structural lemma) ---")
|
|
report("ceiling never strengthens S", test_tombstone_merge_monotonic())
|
|
|
|
print("\n--- I5d: cross-device compaction transparency (suppress-only) ---")
|
|
cd_cube, cd_suppress, cd_v = test_cross_device_compaction_suppression()
|
|
print(f" parameter cube size: {cd_cube} suppressions observed: {cd_suppress}")
|
|
report("suppress-only + recoverable", cd_v)
|
|
|
|
print("\n--- Published-state merge closure (canonical publication guarantee) ---")
|
|
mc_cube, mc_v = test_published_merge_closure()
|
|
print(f" pairs checked: {mc_cube}")
|
|
report("no dead+dead resurrection", mc_v)
|
|
|
|
print("\n--- I6: replay harmless ---")
|
|
report("replay", test_replay_harmless())
|
|
|
|
print("\n--- I7: legacy rewrite (B) ---")
|
|
report("sibling keys survive", test_legacy_rewrite_b())
|
|
|
|
print("\n--- I7: legacy erasure (A) — expected witness ---")
|
|
erased = test_legacy_erasure_a()
|
|
print(f" overrides erased by legacy: {'CONFIRMED' if erased else 'NOT FOUND'}")
|
|
if not erased:
|
|
total_violations += 1
|
|
|
|
print("\n--- I8: bounded growth ---")
|
|
report("canonical wire shape (3 live / 1 tombstone)", test_bounded_growth())
|
|
|
|
print("\n--- I8: exact wire-shape regression ---")
|
|
report("live=3 keys, tombstone=1 key, virgin=0 keys", test_wire_shape_exact())
|
|
|
|
print("\n--- I9: DeviceA counter absorption ---")
|
|
absorbed = test_a_counter_absorption()
|
|
print(f" post-receive counter > received: {'CONFIRMED' if absorbed else 'FAIL'}")
|
|
if not absorbed:
|
|
total_violations += 1
|
|
|
|
print("\n--- Identity-free (B) ---")
|
|
report("reinstall convergence", test_b_identity_free())
|
|
|
|
print("\n--- Legacy prune interaction ---")
|
|
ov_ok, msg_ok, nested_lost = test_legacy_prune_interaction()
|
|
print(f" ov_ keys survive: {'PASS' if ov_ok else 'FAIL'}")
|
|
print(f" msg: pruned at horizon: {'PASS' if msg_ok else 'FAIL'}")
|
|
print(f" nested msg:ov_ lost: {'CONFIRMED (hazard)' if nested_lost else 'NOT FOUND'}")
|
|
if not ov_ok:
|
|
total_violations += 1
|
|
|
|
print("\n--- Legacy trim interaction ---")
|
|
blocked = test_legacy_trim_interaction()
|
|
print(f" excess overrides block publish: {'CONFIRMED (hazard)' if blocked else 'NOT FOUND'}")
|
|
|
|
print("\n--- Multi-slot union ---")
|
|
report("split+merge convergence", test_multi_slot_union())
|
|
|
|
print("\n--- Interleaved delivery + atomic grouping rule ---")
|
|
report("no false clear under slot interleaving", test_interleaved_delivery_grouping())
|
|
|
|
print("\n--- Escaped-context slot-grouping regression ---")
|
|
report("escaped ctx: frontier + ov_* siblings same slot", test_escaped_context_slot_grouping())
|
|
|
|
print("\n--- Reserved key namespace: adversarial prefix collision ---")
|
|
report("escape/unescape + no misparse", test_reserved_namespace_collision())
|
|
|
|
print("\n" + "=" * 60)
|
|
if total_violations == 0:
|
|
print("ALL INVARIANTS HOLD — 0 violations")
|
|
else:
|
|
print(f"VIOLATIONS: {total_violations}")
|
|
print("=" * 60)
|
|
return total_violations
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
sys.exit(0 if run_all() == 0 else 1)
|