From 256ad01fd231945be01e1d2e18fb597e2780c595 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 10 Aug 2018 16:51:44 +0100 Subject: [PATCH] proposal for room version upgrades --- proposals/1501-room-version-upgrades.md | 346 ++++++++++++++++++++++++ proposals/1501-split-dag.png | Bin 0 -> 21104 bytes 2 files changed, 346 insertions(+) create mode 100644 proposals/1501-room-version-upgrades.md create mode 100644 proposals/1501-split-dag.png diff --git a/proposals/1501-room-version-upgrades.md b/proposals/1501-room-version-upgrades.md new file mode 100644 index 00000000..2fd880ea --- /dev/null +++ b/proposals/1501-room-version-upgrades.md @@ -0,0 +1,346 @@ +# Room version upgrades + +## Background + +[MSC1425](https://github.com/matrix-org/matrix-doc/issues/1425) introduces a +mechanism for associating a "version" with a room, which allows us to introduce +changes to the mechanics of rooms. + +Assuming that a given change is successful, the next challenge is to introduce +it to existing rooms. This proposal introduces a mechanism for doing so. + +## Proposal + +In short: room upgrades are implemented by creating a new room, shutting down +the old one, and linking between the two. + +The mechanics of this are as follows. When Alice upgrades a room, her client +hits a new C-S api: + +``` +POST /_matrix/client/r0/room/{roomId}/upgrade +``` +```json +{ + "new_version": "2" +} +``` + +Response: + +```json +{ + "replacement_room": "!QtykxKocfsgujksjgd:matrix.org" +} +``` + +When this is called, the server: + + * Checks that Alice has permissions to send `m.room.tombstone` state events. + + * Creates a replacement room, with an `m.room.create` with a `predecessor` field + which links to the last known event in the old room: + + ```json + { + "sender": "@alice:somewhere.com", + "type": "m.room.create", + "state_key": "", + "room_id": "!QtykxKocfsgujksjgd:matrix.org", + "content": { + "predecessor": { + "room_id": "!cURbaf:matrix.org", + "event_id": "$1235135aksjgdkg:matrix.org" + } + } + } + ``` + + * Replicates PL/privacy/topic/etc settings to the new room. + + * Moves any local aliases to the new room. + + * Sends an `m.room.tombstone` state event in the old room to tell participants + that it is dead: + + ```json + { + "sender": "@alice:somewhere.com", + "type": "m.room.tombstone", + "state_key": "", + "room_id": "!cURbaf:matrix.org", + "content": { + "body": "This room has been replaced", + "replacement_room": "!QtykxKocfsgujksjgd:matrix.org" + } + } + ``` + + * Assuming Alice has the powers to do so, sets the power levels in the old + room to stop people speaking. [TODO: what does this actually mean?] + +Bob's client understands the `m.room.tombstone` event, and: + + * Hides the old room in the room list (the room continues to be accessible + via the old room id (permalinks, backlinks from the new room, etc). + + * Displays, at the very bottom of the timeline of the old room: "This room + has been upgraded. Click here to follow the conversation to the new room". + The link is simply a permalink to the new room. When Bob opens it, he will + get joined to the new room. + + [Note that if Bob is on a version of synapse which doesn't understand room + versions, following the permalink will take him to a room view which churns + for a while and eventually fails. Synapse 0.33.3 / 0.34.0 should at least + give a sensible error code.] + + * Optional extension: if the user is in both rooms, then the "N unread + messages" banner when scrolled up in the old room could be made to track + messages in the new room (so in practice the user would only ever see the + hiatus between the versions if they scrolled all the way to the beginning + of the new room or the end of the old one.) + +Bob's client also understands the `predecessor` field in the `m.room.create`, and: + + * At the top of scrollback in the new room, displays: "This room is a + continuation of a previous room. Click here to see the old conversation." + The link is a permalink to the old room. + + * Optional extensions might include things like extending room search to + work across two rooms. + +### Summary: client changes needed + + * Ability for an op to view the current room version and upgrade it (by + hitting `/upgrade_room`). + + * Also the ability for an op to see what versions the servers in the current + room supports (nb via a cap API) and so how many users will get locked out + [XXX: really? this sounds like a pita] + + * Display `m.room.tombstone`s as a sticky message at the bottom of the old + room (perhaps replacing the message composer input) as “This room has been + replaced. Please click here to continue” or similar. + + * When the user clicks the link, the client attempts to join the new room if + we are not already a member, and then switches to a view on the new room. + + * If the client sees a pair of rooms with a tombstone correctly joined to the + new room, it should hide the old one from the RoomList. + + * If one backpaginates the new room to its creation, we should show the + `m.room.create` as “This room is a continuation of a previous room; click here + to view” (or similar). + + * Search is extended to search old rooms (could be done clientside for now). + +Future eye-candy: + + * When one is viewing the old room: + + * Rather than showing a sticky tombstone at the bottom, one should probably + have the “10 unread messages” section in the status bar which refers to + the current version of the room. + + * We should probably also show the membership list of the current room. (Perhaps?) + +### Problems + + * If Bob is on a remote server, how does it know where to send the + `/make_join` for the new room? + + * We could send it to the server which sent the `tombstone` evnent? (This + would require the client to set the `server_name` param on the `/join` + request, but that seems fine) + + * What about invite-only rooms? Users in the old room won't be able to join + the new room without an invite. + + * For local users, we need to first create an invite for each user in the + room. This is easy, if a bit high-overhead. + + * For remote users: + + * Alice's server could send invites; however this is likely to give an + unsatisfactory UX due to servers being offline, maybe not supporting the + new room version, and then spamming Bob with mysterious invites. + + * We could change the auth rules to treat a membership of the old room as + equivalent to an invite to the new room. However, this is likely to + reintroduce the problems we are trying to solve by replacing the room in + the first place. + + * We could create a new type of membership which acts like an invite for + the purposes of allowing users into invite-only rooms, but doesn't need + to be sent to remote servers. + + * What about clients that don't understand tombstones? + + * I think they'll just show you two separate rooms (both with the same + name), and you won't be able to talk in one of them. It's not great but it + will probably do. + + * It's a shame that scrollback in the new room will show a load of joins + before you get to the link to the old room. + + * Do users stay members of the old room forever? Do they leave after a + respectful period? Do they leave if the user leaves the new room? + +## Dismissed solutions + +### Variations on this proposal + +#### Have servers auto-join their users on upgrade + +In order to make the upgrade more seamless, it might be good for servers to +automatically join any users that were in the old room to the new room. + +In short, when Bob's server receives a tombstone event, it would attempt to +auto-join Bob to the new room, and updates any aliases it may have to point to +the new room. + +It's worth noting that the join may not be successful: for example, because +Bob's server is too old, or because Bob has been blocked from joining the new +room, or because joins are generally flaky. In this case Bob might attempt a +rejoin via his client. + +#### Have servers merge old and new rooms together + +Instead of expecting clients to interpret tombstone events, servers could merge +/sync results together for the two rooms and present both as the old room in +/sync results - and then map from the old room ID back to the new one for API +calls. + +(Note that doing this the other way around (mapping both rooms into the *new* +room ID) doesn't really work without massively confusing clients which have +cached data about the old room.) + +This sounds pretty confusing though: would the server do this forever +for all users on the server? + +#### have clients merge old and new rooms + +At the top of scrollback for the new room, shows some sort of "messages from a +previous version of this room" divider, above which it shows messages from the +old room [1](#f10). + +This gets quite messy quite quickly. For instance, it will be confusing to see +any ongoing traffic in the old room (people whose servers didn't get the memo +about the tombstone; people leaving the old room; etc). Ultimately it's all +client polish though, so we can consider this for the future. + +1 It's probably worth noting that this alone is a somewhat +fundamental reworking of a bunch of complicated stuff in matrix-react-sdk (and +presumably likewise in the mobile clients). [↩](#a10) + +### Counter-proposal: upgrade existing rooms in-place + +The general idea here is to divide the room DAG into v1 and v2 parts. Servers +which do not support v2 will not see the v2 part of the DAG. This might look +like the below: + +![dag](1501-split-dag.png) + +In this model, room version is tracked as a state event: + +```json +{ + "type": "m.room.version", + "content": { + "version": 2 + } +} +``` + +This event can only ever be sent by the **original room creator** - i.e., the +sender of the `m.room.create event` - even if the `m.room.power_levels` say +otherwise [1](#f1) [2](#f2). + +The DAG is now divided into parts where the state of m.room.version is 2, and +those where it is missing (and therefore implicitly 1). + +When sending out federation transactions, v2 events are listed under a new +versioned_pdus key in the /send request: + +``` +PUT /_matrix/federation/v1/send/991079979 + +{ + "origin_server_ts": 1404835423000, + "origin": "matrix.org", + "pdus": [], + "versioned_pdus": { + 2: [...] + }, + "edus": [...] +} +``` + +Old servers which do not support v2 will therefore not receive any v2 parts of +the DAG, and transactions which only contain updates to the v2 part of the DAG +will be treated as no-ops. Servers should ignore entries under versioned_pdus +which correspond to versions they do not know about. + +As a special case, `m.room_version events` themselves are treated as having the +version previously in force in the room, unless that was v1, in which case, +they are treated as being v2. This means that the upgrade from v1 to v2, *and* +the upgrade from v2 to v3, are included under `versioned_pdus[2]`, but the +upgrade from v3 to v4 is included under `versioned_pdus[3]`. If a server +receives an `m.room_version` event with a version it does not understand, it +must refuse to allow any other events into the upgraded part of the DAG (and +probably *should* refuse to allow any events into the DAG at all) +[3](#f3). The reasons for this are as follows: + + * We want to ensure that v1-only servers do not receive the room_version + event, since they don't know how to auth it correctly, and more importantly + we want to ensure that they can't get hold of it to construct bits of DAG + which refer to it, and would have to follow the v2 rules. + + * However, putting subsequent upgrades in a place where older servers will see + it means that we can do more graceful upgrades in future by telling their + clients that the room seems to have been upgraded. + +As normal, we require joining servers to declare their support for room +versions, and reject any which do not include the version of the room given by +the current room state. + +In theory this is all that is required to ensure that v1-only servers never see +any v2 parts of the DAG (events are only sent via /send, and a v1 server should +have no way to reference v2 parts of the DAG); however we may want to consider +adding a "versions" parameter to API calls like /event and /state which allow a +server to specify an event_id to fill from, in case a (non-synapse, presumably) +implementation gets an event id from a /context request or similar and tries to +fill from it. + +The experience for clients could be improved by awareness of the m.room.version +event and querying a capabilities API (e.g. /versions?) to determine whether a +room has been upgraded newer than the server can support. If so, a client +could say: “Your server must be upgraded to continue to participate in this +room” (and manually prevent the user from trying to speak). + +#### Problems + + * This leaves users on old v1-only servers in their own view of the room, + potentially continuing to speak but only seeing a subset of events (those + from their own and other v1 servers). + + In the worst case, they might ask questions which those on v2 servers can see but have no way of replying to. + + One way to mitigate this would be to ensure that the last event sent in the + v1 part of the DAG is an m.room.power_levels to make the room read-only to + casual users; then restore the situation in the v2 DAG. + + * There's nothing to stop "inquisitive" users on v1 servers sending + m.room.version events, which is likely to completely split-brain the room. + + * This doesn't help us do things like change the format of the room id. + +1 This avoids the version event itself being vulnerable to state +resets. [↩](#a1) + +2 This is therefore a change to the event authorisation rules +which we need to introduce with room version 2. [↩](#a2) + +3 It's worth noting this does give the room creator a +kill-switch for the room, even if they've subsequently been de-opped. Such is +life. [↩](#a3) diff --git a/proposals/1501-split-dag.png b/proposals/1501-split-dag.png new file mode 100644 index 0000000000000000000000000000000000000000..73312e5a4488ea254e510d0b9a0b531d434026e8 GIT binary patch literal 21104 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