From 7727cfa4326fb32dc08797a436010acc00e40320 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Fri, 28 Aug 2026 15:18:26 -0400 Subject: [PATCH] =?UTF-8?q?send=20path=20uses=20the=20device=20directory,?= =?UTF-8?q?=20not=20the=20prekey=20route=20=E2=80=94=20every=20send=20burn?= =?UTF-8?q?ed=20bundle=20quota=20and=20popped=20OPKs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../io/flamenet/client/Conversations.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/client/src/main/kotlin/io/flamenet/client/Conversations.kt b/client/src/main/kotlin/io/flamenet/client/Conversations.kt index 601dda7..f13b5e8 100644 --- a/client/src/main/kotlin/io/flamenet/client/Conversations.kt +++ b/client/src/main/kotlin/io/flamenet/client/Conversations.kt @@ -378,7 +378,33 @@ class Conversations( } private fun sendPlaintext(toUserId: Long, text: String): Sent { - val bundles = relay.fetchKeys(toUserId) + // 🔴 **The DIRECTORY, not the key route.** `/e2e/keys` pops a one-time + // prekey per device and is metered per contact pair — calling it on + // EVERY send drained both, and an hour of ordinary chatting ended in + // "Too many attempts". The directory costs nothing; full bundles are + // fetched only when some device has no session yet, which is the one + // moment X3DH actually needs them. + val directory = relay.listDevices(toUserId) + val stubs = (0 until directory.length()).map { i -> + val o = directory.getJSONObject(i) + PeerBundle( + deviceId = o.optString("device_id"), + identityDH = runCatching { Wire.unb64(o.optString("ik_dh")) } + .getOrDefault(ByteArray(0)), + identitySigning = runCatching { Wire.unb64(o.optString("ik_sig")) } + .getOrDefault(ByteArray(0)), + signedPreKeyId = 0, + signedPreKey = ByteArray(0), + signedPreKeySignature = ByteArray(0), + oneTimePreKeyId = 0, + oneTimePreKey = null, + proto = o.optInt("proto", 1), + ) + } + val needsEstablish = stubs.any { + it.deviceId != identity.deviceId && existingSession(it.deviceId) == null + } + val bundles = if (needsEstablish) relay.fetchKeys(toUserId) else stubs guardIdentity(toUserId, bundles) val delivered = mutableListOf() val failed = LinkedHashMap()