Files
buzz/docs/spec/MultiTenantRelay.tla
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

1143 lines
60 KiB
Plaintext

-------------------------- MODULE MultiTenantRelay --------------------------
(***************************************************************************)
(* Formal model of Buzz's proposed multi-tenant relay/database isolation. *)
(* *)
(* This is the TLA+ half of the multi-tenant relay proof. It models N *)
(* stateless relay workers over one shared Postgres database containing a *)
(* community_id-keyed canonical message log, tenant-scoped control-plane *)
(* state, and rebuildable projections. *)
(* *)
(* The master proof obligation is NOT merely "no row with the wrong *)
(* community_id is returned." The theorem contract is non-interference *)
(* encoded as a label/taint invariant: every state element and every *)
(* observation carries the community labels that influence it; no value *)
(* labeled outside a connection's resolved community may flow into that *)
(* connection's typed observational interface. *)
(* *)
(* C1 carve-out (not modeled as a security theorem): bandwidth-limited *)
(* physical resource timing channels such as buffer cache, autovacuum, *)
(* planner stats, hot partition tails, and connection-pool latency. *)
(* *)
(* C2 channels modeled here and closed by invariants: *)
(* - event-id existence oracle: write conflict checks are scoped by *)
(* (community, id), not global id; cross-community same-id writes do *)
(* not suppress each other. A_HASH covers adversarial preimage probing. *)
(* - constraint/error surface: the relay emits only a fixed sanitized *)
(* error alphabet; sanitized error observations are relay-static and *)
(* carry no tenant label. *)
(* - projection rebuild: rebuild touches all communities internally but *)
(* emits no tenant observation; tenant reads see only own projection *)
(* rows (or a subset/none during rebuild). *)
(* *)
(* Source grounding from today's Buzz: *)
(* - migrations/0001_initial_schema.sql: events, channels, *)
(* channel_members, event_mentions, thread_metadata, reactions, *)
(* workflows, api_tokens, relay_members. *)
(* - crates/buzz-db/src/event.rs: EventQuery has channel_id/channel_ids *)
(* but no community_id; inserts use ON CONFLICT DO NOTHING. *)
(* - crates/buzz-db/src/channel.rs: get_accessible_channel_ids currently *)
(* unions all open channels in the DB; that unscoped variant is the *)
(* explicit I1 mutation. *)
(* - crates/buzz-relay/src/state.rs: process-global AppState/caches today. *)
(* *)
(* Mutation tests to keep non-vacuous: *)
(* M1: ReadScoped uses UnscopedAccessible(actor) instead of *)
(* ScopedAccessible(community, actor) -> Inv_NonInterference breaks. *)
(* M2: WriteInsert/AuthCheck use claimedCommunity/h tag instead of *)
(* ChannelCommunity(channel) -> resolution-fence invariants break. *)
(* M3: WriteDuplicate conflict on id only (GlobalConflictRows + guard *)
(* conflicts # {}), not (community,id) -> cross-community suppression *)
(* labels a B write result with A and Inv_NonInterference breaks. *)
(* Confirmed red: Safety violated at depth 3 (see GlobalConflictRows). *)
(* M4: ReadForgotPredicateWithRLS returns candidates without RLSRows -> *)
(* Inv_NonInterference breaks. *)
(* M5: Projection rebuild emits an observation or projection reads ignore *)
(* row labels -> Inv_NonInterference breaks. *)
(* M6: Error observation carries raw/high labels or a value outside the *)
(* sanitized alphabet -> Inv_SanitizedErrors/NI breaks. *)
(* M7: Direct ids lookup ignores ctx and resolves scope from the row/global *)
(* id index -> a B-scoped observation can carry an A-labeled row. *)
(* M8: WriteInsert/AuthCheck/WriteDuplicate drop the host/channel agreement *)
(* fence (accept/report when HostCommunity[host] # ChannelCommunity(ch)) ->*)
(* an A-host op (insert, authz, OR duplicate-probe) on a B-channel is *)
(* accepted/allowed/reported -> Inv_HostBindingFence breaks (an accepted *)
(* write or recorded duplicate carries a host whose mapping # its stamp). *)
(* Confirmed red: Inv_HostBindingFence violated by a 2-state trace *)
(* (Init -> WriteInsert) for the insert path and a 3-state trace *)
(* (Init -> WriteInsert -> WriteDuplicate) for the duplicate path. (Trace *)
(* length, not TLC's run-dependent "depth of complete graph search" line.) *)
(* M9: NIP-43 community admission keyed globally (admit-if-member-of-ANY- *)
(* community) instead of by (community, actor) -> Inv_AdmissionFence *)
(* breaks when a B-admitted actor joins or channel-less-reads in A. *)
(* Confirmed red: 5-state membership trace (Init -> WriteInsert -> *)
(* WriteInsert -> AdmitMember(commA, alice) -> AddMembership(commB)) and *)
(* 4-state channel-less read trace (Init -> WriteInsert -> *)
(* AdmitMember(commB, alice) -> ReadMessageRows(commA, NoChannel, hostA)). *)
(* M10: open-community AUTH admission stamps a default/claimed community *)
(* instead of HostCommunity(host) -> Inv_AdmissionFence catches an *)
(* authRegistration whose host maps elsewhere. Confirmed red: 2-state *)
(* trace (Init -> AuthenticateOpenCommunity(hostB stamps commA)). *)
(* M11: channel creation stamps a default/claimed community instead of *)
(* HostCommunity(host) -> Inv_HostBindingFence / *)
(* Inv_ChannelCommunityImmutable catches the fresh channel's bad owner. *)
(* Confirmed red: 2-state trace (Init -> CreateChannel(hostB stamps commA)).*)
(* M12: no-#h host feed admission is relay-global (GloballyAdmitted(actor)) *)
(* instead of by host community -> Inv_AdmissionFence catches a feed *)
(* read in A by an actor admitted only in B. Confirmed red: 3-state *)
(* trace (Init -> AdmitMember(commB, alice) -> ReadHostFeedRows(hostA)). *)
(* M13: no-#h #e-only aux admission is relay-global (GloballyAdmitted(actor)) *)
(* instead of by host community -> Inv_AdmissionFence catches an aux *)
(* read in A by an actor admitted only in B. Confirmed red: 3-state *)
(* trace (Init -> AdmitMember(commB, alice) -> ReadHostAuxRows(hostA)). *)
(***************************************************************************)
EXTENDS FiniteSets, Naturals, TLC
CONSTANTS
Communities, \* finite set of community ids
Channels, \* finite set of channel ids
Hosts, \* finite set of connection hostnames/URLs (the community selector)
Actors, \* finite set of pubkeys/actors
Workers, \* finite set of relay worker/process ids
MsgIds, \* finite set of event ids (model bound)
AuditVals, \* finite set of audit head values (model bound)
CommA, \* model value: first community in TLC config
CommB, \* model value: second community in TLC config
ChanA1, \* model value: community-A channel in TLC config
ChanA2, \* model value: community-A channel in TLC config
ChanB1, \* model value: community-B channel in TLC config
ChanB2, \* model value: community-B channel in TLC config
ChanFresh, \* model value: initially-unregistered channel in TLC config
HostA, \* model value: host bound to community A
HostB, \* model value: host bound to community B
HostBad, \* model value: unmapped host (resolves to NoCommunity)
NoChannel, \* model value: sentinel channel for channel-less events
NoCommunity, \* model value: sentinel for an unmapped host (fail-closed)
OpenCommunities, \* communities with no NIP-43 member pubkey allowlist
SanitizedErrors \* fixed WebSocket-reachable sanitized error alphabet
ObsKinds == {"ResultRows", "WriteResult", "SanitizedError", "AuditHead", "AuthVerdict"}
MaxObservations == 2
WriteResults == {"Inserted", "Duplicate", "None"}
AuthVerdicts == {"Allow", "Deny", "None"}
NoError == "NoError"
NoAudit == "NoAudit"
InitialChannelOwners == [ch \in Channels |->
CASE ch \in {ChanA1, ChanA2} -> CommA
[] ch \in {ChanB1, ChanB2} -> CommB
[] OTHER -> NoCommunity]
\* resolve_host (P-RESOLVE-HOST): the connection's host is authoritative for the
\* community, exactly as a relay URL is authoritative for the relay today, lifted
\* one level up to community. A host maps to exactly one community, or to the
\* NoCommunity sentinel (an unmapped/unknown host) -> the connection fails closed
\* and no channel-less write may derive a community from it. This is the upstream
\* of ResolveTenant: ctx.community is *derived* from the host, never free-chosen.
HostCommunity == [h \in Hosts |->
CASE h = HostA -> CommA
[] h = HostB -> CommB
[] OTHER -> NoCommunity]
\* Channel-or-sentinel: data rows and write/observation records may be channel-
\* less (channel = NoChannel), in which case the community comes from the host.
ChannelsExt == Channels \cup {NoChannel}
\* Every accepted write and recorded duplicate stamps its real originating host
\* (host \in Hosts): channel-less writes resolve the community from the host, and
\* channel-bearing writes/duplicates stamp the host on agreement (fail-closed on
\* disagreement, so no record carries a host that does not map to its community).
\* This lets Inv_HostBindingFence check that every record's stored community equals
\* its host's mapping -- i.e. the binding is enforced, not defaulted.
Symmetry ==
Permutations(Actors) \cup
Permutations(Workers) \cup
Permutations(MsgIds) \cup
Permutations(AuditVals)
VARIABLES
messages, \* set of canonical message rows (source="message")
projections, \* set of rebuildable projection rows (source="projection")
memberships, \* tenant-scoped active channel membership rows
admittedMembers, \* NIP-43 community member-npub allowlist rows
channelLessReads, \* successful channel-less read admissions (current capabilities)
authRegistrations, \* open-community AUTH auto-registration witnesses
feedReads, \* no-#h kinds-only feed read witnesses
auxReads, \* no-#h #e-only aux read witnesses
openChannels, \* set of open/public channel ids
auditHeads, \* function: community -> current audit head
observations, \* typed outputs visible to tenant-scoped clients
acceptedWrites, \* write requests that inserted a new message row
duplicateWrites, \* write requests that no-op'd on scoped conflict
createdChannels, \* channel-create witnesses stamped from HostCommunity[host]
queryFaults \* fail-closed query-layer faults (e.g. no TenantContext)
vars == <<messages, projections, memberships, admittedMembers,
channelLessReads, authRegistrations, feedReads, auxReads, openChannels,
auditHeads, observations, acceptedWrites, duplicateWrites, createdChannels,
queryFaults>>
ChannelCommunity(ch) ==
IF InitialChannelOwners[ch] \in Communities THEN InitialChannelOwners[ch]
ELSE IF \E created \in createdChannels : created.channel = ch
THEN (CHOOSE created \in createdChannels : created.channel = ch).community
ELSE NoCommunity
ChannelRegistered(ch) == ChannelCommunity(ch) \in Communities
\* ResolveTenant: the single resolver generalizing P-RESOLVE / L1 over both
\* channel-bearing and channel-less events. For a channel-bearing op the
\* community is the server-owned channel mapping (and an h-cross-check requires it
\* to agree with the host community, see WriteInsert). For a channel-less op
\* (channel = NoChannel) the community is the host-derived community.
ResolveTenant(host, ch) ==
IF ch = NoChannel THEN HostCommunity[host] ELSE ChannelCommunity(ch)
DataRows == [
id : MsgIds,
community : Communities,
channel : ChannelsExt,
author : Actors,
source : {"message", "projection"}
]
MessageRows == {r \in DataRows : r.source = "message"}
ProjectionRows == {r \in DataRows : r.source = "projection"}
MembershipRows == [
community : Communities,
channel : Channels,
actor : Actors
]
AdmissionRows == [
community : Communities,
actor : Actors
]
ChannelLessReadRows == [
worker : Workers,
community : Communities,
host : Hosts,
actor : Actors
]
AuthRegistrationRows == [
worker : Workers,
community : Communities,
host : Hosts,
actor : Actors
]
FeedReadRows == [
worker : Workers,
community : Communities,
host : Hosts,
actor : Actors
]
AuxReadRows == [
worker : Workers,
community : Communities,
host : Hosts,
actor : Actors,
id : MsgIds
]
CreatedChannelRows == [
worker : Workers,
community : Communities,
channel : Channels,
host : Hosts,
actor : Actors
]
AcceptedWriteRows == [
worker : Workers,
id : MsgIds,
community : Communities,
channel : ChannelsExt,
host : Hosts,
author : Actors,
claimedCommunity : Communities
]
DuplicateWriteRows == [
worker : Workers,
id : MsgIds,
community : Communities,
channel : ChannelsExt,
host : Hosts,
author : Actors,
claimedCommunity : Communities
]
Observations == [
worker : Workers,
actor : Actors,
community : Communities, \* resolved/request TenantContext community
channel : ChannelsExt, \* target channel; NoChannel for channel-less ops
kind : ObsKinds,
labels : SUBSET Communities, \* taint labels influencing this observation
rows : SUBSET DataRows, \* row/projection dependencies, if any
error : SanitizedErrors \cup {NoError},
result : WriteResults,
verdict : AuthVerdicts,
audit : AuditVals \cup {NoAudit}
]
QueryFaultRows == [
worker : Workers,
actor : Actors,
community : Communities,
reason : {"missing_tenant_context"}
]
MessageRow(id, c, ch, a) ==
[id |-> id, community |-> c, channel |-> ch, author |-> a, source |-> "message"]
ProjectionRow(m) ==
[id |-> m.id, community |-> m.community, channel |-> m.channel,
author |-> m.author, source |-> "projection"]
RowLabels(rows) == {r.community : r \in rows}
MessageKeys == {[community |-> m.community, id |-> m.id] : m \in messages}
ScopedConflictRows(c, id) == {m \in messages : m.community = c /\ m.id = id}
\* Intentionally-bad global conflict set for mutation M3 (the missing-
\* community_id-in-the-unique-index footgun, i.e. UNIQUE(id) instead of
\* UNIQUE(community_id,...,id)). To run M3: in WriteDuplicate substitute
\* conflicts == ScopedConflictRows(real, id)
\* and change the duplicate guard from key \in MessageKeys to conflicts # {}
\* (a global index fires the dup branch whenever the id exists in ANY community).
\* Confirmed red: Invariant Safety violated at depth 3, with a B-scoped
\* WriteResult observation carrying labels |-> {commA} (the C2.1 existence-oracle
\* leak). Closure is A-RLS-5 (composite index), with A_HASH as supporting axiom.
GlobalConflictRows(id) == {m \in messages : m.id = id}
DerivedProjectionRows == {ProjectionRow(m) : m \in messages}
TypeOK ==
/\ Communities # {}
/\ Channels # {}
/\ Actors # {}
/\ Workers # {}
/\ MsgIds # {}
/\ AuditVals # {}
/\ InitialChannelOwners \in [Channels -> Communities \cup {NoCommunity}]
/\ HostCommunity \in [Hosts -> Communities \cup {NoCommunity}]
/\ OpenCommunities \subseteq Communities
/\ CommA \in Communities
/\ CommB \in Communities
/\ CommA # CommB
/\ {ChanA1, ChanA2, ChanB1, ChanB2, ChanFresh} \subseteq Channels
/\ Cardinality({ChanA1, ChanA2, ChanB1, ChanB2, ChanFresh}) = 5
/\ InitialChannelOwners[ChanFresh] = NoCommunity
/\ ChanA1 # ChanB1
/\ {HostA, HostB, HostBad} \subseteq Hosts
/\ NoChannel \notin Channels
/\ NoCommunity \notin Communities
/\ HostCommunity[HostBad] = NoCommunity
/\ SanitizedErrors # {}
/\ NoError \notin SanitizedErrors
/\ NoAudit \notin AuditVals
/\ messages \subseteq MessageRows
/\ projections \subseteq ProjectionRows
/\ projections \subseteq DerivedProjectionRows
/\ memberships \subseteq MembershipRows
/\ admittedMembers \subseteq AdmissionRows
/\ channelLessReads \subseteq ChannelLessReadRows
/\ authRegistrations \subseteq AuthRegistrationRows
/\ feedReads \subseteq FeedReadRows
/\ auxReads \subseteq AuxReadRows
/\ openChannels \subseteq Channels
/\ openChannels \subseteq {ch \in Channels : ChannelRegistered(ch)}
/\ auditHeads \in [Communities -> AuditVals]
/\ observations \subseteq Observations
/\ acceptedWrites \subseteq AcceptedWriteRows
/\ duplicateWrites \subseteq DuplicateWriteRows
/\ createdChannels \subseteq CreatedChannelRows
/\ queryFaults \subseteq QueryFaultRows
\* Tenant-scoped control plane: a membership row's community agrees with
\* the server-owned channel -> community mapping. Memberships are always
\* channel-scoped (no channel-less memberships).
/\ \A m \in memberships : /\ ChannelRegistered(m.channel)
/\ m.community = ChannelCommunity(m.channel)
\* Message/projection rows are stamped with the resolved community: the
\* server-owned channel mapping for channel-bearing rows, and a real
\* (never-sentinel) community for channel-less rows (host-resolved at write).
/\ \A m \in messages :
IF m.channel = NoChannel THEN m.community \in Communities
ELSE /\ ChannelRegistered(m.channel)
/\ m.community = ChannelCommunity(m.channel)
/\ \A p \in projections :
IF p.channel = NoChannel THEN p.community \in Communities
ELSE /\ ChannelRegistered(p.channel)
/\ p.community = ChannelCommunity(p.channel)
Init ==
/\ messages = {}
/\ projections = {}
/\ memberships = {}
/\ admittedMembers = {}
/\ channelLessReads = {}
/\ authRegistrations = {}
/\ feedReads = {}
/\ auxReads = {}
/\ openChannels = {}
/\ auditHeads \in [Communities -> AuditVals]
/\ observations = {}
/\ acceptedWrites = {}
/\ duplicateWrites = {}
/\ createdChannels = {}
/\ queryFaults = {}
IsAdmitted(community, actor) ==
[community |-> community, actor |-> actor] \in admittedMembers
\* Intentionally-bad admission helper for mutation M9: treats the NIP-43 member
\* allowlist as relay-global by dropping community from the key. Substitute this
\* for IsAdmitted in AddMembership / channel-less reads to reproduce M9.
GloballyAdmitted(actor) ==
\E c \in Communities : [community |-> c, actor |-> actor] \in admittedMembers
\* Open-community AUTH mutation M10 helper: stamp every open AUTH registration
\* into CommA, ignoring the authoritative host. To reproduce M10, substitute
\* DefaultOpenAuthCommunity(host) for HostCommunity[host] when building row/reg
\* in AuthenticateOpenCommunity; HostB then records a commA registration and
\* Inv_AdmissionFence fails in a 2-state trace.
DefaultOpenAuthCommunity(host) == CommA
\* Channel-creation mutation M11 helper: stamp every fresh channel into CommA,
\* ignoring the authoritative host. To reproduce M11, substitute
\* DefaultCreatedChannelCommunity(host) for HostCommunity[host] in CreateChannel;
\* HostB then creates a commA-owned channel and the host/immutability fences fail.
DefaultCreatedChannelCommunity(host) == CommA
ScopedAccessible(community, actor) ==
{ch \in Channels :
/\ ChannelCommunity(ch) = community
/\ (ch \in openChannels \/
[community |-> community, channel |-> ch, actor |-> actor]
\in memberships)}
\* Intentionally-bad operator matching today's shared-DB landmine: open channels
\* are global, not scoped by TenantContext. The correct spec does not call this;
\* substitute it into ReadScoped/ReadProjectionRows for mutation M1.
UnscopedAccessible(actor) ==
{ch \in Channels :
ch \in openChannels \/
\E c \in Communities :
[community |-> c, channel |-> ch, actor |-> actor] \in memberships}
VisibleMessageRows(community, actor, targetChannel) ==
{m \in messages :
/\ m.community = community
/\ IF targetChannel = NoChannel
THEN /\ m.channel = NoChannel
/\ IsAdmitted(community, actor)
ELSE m.channel \in ScopedAccessible(community, actor)}
VisibleProjectionRows(community, actor, targetChannel) ==
{p \in projections :
/\ p.community = community
/\ IF targetChannel = NoChannel
THEN /\ p.channel = NoChannel
/\ IsAdmitted(community, actor)
ELSE p.channel \in ScopedAccessible(community, actor)}
VisibleDirectIdRows(community, actor, id, targetChannel) ==
{m \in messages :
/\ m.id = id
/\ m.community = community
/\ IF targetChannel = NoChannel
THEN /\ m.channel = NoChannel
/\ IsAdmitted(community, actor)
ELSE m.channel \in ScopedAccessible(community, actor)}
VisibleHostFeedRows(community, actor) ==
{m \in messages :
/\ m.community = community
/\ IsAdmitted(community, actor)
/\ (m.channel = NoChannel \/ m.channel \in ScopedAccessible(community, actor))}
\* Intentionally-bad host feed helper for mutation M12: answers from the host
\* community but treats admission as relay-global. To reproduce the red mutation,
\* change ReadHostFeedRows' admission guard from IsAdmitted(c, a) to
\* GloballyAdmitted(a) and optionally use this row helper; after an actor is
\* admitted only in B, a HostA feed read is recorded for commA and
\* Inv_AdmissionFence fails.
VisibleHostFeedRows_GlobalAdmission(community, actor) ==
{m \in messages :
/\ m.community = community
/\ GloballyAdmitted(actor)
/\ (m.channel = NoChannel \/ m.channel \in ScopedAccessible(community, actor))}
VisibleHostAuxRows(community, actor, id) ==
{m \in messages :
/\ m.id = id
/\ m.community = community
/\ IsAdmitted(community, actor)
/\ (m.channel = NoChannel \/ m.channel \in ScopedAccessible(community, actor))}
\* Intentionally-bad host aux helper for mutation M13: answers from the host
\* community but treats admission as relay-global. To reproduce the red mutation,
\* change ReadHostAuxRows' admission guard from IsAdmitted(c, a) to
\* GloballyAdmitted(a) and optionally use this row helper; after an actor is
\* admitted only in B, a HostA aux read is recorded for commA and
\* Inv_AdmissionFence fails.
VisibleHostAuxRows_GlobalAdmission(community, actor, id) ==
{m \in messages :
/\ m.id = id
/\ m.community = community
/\ GloballyAdmitted(actor)
/\ (m.channel = NoChannel \/ m.channel \in ScopedAccessible(community, actor))}
\* Intentionally-bad direct lookup mutation: answer by global id first and trust
\* the row's own community as scope. Substitute this into ReadByIdRows for M7.
UnscopedDirectIdRows(actor, id) ==
{m \in messages :
/\ m.id = id
/\ m.channel \in UnscopedAccessible(actor)}
RLSRows(community, rows) == {r \in rows : r.community = community}
\* Channel-bearing write. The community is resolved server-side from the h tag
\* (real == ChannelCommunity(ch)), never the claimed community. But the host is
\* *also* authoritative: an A-host connection presenting a B-channel event is a
\* confused deputy on the host axis (URL-is-community is the admission boundary),
\* so the op fails closed unless HostCommunity[host] = ChannelCommunity(ch).
\* Disagreement (incl. an unmapped HostBad whose mapping is NoCommunity, which can
\* never equal a real channel community) writes nothing, emits a sanitized error,
\* and records a query fault. Never acts as the channel's community.
WriteInsert(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E id \in MsgIds, ch \in Channels, host \in Hosts, a \in Actors, claimed \in Communities :
LET real == ChannelCommunity(ch)
IN
IF ~(real \in Communities) \/ HostCommunity[host] # real
THEN \* fail-closed: unknown channel or host/channel disagreement (A-host + B-channel).
LET obs == [worker |-> w, actor |-> a, community |-> claimed,
channel |-> ch, kind |-> "SanitizedError",
labels |-> {}, rows |-> {}, error |-> "restricted",
result |-> "None", verdict |-> "None", audit |-> NoAudit]
fault == [worker |-> w, actor |-> a, community |-> claimed,
reason |-> "missing_tenant_context"]
IN
/\ observations' = observations \cup {obs}
/\ queryFaults' = queryFaults \cup {fault}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
channelLessReads, authRegistrations, feedReads, auxReads,
openChannels, auditHeads, acceptedWrites, duplicateWrites,
createdChannels>>
ELSE \* host agrees with the channel mapping: accept, stamp the host.
LET key == [community |-> real, id |-> id]
row == MessageRow(id, real, ch, a)
obs == [worker |-> w, actor |-> a, community |-> real, channel |-> ch,
kind |-> "WriteResult", labels |-> {real}, rows |-> {row},
error |-> NoError, result |-> "Inserted", verdict |-> "None", audit |-> NoAudit]
wr == [worker |-> w, id |-> id, community |-> real,
channel |-> ch, host |-> host, author |-> a, claimedCommunity |-> claimed]
IN
/\ key \notin MessageKeys
/\ messages' = messages \cup {row}
/\ observations' = observations \cup {obs}
/\ acceptedWrites' = acceptedWrites \cup {wr}
/\ UNCHANGED <<projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
duplicateWrites, createdChannels, queryFaults>>
\* Channel-less write (kind:0 profiles, 1059 DMs, 30023/30174/30315/30078, lists).
\* There is no h tag to resolve, so the community is derived from the connection's
\* host (P-RESOLVE-HOST / ResolveTenant): the row is stamped channel = NoChannel,
\* community = HostCommunity[host]. The claimed community is adversary-controlled
\* and ignored -- host wins (the confused-deputy fence, lifted to the host).
\* An unmapped host (HostCommunity[host] = NoCommunity) fails closed: no row is
\* written, the connection sees only a sanitized error, and a query fault is
\* recorded. Never a default tenant.
WriteInsertGlobal(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E id \in MsgIds, host \in Hosts, a \in Actors, claimed \in Communities :
LET resolved == HostCommunity[host]
IN
IF resolved = NoCommunity \/ ~IsAdmitted(resolved, a)
THEN \* fail-closed: unmapped/unknown host or community admission miss.
LET obs == [worker |-> w, actor |-> a, community |-> claimed,
channel |-> NoChannel, kind |-> "SanitizedError",
labels |-> {}, rows |-> {}, error |-> "restricted",
result |-> "None", verdict |-> "None", audit |-> NoAudit]
fault == [worker |-> w, actor |-> a, community |-> claimed,
reason |-> "missing_tenant_context"]
IN
/\ observations' = observations \cup {obs}
/\ queryFaults' = queryFaults \cup {fault}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
channelLessReads, authRegistrations, feedReads, auxReads,
openChannels, auditHeads, acceptedWrites, duplicateWrites,
createdChannels>>
ELSE \* mapped host: stamp the host-resolved community, channel-less.
LET key == [community |-> resolved, id |-> id]
row == [id |-> id, community |-> resolved, channel |-> NoChannel,
author |-> a, source |-> "message"]
obs == [worker |-> w, actor |-> a, community |-> resolved,
channel |-> NoChannel, kind |-> "WriteResult",
labels |-> {resolved}, rows |-> {row}, error |-> NoError,
result |-> "Inserted", verdict |-> "None", audit |-> NoAudit]
wr == [worker |-> w, id |-> id, community |-> resolved,
channel |-> NoChannel, host |-> host, author |-> a, claimedCommunity |-> claimed]
IN
/\ key \notin MessageKeys
/\ messages' = messages \cup {row}
/\ observations' = observations \cup {obs}
/\ acceptedWrites' = acceptedWrites \cup {wr}
/\ UNCHANGED <<projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
duplicateWrites, createdChannels, queryFaults>>
\* Duplicate / no-op write outcome (scoped conflict on (community_id, id)). This
\* is client-observable write surface -- the "Duplicate" WriteResult exposes the
\* scoped existence/conflict rows -- so it carries the SAME host-axis obligation as
\* WriteInsert. The community is resolved server-side from the h tag (real ==
\* ChannelCommunity(ch)); the host is *also* authoritative. An A-host presenting a
\* B-channel id is a confused deputy on the host axis: it must NOT learn whether
\* that id already exists in B. So the op fails closed (sanitized error + query
\* fault, no Duplicate result, nothing recorded) unless HostCommunity[host] = real.
WriteDuplicate(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E id \in MsgIds, ch \in Channels, host \in Hosts, a \in Actors, claimed \in Communities :
LET real == ChannelCommunity(ch)
IN
IF ~(real \in Communities) \/ HostCommunity[host] # real
THEN \* fail-closed: unknown channel or host/channel disagreement (A-host + B-channel).
LET obs == [worker |-> w, actor |-> a, community |-> claimed,
channel |-> ch, kind |-> "SanitizedError",
labels |-> {}, rows |-> {}, error |-> "restricted",
result |-> "None", verdict |-> "None", audit |-> NoAudit]
fault == [worker |-> w, actor |-> a, community |-> claimed,
reason |-> "missing_tenant_context"]
IN
/\ observations' = observations \cup {obs}
/\ queryFaults' = queryFaults \cup {fault}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
channelLessReads, authRegistrations, feedReads, auxReads,
openChannels, auditHeads, acceptedWrites, duplicateWrites,
createdChannels>>
ELSE \* host agrees with the channel mapping: report the scoped duplicate.
LET key == [community |-> real, id |-> id]
conflicts == ScopedConflictRows(real, id)
obs == [worker |-> w, actor |-> a, community |-> real, channel |-> ch,
kind |-> "WriteResult", labels |-> RowLabels(conflicts), rows |-> conflicts,
error |-> NoError, result |-> "Duplicate", verdict |-> "None", audit |-> NoAudit]
wr == [worker |-> w, id |-> id, community |-> real,
channel |-> ch, host |-> host, author |-> a, claimedCommunity |-> claimed]
IN
/\ key \in MessageKeys
/\ messages' = messages
/\ observations' = observations \cup {obs}
/\ duplicateWrites' = duplicateWrites \cup {wr}
/\ UNCHANGED <<projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, createdChannels, queryFaults>>
ReadMessageRows(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in ChannelsExt, host \in Hosts :
LET rows == VisibleMessageRows(c, a, ch)
obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "ResultRows", labels |-> RowLabels(rows), rows |-> rows,
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
read == [worker |-> w, community |-> c, host |-> host, actor |-> a]
IN
/\ IF ch = NoChannel
THEN /\ HostCommunity[host] = c
/\ IsAdmitted(c, a)
/\ channelLessReads' = channelLessReads \cup {read}
ELSE channelLessReads' = channelLessReads
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
ReadProjectionRows(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in ChannelsExt, host \in Hosts :
\E rows \in SUBSET VisibleProjectionRows(c, a, ch) :
LET obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "ResultRows", labels |-> RowLabels(rows), rows |-> rows,
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
read == [worker |-> w, community |-> c, host |-> host, actor |-> a]
IN
/\ IF ch = NoChannel
THEN /\ HostCommunity[host] = c
/\ IsAdmitted(c, a)
/\ channelLessReads' = channelLessReads \cup {read}
ELSE channelLessReads' = channelLessReads
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
ReadByIdRows(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in ChannelsExt, host \in Hosts, id \in MsgIds :
LET rows == VisibleDirectIdRows(c, a, id, ch)
obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "ResultRows", labels |-> RowLabels(rows), rows |-> rows,
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
read == [worker |-> w, community |-> c, host |-> host, actor |-> a]
IN
/\ IF ch = NoChannel
THEN /\ HostCommunity[host] = c
/\ IsAdmitted(c, a)
/\ channelLessReads' = channelLessReads \cup {read}
ELSE channelLessReads' = channelLessReads
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* No-#h kinds-only feed/global read. The client sends no community metadata; the
\* relay derives the community from the connection host and fans out across only
\* channel-less rows plus accessible channels inside that same host community.
ReadHostFeedRows(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E host \in Hosts, a \in Actors :
LET c == HostCommunity[host]
IN
/\ c \in Communities
/\ IsAdmitted(c, a)
/\ [worker |-> w, community |-> c, host |-> host, actor |-> a] \notin feedReads
/\ LET rows == VisibleHostFeedRows(c, a)
obs == [worker |-> w, actor |-> a, community |-> c, channel |-> NoChannel,
kind |-> "ResultRows", labels |-> RowLabels(rows), rows |-> rows,
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
read == [worker |-> w, community |-> c, host |-> host, actor |-> a]
IN
/\ observations' = observations \cup {obs}
/\ feedReads' = feedReads \cup {read}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
channelLessReads, authRegistrations, auxReads, openChannels,
auditHeads, acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* No-#h #e-only aux lookup (reactions/edits/deletes/thread metadata shape). It
\* resolves by id only after applying the host-community and accessible-channel
\* fences, so same event id in another community is invisible.
ReadHostAuxRows(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E host \in Hosts, a \in Actors, id \in MsgIds :
LET c == HostCommunity[host]
IN
/\ c \in Communities
/\ IsAdmitted(c, a)
/\ [worker |-> w, community |-> c, host |-> host, actor |-> a, id |-> id] \notin auxReads
/\ LET rows == VisibleHostAuxRows(c, a, id)
obs == [worker |-> w, actor |-> a, community |-> c, channel |-> NoChannel,
kind |-> "ResultRows", labels |-> RowLabels(rows), rows |-> rows,
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
read == [worker |-> w, community |-> c, host |-> host, actor |-> a, id |-> id]
IN
/\ observations' = observations \cup {obs}
/\ auxReads' = auxReads \cup {read}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers,
channelLessReads, authRegistrations, feedReads, openChannels,
auditHeads, acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* Explicit community predicate was accidentally omitted, but the transaction is
\* inside TenantContext and Postgres RLS applies the community fence.
ReadForgotPredicateWithRLS(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in Channels :
LET candidates == {m \in messages : m.channel \in ScopedAccessible(c, a)}
rows == RLSRows(c, candidates)
obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "ResultRows", labels |-> RowLabels(rows), rows |-> rows,
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
IN
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* If the query does not establish TenantContext at all, RLS must fail closed.
ReadNoTenantContext(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in Channels :
LET obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "ResultRows", labels |-> {}, rows |-> {},
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> NoAudit]
fault == [worker |-> w, actor |-> a, community |-> c,
reason |-> "missing_tenant_context"]
IN
/\ observations' = observations \cup {obs}
/\ queryFaults' = queryFaults \cup {fault}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels>>
SanitizedError(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in Channels, e \in SanitizedErrors :
LET obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "SanitizedError", labels |-> {}, rows |-> {},
error |-> e, result |-> "None", verdict |-> "None", audit |-> NoAudit]
IN
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* Channel-bearing authorization. Community resolves from the channel mapping;
\* the host is also authoritative. Host/channel disagreement (A-host + B-channel)
\* is denied -- the host axis of the confused-deputy fence. Even if the actor is
\* a member of the B-channel, an A-host connection cannot drive a B-channel verdict.
AuthCheck(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E ch \in Channels, host \in Hosts, a \in Actors, claimed \in Communities :
LET real == ChannelCommunity(ch)
hostAgrees == real \in Communities /\ HostCommunity[host] = real
allowed == hostAgrees /\ ch \in ScopedAccessible(real, a)
verdict == IF allowed THEN "Allow" ELSE "Deny"
obs == [worker |-> w, actor |-> a, community |-> real, channel |-> ch,
kind |-> "AuthVerdict", labels |-> {real}, rows |-> {},
error |-> NoError, result |-> "None", verdict |-> verdict, audit |-> NoAudit]
IN
/\ real \in Communities
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
AppendAudit(w) ==
\E c \in Communities, newHead \in AuditVals :
/\ newHead # auditHeads[c]
/\ auditHeads' = [auditHeads EXCEPT ![c] = newHead]
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, observations,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
ObserveAuditHead(w) ==
/\ Cardinality(observations) < MaxObservations
/\ \E c \in Communities, a \in Actors, ch \in Channels :
LET obs == [worker |-> w, actor |-> a, community |-> c, channel |-> ch,
kind |-> "AuditHead", labels |-> {c}, rows |-> {},
error |-> NoError, result |-> "None", verdict |-> "None", audit |-> auditHeads[c]]
IN
/\ observations' = observations \cup {obs}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* Projection rebuild is privileged internal work. It may touch all communities
\* and may leave projections temporarily partial, but it emits no observation.
RebuildProjections(w) ==
\E rebuilt \in SUBSET DerivedProjectionRows :
/\ projections' = rebuilt
/\ UNCHANGED <<messages, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads, observations,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* AUTH to an allowlist-less/open community auto-registers the authenticated npub
\* in that host-derived community. Later reads/writes still check IsAdmitted; the
\* row exists because AUTH inserted it, not because the read path bypassed admission.
AuthenticateOpenCommunity(w) ==
\E host \in Hosts, a \in Actors :
LET c == HostCommunity[host]
row == [community |-> c, actor |-> a]
reg == [worker |-> w, community |-> c, host |-> host, actor |-> a]
IN
/\ c \in OpenCommunities
/\ row \notin admittedMembers
/\ admittedMembers' = admittedMembers \cup {row}
/\ authRegistrations' = authRegistrations \cup {reg}
/\ UNCHANGED <<messages, projections, memberships, channelLessReads,
feedReads, auxReads, openChannels, auditHeads, observations,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
\* Channel creation is host-bound: a fresh channel has no community until the
\* relay accepts kind:9007 on a mapped host, then stamps HostCommunity[host]
\* atomically. The client sends no community id/tag.
CreateChannel(w) ==
\E ch \in Channels, host \in Hosts, a \in Actors :
LET c == HostCommunity[host]
created == [worker |-> w, community |-> c, channel |-> ch, host |-> host, actor |-> a]
IN
/\ c \in Communities
/\ ChannelCommunity(ch) = NoCommunity
/\ ~\E old \in createdChannels : old.channel = ch
/\ createdChannels' = createdChannels \cup {created}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
observations, acceptedWrites, duplicateWrites, queryFaults>>
AdmitMember(w) ==
\E c \in Communities, a \in Actors :
LET row == [community |-> c, actor |-> a]
IN
/\ admittedMembers' = admittedMembers \cup {row}
/\ UNCHANGED <<messages, projections, memberships, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
observations, acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
RevokeMember(w) ==
\E c \in Communities, a \in Actors :
LET row == [community |-> c, actor |-> a]
IN
/\ admittedMembers' = admittedMembers \ {row}
/\ memberships' = {m \in memberships : ~(m.community = c /\ m.actor = a)}
/\ channelLessReads' = {r \in channelLessReads : ~(r.community = c /\ r.actor = a)}
/\ feedReads' = {r \in feedReads : ~(r.community = c /\ r.actor = a)}
/\ auxReads' = {r \in auxReads : ~(r.community = c /\ r.actor = a)}
/\ UNCHANGED <<messages, projections, authRegistrations,
openChannels, auditHeads, observations, acceptedWrites,
duplicateWrites, createdChannels, queryFaults>>
AddMembership(w) ==
\E ch \in Channels, a \in Actors :
LET c == ChannelCommunity(ch)
row == [community |-> c, channel |-> ch, actor |-> a]
IN
/\ c \in Communities
/\ IsAdmitted(c, a)
/\ memberships' = memberships \cup {row}
/\ UNCHANGED <<messages, projections, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
observations, acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
RemoveMembership(w) ==
\E ch \in Channels, a \in Actors :
LET c == ChannelCommunity(ch)
row == [community |-> c, channel |-> ch, actor |-> a]
IN
/\ c \in Communities
/\ memberships' = memberships \ {row}
/\ UNCHANGED <<messages, projections, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, openChannels, auditHeads,
observations, acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
OpenChannel(w) ==
\E ch \in Channels :
/\ ChannelRegistered(ch)
/\ openChannels' = openChannels \cup {ch}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, auditHeads, observations,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
CloseChannel(w) ==
\E ch \in Channels :
/\ openChannels' = openChannels \ {ch}
/\ UNCHANGED <<messages, projections, memberships, admittedMembers, channelLessReads,
authRegistrations, feedReads, auxReads, auditHeads, observations,
acceptedWrites, duplicateWrites, createdChannels, queryFaults>>
Next ==
\E w \in Workers :
\/ WriteInsert(w)
\/ WriteInsertGlobal(w)
\/ WriteDuplicate(w)
\/ ReadMessageRows(w)
\/ ReadProjectionRows(w)
\/ ReadByIdRows(w)
\/ ReadHostFeedRows(w)
\/ ReadHostAuxRows(w)
\/ ReadForgotPredicateWithRLS(w)
\/ ReadNoTenantContext(w)
\/ SanitizedError(w)
\/ AuthCheck(w)
\/ AppendAudit(w)
\/ ObserveAuditHead(w)
\/ RebuildProjections(w)
\/ AuthenticateOpenCommunity(w)
\/ CreateChannel(w)
\/ AdmitMember(w)
\/ RevokeMember(w)
\/ AddMembership(w)
\/ RemoveMembership(w)
\/ OpenChannel(w)
\/ CloseChannel(w)
BoundedObservations == Cardinality(observations) <= MaxObservations
BoundedWitnesses ==
/\ Cardinality(messages) <= 1
/\ Cardinality(projections) <= 1
/\ Cardinality(memberships) <= 1
/\ Cardinality(admittedMembers) <= 2
/\ Cardinality(channelLessReads) <= 1
/\ Cardinality(authRegistrations) <= 1
/\ Cardinality(feedReads) <= 1
/\ Cardinality(auxReads) <= 1
/\ Cardinality(openChannels) <= 1
/\ Cardinality(acceptedWrites) <= 1
/\ Cardinality(duplicateWrites) <= 1
/\ Cardinality(createdChannels) <= 1
/\ Cardinality(queryFaults) <= 1
Spec == Init /\ [][Next]_vars
------------------------------------------------------------------------------
\* SAFETY PROPERTIES
\* NI (master): no observation scoped to community C may be influenced by a row,
\* projection, audit head, auth decision, write-conflict source, or error source
\* labeled outside C. This is the single-run label/taint encoding of the
\* two-execution non-interference theorem.
Inv_NonInterference ==
\A o \in observations : o.labels \subseteq {o.community}
\* Label propagation: observation labels are not arbitrary annotations; they are
\* derived from the dependencies each observation can reveal.
Inv_LabelPropagation ==
\A o \in observations :
/\ (o.kind \in {"ResultRows", "WriteResult"} => o.labels = RowLabels(o.rows))
/\ (o.kind = "SanitizedError" => o.labels = {} /\ o.error \in SanitizedErrors)
/\ (o.kind = "AuditHead" => o.labels = {o.community} /\ o.audit \in AuditVals)
/\ (o.kind = "AuthVerdict" => o.labels = {ChannelCommunity(o.channel)} /\ o.community = ChannelCommunity(o.channel))
\* I1 read confinement follows from NI + label propagation, but is kept as a
\* legible mutation target for the current get_accessible_channel_ids landmine.
Inv_ReadConfinement ==
\A o \in observations :
o.kind = "ResultRows" => \A r \in o.rows : r.community = o.community
\* I2 resolution fence: persisted messages and write/auth observations are
\* labeled by the *resolved* community, never a client-supplied claim. For a
\* channel-bearing op the resolver is the server-owned channel->community mapping
\* (the h tag is not the source). For a channel-less op (channel = NoChannel) the
\* resolver is the connection's host (P-RESOLVE-HOST): the stamped community is a
\* real community derived from the host, never the NoCommunity sentinel and never
\* the adversary-controlled claimedCommunity. At N=1 (one host -> one community)
\* the two branches coincide.
Inv_ResolutionFence ==
/\ \A m \in messages :
IF m.channel = NoChannel THEN m.community \in Communities
ELSE /\ ChannelRegistered(m.channel)
/\ m.community = ChannelCommunity(m.channel)
/\ \A w \in acceptedWrites :
IF w.channel = NoChannel THEN w.community \in Communities
ELSE w.community = ChannelCommunity(w.channel)
/\ \A w \in duplicateWrites :
IF w.channel = NoChannel THEN w.community \in Communities
ELSE w.community = ChannelCommunity(w.channel)
/\ \A o \in observations :
o.kind \in {"WriteResult", "AuthVerdict"} =>
(IF o.channel = NoChannel THEN o.community \in Communities
ELSE o.community = ChannelCommunity(o.channel))
\* I2-host fail-closed binding: EVERY accepted write -- channel-bearing or
\* channel-less -- AND every observable duplicate/no-op outcome carries the host it
\* came in on, and that host's mapping is a real community equal to the write's
\* stored community (P-RESOLVE-HOST). For channel-less writes this is the
\* host->community derivation; for channel-bearing writes and duplicate/no-op
\* outcomes it is the host/channel agreement fence (HostCommunity[host] =
\* ChannelCommunity(ch)). This makes BOTH "an unmapped host defaults to a tenant"
\* and "an A-host can drive a B-channel op (insert OR duplicate-probe)" CAUGHT
\* mutations: the fail-closed branches of WriteInsert/WriteInsertGlobal/WriteDuplicate
\* write/record nothing on disagreement, so no accepted write and no recorded
\* duplicate can carry a host whose mapping is NoCommunity or disagrees with the stamp.
Inv_HostBindingFence ==
/\ \A w \in acceptedWrites :
/\ w.host \in Hosts
/\ HostCommunity[w.host] \in Communities
/\ w.community = HostCommunity[w.host]
/\ \A w \in duplicateWrites :
/\ w.host \in Hosts
/\ HostCommunity[w.host] \in Communities
/\ w.community = HostCommunity[w.host]
/\ \A ch \in createdChannels :
/\ ch.host \in Hosts
/\ HostCommunity[ch.host] \in Communities
/\ ch.community = HostCommunity[ch.host]
/\ ChannelCommunity(ch.channel) = ch.community
\* Channel-community immutability: a runtime-created channel may be stamped at
\* most once, and all durable ownership rows for a channel agree on the same
\* host-resolved community. This makes the old constant-function immutability
\* guarantee explicit now that fresh channel creation is modeled as state.
Inv_ChannelCommunityImmutable ==
/\ \A c1, c2 \in createdChannels :
c1.channel = c2.channel => c1.community = c2.community
/\ \A created \in createdChannels :
/\ InitialChannelOwners[created.channel] = NoCommunity
/\ created.community = HostCommunity[created.host]
\* I2-admission fence: NIP-43 member-npub admission is scoped by community, not
\* relay-global pubkey. Active channel memberships and current channel-less read
\* capabilities may only exist when the actor is admitted in that same community;
\* channel-less read capabilities also carry the host that resolved that community.
\* This records the observable admission capability, not historical message rows:
\* revoking admission removes current memberships/reads but does not relabel rows
\* already written while admitted.
Inv_AdmissionFence ==
/\ \A m \in memberships : IsAdmitted(m.community, m.actor)
/\ \A r \in channelLessReads :
/\ IsAdmitted(r.community, r.actor)
/\ HostCommunity[r.host] = r.community
/\ \A r \in authRegistrations :
/\ r.community \in OpenCommunities
/\ HostCommunity[r.host] = r.community
/\ \A r \in feedReads :
/\ IsAdmitted(r.community, r.actor)
/\ HostCommunity[r.host] = r.community
/\ \A r \in auxReads :
/\ IsAdmitted(r.community, r.actor)
/\ HostCommunity[r.host] = r.community
\* TLA-side anti-vacuity / reachability witnesses for surfaces that could
\* otherwise pass only because their actions never fire. These are intentionally
\* false invariants: checking one ad hoc with INVARIANT (plus the normal
\* constraints) must produce a short counterexample trace. If it stays green, the
\* corresponding action is unreachable and the safety proof is vacuous there.
Probe_OpenAuthRegistration_Unreachable ==
authRegistrations = {}
Probe_CreatedChannel_Unreachable ==
createdChannels = {}
Probe_HostFeedRead_Unreachable ==
feedReads = {}
Probe_HostAuxRead_Unreachable ==
auxReads = {}
\* I3a append persistence: every accepted append remains present in the shared log.
Inv_AcceptedWritesPersist ==
\A w \in acceptedWrites : MessageRow(w.id, w.community, w.channel, w.author) \in messages
\* I3b scoped idempotence: ids are unique within a community, not globally. This
\* permits two different communities to store the same content hash while avoiding
\* the event-id existence oracle as a cross-tenant write-conflict channel.
Inv_MessageKeyUnique ==
\A m1, m2 \in messages :
(m1.community = m2.community /\ m1.id = m2.id) => (m1 = m2)
\* I4 fail-closed backstop: missing TenantContext serves no rows. Dropped SQL
\* predicates inside a valid TenantContext are covered by RLSRows and NI.
Inv_NoTenantContextFailsClosed ==
\A o \in observations :
(o.kind = "ResultRows" /\ o.labels = {}) => o.rows = {}
\* Projection rows are derived-only and inherit the source event label.
Inv_ProjectionDerived == projections \subseteq DerivedProjectionRows
\* Sanitized error alphabet is the only client-visible error surface in scope.
Inv_SanitizedErrors ==
\A o \in observations :
o.kind = "SanitizedError" => o.error \in SanitizedErrors
Safety ==
/\ TypeOK
/\ Inv_NonInterference
/\ Inv_LabelPropagation
/\ Inv_ReadConfinement
/\ Inv_ResolutionFence
/\ Inv_HostBindingFence
/\ Inv_ChannelCommunityImmutable
/\ Inv_AdmissionFence
/\ Inv_AcceptedWritesPersist
/\ Inv_MessageKeyUnique
/\ Inv_NoTenantContextFailsClosed
/\ Inv_ProjectionDerived
/\ Inv_SanitizedErrors
=============================================================================