Files
209536ade6 docs(nips): add single-coordinate manual-unread override layer and verification model to NIP-RS (#2864)
## Summary

Amends `docs/nips/NIP-RS.md` with the manual mark-as-unread override
layer and includes `docs/formal/nip-rs-unread/`, the bounded exhaustive
verification model that preceded and informed the spec.

All `ov_*` override state lives in exactly one coordinate per
installation. That single constraint is what makes the rest of the
amendment small: override state never moves between coordinates, so
there is no slot lifecycle to make crash-safe, and the only durability
obligation is carry-forward on `client_id` rotation.

## Spec changes (`docs/nips/NIP-RS.md`)

- **Non-Goals:** drop the stale line stating mark-as-unread is out of
scope; state the `ov_*` durability exception to the
best-effort/time-horizon model.
- **Reserved Namespace:** `ov_` stem and `esc:` escape marker reserved.
Escape on publish (prepend `esc:` to raw IDs beginning with `ov_` or
`esc:`), unescape on receive (strip exactly one `esc:`). Bijection, with
the pre-amendment backward-compat residual documented as a stated
limitation.
- **Content Validation:** override entries are collected and validated
as a complete logical group *before* any decoding, zero-filling,
merging, or canonicalizing. Only two wire shapes are accepted — a
complete live three-key group, or an `ov_c:`-only tombstone floor. Any
other shape rejects the whole group while retaining the frontier entry;
applying the generic per-entry discard rule first is prohibited.
- **`d` Tag:** `<slot-id>` is exactly 32 lowercase hexadecimal
characters, replacing "a random opaque string" of 1–64 ASCII characters.
The fixed shape lets a relay recognize a read-state coordinate
structurally from the `d` tag alone, without decrypting anything, and
apply per-coordinate protections to it — under the old wording a
conforming client could pick a shape that silently forfeits them.
Recognizable coordinates are also what let a relay replace superseded
versions outright rather than accumulating one retained row per publish,
which keeps the coordinate count a full-state load must enumerate near
one per installation. Every client designates one **primary** coordinate
with a stable `<slot-id>` for the installation's lifetime. All `ov_*`
entries, and the frontier entries of the contexts they belong to, MUST
live in the primary. Additional coordinates remain legal for frontier
volume but MUST NOT carry `ov_*`, which keeps them freely rewritable and
freely deletable.
- **`t` Tag:** described as a discoverability marker rather than a
guarantee of relay-side selectivity. A relay MAY apply tag constraints
after its result cap, and `kind:30078` is shared with unrelated
application data, so clients MUST apply the tag as a correctness filter
locally, MUST NOT infer completeness from a short result, and MUST omit
the tag entirely when performing a full-state load.
- **Fetching / Full-State Load:** clients implementing the override
layer MUST NOT apply a finite `since` filter — an encrypted payload
means a relay filter cannot select for override-bearing events, so any
event-level window can exclude the only coordinate holding a tombstone
floor. Removing `since` is not sufficient: relays MAY cap historical
results, MAY cap below the requested `limit`, and emit
end-of-stored-events after the capped query, so neither EOSE nor a short
page proves completeness. No test against the client's requested `limit`
can detect truncation either: the effective cap belongs to the relay, a
relay MAY cap below what was requested, and an advertised maximum limit
is not necessarily the limit enforced.

A full-state load is therefore enumerated on `{"kinds": [30078],
"authors": [<pubkey>], "limit": <n>}` with **no tag constraint**. A
relay MAY apply tag constraints only after its result cap and withhold
the events that fail them, so under a tag-constrained filter the
delivered count is not the count the cap selected — a delivered page can
be empty while older coordinates still exist below it, and `kind:30078`
is arbitrary application data whose `d` tag namespace is open to every
application that has written under the user's key. Omitting the tag
makes delivery observable; read-state selection moves client-side, where
the validation rules already place it.

Completeness is then established by enumeration on a strictly decreasing
cursor: collect a page, descend on the lowest `created_at` across all
delivered events, exhaust that second with a window pinned to it,
continue below it, and treat only an empty delivery as complete. Every
query carries the same explicit `limit` `n` with `n >= L`. Per-second
exhaustion is discharged by comparing the pinned window's delivery
against the largest delivery the relay has already demonstrated in the
same load, floored at `L = 2` so that the ordinary single-coordinate
installation can reach *complete* at all. The comparison fails safe: an
inconclusive window reports *cannot prove complete* rather than
*complete*, and that verdict is terminal for the load.

Because these are addressable events, a coordinate republished mid-load
moves *above* the descending cursor while its previous version stops
existing, so neither is reachable by any later query. A full-state load
is therefore fenced by a live subscription on the same tag-free filter,
established — defined as receipt of end-of-stored-events — before the
first enumeration query and held unbroken on the same connection for the
load's duration. Fence deliveries are collected like enumerated events
but do not contribute to the cursor or to the demonstrated-delivery
bound. Collection deduplicates coordinates on the full NIP-01
addressable ordering — greatest `created_at`, lowest event id on ties —
because an equal-timestamp replacement is legal and is the version the
relay retains. A lapsed or reconnected fence makes the load potentially
incomplete, and a client MUST NOT publish to its own coordinates during
its own load.

Five relay behaviours the *complete* verdict rests on are stated as
normative conformance preconditions rather than assumptions, because
none is verifiable from the responses a client receives: newest-first
prefix delivery with lowest-id tie-breaking (what NIP-01 already
specifies for `limit`), a non-decreasing effective cap within a load,
the floor `L`, push delivery on an open subscription, and a delivery
barrier ordering accepted matching events ahead of a query's
end-of-stored-events on the same connection. Conditioning *complete* on
positive proof of these instead would withdraw the override layer from
every client rather than from the non-conforming relays. A client MUST
NOT load against a relay it has evidence violates them, and MUST treat
any such load as potentially incomplete.

A load that is potentially incomplete, or that failed on any relay the
client publishes to, MUST NOT authorize canonical compaction, publishing
a canonicalized override blob, deleting or abandoning a coordinate, or
reporting a mark-read as successful; the client falls back to local
state.
- **Client-ID Rotation / Orphaned Blob Deletion:** rotation is the only
event that changes an override-bearing coordinate. Before deleting or
abandoning its previous primary, a client MUST republish the
componentwise `max()` of every register that primary holds — every
tombstone ceiling included — under its new primary, and MUST confirm
acceptance on **every relay** from which the old primary will be deleted
or allowed to lapse. Acceptance on one relay does not authorize deletion
on another. Frontier-only orphans are deletable unconditionally; an
unknown same-`client_id` coordinate is treated as a live carrier until
merged.
- **Live Subscription and Convergence:** the re-publish trigger and its
suppression are evaluated on canonicalized state, so a retained live
peer blob the client has already tombstoned cannot trigger an identical
write on every replay.
- **Manual-Unread Override Layer** (new section):
- **Wire encoding:** `ov_s:<ctx>`, `ov_c:<ctx>`, `ov_b:<ctx>` as uint32
siblings in the existing `contexts` map.
- **Merge rule:** componentwise `max()` per counter — no new wire merge
logic.
- **Liveness predicate:** `S > 0 AND F <= B AND S > C`, transcribed from
`model.py::override_set_b`.
- **Actions:** mark-unread bumps S and captures the effective frontier
as B; mark-read bumps C; a natural frontier advance past B deactivates a
stale set with no counter update. Every action requires a complete
full-state load. At the uint32 ceiling, wrapping and resetting are
prohibited: mark-unread is refused, and mark-read completes only if the
resulting state has `override_active == false` — otherwise it fails
visibly rather than reporting success over a still-live override.
- **Tombstone floor:** a dead ever-active register compacts to `RegB(0,
max(S,C), 0)` — a single `ov_c:` key. A virgin register is omitted
entirely. This blocks counter reuse and the resulting resurrection.
- **Mandatory canonical publication:** a protocol requirement, not an
optimization. Publishing raw dead registers lets two independently-dead
registers from different devices produce a live join.
- **Override group co-location rule:** a context's frontier entry and
all its `ov_*` siblings MUST travel in the same event, and that event
MUST be the primary coordinate. An override-bearing context therefore
has exactly one legal destination for its whole group; only
frontier-only groups may be distributed across additional coordinates.
Grouping is per logical context, never per key.
- **Unescape-before-group rule:** the frontier wire key MUST be
unescaped to its raw logical context ID before use as group identity.
Equal normative weight to atomic grouping.
- **Tie policy:** clear-wins is MUST. The tie verdict is not encoded on
the wire, so a selectable policy makes two conforming clients diverge
permanently on both the unread verdict and the canonical wire form.
- **Override State Durability:** `ov_*` entries are exempt from age
pruning and budget eviction permanently, and durability is defined over
retrievable logical state — the containing event must stay reachable and
the load must establish completeness, not merely retain keys. There is
no safe finite GC horizon.
- **Bounds and budget:** byte/key analysis at both small-counter and
uint32-maximum values. Confining `ov_*` to one blob makes its plaintext
budget a hard lifetime ceiling on ever-overridden contexts — roughly 600
tombstones at the worst-case ~54 bytes against 32 KiB, ~730 at the
common ~45 bytes, ~199 simultaneously live overrides at ~164 bytes. At
the ceiling a client MUST refuse mark-unread and MUST NOT split override
state, drop floors, or publish a truncated override set. Same policy
shape as counter exhaustion: visible failure, never silent degradation.
- **Verification artifact:** `docs/formal/nip-rs-unread/`. The model is
a broader predecessor of this NIP: its `split_blob_into_slots` permits
override groups in any slot, so verified atomicity covers every
arrangement this NIP allows, but the converse does not follow. The model
does not verify the single-primary rule, the completeness procedure, the
relay conformance requirements or the mutation fence, or carry-forward;
malformed-group wire validation is likewise normative but outside
verified scope.

- **Abstract / Non-Goals / Backwards Compatibility:** the absolute "no
relay-side logic" and "no relay behavior changes" claims are narrowed to
what remains true — no new event kind, no new wire message, no
relay-stored read-state logic — with the override layer's relay
conformance contract named as the exception. Frontier sync and clients
that skip the override layer are unaffected on any relay.

## Verification model (`docs/formal/nip-rs-unread/`)

Four Python files constituting a bounded exhaustive verification model
for the override layer's register algebra.

**What it does:** constructs a toy universe — 2–3 devices, 2 channels,
every action that can happen (mark-unread, mark-read, late/duplicate
syncs, app reinstall, storage compaction) — and brute-forces every
reachable ordering (14,258 BFS states; 672-point deep-history parameter
cube; 9-mutant harness over ~45,000 merge pairs). After each world-state
it asks: did all devices converge? Did any unread flag get resurrected
after being cleared, or vanish while live?

**What it found and fixed:**

1. **Killed candidate A.** The model produced a concrete kill sequence:
an old client that doesn't know about the new field rewrites its
read-state blob and silently erases unread flags. That witness is why
the spec uses candidate B (two counters that only count up, plus a
snapshot) instead.
2. **Candidate B passes everything.** All delivery orders converge; the
frontier high-water mark never regresses; duplicated/replayed syncs are
harmless; old clients can't destroy it; compaction never resurrects a
dead unread or drops a live one, including
cleanup-followed-by-weeks-late-stale-sync and
tombstone-landing-on-unrelated-live-state corner cases.
3. **Caught a second real bug late.** Two devices each publishing "this
unread is cleared" could, on merge, reactivate it. The fix (canonicalize
before publishing) is a mandatory rule in the spec; the model re-checks
it across ~45,000 merge pairs.

