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
- Mint a Concord v2 community with a public
#general.
- Join as a second identity by direct invite. Note
community_channels.epoch for general vs communities.server_root_epoch.
- Ban any member (triggers a read-cut rekey). Repeat a few times.
- Observe
server_root_epoch advance while general's epoch stays put.
- Call
sync_communities() (or restart). The channel epoch is unchanged.
- 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.
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_sdkbuilt frommaster(e66f5c9) — same behaviour reasoned from published0.7.2control_pkpresent)Observed
A bot member of a community, moderating
#general. Its own SDK database over one 7-minute run in which it issued 4 bans: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 itRestarting the process (which runs
sync_communities()at startup, documented as folding "control events, rekeys, and banlist for every community"):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:
generalepochprivateepochaccept_public_linkThe 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
#general.community_channels.epochforgeneralvscommunities.server_root_epoch.server_root_epochadvance whilegeneral's epoch stays put.sync_communities()(or restart). The channel epoch is unchanged.#generalfrom a third identity — the member from step 2 never receives it, while a fresh joiner does.Impact
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 nocontrol_root, so a staff member loses write access and must be re-granted.