mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-15 15:30:36 +00:00
docs: channel multi-owner plans: per-trigger re-send bits, capped hold ticks, stored revocation entries, accurate forged-leave disclosure
This commit is contained in:
@@ -33,7 +33,7 @@ Link data and keys already *have* a sequencer — the server — so they need on
|
||||
- **Relays:** the published set is authoritative, except this owner's own in-progress relay changes and the relays it knows are gone. A relay it is adding and has not published yet lives in its relay row; a relay it removed, or that left, lives in its relay member row with status removed or left — in channels a relay's removal keeps that row, as a relay's own leave already does (`updateMemberRecordDeleted`, `Internal.hs:2218-2230`), instead of deleting it together with its relay row before the write (`deleteOrUpdateMemberRecordIO` and `fullyDeleteMemberRecordIO`, `Internal.hs:2192-2216`; `group_relays` cascades, `chat_schema.sql:839`), which would leave nothing to subtract — and adding a relay checks for duplicates only among current relays (`APIAddGroupRelays`, `Commands.hs:2782-2789`), so a removed relay can be added again. Each owner caches the last published relay list it knows — a promoted owner reads the link data to fill it before its first write — and a write's relay list is that cache plus its own in-progress additions minus every relay whose member row on this device is removed or left; a write with an empty relay list is refused unless the owner is removing the last relay — today the written list comes only from the writer's own `group_relays` rows (`Store/Groups.hs:1528-1545`), which reference its own chat relays (`chat_schema.sql:840`), so a promoted owner would publish no relays at all. A relay another owner removed is therefore not resurrected by this owner's next write (a write on a stale cache is rejected by the CAS and rebuilt from the returned blob), and a relay another owner added is not dropped.
|
||||
- **Profile and preferences** (whole values): the published value is adopted. An owner's own profile edit is synchronous and write-then-commit: it keeps the profile it started from in memory, writes, and commits locally only on an accepted `LSETH`. On a rejection it merges field by field against the profile it started from — a field only it changed keeps its value, a field only the other owner changed takes theirs, and a field both changed differently is a conflict, surfaced ("profile edit superseded") with the published value kept — then retries. Today `runUpdateGroupProfile` commits locally (`Commands.hs:4172-4174`) before the link write (`:4190`); the order is reversed.
|
||||
|
||||
Because local state thus stays "published + my in-progress changes" (an owner I am adding, relays I am adding or removing), `groupLinkData` builds the write from local state — no separate delta step. Reconcile runs in four places, all the same code: **on channel open** (owners run `APIGetUpdatedGroupLinkData` like subscribers — the owner exclusion is removed, in core at `Commands.hs:1968` and in the apps — with an owner relay step in place of `syncSubscriberRelays`, which would delete this owner's not-yet-published relays and join other owners' relays as a subscriber; the owner step deactivates relays absent from the published data that this owner is not adding, and connects to relays other owners added by joining through the relay's link as itself — which needs a relay change: today a relay accepts a join onto an existing member only for roster roles (`memberJoinRequestViaRelay`, `Subscriber.hs:1696-1706`), so an owner falls through to a fresh member insert; the relay instead accepts a join from a current, non-revoked owner that is signed with that owner's key, with the same `viaRelay` replay check, onto the existing owner row with role owner; on the owner's side, completing that join updates only the relay member — the subscriber path it reuses would overwrite the owner's group profile with the relay's copy and, for the creator, its membership role and status (`updatePreparedUserAndHostMembers'`, `Store/Groups.hs:821-844`), and its connection handler would mark the owner's own membership connected and create the joined-channel items (`Subscriber.hs:892-922`)), **when the owner's chat starts** (so owners that never open the channel view — CLI, bots — also heal revocations and publish the count), **on a write rejection** (adopt the returned blob, then rebuild), and as the missed-event backstop. The `owners` list keeps **insertion (signer) order** — a new entry is appended after the entry that signed it, so `validateLinkOwners` ("signed by root or an earlier entry") holds as-is, with no ordering change; the union merge dedups by memberId. (The key-set hash in §3 *is* recomputed by every owner and so genuinely needs a canonical sort; the owners blob is hashed as ciphertext and never recomputed, so it does not.) `revokedOwners` is a list in the mutable data, append-only (the remover writes it on removal, the leaver on leave, any owner corrects an inconsistency it sees on reconcile); each entry is a `memberId` with the roster version its writer held and whether it is a leave, which bound a leaver's in-flight roster changes (§4) — both asserted by the entry's writer, so a reader caps the version (§4) — and the merge keeps the first published entry for a `memberId`. The count comes from relays (§5).
|
||||
Because local state thus stays "published + my in-progress changes" (an owner I am adding, relays I am adding or removing), `groupLinkData` builds the write from local state — no separate delta step. Reconcile runs in four places, all the same code: **on channel open** (owners run `APIGetUpdatedGroupLinkData` like subscribers — the owner exclusion is removed, in core at `Commands.hs:1968` and in the apps — with an owner relay step in place of `syncSubscriberRelays`, which would delete this owner's not-yet-published relays and join other owners' relays as a subscriber; the owner step deactivates relays absent from the published data that this owner is not adding, and connects to relays other owners added by joining through the relay's link as itself — which needs a relay change: today a relay accepts a join onto an existing member only for roster roles (`memberJoinRequestViaRelay`, `Subscriber.hs:1696-1706`), so an owner falls through to a fresh member insert; the relay instead accepts a join from a current, non-revoked owner that is signed with that owner's key, with the same `viaRelay` replay check, onto the existing owner row with role owner; on the owner's side, completing that join updates only the relay member — the subscriber path it reuses would overwrite the owner's group profile with the relay's copy and, for the creator, its membership role and status (`updatePreparedUserAndHostMembers'`, `Store/Groups.hs:821-844`), and its connection handler would mark the owner's own membership connected and create the joined-channel items (`Subscriber.hs:892-922`)), **when the owner's chat starts** (so owners that never open the channel view — CLI, bots — also heal revocations and publish the count), **on a write rejection** (adopt the returned blob, then rebuild), and as the missed-event backstop. The `owners` list keeps **insertion (signer) order** — a new entry is appended after the entry that signed it, so `validateLinkOwners` ("signed by root or an earlier entry") holds as-is, with no ordering change; the union merge dedups by memberId. (The key-set hash in §3 *is* recomputed by every owner and so genuinely needs a canonical sort; the owners blob is hashed as ciphertext and never recomputed, so it does not.) `revokedOwners` is a list in the mutable data, append-only (the remover writes it on removal, the leaver on leave, any owner corrects an inconsistency it sees on reconcile); each entry is a `memberId` with the roster version its writer held and whether it is a leave, which bound a leaver's in-flight roster changes (§4) — both asserted by the entry's writer, so a reader caps the version (§4) — and the merge keeps the first entry this device saw for a `memberId`. The count comes from relays (§5).
|
||||
|
||||
**Relay activation keys off the agent's `LINK` event** — or, for a promoted owner's foreign write, which produces none, off its `OK` (§9) — and `LINK` fires only after the server accepted the write (`Agent.hs:2255-2258`); under CAS it fires after the accepted `LSETH` and carries the data actually written, after any re-merge. A relay that another owner removed never produces a `LINK` on this device (the TODO at `Subscriber.hs:1475-1477`); reconcile deactivates it.
|
||||
|
||||
@@ -130,7 +130,7 @@ Add the target's `memberId` to **`revokedOwners`** in the mutable link data (`LS
|
||||
- **Promotion is only as safe as the verified key.** An owner's copy of a subscriber's key is relay-asserted (from unsigned `XGrpMemNew`), so a relay that substituted it could otherwise be promoted. The defence is out-of-band verification, which is **already implemented** for channels (`verifyChannelMemberCode`, `Commands.hs:3826`, called at `:2074`, hashes both members' keys, sorted, so comparing detects substitution). It is **advisory**: the promoter is warned in the UI to verify before promoting (matching the existing channel model), not hard-blocked in the backend. Residual risk: an owner who ignores the warning and promotes an unverified member could sign a relay-substituted key into the chain, making that relay an owner. With one key per owner, the key that gains link-queue authority is exactly the member key the promoter was asked to verify; the backend also enforces the consistency check (§6) that the accepted key equals the one O1 already holds.
|
||||
- **Members take an owner's key only from a validated chain entry** — from the link data at join, or from `x.grp.owner.add` (§6) — never from a relay-asserted intro.
|
||||
- **Any owner can destroy the channel** (`Server.hs:1289-1290`: any recipient key authorises `DEL`/`LDEL`) and can RKEY the key set down to itself, evicting the others. Both accepted under any-owner-decides; recorded because they exceed any chat-level action.
|
||||
- **A removed owner can forge a leave while its key is live.** Between its remover's revocation and key drop — indefinitely if the remover stops there for good — it can rewrite its own `revokedOwners` entry as a leave, and with a colluding relay have changes to non-owner members (roles, and removals with their messages) held by an owner that reads that entry first and re-sent under that owner's signature when the hold ends (§4). A holder cannot tell this from a real leave, both being written with the leaver's key; the hold's version cap keeps it from raising any gate, its period bounds the window, and the remover's announcement ends it. Accepted as a residual of an interrupted removal.
|
||||
- **A removed owner can forge a leave while its key is live.** Between its remover's revocation and the key drop — by the remover, or by the first other owner's reconcile (§3) — it can rewrite its own `revokedOwners` entry as a leave, and with a colluding relay have changes to non-owner members (roles, and removals with their messages) held by an owner that reads that entry first and re-sent under that owner's signature when the hold ends (§4). A holder cannot tell this from a real leave, both being written with the leaver's key, and a remover that never announces leaves the forged entry published, so each owner that reads it first holds in turn. The hold's version cap keeps any gate it raises within the departed-owner margin (§4), each holder's hold period bounds its window, and the remover's announcement ends it. Accepted as a residual of an interrupted removal.
|
||||
- **Link writes are attributable to owners, and a promoted owner's network address is visible to the link server.** Any link holder can tell which owner signed each version of the link data (§3). Recipient commands are not proxied — the proxy forwards only `SEND`, `SKEY`, `LKEY`, `LGET` and `RSLV` (`Server.hs:2205-2211`) — so a promoted owner's device connects directly to the link server the creator chose, and that server's operator, holding the public link, can tie the owner's network address to its channel identity. The invitee's confirmation says so (§10), and a commit makes M an owner on its own device only after M accepted (§6) — otherwise any owner could make a subscriber's device connect directly to the link server; Tor or SOCKS avoids the exposure. Foreign-link commands use a transport session of their own, one per link (the agent shares a session per user and server by default, `mkTSession`, `Agent/Client.hs:1442-1443`), so the link server cannot also tie them to the owner's other queues on that server.
|
||||
- **The creator is identifiable among owners**, contrary to the overview's claim that root-signing several owners conceals it (`channels-overview.md:159`): its chain entry is first, the link data it writes is signed with the root key (`Agent.hs:1076`), and every owner it promotes appears root-signed in a promotion it announces. Ordering the chain by memberId would not hide it while the creator root-signs the owners it adds (§6). Qualify the overview.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user