Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
bdfeb90
Client side API's should still use stripped events with MSC4311
MadLittleMods Apr 23, 2026
43a11f5
Add changelog
MadLittleMods Apr 23, 2026
7f25120
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods May 1, 2026
6026aaa
Non-working: Use full PDU's for `invite_room_state` in federation
MadLittleMods May 2, 2026
e0eb224
Iteration that uses current state
MadLittleMods May 2, 2026
3464ec8
Use `get_stripped_room_state_ids_from_event_context`
MadLittleMods May 2, 2026
f96c008
Invite event should be stripped when included in `invite_state`
MadLittleMods May 4, 2026
a088aa8
Sanitize `invite_room_state` received over federation
MadLittleMods May 4, 2026
336b686
Fix test lints
MadLittleMods May 4, 2026
22f4f20
Updated comment
MadLittleMods May 4, 2026
cce5dc7
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods May 21, 2026
7e379e7
Make the TODO more obvious
MadLittleMods May 21, 2026
92d0d8b
Placeholder arg docstring
MadLittleMods May 21, 2026
76b4905
Better logic/reasoning
MadLittleMods May 22, 2026
5103f1b
Better comment flow
MadLittleMods May 22, 2026
5c1f4ca
Use `FIXME(MSC4311)` prefix
MadLittleMods May 22, 2026
374c4c5
Mark down create event after we verify it's valid
MadLittleMods May 22, 2026
ffe5c4b
Small comment to explain (we also explain above)
MadLittleMods May 22, 2026
2b900b4
Better `invite_room_state` scrutiny
MadLittleMods May 22, 2026
5e12cb8
Fix `events` typo
MadLittleMods May 22, 2026
ff533df
Event ID context
MadLittleMods May 22, 2026
de78d9e
Simplify logic to just use `hs.config.api.room_prejoin_state` instead…
MadLittleMods May 22, 2026
4598a43
`_parse_stripped_room_state`
MadLittleMods May 23, 2026
fbff685
`_minimal_parse_stripped_room_state`
MadLittleMods May 23, 2026
75a53ef
Strip the knock itself we add down `/sync`
MadLittleMods May 23, 2026
178dd89
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods May 26, 2026
1926534
Account for previous Matrix versions where `invite_room_state` is opt…
MadLittleMods May 26, 2026
17c3763
Better `msc4311_stripped_state` description
MadLittleMods May 26, 2026
75d1935
The PDU's over federation can be full PDU's now
MadLittleMods May 26, 2026
3888387
Raise 500 when federation returns 400 `M_MISSING_PARAM`
MadLittleMods May 26, 2026
259f151
Add note about future Complement tests
MadLittleMods May 26, 2026
c289e93
Kick CI after Sytest fixes
MadLittleMods May 27, 2026
ec572c6
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods May 27, 2026
4e7f3fc
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods Jul 9, 2026
bf9ab2f
Refactor to include `inviter_user_id` as part of stripped state
MadLittleMods Jul 9, 2026
c54c93b
Just pass in the event
MadLittleMods Jul 9, 2026
17a4ef6
Remove `_calculate_stripped_state_filter`
MadLittleMods Jul 9, 2026
1cf8282
Update date that Synapse is compliant
MadLittleMods Jul 9, 2026
ce17c93
Cross-link #19943
MadLittleMods Jul 9, 2026
5cc8753
Fix `compatibility` typo
MadLittleMods Jul 9, 2026
53a68d7
Use correct `knock_room_state` reference from response
MadLittleMods Jul 9, 2026
7488a24
Use full PDU's for v1 of federation invite API
MadLittleMods Jul 9, 2026
3870034
Fix typo causing extra nesting
MadLittleMods Jul 10, 2026
40d1316
Kick CI after deleting sytest branch with same name
MadLittleMods Jul 10, 2026
3fc94ba
Fix arg ordering typo with `SynapseError`
MadLittleMods Jul 10, 2026
26bf739
Cross-link MSC4319
MadLittleMods Jul 22, 2026
2822f3c
Move parse/serialize as methods of `StrippedStateEvent`
MadLittleMods Jul 28, 2026
3ce6900
Rename `parse` to `from_json_dict`
MadLittleMods Jul 28, 2026
19d8a7a
Rename `serialize` to `as_json_dict`
MadLittleMods Jul 28, 2026
ecab104
Fix `builtins.TypeError: unsupported operand type(s) for |: 'str' and…
MadLittleMods Jul 28, 2026
eed3071
MSC4319 is now merged
MadLittleMods Jul 28, 2026
f751b5f
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods Jul 28, 2026
e06b311
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods Jul 29, 2026
e301cdd
Start error off more plainly: `The other user's server did not unders…
MadLittleMods Jul 29, 2026
bc73bb0
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods Aug 3, 2026
0baa67b
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods Aug 3, 2026
4f70688
Iterate on error
MadLittleMods Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19723.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove flawed [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) partial implementation: Client-side API's like `/sync` should still use stripped events.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Complement tests are expected to fail ❌ as we removed the flawed partial implementation in this PR.

