Skip to content

MSC4242: State DAGs (inbound joins) - #20194

Open
kegsay wants to merge 31 commits into
developfrom
kegan/4242-inbound
Open

kegsay wants to merge 31 commits into
developfrom
kegan/4242-inbound

Conversation

@kegsay

@kegsay kegsay commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Process inbound requests to join a state DAG room.

Split out from #19425

Part of a series of 5x PRs to land the federation part of MSC4242 (#19718, #20127, #20133, inbound-joins (this PR), inbound-pulls).

Has some Complement tests:

PASS TestMSC4242JoinLeaveRejoinPublicRoom 9.19s
PASS TestMSC4242JoinPublicRoomWithRejectedStateDAGEvents 0.88s

Pull Request Checklist

  • Pull request is based on the develop branch
  • Pull request includes a changelog file. The entry should:
    • Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from EventStore to EventWorkerStore.".
    • Use markdown where necessary, mostly for code blocks.
    • End with either a period (.) or an exclamation mark (!).
    • Start with a capital letter.
    • Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
  • Code style is correct (run the linters)

@kegsay
kegsay requested a review from a team as a code owner September 7, 2026 16:02
@kegsay
kegsay requested review from MadLittleMods and removed request for a team September 7, 2026 16:02
@kegsay
kegsay force-pushed the kegan/4242-inbound branch from f5a07a8 to 4fa82bf Compare September 8, 2026 07:38
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
@kegsay
kegsay force-pushed the kegan/4242-inbound branch 2 times, most recently from 76ae862 to 338ed0c Compare September 11, 2026 09:13
Base automatically changed from kegan/4242-serving to develop September 11, 2026 10:44
@kegsay kegsay mentioned this pull request Sep 11, 2026
2 of 3 tasks
Comment thread changelog.d/20194.misc
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py
Comment thread changelog.d/20194.misc
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py
Comment thread changelog.d/20194.misc
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/storage/databases/main/rejections.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/storage/databases/main/rejections.py Outdated
Comment thread changelog.d/20194.misc
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py
Comment thread synapse/handlers/federation_event.py Outdated
Comment thread synapse/handlers/federation_event.py Outdated
Comment on lines +2068 to +2069
batched_auth_events = {
event_id: event_map[event_id]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LLM is spotting another rejection cascade problem:

LLM description

Rejection doesn't cascade on rejoin (most serious).

  • Every event from the response goes into event_map as a fresh copy with rejected_reason=None. That includes events we already stored and rejected on an earlier join.
  • batched_auth_events is built from event_map (:2068). check_state_independent_auth_rules reads prev_state_events from batched_auth_events before it looks at the database (event_auth.py:204).
  • rejections only records decisions made in the current batch.
  • Result: a new event whose prev_state_events includes an event we rejected last time passes the check at event_auth.py:250. This reopens the concern from the earlier thread at :2082, just by a different route.
  • Suggested fix: when processing the seen events, fill rejections from the database with their reasons, or use the stored copies for seen events.

Previous problem: #20194 (comment)

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.

2d9da36 - basically doing the suggested fix of replacing the wire-events with stored-events (which contain rejections).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably worth a test so we don't regress these subtleties

Comment thread synapse/handlers/federation_event.py Outdated
Comment on lines +2012 to +2015
if from_send_join and all(
prev_state_event_id in event_id_to_state_group
for prev_state_event_id in event.prev_state_events
):

@MadLittleMods MadLittleMods Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LLM is mentioning that if an event's prev_state_events mix an event we already persisted and one that's new in this batch, we fall back to compute_state_after_events from the DB. But nothing in the batch is persisted until the end, so the new parent has no state group yet, and its branch is silently dropped from the state.

Also see #20194 (comment)

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably worth a test so we don't regress these subtleties

Comment on lines +2226 to +2227
# If this assumption is ever violated, `compute_state_after_events` will
# yell loudly about missing state groups.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LLM is calling out that _get_state_group_for_events raises RuntimeError for unknown events, but the @cachedList wrapper swallows it and the caller just gets partial results.

This sounds like a pre-existing bug with @cachedList but it makes our comment inaccurate and we should probably mark it with a FIXME with a link to a Synapse issue tracking this problem.

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.

This is related to the comment #20194 (comment) as this should have caused a 500 but instead silently dropped the branch :/

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.

0021dae

I just added a TODO for this as this PR is sprawling enough as it is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The comment should be updated to call this out more clearly as something that's probably unexpected.

        XXX: `_get_state_group_for_events` raises `RuntimeError` for unknown events, but
        the `@cachedList` wrapper swallows it and the caller just gets partial results.
        For the time-being, Callers MUST NOT rely on this to determine missing events,
        as cached responses return None for the state group instead of raising.

        TODO: audit every caller, as some of them may be relying on the current behaviour.

And should be copied over to _get_state_group_for_events as well

raise SynapseError(HTTPStatus.BAD_REQUEST, "Too many prev_events")

if len(ev.auth_event_ids()) > 10:
if supports_msc4242_state_dag(ev) and len(ev.prev_state_events) > 20:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LLM is mentioning that we don't also have a check for this when creating local events which means other servers will just reject events that we create.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see a 👍 but what's the plan here?

kegsay and others added 26 commits October 2, 2026 16:38
Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
Co-authored-by: Eric Eastwood <erice@element.io>
Co-authored-by: Eric Eastwood <erice@element.io>
Co-authored-by: Eric Eastwood <erice@element.io>
- Enable MSC4242 complement tests
- Fix an issue which caused cascade rejections to not be applied to events in the /send_join response becau
se we didn't remember in-memory which events were rejected.
- Accept rejected events in the /send_join response, it gives us a bit of robustness at no safety cost.
- Recalculate room state if backfilled events caused the state DAG fwd extrems to change.
- Don't filter out already seen events from event_map, so the statement "the event_map is the complete stat
e DAG" still holds.
- Add validation to length of prev_state_events
- Don't validate auth_events length for state DAG events as it is done too early, before we've had time to
calculate the auth events (and validation is moot anyway given it isn't attacker controlled anymore)
- Let backfilled events update state dag fwd extrems
- Add test for edge case with backfilled state events
…events that should have been rejected

The cascade rejection only worked on in-flight events, not seen events.
We now overwrite events in event_map with rejected events (thus setting .rejected_reason)
to ensure we continue to cascade rejections correctly.

Complement test asserts this.
… if one of the prev_state_events was already persisted

Tested in Complement as TestMSC4242RejoinMergesOldAndNewStateDAGBranches
…vent is connected to the state DAG

This PR only handles /send_join and connected events in /send,
so everything else should return early with coherent error messages.

The join event must be connected to the main state DAG but we previously
didn't ensure this.
@kegsay
kegsay force-pushed the kegan/4242-inbound branch from 1ef48a8 to 82d52c9 Compare October 2, 2026 15:38
Comment thread changelog.d/20194.misc
@@ -0,0 +1 @@
Add support for joining [MSC4242](https://github.com/matrix-org/matrix-spec-proposals/pull/4242) rooms over federation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CI failing, https://github.com/element-hq/synapse/actions/runs/37028432824/job/110910905270

[FAIL]
Traceback (most recent call last):
  File "/home/runner/work/synapse/synapse/tests/federation/test_federation_server.py", line 519, in test_is_opt_in
    timeline_events = self._get_missing_events(
  File "/home/runner/work/synapse/synapse/tests/federation/test_federation_server.py", line 462, in _get_missing_events
    self.assertEqual(HTTPStatus.OK, channel.code, channel.json_body)
  File "/home/runner/work/synapse/synapse/tests/unittest.py", line 433, in assertEqual
    super().assertEqual(first=first, second=second, msg=msg)
  File "/home/runner/.cache/pypoetry/virtualenvs/matrix-synapse-pswDeSvb-py3.10/lib/python3.10/site-packages/twisted/trial/_synctest.py", line 433, in assertEqual
    super().assertEqual(first, second, msg)
  File "/opt/hostedtoolcache/Python/3.10.21/x64/lib/python3.10/unittest/case.py", line 845, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/opt/hostedtoolcache/Python/3.10.21/x64/lib/python3.10/unittest/case.py", line 838, in _baseAssertEqual
    raise self.failureException(msg)
twisted.trial.unittest.FailTest: <HTTPStatus.OK: 200> != 400 : {'errcode': 'M_UNRECOGNIZED', 'error': '/get_missing_events is not implemented for MSC4242 state DAG rooms'}

tests.federation.test_federation_server.GetMissingEventsStateDagTests.test_is_opt_in

Comment thread synapse/handlers/federation_event.py Outdated
Comment on lines +2068 to +2069
batched_auth_events = {
event_id: event_map[event_id]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably worth a test so we don't regress these subtleties

Comment thread synapse/handlers/federation_event.py Outdated
Comment on lines +2012 to +2015
if from_send_join and all(
prev_state_event_id in event_id_to_state_group
for prev_state_event_id in event.prev_state_events
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably worth a test so we don't regress these subtleties

raise SynapseError(HTTPStatus.BAD_REQUEST, "Too many prev_events")

if len(ev.auth_event_ids()) > 10:
if supports_msc4242_state_dag(ev) and len(ev.prev_state_events) > 20:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see a 👍 but what's the plan here?

Comment on lines +2226 to +2227
# If this assumption is ever violated, `compute_state_after_events` will
# yell loudly about missing state groups.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The comment should be updated to call this out more clearly as something that's probably unexpected.

        XXX: `_get_state_group_for_events` raises `RuntimeError` for unknown events, but
        the `@cachedList` wrapper swallows it and the caller just gets partial results.
        For the time-being, Callers MUST NOT rely on this to determine missing events,
        as cached responses return None for the state group instead of raising.

        TODO: audit every caller, as some of them may be relying on the current behaviour.

And should be copied over to _get_state_group_for_events as well

Comment on lines +2293 to +2294
if len(event.prev_state_events) == 0 and not is_create_event:
raise SynapseError(502, f"event {event.event_id} has no prev_state_events")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why don't we reject the event instead of dropping it?

Related to the other spot where a rejected event in the DAG only logs "Joining anyway", on the grounds that refusing "would cost availability without buying any safety".

This branch has not been deployed

No deployments
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.

2 participants