mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 22:07:34 +00:00
Build a new room's events in one Rust call
Behind `experimental_features.rust_room_creation: on`, the room handler collects the events that follow the create event (the creator's join, power levels, join rules, history visibility, the initial state, name, topic) as `(type, state_key, content)`, and `LocalEventSigner` builds, hashes and signs them all in one call. Each event's prev event, depth and auth events follow from the one before, as `create_event` gives them for a batch. Python keeps every check: the requester checks ran when the create event was built, and each built event still goes through `validate_builder`, `validate_new` and the relation check before joining the batch, which persists through `handle_new_client_event` as before. Invites are still built by `create_event`. Rooms the Rust builder does not cover are built in Python and counted as fallbacks: v1 and v2 rooms, MSC4242 state DAG rooms, appservice senders, modules with `check_event_allowed`, initial `m.room.aliases` events, and `m.call.invite` or `m.room.create` in the initial state. If Rust fails, the events are built in Python. If Python rejects them too, the request was bad and gets Python's error. Otherwise it counts as a Rust error. In tests, `SYNAPSE_TEST_RUST_ROOM_CREATION=on` turns the builder on and makes a failure fail the test. Python orders auth events by iterating a set, so the order changes with the process's hash seed. Rust keeps the order of the auth types. A test creates rooms of several configurations and room versions in Python, rebuilds their events in Rust from the same inputs, and compares them with the auth events taken as a set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVLsNh6akt4ifEizY4D1ta
This commit is contained in:
1 parent
9b6e901fa5
commit
ef18e9ebc7
8 files changed
+741
-59
No files matched your search
+161
-12
@@ -19,7 +19,10 @@
|
||||
//! redacted event with the server's key, and parse the result as an
|
||||
//! [`Event`].
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||
@@ -38,7 +41,8 @@ use super::{
|
||||
Event,
|
||||
};
|
||||
use crate::{
|
||||
canonical_json::{to_vec_canonical, CanonicalizationOptions},
|
||||
canonical_json::{to_vec_canonical, CanonicalizationOptions, MAX_VALID_INTEGER},
|
||||
event_auth::auth_types_for,
|
||||
federation::signing::FederationSigningKey,
|
||||
room_versions::{EventFormatVersions, RoomVersion},
|
||||
};
|
||||
@@ -73,29 +77,174 @@ impl LocalEventSigner {
|
||||
room_version: &Bound<'_, PyAny>,
|
||||
internal_metadata_dict: &Bound<'_, PyDict>,
|
||||
) -> PyResult<Event> {
|
||||
let room_version: &'static RoomVersion = room_version
|
||||
.getattr("identifier")?
|
||||
.extract::<&str>()?
|
||||
.parse()
|
||||
.map_err(|e| PyValueError::new_err(format!("Unsupported room version: {e}")))?;
|
||||
let room_version = room_version_from_py(room_version)?;
|
||||
|
||||
let internal_metadata = EventInternalMetadata::new(internal_metadata_dict)?;
|
||||
|
||||
self.sign(event_json, room_version, internal_metadata)
|
||||
let event: Map<String, Value> = serde_json::from_slice(event_json)
|
||||
.context("Failed to parse event JSON")
|
||||
.map_err(|err| PyValueError::new_err(format!("{err:#}")))?;
|
||||
|
||||
self.sign(event, room_version, internal_metadata)
|
||||
.map_err(|err| PyValueError::new_err(format!("{err:#}")))
|
||||
}
|
||||
|
||||
/// Build the state events that follow the create event of a new room, as
|
||||
/// the room handler's batch builds them one by one: each event's prev
|
||||
/// event is the one before it, its depth one more, and its auth events
|
||||
/// are picked from the state before it, which starts as `state_map`.
|
||||
///
|
||||
/// `specs` are `(type, state_key, content)`, with the content as its
|
||||
/// canonical JSON. Every event gets `origin_server_ts` and a fresh copy
|
||||
/// of `internal_metadata_dict`.
|
||||
///
|
||||
/// Only for room versions whose events refer to other events by ID
|
||||
/// (not v1 and v2) and that have no state DAG (MSC4242). Raises
|
||||
/// `ValueError` for anything it cannot build, and the caller builds the
|
||||
/// events in Python instead.
|
||||
#[pyo3(signature = (
|
||||
room_version, room_id, sender, prev_event_id, depth, state_map, specs,
|
||||
origin_server_ts, internal_metadata_dict,
|
||||
))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_room_events(
|
||||
&self,
|
||||
room_version: &Bound<'_, PyAny>,
|
||||
room_id: &str,
|
||||
sender: &str,
|
||||
prev_event_id: String,
|
||||
depth: i64,
|
||||
state_map: HashMap<(String, String), String>,
|
||||
specs: Vec<(String, String, Vec<u8>)>,
|
||||
origin_server_ts: i64,
|
||||
internal_metadata_dict: &Bound<'_, PyDict>,
|
||||
) -> PyResult<Vec<Event>> {
|
||||
let room_version = room_version_from_py(room_version)?;
|
||||
let to_py = |err: anyhow::Error| PyValueError::new_err(format!("{err:#}"));
|
||||
|
||||
if room_version.event_format == EventFormatVersions::ROOM_V1_V2
|
||||
|| room_version.msc4242_state_dags
|
||||
{
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"Cannot build a room batch for room version {room_version}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut state_map = state_map;
|
||||
let mut prev_event_id = prev_event_id;
|
||||
let mut events = Vec::with_capacity(specs.len());
|
||||
|
||||
for (depth, (event_type, state_key, content_json)) in (depth..).zip(specs) {
|
||||
let internal_metadata = EventInternalMetadata::new(internal_metadata_dict)?;
|
||||
let content: Map<String, Value> = serde_json::from_slice(&content_json)
|
||||
.context("Failed to parse event content")
|
||||
.map_err(to_py)?;
|
||||
|
||||
let auth_event_ids = room_batch_auth_event_ids(
|
||||
room_version,
|
||||
room_id,
|
||||
&event_type,
|
||||
sender,
|
||||
&state_key,
|
||||
&content,
|
||||
&state_map,
|
||||
)
|
||||
.map_err(to_py)?;
|
||||
|
||||
let mut unsigned = Map::new();
|
||||
unsigned.insert(AGE_TS.to_owned(), Value::from(origin_server_ts));
|
||||
|
||||
let mut event = Map::new();
|
||||
event.insert("auth_events".to_owned(), Value::from(auth_event_ids));
|
||||
event.insert(
|
||||
"prev_events".to_owned(),
|
||||
Value::from(vec![prev_event_id.clone()]),
|
||||
);
|
||||
event.insert("type".to_owned(), Value::from(event_type.clone()));
|
||||
event.insert("sender".to_owned(), Value::from(sender));
|
||||
event.insert("content".to_owned(), Value::Object(content));
|
||||
event.insert(UNSIGNED.to_owned(), Value::Object(unsigned));
|
||||
// Python caps the depth of new events, so other servers accept
|
||||
// them.
|
||||
event.insert(
|
||||
"depth".to_owned(),
|
||||
Value::from(depth.min(MAX_VALID_INTEGER)),
|
||||
);
|
||||
event.insert("room_id".to_owned(), Value::from(room_id));
|
||||
event.insert("state_key".to_owned(), Value::from(state_key.clone()));
|
||||
event.insert("origin_server_ts".to_owned(), Value::from(origin_server_ts));
|
||||
event.insert(SIGNATURES.to_owned(), Value::Object(Map::new()));
|
||||
|
||||
let event = self
|
||||
.sign(event, room_version, internal_metadata)
|
||||
.map_err(to_py)?;
|
||||
|
||||
prev_event_id = event.event_id().to_owned();
|
||||
state_map.insert((event_type, state_key), prev_event_id.clone());
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
|
||||
/// The room version Python passes, by its identifier.
|
||||
fn room_version_from_py(room_version: &Bound<'_, PyAny>) -> PyResult<&'static RoomVersion> {
|
||||
room_version
|
||||
.getattr("identifier")?
|
||||
.extract::<&str>()?
|
||||
.parse()
|
||||
.map_err(|e| PyValueError::new_err(format!("Unsupported room version: {e}")))
|
||||
}
|
||||
|
||||
/// The auth events of a new state event, as `compute_auth_events` and the
|
||||
/// builder pick them: the events in `state_map` for the auth types of the
|
||||
/// event, in that order. In MSC4291 rooms the create event is implied by the
|
||||
/// room ID, so it is left out.
|
||||
fn room_batch_auth_event_ids(
|
||||
room_version: &RoomVersion,
|
||||
room_id: &str,
|
||||
event_type: &str,
|
||||
sender: &str,
|
||||
state_key: &str,
|
||||
content: &Map<String, Value>,
|
||||
state_map: &HashMap<(String, String), String>,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let content: BTreeMap<Box<str>, Value> = content
|
||||
.iter()
|
||||
.map(|(key, value)| (Box::from(key.as_str()), value.clone()))
|
||||
.collect();
|
||||
let auth_types = auth_types_for(room_version, event_type, sender, Some(state_key), &content)
|
||||
.map_err(|err| anyhow::anyhow!("{err}"))?;
|
||||
|
||||
let mut auth_event_ids: Vec<String> = auth_types
|
||||
.into_iter()
|
||||
.filter_map(|(auth_type, auth_state_key)| {
|
||||
state_map
|
||||
.get(&(auth_type.to_owned(), auth_state_key.to_owned()))
|
||||
.filter(|event_id| !event_id.is_empty())
|
||||
.cloned()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if room_version.msc4291_room_ids_as_hashes {
|
||||
let create_event_id = format!("${}", &room_id[1..]);
|
||||
let Some(index) = auth_event_ids.iter().position(|id| *id == create_event_id) else {
|
||||
bail!("the create event {create_event_id} is not in the state");
|
||||
};
|
||||
auth_event_ids.remove(index);
|
||||
}
|
||||
|
||||
Ok(auth_event_ids)
|
||||
}
|
||||
|
||||
impl LocalEventSigner {
|
||||
fn sign(
|
||||
&self,
|
||||
event_json: &[u8],
|
||||
mut event: Map<String, Value>,
|
||||
room_version: &'static RoomVersion,
|
||||
internal_metadata: EventInternalMetadata,
|
||||
) -> anyhow::Result<Event> {
|
||||
let mut event: Map<String, Value> =
|
||||
serde_json::from_slice(event_json).context("Failed to parse event JSON")?;
|
||||
|
||||
let reference_hash = add_hashes_and_signature(
|
||||
&mut event,
|
||||
room_version,
|
||||
|
||||
Reference in new issue
Block a user