Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR: ESIG-RDAC

Author: Flori Ava Star Weber

Co-Authors: Rasmus Kjær

Reviewers: Pierre

For room events, we’d like to sign them and verify signatures and authorization, and also be able to handle redactions. This ADR outlines the architecture of arminius-lib-event, a library crate designed to be an easy to use “one-stop-shop” for everything we’d conceivably like to do with room events throughout arminius. Since ruma already implements 85% of “core” behaviors, such as

  • Verifying the auth chain for an event
  • Canonicalizing events
  • Redacting an event

in some form, arminius-lib-event is an abstraction, aimed at bringing all of these behaviours together, creating a type that neatly interfaces with the rest of our code.

Note

Historical Information

As of 2025-12-17, the branch flori/esig-rdac contained a naive sketch of what such a crate might look like. After a first round of reviews pointed out this approaches’ flaws and shortcomings, this ADR was written.

Redesigning RoomEvent

I’d go with my original idea of encoding the state of the event into the type system using generics. States would, in this example, be RoomEvent<Draft>, RoomEvent<Unsigned>, RoomEvent<Signed> and RoomEvent<Verified>. These states would represent the complete lifecycle of an event in all cases:

RoomEvent<Draft>

A Draft RoomEvent contains only content and type and would replace RawEvent. It is essentially just raw user intent (e.g. “Xenia says Hello”).

RoomEvent<Unsigned>

Has auth_events and prev_events added. This would replace the InsertableEvent

RoomEvent<Signed>

Hash event_id added, and signatures generated.

RoomEvent<Verified>

Same as Signed, but with additional metadata on when the verification was attempted. Verification failures get represented through their own error type, such as a enum ValidationError { ... }, containing all the different ways in which a verification can fail.

Tip

A “verification attempt” describes an instance of the verifying function being called on a Signed event. The phrase makes no assumption about whether the verification succeeds. The metadata gets added regardless of whether the verification succeeded.

This invariant ensures that events are guaranteed to be verified at compile-time.

State Transition

stateDiagram-v2
    userEvent: An incoming client request that should create some event
    federatedEvent: Other homeservers
    databaseEvent: Postgres Database

    draftEvent: RoomEvent&ltDraft&gt
    unsignedEvent: RoomEvent&ltUnsigned&gt
    signedEvent: RoomEvent&ltSigned&gt
    verifiedEvent: RoomEvent&ltVerified&gt

    userEvent --> draftEvent: A user can create a draft event
    draftEvent --> unsignedEvent: Our own homeserver attaches auth_events and prev_events
    unsignedEvent --> verifiedEvent: Our own homeserver signs our own events. An event signed by us is inherently verified.
    signedEvent --> verifiedEvent: Our own homeserver can verify foreign events

    federatedEvent --> signedEvent: We can receive events from other homeservers

    databaseEvent --> verifiedEvent: We can read events from the database
    verifiedEvent --> databaseEvent: We can write verified events to the database

    verifiedEvent --> federatedEvent: We can send verified events to other HSs

    classDef eventOrigin fill:white
    class userEvent, federatedEvent, databaseEvent eventOrigin

Draft to Unsigned

To make an Unsigned out of a Draft, the Draft must be contextualized into the DAG of the room.

An extremely naive function (not taking account current database schemas) could take the origin_server_ts, prev_events and auth_events. Auth checks also happen in this step. This invariant also means that Unsigned events are guaranteed to be authorized, which I think is very neat, and has the potential of eliminating a whole class of bugs, because the compiler prevents you from reaching the .sign() method without a successful contextualization function call.

Signing

RoomEvent<Unsigned> exposes a method to sign the event. This yields a RoomEvent<Verified> on success.

Redactions

A redacted event is also just one of Unsigned, Signed, Verified.

Note

Alternative considerations

I previously thought of representing redacted events through their own Redacted state, and using clever trait boundary shenanigans to allow state transitions from both Signed and Redacted into Verified. However, this potentially adds a lot of complexity for something that can be simplified/represented differently.

Verifying

The state transition from Signed to Verified is a method on Signed: async fn verify(key_provider: impl KeyFetcher) -> Result<RoomEvent<Verified>, ValidationError>. KeyFetcher would be a trait, abstracting the fetching logic, implemented by any federation client.

Alternatively…

…don’t abstract with KeyFetcher, just give a FederationNatsClient or something like that directly. Idk. Abstracting feels like we’d do our future selves a favor.

Federation Versus Client Events

RoomEvent<Verified> would expose these methods:

  • .to_pdu(), returning a PDU with DAG links and whatnot
  • .to_client_event(ctx: ClientContext), where ClientContext contains age and transaction_id to populate the unsigned field. Also strips federation fields.

Intended Use Cases

User Fetching a Single Event from the Database

When fetching an event from the database, we receive it as a RoomEvent<Verified>. There are a wide variety of kinds of unsigned information that the user may want, depending on their use case. We provide some ctx: ClientContext in order to construct this context, and then use .to_client_event(ctx)

Receiving an Event from a Client

When a user sends the event, we take the event content and type they sent us, and construct a RoomEvent<Draft>. We then perform the transition Draft -> Unsigned -> Signed, and write it to the database.

Receiving an Event over Federation

When we receive an event from another homeserver, we instantiate it as RoomEvent<Unsigned>. We then use the transition Unsigned -> Verified

Sending an Event over Federation

If we have an event in the database, that another homeserver needs to see, we can fetch the RoomEvent<Verified> from the database.