mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 17:13:10 +00:00
wip
This commit is contained in:
1 parent
d7d4d6f0eb
commit
c98987d014
1 file changed
+6
-7
@@ -13,9 +13,9 @@ The whole problem is ordering concurrent writes, and the answer is fixed by **wh
|
||||
| **Link data** (profile, relay list, owners chain) | one blob on the SMP link queue | the **SMP server** | version-CAS: write-with-expected-version, rejection returns current state, re-merge |
|
||||
| **Owner queue keys** (`recipientKeys`) | one set on the SMP link queue | the **SMP server** | version-CAS, same shape; + an owner-held key→owner map |
|
||||
| **Roster** (member/mod/admin roles) | a signed copy on every relay; no trusted central copy | **none** | deterministic tie-break + owner-side auto-retry |
|
||||
| **Subscriber count** | derived, cosmetic | a **leading owner** (for efficiency, not correctness) | relay-sourced; chain-order handover |
|
||||
| **Subscriber count** | derived, cosmetic | none — any owner, on channel open | relay-sourced max, timestamp freshness-gate |
|
||||
|
||||
Link data and keys already *have* a sequencer — the server — so they need only optimistic concurrency, no consensus. The roster is the only state with no sequencer, which is exactly why it is the hard part. The count needs a soft leader only to avoid write contention.
|
||||
Link data and keys already *have* a sequencer — the server — so they need only optimistic concurrency, no consensus. The roster is the only state with no sequencer, which is exactly why it is the hard part. The count is cosmetic and needs no sequencer at all — any owner corrects it on channel open, gated by a timestamp.
|
||||
|
||||
## 2. Link data
|
||||
|
||||
@@ -57,9 +57,9 @@ 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/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.)
|
||||
**Source — max across relays, with a timestamp.** Each relay batches its own *current absolute* subscriber count and broker timestamp with the join/leave/removal event it already sends owners — no extra SMP block, restated only when the count changes. An owner keeps the latest `(count, ts)` **per relay** (a field on the relay's member row); its estimate is the **max** count, carrying that relay's `ts`. The max is what ignores a newly-added relay that subscribers are still slowly connecting to (its low count stays below the max until it catches up) while still decreasing on a real departure (the max-holding relay's own count drops — a lagging second relay reporting a slightly lower number never lowers the max, so cross-relay skew causes no spurious decrease). Deltas cannot do this: a leave is *deduplicated* across relays, so an owner cannot attribute a decrease; the per-relay count event, being distinct per relay, is not deduplicated.
|
||||
|
||||
**Sequencer — a leading owner** (the lowest `memberId` among current owners — a stable order every device computes the same way) publishes the count to link data. Not for correctness (relay-sourcing already fixed the promoted-owner-undercount) but for **efficiency**: the count writes the shared link blob, so N owners publishing per join would each rewrite it and starve human writes; one publisher is one stream. **Handover follows that order on a membership change** (tentative — see §11): when the leader is removed or leaves, the next-lowest `memberId` owner 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.
|
||||
**Published on channel open, not on events.** Link data carries `(subscriberCount, subscriberCountTs)`. When an owner opens the channel it already reconciles link data (§2); if its estimate differs materially from the published count *and* its `ts` is newer than `subscriberCountTs`, it publishes `(estimate, ts)`. The timestamp is a **freshness gate** — only an owner whose data is newer than what is published writes, so a *behind* owner (stale data, older `ts`) stays quiet and cannot push a wrong value in either direction, which is exactly what lets the update go **both ways** safely. Concurrent opens CAS: one wins, the other re-reads and sees it already fresh. This needs no leading owner, no handover, no staleness detection, no debounce, and no extra query — the count tracks owner activity (fine for a cosmetic number) and writes the link at most once per owner-open, not per join. (Use the relay's *broker* timestamp so owner clock skew is irrelevant; relay-to-relay skew is absorbed by the max.)
|
||||
|
||||
## 6. Owner promotion (adding an owner)
|
||||
|
||||
@@ -95,7 +95,7 @@ Drop the leaver's key from `recipientKeys` (`RKEY`, targeting it via the owner-h
|
||||
|
||||
- **SMP link queue (simplexmq server):** the CAS token per mutable object — the **hash of the stored user-data blob** for link data, and the hash of the sorted key set for `recipientKeys` (the server computes each on write; no version counter). These wire changes gate on a new SMP **relay** version (`currentServerSMPRelayVersion`, `VersionSMP` 18 → 19 — *not* `currentSMPClientVersion`, which is the client↔client envelope and gates nothing server-facing). **Multi-owner requires the link queue's server ≥ v19**: below it, blind `LSET` is merely lossy, but blind `RKEY` *evicts* other owners (replace with no CAS), so promotion must **fail closed** on an old link server, not degrade.
|
||||
- **Agent store (owner device, both SQLite and Postgres trees):** the last-known link-data hash + in-content version cached next to the link credentials; `linkRootSigKey` persisted (`AgentStore.hs:2514`).
|
||||
- **Chat DB (owner device):** owners chain (`owner_auth_sig`/`owner_auth_index` on `group_members`, threaded through `createLinkOwnerMember`/`updateRelayGroupKeys`); pending-promotion record (member-row columns); pending role-deltas; `roster_version_owner_id`; per-relay absolute subscriber count (a field on each relay's member row); the `memberId → linkRcvKey` map. A `DJSOwners` owner-only delivery scope for the key announcement.
|
||||
- **Chat DB (owner device):** owners chain (`owner_auth_sig`/`owner_auth_index` on `group_members`, threaded through `createLinkOwnerMember`/`updateRelayGroupKeys`); pending-promotion record (member-row columns); pending role-deltas; `roster_version_owner_id`; per-relay latest subscriber count + broker timestamp (fields on each relay's member row); the `memberId → linkRcvKey` map. Published link data (mutable) also carries `subscriberCountTs` beside the count. A `DJSOwners` owner-only delivery scope for the key announcement.
|
||||
- **Relay:** a cached signed `memberId → linkRcvKey` map, served on request (for owner removal).
|
||||
- **Apps (iOS + Kotlin):** `canManageLink` on `GroupInfo`; `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.
|
||||
|
||||
@@ -108,10 +108,9 @@ Drop the leaver's key from `recipientKeys` (`RKEY`, targeting it via the owner-h
|
||||
|
||||
## 11. Open decisions
|
||||
|
||||
- **Count-leader handover** is tentative: chain-order takeover on a membership change (§5). Since owners are often-offline phones, the lowest-memberId leader may be absent much of the time, leaving the count stale — so a staleness-triggered takeover (an online owner publishes when the count lags its own relay-derived value, staggered by memberId order so they don't all write at once, CAS resolving any overlap) is likely needed for the count to actually track. To brainstorm.
|
||||
- **`validateLinkOwners` order** (simplexmq): a memberId sort of `owners` (§2) coexists with delegated (non-root-signed) entries only if validation anchors each entry to root through the set rather than requiring "signed by an earlier entry". Decide whether to make it order-independent, or to require all owners root-signed (creator-only adds).
|
||||
|
||||
Settled this round: multi-owner hard-blocks on a pre-v19 link server (§9, no degraded mode); the roster approach is tie-break + auto-retry with no leader (§4); a same-member conflict is surfaced and re-applied through normal UI (§10).
|
||||
Settled this round: the subscriber count publishes on channel open with a timestamp freshness-gate — no leader, no handover (§5); multi-owner hard-blocks on a pre-v19 link server (§9, no degraded mode); the roster approach is tie-break + auto-retry with no leader (§4); a same-member conflict is surfaced and re-applied through normal UI (§10).
|
||||
|
||||
A detailed change-list (per-file edits, migrations, test cases) follows once this design is locked — deliberately omitted here to keep the design reviewable in one pass.
|
||||
|
||||
|
||||
Reference in new issue
Block a user