86 lines
No EOL
8.1 KiB
Markdown
86 lines
No EOL
8.1 KiB
Markdown
事件 *E* 之后的房间状态 *S′(E)* 由事件 *E* 之前的房间状态 *S(E)* 定义,并且依赖于 *E* 是状态事件还是消息事件:
|
||
|
||
- 如果 *E* 是一条消息事件,则 *S′(E)* = *S(E)*。
|
||
- 如果 *E* 是一条状态事件,则 *S′(E)* 与 *S(E)* 相同,除了其与 *E* 的 `event_type` 和 `state_key` 对应的项被 *E* 的 `event_id` 替换。
|
||
|
||
事件 *E* 之前的房间状态 *S(E)* 是 `prev_event` 集合 {*E*<sub>1</sub>, *E*<sub>2</sub>, …} 之后的状态集合 {*S′(E*<sub>1</sub>*)*, *S′(E*<sub>2</sub>*)*, …} 的 *合并与决议结果*。如何对一组状态进行合并与决议,见下述算法。
|
||
|
||
#### 定义
|
||
|
||
版本 2 房间的状态合并算法使用如下定义,并以房间状态集 {*S*<sub>1</sub>, *S*<sub>2</sub>, …} 为输入:
|
||
|
||
**权限事件(Power events)。**
|
||
*权限事件* 指具有类型 `m.room.power_levels` 或 `m.room.join_rules` 的状态事件,或者类型为 `m.room.member` 且 `membership` 字段为 `leave` 或 `ban`,且 `sender` 与 `state_key` 不一致的状态事件。其核心思想为:权限事件是那些可能移除某人在房间内某项操作权限的事件。
|
||
|
||
**无冲突状态映射与冲突状态集。**
|
||
状态映射 *S<sub>i</sub>* 的 key 为形如 *(event_type, state_key)* 的字符串二元组 *K*,对应的 value *V* 为一个状态事件。所有 *S<sub>i</sub>* 的 (K, V) 键值对可划分为两个集合:如果给定 key *K* 在所有 *S<sub>i</sub>* 出现,且值 *V* 在每个状态映射都一致,则此 (K, V) 属于 *无冲突状态映射*;否则,*V* 属于 *冲突状态集*。
|
||
|
||
注意,无冲突状态映射每个 key *K* 只会有一个事件,而冲突状态集可能因同 key 包含多个事件。
|
||
|
||
**鉴权链(Auth chain)。**
|
||
事件 *E* 的 *鉴权链*,是包含 *E* 的所有鉴权事件(auth events)、所有这些事件的鉴权事件,递归回溯直到房间创建为止的集合。换句话说,就是通过事件的 `auth_events` 链接遍历可达的所有事件。
|
||
|
||
**鉴权差集(Auth difference)。**
|
||
*鉴权差集* 的计算方式如下:首先对每个状态 *S*<sub>*i*</sub>,计算其完全鉴权链,即该状态中每个事件的鉴权链的并集。然后找出那些未在所有鉴权链中都出现的事件。若 *C*<sub>*i*</sub> 表示 *S*<sub>*i*</sub> 的完全鉴权链,则鉴权差集为 ∪ *C*<sub>*i*</sub> − ∩ *C*<sub>*i*</sub>。
|
||
|
||
**完整冲突集(Full conflicted set)。**
|
||
*完整冲突集* 是冲突状态集与鉴权差集的并集。
|
||
|
||
**逆拓扑权限排序(Reverse topological power ordering)。**
|
||
一组事件的 *逆拓扑权限排序*,是按鉴权事件形成的有向无环图(DAG)进行拓扑排序,得到字典序最小的排序,并从最早事件到最晚事件排列。比较两个拓扑排序确定哪一个字典序更小时,事件的比较关系如下:对事件 *x* 和 *y*,若
|
||
|
||
1. *x* 的发送者的权限级别 *高于* *y* 的发送者(以各自的 `auth_event` 查得);或
|
||
2. 发送者权限级别相同,但 *x* 的 `origin_server_ts` *小于* *y*;或
|
||
3. 权限级别与 `origin_server_ts` 都相同,但 *x* 的 `event_id` *小于* *y* 的 `event_id`,
|
||
|
||
则 *x* < *y*。
|
||
|
||
逆拓扑权限排序可用 Kahn 算法进行拓扑排序,每步从候选顶点中按上述比较关系选择最小顶点。
|
||
|
||
**主链排序(Mainline ordering)。**
|
||
令 *P* = *P*<sub>0</sub> 为某个 `m.room.power_levels` 事件。从 *i* = 0 开始,反复获取 *P*<sub>*i*+1</sub>,即 *P<sub>i</sub>* 的 `auth_events` 中类型为 `m.room.power_levels` 的事件。每次自增 *i*,直到 *P<sub>i</sub>* 的 `auth_events` 中没有 `m.room.power_levels` 事件为止。*P<sub>0</sub>* 的 *主链* 为 [*P*<sub>0</sub> , *P*<sub>1</sub>, ... , *P<sub>n</sub>*]。
|
||
|
||
若另有事件 *e* = *e<sub>0</sub>*(可以是另一个 `m.room.power_levels` 事件),可以构造类似事件链 [*e*<sub>1</sub>, ..., *e<sub>m</sub>*],其中 *e<sub>j+1</sub>* 为 *e<sub>j</sub>* 的 `auth_events` 中的 `m.room.power_levels` 事件,*e<sub>m</sub>* 没有再指向任何 `m.room.power_levels` 事件。(注意 *e<sub>0</sub>* 本身不包含在该列表中,也有可能该列表为空,因为 *e* 可能没有引用过 `m.room.power_levels` 事件。)
|
||
|
||
对这两条列表进行如下比较:
|
||
* 查找最小的 *j* ≥ 1,使得 *e<sub>j</sub>* 属于 *P* 的主链;
|
||
* 若存在这样的 *j*,则 *e<sub>j</sub>* = *P<sub>i</sub>*,且 *i* 唯一、*i* ≥ 0;否则令 *i* = ∞,其中 ∞ 为一个大于任何整数的特殊标记值;
|
||
* 无论哪种情况,*e* 的 *主链位置* 就是 *i*。
|
||
|
||
以 *P* 计算得主链位置后,*基于 P 的主链排序*,就是将一组事件按以下比较关系(从小到大)排序:对事件 *x* 和 *y*,若
|
||
|
||
1. *x* 的主链位置 **大于** *y*(即 *x* 的鉴权链基于主链上的较早事件);或
|
||
2. 主链位置相同,但 *x* 的 `origin_server_ts` *小于* *y*;或
|
||
3. 主链位置、`origin_server_ts` 都相同,但 *x* 的 `event_id` *小于* *y*,
|
||
|
||
则 *x* < *y*。
|
||
|
||
**迭代鉴权检查(Iterative auth checks)。**
|
||
*迭代鉴权检查算法* 的输入是初始房间状态和已排序的状态事件列表。它通过遍历事件列表,将符合[授权规则](/server-server-api#authorization-rules)的状态事件依次应用到房间状态上。若某状态事件未通过授权规则,则忽略该事件。如果验证授权规则时缺少某个必须的 (event_type, state_key) key,则用事件 `auth_events` 中相应的状态事件(若未被拒绝)替代。
|
||
|
||
#### 算法
|
||
|
||
一组状态的 *合并与决议* 按如下步骤执行:
|
||
|
||
1. 选取出现在 *完整冲突集* 内的所有*权限事件*组成集合 *X*。对于每一个权限事件 *P*,将 *P* 的鉴权链中同时属于完整冲突集的事件也加入 *X*。对 *X* 按 *逆拓扑权限排序* 排序为列表。
|
||
2. 从 *无冲突状态映射* 作为起点,对上一步得到的事件列表应用*迭代鉴权检查算法*,得出部分已决议状态。
|
||
3. 将第 1 步未涉及的所有剩余事件按第 2 步已决议状态中的权限等级,用*主链排序*确定顺序。
|
||
4. 对上述部分已决议状态及新排序的事件列表,再次应用*迭代鉴权检查算法*。
|
||
5. 用*无冲突状态映射*中的相同 key 事件(若存在)替换当前结果中对应事件,得出最终合并决议状态。
|
||
|
||
#### 被拒绝的事件
|
||
|
||
由于基于事件当前状态(而非鉴权链)验证授权而被拒绝的事件,除非另有特别说明,在算法中仍按常规方式处理。
|
||
|
||
注意,那些由于无法通过其鉴权链授权而被拒绝的事件不应出现在此流程中,因为他们不会出现在状态集合之内(本算法只使用状态集中的事件,或状态集中事件的鉴权链中的事件)。
|
||
|
||
{{% boxes/rationale %}}
|
||
这样做有助于保证不同服务器下房间状态更易收敛,因为事件的被拒绝状态可能不同。如果某服务器在另一个服务器作为中转加入房间时返回了不正确的状态(无论是故障还是恶意),就有可能出现此类差异。状态收敛是重要特性,因为它确保房间中所有用户都看到(基本)一致的房间状态。如果各服务器状态视图分歧,可能导致房间分裂,例如因对成员列表存在分歧。
|
||
|
||
直观来看,使用被拒绝的事件似乎有风险,但实际上:
|
||
|
||
1. 服务器无法随意伪造状态,因为它们仍需通过根据事件鉴权链的鉴权检查(例如,若之前没有权限,不能自授权限)。
|
||
2. 若想使一个已被拒绝的事件通过鉴权,必须存在某个状态集允许该事件。恶意服务器可能构造一个分支,声称状态就是该特定状态集,然后复制被拒绝事件指向该分支并发送该事件。复制的事件将通过鉴权检查。因此,忽略被拒绝事件未必能消除潜在攻击路径。
|
||
{{% /boxes/rationale %}}
|
||
|
||
被拒绝的鉴权事件(auth events)故意不参与迭代鉴权检查,因为检查过程中不会对鉴权事件重新授权(但非鉴权事件则会被检查)。 |