Skip to content

Commit 53ca01d

Browse files
committed
MSC4242: State DAGs (Federation)
Builds off #19424 Adds federation compatibility for state DAG rooms. Overview: - Adds extra HTTP API fields as per the MSC. - Adds methods for walking and extracting the state DAG for a room (for `/get_missing_events` and `/send_join` respectively). - Adds impl for processing the federation requests, as well as `/send`.
1 parent dc3db60 commit 53ca01d

16 files changed

Lines changed: 1686 additions & 282 deletions

synapse/federation/federation_client.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@
4242
import attr
4343
from prometheus_client import Counter
4444

45-
from synapse.api.constants import Direction, EventContentFields, EventTypes, Membership
45+
from synapse.api.constants import (
46+
Direction,
47+
EventContentFields,
48+
EventTypes,
49+
Membership,
50+
)
4651
from synapse.api.errors import (
4752
CodeMessageException,
4853
Codes,
@@ -119,6 +124,8 @@ class SendJoinResult:
119124
origin: str
120125
state: list[EventBase]
121126
auth_chain: list[EventBase]
127+
# Only valid for state DAG rooms (MSC4242)
128+
state_dag: list[EventBase] | None
122129

123130
# True if 'state' elides non-critical membership events
124131
partial_state: bool
@@ -658,7 +665,10 @@ async def get_pdu(
658665
@trace
659666
@tag_args
660667
async def get_room_state_ids(
661-
self, destination: str, room_id: str, event_id: str
668+
self,
669+
destination: str,
670+
room_id: str,
671+
event_id: str,
662672
) -> tuple[list[str], list[str]]:
663673
"""Calls the /state_ids endpoint to fetch the state at a particular point
664674
in the room, and the auth events for the given event
@@ -670,7 +680,9 @@ async def get_room_state_ids(
670680
InvalidResponseError: if fields in the response have the wrong type.
671681
"""
672682
result = await self.transport_layer.get_room_state_ids(
673-
destination, room_id, event_id=event_id
683+
destination,
684+
room_id,
685+
event_id=event_id,
674686
)
675687

676688
state_event_ids = result["pdu_ids"]
@@ -1178,7 +1190,6 @@ async def send_request(destination: str) -> SendJoinResult:
11781190
response = await self._do_send_join(
11791191
room_version, destination, pdu, omit_members=partial_state
11801192
)
1181-
11821193
# If an event was returned (and expected to be returned):
11831194
#
11841195
# * Ensure it has the same event ID (note that the event ID is a hash
@@ -1201,14 +1212,22 @@ async def send_request(destination: str) -> SendJoinResult:
12011212
event = pdu
12021213

12031214
state = response.state
1204-
auth_chain = response.auth_events
1205-
1215+
auth_events = response.auth_events
12061216
create_event = None
12071217
for e in state:
12081218
if (e.type, e.state_key) == (EventTypes.Create, ""):
12091219
create_event = e
12101220
break
12111221

1222+
if room_version.msc4242_state_dags and response.state_dag:
1223+
# assign to auth_events to reuse the below code which ultimately just does
1224+
# sig/hash checks. We'll set the right field in SendJoinResult later.
1225+
auth_events = response.state_dag
1226+
for e in response.state_dag:
1227+
if (e.type, e.state_key) == (EventTypes.Create, ""):
1228+
create_event = e
1229+
break
1230+
12121231
if create_event is None:
12131232
# If the state doesn't have a create event then the room is
12141233
# invalid, and it would fail auth checks anyway.
@@ -1227,7 +1246,7 @@ async def send_request(destination: str) -> SendJoinResult:
12271246
)
12281247

12291248
logger.info(
1230-
"Processing from send_join %d events", len(state) + len(auth_chain)
1249+
"Processing from send_join %d events", len(state) + len(auth_events)
12311250
)
12321251

12331252
# We now go and check the signatures and hashes for the event. Note
@@ -1246,7 +1265,7 @@ async def _execute(pdu: EventBase) -> None:
12461265
valid_pdus_map[valid_pdu.event_id] = valid_pdu
12471266

12481267
await concurrently_execute(
1249-
_execute, itertools.chain(state, auth_chain), 10000
1268+
_execute, itertools.chain(state, auth_events), 10000
12501269
)
12511270

12521271
# NB: We *need* to copy to ensure that we don't have multiple
@@ -1259,27 +1278,28 @@ async def _execute(pdu: EventBase) -> None:
12591278

12601279
signed_auth = [
12611280
valid_pdus_map[p.event_id]
1262-
for p in auth_chain
1281+
for p in auth_events
12631282
if p.event_id in valid_pdus_map
12641283
]
12651284

12661285
# NB: We *need* to copy to ensure that we don't have multiple
12671286
# references being passed on, as that causes... issues.
1287+
# TODO(kegan): It's unclear why we only need to do this for state and not auth_events
12681288
for s in signed_state:
12691289
s.internal_metadata = s.internal_metadata.copy()
12701290

1271-
# double-check that the auth chain doesn't include a different create event
1272-
auth_chain_create_events = [
1291+
# double-check that the auth events doesn't include a different create event
1292+
auth_events_create_events = [
12731293
e.event_id
12741294
for e in signed_auth
12751295
if (e.type, e.state_key) == (EventTypes.Create, "")
12761296
]
1277-
if auth_chain_create_events and auth_chain_create_events != [
1297+
if auth_events_create_events and auth_events_create_events != [
12781298
create_event.event_id
12791299
]:
12801300
raise InvalidResponseError(
12811301
"Unexpected create event(s) in auth chain: %s"
1282-
% (auth_chain_create_events,)
1302+
% (auth_events_create_events,)
12831303
)
12841304

12851305
servers_in_room = None
@@ -1301,10 +1321,21 @@ async def _execute(pdu: EventBase) -> None:
13011321
# Fix things up in case the remote homeserver is badly behaved.
13021322
servers_in_room.add(destination)
13031323

1324+
signed_auth_events = signed_auth
1325+
signed_state_dag = None
1326+
if room_version.msc4242_state_dags:
1327+
# We previously set the state dag to auth_events so re-assign it correctly
1328+
signed_state_dag = signed_auth
1329+
# Ensure the caller cannot accidentally use these values even if the server
1330+
# returned them.
1331+
signed_state = []
1332+
signed_auth_events = []
1333+
13041334
return SendJoinResult(
13051335
event=event,
13061336
state=signed_state,
1307-
auth_chain=signed_auth,
1337+
auth_chain=signed_auth_events,
1338+
state_dag=signed_state_dag,
13081339
origin=destination,
13091340
partial_state=response.members_omitted,
13101341
servers_in_room=servers_in_room or frozenset(),
@@ -1608,6 +1639,7 @@ async def get_missing_events(
16081639
limit: int,
16091640
min_depth: int,
16101641
timeout: int,
1642+
state_dag: bool = False,
16111643
) -> list[EventBase]:
16121644
"""Tries to fetch events we are missing. This is called when we receive
16131645
an event without having received all of its ancestors.
@@ -1623,6 +1655,7 @@ async def get_missing_events(
16231655
limit: Maximum number of events to return.
16241656
min_depth: Minimum depth of events to return.
16251657
timeout: Max time to wait in ms
1658+
state_dag: True to walk the state DAG (MSC4242 rooms)
16261659
"""
16271660
try:
16281661
content = await self.transport_layer.get_missing_events(
@@ -1633,6 +1666,7 @@ async def get_missing_events(
16331666
limit=limit,
16341667
min_depth=min_depth,
16351668
timeout=timeout,
1669+
state_dag=state_dag,
16361670
)
16371671

16381672
room_version = await self.store.get_room_version(room_id)

synapse/federation/federation_server.py

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
Callable,
2929
Collection,
3030
Mapping,
31+
Sequence,
32+
cast,
3133
)
3234

3335
from prometheus_client import Counter, Gauge, Histogram
@@ -53,7 +55,7 @@
5355
)
5456
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
5557
from synapse.crypto.event_signing import compute_event_signature
56-
from synapse.events import EventBase
58+
from synapse.events import EventBase, FrozenEventVMSC4242
5759
from synapse.events.snapshot import EventPersistencePair
5860
from synapse.federation.federation_base import (
5961
FederationBase,
@@ -596,7 +598,10 @@ async def _process_edu(edu_dict: JsonDict) -> None:
596598
)
597599

598600
async def on_room_state_request(
599-
self, origin: str, room_id: str, event_id: str
601+
self,
602+
origin: str,
603+
room_id: str,
604+
event_id: str,
600605
) -> tuple[int, JsonDict]:
601606
await self._event_auth_handler.assert_host_in_room(room_id, origin)
602607
origin_host, _ = parse_server_name(origin)
@@ -620,7 +625,10 @@ async def on_room_state_request(
620625
@trace
621626
@tag_args
622627
async def on_state_ids_request(
623-
self, origin: str, room_id: str, event_id: str
628+
self,
629+
origin: str,
630+
room_id: str,
631+
event_id: str,
624632
) -> tuple[int, JsonDict]:
625633
if not event_id:
626634
raise NotImplementedError("Specify an event")
@@ -641,17 +649,27 @@ async def on_state_ids_request(
641649
@trace
642650
@tag_args
643651
async def _on_state_ids_request_compute(
644-
self, room_id: str, event_id: str
652+
self,
653+
room_id: str,
654+
event_id: str,
645655
) -> JsonDict:
646-
state_ids = await self.handler.get_state_ids_for_pdu(room_id, event_id)
656+
state_ids = await self.handler.get_state_ids_for_pdu(
657+
room_id,
658+
event_id,
659+
)
647660
auth_chain_ids = await self.store.get_auth_chain_ids(room_id, state_ids)
648661
return {"pdu_ids": state_ids, "auth_chain_ids": list(auth_chain_ids)}
649662

650663
async def _on_context_state_request_compute(
651-
self, room_id: str, event_id: str
664+
self,
665+
room_id: str,
666+
event_id: str,
652667
) -> dict[str, list]:
653668
pdus: Collection[EventBase]
654-
event_ids = await self.handler.get_state_ids_for_pdu(room_id, event_id)
669+
event_ids = await self.handler.get_state_ids_for_pdu(
670+
room_id,
671+
event_id,
672+
)
655673
pdus = await self.store.get_events_as_list(event_ids)
656674

657675
auth_chain = await self.store.get_auth_chain(
@@ -759,6 +777,10 @@ async def on_send_join_request(
759777
origin, content, Membership.JOIN, room_id
760778
)
761779

780+
if event.room_version.msc4242_state_dags and caller_supports_partial_state:
781+
# TODO(kegan): for now, MSC4242 won't support partial state for ease of prototyping.
782+
caller_supports_partial_state = False
783+
762784
prev_state_ids = await context.get_prev_state_ids()
763785

764786
state_event_ids: Collection[str]
@@ -769,15 +791,31 @@ async def on_send_join_request(
769791
event, prev_state_ids, summary
770792
)
771793
servers_in_room = await self.state.get_hosts_in_room_at_events(
772-
room_id, event_ids=event.prev_event_ids()
794+
room_id,
795+
event_ids=event.prev_state_events
796+
if isinstance(event, FrozenEventVMSC4242)
797+
else event.prev_event_ids(),
773798
)
774799
else:
775800
state_event_ids = prev_state_ids.values()
776801
servers_in_room = None
777802

778-
auth_chain_event_ids = await self.store.get_auth_chain_ids(
779-
room_id, state_event_ids
780-
)
803+
state_dag = None
804+
auth_chain_event_ids = set()
805+
if event.room_version.msc4242_state_dags:
806+
assert isinstance(event, FrozenEventVMSC4242)
807+
# NOTE: we don't return the state dag for forward extremities that aren't part of this
808+
# join event to make it easier for the receiving server to set their own forward
809+
# extremities (they are equal to the join event's prev_state_events). This means we may
810+
# fail to sync concurrent forks not on the path to the join event, but this is an
811+
# outstanding problem in general.
812+
state_dag = await self.store.get_state_dag(
813+
room_id, set(event.prev_state_events)
814+
)
815+
else:
816+
auth_chain_event_ids = await self.store.get_auth_chain_ids(
817+
room_id, state_event_ids
818+
)
781819

782820
# if the caller has opted in, we can omit any auth_chain events which are
783821
# already in state_event_ids
@@ -794,13 +832,18 @@ async def on_send_join_request(
794832
resp = {
795833
"event": event_json,
796834
"state": serialize_and_filter_pdus(state_events, time_now),
797-
"auth_chain": serialize_and_filter_pdus(auth_chain_events, time_now),
798835
"members_omitted": caller_supports_partial_state,
799836
}
837+
if state_dag is None:
838+
resp["auth_chain"] = serialize_and_filter_pdus(auth_chain_events, time_now)
839+
else:
840+
resp["state_dag"] = serialize_and_filter_pdus(
841+
cast(Sequence[EventBase], state_dag.values()), time_now
842+
)
843+
del resp["state"]
800844

801845
if servers_in_room is not None:
802846
resp["servers_in_room"] = list(servers_in_room)
803-
804847
return resp
805848

806849
async def on_make_leave_request(
@@ -1097,6 +1140,7 @@ async def on_get_missing_events(
10971140
earliest_events: list[str],
10981141
latest_events: list[str],
10991142
limit: int,
1143+
walk_state_dag: bool = False,
11001144
) -> dict[str, list]:
11011145
async with self._server_linearizer.queue((origin, room_id)):
11021146
origin_host, _ = parse_server_name(origin)
@@ -1111,7 +1155,7 @@ async def on_get_missing_events(
11111155
)
11121156

11131157
missing_events = await self.handler.on_get_missing_events(
1114-
origin, room_id, earliest_events, latest_events, limit
1158+
origin, room_id, earliest_events, latest_events, limit, walk_state_dag
11151159
)
11161160

11171161
if len(missing_events) < 5:

synapse/federation/sender/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
import synapse.metrics
149149
from synapse.api.constants import EventTypes, Membership
150150
from synapse.api.presence import UserPresenceState
151-
from synapse.events import EventBase
151+
from synapse.events import EventBase, FrozenEventVMSC4242
152152
from synapse.federation.sender.per_destination_queue import (
153153
CATCHUP_RETRY_INTERVAL,
154154
PerDestinationQueue,
@@ -660,7 +660,10 @@ async def handle_event(event: EventBase) -> None:
660660
# banned then it won't receive the event because it won't
661661
# be in the room after the ban.
662662
destinations = await self.state.get_hosts_in_room_at_events(
663-
event.room_id, event_ids=event.prev_event_ids()
663+
event.room_id,
664+
event_ids=event.prev_state_events
665+
if isinstance(event, FrozenEventVMSC4242)
666+
else event.prev_event_ids(),
664667
)
665668
except Exception:
666669
logger.exception(

0 commit comments

Comments
 (0)