**Scope and caveats:** bounded to 2–3 devices and 2 channels. Can't
prove the infinite case. `NOTE.md` documents the exact verification
scope and the gap between the model's `split_blob_into_slots` generality
and the single-primary rule the spec adds on top.

**Why it's in the repo:** the spec asserts "verified by bounded
exhaustive model checking." Keeping the artifact in-repo means anyone
who later amends the merge/compaction rules can `python3 exhaustive.py
&& python3 mutation.py` (deterministic, exit 0) and confirm the
guarantees hold. Without it the spec claims a proof nobody can check.

## Diff scope

`docs/nips/NIP-RS.md` — spec amendment, zero product code.

`docs/formal/nip-rs-unread/{NOTE.md,model.py,exhaustive.py,mutation.py}`
— bounded exhaustive verification model, zero product code.
`.gitignore` — `__pycache__/` and `*.pyc` entries for the model
directory.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-07-31 13:06:21 -04:00

520 lines
22 KiB
Python

"""Mutation harness for candidate B (two-counter) model.
Each mutant: subclass DeviceB with a weakened rule, run the BFS explorer,
require a recorded counterexample. A model that stays green under a real
weakening is worthless.
Mutants:
M1: drop baseline dominance (frontier > B no longer clears stale set)
M2: drop max(S,C)+1 bump (use S+1 or C+1 — counter can regress)
M3: flip tie policy (verify the model distinguishes them)
M4: revert to delete-on-dominance compaction (drops the tombstone floor
entirely instead of zeroing S and keeping max(S,C) as C) — reproduces
Thufir's pass-3 CRITICAL: stale-replay resurrection after counter reuse
M5: uint32 overflow bypass (legacy sanitization disabled)
M6: componentwise-max -> last-write-wins merge (convergence breaks)
M7: publish without canonicalization (serialize raw registers instead
of the compact-at-publish canonical form) — reproduces Thufir's
pass-1/2 CRITICAL: dead+dead merge resurrection
M8: revert split_blob_into_slots to per-entry splitting (violates the
atomic-grouping rule) — reproduces Thufir's pass-2/2 CRITICAL:
partial-slot reconstruction of a live RegB creates a false tombstone
that permanently suppresses the override after eventual full delivery
M9: revert split_blob_into_slots to escaped-key grouping (groups frontier
by wire key instead of unescaped logical ID) — reproduces Thufir's
round-2 CRITICAL: for a context whose raw ID starts with a reserved
prefix (e.g. "ov_s:evil"), the frontier's escaped wire key
("esc:ov_s:evil") and the ov_* siblings (keyed by raw suffix "ov_s:evil")
resolve to different groups → register split across slots →
old/new slot-coordinate mixture produces partial reconstruction →
false tombstone → permanent false clear across publication cycles
Each mutant is injected into the model via DeviceB subclass, then the
explorer or invariant suite is rerun. The counterexample (first violation)
is recorded and printed.
"""
from copy import deepcopy
from model import (
RegB, merge_reg_b, override_set_b, compact_b,
DeviceB, legacy_sanitize_blob,
escape_context_key,
SET, CLEAR,
)
from exhaustive import (
explore_b, test_concurrent_stability,
test_compaction_register_exhaustive, test_deep_history_compaction,
test_published_merge_closure, test_interleaved_delivery_grouping,
test_escaped_context_slot_grouping,
CONTEXTS,
)
# ---------------------------------------------------------------------------
# M1: drop baseline dominance
# ---------------------------------------------------------------------------
class M1_NoBaselineDominance(DeviceB):
def _override_set(self, reg, frontier_val, tie_policy):
if reg is None:
return False
if reg.s > reg.c:
return True
if reg.s == reg.c and reg.s > 0:
return tie_policy == SET
return False
def _compact(self, reg, frontier_val, tie_policy):
if reg.s == 0 and reg.c == 0:
return None
if self._override_set(reg, frontier_val, tie_policy):
return reg
if reg.c > reg.s:
return RegB(s=0, c=reg.c, b=0)
if reg.c == reg.s and tie_policy == CLEAR:
return RegB(s=0, c=reg.c, b=0)
return reg
def mutant_m1():
"""M1: without baseline dominance, a stale set persists after frontier
advance past baseline. Verify by constructing the scenario directly:
mark-unread at frontier=10, then advance frontier to 100. The correct
model clears the override; the mutant keeps it live."""
violations = []
for ctx in CONTEXTS:
dev = M1_NoBaselineDominance("d0")
dev.frontier[ctx] = 10
dev.do_mark_unread(ctx)
dev.do_advance_frontier(ctx, 100)
correct = override_set_b(dev.overrides[ctx], 100, CLEAR)
mutant_result = dev.override_is_set(ctx, CLEAR)
if correct != mutant_result:
violations.append((
"baseline-dominance-missing", ctx,
dev.overrides[ctx], 100,
f"correct={correct}", f"mutant={mutant_result}",
))
if not violations:
_, violations = explore_b(max_depth=3, tie_policy=CLEAR,
device_cls=M1_NoBaselineDominance)
return violations
# ---------------------------------------------------------------------------
# M2: drop max(S,C)+1 bump
# ---------------------------------------------------------------------------
class M2_NoBump(DeviceB):
"""Each counter bumps only itself: mark_unread does S := S+1,
mark_read does C := C+1. When S > C from a prior set, a clear
at C+1 can produce C < S even though the clear is causally later."""
def do_mark_unread(self, ctx):
if self.is_legacy:
return
cur = self.overrides.get(ctx, RegB())
self.overrides[ctx] = RegB(s=cur.s + 1, c=cur.c,
b=self.effective_frontier(ctx))
def do_mark_read(self, ctx, frontier_ts):
self.frontier[ctx] = max(self.frontier.get(ctx, 0), frontier_ts)
if not self.is_legacy:
cur = self.overrides.get(ctx, RegB())
self.overrides[ctx] = RegB(s=cur.s, c=cur.c + 1, b=cur.b)
def mutant_m2():
"""M2: each counter bumps independently. After set→set→clear at
the SAME frontier (no advance past baseline): correct clear has
C=3 > S=2, mutant clear has C=1 < S=2 — a causally later clear
fails to dominate.
Use mark_read at the current frontier (not advancing past baseline)
so baseline dominance doesn't mask the counter discrepancy.
"""
violations = []
for ctx in CONTEXTS:
front = 10
dev_correct = DeviceB("d0")
dev_correct.frontier[ctx] = front
dev_correct.do_mark_unread(ctx)
dev_correct.do_mark_unread(ctx)
dev_correct.do_mark_read(ctx, front)
dev_mutant = M2_NoBump("d0")
dev_mutant.frontier[ctx] = front
dev_mutant.do_mark_unread(ctx)
dev_mutant.do_mark_unread(ctx)
dev_mutant.do_mark_read(ctx, front)
correct_set = dev_correct.override_is_set(ctx, CLEAR)
mutant_set = dev_mutant.override_is_set(ctx, CLEAR)
if correct_set != mutant_set:
violations.append((
"bump-independent", ctx,
f"correct={dev_correct.overrides[ctx]}",
f"mutant={dev_mutant.overrides[ctx]}",
f"correct_set={correct_set}", f"mutant_set={mutant_set}",
))
if not violations:
_, violations = explore_b(max_depth=4, tie_policy=CLEAR,
device_cls=M2_NoBump)
return violations
# ---------------------------------------------------------------------------
# M3: tie policy distinguishable
# ---------------------------------------------------------------------------
def mutant_m3():
"""M3: tie policy is load-bearing — S==C must produce different verdicts.
Not a DeviceB mutation; tests the model function directly."""
reg = RegB(s=1, c=1, b=10)
frontier = 10
v_clear = override_set_b(reg, frontier, CLEAR)
v_set = override_set_b(reg, frontier, SET)
if v_clear == v_set:
return []
return [("tie-distinguishable", v_clear, v_set, reg, frontier)]
# ---------------------------------------------------------------------------
# M4: revert to delete-on-dominance compaction (drops the tombstone floor)
# ---------------------------------------------------------------------------
class M4_DeleteOnDominance(DeviceB):
"""The pre-fix compaction rule: any dead/dominated register is deleted
entirely rather than reduced to the tombstone floor RegB(0, max(S,C), 0).
This makes counters reusable — a later local set/clear pair restarts
from S=0/C=0, so a delayed stale peer snapshot can dominate it on
replay. This is exactly the rule Thufir's pass-3 CRITICAL found live
at e453b3945."""
def _compact(self, reg, frontier_val, tie_policy):
if reg.s == 0 and reg.c == 0:
return None
if self._override_set(reg, frontier_val, tie_policy):
return reg
if frontier_val > reg.b:
return None
if reg.c > reg.s:
return RegB(s=0, c=reg.c, b=0)
if reg.c == reg.s and tie_policy == CLEAR:
return RegB(s=0, c=reg.c, b=0)
return reg
def mutant_m4():
"""M4: without the tombstone floor, compaction deletes the counter
ceiling instead of preserving it. Reproduce Thufir's exact witness
directly: RegB(3,0,10) at frontier=20 compacts to None under the old
rule (vs. RegB(0,3,0) under the fix); a subsequent local set+clear
reuses counters from zero; the stale ancestor then replays and
resurrects (S>C) under both tie policies.
Then confirm the explorer/deep-history suite also catches it (defense
in depth — a mutant that only fails a hand-built scenario would still
be a real bug, but the directed check is what's supposed to catch this
class per T2/T3)."""
violations = []
stale = RegB(s=3, c=0, b=10)
frontier_after = 20
for tie_policy in (CLEAR, SET):
dev = M4_DeleteOnDominance("d0")
dev.frontier["c0"] = 10
dev.overrides["c0"] = stale
dev.do_advance_frontier("c0", frontier_after)
dev.do_compact("c0", tie_policy)
if "c0" in dev.overrides:
continue # old rule didn't drop it here; not the witness shape
dev.do_mark_unread("c0") # S := 1, B := 20
dev.do_mark_read("c0", frontier_after) # C := 2
stale_blob = {"contexts": {"ov_s:c0": stale.s, "ov_c:c0": stale.c, "ov_b:c0": stale.b}}
dev.receive_merge(stale_blob)
resurrected = dev.override_is_set("c0", tie_policy)
if resurrected:
violations.append((
"M4-delete-on-dominance-resurrection", tie_policy,
f"stale_ancestor={stale}", f"post_compact_reuse=(set,clear)",
f"final_reg={dev.overrides['c0']}", f"override_is_set={resurrected}",
))
if not violations:
_, violations = test_deep_history_compaction(device_cls=M4_DeleteOnDominance)
return violations
# ---------------------------------------------------------------------------
# M5: uint32 overflow bypass
# ---------------------------------------------------------------------------
def mutant_m5():
"""M5: values outside uint32 range must fail legacy sanitization."""
blob = {"v": 1, "client_id": "x", "contexts": {
"ov_s:c0": 4294967296,
"ov_c:c0": 0,
"ov_b:c0": 10,
}}
sanitized = legacy_sanitize_blob(blob)
if "ov_s:c0" in sanitized["contexts"]:
return []
return [("overflow-rejected", blob["contexts"]["ov_s:c0"],
sanitized["contexts"])]
# ---------------------------------------------------------------------------
# M6: last-write-wins merge (breaks convergence)
# ---------------------------------------------------------------------------
class M6_LastWriteWins(DeviceB):
def _merge_reg(self, a, b):
if a is None:
return b
if b is None:
return a
return b
def mutant_m6():
"""M6: replace componentwise max with last-write-wins. Convergence must
break — different delivery orders produce different final states."""
_, violations = explore_b(max_depth=3, tie_policy=CLEAR,
device_cls=M6_LastWriteWins)
return violations
# ---------------------------------------------------------------------------
# M7: publish without canonicalization (reproduces Thufir's pass-1/2
# CRITICAL — dead+dead merge resurrection)
# ---------------------------------------------------------------------------
class M7_PublishWithoutCanonicalization(DeviceB):
"""Reverts `publish_blob` to serialize raw, uncompacted registers —
the exact pre-fix behavior Thufir's pass-1/2 CRITICAL exploited:
a dead register's baseline-relative death (or clear-count-relative
death) never gets folded into a globally-comparable ceiling before
hitting the wire, so two individually-dead registers can
componentwise-max-merge into a live join."""
def publish_blob(self, tie_policy=CLEAR):
blob_ctx = {escape_context_key(k): v for k, v in self.frontier.items()}
if not self.is_legacy:
for k, reg in self.overrides.items():
blob_ctx[f"ov_s:{k}"] = reg.s
blob_ctx[f"ov_c:{k}"] = reg.c
blob_ctx[f"ov_b:{k}"] = reg.b
return {"v": 1, "client_id": self.client_id, "contexts": blob_ctx}
def mutant_m7():
"""M7: publish-without-canonicalization must be caught by the
published-state merge-closure invariant — proving that invariant
has teeth. Reproduce Thufir's exact witness directly first (fast,
deterministic); fall back to the full search if the hand-built
scenario doesn't trigger under a given tie policy."""
violations = []
for tie_policy in (CLEAR, SET):
dev_a = M7_PublishWithoutCanonicalization("a")
dev_a.frontier["c0"] = 50
dev_a.overrides["c0"] = RegB(s=3, c=2, b=0)
dev_b = M7_PublishWithoutCanonicalization("b")
dev_b.frontier["c0"] = 100
dev_b.overrides["c0"] = RegB(s=1, c=2, b=100)
blob_a = dev_a.publish_blob(tie_policy)
blob_b = dev_b.publish_blob(tie_policy)
for first, second in [(blob_a, blob_b), (blob_b, blob_a)]:
recv = M7_PublishWithoutCanonicalization("recv")
recv.receive_merge(first)
recv.receive_merge(second)
if recv.override_is_set("c0", tie_policy):
violations.append((
"M7-publish-without-canonicalization-resurrection",
tie_policy, blob_a, blob_b, recv.overrides["c0"],
))
if not violations:
_, violations = test_published_merge_closure(
device_cls=M7_PublishWithoutCanonicalization
)
return violations
# ---------------------------------------------------------------------------
# M8: revert split_blob_into_slots to per-entry splitting
# (violates the atomic-grouping rule — reproduces Thufir's pass-2/2 CRITICAL)
# ---------------------------------------------------------------------------
class M8_PerEntrySplit(DeviceB):
"""Reverts `split_blob_into_slots` to a per-entry split that violates the
atomic-grouping rule by separating `ov_s:` + frontier from `ov_b:` + `ov_c:`.
This reproduces Thufir's exact transport witness:
- Slot 0: frontier key + `ov_s:` entry (the "partial set" slot)
- Slot 1: `ov_c:` + `ov_b:` entries
An observer receiving only slot 0 reconstructs `RegB(s=1, c=0, b=0)` at
`frontier=10`. Because `frontier(10) > b(0)`, the override is baseline-dead.
Canonical re-publication emits tombstone `RegB(0, 1, 0)`. After full
eventual delivery (both original slots + transient tombstone), the merged
result is `RegB(s=1, c=1, b=10)` — dead under clear-wins — permanently
suppressing a live override.
"""
def split_blob_into_slots(self, tie_policy=CLEAR, n_slots=2):
"""Split by key type: frontier + ov_s: in slot 0, ov_b: + ov_c: in slot 1.
Violates the atomic-grouping rule by separating ov_s: from ov_b:."""
blob = self.publish_blob(tie_policy)
slots = [{"v": blob["v"], "client_id": blob["client_id"], "contexts": {}}
for _ in range(n_slots)]
for wire_key, value in blob["contexts"].items():
if wire_key.startswith("ov_b:") or wire_key.startswith("ov_c:"):
# ov_b and ov_c go to slot 1 — separated from their ov_s: sibling
slots[1]["contexts"][wire_key] = value
else:
# frontier keys and ov_s: go to slot 0
slots[0]["contexts"][wire_key] = value
return slots
def mutant_m8():
"""M8: per-entry splitting must be caught by test_interleaved_delivery_grouping —
proving that the new interleaved-delivery test has teeth.
Reproduce Thufir's exact transport witness directly: source live
`RegB(1,0,10)` at frontier=10. Per-entry split puts frontier+`ov_s:c0`
in slot 0 and `ov_c:c0`+`ov_b:c0` in slot 1. An observer receiving only
slot 0 reconstructs `RegB(1,0,0)`, re-publishes tombstone `RegB(0,1,0)`.
Full merge including the transient: `RegB(1,1,10)` → inactive.
Confirmed by running test_interleaved_delivery_grouping with M8_PerEntrySplit;
the witness must be caught before resorting to the full suite."""
return test_interleaved_delivery_grouping(device_cls=M8_PerEntrySplit)
# ---------------------------------------------------------------------------
# M9: revert split_blob_into_slots to escaped-key grouping
# (groups frontier by wire key instead of unescaped logical ID —
# reproduces Thufir's round-2 CRITICAL)
# ---------------------------------------------------------------------------
class M9_EscapedKeyGrouping(DeviceB):
"""Reverts `split_blob_into_slots` to group the frontier key by its
ESCAPED wire key rather than the unescaped logical context ID.
For a normal context like "c0", this is a no-op (escape_context_key("c0")
== "c0"), so M9 is identical to the correct model on normal contexts.
The defect only manifests when the raw context ID starts with a reserved
prefix — e.g. raw "ov_s:evil" escapes to frontier wire key "esc:ov_s:evil".
The ov_* sibling keys are keyed by the RAW suffix ("ov_s:evil"), while
the frontier is keyed by the escaped wire key ("esc:ov_s:evil") — two
identities for one logical context, so they land in different slots.
This reproduces Thufir's round-2 CRITICAL: across publication cycles an
observer can receive the new frontier slot (esc:ov_s:evil=10) plus the
stale old-cycle override slot (ov_s/ov_c/ov_b at b=0), reconstructing
RegB(s=1,c=0,b=0) at frontier=10 — baseline-dead — and emitting tombstone
RegB(0,1,0). Full eventual delivery merges to RegB(1,1,10) — dead under
clear-wins — permanently suppressing a live override.
"""
def split_blob_into_slots(self, tie_policy=CLEAR, n_slots=2):
"""Split by original (escaped) wire key identity — does not unescape
frontier keys before grouping, so escaped contexts split incorrectly."""
blob = self.publish_blob(tie_policy)
contexts = blob["contexts"]
groups = {} # wire_key -> list of (wire_key, value)
for wire_key, value in contexts.items():
if wire_key.startswith("ov_s:"):
ctx = wire_key[5:]
elif wire_key.startswith("ov_c:"):
ctx = wire_key[5:]
elif wire_key.startswith("ov_b:"):
ctx = wire_key[5:]
else:
ctx = wire_key # frontier: use escaped wire key as group ID (BUG)
groups.setdefault(ctx, []).append((wire_key, value))
slots = [{"v": blob["v"], "client_id": blob["client_id"], "contexts": {}}
for _ in range(n_slots)]
for i, (_ctx, pairs) in enumerate(sorted(groups.items())):
slot = slots[i % n_slots]
for wire_key, value in pairs:
slot["contexts"][wire_key] = value
return slots
def mutant_m9():
"""M9: escaped-key grouping must be caught by test_escaped_context_slot_grouping —
proving that the escaped-context regression test has teeth.
For a context whose raw ID starts with a reserved prefix ("ov_s:evil"),
the frontier wire key is "esc:ov_s:evil" and the ov_* sibling keys are
"ov_s:ov_s:evil", "ov_c:ov_s:evil", "ov_b:ov_s:evil". The escaped-key
grouping treats "esc:ov_s:evil" (frontier) and "ov_s:evil" (ov_* suffix)
as different groups, splitting the register across slots.
Old/new slot-coordinate mixture across publication cycles then reproduces
the round-1 transport poison: partial reconstruction → false tombstone →
permanent false clear of a live override.
The test is parameterized to route through the "mismatched grouping" path
(else branch) when the M9 split puts frontier and siblings in different slots,
and the witness must be caught."""
return test_escaped_context_slot_grouping(device_cls=M9_EscapedKeyGrouping)
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
def run_mutations():
mutants = [
("M1: drop baseline dominance", mutant_m1),
("M2: drop max(S,C)+1 bump", mutant_m2),
("M3: tie policy distinguishable", mutant_m3),
("M4: revert to delete-on-dominance compaction (reproduces pass-3 CRITICAL)", mutant_m4),
("M5: uint32 overflow bypass", mutant_m5),
("M6: last-write-wins merge", mutant_m6),
("M7: publish without canonicalization (reproduces pass-1/2 CRITICAL)", mutant_m7),
("M8: per-entry split violates atomic-grouping rule (reproduces pass-2/2 CRITICAL)", mutant_m8),
("M9: escaped-key grouping splits escaped-ctx register across slots (reproduces round-2 CRITICAL)", mutant_m9),
]
print("=" * 60)
print("Mutation harness — candidate B")
print("=" * 60)
caught = []
missed = []
for name, fn in mutants:
violations = fn()
if violations:
caught.append(name)
v = violations[0]
detail = str(v)[:200]
print(f" CAUGHT: {name}")
print(f" counterexample: {detail}")
else:
missed.append(name)
print(f" MISSED: {name}")
print(f"\nCaught {len(caught)}/{len(mutants)} mutants")
if missed:
print(f"MISSED: {missed}")
print("=" * 60)
return len(missed) == 0
if __name__ == "__main__":
import sys
sys.exit(0 if run_mutations() else 1)