mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix: don't cap Concord invite links at 3 bootstrap relays
`ConcordInviteLink.encodeFragment` refused to encode more than `MAX_RELAYS = 3` bootstrap relays, but both mint call sites — `AccountConcordActions.mintConcordInvite` and `amy concord invite` — hand it the community's full relay list. A community with more than three relays therefore blew up the invite button with `IllegalArgumentException: at most 3 relays, was 5`. The cap was self-imposed: nothing in the fragment format needs it. The layout is `[version][flags][count][relays...][token:16]` and the relay count is a whole byte, so the format's own ceiling is 255. `MAX_RELAYS` is deleted and the only remaining guard is that ceiling — without it a 256-relay list would wrap the count byte to 0 and silently strand every relay in the fragment. The stock set still collapses to flag `0x01` and zero relay bytes, so the common invite is unchanged in length. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WcaCNiqN8T9izve4AXcake
This commit is contained in:
+14
-7
@@ -53,15 +53,14 @@ class ParsedInviteLink(
|
||||
* `(33301, link_signer_pubkey, d="")`. The `#fragment` is **never sent to any
|
||||
* server**: it is base64url of `[version=4][flags][relays?][token:16]`, carrying
|
||||
* the 16-byte unlock token (→ [com.vitorpamplona.quartz.concord.crypto
|
||||
* .ConcordKeyDerivation.inviteBundleKey]) and, when flag `0x01` is unset, up to
|
||||
* three bootstrap relays encoded against [InviteRelayDictionary].
|
||||
* .ConcordKeyDerivation.inviteBundleKey]) and, when flag `0x01` is unset, the
|
||||
* bootstrap relays encoded against [InviteRelayDictionary].
|
||||
*
|
||||
* Pinned to the Concord v2 reference client for interop.
|
||||
*/
|
||||
object ConcordInviteLink {
|
||||
const val VERSION = 4
|
||||
const val FLAG_STOCK_RELAYS = 0x01
|
||||
const val MAX_RELAYS = 3
|
||||
|
||||
private const val MARKER_WSS_HOST = 0
|
||||
private const val MARKER_FULL_URL = 255
|
||||
@@ -70,8 +69,13 @@ object ConcordInviteLink {
|
||||
|
||||
/**
|
||||
* Encodes the fragment for [token] and optional [relays]. Passing null or the
|
||||
* exact stock set uses flag `0x01` and emits no relay bytes; otherwise up to
|
||||
* [MAX_RELAYS] relays are encoded (dictionary id, `wss://` host, or full URL).
|
||||
* exact stock set uses flag `0x01` and emits no relay bytes; otherwise every
|
||||
* relay is encoded (dictionary id, `wss://` host, or full URL).
|
||||
*
|
||||
* The only ceiling is the format's own: the relay count is a single byte, so at
|
||||
* most 255 relays fit. Each carries its own byte cost, so a long list makes a long
|
||||
* link — pick relays that can actually serve the bundle rather than pasting a
|
||||
* whole relay list in.
|
||||
*/
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun encodeFragment(
|
||||
@@ -86,7 +90,7 @@ object ConcordInviteLink {
|
||||
if (useStock) {
|
||||
out.add(FLAG_STOCK_RELAYS.toByte())
|
||||
} else {
|
||||
require(relays.size <= MAX_RELAYS) { "at most $MAX_RELAYS relays, was ${relays.size}" }
|
||||
require(relays.size <= 255) { "relay count must fit in one byte, was ${relays.size}" }
|
||||
out.add(0)
|
||||
out.add(relays.size.toByte())
|
||||
for (r in relays) {
|
||||
@@ -157,7 +161,10 @@ object ConcordInviteLink {
|
||||
return InviteFragment(bytes.copyOfRange(pos, pos + TOKEN_LEN), relays, usedStock)
|
||||
}
|
||||
|
||||
/** Builds a full shareable invite URL under [base]. */
|
||||
/**
|
||||
* Builds a full shareable invite URL under [base], carrying every relay in [relays]
|
||||
* as the bootstrap set (or the stock flag when null / exactly the stock set).
|
||||
*/
|
||||
fun buildUrl(
|
||||
base: String,
|
||||
linkSignerPubKey: String,
|
||||
|
||||
+40
@@ -66,6 +66,46 @@ class ConcordInviteLinkTest {
|
||||
assertEquals(relays, frag.relays)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildUrlCarriesEveryRelayOfAnOversizedList() {
|
||||
// A community with five relays used to crash the mint ("at most 3 relays, was 5").
|
||||
// There is no cap below the format's own, so all five make the round trip.
|
||||
val relays =
|
||||
listOf(
|
||||
"wss://one.example",
|
||||
"wss://two.example",
|
||||
"wss://three.example",
|
||||
"wss://four.example",
|
||||
"wss://five.example",
|
||||
)
|
||||
val parsed = ConcordInviteLink.parseUrl(ConcordInviteLink.buildUrl("https://vector.chat", signer, token, relays))
|
||||
assertNotNull(parsed)
|
||||
assertEquals(relays, parsed.fragment.relays)
|
||||
assertContentEquals(token, parsed.fragment.token)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildUrlKeepsTheFourStockRelaysAsTheStockFlag() {
|
||||
// The stock set still collapses to flag 0x01 and zero relay bytes rather than
|
||||
// being spelled out as four dictionary ids.
|
||||
val parsed = ConcordInviteLink.parseUrl(ConcordInviteLink.buildUrl("https://vector.chat", signer, token, InviteRelayDictionary.STOCK))
|
||||
assertNotNull(parsed)
|
||||
assertTrue(parsed.fragment.usedStockRelays)
|
||||
assertEquals(InviteRelayDictionary.STOCK, parsed.fragment.relays)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodesTheLargestRelayListTheCountByteCanHold() {
|
||||
// 255 is the format ceiling, not a policy one: the relay count is a single byte.
|
||||
val relays = List(255) { "wss://relay$it.example" }
|
||||
val frag = ConcordInviteLink.decodeFragment(ConcordInviteLink.encodeFragment(token, relays))
|
||||
assertEquals(relays, frag.relays)
|
||||
assertContentEquals(token, frag.token)
|
||||
|
||||
// One more would silently wrap the count byte to 0 and strand every relay, so it throws.
|
||||
assertFailsWith<IllegalArgumentException> { ConcordInviteLink.encodeFragment(token, relays + "wss://overflow.example") }
|
||||
}
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun rejectsWrongVersion() {
|
||||
|
||||
Reference in New Issue
Block a user