Port Event.unsigned field to Rust#19708
Port Event.unsigned field to Rust#19708erikjohnston wants to merge 15 commits intoerikj/port_signatures_fieldfrom
Event.unsigned field to Rust#19708Conversation
0935c3d to
764b1eb
Compare
e0bb615 to
4467294
Compare
We've always done this copy, I'm not sure why. We don't share the unsigned field between events so it's not that. We also edit the event that is in the cache, so its not even to stop it from getting added to the cached event.
There was a problem hiding this comment.
Pull request overview
Ports the Event.unsigned field to a Rust-backed Unsigned class, tightening which unsigned keys are retained/persisted and improving federation handling of unsigned.age by converting it into Synapse’s internal age_ts.
Changes:
- Introduce Rust
Unsignedtype with separate “for event” vs “for persistence” views. - Update event persistence to store a persistence-safe event dict, and update federation parsing to compute
unsigned.age_tsfromunsigned.ageusing a received timestamp. - Update tests to reflect the new
age_tsbehavior and the restricted unsigned key set.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/federation/test_federation_server.py | Updates federation tests to assert age_ts behavior using received_time. |
| tests/events/test_utils.py | Updates unsigned-field tests to use recognized unsigned keys and for_event() assertions. |
| synapse/synapse_rust/events.pyi | Adds Python typing stub for the new Rust-backed Unsigned class. |
| synapse/storage/databases/main/events_worker.py | Removes redundant dict(...) conversion before adding prev_* unsigned fields. |
| synapse/storage/databases/main/events.py | Persists events via get_dict_for_persistence() to avoid persisting in-memory-only unsigned fields. |
| synapse/handlers/message.py | Comments out a room_state unsigned mutation during remote invite signing flow. |
| synapse/federation/federation_server.py | Passes request_time into PDU parsing to compute age_ts. |
| synapse/federation/federation_client.py | Passes a consistent received_time when parsing fetched missing events. |
| synapse/federation/federation_base.py | Adds received_time plumbing and converts unsigned.age → unsigned.age_ts. |
| synapse/events/utils.py | Updates maybe_upsert_event_field to accept the Rust Unsigned container. |
| synapse/events/init.py | Wraps event unsigned in Rust Unsigned; adds get_dict_for_persistence(); adjusts unsigned extraction. |
| rust/src/events/unsigned.rs | New Rust implementation of Unsigned with allowed-field enforcement and persistence/event views. |
| rust/src/events/mod.rs | Registers the new Rust Unsigned class with the Python module. |
| changelog.d/19708.misc | Adds changelog entry for the unsigned port. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl<'a, 'py> FromPyObject<'a, 'py> for PersistedUnsignedFields { | ||
| type Error = PyErr; | ||
|
|
||
| fn extract(obj: pyo3::Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> { | ||
| Ok(depythonize(&obj)?) | ||
| } | ||
| } |
There was a problem hiding this comment.
Is this any different from the blanket implementation you get from #[pyclass(from_py_object)]?
There was a problem hiding this comment.
Yes, as the default is to convert the fields to attributes (rather than dict items). We actually don't rely on these traits now, so for clarity I've removed them and replaced usages with explicit depythonize.
| invitee.domain, event | ||
| ) | ||
| event.unsigned.pop("room_state", None) | ||
| # event.unsigned.pop("room_state", None) |
There was a problem hiding this comment.
Ooops, how did I miss that. This is actually a no-op since that field doesn't actually exist (it's called 'invite_room_state'), so I've removed it.
This isn't actually what the field is called, so it's always a no-op. (The actual field name is 'invite_room_state').
We didn't actually rely on them, and it is a bit misleading given the Python equivalents are dicts (rather than proper objects).
Turns out the we have a `clone_event(..)` that roundtrips the unsigned field, and so we need to also parse the unpersisted fields. When we receive a PDU over federation we filter the fields, so we don't need to worry that we accidentally accept internal fields from remote sources here.
Teh `depythonize` accepts any mapping happily.
4ee1ea0 to
072b09c
Compare
This isn't really related to this PR (which just moves the logic around), but got raised in review.
072b09c to
a99443f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
synapse/federation/federation_base.py:365
- Typo in the error message: "intger" should be "integer".
if type(depth) is not int: # noqa: E721
raise SynapseError(400, "Depth %r not an intger" % (depth,), Codes.BAD_JSON)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Handle the `age` field, which is sent by some servers as part of the | ||
| # `unsigned` block. We convert this into an `age_ts` field, which is | ||
| # what Synapse uses internally. We also remove the `age` field to avoid | ||
| # confusion. | ||
| # | ||
| # c.f. https://github.com/matrix-org/synapse/issues/8429 | ||
| unsigned = pdu_json["unsigned"] |
There was a problem hiding this comment.
Added a blunt try/except so that failing to deserialize doesn't block the entire transaction: a5bcd34.
We should probably do something better here, like make make_event_from_dict always return a SynapseError for invalid data, but that is quite subtle and not really what happens today.
Question: should we drop the invite_room_state if its the wrong format rather than dropping the entire event?
| if get_prev_content: | ||
| if "replaces_state" in event.unsigned: | ||
| prev = await self.get_event( | ||
| event.unsigned["replaces_state"], | ||
| get_prev_content=False, | ||
| allow_none=True, | ||
| ) | ||
| if prev: | ||
| event.unsigned = dict(event.unsigned) | ||
| event.unsigned["prev_content"] = prev.content | ||
| event.unsigned["prev_sender"] = prev.sender |
There was a problem hiding this comment.
This is fine, I've added some comments and an extra check in d2d4c9e
|
The complement tests are failing due to not having pulled in develop (as I haven't merged the base branch yet) |
Similar to #19706, let's port the
unsignedfield into a Rust class.This does change things a bit in that we now define exactly what unsigned fields that are allowed to be added to an event, and what actually gets persisted. This should be a noop though, as we carefully filter out what unsigned fields we allow in from federation, for example
As a side effect of this cleanup, I think this fixes handling
unsigned.ageon events received over federation.