mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-14 09:20:28 +00:00
fable review fixes
This commit is contained in:
@@ -84,7 +84,11 @@ assembled, transported or applied.
|
||||
p2p group is whatever the admins have told each member, so an admin can put identities into the electorate that do not
|
||||
exist and vote with them. Nothing in this tier stops that. What it does provide is that every such move is *visible to
|
||||
every member*: fabricated members arrive as ordinary member-added events, purges are broadcast, every vote and
|
||||
certificate is signed and attributable, and the result of every referendum is inspectable after the fact. Tier 1 is
|
||||
certificate is signed and attributable, and the result of every referendum is inspectable after the fact. Read that
|
||||
claim strictly: it covers what an admin **sends**. An admin's other lever is to send nothing, and silence produces no
|
||||
artifact to attribute. Withholding a member-added event, a proposal, or a certificate from a chosen receiver is
|
||||
invisible at that receiver and indistinguishable from an offline peer, and it is the residue tier 1 does not address
|
||||
(see [Limitations](#limitations)). Tier 1 is
|
||||
therefore appropriate for a group that already trusts its admins not to attack it, and wants recourse and an audit
|
||||
trail when one turns out to be careless, compromised, or simply wrong. It is not appropriate against an admin who is
|
||||
hostile from the outset. [Tier 2](#tier-2-verification-gated-enfranchisement) closes that, at a usability cost, by
|
||||
@@ -96,10 +100,11 @@ making enfranchisement depend on something admins cannot manufacture.
|
||||
|
||||
- p2p groups only (`useRelays = false`). Relay groups and channels are future work: they are single-owner today, and
|
||||
replacing their owner set needs changes to the short-link owner chain in simplexmq.
|
||||
- Two referendum actions: replace the set of `GRAdmin` members, and remove members. Moderator roles are untouched.
|
||||
Removal is in v1 because otherwise a group can only purge accounts an admin planted by first electing new admins and
|
||||
asking them to act, which leaves a window, and because a group whose admins refuse to act has no other remedy. The
|
||||
action is a sum type, so profile changes, parameter changes and group deletion can be added without redesign.
|
||||
- Referendum actions: replace the set of `GRAdmin` members, and remove members; tier 2 adds enfranchisement. Moderator
|
||||
roles are untouched. Removal is in v1 because otherwise a group can only purge accounts an admin planted by first
|
||||
electing new admins and asking them to act, which leaves a window, and because a group whose admins refuse to act has
|
||||
no other remedy. The action is a sum type, so profile changes, parameter changes and group deletion can be added
|
||||
without redesign.
|
||||
- Groups of roughly 5 to 50 members, degrading past 100. Below five, referenda are ceremony over conversation.
|
||||
- Tier 1 assumes admins are trusted not to fabricate members, and relies on members noticing if one does. Groups
|
||||
needing more should wait for tier 2.
|
||||
@@ -123,9 +128,14 @@ conflicting binding, invisibly and for every member added after enabling. `XGrpM
|
||||
that value is what gets pinned, and an introducer's copy is a hint to be confirmed rather than the trust anchor.
|
||||
|
||||
**A prerequisite bug fix, outside governance.** `xGrpMemIntro` accepts the introduced member's role verbatim: unlike
|
||||
`xGrpMemNew` and `xGrpMemFwd`, its p2p branch has no `checkHostRole`, so an admin can introduce a member carrying role
|
||||
`xGrpMemNew` and `xGrpMemFwd`, its p2p branch inlines only the first conjunct of `checkHostRole`
|
||||
(`Subscriber.hs:3223`, the introducer must itself be `GRAdmin`) and omits the `memberRole < memRole` cap, so an admin
|
||||
can introduce a member carrying role
|
||||
`GROwner` into a victim's local view, and that fake owner then satisfies every owner-gated check there. This is a
|
||||
pre-existing hole with consequences beyond governance; `xGrpMemIntro` must cap the introduced role at the introducer's.
|
||||
pre-existing hole with consequences beyond governance: `memInfo` reaches `createIntroReMember` verbatim
|
||||
(`Subscriber.hs:3232`) and the role is written straight through (`Store/Groups.hs:2495`). `xGrpMemIntro` must cap the
|
||||
introduced role at the introducer's; the relay branch just above it (`Subscriber.hs:3213-3221`) already does this via
|
||||
`unknownMemberRole` and is the template.
|
||||
|
||||
### Enabling governance: the genesis certificate
|
||||
|
||||
@@ -133,8 +143,7 @@ Preconditions: every current member supports the governance chat version, and ev
|
||||
|
||||
Enabling requires an **authorising set**: all current owners for a group that has them, or a strict majority of
|
||||
current members for an ownerless group, each signing with their member key. The ownerless case exists because an empty
|
||||
owner set would otherwise be satisfied by an empty signer set. The set must be non-empty, and every signer must be a
|
||||
member the receiver has itself connected to.
|
||||
owner set would otherwise be satisfied by an empty signer set. The set must be non-empty.
|
||||
|
||||
The initiator broadcasts `x.grp.gov.enable` carrying a random 256-bit `governanceId`, the parameters, and signed bytes
|
||||
`smpEncode ("SXGG", groupIdentity, governanceId, params, memberSetHash)`. Receivers validate, fail-closed on any
|
||||
@@ -144,7 +153,10 @@ The initiator broadcasts `x.grp.gov.enable` carrying a random 256-bit `governanc
|
||||
owned-group rule would be vacuously satisfied by an unsigned certificate; without this check an attacker could
|
||||
re-genesis at will, resetting the parameters and the version chain. An identical repeat is a no-op; a different
|
||||
genesis for a governed group is surfaced, not applied;
|
||||
2. the authorising set matches the rule above, is non-empty, and every signer is connected to the receiver;
|
||||
2. the authorising set matches the rule above and is non-empty. It does **not** additionally require that the receiver
|
||||
be connected to each signer: that requirement is dropped here for the same reason it is dropped from vote counting
|
||||
below, since introductions are admin-controlled and so being connected is not evidence of anything, and here it
|
||||
would also make genesis fail for a receiver that simply has not been introduced to an owner yet;
|
||||
3. the parameters are within bounds: `7 ≤ maxReferendumDays ≤ 30`, `24 ≤ challengeHours ≤ 168`. Both ends bind. An
|
||||
unbounded window would make every non-unconditional referendum unresolvable; a one-day period with a one-hour
|
||||
window would let two confederates replace the admin set inside a day, irreversibly;
|
||||
@@ -187,11 +199,40 @@ and admins control introductions, so an admin can have every victim "connect to"
|
||||
rule that looks like a Sybil defence and is not is worse than none, because it invites reliance. The honest statement
|
||||
is that fabrication is prevented at tier 2 and merely *visible* at tier 1.
|
||||
|
||||
The same lever runs in the other direction and is quieter. Because `E` is what the *receiver* recorded, an admin that
|
||||
simply never relays some member-added events to a chosen receiver leaves that receiver with a small `E`, so a
|
||||
certificate that is a minority of the real group is unconditional and instantly ripe there. Inflation needs fabricated
|
||||
identities that everyone can see arrive; deflation needs only silence, and the victim's view is indistinguishable from
|
||||
a small group. Divergent `E` across members is visible in the applied-attestations of rule 5 below, which is detection
|
||||
after the fact rather than prevention. Tier 2 does not close this either: it gates *enfranchisement*, and a member
|
||||
never told about is a member never enfranchised.
|
||||
|
||||
**Eligibility is read at counting time, not anchored to first sight.** An earlier revision anchored both the voter set
|
||||
and `E` to what the receiver recorded when it first saw the proposal, to stop mid-referendum packing: adding `k`
|
||||
identities that vote aye satisfies `2(A + k) > E + k`, which reduces to `2A + k > E`, so each one also lowers the bar.
|
||||
Anchoring is withdrawn, for two reasons. It contradicts the running maximum below, which exists because a denominator
|
||||
that can be made too small passes fraudulent certificates; with an anchored `E` an admin need not withhold member-adds
|
||||
at all, only *deliver the proposal first*, and a receiver that anchors `E = 10` in a group of forty accepts six ayes as
|
||||
unconditional and instantly ripe. Reordering is cheap, transient and leaves nothing behind; withholding is neither. And
|
||||
anchoring makes vote validity receiver-relative, so a member that joins legitimately mid-referendum is an elector at
|
||||
some receivers and not others and the same certificate bytes yield different tallies, which is divergence with no
|
||||
attacker present.
|
||||
|
||||
The packing it was meant to stop is therefore left as what it is: an instance of fabrication, which tier 1 concedes it
|
||||
cannot prevent and only makes visible, and which tier 2 closes at the root, since an identity added mid-referendum is
|
||||
not enfranchised and so neither votes nor counts in `E`.
|
||||
|
||||
**An action may name only members that the receiver recorded when it first saw the proposal.** Evaluating candidacy at
|
||||
apply time instead would hand any incumbent a one-message veto over every election: remove one named candidate before
|
||||
the certificate ripens and the action names a non-member, so the certificate is void, repeatable indefinitely and
|
||||
selectively. Anchoring on `firstSeenAt`, which the receiver already stores, closes that; naming members who have since
|
||||
left is harmless, since the action changes roles rather than presence.
|
||||
selectively. Anchoring on `firstSeenAt`, which the receiver already stores, closes that. Naming a member
|
||||
that has since left is then tolerated rather than void, since the action changes roles rather than presence, but that
|
||||
tolerance needs a bound: **apply aborts if the resulting admin set would be empty.** Without it, proposing
|
||||
`GAReplaceAdmins [X]` and arranging for X to leave before the certificate ripens turns the veto this rule closed into a
|
||||
one-message decapitation, since setting an absent X to `GRAdmin` is a no-op while demoting every incumbent is not. An
|
||||
ownerless, adminless p2p group cannot forward for unconnected pairs (`isUserGrpFwdRelay`, `Subscriber.hs:3694`), admit
|
||||
anyone (`Subscriber.hs:3223`), or remove anyone (`Subscriber.hs:3674-3675`), and the referendum needed to recover has
|
||||
no forwarder.
|
||||
|
||||
**Recently removed members still vote, if they predate the referendum.** Because `E` does not appear in the pass
|
||||
condition `A > B`, a removal that only shrank the denominator would *lower* the bar a purger has to clear. Keeping the
|
||||
@@ -228,10 +269,11 @@ XGrpGovEnable -- genesis, above
|
||||
|
||||
XGrpGovPropose
|
||||
{ governanceId, govVersion :: Int64
|
||||
, action :: GovAction -- GAReplaceAdmins [MemberId] | GARemoveMembers [MemberId]
|
||||
-- sorted, non-empty, every named member a current
|
||||
-- member (not merely in E)
|
||||
, prevProposalHash :: ByteString -- proposal applied at the previous version,
|
||||
, action :: GovAction -- GAReplaceAdmins | GARemoveMembers
|
||||
-- | GAEnfranchise (tier 2, exactly one member)
|
||||
-- sorted, non-empty, every named member recorded by
|
||||
-- the receiver at firstSeenAt (candidacy rule below)
|
||||
, prevProposalHash :: ByteString -- proposal *held* at the previous version,
|
||||
-- or the genesis hash at govVersion = 2
|
||||
, proposer :: MemberId, sig :: Signature }
|
||||
-- proposalHash = sha256 (smpEncode ("SXGP", groupIdentity, governanceId, govVersion, action,
|
||||
@@ -245,17 +287,30 @@ XGrpGovVote
|
||||
|
||||
XGrpGovCert
|
||||
{ governanceId, proposalHash, votes :: [(MemberId, Vote, Signature)] }
|
||||
-- announcement form omits `votes`, carrying {certHash, tally} instead
|
||||
-- announcement form omits `votes`, carrying {proposalHash, certHash, tally}
|
||||
|
||||
XGrpGovRequest
|
||||
{ governanceId, haveVersion :: Int64, proposalHash :: Maybe ByteString }
|
||||
{ governanceId, haveVersion :: Int64, proposalHash :: Maybe ByteString
|
||||
, requester :: MemberId, sig :: Signature }
|
||||
```
|
||||
|
||||
The request carries a signature although it authorises nothing, because rule 1 below says governance events are
|
||||
self-authenticating and any member may forward them, and that premise has to hold for all five. Unsigned, it can be
|
||||
forged in a victim V's name with `haveVersion = 0`, and every member that honours it sends V a full catch-up bundle:
|
||||
`gov_served_version` bounds each responder individually but not the aggregate, so V receives N-1 bundles and exhausts
|
||||
its queue quota (`defaultMsgQueueQuota = 128`, `simplexmq Server/Env/STM.hs:255-256`), surfacing as `SMP.QUOTA` and
|
||||
marking the connection inactive. Serving is additionally bound to the connection the request arrived on.
|
||||
|
||||
`govVersion` must be the receiver's stored version + 1 for full processing; a higher claim is not retained and only
|
||||
marks a possible gap, triggering one rate-limited request with backoff. Multiple proposals may coexist at a version; a
|
||||
client retains the first from each proposer and ignores that proposer's later ones, so nobody can refresh a deferral
|
||||
timer by re-proposing. `prevProposalHash` is validated on every path, not only during catch-up: it must name a
|
||||
proposal the receiver holds at the previous version, or the genesis hash. Left unvalidated it is a free 32-byte nonce,
|
||||
proposal the receiver **holds** at the previous version, or the genesis hash. Holds, not applied, and the schema
|
||||
comment saying "applied" is wrong. Version skipping is permitted and same-version supersede means members legitimately
|
||||
apply different proposals at one version, so an applied-reading would permanently fail every later proposal for any
|
||||
member that skipped a version or took the superseded branch, since the chain would run through something it never
|
||||
applied. Catch-up supplies the proposals a member holds without requiring it to have
|
||||
applied them. Left unvalidated it is a free 32-byte nonce,
|
||||
and since it sits inside `proposalHash` an attacker could grind the mandate-order tie-break at will.
|
||||
|
||||
Every signature binds a `groupIdentity`, so a member key cannot be replayed across groups; `governanceId` is chosen by
|
||||
@@ -276,18 +331,37 @@ bytes per vote, groups beyond ~120 members need the chunked-blob transport alrea
|
||||
### Transport rules
|
||||
|
||||
1. All `x.grp.gov.*` events are added to `isForwardedGroupMsg` and exempt from `expectedForwarder` and the admin-only
|
||||
forwarder check: they are self-authenticating, so any member may forward or rebroadcast them, with the existing
|
||||
`sharedMsgId` dedup. An admin dropping them achieves nothing while any other path exists.
|
||||
forwarder check: they are self-authenticating, so any member may forward or rebroadcast them. An admin dropping
|
||||
them achieves nothing while any other path exists. **Governance events must not use the existing `sharedMsgId`
|
||||
dedup**; they are deduplicated on a hash of their own signed content. The existing duplicate check keys on
|
||||
`(group_id, shared_msg_id)` with no author column and runs in `createNewRcvMessage` before the event is decoded or
|
||||
any signature checked (`Store/Messages.hs:310-328`), so any member could send a victim any well-formed message
|
||||
carrying a proposal's `sharedMsgId` and the genuine proposal would then be dropped as `SEDuplicateGroupMessage` on
|
||||
every path, permanently. Self-authentication is no defence against a check that runs before it. Content-hash dedup
|
||||
also makes rebroadcast idempotent, which the multi-path rule needs to avoid a rebroadcast storm.
|
||||
2. Governance events are exempt from `blockedByAdmin` forwarding suppression, and from `XGrpMemRestrict` restrictions
|
||||
generally, including the per-member send limits of `2025-02-17-member-send-limits.md`: those are set by a member of
|
||||
equal or higher role, so leaving them in force would give an admin a throttle on a proposer's governance traffic.
|
||||
3. Demotion and blocking never affect voting, since eligibility depends only on membership.
|
||||
4. **Removal deferral.** In a governed group, `XGrpMemDel` for a member does not tear down that member's connections
|
||||
3. Governance events must not be placed behind the `memberCanSend` role gate (`Subscriber.hs:1705-1713`), which drops
|
||||
*received* messages from any member at `GRObserver`. This is a prohibition rather than an exemption: the gate is not
|
||||
general, it is applied at five call sites covering `XMsgNew`, `XMsgUpdate` and `XGrpDirectInv` only
|
||||
(`Subscriber.hs:1073, 1080, 1115, 3902, 3908`), so new event types are ungated by default and nothing has to be
|
||||
carved out. It is stated because the alternative is fatal to rule 4: an admin demoting a proposer to observer would
|
||||
not merely mark it, it would make every recipient silently discard that member's proposals and votes.
|
||||
4. Demotion and blocking never affect voting, since eligibility depends only on membership, and given the exemptions
|
||||
above they do not affect delivery either.
|
||||
5. **Removal deferral.** In a governed group, `XGrpMemDel` for a member does not tear down that member's connections
|
||||
for `maxReferendumDays`, and `x.grp.gov.*` continues to flow over them in both directions. Deferral is armed by the
|
||||
group being governed, *not* by holding a proposal. An earlier rule armed it on holding an unresolved proposal, which
|
||||
an incumbent defeats by ordering: purge first, and the victims lose their connections before any proposal exists, so
|
||||
they can neither receive it nor return a vote, while still counting in `E` and thereby raising the bar for the
|
||||
referendum against the purger. Deferral must last exactly as long as the recency allowance it exists to make usable.
|
||||
referendum against the purger. Deferral lasts `2 × maxReferendumDays`, not one period: a member removed at `t` is
|
||||
within the recency allowance of any referendum first seen before `t + maxReferendumDays`, and that referendum can
|
||||
resolve as late as `t + 2 × maxReferendumDays`. A one-period deferral would leave a window in which the removed
|
||||
member still counts in `E`, and so still raises the bar, with no connection over which to vote, which is the exact
|
||||
failure this rule exists to prevent. Deferral does not apply to a member removed *by certificate*, consistent with
|
||||
those removals taking effect at once; arming on governedness alone would give a member the group voted out a live
|
||||
moderation-exempt channel for a further two periods.
|
||||
|
||||
### Tally: majority of votes cast
|
||||
|
||||
@@ -328,8 +402,15 @@ Because the tally counts nays against the proposal, a hostile assembler would om
|
||||
to selective inclusion.
|
||||
|
||||
- A certificate is **unconditional** if it would pass with every member not in it counted as nay. Substituting
|
||||
`B ← B + (E − T)` into `A > B` gives exactly `2A > E`. No outstanding vote can flip it, so it applies at once. The
|
||||
test asks whether any outstanding vote could change the outcome, so it is independent of the tally rule.
|
||||
`B ← B + (E − T)` into `A > B` gives exactly `2A > E`, so no vote the receiver has not yet seen can flip it and it
|
||||
applies at once. The test asks whether any outstanding vote could change the outcome, so it is independent of the
|
||||
tally rule. The word to be careful with is *outstanding*: it quantifies over votes not yet cast or not yet arrived,
|
||||
and does not cover a vote already counted being withdrawn, which is what annulment does when a conflicting signature
|
||||
arrives from a member already counted as an aye. At `2A = E + 1` one annulment flips the result. For reversible
|
||||
actions that is tolerable, since the group can answer with another referendum. For removal it is not, and removal is
|
||||
the action for which the unconditional requirement exists, so **a removal certificate must be unconditional *and*
|
||||
observe the challenge window** rather than applying at once: the window is the only interval in which annulment
|
||||
evidence is collected, and skipping it for precisely the irreversible action inverts the intent.
|
||||
- Any other valid certificate opens a local **challenge window** of `challengeHours`, starting at
|
||||
`max(local first processing, ripeAt)`. First processing means when this client decoded and validated it, not when it
|
||||
was delivered: anchoring on delivery would give a member offline for a month a window that closed before it looked.
|
||||
@@ -370,10 +451,12 @@ On receiving `x.grp.gov.cert`, a member:
|
||||
determined one;
|
||||
7. applies atomically. For `GAReplaceAdmins`, set every named member to `GRAdmin` and demote every other current
|
||||
`GRAdmin` to `GRMember`. For `GARemoveMembers`, remove every named member, effective on the electorate at once and
|
||||
with no recency allowance; **a removal certificate must be unconditional** (`2A > E`), since removal is the one
|
||||
action with no inverse (there is no `GAAddMembers`, and a removed member can no longer vote or be named), and
|
||||
without that condition a single unanswered aye in a quiet week would empty the group. Then store the version,
|
||||
proposal and certificate;
|
||||
with no recency allowance; **a removal certificate must be unconditional (`2A > E`) *and* must observe the challenge
|
||||
window**, since removal is the one action with no inverse (there is no `GAAddMembers`, and a removed member can no
|
||||
longer vote or be named). Without the first, a single unanswered aye in a quiet week would empty the group; without
|
||||
the second, an annulment that would have flipped a knife-edge tally arrives after the members are already gone. For
|
||||
`GAEnfranchise`, which names exactly one member, enfranchise it; the certificate must likewise be unconditional.
|
||||
Abort the apply if the resulting admin set would be empty. Then store the version, proposal and certificate;
|
||||
8. announces the applied certificate once to all connections as `{proposalHash, certHash, tally}`. The announcement is
|
||||
a display-only hint that peers verify by fetching the certificate. It exists because a same-version disagreement
|
||||
produces no version gap and so no catch-up trigger, and without it two halves of a group would never compare.
|
||||
@@ -404,35 +487,132 @@ Tier 1 leaves one hole, and everything else in this document is downstream of it
|
||||
same parties governance is meant to constrain. Tier 2 closes it with the one primitive in SimpleX that an admin cannot
|
||||
manufacture, because it requires a human at the other end.
|
||||
|
||||
SimpleX already implements out-of-band member verification: two members compare a security code over a channel the
|
||||
protocol does not carry, and the result is stored as `memberVerifiedCode` on the member record, set through
|
||||
`APIVerifyGroupMember`. An admin can fabricate a member, introduce it, and hold its key; it cannot make you compare
|
||||
codes with a person who does not exist, and it cannot substitute itself into a comparison you make in a video call or
|
||||
in a room.
|
||||
SimpleX already implements out-of-band verification: two members compare a code over a channel the protocol does not
|
||||
carry, through `APIVerifyGroupMember`. An admin can fabricate a member, introduce it, and hold its key; it cannot make
|
||||
you compare codes with a person who does not exist, and it cannot substitute itself into a comparison you make in a
|
||||
video call or in a room. That is the property tier 2 needs, but the existing mechanism cannot be used as-is, for three
|
||||
reasons that each require a change.
|
||||
|
||||
The rule is one line: **in a tier-2 group, a member is enfranchised only once `verifyCount` already-enfranchised
|
||||
members have verified it out of band**, `verifyCount` being a genesis parameter with a small default. Everything else
|
||||
in this document is unchanged: the tally, the clock, certificates, mandate order and the admin-set rule all operate on
|
||||
the enfranchised set rather than on the recorded members. `E` is the count of enfranchised members. Verification is
|
||||
already per-member local state, so the check needs no new wire format and no new trust root.
|
||||
**Verify the member's governance key, not the connection.** In p2p groups `APIVerifyGroupMember` verifies the pairwise
|
||||
*connection*: it stores `connections.security_code`, not `memberVerifiedCode`, which is the channel-only field. Three
|
||||
consequences follow. It needs an established connection, and connections are created by admin-brokered introductions,
|
||||
so an admin could deny enfranchisement forever by never introducing a member, silently and deniably. It is per-session,
|
||||
and it is **cleared automatically on ratchet re-sync**, so an admin that is a member's forwarder can induce a desync
|
||||
and silently revoke verification, which presents as network trouble and leaves no artifact. And it says nothing about
|
||||
the member's signing key, which is what votes are checked against. Tier 2 therefore verifies a fingerprint of the
|
||||
member's governance key, which needs no connection, survives re-sync, and binds exactly the object in question.
|
||||
|
||||
**Verification must be published, as a signed claim.** The enfranchisement test is global (a majority of the electorate
|
||||
must have verified X) but verification is written only in the verifier's own database and never transmitted. No
|
||||
receiver can evaluate the rule from local state. A verifier therefore publishes `smpEncode ("SXGKV", groupIdentity,
|
||||
governanceId, proposalHash, subject, subjectKey)` signed with its own key, which is an aye vote on `GAEnfranchise`
|
||||
and is what makes the referendum evaluable. Such a claim is forgeable by anyone holding the verifier's key, so tier 2
|
||||
is no stronger than the keys of its existing electorate: it converts fabrication-at-will into key-compromise, which is
|
||||
the point, but it is not a step to a different trust root.
|
||||
|
||||
`proposalHash` is in the preimage for the same reason `voter` is in a vote's. Without it a claim is a standing aye on
|
||||
every present and future `GAEnfranchise` naming that subject, so a member removed by referendum could be re-enfranchised
|
||||
by replaying the original claims with no human re-verifying anything, and a verifier could never withdraw. With it, the
|
||||
human act and the signed artifact separate cleanly: comparing the fingerprint is done once and recorded locally, and
|
||||
the client re-signs per proposal from that local record, which a verifier can decline.
|
||||
|
||||
The rule is one line: **in a tier-2 group, a member is enfranchised only once a strict majority of the current
|
||||
electorate has verified it out of band.** Everything else in this document is unchanged: the tally, the clock,
|
||||
certificates, mandate order and the admin-set rule all operate on the enfranchised set rather than on the recorded
|
||||
members, and `E` is the count of enfranchised members.
|
||||
|
||||
A majority rather than a fixed count, because a constant would be self-defeating: an attacker that once controlled
|
||||
`k` enfranchised identities could enfranchise unboundedly many more, each new one becoming a witness for the next,
|
||||
which is the seeding problem that defeats every fixed threshold. Requiring a majority means growing the electorate
|
||||
requires already controlling it, and an attacker that controls a majority has won by ordinary means and needs no
|
||||
fabrication. It also removes a parameter from the enabler's hands.
|
||||
|
||||
Mechanically the rest is not new machinery. It is an ordinary referendum on a new action, `GAEnfranchise MemberId`,
|
||||
whose aye votes are exactly the signed verification claims above, and which reuses the certificate, ripeness, mandate
|
||||
order and audit trail unchanged. Two clauses are specific to it and both are load-bearing.
|
||||
|
||||
**An enfranchisement certificate must be unconditional** (`2A > E`), stated explicitly and not inherited. The ordinary
|
||||
pass condition is `A > B`, and under it a single aye with no nays enfranchises: one compromised identity signs a claim
|
||||
for a Sybil, nobody thinks to vote nay against a stranger, the certificate ripens, and the Sybil is then itself a
|
||||
witness for the next. That is precisely the seeding the majority rule exists to prevent, reproduced with `k = 1`.
|
||||
`2A > E` is the strict majority the rule states, and it must appear as its own clause exactly as it does for removals.
|
||||
|
||||
**`GAEnfranchise` names exactly one member.** A claim binds one `subject`, so a list would need an aggregation rule,
|
||||
and the natural one (a verifier's claim for any named subject counts as its aye on the action) is catastrophic: twenty
|
||||
people who verified one newcomer would enfranchise `[P, S1 ... S9]` unconditionally, nine Sybils riding on twenty
|
||||
humans' verification of P. Singleton actions make the question not arise. Enfranchising several members is several
|
||||
referenda, which is also the honest presentation of the cost.
|
||||
|
||||
**Claims must agree on `subjectKey`.** Only claims naming the same key count toward the same tally. Without this,
|
||||
verifiers who pinned different keys for X all produce valid claims, X is enfranchised, and X's votes then verify at
|
||||
some members and not others, permanently.
|
||||
|
||||
**Enfranchisement is evaluated once and does not lapse.** A member enfranchised when the electorate was ten stays
|
||||
enfranchised when it is forty, even though its original supporters are no longer a majority. Recomputing against the
|
||||
live electorate would make the predicate self-referential, since the electorate is what it defines, and this design
|
||||
has twice been bitten by exactly that: the certificate is applied once, like any other, and the result is stored.
|
||||
|
||||
What this buys is precise. Fabricated identities cannot vote, cannot be counted in `E`, cannot propose, and cannot be
|
||||
named by an action, so the whole family of attacks in which an admin manufactures an electorate disappears rather than
|
||||
being mitigated. It also repairs key binding as a side effect: verification confirms the key of the member you
|
||||
verified, so a MITM-ing introducer is caught by the same act, and the pinning problem tier 1 cannot solve stops
|
||||
mattering.
|
||||
mattering. This is where the tier-1 prerequisite that a key be pinned from the direct handshake rather than from the
|
||||
introducer is superseded rather than relied on: an agreeing majority of claims is itself a stronger pinning path than
|
||||
either, and a tier-2 client takes it as authoritative.
|
||||
|
||||
What it costs is equally precise, and it is not small. Verification is manual and most people never do it, so a group
|
||||
must run a deliberate round of code-checking before its first referendum, and the electorate is whoever bothered. That
|
||||
is a different proposition from tier 1: governance by the members who have met each other, not by everyone in the
|
||||
group. It bootstraps awkwardly, since the first members have nobody enfranchised to verify them and must be taken from
|
||||
the genesis set. It can produce cliques, where a well-connected subgroup is enfranchised and a peripheral one is not,
|
||||
which is a real fairness problem and not merely a usability one. And it does not touch classic Sybil: an admin who
|
||||
recruits `n` real people, or who verifies with `n` humans it controls, enrols `n` genuine voters. That residual is
|
||||
**Tier 2 therefore repairs a deflated electorate, which tier 1 cannot.** If an admin withholds a member-added event
|
||||
for X from receiver R, R at tier 1 has no way to learn X's key, because keys arrive only in admin-originated
|
||||
member-adds; `expectedForwarder` (`Subscriber.hs:3891-3894`) accepts another admin's forward only from R's host or X's
|
||||
inviter, and while an unknown author is auto-created on any admin's forward, `createNewUnknownGroupMember`
|
||||
(`Groups.hs:3483-3502`) writes no key, so R gains a name it cannot verify a signature against. At tier 2 the claims
|
||||
themselves carry `subjectKey` and are signed by members R already knows, and they travel as ordinary governance events,
|
||||
forwardable by any member. R can therefore reconstruct both X's key and X's enfranchisement over a path incumbents do
|
||||
not control. Deflation is a tier-1 limitation, not a tier-2 one.
|
||||
|
||||
What it costs is equally precise, and it is not small.
|
||||
|
||||
Verification is manual, and a majority is a lot of it: admitting one member to the electorate of a thirty-member group
|
||||
means sixteen people each comparing a code with the newcomer out of band. In practice the electorate closes as the
|
||||
group grows, and a tier-2 group should expect to enfranchise in occasional deliberate rounds rather than continuously.
|
||||
Whether that is a defect or the point is a judgement about the group: it is the same threshold the group uses for every
|
||||
other decision, and an electorate that admits members more cheaply than it makes decisions is the weaker link.
|
||||
|
||||
The primitive does not exist yet, and the implementation sketch has to say so. The only key-derived code today is
|
||||
`channelMemberCode` (`Types.hs:1912-1920`), which is *pairwise*, over `(ownKey, peerKey)`, and reachable only under
|
||||
`useRelays'`; the p2p code is `verificationCode <$> getConnectionRatchetAdHash` (`Commands.hs:3684-3685`), the
|
||||
double-ratchet AD hash, which has no relationship to any signing key. Tier 2 needs a single-member fingerprint over
|
||||
the governance key, comparison UI that presents it as a property of the person rather than of the session, and a
|
||||
p2p-writable store column; `group_members.member_security_code` exists but is written only from the channel arm.
|
||||
|
||||
Member keys are per group, so verification does not transfer between groups: the same human must be verified again in
|
||||
every tier-2 group shared with them. That follows from the unlinkability the key design exists to preserve, and it is
|
||||
a real cost to state rather than a defect to fix.
|
||||
|
||||
A member that joins after enabling cannot reconstruct the founding enfranchised set. Genesis carries `memberSetHash`,
|
||||
not the member set, and members present at enabling are enfranchised without verification, so a joiner takes that set
|
||||
from its host, which is an admin. Its `E`, its vote validation and its `2A > E` are all admin-asserted, which is the
|
||||
thing tier 2 exists to remove. Later enfranchisements are self-authenticating through their claims, so the exposure
|
||||
shrinks as the group turns over, but it never reaches zero for a joiner that cannot check the founding set out of band.
|
||||
Carrying the member set, rather than its hash, in the genesis event would close it and is the obvious v2 change.
|
||||
|
||||
The trusted setup is load-bearing. Members present at enabling are enfranchised without verification, so tier 2's
|
||||
guarantee is only as good as the founding membership; a genesis sealed over identities an owner fabricated inherits
|
||||
them permanently, and no later verification undoes it. The security of the whole tier reduces to the honesty of the
|
||||
group at the moment it opts in, which is why the enabling limitation below matters more at tier 2 than at tier 1.
|
||||
|
||||
It can produce cliques, where a well-connected subgroup is enfranchised and a peripheral one is not, which is a
|
||||
fairness problem and not merely a usability one. An admin can also shrink the electorate by removing enfranchised
|
||||
members, lowering the majority that the remainder needs to admit new ones; that is visible and bounded by the recency
|
||||
allowance, but it is a real path and it is not closed here. And none of this touches classic Sybil: an admin who
|
||||
recruits `n` real people, or verifies with `n` humans it controls, enrols `n` genuine voters. That residual is
|
||||
irreducible without a central authority, and is the same one every democracy has.
|
||||
|
||||
Tiers are per group and chosen at enabling. A group may start at tier 1 and move to tier 2 later, since raising the
|
||||
bar only shrinks the electorate and needs no new genesis; the reverse is not offered.
|
||||
Tiers are per group and chosen at enabling, and v1 offers no way to change tier afterwards. An earlier sentence here
|
||||
claimed a group could move from tier 1 to tier 2 later "since raising the bar only shrinks the electorate": that is
|
||||
wrong in both halves. No mechanism exists, governance parameters are fixed at enabling by design, and shrinking the
|
||||
electorate makes `2A > E` *easier* and ripeness *shorter*, so a switch would hand whichever clique had verified each
|
||||
other an instant unconditional certificate. A tier change is a parameter change and belongs with `GAChangeGovernance`
|
||||
in future work.
|
||||
|
||||
## Threat model
|
||||
|
||||
@@ -470,7 +650,10 @@ itself; veto a referendum by procedure, since no admin signature appears in prop
|
||||
judged as of each receiver's first sight of the proposal; censor one, since governance events are forwardable by any
|
||||
member; silence a voter by demotion or blocking, or by removal within the recency period; rush a result, since speed
|
||||
must be bought with votes and no timestamp a proposer writes is read by any rule; empty the group with a thin
|
||||
referendum, since removal certificates must be unconditional.
|
||||
referendum, since removal certificates must be unconditional. It *can* reach the same end in two steps, by passing a
|
||||
thin `GAReplaceAdmins` that installs cooperating admins and having those admins remove members with ordinary admin
|
||||
powers, which governance does not touch. The unconditional rule bounds what a certificate does, not what the admins a
|
||||
certificate installs may then do.
|
||||
|
||||
#### A group member, in a governed group
|
||||
|
||||
@@ -489,7 +672,10 @@ three the delay is hours rather than weeks.
|
||||
aggregate enough nays; bank a passing certificate and enact it later through the corroborated path; capture a member
|
||||
whose only reachable peers are the conspiracy; with a genuine majority, do anything the group can do.
|
||||
|
||||
*cannot:* satisfy the attestation bound with its own supporters, or with attestations for a different proposal. Note
|
||||
*cannot:* satisfy the attestation bound with attestations for a different proposal, and cannot satisfy it with its own
|
||||
supporters *below four members*, which is where the bound bites and where it stops: one aye plus three confederates
|
||||
outside the aye set clears `min(3, ·)`, exactly as step 6 concedes when it calls the constant a cost multiplier against
|
||||
a small conspiracy rather than a bound against a determined one. Note
|
||||
that "a genuine majority of the receiver's electorate" is only meaningful at tier 2: at tier 1 the electorate is
|
||||
authored by admins, so a colluding admin reaches any majority it likes.
|
||||
|
||||
@@ -539,8 +725,20 @@ authored by admins, so a colluding admin reaches any majority it likes.
|
||||
today keeps a live queue to a targeted member for weeks and can fill it against the 128-message quota. Today that
|
||||
member is disconnected in seconds. This is a capability the feature creates, not a tuning parameter, and it is the
|
||||
clearest respect in which enabling governance is worse than not.
|
||||
- **Omission is the unhandled class.** The design is built around making positive acts signed, ordered and
|
||||
attributable, and it does that. It has no answer to an admin that acts by not acting. Because admins are the sole
|
||||
forwarders for pairs that have not yet connected, withholding is a per-receiver, per-message capability, and every
|
||||
use of it presents as ordinary network trouble: a proposal that never arrives, a member whose existence a receiver is
|
||||
never told about (deflating that receiver's `E`), a member never introduced and so never verifiable at tier 2, an
|
||||
admit timed for when a target is offline. The mitigations are partial and all detective rather than preventive:
|
||||
governance events are forwardable by any member, so a proposal reaches anyone with one honest path; and applied
|
||||
attestations expose divergent views once members compare them. A group that cannot compare notes out of band gets
|
||||
no protection from either.
|
||||
- **Governance traffic is unsuppressible.** The exemptions that make censorship hard mean no moderation lever closes
|
||||
that channel; a per-peer budget is needed at implementation time.
|
||||
that channel; a per-peer budget is needed at implementation time. The factor that should set the constant is
|
||||
certificate verification: proposals are bounded to the first from each proposer and votes by a uniqueness
|
||||
constraint, but nothing bounds certificates per proposal per sender, and each costs up to `E` signature
|
||||
verifications, so one message buys `E` units of work from every peer.
|
||||
- **Small electorates get hours, not weeks.** At `E = 2` a single aye is half the electorate, so the delay is zero and
|
||||
only the challenge window protects; at `E = 3` it is 10 days. The month-long deterrent exists from about `E = 10`.
|
||||
- **Knife-edge divergence.** Non-unconditional certificates can resolve differently across members whose windows saw
|
||||
@@ -552,14 +750,18 @@ authored by admins, so a colluding admin reaches any majority it likes.
|
||||
- **No ballot secrecy.** Votes are signed and verifiable within the group, so incumbents can identify nay voters and
|
||||
retaliate. It is the price of certificates any member can carry past a hostile forwarder.
|
||||
- **Governance is a one-way door.** There is no path to disable it or change its parameters in v1, and a governed
|
||||
group can never be deleted remotely.
|
||||
group can never be deleted remotely. It also cannot be deleted *locally* by a current member unless the owner
|
||||
assertions are relaxed: `APIDeleteChat` requires `isOwner || not (memberCurrent membership)`
|
||||
(`Commands.hs:1338-1341`), so in an owner-free group every current member's delete throws. Leaving first and then
|
||||
deleting works, but that is a workaround, not a design.
|
||||
- **The lost-device case is unsolved.** An owner who lost their device has not left, so the `GROwner` record persists
|
||||
and only an owner can remove an owner. Closing it needs a third authorising rule keyed on demonstrable
|
||||
unreachability.
|
||||
|
||||
## Implementation sketch
|
||||
|
||||
- `Protocol.hs`: `GovAction` (`GAReplaceAdmins`, `GARemoveMembers`), the five event types and tags (added to
|
||||
- `Protocol.hs`: `GovAction` (`GAReplaceAdmins`, `GARemoveMembers`, and `GAEnfranchise` for tier 2), the five event
|
||||
types and tags (added to
|
||||
`isForwardedGroupMsg`), deterministic binary encodings with domain separation (`SXGG`/`SXGP`/`SXGV`/`SXGS`).
|
||||
- **Prerequisite:** the `xGrpMemIntro` role cap, for all p2p groups rather than only governed ones.
|
||||
- Key management: per-group member keypair for p2p governed groups, population of `memberPubKey` on join and
|
||||
@@ -573,7 +775,10 @@ authored by admins, so a colluding admin reaches any majority it likes.
|
||||
voting; the apply procedure above; catch-up serving with per-requester bounds; forwarder and `blockedByAdmin`
|
||||
exemptions; removal deferral; certificate removals applied without the recency allowance; **rejection of any admin-set
|
||||
change outside a certificate**, and of owner-role members in `xGrpMemNew`/`xGrpMemIntro`/`xGrpMemFwd`/`xGrpMemRole`.
|
||||
- `Commands.hs`: relax the `GROwner` assertion in `runUpdateGroupProfile` to `GRAdmin` for governed groups.
|
||||
- `Commands.hs`: relax the `GROwner` assertions for governed groups, not only the one in `runUpdateGroupProfile`.
|
||||
The sweep is `Commands.hs:1192` (root key), `1338-1341` (`APIDeleteChat`), `1911`, `2718` (`APIAddGroupRelays`),
|
||||
`4020` (`runUpdateGroupProfile`), `4088`, and `checkSendAsGroup` (`Subscriber.hs:1065-1067`). Channel-only sites are
|
||||
included, since the rule is that a governed client rejects any event that would create or set an owner.
|
||||
- Store:
|
||||
|
||||
```sql
|
||||
|
||||
Reference in New Issue
Block a user