mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix(concord): keep at most 32 retired keys per private channel
`priors` gained an entry per rotation and was never pruned. A List entry that outgrows its fragment fails every List write (joins and leaves too), which is worse than losing the oldest era's history. Keep the 32 newest epochs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c0c671663c
commit
6ba62525b3
+11
-1
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
@@ -61,6 +62,9 @@ object ConcordChannelKeyring {
|
||||
const val CHANNEL_CUTS = "channel_cuts"
|
||||
const val PRIORS = "priors"
|
||||
|
||||
/** How many retired keys a channel keeps in `priors`, newest epochs first. */
|
||||
const val MAX_PRIORS = 32
|
||||
|
||||
private val HEX64 = Regex("^[0-9a-fA-F]{64}$")
|
||||
|
||||
/** The current key held for [channelIdHex], or null (a keyless listing is not a key). */
|
||||
@@ -185,9 +189,15 @@ object ConcordChannelKeyring {
|
||||
JsonObject(mapOf("key" to JsonPrimitive(k), "epoch" to JsonPrimitive(epoch), "retired_at" to JsonPrimitive(at)))
|
||||
}
|
||||
if (added.isEmpty()) return extras
|
||||
return JsonObject(extras + (PRIORS to JsonArray(existing + added)))
|
||||
// Bounded: every rotation adds one, and a List entry that outgrows its fragment fails every
|
||||
// List write (joins and leaves included), which is worse than losing the oldest era's history.
|
||||
val all = existing + added
|
||||
val kept = if (all.size <= MAX_PRIORS) all else all.sortedByDescending { priorEpoch(it) }.take(MAX_PRIORS)
|
||||
return JsonObject(extras + (PRIORS to JsonArray(kept)))
|
||||
}
|
||||
|
||||
private fun priorEpoch(prior: JsonElement): Long = ((prior as? JsonObject)?.get("epoch") as? JsonPrimitive)?.longOrNull ?: Long.MIN_VALUE
|
||||
|
||||
/**
|
||||
* [entry] after a rotation to [cutEpoch] cut this member from [channelIdHex] (CORD-06 §2): the
|
||||
* key leaves `channels` and the cut is recorded, so no older key can come back.
|
||||
|
||||
+11
@@ -145,6 +145,17 @@ class ConcordChannelKeyringTest {
|
||||
assertFalse(ConcordChannelKeyring.historicalKeys(rotated, chan).any { it.key == "13".repeat(32) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun priorsKeepOnlyTheNewestEpochs() {
|
||||
var e = entry(listOf(PrivateChannelKey(chan, 1.toString(16).padStart(64, '0'), 1, "mods")))
|
||||
for (epoch in 2L..(ConcordChannelKeyring.MAX_PRIORS + 10)) {
|
||||
e = assertNotNull(ConcordChannelKeyring.withRotatedKey(e, chan, epoch.toString(16).padStart(64, '0'), epoch, retiredAt = epoch))
|
||||
}
|
||||
val epochs = ConcordChannelKeyring.historicalKeys(e, chan).map { it.epoch }
|
||||
assertEquals(ConcordChannelKeyring.MAX_PRIORS, epochs.size)
|
||||
assertEquals(ConcordChannelKeyring.MAX_PRIORS + 9L, epochs.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aRotationKeepsEveryEarlierKeyReadableAfterARoundTrip() {
|
||||
// A channel created after the join has no `seed` anchor: the List is the only place its
|
||||
|
||||
Reference in New Issue
Block a user