mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-16 02:20:37 +00:00
docs: channel multi-owner plans: hold time bounds per stretch end, group-scope ordering wording
This commit is contained in:
@@ -71,7 +71,7 @@ Because local state thus stays "published + my in-progress changes" (an owner I
|
||||
|
||||
**A departing owner's changes outlive it.** Every device ignores content from an owner that has left or been removed (`Subscriber.hs:4019-4020`) — accepting it would let a removed owner, through one relay, keep changing roles — yet a departing owner's last changes are often still in flight: the creator changes a role and then leaves, and the new owner reconciles the link data, which shows the revocation the leave writes first (§7), before a relay has forwarded the change; a member does the same on channel open; an owner back after its queued events expired learns of the departure at chat start. Left alone, the change would end up on some members and relays only — when it was a demotion, relays and joiners would keep the member's old privileged role. Remaining owners carry a leaver's changes, in three steps; members and relays keep ignoring departed authors.
|
||||
|
||||
- **Hold, for a leave.** A relay forwards events in the order it received them (above), so a leaver's events reach every device, through each relay, before its `x.grp.leave`. An owner that learns of a leave from the link data first (a `revokedOwners` entry marked as a leave, §2) keeps applying the leaver's role changes, and removals of members, by key as before — whether they arrive forwarded, passed on, in a relay's departure report or as catch-up — until it processes the leaver's `x.grp.leave` (which it does for this even though it already holds the author as departed) or a removal of the leaver, or every relay that was current and connected when the hold started has reported applying the departure (below), or, for a relay that never reports, a fixed period has passed during which the device heard from the channel's relays; time spent offline, asleep, or with the relays down counts toward a hold at most about one capped interval per stretch. Only originals (not retries) are held, made at a version no higher than the one published with the leave — capped at the holder's own version plus the departed-owner margin (part 2), far above any real gap between owners, since the entry's writer asserts it — never naming an owner, and verified against the key kept with the removal record, as member cleanup may have deleted the leaver's row. A leaver colluding with a relay can thus, until the hold ends, make only a change it could have made before leaving; it cannot raise a gate or touch an owner. A removal starts no hold: the remover cannot vouch for the removed owner's last changes, which may be the very reason for the removal. A leave entry cannot be told from one a removed owner forged with its own key before its key was dropped, so such an owner can, with a relay, have changes to non-owner members held by an owner that reads that entry first, and adopted when the hold ends — until its remover's announcement, or, if the remover never announces, until its own `x.grp.leave` or the hold period (§8).
|
||||
- **Hold, for a leave.** A relay forwards events in the order it received them (above), so a leaver's events reach every device, through each relay, before its `x.grp.leave`. An owner that learns of a leave from the link data first (a `revokedOwners` entry marked as a leave, §2) keeps applying the leaver's role changes, and removals of members, by key as before — whether they arrive forwarded, passed on, in a relay's departure report or as catch-up — until it processes the leaver's `x.grp.leave` (which it does for this even though it already holds the author as departed) or a removal of the leaver, or every relay that was current and connected when the hold started has reported applying the departure (below), or, for a relay that never reports, a fixed period has passed during which the device heard from the channel's relays; time spent offline, asleep, or with the relays down counts toward a hold at most about one cleanup interval (30 minutes) per sleep, and at each end of an offline or relays-down stretch. Only originals (not retries) are held, made at a version no higher than the one published with the leave — capped at the holder's own version plus the departed-owner margin (part 2), far above any real gap between owners, since the entry's writer asserts it — never naming an owner, and verified against the key kept with the removal record, as member cleanup may have deleted the leaver's row. A leaver colluding with a relay can thus, until the hold ends, make only a change it could have made before leaving; it cannot raise a gate or touch an owner. A removal starts no hold: the remover cannot vouch for the removed owner's last changes, which may be the very reason for the removal. A leave entry cannot be told from one a removed owner forged with its own key before its key was dropped, so such an owner can, with a relay, have changes to non-owner members held by an owner that reads that entry first, and adopted when the hold ends — until its remover's announcement, or, if the remover never announces, until its own `x.grp.leave` or the hold period (§8).
|
||||
- **Adoption.** When an owner applies another owner's departure — the end of its hold, the departure event when it learned of the departure from it, or the announcement of its own removal of that owner — it re-sends, as retries, every member whose key the departed owner authored and that it has not already re-sent, with its blob (part 2's re-send; when none is left, the blob alone at the version it holds, which reads as no gap): the departed owner can no longer re-send its own changes, and devices that learned of the departure first dropped them. Retries carry the departed owner's key, so every owner compares them alike, and a member that already holds the value sees no change.
|
||||
- **Undo.** A role change the departed owner made that no remaining owner holds — one racing its removal, or one a device received after the departure event through a slower relay — may still have been applied by some members and relays. A relay that applies an owner's departure passes its kept copies of that owner's events to every current owner and then reports that it applied the departure; catch-up does the same for an owner that was away. An owner that has applied the departure and holds a different key for that member, for a copy made at or below the roster version published with the departure plus the departed-owner margin, re-sends that member's current value — at most once for that member and its own current key, and at most once per relay per interval, so neither a replayed nor a fabricated copy can make it re-send more. If another remaining owner does hold that change, it rejects the undo as a losing different value and re-sends the change (part 2), and every owner adopts it by key. Either way every device ends with one role per member. A departed owner's removal that a remaining owner does not hold cannot be undone — a removal record is permanent (§6) — and is not adopted, since a removed owner and a relay could fake one: the owner is shown that the departed owner removed that member on some devices, and finishes the removal itself if it agrees.
|
||||
|
||||
@@ -140,7 +140,7 @@ Add the target's `memberId` to **`revokedOwners`** in the mutable link data (`LS
|
||||
- **Agent (owner device):** no new stored state. The CAS APIs are stateless — the expected hash in; `OK` with the new hash, or the current state with its hash, out — and chat keeps the hashes. A promoted owner writes through a foreign-link API with explicit credentials (§6) — `LSETH`, `RKEYH` and `DEL` on a queue the device does not own — so no agent row needs `linkRootSigKey` (`AgentStore.hs:2903` keeps `Nothing` for the creator, whose link signing key is the root). Foreign writes are synchronous: an agent async command reports to a connection, and a promoted owner has no link connection; callers that are asynchronous today run them in a thread, and their intents are durable in chat state (relay rows, promotion and removal records) that reconcile re-drives. The creator's key rotation (§3), a read of its own link queue's recipient id (§6), forgetting a connection without `DEL` (§7), and a read of whether the link queue's server session supports v24 — which the inviter checks before sending `inv`, and below which the CAS calls fail with a typed version error — are new agent operations.
|
||||
- **Chat (owner device), one writer for every link write:** each link-write site resolves the writer once, from the channel's keys — the creator's own link connection when it holds the root private key, otherwise a promoted owner's foreign-link credentials — never from whether a group link row exists: both apps create a group link when an owner opens the link screen without one (`GroupLinkView.swift:164-166`), and channels refuse `APICreateGroupLink`. Today every site needs the creator's group link row or connection, and the profile write silently skips without one (`setGroupLinkData'`, `Internal.hs:1581-1586`). Relay activation, which today runs in the handler of the agent's `LINK` event (`Subscriber.hs:1431-1481`), becomes a function of the written data, called from that event and after a foreign write returns `OK` — a foreign write produces no `LINK` event.
|
||||
- **Chat DB (owner device):** owners chain (`owner_auth_sig`/`owner_auth_index` on `group_members`, threaded through `createLinkOwnerMember`/`updateRelayGroupKeys`); a removal record on every member's device — memberId, key, whether this device confirmed the key drop, and for a removal or leave this device runs the step reached (written before its first `LSETH`), so a crashed one is re-driven at chat start — in a group-level table that member cleanup does not touch (§6); the last published relay list (§2), with removed relays' member rows kept rather than deleted; pending-promotion record (member-row columns, with the step reached; member cleanup skips a row with a pending promotion); per member, the key of the change that set its role (made-at version, author, removal — or an empty key with its floor), per relay the highest version that relay has served it, and on a leaver's removal record its roster bound and hold state — which relays have reported its departure, and the running time left (§4); on a relay, a copy of each member's latest owner's role or removal event (§4); a cross-device roster-blob-author `memberId` on `groups` beside `stored_roster_version` (superseding the device-local `roster_sending_owner_gm_id` for the §4 blob tie-break); per-relay latest subscriber count + the relay's timestamp (fields on each relay's member row); the last-known link-data and key-set hashes; and, for a promoted owner, the link queue's `linkRcvId` (its server, `LinkKey`, root key and member key are already stored). Published link data — chat's `GroupShortLinkData` JSON inside `UserContactData.userData`, so simplexmq's link-data encoding is unchanged — gains `linkDataVersion`, `revokedOwners` (a list of `memberId`, each with the roster version its writer held and whether it is a leave) and `subscriberCountTs` beside the count; every member and relay stores the version and timestamp it holds, and owners cache the rest locally for reconcile.
|
||||
- **Relay:** creates owner rows for chain entries it has none for, skipping entries in `revokedOwners` (today `updateRelayGroupKeys` throws on a missing owner, `Store/Groups.hs:2366-2368`); keeps the removal record; applies `x.grp.owner.add`, and sends a new owner its members and count reading when it starts treating it as an owner and on its `x.grp.owner.sync` (§6); applies the chain from the link data in its periodic check, a newly seen revocation as a removal (§6); sends `x.grp.relay.count` whenever its subscriber count changes (§5); passes an owner's role or removal event its gate drops to its other current owners and tells the sender, keeps a copy of each member's latest owner's role or removal event and serves them to an owner as catch-up, passes one to every current owner when a blob reverts its member, passes a departed owner's copies to every current owner when it applies the departure and then reports it, and forwards every channel event in the order it received it (§4); accepts a signed join from a current owner onto its existing row (§2); and serves a no-longer-current owner's as-channel history posts as channel posts without a signature, which a joiner holding no key or owner role for that author would otherwise drop (today a signed as-channel item is forwarded under its author, `Internal.hs:1540-1548`). A relay without these keeps a promoted owner an observer and drops its posts, never forwards `x.grp.owner.add` (`isForwardedGroupMsg`, `Protocol.hs:528`), and reports no count. So promotion requires every current relay to support the multi-owner chat version (`multiOwnerVersion`, the next `VersionChat`) — owners already check a relay's version (`supportsVersion`, `Subscriber.hs:933`) — and is refused until they all do. Adding a relay to a channel with more than one owner fails closed instead: a relay's version is known only once it answers, and an older relay never answers (it throws on the owner it has no row for), so the owner times out the invitation and shows an error.
|
||||
- **Relay:** creates owner rows for chain entries it has none for, skipping entries in `revokedOwners` (today `updateRelayGroupKeys` throws on a missing owner, `Store/Groups.hs:2366-2368`); keeps the removal record; applies `x.grp.owner.add`, and sends a new owner its members and count reading when it starts treating it as an owner and on its `x.grp.owner.sync` (§6); applies the chain from the link data in its periodic check, a newly seen revocation as a removal (§6); sends `x.grp.relay.count` whenever its subscriber count changes (§5); passes an owner's role or removal event its gate drops to its other current owners and tells the sender, keeps a copy of each member's latest owner's role or removal event and serves them to an owner as catch-up, passes one to every current owner when a blob reverts its member, passes a departed owner's copies to every current owner when it applies the departure and then reports it, and forwards every group-scope channel event in the order it received it (§4); accepts a signed join from a current owner onto its existing row (§2); and serves a no-longer-current owner's as-channel history posts as channel posts without a signature, which a joiner holding no key or owner role for that author would otherwise drop (today a signed as-channel item is forwarded under its author, `Internal.hs:1540-1548`). A relay without these keeps a promoted owner an observer and drops its posts, never forwards `x.grp.owner.add` (`isForwardedGroupMsg`, `Protocol.hs:528`), and reports no count. So promotion requires every current relay to support the multi-owner chat version (`multiOwnerVersion`, the next `VersionChat`) — owners already check a relay's version (`supportsVersion`, `Subscriber.hs:933`) — and is refused until they all do. Adding a relay to a channel with more than one owner fails closed instead: a relay's version is known only once it answers, and an older relay never answers (it throws on the owner it has no row for), so the owner times out the invitation and shows an error.
|
||||
- **Apps (iOS + Kotlin):** `canManageLink` on `GroupInfo`; a backend-supplied flag on `GroupInfo` that another live owner exists (the chat list has no member list); `MemberRoleProposal` (`MRProposed`/`MRRejected`) + `promotionPending` on `GroupMember`; a new `RcvGroupEvent` case for the promotion service item — all optional/forward-compatible so remote-desktop parsing across versions holds.
|
||||
|
||||
## 10. UI/UX
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user