mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## 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>
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)
|