mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 12:53:18 +00:00
wip
This commit is contained in:
1 parent
e280ba6d16
commit
452c6f2a6b
1 file changed
+3
-3
@@ -37,7 +37,7 @@ Link data and keys already *have* a sequencer — the server — so they need on
|
||||
|
||||
**Correlation for removal — a signed map, cached on relays, never on the link server.** To remove owner X you must know *which* key is X's, but the server's set is deliberately **anonymous**: any public or link-server key→owner map lets a joining SMP operator (who reads the OwnerAuth chain and sees which key writes) tie an owner's link-queue activity to their identity. So the `memberId → linkRcvKey` map lives elsewhere: a new owner announces its key (own protocol event, delivered owner-scoped via `getGroupOwners`/a `DJSOwners` scope, carrying the `key_set_version` from its `RKEY` success), and — critically — **relays cache the signed map and serve it on request**. Each entry is owner-signed, so a relay cannot forge one; a relay sees the map but is not the link-queue operator, so it cannot correlate link-queue writes. Because relays are always-on, an owner can always obtain the map without waiting for a peer owner.
|
||||
|
||||
**So removal never blocks and self-heals:** it is an **async durable worker** (fetch the map from a relay if the local copy lacks the target → `RKEY` out the key → `LSET` the revocation), not a synchronous command. A missing *addition* self-heals via CAS (stale `key_set_version` → rejection → returned set repairs it). A relay withholding the map is handled by asking another relay.
|
||||
**So removal never blocks and self-heals:** it is an **async command** whose steps chain by continuation (fetch the map from a relay if the local copy lacks the target → `RKEY` out the key → `LSET` the revocation), CAS rejections retrying inline — no new durable-worker type. A crash mid-removal is corrected by reconcile-on-open (§2): any owner that sees an `OwnerAuth` whose key is no longer in the set treats it as revoked and completes the marker. A missing *addition* self-heals via CAS (stale `key_set_version` → rejection → returned set repairs it). A relay withholding the map is handled by asking another relay.
|
||||
|
||||
## 4. Roster
|
||||
|
||||
@@ -57,7 +57,7 @@ Link data and keys already *have* a sequencer — the server — so they need on
|
||||
|
||||
**What:** the "subscribers: N" display — cosmetic, best-effort. Owners have no subscriber connections (subscribers connect to relays), so the count can only come from relays.
|
||||
|
||||
**Source — absolute per-relay counts, take the max.** Each relay states its own *current absolute* subscriber count as a distinct per-relay event **batched with the join/leave event it already sends to owners** — so it costs no extra SMP block, and it need only be restated when the count changes, which is exactly on a join/leave (no periodic reporting). The owner stores the latest **per relay** (a field on the relay's member row); the published count is the **max** across relays. Absolute-plus-max makes the hard case correct: a newly added relay that subscribers are still slowly connecting to reports a low number that stays below the max and is ignored until it catches up; a subscriber leaving that relay drops only its entry, not the max; a real departure (from all relays) lowers every entry and so the max. **Deltas fail here** — a leave is forwarded by several relays and *deduplicated*, so an owner cannot attribute a decrease to a relay, and a low new relay's decrease would wrongly crash the total. The count event, being a separate per-relay event, is not deduplicated even though the join/leave it batches with is. (Relays need not talk to each other; the max of independent reports suffices.)
|
||||
**Source — absolute per-relay counts, take the max.** Each relay states its own *current absolute* subscriber count as a distinct per-relay event **batched with the join/leave/removal event it already sends to owners** — so it costs no extra SMP block, and it need only be restated when the count changes, which is exactly on a join/leave/removal (no periodic reporting). The owner stores the latest **per relay** (a field on the relay's member row); the published count is the **max** across relays. Absolute-plus-max makes the hard case correct: a newly added relay that subscribers are still slowly connecting to reports a low number that stays below the max and is ignored until it catches up; a subscriber leaving that relay drops only its entry, not the max; a real departure (from all relays) lowers every entry and so the max. **Deltas fail here** — a leave is forwarded by several relays and *deduplicated*, so an owner cannot attribute a decrease to a relay, and a low new relay's decrease would wrongly crash the total. The count event, being a separate per-relay event, is not deduplicated even though the join/leave it batches with is. (Relays need not talk to each other; the max of independent reports suffices.)
|
||||
|
||||
**Sequencer — a leading owner** (the lowest `owner_auth_index` among current owners) publishes the count to link data. Not for correctness (relay-sourcing already fixed the promoted-owner-undercount) but for **efficiency**: the count shares `link_data_version`, so N owners publishing per join would bump it constantly and starve human writes; one publisher is one stream. **Handover follows chain order on a membership change** (tentative): when the leader is removed or leaves, the next owner in `owner_auth_index` order automatically becomes the publisher — no liveness detection. A leader that is merely *offline* is not handed over; the count just goes stale until it returns, which is fine because the count is cosmetic. A new owner seeds from the published value and does not publish until it has its own relay reports.
|
||||
|
||||
@@ -74,7 +74,7 @@ x.grp.promote.cancel { invitationId } -- O1 -> M
|
||||
```
|
||||
`memberRole` is the offered role; `roleData` is an optional role-scoped block (owner: `linkRcvId` in inv, fresh `linkRcvKey` in acpt); `memberKey` is M's existing key, which O1 must check **equals the key it already holds for M** — a consistency check so the acceptance cannot introduce a new key (distinct from the OOB verification in §8, which is advisory). A future role adds a `roleData` variant; the parser must decode an unknown tag to an opaque/ignored value (not error), since `omittedField` only covers an *absent* field.
|
||||
|
||||
**Consent:** acceptance is not automatic — M confirms in the UI. A pending record on each side; reject or cancel clears it. On acceptance O1 runs a durable, resumable worker: `RKEY` (add M's key, §3) → `LSET` (append M's `OwnerAuth`, §2) → `x.grp.mem.role … GROwner` (the **commit point** — M treats itself as owner only on receiving this). O1 also announces M's key to the other owners and sends M the current key-map (§3).
|
||||
**Consent:** acceptance is not automatic — M confirms in the UI. A pending record on each side; reject or cancel clears it. On acceptance O1 chains, as an async command resumable from the pending record (no new worker type — the record is the durable state, re-driven on receipt and on startup): `RKEY` (add M's key, §3) → `LSET` (append M's `OwnerAuth`, §2) → `x.grp.mem.role … GROwner` (the **commit point** — M treats itself as owner only on receiving this). O1 also announces M's key to the other owners and sends M the current key-map (§3).
|
||||
|
||||
**Transport is not confidential, and that is fine.** Support scope reaches M plus all moderators, so a moderator sees the invitation — but nothing secret is in it (`linkRcvId`/`linkRcvKey` are inert public data), and a moderator that *injects* a fake `x.grp.promote.inv` achieves nothing: completion needs an owner holding a matching pending record, which no one does, so no key or link write ever happens. M additionally verifies the `.inv` is owner-signed and ignores others.
|
||||
|
||||
|
||||
Reference in new issue
Block a user