feat(concord): add control edition parsing and chain folding

- ControlEdition: parses a verified kind-3308 rumor's vsk/eid/ev/ep/vac tags into
  a typed edition, computes its domain-separated edition hash, and rejects
  malformed editions (bad kind, missing eid/ev, unknown vsk, bad hex)
- AuthorityCitation: the vac Grant pin an actor claims rank under
- EditionFold: folds editions into each entity's current head — genesis
  anchoring, intact-chain / no-downgrade advancement, deterministic lower-rumor-id
  tie-break for convergence (authority-weighted tie-break layered in the resolver)

Tests cover tag parsing + hash, malformed rejection, genesis prev handling,
order-independent chain walk, downgrade/broken-chain refusal, tie-break, and the
hold-without-genesis case. 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 21:51:35 +00:00
parent 828792eb40
commit c865d0f4eb
3 changed files with 361 additions and 0 deletions
@@ -0,0 +1,124 @@
/*
* 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.crypto.EditionHash
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.firstTagValue
import com.vitorpamplona.quartz.nip01Core.core.firstTagValueAsLong
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
/**
* The exact Grant edition an actor claims authority under (the `vac` tag,
* CORD-04). Verifiers block until they have synced this Grant, then resolve the
* actor's rank against it — a demoted member's stale citation is dropped.
*/
class AuthorityCitation(
val grantId: ByteArray,
val grantVersion: Long,
val grantHash: ByteArray,
)
/**
* A single Control Plane edition (a kind-3308 author rumor, CORD-02/04).
*
* Editions are versioned, chainable state: each carries an entity id ([entityId],
* `eid`), a monotonically increasing [version] (`ev`), the [prevHash] of the
* previous edition (`ep`, absent for the genesis edition), an optional
* [authorityCitation] (`vac`) pinning the Grant the [author] acts under, and the
* verbatim entity [content]. Its identity is [hash] — a domain-separated hash of
* exactly those fields (see [EditionHash]) — which the next edition cites in `ep`,
* forming an unforgeable chain.
*/
class ControlEdition(
val entityKind: ControlEntityKind,
val entityId: ByteArray,
val version: Long,
val prevHash: ByteArray?,
val authorityCitation: AuthorityCitation?,
val content: String,
/** The actor's real pubkey (the seal/rumor author). */
val author: String,
/** The rumor's event id — the deterministic tie-break key at equal version. */
val rumorId: String,
val createdAt: Long,
) {
/** Domain-separated edition identity; the next edition's `ep` cites this. */
val hash: ByteArray by lazy { EditionHash.hash(entityId, version, prevHash, content) }
val entityIdHex: String get() = entityId.toHexKey()
val hashHex: String get() = hash.toHexKey()
companion object {
const val TAG_VSK = "vsk"
const val TAG_EID = "eid"
const val TAG_EV = "ev"
const val TAG_EP = "ep"
const val TAG_VAC = "vac"
/**
* Parses a decrypted, verified kind-3308 [rumor] (its [author] is the
* rumor's pubkey) into a [ControlEdition], or returns null if it is not a
* well-formed control edition (unknown/absent `vsk`, missing `eid`/`ev`,
* malformed hex, …) so the caller drops it rather than folding garbage.
*/
fun fromRumor(
rumor: Event,
author: String = rumor.pubKey,
): ControlEdition? {
if (rumor.kind != ConcordKinds.CONTROL) return null
val entityKind = rumor.tags.firstTagValue(TAG_VSK)?.let { ControlEntityKind.of(it) } ?: return null
val entityId =
rumor.tags
.firstTagValue(TAG_EID)
?.hexToByteArrayOrNull()
?.takeIf { it.size == 32 } ?: return null
val version = rumor.tags.firstTagValueAsLong(TAG_EV) ?: return null
if (version < 0) return null
val prevValue = rumor.tags.firstTagValue(TAG_EP)
val prevHash = if (prevValue.isNullOrBlank()) null else prevValue.hexToByteArrayOrNull()?.takeIf { it.size == 32 } ?: return null
val vacTag = rumor.tags.firstOrNull { it.size >= 4 && it[0] == TAG_VAC }
val vac =
vacTag?.let {
val gid = it[1].hexToByteArrayOrNull()?.takeIf { b -> b.size == 32 } ?: return null
val gver = it[2].toLongOrNull() ?: return null
val ghash = it[3].hexToByteArrayOrNull()?.takeIf { b -> b.size == 32 } ?: return null
AuthorityCitation(gid, gver, ghash)
}
return ControlEdition(
entityKind = entityKind,
entityId = entityId,
version = version,
prevHash = prevHash,
authorityCitation = vac,
content = rumor.content,
author = author,
rumorId = rumor.id,
createdAt = rumor.createdAt,
)
}
}
}
@@ -0,0 +1,81 @@
/*
* 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.nip01Core.core.toHexKey
/**
* Folds Control Plane editions into the current head of each entity (CORD-04
* §Edition Hashing & Chain Integrity).
*
* Rules enforced here:
* - **Genesis anchoring** — a chain starts at the lowest-version edition with no
* `ep` (prev hash). Without a genesis, the entity has no accepted head (we
* hold rather than trust an unrooted later edition).
* - **Intact chain / no downgrades** — the head advances to `version + 1` only
* when that edition's `ep` cites the current head's [ControlEdition.hash].
* Lower or non-chaining versions are ignored.
* - **Deterministic convergence** — at equal version, ties break on the lower
* rumor id, so every honest client folds to the same head.
*
* Authority-weighted tie-break ("authority first, then the lower rumor id") and
* the owner-rooted `vac` verification are applied by the resolver layer on top of
* this structural fold; this class is purely the chain walk.
*/
object EditionFold {
/** Groups mixed [editions] by entity id and folds each to its head. */
fun fold(editions: Collection<ControlEdition>): Map<String, ControlEdition> {
val byEntity = editions.groupBy { it.entityIdHex }
val out = HashMap<String, ControlEdition>(byEntity.size)
for ((entity, list) in byEntity) {
foldEntity(list)?.let { out[entity] = it }
}
return out
}
/** Folds the editions of a single entity into its current head, or null. */
fun foldEntity(editions: List<ControlEdition>): ControlEdition? {
if (editions.isEmpty()) return null
// Index editions by version, keeping the tie-break winner where several
// share a version (lower rumor id wins).
val byVersion = HashMap<Long, MutableList<ControlEdition>>()
for (e in editions) byVersion.getOrPut(e.version) { ArrayList() }.add(e)
// Genesis: lowest version with no prev hash. Prefer the tie-break winner.
var head =
editions
.filter { it.prevHash == null }
.minWithOrNull(compareBy({ it.version }, { it.rumorId }))
?: return null
// Walk the chain upward while the next version chains from the current head.
while (true) {
val next =
byVersion[head.version + 1]
?.filter { it.prevHash != null && it.prevHash.toHexKey() == head.hashHex }
?.minByOrNull { it.rumorId }
?: break
head = next
}
return head
}
}
@@ -0,0 +1,156 @@
/*
* 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.crypto.EditionHash
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class ControlEditionTest {
private val author = KeyPair().pubKey.toHexKey()
private val eid = ByteArray(32) { 0xAB.toByte() }
private fun edition(
version: Long,
prevHash: ByteArray?,
content: String,
rumorId: String,
) = ControlEdition(
entityKind = ControlEntityKind.CHANNEL,
entityId = eid,
version = version,
prevHash = prevHash,
authorityCitation = null,
content = content,
author = author,
rumorId = rumorId,
createdAt = 1_700_000_000L + version,
)
// ---- fromRumor ------------------------------------------------------------
@Test
fun fromRumorParsesTagsAndComputesHash() {
val prev = ByteArray(32) { 0x01 }
val grantId = ByteArray(32) { 0x02 }
val grantHash = ByteArray(32) { 0x03 }
val content = """{"member":"aa","role_ids":["bb"]}"""
val tags =
arrayOf(
arrayOf("vsk", "3"),
arrayOf("eid", eid.toHexKey()),
arrayOf("ev", "4"),
arrayOf("ep", prev.toHexKey()),
arrayOf("vac", grantId.toHexKey(), "2", grantHash.toHexKey()),
)
val rumor = RumorAssembler.assembleRumor<Event>(author, 1_700_000_000L, ConcordKinds.CONTROL, tags, content)
val ed = ControlEdition.fromRumor(rumor)
assertNotNull(ed)
assertEquals(ControlEntityKind.GRANT, ed.entityKind)
assertContentEquals(eid, ed.entityId)
assertEquals(4, ed.version)
assertContentEquals(prev, ed.prevHash)
assertEquals(author, ed.author)
assertEquals(rumor.id, ed.rumorId)
assertContentEquals(EditionHash.hash(eid, 4, prev, content), ed.hash)
assertNotNull(ed.authorityCitation)
assertContentEquals(grantId, ed.authorityCitation.grantId)
assertEquals(2, ed.authorityCitation.grantVersion)
}
@Test
fun fromRumorRejectsMalformed() {
// wrong kind
assertNull(ControlEdition.fromRumor(RumorAssembler.assembleRumor<Event>(author, 1L, 9, arrayOf(arrayOf("vsk", "0")), "{}")))
// missing eid
assertNull(
ControlEdition.fromRumor(
RumorAssembler.assembleRumor<Event>(author, 1L, ConcordKinds.CONTROL, arrayOf(arrayOf("vsk", "0"), arrayOf("ev", "0")), "{}"),
),
)
// unknown vsk (bit 7 retired)
assertNull(
ControlEdition.fromRumor(
RumorAssembler.assembleRumor<Event>(
author,
1L,
ConcordKinds.CONTROL,
arrayOf(arrayOf("vsk", "7"), arrayOf("eid", eid.toHexKey()), arrayOf("ev", "0")),
"{}",
),
),
)
}
@Test
fun genesisHasNullPrevWhenEpAbsent() {
val tags = arrayOf(arrayOf("vsk", "2"), arrayOf("eid", eid.toHexKey()), arrayOf("ev", "0"))
val ed = ControlEdition.fromRumor(RumorAssembler.assembleRumor<Event>(author, 1L, ConcordKinds.CONTROL, tags, """{"name":"general"}"""))
assertNotNull(ed)
assertNull(ed.prevHash)
}
// ---- fold -----------------------------------------------------------------
@Test
fun foldWalksIntactChainToHead() {
val v0 = edition(0, null, """{"name":"general"}""", "id0")
val v1 = edition(1, v0.hash, """{"name":"lounge"}""", "id1")
val v2 = edition(2, v1.hash, """{"name":"lobby"}""", "id2")
// order shuffled to prove fold is order-independent
val head = EditionFold.foldEntity(listOf(v2, v0, v1))
assertEquals(v2.rumorId, head?.rumorId)
}
@Test
fun foldStopsAtBreakAndRefusesDowngrade() {
val v0 = edition(0, null, """{"name":"general"}""", "id0")
// v2 present but v1 missing ⇒ head cannot advance past v0
val v2 = edition(2, ByteArray(32) { 0x09 }, """{"name":"lobby"}""", "id2")
assertEquals(v0.rumorId, EditionFold.foldEntity(listOf(v0, v2))?.rumorId)
// v1 with a prev that does not chain from v0 is ignored
val badV1 = edition(1, ByteArray(32) { 0x07 }, """{"name":"x"}""", "id1")
assertEquals(v0.rumorId, EditionFold.foldEntity(listOf(v0, badV1))?.rumorId)
}
@Test
fun foldTieBreaksOnLowerRumorId() {
val a = edition(0, null, """{"name":"a"}""", "aaa")
val b = edition(0, null, """{"name":"b"}""", "bbb")
assertEquals("aaa", EditionFold.foldEntity(listOf(b, a))?.rumorId)
}
@Test
fun foldWithoutGenesisReturnsNull() {
val v1 = edition(1, ByteArray(32) { 0x05 }, """{"name":"x"}""", "id1")
assertNull(EditionFold.foldEntity(listOf(v1)))
}
}