feat(concord): add community creation factory and entity coordinates

Completes the "create a community" path (CORD-02 Genesis), pinned to Armada
(concord-v2 control.ts/community.ts):

- ConcordKeyDerivation: control/guestbook plane keys and the keyless entity
  coordinates — grantCoordinate = hkdf32(communityId, "concord/grant"||member),
  banlistCoordinate (ZERO32 id), inviteLinksCoordinate (creator)
- ControlEditionBuilder: assembles kind-3308 edition rumors (vsk/eid/ev/ep/vac),
  the inverse of ControlEdition.fromRumor
- MetadataEntity gains relays
- ConcordCommunityFactory.create: mints owner_salt + self-certifying community_id,
  an independent community_root, and two owner-signed genesis editions (metadata
  with eid=communityId, and a public #general channel) as plaintext-seal wraps on
  the Control Plane at epoch 0

Test creates a community, verifies the id commitment, opens the genesis wraps
(20014 seals, owner-authored), folds them into live ConcordCommunityState with a
#general channel and owner authority, and confirms one owner yields distinct
communities. Green on :quartz:jvmTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
Claude
2026-07-09 22:53:13 +00:00
parent b96960db88
commit f42f587ade
5 changed files with 353 additions and 1 deletions
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.concord.cord02Community
import com.vitorpamplona.quartz.concord.cord04Roles.ChannelEntity
import com.vitorpamplona.quartz.concord.cord04Roles.ConcordJson
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEdition
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEditionBuilder
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEntityKind
import com.vitorpamplona.quartz.concord.cord04Roles.MetadataEntity
import com.vitorpamplona.quartz.concord.crypto.ConcordKeyDerivation
import com.vitorpamplona.quartz.concord.crypto.GroupKey
import com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.utils.RandomInstance
/**
* A freshly created Concord community: its self-certifying identity and access
* secrets, plus the genesis Control Plane wraps to publish and the equivalent
* editions to fold locally.
*/
class NewConcordCommunity(
val communityId: ByteArray,
val ownerPubKey: String,
val ownerSalt: ByteArray,
val communityRoot: ByteArray,
val rootEpoch: Long,
val generalChannelId: ByteArray,
val controlPlane: GroupKey,
/** The kind-1059 control-plane wraps to publish (metadata + #general). */
val genesisWraps: List<Event>,
/** The same editions as parsed [ControlEdition]s, for immediate local folding. */
val genesisEditions: List<ControlEdition>,
) {
val communityIdHex: String get() = communityId.toHexKey()
val generalChannelIdHex: String get() = generalChannelId.toHexKey()
}
/**
* Creates new Concord communities (CORD-02 Genesis).
*
* `create` mints a random `owner_salt`, derives the self-certifying
* `community_id = sha256("concord/community" ‖ owner ‖ salt)`, generates an
* independent random `community_root` (so access can rotate while identity stays
* fixed), and emits exactly two owner-signed genesis editions — the community
* metadata and a public `#general` channel — as plaintext-seal wraps on the
* Control Plane at epoch 0.
*/
object ConcordCommunityFactory {
const val GENERAL_CHANNEL_NAME = "general"
suspend fun create(
ownerSigner: NostrSigner,
name: String,
createdAt: Long,
description: String? = null,
relays: List<String> = emptyList(),
icon: String? = null,
): NewConcordCommunity {
val ownerXOnly = ownerSigner.pubKey.hexToByteArray()
val ownerSalt = ConcordKeyDerivation.newOwnerSalt()
val communityId = ConcordKeyDerivation.communityId(ownerXOnly, ownerSalt)
val communityRoot = RandomInstance.bytes(32)
val generalChannelId = RandomInstance.bytes(32)
val rootEpoch = 0L
val controlPlane = ConcordKeyDerivation.controlPlaneKey(communityRoot, communityId, rootEpoch)
val metadataJson =
ConcordJson.instance.encodeToString(
MetadataEntity.serializer(),
MetadataEntity(name = name, icon = icon, description = description, relays = relays),
)
val channelJson =
ConcordJson.instance.encodeToString(
ChannelEntity.serializer(),
ChannelEntity(name = GENERAL_CHANNEL_NAME, private = false),
)
val metadataRumor =
ControlEditionBuilder.rumor(
authorPubKey = ownerSigner.pubKey,
entityKind = ControlEntityKind.METADATA,
entityId = communityId, // metadata eid == community id
version = 0,
prevHash = null,
content = metadataJson,
createdAt = createdAt,
)
val channelRumor =
ControlEditionBuilder.rumor(
authorPubKey = ownerSigner.pubKey,
entityKind = ControlEntityKind.CHANNEL,
entityId = generalChannelId, // channel eid == channel id
version = 0,
prevHash = null,
content = channelJson,
createdAt = createdAt,
)
// Control Plane uses plaintext (20014) seals so signatures survive re-encryption across epochs.
val metadataWrap = ConcordStreamEnvelope.wrap(metadataRumor, controlPlane, ownerSigner, encrypted = false, createdAt = createdAt)
val channelWrap = ConcordStreamEnvelope.wrap(channelRumor, controlPlane, ownerSigner, encrypted = false, createdAt = createdAt)
return NewConcordCommunity(
communityId = communityId,
ownerPubKey = ownerSigner.pubKey,
ownerSalt = ownerSalt,
communityRoot = communityRoot,
rootEpoch = rootEpoch,
generalChannelId = generalChannelId,
controlPlane = controlPlane,
genesisWraps = listOf(metadataWrap, channelWrap),
genesisEditions =
listOfNotNull(
ControlEdition.fromRumor(metadataRumor),
ControlEdition.fromRumor(channelRumor),
),
)
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.concord.cord04Roles
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
/**
* Builds unsigned kind-3308 Control Plane edition rumors (the inverse of
* [ControlEdition.fromRumor]). Seal these with a plaintext (20014) seal and wrap
* them on the community's Control Plane so the author's signature survives
* re-encryption across epochs.
*/
object ControlEditionBuilder {
/**
* Assembles a control edition rumor for [entityKind]/[entityId] at [version].
* Pass [prevHash] to chain onto the previous edition (null for genesis) and
* [authorityCitation] to pin the Grant the [authorPubKey] acts under.
*/
fun rumor(
authorPubKey: HexKey,
entityKind: ControlEntityKind,
entityId: ByteArray,
version: Long,
prevHash: ByteArray?,
content: String,
createdAt: Long,
authorityCitation: AuthorityCitation? = null,
): Event {
val tags = ArrayList<Array<String>>(5)
tags.add(arrayOf(ControlEdition.TAG_VSK, entityKind.wire))
tags.add(arrayOf(ControlEdition.TAG_EID, entityId.toHexKey()))
tags.add(arrayOf(ControlEdition.TAG_EV, version.toString()))
if (prevHash != null) tags.add(arrayOf(ControlEdition.TAG_EP, prevHash.toHexKey()))
if (authorityCitation != null) {
tags.add(
arrayOf(
ControlEdition.TAG_VAC,
authorityCitation.grantId.toHexKey(),
authorityCitation.grantVersion.toString(),
authorityCitation.grantHash.toHexKey(),
),
)
}
return RumorAssembler.assembleRumor(authorPubKey, createdAt, ConcordKinds.CONTROL, tags.toTypedArray(), content)
}
}
@@ -98,10 +98,14 @@ class ChannelEntity(
val deleted: Boolean = false,
)
/** A community's Metadata content (CORD-02): display name, icon, and description. */
/**
* A community's Metadata content (CORD-02): display [name], optional [icon] and
* [description], and the community's bootstrap [relays]. Client-extensible.
*/
@Serializable
class MetadataEntity(
val name: String = "",
val icon: String? = null,
val description: String? = null,
val relays: List<String> = emptyList(),
)
@@ -180,6 +180,41 @@ object ConcordKeyDerivation {
identity: String,
): ByteArray = hkdf32(voiceMediaKey, buildInfo(ConcordLabels.VOICE_SENDER, sha256(identity.encodeToByteArray())))
// ---- Plane keys (CORD-02) -------------------------------------------------
/** The Control Plane address for a community at [epoch] (holders of the root only). */
fun controlPlaneKey(
communityRoot: ByteArray,
communityId: ByteArray,
epoch: Long,
): GroupKey = groupKey(ConcordLabels.CONTROL, communityRoot, communityId, epoch)
/** The Guestbook Plane address for a community at [epoch]. */
fun guestbookPlaneKey(
communityRoot: ByteArray,
communityId: ByteArray,
epoch: Long,
): GroupKey = groupKey(ConcordLabels.GUESTBOOK, communityRoot, communityId, epoch)
// ---- Control entity coordinates (CORD-04) ---------------------------------
// Keyless coordinates: the community id is the HKDF ikm; distinct labels and
// id bytes give each entity kind its own address. All raw hkdf32 (32 bytes).
/** The Grant entity id for a member: `hkdf32(communityId, "concord/grant" ‖ 0x00 ‖ member)`. */
fun grantCoordinate(
communityId: ByteArray,
memberXOnly: ByteArray,
): ByteArray = hkdf32(communityId, buildInfo(ConcordLabels.GRANT, memberXOnly))
/** The community-wide Banlist entity id: `hkdf32(communityId, "concord/banlist" ‖ 0x00 ‖ ZERO32)`. */
fun banlistCoordinate(communityId: ByteArray): ByteArray = hkdf32(communityId, buildInfo(ConcordLabels.BANLIST, ByteArray(32)))
/** The invite-registry entity id for a creator: `hkdf32(communityId, "concord/invite-links" ‖ 0x00 ‖ creator)`. */
fun inviteLinksCoordinate(
communityId: ByteArray,
creatorXOnly: ByteArray,
): ByteArray = hkdf32(communityId, buildInfo(ConcordLabels.INVITE_LINKS, creatorXOnly))
// ---- CORD-05 invite bundle key --------------------------------------------
/**
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.concord.cord02Community
import com.vitorpamplona.quartz.concord.crypto.ConcordKeyDerivation
import com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ConcordCommunityFactoryTest {
private val owner = NostrSignerInternal(KeyPair())
@Test
fun createsSelfCertifyingCommunityWithGenesisEditions() =
runTest {
val community =
ConcordCommunityFactory.create(
ownerSigner = owner,
name = "Nostrichs",
createdAt = 1_700_000_000L,
description = "a cozy place",
relays = listOf("wss://relay.example"),
)
// community_id is the self-certifying commitment to owner + salt
assertContentEquals(
ConcordKeyDerivation.communityId(owner.pubKey.hexToByteArray(), community.ownerSalt),
community.communityId,
)
// Two genesis wraps, both authored by the Control Plane address.
assertEquals(2, community.genesisWraps.size)
community.genesisWraps.forEach {
assertEquals(ConcordStreamEnvelope.KIND_WRAP, it.kind)
assertEquals(community.controlPlane.publicKeyHex, it.pubKey)
}
// Genesis wraps open with plaintext (20014) seals, authored by the owner.
val opened = community.genesisWraps.map { ConcordStreamEnvelope.open(it, community.controlPlane) }
opened.forEach {
assertEquals(ConcordStreamEnvelope.KIND_SEAL_PLAINTEXT, it.sealKind)
assertEquals(owner.pubKey, it.author)
}
}
@Test
fun genesisFoldsToLiveCommunityStateWithGeneralChannelAndOwnerAuthority() =
runTest {
val community =
ConcordCommunityFactory.create(owner, name = "Gamers", createdAt = 1L, relays = listOf("wss://r.example"))
val state = ConcordCommunityState.fold(community.genesisEditions, community.ownerPubKey)
assertEquals("Gamers", state.metadata?.name)
assertEquals(listOf("wss://r.example"), state.metadata?.relays)
val general = state.channels[community.generalChannelIdHex]
assertNotNull(general)
assertEquals(ConcordCommunityFactory.GENERAL_CHANNEL_NAME, general.definition.name)
assertFalse(general.definition.private)
// The owner is supreme from genesis; no channels are private, none deleted.
assertTrue(state.authority.isOwner(owner.pubKey))
assertEquals(0L, state.authority.rank(owner.pubKey))
assertFalse(state.dissolved)
}
@Test
fun differentCommunitiesFromSameOwnerHaveDistinctIds() =
runTest {
val a = ConcordCommunityFactory.create(owner, "A", 1L)
val b = ConcordCommunityFactory.create(owner, "B", 1L)
// distinct salts ⇒ distinct ids (one owner, many communities)
assertFalse(a.communityIdHex == b.communityIdHex)
assertFalse(a.communityRoot.toHexKey() == b.communityRoot.toHexKey())
}
}