fix(concord): terminal channel deletion and the 64-byte name cap

CORD-03 §2 (I14):

- Deletion is terminal across the whole accepted edition set: any
  MANAGE_CHANNELS-gated channel edition with `deleted: true` retires the
  channel even if a later edition "restores" it (Armada `everDeleted`).
- The channel gate enforces the name rule (non-empty, at most 64 UTF-8
  bytes): an edition breaking it is unauthorized and the fold falls back
  to the previous candidate. ConcordModeration.defineChannel refuses to
  mint one.

ControlPlaneVersionExhaustionTest's channel case now poisons with a
max-version rename: a max-version delete from an authorized holder is
terminal by design now, while a max-version edition still must not pin
the channel's content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmuzkD5qdqgv6dMeRMoC5N
This commit is contained in:
Claude
2026-09-29 15:21:15 +00:00
parent f231b95384
commit e94bb2f637
5 changed files with 183 additions and 9 deletions
@@ -162,6 +162,9 @@ object ConcordModeration {
citation: AuthorityCitation? = null,
owner: HexKey,
): Event {
// Every reader drops an edition naming an empty or over-cap Channel (CORD-03 §2), so
// refuse to mint one rather than publish an edition nobody will honor.
require(channel.hasValidName()) { "Channel name must be 1..${ChannelEntity.NAME_MAX_BYTES} UTF-8 bytes" }
val (version, prev) = versioning(current, channelId, owner)
val content = contentOver(ChannelEntity.serializer(), channel, current, channelId, owner)
return wrap(actor, controlPlane, ControlEntityKind.CHANNEL, channelId, version, prev, content, createdAt, citation)
@@ -171,8 +171,24 @@ data class ConcordCommunityState(
?.let { ConcordJson.decodeOrNull<MetadataEntity>(it.content) }
// Channels are gated by MANAGE_CHANNELS, per channel entity, dropping the tombstoned ones.
// The gate also enforces the name rule (non-empty, <= 64 UTF-8 bytes, CORD-03 §2): an
// edition breaking it is unauthorized and the fold falls back to the previous candidate.
val channelEditions = editions.filter { it.entityKind == ControlEntityKind.CHANNEL }
val channelGate = { edition: ControlEdition ->
(authority.isOwner(edition.author) || authority.hasPermission(edition.author, ConcordPermissions.MANAGE_CHANNELS)) &&
ConcordJson.decodeOrNull<ChannelEntity>(edition.content)?.hasValidName() == true
}
// Deletion is terminal (CORD-03 §2): any gated edition anywhere in a channel's accepted
// chain that says `deleted` retires it for good, even if a later edition "restores" it —
// members may already have discarded its keys, so a resurrection would split them.
val everDeleted =
channelEditions
.filter { edition ->
channelGate(edition) && ConcordJson.decodeOrNull<ChannelEntity>(edition.content)?.deleted == true
}.mapTo(HashSet()) { it.entityIdHex }
val channels = LinkedHashMap<String, ConcordChannel>()
for (head in foldGatedBy(ControlEntityKind.CHANNEL, ConcordPermissions.MANAGE_CHANNELS).values) {
for (head in EditionFold.foldGated(channelEditions, floors, snapshot = snapshot, gate = channelGate).values) {
if (head.entityIdHex in everDeleted) continue
val def = ConcordJson.decodeOrNull<ChannelEntity>(head.content) ?: continue
if (def.deleted) continue
channels[head.entityIdHex] = ConcordChannel(head.entityIdHex, def)
@@ -162,7 +162,23 @@ data class ChannelEntity(
val name: String = "",
val private: Boolean = false,
val deleted: Boolean = false,
)
) {
/** True when [name] is within the protocol's name rule ([isValidName]). */
fun hasValidName(): Boolean = isValidName(name)
companion object {
/** The protocol-wide name cap, in UTF-8 bytes (CORD-03 §2, CORD-04). */
const val NAME_MAX_BYTES = 64
/**
* A Channel name must be non-empty and at most [NAME_MAX_BYTES] UTF-8 bytes. Enforced when
* building an edition and again when folding one: an edition naming an empty or over-cap
* Channel is unauthorized, and the fold falls back to the previous candidate (the reference
* client's channel gate).
*/
fun isValidName(name: String): Boolean = name.isNotEmpty() && name.encodeToByteArray().size <= NAME_MAX_BYTES
}
}
/**
* A community's Metadata content (CORD-02): display [name], optional [description], the community's
@@ -0,0 +1,112 @@
/*
* 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.ControlEdition
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEntityKind
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* CORD-03 §2 channel fold rules: deletion is terminal across the whole accepted chain, and a
* Channel name is 1..64 UTF-8 bytes — an edition breaking the name rule is unauthorized and the
* fold falls back to the previous candidate (the reference client's channel gate).
*/
class ChannelFoldConformanceTest {
private val owner = "0f".repeat(32)
private val chan = "c1".repeat(32)
private fun chained(
version: Long,
prev: ControlEdition?,
content: String,
author: String = owner,
) = ControlEdition(ControlEntityKind.CHANNEL, chan.hexToByteArray(), version, prev?.hash, null, content, author, "r-$version", version)
@Test
fun aDeletedChannelCannotBeResurrectedByALaterEdition() {
val c0 = chained(0, null, """{"name":"general"}""")
val c1 = chained(1, c0, """{"name":"general","deleted":true}""")
val c2 = chained(2, c1, """{"name":"general","deleted":false}""")
val state = ConcordCommunityState.fold(listOf(c0, c1, c2), owner)
assertNull(state.channels[chan], "a deletion anywhere in the accepted chain is terminal")
}
@Test
fun anUnauthorizedDeleteDoesNotRetireTheChannel() {
val troll = "77".repeat(32)
val c0 = chained(0, null, """{"name":"general"}""")
val forged = chained(1, c0, """{"name":"general","deleted":true}""", author = troll)
val state = ConcordCommunityState.fold(listOf(c0, forged), owner)
assertEquals("general", state.channels[chan]?.definition?.name)
}
@Test
fun anOverCapOrEmptyNameFallsBackToThePreviousEdition() {
val c0 = chained(0, null, """{"name":"general"}""")
val tooLong = chained(1, c0, """{"name":"${"x".repeat(65)}"}""")
assertEquals(
"general",
ConcordCommunityState
.fold(listOf(c0, tooLong), owner)
.channels[chan]
?.definition
?.name,
)
val empty = chained(1, c0, """{"name":""}""")
assertEquals(
"general",
ConcordCommunityState
.fold(listOf(c0, empty), owner)
.channels[chan]
?.definition
?.name,
)
// Exactly 64 bytes is fine — counted in UTF-8, so 16 four-byte emoji hit the cap too.
val atCap = chained(1, c0, """{"name":"${"x".repeat(64)}"}""")
assertEquals(
"x".repeat(64),
ConcordCommunityState
.fold(listOf(c0, atCap), owner)
.channels[chan]
?.definition
?.name,
)
}
@Test
fun theNameRuleCountsUtf8Bytes() {
val emoji = "😀" // 4 bytes in UTF-8
assertTrue(ChannelEntity.isValidName(emoji.repeat(16)))
assertFalse(ChannelEntity.isValidName(emoji.repeat(16) + "a"))
assertFalse(ChannelEntity.isValidName(""))
assertTrue(ChannelEntity(name = "general").hasValidName())
}
}
@@ -127,24 +127,51 @@ class ControlPlaneVersionExhaustionTest {
)
}
/**
* The poison here renames rather than deletes: CORD-03 §2 makes a Channel deletion by any
* authorized holder terminal across the whole accepted edition set (the reference client's
* `everDeleted`), whatever its version, so a max-version *delete* from bob now retires the
* Channel by design. What must still hold is that a max-version edition cannot pin the Channel
* to bob's content.
*/
@Test
fun oneEditionAtMaxVersionNoLongerDeletesAChannel() {
fun oneEditionAtMaxVersionNoLongerPinsAChannel() {
val channelV0 = edition(ControlEntityKind.CHANNEL, channelEntity, 0, null, """{"name":"general"}""", owner, "chan-0")
val community = communityWhereBobHolds(ConcordPermissions.of(ConcordPermissions.MANAGE_CHANNELS).toWire(), channelV0)
val floorsBefore = ConcordCommunityState.authorizedHeads(community, owner)
val poison = edition(ControlEntityKind.CHANNEL, channelEntity, Long.MAX_VALUE, channelV0.hash, """{"name":"general","deleted":true}""", bob, "chan-poison")
val poison = edition(ControlEntityKind.CHANNEL, channelEntity, Long.MAX_VALUE, channelV0.hash, """{"name":"PWNED"}""", bob, "chan-poison")
val floorsAfter = ConcordCommunityState.authorizedHeads(community + poison, owner, floorsBefore)
val repair = edition(ControlEntityKind.CHANNEL, channelEntity, 1, channelV0.hash, """{"name":"general"}""", owner, "chan-1")
val pool = community + poison + repair
assertEquals(1, ConcordCommunityState.fold(pool, owner).channels.size, "a fresh joiner still sees the channel")
assertEquals(1, ConcordCommunityState.fold(pool, owner, floorsAfter).channels.size, "and so does a client holding a floor")
assertEquals(
1,
ConcordCommunityState.fold(community + repair, owner, floorsAfter).channels.size,
"the channel survives a Refounding too",
"general",
ConcordCommunityState
.fold(pool, owner)
.channels[channelEntity]
?.definition
?.name,
"a fresh joiner follows the honest chain",
)
assertEquals(
"general",
ConcordCommunityState
.fold(pool, owner, floorsAfter)
.channels[channelEntity]
?.definition
?.name,
"and so does a client holding a floor",
)
assertEquals(
"general",
ConcordCommunityState
.fold(community + repair, owner, floorsAfter)
.channels[channelEntity]
?.definition
?.name,
"the channel stays repaired across a Refounding too",
)
}