Skip to main content

arminius_lib_events/
states.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Event lifecycle state type definitions.
6//!
7//! This module defines the four states an event can be in during its lifecycle:
8//!
9//! - [`Draft`]: Raw user intent (content + type only)
10//! - [`Contextualized`]: Contextualized in the DAG with auth/prev events
11//! - [`Signed`]: Signed with event ID and signatures
12//! - [`Verified`]: Verified signatures and authorization
13
14use ruma::{
15	CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
16	OwnedServerName, OwnedUserId,
17	canonical_json::{self},
18	events::{
19		AnyMessageLikeEventContent, AnyStateEventContent, RawExt as _, TimelineEventType,
20		room::{create::RoomCreateEventContent, member::MembershipState},
21	},
22	room_version_rules::EventIdFormatVersion,
23	serde::Raw,
24};
25use serde::{Deserialize, Serialize};
26
27use crate::{
28	EventFormatOptions, EventHash, Pdu, PduCommon, V1Pdu, V3Pdu, V12Pdu,
29	conversions::raw_json,
30	errors::{
31		EventContextualizationError, EventIdGenerationError, MismatchedEventRefsError, SigningError,
32	},
33	pdu::V6Pdu,
34	types::{
35		EventRefs, KnownEventsReferenceFormatVersion, KnownRoomVersionId, PduFormat, StateKey,
36		VerificationMetadata,
37	},
38};
39
40/// State representing a draft event containing only user intent.
41///
42/// A draft is the initial state of an event,
43#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct Draft {
45	/// Matrix timeline event type.
46	pub timeline_event_type: TimelineEventType,
47	/// Raw JSON event content.
48	pub content: Box<serde_json::value::RawValue>,
49	/// State key for state events (`Some`), or `None` for message-like
50	/// events. State events may carry an empty-string state key (e.g.
51	/// `m.room.create`).
52	pub state_key: Option<StateKey>,
53	/// The user that sent the event
54	pub(crate) sender: OwnedUserId,
55	/// Redacted event id for redaction events
56	pub(crate) redacts: Option<OwnedEventId>,
57}
58
59impl Draft {
60	/// Initializes a [`Draft`] from, validating content
61	/// shape.
62	///
63	/// ## Errors
64	///
65	/// Returns a `serde_json::Error` if the `content` does not have the format
66	/// expected of the `event_type`
67	///
68	/// > "Re-fried deep fried beans!"
69	pub fn new(
70		event_type: TimelineEventType,
71		content: Box<serde_json::value::RawValue>,
72		state_key: Option<StateKey>,
73		sender: OwnedUserId,
74		redacts: Option<OwnedEventId>,
75	) -> Result<Self, serde_json::Error> {
76		Self::build(event_type, content, state_key, sender, redacts, false)
77	}
78
79	/// Initializes a [`Draft`] from the content of a federation PDU, tolerating
80	/// redacted content.
81	///
82	/// Unlike [`Draft::new`], empty (`{}`) message-like content is accepted
83	/// rather than rejected, since a redacted event may have had its content
84	/// stripped to `{}`. This is only appropriate for already-signed events
85	/// received over federation, never for client-originated content, which
86	/// must go through [`Draft::new`].
87	///
88	/// This constructor lives on [`Draft`] rather than on [`Signed`] because it
89	/// runs while a PDU is being parsed into a [`Signed`], before that value
90	/// exists.
91	///
92	/// ## Errors
93	///
94	/// Returns a `serde_json::Error` if the `content` does not have the format
95	/// expected of the `event_type`.
96	pub(crate) fn from_signed(
97		event_type: TimelineEventType,
98		content: Box<serde_json::value::RawValue>,
99		state_key: Option<StateKey>,
100		sender: OwnedUserId,
101		redacts: Option<OwnedEventId>,
102	) -> Result<Self, serde_json::Error> {
103		Self::build(event_type, content, state_key, sender, redacts, true)
104	}
105
106	/// Shared constructor validating content shape.
107	///
108	/// When `allow_redacted_content` is set, empty (`{}`) message-like content
109	/// is accepted instead of being validated, to allow loading redacted
110	/// events.
111	fn build(
112		event_type: TimelineEventType,
113		content: Box<serde_json::value::RawValue>,
114		state_key: Option<StateKey>,
115		sender: OwnedUserId,
116		redacts: Option<OwnedEventId>,
117		allow_redacted_content: bool,
118	) -> Result<Self, serde_json::Error> {
119		// Deserialize it once, to make sure the body has all the required
120		// fields
121
122		if state_key.is_some() {
123			Raw::<AnyStateEventContent>::from_json(content.clone())
124				.deserialize_with_type(&event_type.to_string())?;
125		} else {
126			// FIXME(#198): Accept redacted state events too.
127			//
128			// Skipping validation for empty (`{}`) message-like content is a
129			// temporary hack to import rooms from Synapse servers, which
130			// contain redacted events. Redacting an event strips its content
131			// down to the keys the room version's redaction rules preserve for
132			// that event type; for some message-like events that leaves `{}`,
133			// which no typed content can parse. Accepting `{}` here lets those
134			// redacted events load without validation.
135			//
136			// This is only a heuristic: `{}` is not a reliable marker of
137			// redaction. Some events keep content keys after redaction (e.g.
138			// `m.room.redaction`, itself a message-like event, keeps
139			// `content.redacts` from room version 11 on), and some non-redacted
140			// events may legitimately carry empty content. We restrict the skip
141			// to message-like events only because redacted *state* events
142			// aren't handled yet; rooms rarely redact state events, so
143			// this unblocks testing for now.
144			let is_empty_content =
145				serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(content.get())
146					.is_ok_and(|map| map.is_empty());
147			if !(allow_redacted_content && is_empty_content) {
148				Raw::<AnyMessageLikeEventContent>::from_json(content.clone())
149					.deserialize_with_type(&event_type.to_string())?;
150			}
151		}
152
153		Ok(Self { timeline_event_type: event_type, content, state_key, sender, redacts })
154	}
155
156	/// Initialize a new Draft state, assuming it has been redacted.
157	///
158	/// This function assumes that the event content is correct event content,
159	/// which has passed through the [`Draft::new`], and subsequently the
160	/// redaction module. Therefore, no explicit verification is made
161	///
162	/// ## Errors
163	///
164	/// Returns a `serde_json::Error` if, for a room-create event, the redacted
165	/// content cannot be parsed as `m.room.create` content.
166	pub fn redact(
167		&mut self,
168		redacted_content: Box<serde_json::value::RawValue>,
169	) -> Result<(), serde_json::Error> {
170		// FIXME(#1889): This requirement should be enforced on the compiler
171		// level, e.g. by a type
172		if self.timeline_event_type == TimelineEventType::RoomCreate {
173			serde_json::from_str::<RoomCreateEventContent>(redacted_content.get())?;
174		}
175		self.content = redacted_content;
176		Ok(())
177	}
178
179	/// Get the event content as a JSON object map.
180	///
181	/// ## Panics
182	///
183	/// Panics if the content is not a valid JSON object.
184	/// [`crate::states::Draft::new`] guarantees that the content is valid, so
185	/// this is only reachable for events constructed bypassing that
186	/// constructor.
187	pub(crate) fn content_to_map(&self) -> serde_json::Map<String, serde_json::Value> {
188		#[expect(
189			clippy::expect_used,
190			reason = "The Draft struct guarantees that the draft field is always correct"
191		)]
192		serde_json::from_str(self.content.get())
193			.expect("We guarantee that the content field here is correct")
194	}
195}
196
197/// Used for checking round-trip correctness of converting an event to and from
198/// a PDU
199///
200/// Only enabled for tests, because the way we compare the content is not
201/// completely good
202#[cfg(any(feature = "test_utils", test))]
203impl PartialEq for Draft {
204	fn eq(&self, other: &Self) -> bool {
205		if self.timeline_event_type != other.timeline_event_type
206			|| self.sender != other.sender
207			|| self.state_key != other.state_key
208			|| self.redacts != other.redacts
209		{
210			return false;
211		}
212		let v1 = serde_json::from_str::<serde_json::Value>(self.content.get());
213		let v2 = serde_json::from_str::<serde_json::Value>(other.content.get());
214		match (v1, v2) {
215			(Ok(v1), Ok(v2)) => v1 == v2,
216			(Ok(_), Err(_)) | (Err(_), Ok(_)) => false,
217			(Err(_), Err(_)) => self.content.get() == other.content.get(),
218		}
219	}
220}
221
222#[cfg(any(feature = "test_utils", test))]
223impl Eq for Draft {}
224
225/// State representing an event contextualized in the room DAG.
226///
227/// A contextualized event has been placed in the context of the room's directed
228/// acyclic graph (DAG) by adding `auth_events` and `prev_events` references,
229/// along with the `origin_server_ts` and room version rules.
230///
231/// This state does not guarantee authorization - that check happens during
232/// verification via [`super::RoomEvent<Signed>::verify()`].
233#[cfg_attr(any(feature = "test_utils", test), derive(Eq, PartialEq))]
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct Contextualized {
236	/// The draft event data
237	pub(crate) draft: Draft,
238	/// The depth of the event
239	pub(crate) depth: u64,
240	/// Room version for the event
241	pub(crate) room_version: KnownRoomVersionId,
242	/// The auth events
243	pub(crate) auth_events: EventRefs,
244	/// The previous events
245	pub(crate) prev_events: EventRefs,
246	/// The previous state field.
247	///
248	/// Not externally visible outside of the crate, but used by some room
249	/// versions for nothing
250	pub(crate) prev_state: Option<EventRefs>,
251	/// The membership field.
252	///
253	/// Not externally visible outside of the crate, but used by some room
254	/// versions for nothing
255	pub(crate) membership: Option<MembershipState>,
256	/// The origin field.
257	///
258	/// Not externally visible outside of NLE, but used by some room versions
259	/// for nothing
260	pub(crate) origin: Option<OwnedServerName>,
261	/// The origin server timestamp
262	pub(crate) origin_server_ts: MilliSecondsSinceUnixEpoch,
263	/// The event ID
264	pub(crate) event_id: OwnedEventId,
265	/// The room ID
266	pub(crate) room_id: OwnedRoomId,
267	/// The content hashes
268	pub(crate) hashes: EventHash,
269}
270
271impl Contextualized {
272	/// Contextualize an event
273	///
274	/// Can optionally be provided an event ID. If none is provided, one will
275	/// always be generated, also for room versions 1 and 2.
276	#[expect(clippy::too_many_arguments, reason = "Matches the contents of the struct")]
277	pub(crate) fn new(
278		draft: Draft,
279		depth: u64,
280		room_version: KnownRoomVersionId,
281		auth_events: EventRefs,
282		prev_events: EventRefs,
283		prev_state: Option<EventRefs>,
284		membership: Option<MembershipState>,
285		origin: Option<OwnedServerName>,
286		origin_server_ts: MilliSecondsSinceUnixEpoch,
287		event_id: Option<OwnedEventId>,
288		room_id: Option<OwnedRoomId>,
289		hashes: Option<EventHash>,
290	) -> Result<Self, EventContextualizationError> {
291		match room_version.events_reference_format() {
292			KnownEventsReferenceFormatVersion::V1 => {
293				if !matches!(auth_events, EventRefs::Hashed(_) | EventRefs::Empty)
294					|| !matches!(prev_events, EventRefs::Hashed(_) | EventRefs::Empty)
295				{
296					return Err(MismatchedEventRefsError::ExpectedV1GotV2.into());
297				}
298			}
299			KnownEventsReferenceFormatVersion::V2 => {
300				if !matches!(auth_events, EventRefs::Plain(_) | EventRefs::Empty)
301					|| !matches!(prev_events, EventRefs::Plain(_) | EventRefs::Empty)
302				{
303					return Err(MismatchedEventRefsError::ExpectedV2GotV1.into());
304				}
305			}
306		}
307		if auth_events.len() > 10 {
308			return Err(EventContextualizationError::TooManyAuthEvents);
309		}
310		if prev_events.len() > 20 {
311			return Err(EventContextualizationError::TooManyPrevEvents);
312		}
313
314		// If we don't keep these fields after redaction, there's no point at
315		// all to keeping them
316		let origin = room_version
317			.rules()
318			.redaction
319			.keep_origin_membership_prev_state
320			.then_some(origin)
321			.flatten();
322		let membership = room_version
323			.rules()
324			.redaction
325			.keep_origin_membership_prev_state
326			.then_some(membership)
327			.flatten();
328		let prev_state = room_version
329			.rules()
330			.redaction
331			.keep_origin_membership_prev_state
332			.then_some(prev_state)
333			.flatten();
334
335		// We generate a temporary object, just to generate an `event_id` and
336		// potentially a `room_id`
337		let mut temporary_contextualized = Self {
338			draft,
339			depth,
340			room_version,
341			auth_events,
342			prev_events,
343			membership,
344			origin: origin.clone(),
345			origin_server_ts,
346			prev_state,
347			event_id: ruma::owned_event_id!("$event"),
348			room_id: room_id.clone().unwrap_or_else(|| ruma::owned_room_id!("!dummy")),
349			hashes: EventHash::new(""),
350		};
351
352		temporary_contextualized.hashes = match hashes {
353			None => temporary_contextualized.generate_hashes()?,
354			Some(hashes) => hashes,
355		};
356
357		let event_id = match room_version {
358			KnownRoomVersionId::V1 | KnownRoomVersionId::V2 => event_id.map_or_else(
359				|| {
360					temporary_contextualized.generate_event_id(Some(&origin.unwrap_or_else(|| {
361						temporary_contextualized.draft.sender.server_name().to_owned()
362					})))
363				},
364				Ok,
365			)?,
366			KnownRoomVersionId::V3
367			| KnownRoomVersionId::V4
368			| KnownRoomVersionId::V5
369			| KnownRoomVersionId::V6
370			| KnownRoomVersionId::V7
371			| KnownRoomVersionId::V8
372			| KnownRoomVersionId::V9
373			| KnownRoomVersionId::V10
374			| KnownRoomVersionId::V11
375			| KnownRoomVersionId::V12 => temporary_contextualized
376				.generate_event_id(Some(temporary_contextualized.draft.sender.server_name()))?,
377		};
378
379		#[expect(
380			clippy::expect_used,
381			reason = "We know that event IDs will have a $ prefix, and that the room ID will be the correct format"
382		)]
383		let room_id = if let Some(room_id) = room_id {
384			room_id
385		} else {
386			ruma::RoomId::parse(format!(
387				"!{}",
388				event_id.to_string().strip_prefix("$").expect("We know event IDs have this prefix")
389			))
390			.expect("We know this must be correct")
391		};
392
393		Ok(Self { event_id, room_id, ..temporary_contextualized })
394	}
395
396	/// Recovers the [`Contextualized`] event from a [`Signed`] one for
397	/// re-signing, discarding the existing signatures and hashes.
398	///
399	/// Only `m.room.member` events are supported, since re-signing from an
400	/// already-signed state is currently only performed on member events.
401	/// Room-create events are rejected here rather than special-cased.
402	///
403	/// The content was already validated when the [`Signed`] event was built,
404	/// so the inner [`Contextualized`] is reused as-is without re-validation.
405	///
406	/// ## Errors
407	///
408	/// Returns [`SigningError::InvalidContent`] if `signed`'s event type is not
409	/// `m.room.member`.
410	pub(crate) fn from_signed_for_resign(signed: Signed) -> Result<Self, SigningError> {
411		if signed.contextualized.draft.timeline_event_type != TimelineEventType::RoomMember {
412			return Err(SigningError::InvalidContent(
413				"from_signed_for_resign only reconstructs m.room.member events".to_owned(),
414			));
415		}
416		Ok(signed.contextualized)
417	}
418
419	/// Builds the minimal event object for signing and hashing.
420	///
421	/// ## Panics
422	///
423	/// Panics if a non-create event has no room ID.
424	#[must_use]
425	fn build_minimal_event_object(&self) -> serde_json::Map<String, serde_json::Value> {
426		let room_version_rules = &self.room_version.rules();
427		let event_format = EventFormatOptions::from_room_version(room_version_rules);
428		let mut content_map = self.draft.content_to_map();
429		let redacts_value =
430			self.draft.redacts.as_ref().map(|id| serde_json::Value::String(id.to_string()));
431		let redacts_is_content_field = event_format.content_field_redacts;
432		if let Some(ref value) = redacts_value
433			&& redacts_is_content_field
434		{
435			content_map.insert("redacts".to_owned(), value.clone());
436		}
437
438		let mut event = serde_json::Map::new();
439		event.insert(
440			"type".to_owned(),
441			serde_json::Value::String(self.draft.timeline_event_type.to_string()),
442		);
443		event.insert("content".to_owned(), serde_json::Value::Object(content_map));
444
445		let is_room_create = !event_format.require_room_create_room_id
446			&& matches!(self.draft.timeline_event_type, TimelineEventType::RoomCreate);
447		if !is_room_create {
448			event.insert("room_id".to_owned(), serde_json::Value::String(self.room_id.to_string()));
449		}
450		event.insert("sender".to_owned(), serde_json::Value::String(self.draft.sender.to_string()));
451
452		if let Some(membership) = &self.membership {
453			event
454				.insert("membership".to_owned(), serde_json::Value::String(membership.to_string()));
455		}
456
457		if let Some(origin) = &self.origin {
458			event.insert("origin".to_owned(), serde_json::Value::String(origin.to_string()));
459		}
460
461		event.insert(
462			"origin_server_ts".to_owned(),
463			serde_json::Value::Number(serde_json::Number::from(u64::from(self.origin_server_ts.0))),
464		);
465
466		if let Some(state_key) = &self.draft.state_key {
467			event.insert("state_key".to_owned(), serde_json::Value::String(state_key.to_owned()));
468		}
469		if let Some(value) = redacts_value
470			&& !redacts_is_content_field
471		{
472			event.insert("redacts".to_owned(), value);
473		}
474
475		event.insert("auth_events".to_owned(), self.auth_events.pdu_serialize());
476		event.insert("prev_events".to_owned(), self.prev_events.pdu_serialize());
477		if let Some(prev_state) = &self.prev_state {
478			event.insert("prev_state".to_owned(), prev_state.pdu_serialize());
479		}
480		event.insert("depth".to_owned(), self.depth.into());
481		event
482	}
483
484	/// Builds the canonical event object for signing and hashing.
485	///
486	/// ## Errors
487	///
488	/// Returns a [`SigningError`] if JSON canonicalization fails.
489	pub(crate) fn build_canonical_event_object(&self) -> Result<CanonicalJsonObject, SigningError> {
490		let value = serde_json::Value::Object(self.build_minimal_event_object());
491		let canonical = canonical_json::to_canonical_value(value)
492			.map_err(|error| SigningError::JsonCanonicalization(error.to_string()))?;
493		let CanonicalJsonValue::Object(object) = canonical else {
494			panic!("When canonicalizing a JSON object, the result should also be an object");
495		};
496		Ok(object)
497	}
498
499	/// Generate the content hashes of the object
500	///
501	/// See here <https://spec.matrix.org/latest/server-server-api/#calculating-the-content-hash-for-an-event>
502	///
503	/// ## Errors
504	///
505	/// Returns an [`EventIdGenerationError`] if the event cannot be
506	/// canonicalized or hashed, or if a room version 1 event has no server
507	/// name.
508	fn generate_hashes(&self) -> Result<EventHash, EventIdGenerationError> {
509		let canonical = self
510			.build_canonical_event_object()
511			.map_err(|err| EventIdGenerationError::InvalidEvent(err.to_string()))?;
512		let hashes = ruma::signatures::content_hash(&canonical)
513			.map_err(|err| EventIdGenerationError::InvalidEvent(err.to_string()))?;
514		Ok(EventHash::new(hashes.to_string()))
515	}
516
517	/// Generates an event ID matching the room version.
518	///
519	/// ## Errors
520	///
521	/// Returns an [`EventIdGenerationError`] if the event cannot be
522	/// canonicalized or hashed, or if a room version 1 event has no server
523	/// name.
524	fn generate_event_id(
525		&self,
526		server_name: Option<&ruma::ServerName>,
527	) -> Result<OwnedEventId, EventIdGenerationError> {
528		let mut canonical = self
529			.build_canonical_event_object()
530			.map_err(|err| EventIdGenerationError::InvalidEvent(err.to_string()))?;
531		canonical.insert("hashes".to_owned(), self.hashes.to_canonical_json_value());
532
533		let reference_hash =
534			ruma::signatures::reference_hash(&canonical, &self.room_version.rules())
535				.map_err(|err| EventIdGenerationError::InvalidEvent(err.to_string()))?;
536		let event_id_str = match self.room_version.rules().event_id_format {
537			EventIdFormatVersion::V1 => {
538				let server_name = server_name.ok_or(EventIdGenerationError::V1MissingServerName)?;
539				format!("${reference_hash}:{server_name}")
540			}
541			_ => format!("${reference_hash}"),
542		};
543		#[expect(
544			clippy::expect_used,
545			reason = "This parse is guaranteed to succeed, because we construct the string ourselves"
546		)]
547		let expected_event_id =
548			ruma::EventId::parse(event_id_str).expect("Generated event ID is invalid");
549		Ok(expected_event_id)
550	}
551}
552
553/// State representing a signed event with event ID and signatures.
554///
555/// A signed event has been cryptographically signed and has a computed event ID
556/// based on the reference hash of the canonical JSON representation.
557#[derive(Debug, Clone, Serialize, Deserialize)]
558#[cfg_attr(any(feature = "test_utils", test), derive(Eq, PartialEq))]
559pub struct Signed {
560	/// The contextualized event data
561	pub(crate) contextualized: Contextualized,
562	/// The signatures
563	pub(crate) signatures: ruma::ServerSignatures,
564}
565
566impl Signed {
567	/// Build the event JSON, similar to the one for a Contextualized event, but
568	/// with the event_id, signatures, and hashes
569	#[must_use]
570	pub fn build_event_value(&self) -> serde_json::Value {
571		let mut object = self.contextualized.build_minimal_event_object();
572		if self.contextualized.room_version.rules().event_id_format == EventIdFormatVersion::V1 {
573			object.insert("event_id".to_owned(), self.contextualized.event_id.to_string().into());
574		}
575
576		#[expect(
577			clippy::expect_used,
578			reason = "The object being serialized is a map from strings to strings, which should not be fallible"
579		)]
580		object.insert(
581			"signatures".to_owned(),
582			serde_json::to_value(&self.signatures)
583				.expect("The serialization of ruma signatures cannot actually fail"),
584		);
585		object.insert("hashes".to_owned(), self.contextualized.hashes.to_json_value());
586
587		serde_json::Value::Object(object)
588	}
589
590	/// Build the event object, similar to the one for a Contextualized event,
591	/// but with the event_id, signatures, and hashes
592	pub fn build_event_object(&self) -> Result<CanonicalJsonObject, SigningError> {
593		let value = self.build_event_value();
594
595		let canonical = canonical_json::to_canonical_value(value)
596			.map_err(|error| SigningError::JsonCanonicalization(error.to_string()))?;
597
598		let CanonicalJsonValue::Object(object) = canonical else {
599			panic!("Expected an object");
600		};
601		Ok(object)
602	}
603
604	/// Builds a PDU representation for use in authorization checks.
605	#[must_use]
606	pub fn build_pdu(&self) -> Pdu {
607		let room_version = self.contextualized.room_version;
608		let pdu_format = PduFormat::from(room_version);
609		let value = self.build_event_value();
610
611		let raw = raw_json(&value);
612
613		let common = PduCommon {
614			event_id: self.contextualized.event_id.clone(),
615			room_id: self.contextualized.room_id.clone(),
616			sender: self.contextualized.draft.sender.clone(),
617			event_type: self.contextualized.draft.timeline_event_type.clone(),
618			content: self.contextualized.draft.content.clone(),
619			state_key: self.contextualized.draft.state_key.clone(),
620			origin_server_ts: self.contextualized.origin_server_ts,
621			redacts: self.contextualized.draft.redacts.clone(),
622			rejected: false,
623			raw,
624		};
625
626		match pdu_format {
627			PduFormat::V1 => Pdu::V1(V1Pdu {
628				common,
629				prev_events: Vec::<OwnedEventId>::from(&self.contextualized.prev_events),
630				auth_events: Vec::<OwnedEventId>::from(&self.contextualized.auth_events),
631				depth: self.contextualized.depth,
632			}),
633			PduFormat::V3 => Pdu::V3(V3Pdu {
634				common,
635				prev_events: Vec::<OwnedEventId>::from(&self.contextualized.prev_events),
636				auth_events: Vec::<OwnedEventId>::from(&self.contextualized.auth_events),
637				depth: self.contextualized.depth,
638			}),
639			PduFormat::V6 => Pdu::V6(V6Pdu {
640				common,
641				prev_events: Vec::<OwnedEventId>::from(&self.contextualized.prev_events),
642				auth_events: Vec::<OwnedEventId>::from(&self.contextualized.auth_events),
643				depth: self.contextualized.depth,
644			}),
645			PduFormat::V12 => Pdu::V12(V12Pdu {
646				common,
647				prev_events: Vec::<OwnedEventId>::from(&self.contextualized.prev_events),
648				auth_events: Vec::<OwnedEventId>::from(&self.contextualized.auth_events),
649				depth: self.contextualized.depth,
650			}),
651		}
652	}
653}
654
655/// State representing a fully verified event ready for persistence or
656/// distribution.
657///
658/// A verified event has passed all cryptographic signature verification and
659/// authorization checks. It is safe to store in the database or send to other
660/// homeservers.
661#[cfg_attr(any(feature = "test_utils", test), derive(Eq, PartialEq))]
662#[derive(Clone, Debug, Serialize, Deserialize)]
663pub struct Verified {
664	/// The signed event data.
665	pub(crate) signed: Signed,
666	/// The verification metadata
667	pub(crate) metadata: VerificationMetadata,
668}