mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
fix(nip-rs): close round-4 findings — projection-sole-authority, torn-register witness
Remove fetchedTombstoneChannels: the observer cannot adjudicate cross-source ordering without coordinate/version provenance. The complete manager projection is the SOLE override-liveness authority. Any transient false-positive (live projection register vs stale fetched tombstone) resolves when the manager ingests the tombstone via its own CRDT path. The inverse false-negative (hiding the user's fresh mark-unread until relay publish catches up) is the more harmful failure; the categorical tombstone-wins rule produced it. Rewrite adversarial-5: same inputs (complete live projection S=5,C=0,B=100,F=50 + fetched tombstone ov_c=4, no ov_s), expected outcome inverts to hasUnread:true. The test now proves the correct policy: fetched tombstone does not suppress a live projection register when the projection is the sole authority. Add adversarial-6 (torn-register rail witness): fetched event with ov_s+ov_c present and ov_b absent (illegal S+C partial group) + no message events. The parser drops the group; no override register reaches liveness evaluation; rail stays dark. Locks the parser → structured merge → rail integration path. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
parent
0edaad3d8c
commit
4e65142a0e
@@ -1061,17 +1061,19 @@ test("fetchCommunityUnread adversarial-2: projection with frontier deactivates r
|
||||
assert.deepEqual(result, { hasUnread: false, mentionCount: 0 });
|
||||
});
|
||||
|
||||
test("fetchCommunityUnread adversarial-5: stale_complete_projection_yieldsTo_fresher_fetched_tombstone", async () => {
|
||||
// CRITICAL 2 ordering fix: `loadComplete` establishes enumeration completeness,
|
||||
// NOT temporal ordering. A complete projection with a live register
|
||||
// (S=5,C=0,B=100,F=50) must yield to a fresher fetched tombstone (S=0,C=4,B=0)
|
||||
// for the same channel. Without the fetchedTombstoneChannels guard, the projection
|
||||
// overrides authoritative map (S=5,C=0,B=100) would produce hasUnread:true;
|
||||
// with the guard, the fetched tombstone wins and the rail stays dark.
|
||||
test("fetchCommunityUnread adversarial-5: complete_projection_live_register_not_darkened_by_stale_fetched_tombstone", async () => {
|
||||
// A fresh local mark-unread committed into the projection (S=5,C=0,B=100,F=50)
|
||||
// must NOT be darkened by an independently fetched tombstone (S=0,C=4,B=0).
|
||||
//
|
||||
// Wire: fetched event has ov_c:CHANNEL_ID=4 but no ov_s: → decoded as {s:0,c:4,b:0}.
|
||||
// fetchedTombstoneChannels includes CHANNEL_ID (s===0). Override liveness check is
|
||||
// skipped for that channel even though authoritative (projection) has s=5.
|
||||
// The observer cannot adjudicate cross-source ordering: the manager owns
|
||||
// coordinate dedupe and CRDT ingest. A categorical tombstone-wins rule would
|
||||
// hide the user's mark-unread until the relay publish catches up (the
|
||||
// false-negative direction), which is the more harmful failure.
|
||||
//
|
||||
// When the projection is complete, projection.overrides is the SOLE liveness
|
||||
// authority. The fetched register shape is irrelevant. Any transient
|
||||
// false-positive resolves when the manager ingests the tombstone via its own
|
||||
// CRDT path.
|
||||
const SLOT = "a".repeat(32); // valid 32-hex slot
|
||||
const relay = relayFor([
|
||||
// 1. member events
|
||||
@@ -1096,7 +1098,7 @@ test("fetchCommunityUnread adversarial-5: stale_complete_projection_yieldsTo_fre
|
||||
() => [],
|
||||
// 4. read-state: tombstone wire format (ov_c present, ov_s absent → s=0,c=4,b=0)
|
||||
// Frontier F=50 is also present so the projection frontier is not strictly
|
||||
// newer — only the tombstone vs live-register ordering is under test.
|
||||
// newer — only the live-register vs concurrent-tombstone ordering is under test.
|
||||
() => [
|
||||
event({
|
||||
pubkey: PUBKEY,
|
||||
@@ -1118,14 +1120,14 @@ test("fetchCommunityUnread adversarial-5: stale_complete_projection_yieldsTo_fre
|
||||
],
|
||||
// 5. mutes
|
||||
() => [],
|
||||
// 6. unread events — none
|
||||
// 6. unread events — none (liveness from override register, not messages)
|
||||
() => [],
|
||||
// 7. mention events — none
|
||||
() => [],
|
||||
]);
|
||||
|
||||
// Complete projection: live register S=5,C=0,B=100 and frontier F=50.
|
||||
// F=50 ≤ B=100, S=5 > C=0 → isOverrideActive would return true WITHOUT the guard.
|
||||
// F=50 ≤ B=100, S=5 > C=0 → isOverrideActive returns true.
|
||||
const overrides = new Map([[CHANNEL_ID, { s: 5, c: 0, b: 100 }]]);
|
||||
const frontiers = new Map([[CHANNEL_ID, 50]]);
|
||||
|
||||
@@ -1139,7 +1141,85 @@ test("fetchCommunityUnread adversarial-5: stale_complete_projection_yieldsTo_fre
|
||||
getProjection: () => ({ loadComplete: true, frontiers, overrides }),
|
||||
});
|
||||
|
||||
// Fetched tombstone (s=0) is authoritative; projection's stale live register must not
|
||||
// resurrect unread when the relay has confirmed a clear.
|
||||
// Projection is the sole override authority when complete. The fetched
|
||||
// tombstone does not suppress the live projection register — the rail lights.
|
||||
assert.deepEqual(result, { hasUnread: true, mentionCount: 0 });
|
||||
});
|
||||
|
||||
test("fetchCommunityUnread adversarial-6: torn fetched register (ov_s+ov_c without ov_b) → rail stays dark", async () => {
|
||||
// Integration witness: a fetched read-state event carrying an illegal partial
|
||||
// override group — ov_s and ov_c present, ov_b ABSENT — must not light the
|
||||
// rail. The parser drops incomplete groups (any shape other than all-three or
|
||||
// C-only tombstone), so the register never reaches liveness evaluation.
|
||||
//
|
||||
// This locks the parser → structured merge → rail path at the observer level.
|
||||
// The readStateOverride parser-matrix tests confirm the drop at the unit level;
|
||||
// this witness confirms the integration: a torn fetched group cannot influence
|
||||
// fetchCommunityUnread() rail output.
|
||||
const SLOT = "b".repeat(32); // valid 32-hex slot, distinct from other tests
|
||||
const relay = relayFor([
|
||||
// 1. member events
|
||||
() => [
|
||||
event({
|
||||
tags: [
|
||||
["d", CHANNEL_ID],
|
||||
["p", PUBKEY],
|
||||
],
|
||||
}),
|
||||
],
|
||||
// 2. metadata events
|
||||
() => [
|
||||
event({
|
||||
tags: [
|
||||
["d", CHANNEL_ID],
|
||||
["t", "stream"],
|
||||
],
|
||||
}),
|
||||
],
|
||||
// 3. visibility events
|
||||
() => [],
|
||||
// 4. read-state: torn group — ov_s + ov_c present, ov_b absent (S+C shape).
|
||||
// The parser treats any shape other than {S,C,B} or {C-only tombstone}
|
||||
// as invalid and drops the entire group. No override register reaches
|
||||
// the authoritative map. No message events follow either.
|
||||
() => [
|
||||
event({
|
||||
pubkey: PUBKEY,
|
||||
created_at: 300,
|
||||
tags: [
|
||||
["d", `read-state:${SLOT}`],
|
||||
["t", "read-state"],
|
||||
],
|
||||
content: JSON.stringify({
|
||||
v: 1,
|
||||
client_id: "client",
|
||||
contexts: {
|
||||
[`ov_s:${CHANNEL_ID}`]: 5,
|
||||
[`ov_c:${CHANNEL_ID}`]: 0,
|
||||
// ov_b: intentionally absent → illegal S+C partial group, dropped
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
// 5. mutes
|
||||
() => [],
|
||||
// 6. unread events — none
|
||||
() => [],
|
||||
// 7. mention events — none
|
||||
() => [],
|
||||
]);
|
||||
|
||||
const result = await fetchCommunityUnread({
|
||||
client: relay,
|
||||
pubkey: PUBKEY,
|
||||
nowSeconds: 400,
|
||||
decryptReadState: async (v) => v,
|
||||
decryptMutes: async (v) => v,
|
||||
readThreadRelationships: readRelationships(),
|
||||
// No getProjection — fetched-deduped is sole authority, torn group is dropped.
|
||||
});
|
||||
|
||||
// Torn group is silently dropped by the parser; no override register present;
|
||||
// no message events — rail must remain dark.
|
||||
assert.deepEqual(result, { hasUnread: false, mentionCount: 0 });
|
||||
});
|
||||
|
||||
@@ -221,21 +221,18 @@ export async function fetchCommunityUnread(args: {
|
||||
const projection = args.getProjection?.() ?? null;
|
||||
const completeProjection = projection?.loadComplete ? projection : null;
|
||||
|
||||
// Override liveness resolution: projection wins when complete, BUT a fetched
|
||||
// wire tombstone (s===0, c>0) for the same coordinate is treated as
|
||||
// authoritative — it proves the relay consensus has a clear with no
|
||||
// countering mark-unread, and the projection's live S counter may be from
|
||||
// an uncommitted/unpublished local action that the relay hasn't confirmed.
|
||||
// The observer must not adjudicate cross-source ordering it cannot see.
|
||||
// The manager owns event-level coordinate dedupe and CRDT ingest; a fetched
|
||||
// relay snapshot carries no coordinate/version provenance that would let the
|
||||
// UI compare it against a locally-committed projection register.
|
||||
//
|
||||
// fetchedTombstoneChannels is the set of channel IDs whose fetched register
|
||||
// has s===0 (tombstone wire format). We skip projection liveness for those.
|
||||
const fetchedTombstoneChannels = new Set<string>();
|
||||
if (completeProjection !== null) {
|
||||
for (const [ctx, reg] of readState.overrides) {
|
||||
if (reg.s === 0) fetchedTombstoneChannels.add(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Therefore: when a complete projection is available, projection.overrides is
|
||||
// the SOLE override-liveness authority — no exceptions for fetched tombstone
|
||||
// shapes. A transient false-positive (e.g. projection live register vs stale
|
||||
// fetched tombstone) resolves when the manager ingests the tombstone through
|
||||
// its own CRDT path. A categorical tombstone-wins rule would produce the
|
||||
// inverse false-negative: hiding a user's fresh local mark-unread until the
|
||||
// relay publish catches up, which is the more harmful failure.
|
||||
const authoritative: ReadonlyMap<string, OverrideRegister> =
|
||||
completeProjection !== null
|
||||
? completeProjection.overrides
|
||||
@@ -295,15 +292,9 @@ export async function fetchCommunityUnread(args: {
|
||||
// Override liveness: evaluate the authoritative register against the
|
||||
// effective frontier. Uses projection.overrides when complete (no
|
||||
// per-field max with fetched); fetched-deduped otherwise.
|
||||
// Skip projection liveness when fetched relay has a wire tombstone (s=0)
|
||||
// for this channel — a fresher relay tombstone is authoritative.
|
||||
if (!hasUnread) {
|
||||
const reg = authoritative.get(channel.id);
|
||||
if (
|
||||
reg !== undefined &&
|
||||
!fetchedTombstoneChannels.has(channel.id) &&
|
||||
isOverrideActive(reg, readAt ?? 0)
|
||||
) {
|
||||
if (reg !== undefined && isOverrideActive(reg, readAt ?? 0)) {
|
||||
hasUnread = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user