Skip to content

Public channel epoch does not follow a base rekey — member goes deaf, sync_communities() does not repair it #84

Description

@derekross

Summary

After a base rekey, an existing member's public channel keeps its old epoch and the member goes deaf on that channel. sync_communities() does not repair it — only a fresh join does.

CORD-02 §5 says a public channel "derives its stream from the community_root per held root epoch (readable by every member, rotates with the base for free)". In practice the per-channel epoch is stored and never rotated.

This is particularly damaging for a moderation bot: every ban triggers a read-cut rekey, so a bot that bans a spammer deafens itself on the channel it moderates. The more effectively it moderates, the faster it stops working.

Versions

  • vector-core / vector_sdk built from master (e66f5c9) — same behaviour reasoned from published 0.7.2
  • Community minted by armada (Concord v2, split control plane, control_pk present)

Observed

A bot member of a community, moderating #general. Its own SDK database over one 7-minute run in which it issued 4 bans:

                 before run     after run
community root_epoch    4            8      ← advanced (4 bans → 4 read-cut rekeys)
channel "general"       4            4      ← frozen

Message reception collapsed accordingly: the bot saw 18 of ~61 messages sent, all before its first ban. An ordinary member joining the same community at the same moment received all of them.

sync_communities() does not repair it

Restarting the process (which runs sync_communities() at startup, documented as folding "control events, rekeys, and banlist for every community"):

BEFORE restart:  community=8   general=4
AFTER  restart:  community=8   general=4

The community root epoch is tracked correctly and the member holds the epoch keys (0…8 present in community_epoch_keys). Only the channel row is stale.

The two join paths disagree

Joining the same community at the same moment, both on the same build:

join path general epoch private epoch
accept_public_link current 0
direct invite 0 current

The public channel is not vended in the bundle at all (armada's bundle carries only private-channel grants), so both rows are derived by vector-core. The direct-invite path seeds the public channel at epoch 0, and nothing later repairs it.

Reproduce

  1. Mint a Concord v2 community with a public #general.
  2. Join as a second identity by direct invite. Note community_channels.epoch for general vs communities.server_root_epoch.
  3. Ban any member (triggers a read-cut rekey). Repeat a few times.
  4. Observe server_root_epoch advance while general's epoch stays put.
  5. Call sync_communities() (or restart). The channel epoch is unchanged.
  6. Send a message to #general from a third identity — the member from step 2 never receives it, while a fresh joiner does.

Impact

  • Any member who joined by direct invite is on a stale public-channel address from the start.
  • Any member at all goes deaf on public channels after a rekey they didn't rejoin through.
  • Silent: the member remains in the community, the control plane still folds (banlist stays in sync), and the only symptom is that messages stop arriving.

Workaround

Re-join via a public invite link, which re-derives every channel from current state. We added a one-shot --join <link> to our bot for exactly this. It has to be re-run after every rekey, so it is not viable in production — and a link join vends no control_root, so a staff member loses write access and must be re-granted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions