Separating identity, access, and authority for trustless Nostr communities
The Three Secrets of Concord Communities
Separating identity, access, and authority for trustless Nostr communities
Protocol reference: Concord (CORD-01 through CORD-08), Nostr NIP-01/17/40/44/59.
TL;DR. Every private group chat makes one of two promises: trust our server or trust our cryptography. Concord picks the second — but the encryption alone isn’t what makes it trustless. The design that does is the separation of a community into three independent secrets: community_id for identity, community_root for access, control_root for authority. Each has a different holder, a different lifecycle, and a different blast radius if it leaks. Because they never overlap, a community can rotate every key it owns without changing what it is — which is the one property no single-secret group can offer.
The single-secret trap
The simplest way to build a private group is one shared secret. Everyone holds the key; holding the key is membership; the key also encrypts messages. Discord servers, self-hosted Matrix rooms, and naive Nostr group schemes all collapse identity, access, and authority into a single artifact — usually a database row plus an admin password.
The failure modes are structural, not incidental:
Failure What a single-secret design does
A member is removed Rekeys everything — losing history, identity, and addressing in the same operation
A staff member is compromised Whoever holds the shared write key can forge authority and read the ciphertext
Ownership is transferred Impossible without changing the group’s identity; the group and its owner are fused
A relay is compromised Any shared key material that touched the relay is exposed — both read and write
A community is flooded Any member can drown the authoritative state everyone must sync
Concord’s designers noticed that identity, access, and authority have different lifecycles and different blast radii. They should therefore be different secrets — with different holders, different addressing, and different rotation schedules.
That observation is the whole paper. The rest is machinery in its service.
The three-secret split
Concord runs on three values. Two are secrets; one deliberately is not.
community_id community_root control_root
What it is A hash commitment 32 random bytes 32 random bytes
Derived from the others? No — commits to the owner key No — independent No — independent
Who holds it Everyone, including non-members via invites Every member Owner + staff only
Distribution Invites, in the clear Invites, rekey blobs control_wrap inside a Grant edition
Rotates? Never Every refounding Every refounding
Compromise grants Nothing (it’s public) Read access + membership Ability to flood the Control Plane
Compromise does NOT grant — Write authority Authority
Loss is Terminal — no succession Membership loss for that holder Flood-only until rotation
Read the “compromise grants” column again. No row says everything. Each secret is scoped to exactly one capability, and each compromise is bounded by construction rather than by policy.
Three properties fall out of this picture, and they are the pillars of trustlessness:
- Identity is a commitment, not a capability. Anyone can recompute community_id from a public owner key and salt. It proves who founded a community and nothing else. Its security rests on the second-preimage resistance of SHA-256. Because it never appears on the wire, it isn’t a target for exfiltration — only for collision, which SHA-256 resists.
- Access is a possession, not a signature. Holding the current community_root derives the keys to every public channel and the read key to the Control Plane. There’s no membership list to check. Being inside means deriving an address; being outside means you can’t even find the address to subscribe to.
- Authority is a write capability, not a verdict. Holding control_root derives the only keypair that produces a valid wrap at the Control Plane’s address — but the wrap’s signature proves only that some staff member published it. Whether the action takes effect is decided by the actor’s real npub and rank inside the seal, checked against an owner-rooted roster by every client.
Secret ① — community_id: identity without a server
The construction
community_id = sha256( utf8("concord/community") || owner_xonly[32] || owner_salt[32] )
Three inputs, three roles:
· The domain label separates this hash from any other use of SHA-256 in the protocol.
· The owner’s x-only public key makes the commitment self-certifying: the id is a proof of who founded the community.
· The salt lets one owner run many communities without address collisions, and makes the id non-enumerable — you can’t grind a target without knowing the owner.
What it is not
· Not secret. It rides inside every invite. Anyone who receives one can recompute it and verify the founder.
· Not an access key. You can’t decrypt anything with it.
· Not an authority key. You can’t sign an edition with it.
· Not on the wire. Every protocol address derives from it one-way; the id itself never appears in a published event, only inside invites.
The cost of unforgeable ownership
Because the id commits to the owner’s key, forging a different owner onto an existing id would require a second-preimage attack on SHA-256. That buys unforgeable ownership at a known, explicit price:
Ownership cannot be transferred. A lost owner key cannot be replaced. A stolen owner key is stolen supremacy.
This isn’t a bug to paper over; it’s the cost of a self-certifying identity. The protocol’s clean exit for a compromised owner is Dissolution — an owner-signed tombstone at a coordinate derived from community_id alone, making the community permanently read-only. Nothing can undo a tombstone. A voluntary, owner-signed succession is listed as future work, precisely because it must not reopen the forgeable-ownership attack the current design closes.
Derive and verify
import hashlib, os
def mint_community_id(owner_pubkey_hex: str) -> tuple[bytes, bytes]:
owner = bytes.fromhex(owner_pubkey_hex)
assert len(owner) == 32
salt = os.urandom(32)
return salt, derive_community_id(owner, salt)
def derive_community_id(owner: bytes, salt: bytes) -> bytes:
assert len(owner) == 32 and len(salt) == 32
return hashlib.sha256(b"concord/community" + owner + salt).digest()
def verify_invite_owner(owner_hex: str, salt_hex: str, claimed_id_hex: str) -> bool:
# An invite carries owner, salt, and community_id; the three must agree.
return derive_community_id(bytes.fromhex(owner_hex),
bytes.fromhex(salt_hex)).hex() == claimed_id_hex
Invariant 1 (Identity). For any two invites presenting the same community_id, a SHA-256 second preimage would be required to present a different owner. A joiner MUST refuse a bundle whose owner and owner_salt do not reproduce the community_id.
Secret ② — community_root: access that rotates
The construction
community_root is 32 random bytes, minted alongside the community. Crucially, it is not derived from community_id:
community_id ──╳──> community_root (no derivation path — by design)
community_id ────> entity coordinates (guestbook, banlist, grants, pins)
community_root ───> public channel keys
community_root ───> guestbook key
community_root ───> Control Plane read key (conv_key)
That explicit non-derivation is load-bearing. Because community_root is independent of community_id, access can rotate while identity stays fixed. A community that removes a member rolls community_root, derives fresh addresses for every public channel and the Control Plane read key, and severs that member — while remaining recognizably the same community, with the same founder commitment, the same invite locators at the protocol level, and the same audit trail.
What holding it means
Holding the current community_root is membership. There is no list to check, no server to ask. Concretely, it lets you:
· Derive and read every public channel address at the current epoch.
· Derive the Control Plane read key, so you can subscribe to, decrypt, and verify every staff action.
· Derive the Guestbook address, so you can announce your own join or leave.
It does not let you:
· Write to the Control Plane (that requires control_root).
· Read a private channel you weren’t granted (private channels carry independent random keys).
· Impersonate a staff member — the Control Plane wrap’s content is readable, but its signer isn’t forgeable without control_root.
Rotation is the only real removal
Concord is explicit that “remove this member” is three mechanisms with three guarantees:
Step Mechanism What it does What it does not do
1 Role removal (Control Plane edition) Revokes authority They remain a member, still reading and writing
2 Cooperative kick (Guestbook directive) Asks their client to leave A modified client still holds every key
3 Rekey or refounding (cryptographic) Rotates keys — this is the only step that enforces
Steps 1 and 2 degrade gracefully when partially propagated. Step 3 is the only cryptographic cut, and it works by rolling community_root (plus private channel keys) to a new epoch and distributing the new keys only to the surviving members.
Deriving a public channel address
# hkdf(secret, label, id, epoch) -> 32-byte seed
# group_key() normalizes to secp256k1, returns (sk, pk, conv_key)
def derive_public_channel(community_root: bytes, channel_id: bytes,
epoch: int) -> tuple[bytes, bytes]:
return group_key(
label=b"concord/channel",
secret=community_root,
id=channel_id,
epoch=epoch,
) # -> (pk=address, conv_key=encryption key)
def subscribe_filter(stream_pk_hex: str) -> dict:
return {"kinds": [1059], "authors": [stream_pk_hex]}
Invariant 2 (Access). Being able to derive the address is being a member. There is no out-of-band membership list a relay, server, or admin can consult. Revocation means re-deriving — never editing a list.
Secret ③ — control_root: authority you can revoke without breaking
The construction
control_root is 32 random bytes, minted alongside community_root and held only by the owner and staff. It gates the one plane every member must sync in full. Without this split, any member could flood the Control Plane with valid-looking wraps by the mile, drowning moderator actions behind spam.
Two distinct keys, two distinct capabilities:
· control_pk — the address of the Control Plane, derived from control_root. Only a holder can sign a wrap that verifies at that address.
· control_conv_key — the read key, derived from community_root. Every member has it; that’s why every member can read the plane.
control_root ──group_key("concord/control-signer")──> control_pk (address) + signing key
community_root ──group_key("concord/control")─────────> control_conv_key (wrap reader)
This is CORD-01’s Write-Restricted Stream: the signing keypair is held by a narrower set than the readership. In one sentence — publishing requires the secret; reading only requires the derived pubkey and the community read key.
Authority is checked inside the encryption
The staff-held write key only decides who can publish. It does not decide what counts. Every Control Plane edition is a kind: 3308 rumor sealed to the actor’s real npub inside the plaintext seal. Whether it takes effect depends on:
- The actor’s effective permissions, folded from the roster.
- The actor strictly outranking the target (position lower is higher; equal cannot act on equal).
- The actor citing the exact Grant (vac) they claim authority under, by coordinate, version, and content hash.
A demoted staff member who kept the write key can flood the plane. They cannot forge authority, because every client folds the roster and drops editions whose signer no longer outranks the target. At the next refounding, control_root rotates and they lose even the ability to flood.
Position and the anti-self-promotion rule
position orders authority. The owner is position 0, supreme and unremovable. A member’s rank is the lowest position among their roles. Two hard rules bind every edition:
· No edition may claim a position at or above its own signer. This blocks self-promotion toward the top.
· No role may ever claim position 0. Even the owner cannot create a peer nobody outranks.
Together these give an owner-rooted chain: every grant traces back, through strict ** outranking, to the ownertwo. An entry that doesn’t trace things to the owner is not authority, however in validly it’s signed.
the### The Grant’s dual payload: rank same and the key
When a Grant is the one that first makes a member staff, it carries signed edition**:
· The roles (rank and permission bits).
· A control_wrap — the current control_root, NIP-44-encrypted under the grantor↔member pairwise conversation key, its plaintext the fixed 40-byte shape epoch_be[8] ‖ control_root[32].
The promotion sequence, in numbered steps:
- The grantor computes pairwise = NIP-44 conv key(grantor_sk, member_pk).
- The grantor builds the 40-byte plaintext epoch_be ‖ control_root.
- The grantor encrypts it: control_wrap = nip44.encrypt(pairwise, plaintext).
- The grantor publishes a Grant edition (vsk 3) carrying roles=[…] and control_wrap=… onto the Control Plane.
- The promotee folds the plane they already sync, finds their own Grant, and decrypts control_wrap to recover (epoch, control_root).
- The promotee derives control_pk from that control_root at the named epoch.
- If and only if the derived control_pk matches the one they already hold for that epoch, they adopt the secret. Any mismatch is dropped — never adopted.
The adoption check is what makes delivery safe: a wrapped secret is never adopted unless it derives to exactly the control_pk the recipient already holds for the named epoch. A garbage wrap becomes attributable griefing — only the edition’s signer authors it — and the recipient simply waits for a re-delivery.
Verifying a Control Plane edition
REQUIRED_BIT = {
"manage_roles": 1 << 0,
"manage_channels": 1 << 1,
"manage_metadata": 1 << 2,
"kick": 1 << 3,
"ban": 1 << 4,
"manage_messages": 1 << 5,
"create_invite": 1 << 6,
"view_audit_log": 1 << 8,
"mention_everyone": 1 << 9,
"pin_messages": 1 << 11,
}
def honor_edition(edition, folded_roster, held_editions, community_id):
# 1. Verify the seal; recover the actor's real npub.
actor = verify_seal_signature(edition.seal)
if actor is None:
return DROP("bad seal")
# 2. Fold the roster; resolve permissions and position.
perm_bits, position = folded_roster.effective(actor)
# 3. The staff write gate is NOT the verdict — the seal's actor is.
required = REQUIRED_BIT[edition.required_permission]
if not (perm_bits & required):
return DROP("missing bit")
# 4. Strict outranking against the *current* roster, not the cited moment.
target = edition.target_npub()
target_pos = folded_roster.position_of(target)
if not (position < target_pos):
return DROP("equal cannot act on equal")
# 5. Authority citation: block until synced, then resolve.
if edition.vac:
cited = held_editions.get(edition.vac.coordinate)
if cited is None or cited.version < edition.vac.version:
return PARK("vac unsynced")
if hash_edition(cited) != edition.vac.hash:
return PARK("vac hash mismatch")
# 6. Refuse to downgrade; tie-break by authority then lower rumor id.
head = folded_roster.head_of(edition.entity)
if head and edition.version <= head.version:
return DROP("downgrade")
return HONOR(edition)
Invariant 3 (Authority). A valid wrap at control_pk proves only that some control_root holder published it. It never proves who, or that they were authorized. Authority lives in the sealed actor’s rank, traced to the owner.
The derivation machinery
All three secrets feed one frozen derivation shape. Getting a single label byte wrong sends you to a different address silently — no error, no connection, just an empty community. That’s why the derivation deserves to be treated as the protocol’s load-bearing wall.
The shape, in words: take a 32-byte secret as the HKDF input keying material. Use a fixed domain label, a 32-byte id (all-zero where the label has no meaningful id), and an optional u64 big-endian epoch as the HKDF info. Hash the 32-byte output through scalar_normalize (retrying with an incrementing counter byte if the seed isn’t a valid secp256k1 scalar). The resulting secret key yields both the x-only pubkey — the stream address — and, via self-ECDH, the NIP-44 conversation key that encrypts the wraps.
The labels you’ll meet most often:
Label Secret ID Epoch Yields
concord/channel community_root (public) or channel key (private) channel_id yes channel address + conv_key
concord/control community_root community_id yes Control Plane read key
concord/control-signer control_root community_id yes Control Plane address + signer
concord/guestbook community_root community_id yes Guestbook address
concord/base-rekey-pseudonym prior community_root community_id next base rekey address
concord/recipient-pseudonym rotator ‖ recipient pubkeys scope_id next per-recipient rekey blob locator
concord/dissolved community_id all-zero — dissolution tombstone address
group_key
from hashlib import sha256
from hmac import HMAC
from secp256k1 import PrivateKey # any audited lib
def hkdf_sha256(ikm: bytes, info: bytes, length: int = 32) -> bytes:
# salt = ∅ per CORD-02 A.1
prk = HMAC(b"\x00" * 32, ikm, sha256).digest()
okm, t, i = b"", b"", 1
while len(okm) < length:
t = HMAC(prk, t + info + bytes([i]), sha256).digest()
okm += t; i += 1
return okm[:length]
def scalar_normalize(seed: bytes) -> bytes:
counter = 0
while True:
try:
sk = PrivateKey(seed) # raises if invalid scalar
return sk.serialize()
except ValueError:
info = seed + bytes([counter]) # deterministic retry
seed = hkdf_sha256(seed, info)
counter += 1
def group_key(label: bytes, secret: bytes, ident: bytes, epoch: int | None):
assert len(secret) == 32 and len(ident) == 32
info = label + b"\x00" + ident + (epoch.to_bytes(8, "big") if epoch is not None else b"")
seed = hkdf_sha256(secret, info, 32)
sk = scalar_normalize(seed)
pk = xonly_pubkey(sk)
conv_key = nip44_conversation_key(sk, pk) # self-ECDH
return sk, pk, conv_key
Invariant 4 (Derivation). Every address in the protocol is one-way derived from a secret the address-holder doesn’t need to share. An outsider cannot identify a community’s planes, let alone read or forge at them.
Lifecycle: how the three secrets interact
The three-secret design only pays off when the lifecycle events that touch them are ordered correctly. Three events matter most.
Founding
The owner mints owner_salt (32 random bytes), computes community_id = sha256(label ‖ owner_pk ‖ salt), and mints community_root and control_root as two independent random values. Genesis is then exactly two owner-signed editions: community metadata (vsk 0) and one public channel named #general (vsk 2).
Deliberately minimal. No default roles, no scaffolding. Everything else — roles, grants, private channels — is a later edition gated by the appropriate permission. This keeps the state everyone must sync small at the moment the community exists.
Promotion (a new staff member)
The Grant that first promotes a member to staff delivers control_root inside the same signed edition — no separate message to send, no race to heal, no inbox to watch. The sequence is the one spelled out in §5.4 above.
Two rules keep this safe across a rotation:
· The epoch rides inside the ciphertext (staleness is structural).
· A folded head can carry a wrap from a prior epoch; the derivation check fails closed, so no harm. Staff crossing a rotation get the fresh control_root in their 136-byte base rekey blob regardless.
Removal and refounding
The order of operations is what makes removal work under partial failure:
- Banlist edition lands first — silencing is instant and free.
- Grant strip lands alongside — authority dies with the grant.
- Compaction of the Control Plane is prepared — abort if the fold is unreliable.
- New community_root and new control_root are minted.
- prevcommit is computed over the prior key, so survivors can verify continuity.
- Base rekey blobs are published at the next epoch: 104 bytes for a member, 136 bytes for staff.
- The compacted Control Plane is republished at the new epoch, signed by the new control_root-derived signer.
- Private channels are rekeyed, sealed under the prior root.
- Guestbook snapshot is seeded (best-effort).
The removed member receives no blob. Their old keys no longer address anything the community uses.
Each layer validates independently. A partially propagated removal degrades to a weaker removal, never a broken one.
Race resolution. Two refoundings racing to the same epoch converge deterministically: among authorized candidates at the same continuity point, the lexicographically lowest new base key wins. The heal is down-only — a held epoch re-converges only to a strictly lower sibling — so a flaky fetch cannot re-fork a settled epoch. Channel rekeys during a refounding are sealed under the prior root, so a base-fork loser can still open them.
Why the separation is trustless
A Concord community is trustless not because a single primitive is strong, but because the three secrets have non-overlapping failure modes, and each failure is contained.
If this leaks or is lost… …then
community_id Nothing. It’s public. It cannot be leaked.
community_root Access rotates. Identity stays fixed. The community survives.
control_root The holder can flood, not forge. A refounding severs them.
Concretely, the design guarantees four properties no single-secret system can:
(a) Identity survives access rotation. Because community_id isn’t derived from community_root, a refounding changes every address, every conversation key, and every private channel key — while the community’s founder commitment, invite locators at the protocol level, and audit chain of entity coordinates remain valid. A member can prove they joined the same community they were in before the rotation.
(b) Access rotation does not require identity rotation. Removing a member rolls community_root (and the private channels that member could read). No new community_id is minted, no new community is founded, no invite links need re-issuing at the protocol level. Contrast this with a single-secret model, where revocation and re-founding are the same operation.
(c) Authority revocation does not require access revocation. A demoted staff member loses authority the instant the roster folds their new position — before any key rotation. They remain a member (they can still read and write in the channels they hold keys for) until a refounding severs them. This two-step decoupling lets communities act quickly against misbehaving staff (revoke rank) and gradually against removed members (rotate keys).
(d) Revocation is containment, not prevention. Concord accepts that a member who copied what they read cannot be un-copied. What the protocol can do — and does — is ensure the past stays decryptable to them and the future does not. That’s the entire point of the epoch mechanism: rotate after removal, and no new event addresses anything the removed member can derive.
Attack surface and defenses
Attack Defense Residual
Forged owner in invite Refuse if sha256(owner ‖ salt) != community_id —
Malicious invite flood Bound channel count; truncate relays before allocating —
Demoted staff publishing Roster check drops their editions; flood possible until rotation Refounding rotates control_root
Removed member replaying a rotation Every client folds the roster; drops the rotation Holding a key ≠ authority
Stolen control_root Cannot forge (rank check), only flood Refounding rotates the secret
Stolen community_root Can read public channels and the Control read key Private channels unaffected; refounding rolls it
Stolen community_id Can derive only the dissolution address Cannot forge the owner-signed tombstone
Malicious relay Drop/replay/refuse only Cannot read, forge, or attribute
Tombstone replay across communities Client MUST check eid == community_id; refuse all-zero placeholder —
Wrong control_pk in invite Fail closed on adoption; next rotation re-delivers Eclipse-class self-harm by inviter
Pins leaked across communities Channel binding in rumor; proof includes channel tag Named residual: channel_id is client-chosen
Two residuals deserve emphasis because they are accepted, not fixed:
· No ratcheting. Anyone with a channel’s epoch key reads every message from that epoch. This is a deliberate trade for asynchronous sync at scale: a member who has been offline for a month opens their client and catches up with no coordination. For six people planning something sensitive, use a ratcheted protocol. For six hundred people with channels, roles, and moderators, use Concord.
· Members can copy what they can read. Disappearing messages protect against a future seizure, not against present recipients. This is true of every disappearing-message scheme.
Implementation checklist
The three-secret design is unforgiving of implementation shortcuts. Silent failures dominate.
The four tests to run first — in this order, because implementations most often diverge on the rekey path, and the symptom is a member silently locked out:
- Derive a known address (control_pk + channel pk) and confirm you match a shipped client.
- Decrypt one message through all three layers, ending at a rumor whose channel and epoch tags match what you expect.
- Fold a roster and confirm you reach the same head as another client, including refusing to downgrade.
- Survive a rotation — find your locator in a blob and adopt the new key without an error.
Then check each category.
Identity
· community_id is computed with sha256, not HKDF.
· Owner is x-only hex (64 chars), never bech32, never 33-byte compressed.
· Salt is 32 raw bytes; invites carry it as hex.
· A bundle whose owner and salt fail to reproduce the community_id is refused.
Access
· community_root is 32 random bytes, never derived from community_id.
· Epoch is a u64 big-endian.
· Public channel keys derive from community_root; private channel keys are independent.
· Every comparison uses created_at * 1000 + ms.
Authority
· Control Plane seals are kind 20014 (plaintext). Chat, Guestbook, and rekey seals are kind 20013 (encrypted). Fixed per plane; never per message.
· permissions rides the wire as a decimal string — a JSON number is a 64-bit float in JavaScript and silently corrupts past 2^53.
· No edition may claim a position at or above its own signer; no role may claim position 0.
· Actor must strictly outrank the target; equal cannot act on equal.
· vac citation parks until synced; then resolves the actor’s rank against the current roster, not the cited moment.
· A citation whose hash does not match the edition held at that version parks like an unsynced one.
· Drop every event from a banned pubkey.
· A staff-making Grant delivers control_root in control_wrap, adopted only if it derives to the held control_pk at that epoch.
Rotation
· Precompute the next rekey address per private channel and for the base; subscribe to it.
· A missing chunk is never a removal. Conclude removal only when you hold all n chunks and none carries your locator.
· Verify scope and epoch inside the ciphertext against the event’s tags before accepting a key.
· Reject any blob width other than 72 / 104 / 136 bytes.
· Verify continuity (prevcommit) against the key you currently hold.
· Verify the rotator’s authority against the folded roster — holding a key is never authority.
· Seal channel rekeys during a refounding under the prior root, never the freshly minted one.
· Acquire all state to be rotated before the first publish.
Refounding — reference implementation
def refound(community, removed_members, held_editions, client):
# 1. Compact the Control Plane — do not proceed if the fold is unreliable.
compaction = compact_heads(held_editions)
assert compaction.is_complete(), "Abort: cannot reliably fold"
# 2. Mint fresh secrets alongside one another.
new_root = os.urandom(32)
new_control_root = os.urandom(32)
new_epoch = community.root_epoch + 1
prevcommit = sha256(
b"concord/epoch-key-commitment"
+ community.root_epoch.to_bytes(8, "big")
+ community.community_root
).digest()
# 3. Roll the base: deliver to survivors via rekey blobs.
survivors = community.members - removed_members
for recipient in survivors:
locator = hkdf(
rotator=client.pubkey, recipient=recipient.pubkey,
label=b"concord/recipient-pseudonym",
scope_id=zeros(32), epoch=new_epoch,
)
if is_staff(recipient, community):
plaintext = (zeros(32) + new_epoch.to_bytes(8, "big")
+ new_root + derive_control_pk(new_control_root, new_epoch)
+ new_control_root) # 136 B
else:
plaintext = (zeros(32) + new_epoch.to_bytes(8, "big")
+ new_root + derive_control_pk(new_control_root, new_epoch)) # 104 B
wrapped = nip44(pairwise(client.sk, recipient.pk), plaintext)
client.publish_rekey_blob(locator, wrapped,
epoch=new_epoch, prevcommit=prevcommit)
# 4. Republish compacted Control Plane at new epoch,
# signed by the new control_root-derived signer.
new_signer_sk, new_signer_pk, _ = group_key(
b"concord/control-signer", new_control_root,
community.community_id, new_epoch)
for entity in compaction.entities:
client.publish_wrap(new_signer_pk, new_signer_sk,
plaintext_seal(entity)) # 20014
# 5. Rekey private channels — sealed under the PRIOR root.
for ch in community.private_channels:
new_key = os.urandom(32)
for recipient in survivors:
if has_channel_access(recipient, ch):
deliver(ch, recipient, new_key, prior_root=community.community_root)
# 6. Seed Guestbook snapshot (best-effort).
snapshot = survivors_sample(survivors, limit=400) # 400/event
for chunk in snapshot.chunks:
client.publish_guestbook_snapshot(chunk)
Note the ordering: compaction is prepared before the first publish, so a mid-flight failure never leaves half a rotation as the only copy. Every step is idempotent; a crashed refounder resumes.
Consider The Full Scope
Concord’s trustlessness is often described as “end-to-end encryption with a signature chain.” That’s accurate but incomplete. The encryption hides the content; the signature chain orders authority. What actually makes the model survive rotation, removal, and defection is the three-secret split:
· community_id gives the community an unforgeable, transferable-to-no-one identity, so ownership is provable offline and forgery requires breaking SHA-256.
· community_root gives it access that rotates independently of identity, so a community can sever a member and remain itself — a property no single-secret group can offer.
· control_root gives it authority that can be granted and revoked without touching access, so a demoted staffer loses their voice before they lose their seat, and a removed member loses both in a defined order.
Put together, the design turns three classic trade-offs into non-trade-offs:
Classic trade-off Concord’s resolution
Secure removal vs. history continuity Rotate access; identity stays fixed.
Fast demotion vs. irrevocable severance Revoke rank first; rotate keys last.
Strong ownership vs. recoverability Ownership is unforgeable and unrecoverable — the trade is made explicit.
Censorship resistance vs. moderation Clients ignore unauthorized actions; the owner roots all authority.
The community that results is harder to seize, easier to audit, and capable of growing from a forum into a mass, user-owned network — without pretending the trade-offs do not exist. The three secrets are the reason.
Appendix A — One-page reference
community_id
· sha256 over label, owner key, and salt.
· Public and offline-verifiable.
· Unforgeable ownership.
· Dissolution address derives from it.
· Never rotates.
community_root
· Random 32 bytes.
· Possession is membership.
· Rotates on refounding.
· Gates public channels and the Control Plane read key.
· Never derived from community_id.
control_root
· Random 32 bytes, held by staff only.
· Derives control_pk and the wrap signer.
· Delivered in a staff-making Grant via control_wrap.
· Rotates alongside community_root.
· A spam gate, never a verdict.
Separation yields
· Identity survives rotation.
· Revocation without refounding.
· Flood gate, not verdict.
· Containment, not prevention.
Appendix B — Glossary cross-reference
Term Belongs to One-line definition
edition authority Versioned, chained Control Plane document signed by its real actor.
fold authority Client-side reduction to one head per entity: highest intact version, refuse downgrade, tie by authority then lower rumor id.
epoch access Counter on each key; bumps only on removal. Rotates every derived address.
vac authority The cited Grant, pinned by coordinate/version/hash; a sync floor, not a verdict.
prevcommit access Continuity commitment over the prior key; a convergence check, not secrecy.
locator access Per-recipient value derived from public keys, used to find your blob in a rotation.
control_wrap authority The 40-byte epoch ‖ control_root delivered inside a staff-making Grant.
refounding access + authority Whole-community rotation: roll community_root, mint control_root, rekey, compact, snapshot.
tombstone identity Owner-signed dissolution at a community_id-derived address; must name its community.
MIT-licensed specification: github.com/concord-protocol/concord. Reference clients: Vector (Rust/Tauri), Armada (TypeScript/Capacitor), Accordion (browser). SDKs: vector_sdk (Rust), applesauce-concord (TypeScript).
Write a comment