The Complement tests have been updated in matrix-org/complement#796 and pass locally. We will merge both PRs at the same time.

Comment thread
MadLittleMods marked this conversation as resolved.
37 changes: 37 additions & 0 deletions rust/src/room_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ pub struct RoomVersion {
/// This is similar to how doubly-linked lists can potentially not refer to previous items correctly
/// without verifying the list's integrity, but doing it on every insert is too expensive.
pub msc4242_state_dags: bool,
/// Whether the `m.room.create` event is required in
/// `invite_room_state`/`knock_room_state` when receiving invites/knocks over the
/// federation API's.
///
/// Also determines whether we expect full PDU's in the
/// `invite_room_state`/`knock_room_state` in the federation API. The client API
/// still uses stripped state.
///
/// According to MSC4311:
/// > If any of the events are not a PDU, not for the room ID specified, or fail
/// > signature checks, or the `m.room.create` event is missing, the receiving
/// > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix
/// > error (new to the endpoint). For invites to room version 12+ rooms, servers
/// > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`.
///
/// This does *not* determine whether we should include the `m.room.create` event in
/// stripped state or use full PDU's in stripped state over federation. We should
/// always do this.
pub msc4311_stripped_state: bool,
}

const ROOM_VERSION_V1: RoomVersion = RoomVersion {
Expand All @@ -182,6 +201,7 @@ const ROOM_VERSION_V1: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V2: RoomVersion = RoomVersion {
Expand All @@ -207,6 +227,7 @@ const ROOM_VERSION_V2: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V3: RoomVersion = RoomVersion {
Expand All @@ -232,6 +253,7 @@ const ROOM_VERSION_V3: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V4: RoomVersion = RoomVersion {
Expand All @@ -257,6 +279,7 @@ const ROOM_VERSION_V4: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V5: RoomVersion = RoomVersion {
Expand All @@ -282,6 +305,7 @@ const ROOM_VERSION_V5: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V6: RoomVersion = RoomVersion {
Expand All @@ -307,6 +331,7 @@ const ROOM_VERSION_V6: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V7: RoomVersion = RoomVersion {
Expand All @@ -332,6 +357,7 @@ const ROOM_VERSION_V7: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V8: RoomVersion = RoomVersion {
Expand All @@ -357,6 +383,7 @@ const ROOM_VERSION_V8: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V9: RoomVersion = RoomVersion {
Expand All @@ -382,6 +409,7 @@ const ROOM_VERSION_V9: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V10: RoomVersion = RoomVersion {
Expand All @@ -407,6 +435,7 @@ const ROOM_VERSION_V10: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

/// MSC3389 (Redaction changes for events with a relation) based on room version "10".
Expand All @@ -433,6 +462,7 @@ const ROOM_VERSION_MSC3389V10: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: true,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

/// MSC1767 (Extensible Events) based on room version "10".
Expand All @@ -459,6 +489,7 @@ const ROOM_VERSION_MSC1767V10: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

/// MSC3757 (Restricting who can overwrite a state event) based on room version "10".
Expand All @@ -485,6 +516,7 @@ const ROOM_VERSION_MSC3757V10: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V11: RoomVersion = RoomVersion {
Expand All @@ -510,6 +542,7 @@ const ROOM_VERSION_V11: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: true, // Changed from v10
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

/// MSC3757 (Restricting who can overwrite a state event) based on room version "11".
Expand All @@ -536,6 +569,7 @@ const ROOM_VERSION_MSC3757V11: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: true,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion {
Expand All @@ -561,6 +595,7 @@ const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: true, // Changed from v11
strict_event_byte_limits_room_versions: true,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

const ROOM_VERSION_V12: RoomVersion = RoomVersion {
Expand All @@ -586,6 +621,7 @@ const ROOM_VERSION_V12: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: true, // Changed from v11
strict_event_byte_limits_room_versions: true,
msc4242_state_dags: false,
msc4311_stripped_state: true,
};

const ROOM_VERSION_MSC4242V12: RoomVersion = RoomVersion {
Expand All @@ -611,6 +647,7 @@ const ROOM_VERSION_MSC4242V12: RoomVersion = RoomVersion {
msc4291_room_ids_as_hashes: true,
strict_event_byte_limits_room_versions: true,
msc4242_state_dags: true,
msc4311_stripped_state: true,
};

/// Helper class for managing the known room versions, and providing dict-like
Expand Down
18 changes: 9 additions & 9 deletions synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,15 +1019,6 @@ def strip_event(event: EventBase) -> JsonDict:
Stripped state events can only have the `sender`, `type`, `state_key` and `content`
properties present.
"""
# MSC4311: Ensure the create event is available on invites and knocks.
# TODO: Implement the rest of MSC4311
if (
event.room_version.msc4291_room_ids_as_hashes
and event.type == EventTypes.Create
and event.get_state_key() == ""
):
return event.get_pdu_json()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removing the flawed MSC4311 partial implementation

return {
"type": event.type,
"state_key": event.state_key,
Expand Down Expand Up @@ -1061,3 +1052,12 @@ def parse_stripped_state_event(raw_stripped_event: Any) -> StrippedStateEvent |
)

return None


def serialize_stripped_state_event(stripped_event: StrippedStateEvent) -> JsonDict:
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
return {
"type": stripped_event.type,
"state_key": stripped_event.state_key,
"sender": stripped_event.sender,
"content": stripped_event.content,
}
62 changes: 57 additions & 5 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
RoomVersions,
)
from synapse.events import EventBase, builder, make_event_from_dict
from synapse.events.snapshot import EventContext
from synapse.events.utils import parse_stripped_state_event
from synapse.federation.federation_base import (
FederationBase,
InvalidEventSignatureError,
Expand All @@ -71,7 +73,13 @@
from synapse.http.types import QueryParams
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace
from synapse.metrics import SERVER_NAME_LABEL
from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id
from synapse.types import (
JsonDict,
StrCollection,
UserID,
get_domain_from_id,
)
from synapse.types.state import StateFilter
from synapse.util.async_helpers import concurrently_execute
from synapse.util.caches.expiringcache import ExpiringCache
from synapse.util.duration import Duration
Expand Down Expand Up @@ -1309,12 +1317,12 @@ async def send_invite(
self,
destination: str,
room_id: str,
event_id: str,
pdu: EventBase,
context: EventContext,
) -> EventBase:
room_version = await self.store.get_room_version(room_id)

content = await self._do_send_invite(destination, pdu, room_version)
content = await self._do_send_invite(destination, pdu, context, room_version)

pdu_dict = content["event"]

Expand All @@ -1335,7 +1343,11 @@ async def send_invite(
return pdu

async def _do_send_invite(
self, destination: str, pdu: EventBase, room_version: RoomVersion
self,
destination: str,
pdu: EventBase,
context: EventContext,
room_version: RoomVersion,
) -> JsonDict:
"""Actually sends the invite, first trying v2 API and falling back to
v1 API if necessary.
Expand All @@ -1350,6 +1362,34 @@ async def _do_send_invite(
"""
time_now = self._clock.time_msec()

# MSC4311: For the federation API, format events in `invite_room_state` as full
# PDU's
#
# First get all of the expected stripped state events that should be included.
# We will derive these from the `unsigned` part of the PDU but this doesn't
# include any event ID information so we need to look it up based on the state
# at the time of the invite.
stripped_state_types = []
for raw_stripped_event in pdu.unsigned.get("invite_room_state", []):
stripped_state_event = parse_stripped_state_event(raw_stripped_event)
# Since this is our own invite, it should always be well-formed
assert stripped_state_event is not None, (
"Unable to parse one of the evnts from the `invite_room_state` as a stripped state event"
)
stripped_state_types.append(
(stripped_state_event.type, stripped_state_event.state_key)
)

# Find the full events based on the state at the time of the invite
state_filter = StateFilter.from_types(stripped_state_types)
state_ids = await self.store.get_stripped_room_state_ids_from_event_context(
context, state_filter
)
state_events = await self.store.get_events(state_ids)
assert set(state_ids) == set(state_events.keys()), (
"We should have all events available that were set as stripped state."
)

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.

Hmmm, this is technically a behaviour change where we are recalculating the stripped state here rather than using the stripped state we calculated at event creation.

Generally, I'm wondering if we should change the format of what we store as invite_room_state in the event json in the DB, rather than having this somewhat weird thing going where we have to recalculate it. Not really sure how tractable it is, but you can imagine a world where we store in a separate table the stripped state event IDs for locally generated events.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤷 Future plans


try:
return await self.transport_layer.send_invite_v2(
destination=destination,
Expand All @@ -1358,10 +1398,18 @@ async def _do_send_invite(
content={
"event": pdu.get_pdu_json(time_now),
"room_version": room_version.identifier,
"invite_room_state": pdu.unsigned.get("invite_room_state", []),
"invite_room_state": [
state_event.get_pdu_json(time_now)
for state_event in state_events.values()
],
},
)
except HttpResponseException as e:
# TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx
# error by the sending server over the Client-Server API. This is done
# because there's nothing the client can materially do differently to make
# the request succeed.

# If an error is received that is due to an unrecognised endpoint,
# fallback to the v1 endpoint if the room uses old-style event IDs.
# Otherwise, consider it a legitimate error and raise.
Expand All @@ -1385,6 +1433,10 @@ async def _do_send_invite(
event_id=pdu.event_id,
content=pdu.get_pdu_json(time_now),
)
# TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx
# error by the sending server over the Client-Server API. This is done
# because there's nothing the client can materially do differently to make
# the request succeed.
return content

async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None:
Expand Down
37 changes: 34 additions & 3 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,8 +772,22 @@ async def on_make_join_request(
return {"event": pdu.get_templated_pdu_json(), "room_version": room_version}

async def on_invite_request(
self, origin: str, content: JsonDict, room_version_id: str
self,
*,
origin: str,
expected_room_id: str,
expected_event_id: str,
event_json: JsonDict,
room_version_id: str,
) -> dict[str, Any]:
"""
Args:
origin:
expected_room_id: The room ID specified in the
`/_matrix/federation/v1/invite/{roomId}/{eventId}` request that we expect to
match in the actual event itself.
"""

room_version = KNOWN_ROOM_VERSIONS.get(room_version_id)
if not room_version:
raise SynapseError(
Expand All @@ -782,9 +796,21 @@ async def on_invite_request(
Codes.UNSUPPORTED_ROOM_VERSION,
)

pdu = event_from_pdu_json(content, room_version)
pdu = event_from_pdu_json(event_json, room_version)
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, pdu.room_id)
if pdu.event_id != expected_event_id:
raise SynapseError(
400,
Codes.INVALID_PARAM,
"Invite event ID must match event ID specified in the federation `/invite` request",
)
if pdu.room_id != expected_room_id:
raise SynapseError(
400,
Codes.INVALID_PARAM,
"The room_id specified in the invite event must match room ID specified in the federation `/invite` request",
)
if await self._spam_checker_module_callbacks.should_drop_federated_event(pdu):
logger.info(
"Federated event contains spam, dropping %s",
Expand All @@ -797,7 +823,11 @@ async def on_invite_request(
errmsg = f"event id {pdu.event_id}: {e}"
logger.warning("%s", errmsg)
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version)
ret_pdu = await self.handler.on_invite_request(
origin=origin,
event=pdu,
room_version=room_version,
)
time_now = self._clock.time_msec()
return {"event": ret_pdu.get_pdu_json(time_now)}

Expand Down Expand Up @@ -965,6 +995,7 @@ async def on_send_knock_request(
# server. This will allow the remote server's clients to display information
# related to the room while the knock request is pending.
stripped_room_state = (
# TODO: Implement MSC4311 and use full PDUs here
await self.store.get_stripped_room_state_from_event_context(
context, self._room_prejoin_state_types
)
Expand Down
Loading
Loading