0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-12-11 08:02:59 +01:00
construct/include/ircd/m/README.md
2020-09-30 16:35:34 -07:00

536 lines
22 KiB
Markdown

# Matrix Protocol
## Introduction
*The authoritative place for learning about matrix is at [matrix.org](https://matrix.org) but
it may be worthwhile to spend a moment and consider this introduction which explains things
by distilling the formal core of the protocol before introducing all of the networking and
communicative accoutrements...*
### Identity
The Matrix-ID or `mxid` is a universally unique plain-text string allowing
an entity to be addressed internet-wide which is fundamental to the matrix
federation in contrast to the traditional IRC server/network. An example of an
mxid: "@user:host" where `host` is a public DNS name, `user` is a party to
`host`, and the '@' character is replaced to convey type information. The
character, called a `sigil`, is defined to be '!' for `room_id` identifiers,
'$' for `event_id` identifiers, '#' for room aliases, and '@' for users.
### Event
The fundamental primitive of this protocol is the `event` object. This object
contains some set of key/value pairs and the protocol defines a list of such keys
which are meaningful to the protocol. Other keys which are not meaningful to the
protocol can be included directly in the `event` object but there are no guarantees
for if and how a party will pass these keys. To dive right in, here's the list
of recognized keys for an `event`:
```
auth_events
content
depth
event_id
hashes
membership
origin
origin_server_ts
prev_events
prev_state
redacts
room_id
sender
signatures
state_key
type
```
In the event structure, the value for `sender` and `room_id` and `event_id` are
all an `mxid` of the appropriate type.
The `event` object is also the *only* fundamental primitive of the protocol; in other
words: everything is an `event`. All information is conveyed in events, and governed
by rules for proper values behind these keys. The rest of the protocol specification
describes an *abstract state machine* which has its state updated by an event, in
addition to providing a standard means for communication of events between parties
over the internet. That's it.
### Timeline
The data tape of the matrix machine consists of a singly-linked list of `event`
objects with each referencing the `event_id` of its preceding parents somewhere
in the `prev_` keys; this is called the `timeline`. Each event is signed by its
creator and affirms all referenced events preceding it. This is a very similar
structure to that used by software like Git, and Bitcoin. It allows looking back
into the past from any point.
### State
The `state` consists of a subset of events which are accumulated according to a
few rules when playing the tape through the machine. Events which are selected
as `state` will overwrite a matching previously selected `state event` and thus
reduce the number of events in this set to far less than the entire `timeline`.
The `state` is then used to satisfy queries for deciding valid transitions for
the machine. This is like the "work tree" in Git when positioned at some commit.
* Events with a `state_key` are considered state.
* The identity of a `state event` is the concatenation of the `room_id`
value with the `type` value with the `state_key` value. Thus an event
with the same `room_id, type, state_key` replaces an older event in `state`.
* Some `state_key` values are empty strings `""`. This is a convention for
singleton `state` events, like an `m.room.create` event. The `state_key`
is used to represent a set, like with `m.room.member` events, where the
value of the `state_key` is a user `mxid`.
### Room
The `room` structure encapsulates an instance of the matrix machine. A room
is a container of `event` objects in the form of a timeline. The query
complexity for information in a room timeline is as follows:
- Ephemeral (non-state) events in the timeline have a linear lookup time:
the timeline must be iterated in sequence to find a satisfying message.
- State events in the timeline have a logarithmic lookup: the implementation
is expected to maintain a map of the `type`,`state_key` values for events
present in the timeline.
The matrix protocol specifies certain `event` types which are recognized to
affect the behavior of the `room`; here is a list of some types:
```
m.room.name
m.room.create
m.room.topic
m.room.avatar
m.room.aliases
m.room.canonical_alias
m.room.join_rules
m.room.power_levels
m.room.member
m.room.message
...
```
Some of these events are state events and some are ephemeral (these will be
detailed later). All `m.room.*` namespaced events govern the functionality of the
room. Rooms may contain events of any `type`, but we don't invent new `m.room.*`
type events ourselves. This project tends to create events in the namespace
`ircd.*` These events should not alter the room's functionality for a client
with knowledge of only the published `m.room.*` events wouldn't understand.
### Server
Matrix rooms are intended to be distributed entities that are replicated by
all participating servers. It is important to make this clear by contrasting
it with a common assumption that rooms are "hosted" by some entity. For example
when one sees an `mxid` such as `!matrix:matrix.org` it is incorrect to assume
that `matrix.org` is "hosting" this room or that it plays any critical role in
its operation.
Servers are simply obliged to adhere to the rules of the protocol. There is no
cryptographic guarantee that rules will be followed (e.g. zkSNARK), and no
central server to query as an authority. Servers should disregard violations
of the protocol as the room unfolds from the point of its creation.
The notion of hosting does exist for room aliases. In the above example the
room mxid `!matrix:matrix.org` may be referred to by `#matrix:matrix.org`.
As an analogy, if one considers the room mxid to be an IP address then the
alias is like a domain name pointing to the IP address. The example alias is
hosted by `matrix.org` under their authority and may be directed to any room
mxid. The alias is not useful when its host is not available, but the room
itself is still available from all other servers.
### Communication
Servers communicate by broadcasting events to all other servers joined to the
room. When a server wishes to broadcast a message, it constructs an event which
references all previous events in the timeline which have not yet been
referenced. This forms a directed acyclic graph of events, or DAG.
The conversation of messages moves in one direction: past to future. Messages
only reference other messages which have a lower degree of separation indicated
by the `depth` from the first message in the graph (where `type` was
`m.room.create`).
* The monotonic increase in `depth` contributes to an intuitive "light cone"
read coherence. Knowledge of any piece of information (like an event) offers
strongly ordered knowledge of all known information which preceded it at
that point.
* Write consistency is relaxed. Multiple messages may be issued at the same
depth from independent actors and multiple reference trees may form
independent of others. This provides the scalar for performance in a large
distributed internet system.
#### DAG
The DAG is a tool which aids with the presentation of a coherent conversation
for the room in lieu of the relaxed write consistency as previously mentioned.
In this section we will analyze various examples of DAG's and how certain
cases are approached by our implementation.
```
[M00]
|
[M01]
```
Above: a very simple DAG: message M01 references M00; this means M00 is in the
light-cone of M01. In other words, when one obtains M01 one obtains M00 along
with it (M01 *brings* M00 and everything behind it at that point). This is the
theoretical causality offered by the DAG, which is considered one of the stronger
types of orderings in the various schemes known to distributed system academics.
```
[M00]
|
[M01]
|
[M02]
```
Above: The normal addition of M02. Now M01 and M00 are "brought by" M02.
```
[M00]--o
| |
[M01] |
| |
[M02]--o
```
Above: Example of a redundant reference. Note that this is a valid
configuration in the matrix protocol and implementations; we do not reject M02
as ill-formed though our implementation prefers to avoid generating such
references if possible, it may still do so legitimately. We consider this
redundant because:
1. M00 is already reachable through the reference to M01; the reference around
M01 reveals no new information in the light-cone which wasn't already known.
2. Since M02 absolutely happens-after M01 and the same holds true for M01 and
M00: distinguishing which is the legitimate reference and which is unnecessary
is clear.
Redundant references may still be produced for legitimate reasons:
1. Voluntary DAG gaps: For huge rooms as well as long periods of downtime an
implementation may not have a complete message graph. In this case it may
not know (or care) why there is a gap in the room's DAG, and it simply
"bridges" the gap for its own satisfaction to create a unified graph even
though such an action is gratuitious to others.
2. Involuntary DAG gaps: In a federated system comprised of many
implementations there is a non-zero proability that eventually some message
will be accepted or rejected differently. That is a problem for an essentially
linked-list data format: the inability to traverse through one event relegates
everything on the other side of it inaccessible. A redundant reference may be
used to "route around" a malformed event which is considered essential for some
and redundant by others.
```
[M00]
|
[M01]
/ \
[M02] [M02]
```
Above: When two servers transmit at the same time. This is where the DAG shines
by relaxing write-consistency for a large scale distributed system: two actors
can issue messages at the same time without explicit synchronization. There is
no magic here (yet), and as one would assume, two simultaneous messages as
illustrated may still be conflicting.
```
[M00]
|
[M01]
/ \
[M02A] [M02B]
\ /
[M03]
```
Above: The M03 message "sees" both M02 messages and makes two references.
```
[M00]
|
[M01]
/ | \
[M02] [M02] [M02]
\ | /
[M03]
|
[M04]
|
```
Below: when three servers transmit, and then a fourth server transmits
having only received two out of the three transmissions in the previous round.
```
[M00]
|
[M01]
/ | \
[M02] [M02] [M02]
\ \ /
\ [M03]
\ /
[M04]
|
[M05]
|
```
#### Coherence
Keen observers may have realized by now this system is not fully coherent.
To be coherent, a system must leverage *entry consistency* and/or *release
consistency*. Translated to this system:
* *Entry* is the point where an event is created containing references to
all previous events. *Entry consistency* would mean that the knowledge
of all those references is revealed from all parties to the issuer such that
the issuer would not be issuing a conflicting event.
* *Release* is the act of broadcasting that event to other servers. *Release
consistency* would mean that the integration of the newly issued event does not
conflict at the point of acceptance by each and every party.
This system appears to strive for *eventual consistency*. To be pedantic, that
is not a third lemma supplementing the above: it's a higher order composite (like
mutual exclusion, or other algorithms). What this system wants to achieve is a
byzantine tolerance which can be continuously corrected as more information is
learned. This is a *tolerance*, not a *prevention*, because the relaxed write
consistency is of extreme practical importance.
For *eventual consistency* to be coherent, the "seeds" of a correction have to
be planted early on before any fault. When the fault occurs, all deviations
can be corrected toward some single coherent state as each party learns more
information. Once all parties learn all information from the system, there is
no possibility for incoherence. The caveat is that some parties may need to
roll back certain decisions they made without complete information.
Consider the following: `Alice` is a room founder and has one other member
`Bob` who is an op. `Alice` outranks `Bob`. Consider the following scenario:
> 1. `Charlie` joins the room. Now the room has three members. Everyone is
> still in full agreement.
>
> 2. `GNAA` ddos's `Alice` so she can't reach the internet but she can still
> use her server on her LAN.
>
> 3. `Alice` likes `Charlie` so she gives him `+e` or some ban immunity.
>
> 4. `Bob` doesn't like `Charlie` so he bans him.
Now there is a classic byzantine fault. The internet sees a room with two
members `Alice` and `Bob` again while `Alice` sees a room with three: `Alice`, `Bob`
and `Charlie`.
> 5. `GNAA` stops the ddos.
This fault now has to be resolved. This is called "state conflict resolution"
and the matrix specification does not know how to do this. What is currently
specified is that `Alice` and `Bob` can only perform actions that are valid
with the knowledge they had when they performed them. In fact, that was true
in this scenario.
Intuitively, `Alice` needs to dominate the resolution because `Alice` outranks
`Bob`. `Charlie` must not be banned and the room must continue with three
members. Exactly how to roll back the ban and reinstate `Charlie` may seem
obvious but there are practicalities to consider: Perhaps `Alice` is ddosed for
something like a year straight and `Charlie` has entirely given up on socializing
over the internet. A seemingly random and irrelevant correction will be in store
for the room and the effects might be far more complicated.
### Implementation
#### Model
This system embraces the fact that "everything is an event." It then follows
that everything is a room. We use rooms for both communication and storage of
everything.
There is only one† backend database and it stores events. For example: there
is no "user accounts database" holding all of the user data for the server-
instead there is an `!accounts` *room*. To use these rooms as efficient
databases we categorize a piece of data with an event `type` and key it with
the event `state_key` and the value is the event `content`. Iteration of these
events is also possible. This is now a sufficient key-value store as good as
any other approach; better though, since such a databasing room retains all
features and distributed capabilities of any other room. We then focus our
efforts to optimize the behavior of a room, to the benefit of all rooms, and
all things.
† Under special circumstances other databases may exist but they are purely
slave to the events database: i.e one could `rm -rf` a slave database and it
would be rebuilt from the events database. These databases only exist if an
event is *truly* inappropriate and doesn't fit the model even by a stretch.
An example of this is the search-terms database which specializes in indexing
individual words to the events where they are found so content searches can be
efficient.
#### Technique
The Matrix room, as described earlier, is a state machine underwritten by
timelines of events in a directed acyclic graph with eventual consistency.
To operate effectively, the state machine must respond to queries about
the state of the room at the point of any event in the past. This is similar
to a `git reset` to some specific commit where one can browse the work tree
as it appeared at that commit.
> Was X a member of room Y when event Z happened?
The naive approach is to trace the graph from the event backward collecting
all of the state events to then satisfy the query. Sequences of specific state
event types can be held by implementations to hasten such a trace.
Alternatively, a complete list of all state events can be created for each
modification to the state to avoid the reference chasing of a trace at the
cost of space.
Our approach is more efficient. We create a b-tree to represent the complete
state to satisfy any query in logarithmic time. When the state is updated,
only one path in the tree is modified and the root is stored with that event.
This structure is actually immutable: the previous versions of the affected
nodes are not discarded allowing past configurations of the tree to be
represented. We further benefit from the fact that each node is referenced by
the hash of its content for efficient reuse, as well as our database being
well compressed.
#### Flow
This is a single-writer/multiple-reader approach. The "core" is the only writer.
The write itself is just the saving of an event. This serves as a transaction
advancing the state of the machine with effects visible to all future
transactions and external actors.
The core takes the pattern of
`evaluate + exclude -> write commitment -> release sequence`. The single
writer approach means that we resolve all incoherence using exclusion or
reordering or rejection on entry and before any writing and release of the
event. Many ircd::ctx's can orbit the inner core resolving their evaluation
with the tightest exclusion occurring around the write at the inner core.
This also gives us the benefit of a total serialization at this point.
:::::::
||||||| <-- evaluation + rejection
\|/ <-- evaluation + exclusion / reordering
!
* <-- actor serialized core write commitment
//|||\\
//|// \\|\\
::::::::::::: <-- release sequence propagation cone
The entire core commitment process relative to an event riding through it
on an ircd::ctx has a duration tolerable for something like a REST interface,
so the response to the user can wait for the commitment to succeed or fail
and properly inform them after.
The core process is then optimized by the following facts:
* The resource exclusion zone around most matrix events is either
small or non-existent because of its relaxed write consistency.
* Writes in this implementation will not delay.
"Core dilation" is a phenomenon which occurs when large numbers of events
which have relaxed dependence are processed concurrently because none of
them acquire any exclusivity which impede the others.
:::::::
|||||||
||||||| <-- Core dilation; flow shape optimized for volume.
|||||||
/|||||\
///|||\\\
//|/|||\|\\
:::::::::::::
Close up of the charybdis's write head when tight to one schwarzschild-radius of
matrix room surface which propagates only one event through at a time. Vertical
tracks are contexts on their journey through each evaluation and exclusion step
to the core.
Input Events Phase
:::::::::::::::::::::::::::::::::::::::::::::::::::::: validation / dupcheck
|||||||||||||||||||||||||||||||||||||||||||||||||||||| identity/key resolution
|||||||||||||||||||||||||||||||||||||||||||||||||||||| verification
|||| ||||||||||||||| ||||||||||||||| ||||||||||||||||| head resolution
--|--|----|-|---|--|--|---|---|---|---------|---|---|- graph resolutions
----------|-|---|---------|-------|-----------------|- module evaluations
\ | | | | / post-commit prefetching
== ==============| | == Lowest velocity locks
\ | | /
== | | == Mid velocity locks
\ | | /
== | / == High velocity locks
\ | / /
== =====/= == Highest velocity lock
\ / /
\__ / __/
_ | _
! Write commitment
Above, two contexts are illustrated as contending for the highest velocity
lock. The highest velocity lock is not held for significant time.
* Singularity
[ ]
/-------------[---]-------------\
/ : : \ Federation send
/ /---------[---]---------\ \
/ : : \ Client sync
out / /------[---]------\ \ out
/ / : : \ \
/ out / | | \ out \
/ out / \ out \
/ \
return
| result to |
| evaluator |
-------------
Above, a close-up of the release sequence. The new event is being "viewed" by
each consumer context separated by the horizontal lines representing a context
switch from the perspective of the event travelling down. Each consumer
performs its task for how to propagate the commissioned event.
#### Scaling
Scaling beyond the limit of a single CPU core can be done with multiple instances
of IRCd which form a cluster of independent actors. This cluster can extend
to other machines on the network too. The independent actors leverage the weak
write consistency and strong ordering of the matrix protocol to scale the same
way the federation scales.
Interference pattern of two IRCd'en:
```
::::::::::::::::::::::::::::::::::::
--------\:::::::/--\:::::::/--------
||||||| |||||||
\|/ \|/
! !
* *
//|||\\ //|||\\
//|// \\|\\//|// \\|\\
/|/|/|\|\|\/|/|/|\|\|\|\
```