Skip to content

Defer key bundle downloads on server-initiated joins (MSC4509) - #5426

Open
ara4n wants to merge 4 commits into
developfrom
matthew/auto-join-key-bundle
Open

Defer key bundle downloads on server-initiated joins (MSC4509)#5426
ara4n wants to merge 4 commits into
developfrom
matthew/auto-join-key-bundle

Conversation

@ara4n

@ara4n ara4n commented Jul 16, 2026

Copy link
Copy Markdown
Member

A server-initiated join (e.g. an auto-accepted knock) lands on all of our user's devices in the same sync instant, so each would eagerly download the MSC4268 room key bundle. Instead, only download it when the server designates this device as the claimer (a new org.matrix.msc4509.key_bundle_claim to-device message, only trusted from our own user), or lazily upon the first undecryptable event in the room. Client-initiated joins via joinRoom keep the eager behaviour.

The MSC4509 behaviour is unstable, so it is opt-in via the new unstableMSC4509KeyBundleClaim client option: when unset (the default), key_bundle_claim messages are ignored and server-initiated joins keep the eager per-device download. Element Web enables it behind a share_history_on_autojoin feature flag (PR to follow).

@ara4n
ara4n requested a review from a team as a code owner July 16, 2026 11:27
@ara4n
ara4n requested review from MidhunSureshR and t3chguy and removed request for a team July 16, 2026 11:27
@t3chguy
t3chguy requested review from a team, andybalaam and uhoreg July 16, 2026 15:07
@andybalaam

Copy link
Copy Markdown
Member

@ara4n where can we read about this org.matrix.mscxxxx.key_bundle_claim? Is this something you are proposing merging now, or e.g. to support an in-progress MSC?

@richvdh

richvdh commented Jul 16, 2026

Copy link
Copy Markdown
Member

msc4509 it appears.

Please can such unspecced features be gated behind some sort of opt-in from the application (which in turn should gate it behind a labs flag)

ara4n added 3 commits July 24, 2026 02:03
If the server joins us to a room we were invited to (e.g. auto-accepting
a knock, synapse#16307), joinRoom never runs, so the MSC4268 key bundle
sent by the inviter was silently dropped ('Not yet accepting key bundle
for room where we are not awaiting a bundle') and pre-join history stayed
undecryptable. Watch our own invite->join transitions and run the same
markRoomAsPendingKeyBundle/maybeAcceptKeyBundle dance as joinRoom.
An accepted knock can be auto-joined straight past the invited state
(the invite coalesced out of sync), and the invite-time record is
in-memory only, so recover the inviter from the join event's
unsigned.prev_sender — the same Synapse extension getDMInviter already
relies on. Mirrors the equivalent rust-sdk fix.
A server-initiated join (e.g. an auto-accepted knock) lands on all of
our user's devices in the same sync instant, so each would eagerly
download the MSC4268 room key bundle. Instead, only download it when
the server designates this device as the claimer (a new
org.matrix.msc4509.key_bundle_claim to-device message, only trusted
from our own user), or lazily upon the first undecryptable event in the
room. Client-initiated joins via joinRoom keep the eager behaviour.

The MSC4509 behaviour is unstable, so it is opt-in via the new
unstableMSC4509KeyBundleClaim client option: when unset (the default),
key_bundle_claim messages are ignored and server-initiated joins keep
the eager per-device download.
Covers the eager (default) path incl. the unsigned.prev_sender fallback,
and the MSC4509 deferral: designation claims, designations racing ahead
of the join, the undecryptable-event fallback, sender/room_id validation,
leave cleanup, and the error paths.

@richvdh richvdh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts

Comment thread src/client.ts
Comment on lines +1328 to +1340
/** Map from room ID to the user who invited us, for rooms with a pending invite.
* Used to do the key-bundle bookkeeping if the server joins us to the room itself
* (e.g. auto-accepting a knock, synapse#16307) rather than via `joinRoom`. */
private readonly pendingKeyBundleInviters = new Map<string, string>();

/** Map from room ID to inviter, for rooms the server joined us to whose key bundle
* we are deferring (MSC4509): the bundle is only downloaded if the server designates
* us as the claimer, or lazily on the first undecryptable event in the room. */
private readonly lazyKeyBundleRooms = new Map<string, string>();

/** Room IDs for which an `org.matrix.msc4509.key_bundle_claim` designation arrived
* before we processed the corresponding join. */
private readonly earlyKeyBundleClaims = new Set<string>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MatrixClient is already well over 7000 lines long and has far too many properties. Please can we move all the logic and these fields out to a different class, which can be gated as whole behind unstableMSC4509KeyBundleClaim?

Comment thread src/client.ts
// MSC4509: the server may designate this device as the one which should eagerly
// download a room key bundle after a server-initiated join (see above). Only our
// own homeserver can deliver a to-device message with our own user as sender.
this.on(ClientEvent.ToDeviceEvent, (event) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Sonar points out, ToDeviceEvent is deprecated.

Comment thread src/client.ts
Comment on lines +1506 to +1507
// MSC4509 disabled: download eagerly, as `joinRoom` would.
await cryptoBackend.maybeAcceptKeyBundle(member.roomId, finalInviter);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not sneak this change of behaviour in as part of implementing an unstable MSC.

Comment thread src/client.ts
* If the room's join hasn't been processed yet (a designation racing ahead of the
* join in the same sync), remember the designation for the join handler instead.
*/
private claimLazyKeyBundle(roomId: string, reason: string): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a somewhat misleading name, given it won't actually "claim" the bundle if we haven't been invited yet.

Comment thread src/client.ts
this.logger.info(`Claiming deferred key bundle for ${roomId}: ${reason}`);
cryptoBackend.maybeAcceptKeyBundle(roomId, inviter).catch((e) => {
this.logger.error(`Error accepting deferred key bundle for room ${roomId}:`, e);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably this means that if the attempt to download the bundle fails (eg the server gives a 500), we'll never actually do the download? Is that a known problem?

Comment thread src/client.ts
Comment on lines +1477 to +1479
if (member.membership === KnownMembership.Invite) {
const inviter = event.getSender();
if (inviter) this.pendingKeyBundleInviters.set(member.roomId, inviter);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it will fail in the case of a gappy sync, since we might not see the invite event?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants