From 3e42bdc8f85893ec75291f8ce9c0a37d72d87603 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:06:50 +0000 Subject: [PATCH 1/5] Allow Complement to create v12 rooms Previously we always relied on the HS under test creating v12 rooms, and Complement could only join/send into those rooms. This is a problem if we want to build room versions on top of v12 rooms, as we need Complement to be able to create v12 rooms too. --- federation/server.go | 18 +++++++++++++++++- federation/server_room.go | 14 +++++++++++--- tests/v12_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/federation/server.go b/federation/server.go index ba7478de..b3997d74 100644 --- a/federation/server.go +++ b/federation/server.go @@ -180,18 +180,34 @@ func (s *Server) MustMakeRoom(t ct.TestLike, roomVer gomatrixserverlib.RoomVersi if !s.listening { ct.Fatalf(s.t, "MustMakeRoom() called before Listen() - this is not supported because Listen() chooses a high-numbered port and thus changes the server name and thus changes the room ID. Ensure you Listen() first!") } + // Generate a unique room ID, prefixed with an incrementing counter. // This ensures that room IDs are not re-used across tests, even if a Complement server happens // to re-use the same port as a previous one, which // * reduces noise when searching through logs and // * prevents homeservers from getting confused when multiple test cases re-use the same homeserver deployment. roomID := fmt.Sprintf("!%d-%s:%s", len(s.rooms), util.RandomString(18), s.serverName) - t.Logf("Creating room %s with version %s", roomID, roomVer) room := NewServerRoom(roomVer, roomID) for _, opt := range opts { + // let the caller replace the room impl before we try to create events opt(room) } + iRoomVer := gomatrixserverlib.MustGetRoomVersion(roomVer) + if iRoomVer.DomainlessRoomIDs() { + if len(events) == 0 || events[0].Type != spec.MRoomCreate { + ct.Fatalf(s.t, "MustMakeRoom: room version %s requires the create event as an initial event but it wasn't found", roomVer) + } + room.RoomID = "" + // build and sign the create event to work out the room ID + createEvent := s.MustCreateEvent(t, room, events[0]) + events = events[1:] + room.RoomID = "!" + createEvent.EventID()[1:] + room.AddEvent(createEvent) + } + + t.Logf("Creating room %s with version %s", room.RoomID, roomVer) + // sign all these events for _, ev := range events { signedEvent := s.MustCreateEvent(t, room, ev) diff --git a/federation/server_room.go b/federation/server_room.go index 5401a234..fbd07918 100644 --- a/federation/server_room.go +++ b/federation/server_room.go @@ -302,7 +302,7 @@ func (r *ServerRoom) GetEventInTimeline(eventID string) (gomatrixserverlib.PDU, return nil, false } -func initialPowerLevelsContent(roomCreator string) (c gomatrixserverlib.PowerLevelContent) { +func initialPowerLevelsContent(ver gomatrixserverlib.IRoomVersion, roomCreator string) (c gomatrixserverlib.PowerLevelContent) { c.Defaults() c.Events = map[string]int64{ "m.room.name": 50, @@ -312,14 +312,18 @@ func initialPowerLevelsContent(roomCreator string) (c gomatrixserverlib.PowerLev "m.room.avatar": 50, "m.room.aliases": 0, // anyone can publish aliases by default. Has to be 0 else state_default is used. } - c.Users = map[string]int64{roomCreator: 100} + if ver.PrivilegedCreators() { + c.Users = map[string]int64{} + } else { + c.Users = map[string]int64{roomCreator: 100} + } return c } // InitialRoomEvents returns the initial set of events that get created when making a room. func InitialRoomEvents(roomVer gomatrixserverlib.RoomVersion, creator string) []Event { // need to serialise/deserialise to get map[string]interface{} annoyingly - plContent := initialPowerLevelsContent(creator) + plContent := initialPowerLevelsContent(gomatrixserverlib.MustGetRoomVersion(roomVer), creator) plBytes, _ := json.Marshal(plContent) var plContentMap map[string]interface{} json.Unmarshal(plBytes, &plContentMap) @@ -441,6 +445,7 @@ func (i *ServerRoomImplDefault) ProtoEventCreator(room *ServerRoom, ev Event) (* PrevEvents: prevEvents, AuthEvents: ev.AuthEvents, Redacts: ev.Redacts, + Version: gomatrixserverlib.MustGetRoomVersion(room.Version), } if err := proto.SetContent(ev.Content); err != nil { return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) @@ -454,6 +459,9 @@ func (i *ServerRoomImplDefault) ProtoEventCreator(room *ServerRoom, ev Event) (* if err != nil { return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) } + if proto.Version.DomainlessRoomIDs() { + stateNeeded.Create = false + } proto.AuthEvents = room.AuthEvents(stateNeeded) } return &proto, nil diff --git a/tests/v12_test.go b/tests/v12_test.go index 70c86f53..d4fa62c9 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -663,6 +663,36 @@ func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { assertCreateEventIsRoomID(t, alice, roomID) } +func TestComplementCanCreateValidV12Rooms(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + srvRoom := srv.MustMakeRoom(t, roomVersion12, federation.InitialRoomEvents(roomVersion12, bob)) + alice.MustJoinRoom(t, srvRoom.RoomID, []spec.ServerName{srv.ServerName()}) + + msg := srv.MustCreateEvent(t, srvRoom, federation.Event{ + Type: "m.room.message", + Sender: bob, + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello world", + }, + }) + srvRoom.AddEvent(msg) + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{msg.JSON()}, nil) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(srvRoom.RoomID, msg.EventID())) +} + func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing.T) { deployment := complement.Deploy(t, 1) defer deployment.Destroy(t) From 4dffa750be75d401ca1ab83494bdd49b89b7a9a2 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:14:23 +0000 Subject: [PATCH 2/5] Remove v12 shims as it's native now --- tests/v12_test.go | 57 ++++------------------------------------------- 1 file changed, 4 insertions(+), 53 deletions(-) diff --git a/tests/v12_test.go b/tests/v12_test.go index d4fa62c9..6139bc6f 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -29,55 +29,6 @@ var maxCanonicalJSONInt = math.Pow(2, 53) - 1 const roomVersion12 = "12" -var V12ServerRoom = federation.ServerRoomImplCustom{ - ProtoEventCreatorFn: Protov12EventCreator, -} - -// Override how Complement makes proto events so we can conditionally disable/enable the inclusion of the create event -// depending on whether we're running in combined mode or not. -// Complement also doesn't set the room version correctly on the ProtoEvent as this was a new addition to GMSL. -func Protov12EventCreator(def federation.ServerRoomImpl, room *federation.ServerRoom, ev federation.Event) (*gomatrixserverlib.ProtoEvent, error) { - var prevEvents interface{} - if ev.PrevEvents != nil { - // We deliberately want to set the prev events. - prevEvents = ev.PrevEvents - } else { - // No other prev events were supplied so we'll just - // use the forward extremities of the room, which is - // the usual behaviour. - prevEvents = room.ForwardExtremities - } - proto := gomatrixserverlib.ProtoEvent{ - SenderID: ev.Sender, - Depth: int64(room.Depth + 1), // depth starts at 1 - Type: ev.Type, - StateKey: ev.StateKey, - RoomID: room.RoomID, - PrevEvents: prevEvents, - AuthEvents: ev.AuthEvents, - Redacts: ev.Redacts, - Version: gomatrixserverlib.MustGetRoomVersion(room.Version), - } - if err := proto.SetContent(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) - } - if err := proto.SetUnsigned(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) - } - if proto.AuthEvents == nil { - var stateNeeded gomatrixserverlib.StateNeeded - // this does the right thing for v12 - stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto) - if err != nil { - return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) - } - // we never include the create event if the HS supports MSC4291 - stateNeeded.Create = false - proto.AuthEvents = room.AuthEvents(stateNeeded) - } - return &proto, nil -} - // Test that the creator can kick an admin created both via // trusted_private_chat and by explicit promotion, including beyond PL100. // Also checks the creator isn't in the PL event. @@ -246,7 +197,7 @@ func TestMSC4289PrivilegedRoomCreators(t *testing.T) { "room_version": roomVersion12, "preset": "public_chat", }) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob) plEventID := alice.SendEventSynced(t, roomID, b.Event{ Type: spec.MRoomPowerLevels, StateKey: b.Ptr(""), @@ -713,7 +664,7 @@ func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing. defer cancel() bob := srv.UserID("bob") - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob) createEvent := room.CurrentState(spec.MRoomCreate, "") if createEvent == nil { @@ -984,7 +935,7 @@ func TestMSC4297StateResolutionV2_1_starts_from_empty_set(t *testing.T) { "preset": "public_chat", }) bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie) joinRulePublic := room.CurrentState(spec.MRoomJoinRules, "") aliceJoin := room.CurrentState(spec.MRoomMember, alice.UserID) synchronisationEventID := bob.SendEventSynced(t, room.RoomID, b.Event{ @@ -1180,7 +1131,7 @@ func TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph(t *testing.T) { }) alice.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie) firstPowerLevelEvent := room.CurrentState(spec.MRoomPowerLevels, "") alice.SendEventSynced(t, roomID, b.Event{ Type: spec.MRoomPowerLevels, From 901d8d23b29018f12ccc6146b20f17580e3386ea Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:01:25 +0000 Subject: [PATCH 3/5] Set unsigned field correctly --- federation/server_room.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/federation/server_room.go b/federation/server_room.go index fbd07918..a6208cfc 100644 --- a/federation/server_room.go +++ b/federation/server_room.go @@ -450,8 +450,10 @@ func (i *ServerRoomImplDefault) ProtoEventCreator(room *ServerRoom, ev Event) (* if err := proto.SetContent(ev.Content); err != nil { return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) } - if err := proto.SetUnsigned(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + if len(ev.Unsigned) > 0 { + if err := proto.SetUnsigned(ev.Unsigned); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + } } if proto.AuthEvents == nil { var stateNeeded gomatrixserverlib.StateNeeded From 1bf611fdfb1df467fd9244f78edd4be8a34b2cf8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:33:25 +0000 Subject: [PATCH 4/5] Add entropy --- federation/server_room.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/federation/server_room.go b/federation/server_room.go index a6208cfc..61c40ed3 100644 --- a/federation/server_room.go +++ b/federation/server_room.go @@ -10,6 +10,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" "github.com/matrix-org/complement/b" "github.com/matrix-org/complement/ct" @@ -335,6 +336,11 @@ func InitialRoomEvents(roomVer gomatrixserverlib.RoomVersion, creator string) [] Content: map[string]interface{}{ "creator": creator, "room_version": roomVer, + // We have to add randomness to the create event, else if you create 2x v12+ rooms in the same millisecond + // they will get the same room ID, clobbering internal data structures and causing extremely confusing + // behaviour. By adding this entropy, we ensure that even if rooms are created in the same millisecond, their + // hashes will not be the same. + "complement_entropy": util.RandomString(18), }, }, { From cd90731f4c16ca2648ee6dceb6d1a042b6721aff Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:25:31 +0000 Subject: [PATCH 5/5] Review comments --- federation/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/federation/server.go b/federation/server.go index b3997d74..3e0ed3cf 100644 --- a/federation/server.go +++ b/federation/server.go @@ -186,6 +186,7 @@ func (s *Server) MustMakeRoom(t ct.TestLike, roomVer gomatrixserverlib.RoomVersi // to re-use the same port as a previous one, which // * reduces noise when searching through logs and // * prevents homeservers from getting confused when multiple test cases re-use the same homeserver deployment. + // This value is temporary for domainless room IDs and will be replaced with the create event ID. roomID := fmt.Sprintf("!%d-%s:%s", len(s.rooms), util.RandomString(18), s.serverName) room := NewServerRoom(roomVer, roomID) for _, opt := range opts {