macltok request, pre-share room keys before send
Problem
Sending into an encrypted room is slow in malice (and Element X), and under federation lag it can take over a minute. Clients on other SDKs feel instant because they do the key work before the user presses send.
matrix-sdk 0.18 does all of it inline on the send path. SendRawMessageLikeEvent::into_future (matrix-sdk-0.18.0/src/room/futures.rs:190-245) calls the private ensure_room_encryption_ready (futures.rs:523-537), which runs, in the room’s single send-queue worker and before the /send PUT
sync_members()if!are_members_synced()query_keys_for_untracked_or_dirty_users(), a/keys/querythat the homeserver fans out over federationpreshare_room_key()(room/mod.rs:2396-2431), which claims one-time keys (/keys/claim, 10s server timeout, also federated) and then sends the megolm key to every device as to-device messages, awaiting each
Every request uses the default 30s timeout, and the send queue retries with short_retry (3 attempts, exponential backoff). A slow remote server stalls the send and everything queued behind it in that room.
For comparison, matrix-dart-sdk calls prepareOutboundGroupSession unawaited from Client.setTyping, so FluffyChat has usually finished steps 1 to 3 by the time the user hits send. matrix-js-sdk exposes CryptoApi.prepareToEncrypt(room) for the same purpose. matrix-sdk has the primitive but keeps it private, with this at room/mod.rs:2392
// TODO: expose this publicly so people can pre-share a group session if
// e.g. a user starts to type a message for a room.What is reachable today
Checked against the 0.18 sources. Only the first step is public.
| step | public from matrix_sdk? |
|---|---|
Room::sync_members, are_members_synced | yes |
Room::query_keys_for_untracked_or_dirty_users | no, private. Encryption::request_user_identity(user_id) does a per-user /keys/query |
Client::claim_one_time_keys | no, pub(crate) |
Room::share_room_key, Room::preshare_room_key | no, private. OlmMachine::share_room_key is pub but Client::olm_machine() is pub(crate) |
ensure_room_encryption_ready | no, private free fn |
There is no public encrypt-without-send path either. Every caller of ensure_room_encryption_ready goes on to send.
Request
Expose a command that runs the send path’s encryption prep without sending anything, fire and forget, deduplicated per room.
impl ClientHandle {
/// no-op for unencrypted rooms, coalesces with an in-flight prepare for the same room
pub async fn prepare_to_send(&self, room_id: OwnedRoomId) -> Result<(), Error>;
}malice would call it when the user starts typing in a room (alongside a typing notice) and probably when a room is opened. A later send then finds members synced, device lists fresh and the outbound session already shared, so only the /send PUT remains. Correctness is unchanged, since the send path still runs ensure_room_encryption_ready and redoes whatever went stale in between (a member joining after the prepare, say).
Suggested route, in order of preference
- Upstream a small matrix-rust-sdk PR making
Room::preshare_room_keypublic, or addingpub async fn Room::prepare_encryption(&self)that wrapsensure_room_encryption_ready. The TODO invites exactly this. Until it is released macltok could carry it via a[patch.crates-io]git fork, if that fits its dependency policy. - A partial warm-up using only public API while the PR is pending.
sync_members()when not synced, thenrequest_user_identityfor members whose device lists are unknown, capped (members can be many and each query is a federated round trip). This removes step 1 and the/keys/querypart of step 2 from the send path, which is the part federation lag hurts most. Steps 3 (/keys/claimand the to-device share) stay on the send path. It does not know which users are dirty (onlytracked_users()is public), so it can over-query. Worth it only if option 1 will take a while.
malice will pair it with the existing ClientHandle::set_typing(room_id, bool) (transport.rs:357), so no new typing API is needed.
Not requested
A “send without waiting for fresh device lists” mode. It would encrypt to stale device lists, so new members and new devices miss the key and see undecryptable messages. Pre-sharing gives the same latency without that.