mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix: resolve a Concord invite coordinate newest-wins on the live branch too
`ConcordInviteBundle.classify` resolved a revocation tombstone by `createdAt` (newest wins, per CORD-05 §2 replaceable semantics) but picked the live bundle with `firstNotNullOfOrNull` — i.e. whichever copy happened to decrypt first in relay arrival order. `wraps` comes straight off `fetchAll` at every call site (`joinConcordViaInvite`, `amy concord join`, `refreshConcordInviteLinks`), and `fetchAll` gives no ordering guarantee. A Refounding re-mints every live link at its OWN coordinate carrying the new epoch's root. Until now, a relay still serving the pre-Refounding bundle could hand a joiner the root of the epoch the community had just left: they would join, post into planes nobody reads, and see no error explaining why. The file's own KDoc and the CLI's redeem comment both already claimed newest-wins here. Sorting newest-first also decrypts fewer bundles in the common case, since the current edition is now tried first. Regression test covers both fetch orders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcaCNiqN8T9izve4AXcake
This commit is contained in:
+9
-2
@@ -144,7 +144,7 @@ object ConcordInviteBundle {
|
||||
* [InviteBundleStatus] under CORD-05 §2 replaceable semantics. The newest event
|
||||
* wins: a `vsk=9` revocation tombstone marks the link [InviteBundleStatus.Revoked]
|
||||
* even when an older, still-openable bundle is also present (so a stale relay copy
|
||||
* can't resurrect a retired link). Otherwise the first `vsk=6` bundle that opens +
|
||||
* can't resurrect a retired link). Otherwise the **newest** `vsk=6` bundle that opens +
|
||||
* validates with [token] is [InviteBundleStatus.Live]; anything else present is
|
||||
* [InviteBundleStatus.Unreadable], and an empty set is [InviteBundleStatus.Absent].
|
||||
*
|
||||
@@ -160,7 +160,14 @@ object ConcordInviteBundle {
|
||||
): InviteBundleStatus {
|
||||
val newest = wraps.maxByOrNull { it.createdAt } ?: return InviteBundleStatus.Absent
|
||||
if (newest.tags.vsk() == ControlEntityKind.INVITE_REVOKED) return InviteBundleStatus.Revoked
|
||||
val invite = wraps.firstNotNullOfOrNull { parse(it, token)?.takeIf { i -> validate(i) } }
|
||||
// Newest-first, not fetch order. [wraps] arrives straight off `fetchAll`, i.e. in relay
|
||||
// arrival order, so opening whichever copy decrypts first is a coin flip between editions.
|
||||
// That matters because a Refounding re-mints every live link at its OWN coordinate with the
|
||||
// new epoch's root: a relay still serving the pre-Refounding bundle would otherwise hand the
|
||||
// joiner the root of the epoch the community just left, and they would join, see planes
|
||||
// nobody reads, and get no error saying why. The revocation branch above already resolves
|
||||
// newest-wins; the live branch has to agree with it.
|
||||
val invite = wraps.sortedByDescending { it.createdAt }.firstNotNullOfOrNull { parse(it, token)?.takeIf { i -> validate(i) } }
|
||||
return when {
|
||||
invite == null -> InviteBundleStatus.Unreadable
|
||||
isExpired(invite, nowMs) -> InviteBundleStatus.Expired(invite)
|
||||
|
||||
+22
@@ -96,6 +96,28 @@ class ConcordInviteClassifyTest {
|
||||
assertEquals(InviteBundleStatus.Revoked, ConcordInviteBundle.classify(listOf(tombstone, minted.bundleEvent), minted.token))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remintAtTheSameCoordinateWinsOverAStaleCopy() =
|
||||
runTest {
|
||||
// The Refounding path (CORD-05 §1): every live link is re-minted at its own coordinate
|
||||
// carrying the new epoch's root. A relay that still serves the pre-Refounding bundle
|
||||
// must not be able to hand a joiner the epoch the community just left.
|
||||
val community = ConcordCommunityFactory.create(owner, "Nostrichs", createdAt = 1L, relays = listOf("wss://relay.example"))
|
||||
val before = inviteFor(community).copy(communityRoot = "aa".repeat(32), rootEpoch = 1L)
|
||||
val minted = ConcordInviteBundle.mintLink("https://vector.chat", before, createdAt = 1000L)
|
||||
|
||||
val after = before.copy(communityRoot = "bb".repeat(32), rootEpoch = 2L)
|
||||
val remint = ConcordInviteBundle.build(minted.linkSignerPrivKey, minted.token, after, createdAt = 2000L)
|
||||
|
||||
// Both fetch orders must resolve to the re-mint — `fetchAll` gives no ordering guarantee.
|
||||
listOf(listOf(minted.bundleEvent, remint), listOf(remint, minted.bundleEvent)).forEach { wraps ->
|
||||
val status = ConcordInviteBundle.classify(wraps, minted.token)
|
||||
assertTrue(status is InviteBundleStatus.Live)
|
||||
assertEquals("bb".repeat(32), status.invite.communityRoot)
|
||||
assertEquals(2L, status.invite.rootEpoch)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownSubKindIsUnreadable() =
|
||||
runTest {
|
||||
|
||||
Reference in New Issue
Block a user