# 大规模灵活共识机制技术文档
开源代码地址:https://git.chainweaver.org.cn/stcsm/flextender
## 整体架构
针对区块链技术当前发展趋势以及长安链应用层的特定需求(例如央行金融监管),采用**排序-执行-仲裁架构**,以智能合约为中心,针对智能合约灵活定制仲裁策略,并以智能合约为基本单位判定交易的冲突关系,整体架构如下:
- 排序阶段:针对交易集合给出一个(不可信)定序。
- 执行阶段:根据排序结果(预)执行交易。
- 根据排序结果,在精确的执行现场下执行交易,确定每个交易涉及的智能合约,并得到交易需要的**仲裁节点集合**。
- 一个交易批次内包含多个交易,这些交易可能具有不同的仲裁节点集合。
- 该步骤可额外引入**交易并行执行策略**,但需要保证每个节点的执行顺序和执行结果相同。
- 仲裁阶段:节点对交易批次发表仲裁意见,收到仲裁意见后根据仲裁策略得到对该交易批次的仲裁结果。
- 对于批次中任一交易,节点如果是该交易的仲裁节点,则需要对该交易发表仲裁意见。
- 节点需要对交易批次的仲裁结果**达成共识**。
- 当批次中的所有交易仲裁成功时,该批次即可上链。
当交易批次中存在交易仲裁失败时,则需要进行回滚处理:
- 如果存在仲裁失败的交易需要中止(abort),可通过剔除这些交易构造新的批次;
- 或者重新选择交易进行排序,组成新的批次,重新进行执行和仲裁;
- 如果仲裁过程中再次出现新的仲裁失败交易,则重复上述过程,直到最终批次内所有交易都仲裁成功。
最终提交的区块应额外包含上述过程中所有中止的交易及其仲裁失败的证据,以供客户端查询。如果仲裁失败的交易仍需上链,则应作为新的交易从**排序阶段**重新开始。
## 基于排序-执行-仲裁架构的 Tendermint 算法
根据对排序-执行-仲裁架构各阶段功能的分析,有以下观察结果:
- 排序阶段可以对应有主共识中**主节点对交易批次中的交易给出排序**;
- 仲裁阶段中可能需要特殊仲裁节点发表意见,而无限制地等待特殊节点的消息将会破坏整个系统的活性,因此要求系统在**额外的时间假设**下运行以保证活性;
- 仲裁节点发表仲裁意见的过程可以与**共识原本的投票过程结合**,以避免额外通信开销;
- 当交易批次中存在交易仲裁失败时需要回滚,直到批次中所有交易都仲裁成功才可上链,可以发现这一过程与**基于轮次的共识机制**相似:
- 有主共识中,如果在主节点的领导下经过若干轮次的消息交互和/或触发超时后仍无法达成共识,则需要更换主节点进入新轮次重新进行共识,
- 如Tendermint中一个高度的共识可能需要**多个轮次**完成,HotStuff中一次提交可能需要**多个视图**完成。
因此,考虑将架构各阶段融入到适用于联盟链的经典半同步BFT共识算法Tendermint中,对其各个步骤进行相应改造,以满足以下两个特征:
- **支持交易级别的灵活仲裁**;
- **不会因为交易冲突而回滚**。
### 时间假设
算法在**半同步时间假设**下运行:存在一个时间上限 Δ 和一个全局稳定时间(Global Stabilization Time, GST),满足正确节点间在 GST 之后的所有通信都能在 Δ 时间内送达。
基于该假设,采用 gossip 网络通信,具备以下关键性质:
> **Gossip communication**:如果一个正确节点在时刻 *t* 发送了一个消息,那么所有正确节点将在时刻 *max*{*t*, GST} + Δ 前收到该消息。进一步,如果一个正确节点在时刻 *t* 收到了一个消息,那么所有正确节点也将在时刻 *max*{*t*, GST} + Δ 前收到该消息。
Tendermint 的活性保障强依赖于该性质。
### 算法描述
基于排序-执行-仲裁架构的 Tendermint 算法在每个高度 *h* 上的每个轮次 *r* 的通信模式如下图所示,在 Tendermint 三阶段提交模式的基础上融入了架构各阶段的功能。为防止等待特殊仲裁节点导致活性丧失,在 Prevote 阶段引入了一个新计时器 *tmrA,r*。
单笔交易的仲裁有以下3种情况:
- 在 *tmrA,r* 超时前收集到了足够仲裁成功的仲裁意见,则仲裁为 1,即仲裁成功;
- 在 *tmrA,r* 超时前收集到了足够仲裁失败的仲裁意见,则仲裁为 0,即仲裁失败;
- 在 *tmrA,r* 触发超时后仍未收集到足够的仲裁意见,则仲裁为 0,即因为超时而仲裁失败。
为了保证系统吞吐量,采用将交易按批次打包的方式,通过算法对整个交易批次执行和仲裁达成共识:
- 在每个高度 *h* 上,算法基于轮次 *r* 运行;
- 只有当批次中所有交易都仲裁成功(**全 1 共识**),该批次才能上链;
- 如果无法达成全 1 共识(存在交易仲裁失败),则进入下一轮次继续共识;
- 在新的轮次中,主节点根据上一轮的仲裁结果删除失败交易后重新发起共识;
- 直到在某个批次上达成了全1共识,才能进入下一高度。
上述过程将针对交易仲裁失败的回滚机制融入到了基于轮次的共识机制之中,通过新一轮次的共识完成了回滚。具体对 Tendermint 算法做出以下改造:
#### 本地变量修改
- *lockedValue* 更新条件:
在某轮的Prevote阶段中,如果主节点提议的交易批次 *b* 收到来自至少 *n - f* 个节点的包含 hash(*b*) 的 PREVOTE 投票消息,且其中每笔交易都收集到了能够仲裁成功的仲裁意见,则将 *lockedValue* 更新为 *b*。
- *validValue* 更新条件:
如果在某轮的Prevote或Precommit阶段满足上述条件,则将 *validValue* 更新为 *b*。
为防止拜占庭主节点随意修改交易批次并配合其他拜占庭节点影响共识活性,新增变量 *refRound* 用于记录修改批次时所参考的轮次。
- *refRound* 更新条件:当节点收到某轮的 PROPOSAL 提案消息与来自至少 *n - f* 个节点的包含仲裁结果的 PRECOMMIT 投票消息,如果
- 本地 *refRound* = -1,
- 或者,当前轮次大于本地 *refRound* 且 PROPOSAL 包含的交易数小于等于 *refRound* 轮次的提案包含的交易数,
则更新 *refRound* 为该轮次。
主节点修改交易批次时必须依据 *refRound* 轮次的投票包含的仲裁意见与仲裁结果,按照规则对该轮次的交易批次进行修改得到新的交易批次;其他节点收到主节点广播的交易批次时需验证修改是否符合规则(详见后续伪代码部分)。
#### Propose 阶段
每轮开始时:
1. 如果主节点本地 *validValue* 非空,则直接将其作为本轮提议的交易批次;
2. 否则,对交易进行排序:
- 如果本地 *refRound* = -1,则可任意选取交易进行排序组成新的批次;
- 否则依据 *refRound* 轮次的 PREVOTE 投票包含的仲裁意见与至少 *n - f* 个 PRECOMMIT 投票包含的仲裁结果修改 *refRound* 轮次提案包含的交易批次。按照批次中的交易顺序,第一个满足下述条件的交易会被删除:如果存在能够将该交易仲裁为 0 的仲裁意见,或者该交易在至少 *f + 1* 个节点的仲裁结果中被仲裁为 0。
3. 主节点将包含该交易批次的 PROPOSAL 提案消息广播给所有节点,并附带参考的轮次 *refRound*。
#### Prevote 阶段与Precommit 阶段
节点在 *tmrP,r* 超时前收到主节点广播的交易批次 *b* 与参考轮次 *rr* 后:
1. 验证交易批次是否为按照规则从 *rr* 轮次修改而来;
2. 如果上述验证通过,且满足针对 *lockedValue* 的解锁条件,则依序执行批次内的交易;
3. 执行后广播包含 hash(*b*) 的 PREVOTE 消息:
- 作为共识的第一轮投票;
- 同时作为对 *b* 中交易的仲裁意见(集合形式,明确指出反对的交易序号,可为空集)。
如果节点在 *tmrP,r* 触发超时后仍未收到来自主节点的提案,则广播空的 PREVOTE 消息。
当收到来自 *n - f* 个节点的 PREVOTE 消息后,节点将启动两个计时器:
- *tmrV,r*:Tendermint 中的 *timeoutPrevote*
- 节点如果在 *tmrV,r* 超时前收到来自至少 *n - f* 个节点的包含 hash(*b*) 的 PREVOTE 消息,则根据收集到的仲裁意见对交易进行仲裁;
- 否则,当 *tmrV,r* 触发超时,节点广播空的 PRECOMMIT 消息。
- *tmrA,r*:防止无限等待特殊仲裁节点
- 节点如果在 *tmrA,r* 超时前对批次内的每笔交易都收到了足够的仲裁意见,则按照各交易的仲裁策略进行仲裁,得到每笔交易的仲裁结果(1 表示节点认为该交易应该仲裁成功,0 表示节点认为该交易应该仲裁失败);
- 当 *tmrA,r* 触发超时,如果节点仍未收到对某笔交易足够的仲裁意见,则视为该交易仲裁失败;
- 整个批次的仲裁结果由所有交易的仲裁结果按交易顺序排序组成;
- 节点广播包含 hash(*b*) 和整个批次仲裁结果的 PRECOMMIT 消息。
由于仲裁节点的特殊性,为保证节点尽可能收集到足够的仲裁意见,*tmrA,r* 需要被设置得尽可能保守,因此一般情况下有 *tmrA,r* > *tmrV,r*。
当收到来自 *n - f* 个节点的 PRECOMMIT 消息后,节点启动计时器 *tmrC,r*。
共识终止条件如下:
- 当节点收到来自 *n - f* 个节点的 PRECOMMIT 消息均包含 hash(*b*) 且仲裁结果均为全为 1 时,则完成当前高度下的共识并提交区块,然后进入下一高度;
- 如果无法达成全 1 共识,但能够根据当前轮次的 PROPOSAL 消息和至少 *n - f* 个 PRECOMMIT 消息更新本地 *refRound*,则进入下一轮次;
- 否则,等待 *tmrC,r* 超时后进入下一轮次。
#### 交易级仲裁示例流程
以交易 *tx1* 为例,展示其仲裁与共识过程:
假设系统中有4个节点:Node1, Node2, Node3, Node4。主节点提出 *tx1* 并广播给其他节点,节点执行后发现:*tx1* 的执行经过了智能合约 *A*,对应仲裁节点集合 {Node3, Node4},即 *tx1* 需获得 Node3 和 Node4 的认可意见才可仲裁成功。
- Prevote 阶段:
- 节点需要等待来自全体 Quorum 的投票 + 所有仲裁节点的仲裁意见;
- 如果在超时前收到足够意见 → 认为仲裁成功;
- 否则视为仲裁失败。
- Precommit 阶段:
- 如果节点收集到全体 Quorum 投票都认为 *tx1* 仲裁成功 → 上链,进入下一高度;
- 否则进入下一轮次,新的主节点如果发现 *tx1* 仲裁失败,则将其剔除,对其他交易进行新一轮共识。
下图展示了 *tx1* 能够成功上链的一种情况:
- Node1, Node2, Node3 在 Prevote 阶段收集齐所有仲裁意见 → 仲裁成功;
- Node4 未及时收到 Node3 的仲裁意见 → 视为仲裁失败。
- 由于 Quorum(Node1, Node2, Node3)的 PRECOMMIT 结果为全 1,最终 *tx1* 上链成功。
如果在上述的示例中 Node4 对 *tx1* 发表了反对意见,情况如下图所示:
- Node4 对 *tx1* 发表反对意见且所有节点收到该意见,所有节点将 *tx1* 仲裁为 0;
- 在 Precommit 阶段无法收集到全 1 的投票,进入下一轮次;
- 新主节点发现有 *tx1* 仲裁为 0 的证据(Node4 的否决),剔除 *tx1*,对新的交易批次重新发起共识;
- 新的交易批次执行和仲裁成功后方可上链。
### 算法伪代码
设总共有 *n* 个节点,其中存在最多 *f* 个拜占庭节点,满足 *n ≥ 3f + 1*。完整的算法流程如下所述。
**算法:** 基于排序-执行-仲裁架构的Tendermint算法(节点 *i* 视角)
**初始化:**
当前高度 *h* ← 0,
当前轮次 *r* ← 0,
锁定批次 *lockedValue* ← ⊥,
锁定轮次 *lockedRound* ← -1,
有效批次 *validValue* ← ⊥,
有效轮次 *validRound* ← -1,
参考轮次 *refRound* ← -1
**函数** StartRound(*r’*):
1. *r ← r’*;
2. 如果 *i* 是主节点 (*h, r*),则:
- 如果 *validValue* ≠ ⊥,则 *b ← validValue*;
- 否则,如果 *refRound* = -1,则任意选取交易排序形成新的交易批次 *b*;
- 否则,根据 *refRound* 轮次的 PREVOTE 投票包含的仲裁意见与 PRECOMMIT 投票包含的仲裁结果修改refRound 轮次的提案形成新的交易批次 *b* ← ModifyBatch(*refRound*);
- 广播消息 ⟨PROPOSAL, *h*, *r*, *b*, *validRound*, *refRound*⟩;
3. 否则,启动计时器 *tmrP,r*。
**函数** ModifyBatch(*r’*):
根据当前收到的来自主节点 (*h, r’*) 的消息 ⟨PROPOSAL, *h*, *r’*, *b*, \*, \*⟩,以及所有 ⟨PREVOTE, *h*, *r’*, hash(*b*), \*⟩ 和 *n - f* 个 ⟨PRECOMMIT, *h*, *r’*, hash(*b*), *A*⟩,
1. 按顺序遍历 *b* 中的交易,第一个满足下述条件的交易 *tx* 将从 *b* 中剔除:如果这些 PRECOMMIT 消息中存在至少 *f + 1* 个消息将 *tx* 仲裁为 0 或者收到足够的 PREVOTE 消息能够将 *tx* 仲裁为 0;
2. *b* 中剩下的交易组成 *newBatch*,返回 *newBatch*。
**函数** VerifyReference(*b’, rr*):
1. 如果 *rr* = -1 且 *refRound* > -1,返回 False;
2. 在 *tmrP,r* 触发超时前,当收到来自主节点 (*h, rr*) 的消息 ⟨PROPOSAL, *h*, *rr*, *b*, \*, \*⟩,
- 如果 len(*b*) ≠ len(*b’*) + 1 或者存在 *tx* ∈ *b’* 但是 *tx* ∉ *b*,返回 False;
- *verifying* ← {};*verified* ← {};*accCounts*[\*] ← 0;*rejCounts*[\*] ← 0;
- 遍历 *b* 中的每个交易 *txj*,
- *verifying* ← *verifying* ∪ {*j*};
- 如果 *txj* ∉ *b’*,break;
3. 在 *tmrP,r* 触发超时前,当收到来自任意节点的消息 ⟨PREVOTE, *h*, *rr*, hash(*b*), \*⟩ 时,
- 如果收到足够的 PREVOTE 消息能够将 *verifying* 中的交易 *txj* 仲裁为 0 且 *b’* 不包含 *txj*,则 *verified* ← *verified* ∪ {*j*};
- 如果 |*verified*| = len(*verifying*),则返回 True;
4. 在 *tmrP,r* 触发超时前,当收到来自任意节点的消息 ⟨PRECOMMIT, *h*, *rr*, hash(*b*), *A*⟩ 时,按顺序遍历 *verifying* 中的每个交易 *txj*,
- 如果 *A*[*j*] = 1,则 *accCounts*[*j*] ← *accCounts*[*j*] + 1,进一步如果 *accCounts*[*j*] ≥ *f + 1* 且 *txj* ∈ *b’*,则 *verified* ← *verified* ∪ {*j*};
- 如果 *A*[*j*] = 0,则 *rejCounts*[*j*] ← *rejCounts*[*j*] + 1,进一步如果 *rejCounts*[*j*] ≥ *f + 1* 且 *txj* ∉ *b’*,则 *verified* ← *verified* ∪ {*j*};
- 如果 |*verified*| = len(*verifying*),则返回 True。
**主流程:**
1. 调用 StartRound(0);
2. 当收到主节点 (*h, r*) 的消息 ⟨PROPOSAL, *h*, *r*, *b*, -1, *rr*⟩ 满足 VerifyReference(*b, rr*) 且未发送过消息 ⟨PREVOTE, *h*, *r*, \*, \*⟩ 时,
- 如果 *lockedRound* = -1 或 *lockedValue* = *b*,则执行 *b* 包含的交易,执行后将交易执行读写集上传仲裁服务端,获取交易的仲裁意见,记录反对的交易序号集合 *D*(可为空集Ø),广播消息 ⟨PREVOTE, *h*, *r*, hash(*b*), *D*⟩;
- 否则,广播消息 ⟨PREVOTE, *h*, *r*, ⊥, ⊥⟩;
3. 当收到来自主节点 (*h, r*) 的消息 ⟨PROPOSAL, *h*, *r*, *b*, *vr*, \*⟩,*n - f* 个消息 ⟨PREVOTE, *h*, *vr*, hash(*b*), \*⟩ 以及 *b* 中每个交易在 *vr* 轮能够仲裁成功的仲裁意见,满足 0 ≤ *vr* < *r* 且未发送过消息 ⟨PREVOTE, *h*, *r*, \*, \*⟩ 时,
- 如果 *lockedRound* ≤ *vr* 或 *lockedValue* = *b*,则执行 *b* 包含的交易并广播消息 ⟨PREVOTE, *h*, *r*, hash(*b*), ⊥⟩;
- 否则,广播消息 ⟨PREVOTE, *h*, *r*, ⊥, ⊥⟩;
4. 当 *tmrP,r* 触发超时,如果未发送过消息 ⟨PREVOTE, *h*, *r*, \*, \*⟩,则广播消息 ⟨PREVOTE, *h*, *r*, ⊥, ⊥⟩;
5. 当收到 *n - f* 个消息 ⟨PREVOTE, *h*, *r*, \*, \*⟩ 时,启动计时器 *tmrV,r* 和 *tmrA,r*;
6. 当收到来自主节点 (*h, r*) 的消息 ⟨PROPOSAL, *h*, *r*, *b*, \*, \*⟩ 和 *n - f* 个 ⟨PREVOTE, *h*, *r*, hash(*b*), \*⟩ 时,
- 如果 *tmrV,r* 和 *tmrA,r* 均未触发超时,则等待让 *b* 中每个交易足够仲裁的仲裁意见,最多等待至 *tmrA,r* 触发超时:
- 如果在 *tmrA,r* 触发超时前所有交易都收集到了足够的仲裁意见,则按照仲裁策略对每笔交易进行仲裁,得到 *b* 的仲裁结果 *A*;如果 *A* 为全 1,则更新 *lockedValue* ← *b*,*lockedRound* ← *r*;
- 在 *tmrA,r* 触发超时后对仍未收集到足够仲裁意见的交易仲裁为 0,得到 *b* 的仲裁结果 *A*;
- 广播消息 ⟨PRECOMMIT, *h*, *r*, hash(*b*), *A*⟩;
- 如果收到了足够让 *b* 的仲裁结果 *A* 为全 1 的仲裁意见,则更新 *validValue* ← *b*,*validRound* ← *r*;
7. 当收到来自主节点 (*h, r*) 的消息 ⟨PROPOSAL, *h*, *r*, *b*, *vr*, \*⟩ 和 *n - f* 个消息 ⟨PREVOTE, *h*, *r*, hash(*b*), ⊥⟩ 满足 0 ≤ *vr* < *r* 且未发送过消息 ⟨PRECOMMIT, *h*, *r*, \*, \*⟩ 时,
- 令仲裁结果 *A* 为全 1;
- 广播消息 ⟨PRECOMMIT, *h*, *r*, hash(*b*), *A*⟩;
8. 当 *tmrV,r* 触发超时,如果未发送过消息 ⟨PRECOMMIT, *h*, *r*, \*, \*⟩ ,则广播消息 ⟨PRECOMMIT, *h*, *r*, ⊥, ⊥⟩;
9. 当收到 *n - f* 个消息 ⟨PREVOTE, *h*, *r*, ⊥, ⊥⟩ 且未发送过消息 ⟨PRECOMMIT, *h*, *r*, \*, \*⟩,则广播消息 ⟨PRECOMMIT, *h*, *r*, ⊥, ⊥⟩;
10. 当收到 *n - f* 个消息 ⟨PRECOMMIT, *h*, *r*, \*, \*⟩ 时,启动计时器 *tmrC,r*;
11. 当收到来自主节点 (*h, r’*) 的消息 ⟨PROPOSAL, *h*, *r’*, *b*, \*, \*⟩ 和 *n - f* 个消息 ⟨PRECOMMIT, *h*, *r’*, hash(*b*), *A*⟩ 满足 *refRound* = -1,或者针对来自主节点 (*h, refRound*) 的消息 ⟨PROPOSAL, *h*, *refRound*, *b’*, \*, \*⟩ 有 *r’* > *refRound* 且 len(*b*) ≤ len(*b’*),更新 *refRound* ← *r’*;
12. 当收到来自主节点 (*h, r’*) 的消息 ⟨PROPOSAL, *h*, *r’*, *b*, \*, \*⟩ 和 *n - f* 个 ⟨PRECOMMIT, *h*, *r’*, hash(*b*), *A*⟩ 满足 *A* 为全 1 时,
- 完成 *b* 中交易及执行结果的上链;
- *h* ← *h* + 1;
- 重置 *lockedValue*, *lockedRound*, *validValue*, *validRound*, *refRound*;
- 清空消息日志;
- 调用 StartRound(0);
13. 当 *tmrC,r* 触发超时,调用 StartRound(*r* + 1);
14. 当收到 *f + 1* 个消息 ⟨\*, *h*, *r’*, \*, \*⟩ 满足 *r’* > *r*,则调用 StartRound(*r’*)。
### 修改交易批次的参考与验证——防止拜占庭节点破坏活性
当允许主节点任意修改交易批次时,考虑以下两种拜占庭节点的攻击方式:
#### 任意添加交易
- 拜占庭节点成为主节点时,添加大量需要拜占庭节点仲裁的交易集 *T*。
- 接下来在主节点为诚实节点的轮次,拜占庭节点通过投票让 *T* 中部分交易仲裁失败,阻碍全 1 的共识结果达成。
- 即使新的主节点为诚实节点,可以将这些仲裁失败的交易从批次中删除,但 *T* 中剩下的其他交易的仲裁结果仍会受到拜占庭节点投票的操纵。
- 假设在主节点为诚实节点的轮次 *r*,拜占庭节点可以让 *T* 中 *tr* 个交易被仲裁失败,在主节点轮询的情况下,最多连续 *2(n − f)* 个轮次的主节点为诚实主节点可删除这些交易,只要 *∑ r=kk+2(n-f)-1 tr < |T|*,这些轮次就无法达成共识。
- 而下一轮次的主节点将为拜占庭节点,可重新添加一批这样的交易 *T'*,重复上述流程,共识将永远无法达成,系统活性遭到破坏。
#### 任意删除交易
- 拜占庭节点成为主节点时,任意删除批次中的交易。
- 最坏情况下,如果没有超过 *f* 个诚实节点锁定某个交易批次上时,则新的拜占庭主节点可将批次中所有交易都剔除;
- 没有被锁住的节点都会接受这一空的交易批次,最终系统将在空块上达成共识。
- 如果每个高度的共识结果都是空块,系统活性在实际上遭到破坏。
#### 限制主节点修改能力的对策
融合了灵活仲裁功能的 Tendermint 算法相比原版 Tendermint 算法存在一个**关键区别**:
> 原版 Tendermint 算法保证了在 GST 之后只要主节点为诚实节点,就一定能够达成共识;而灵活仲裁功能的加入让这一特性不再保持,因为拜占庭节点能够通过发表反对的仲裁意见来操纵部分交易的仲裁结果,从而阻碍共识达成,进而可能破坏系统活性。
因此需限制主节点修改交易批次的能力,仅允许主节点通过已有的仲裁意见和 *n - f* 个仲裁结果针对性地剔除交易批次中**第一个**:
- 存在仲裁失败证据的交易;
- 或者存在 *f + 1* 个节点认为仲裁失败的交易。
修改交易批次时只剔除**第一个**满足上述条件的交易,是因为该交易剔除后可能导致按顺序执行后续交易时读写集发生变化,因此后续交易的仲裁结果可能也会发生变化。
同时,*refRound* 的更新与验证规则保证了正确节点间传递的交易批次所包含的交易数量随轮次变化一定是**单调非增的**,那么正确节点将不断剔除掉交易批次中受拜占庭节点恶意操纵仲裁结果的交易,最终剩下的交易能够在GST之后达成确定的仲裁结果。
为了让正确节点能够验证正确主节点的修改合法,
- 节点在 VerifyReference 中应在 *tmrP,r* 超时前持续等待主节点参考轮次 *refRound* 的 PREVOTE 和 PRECOMMIT 投票消息。
- 如果在 *tmrP,r* 超时前验证通过,则正常处理提案并进行 PREVOTE 投票;否则在 *tmrP,r* 触发超时后广播 PREVOTE 空票。
> 该机制保证在 GST 之后,正确节点一定能够在 *tmrP,r* 超时前收到正确主节点修改交易批次时参考的证据,从而能够验证其修改的合法性。
### 仲裁证据的复用与验证——避免“模棱两可”的重复仲裁
一个诚实主节点提出一个交易批次的目的,是希望该其能够被共识为**全 1**,从而完成上链。为实现这一目标:
- 当主节点未观察到可能锁定的交易批次(即 *validValue* 为空)时,
- 如果 *refRound* = -1,则可任意选择交易排序形成新的交易批次;
- 否则,根据 *refRound* 轮次的投票包含的仲裁意见和仲裁结果,判断 *refRound* 轮次提出的交易批次中哪个交易可被或应被剔除,由此修改后作为本轮提议的交易批次。
- 当主节点为诚实节点且本地 *validValue* 不为空时,说明已有该交易批次能够被**仲裁为全 1 的证据**。由于 **Gossip communication** 性质,这些证据在GST之后一定能在 Δ 时间内被所有正确节点收到,因此无需重复仲裁,只需通过两轮共识投票完成对该交易批次的确认,即可达成共识。此时:
- 节点发送的 PREVOTE 消息不需要附带反对交易序号集合,对应字段为空 ⊥ 而非空集 Ø;
- 当节点收到来自 *n - f* 个节点的 PREVOTE 消息都包含同一个交易批次的哈希值且反对交易序号集合字段为空时,即可广播仲裁结果为全 1 的 PRECOMMIT 消息。
#### 仲裁证据复用示例
设在第 *r* 轮提出的交易 *tx1* 需要 Node3 和 Node4 都认可才可仲裁成功,因为网络延迟 Node4 的仲裁意见在 *tmrA,r* 超时瞬间才送达其他节点。
此时其他节点已将 *tx1* 仲裁为 0,但是又由于收到了足够 *tx1* 仲裁成功的仲裁意见,可更新 *validValue* ← *tx1*, *validRound* ← *r*。
进入第 *r + 1* 轮后,新主节点即可直接提出该 *validValue* 并附带 *validRound*。其他节点在验证到已经在 *validRound* 轮收到了足够 *tx1* 仲裁成功的证据之后,只需完成共识的两轮投票即可完成 *tx1* 上链。
#### 一般性分析
假设每个仲裁节点的仲裁意见唯一,在任意一个节点的视角下,无论这些仲裁意见以任何顺序送达,节点依据仲裁策略得到的仲裁结果都相同,那么该仲裁策略本身不存在模棱两可。那么,当仲裁策略本身不存在模棱两可的情况时,在 GST 之后新的正确主节点在修改 *refRound* 轮提出的交易批次 *b* 时有以下三种可能:
- 未收到 *b* 在 *refRound* 轮能够被仲裁为全 1 的证据,且 *b* 中存在某个交易有仲裁失败的证据或被 *f + 1* 个节点仲裁为 0:
- 删除第一个满足该条件的交易,
- 剩余交易组成新批次重新仲裁。
- 未收到 *b* 在 *refRound* 轮能够被仲裁为全 1 的证据,但 *b* 中**不存在**任何一个交易有仲裁失败的证据或被 *f + 1* 个节点仲裁为 0:
- 对于 *b* 中的每个交易 *tx*,至少存在一个正确节点在 *refRound* 轮收到了 *tx* 能够被仲裁为 1 的证据。
- 此时,无论新主节点诚实与否、是否修改 *b* 然后发起共识,都不会对系统的活性造成任何影响,因为
- 在 GST 之后所有正确节点都能在 Δ 时间内收到 *b* 中每个交易能够被仲裁成功的证据,即 *b* 能够仲裁为全 1 的证据,
- 从而能够更新本地 *validValue* ← *b*(或者此时已有更高轮次的 *validValue*),最终在 *validValue* 上达成共识。
- 已收到 *b* 在 *refRound* 轮能够被仲裁为全 1 的证据(*validValue* = *b*):
- 重新提出 *b* 并附带 *validRound* = *refRound*,
- 无需重复仲裁,两轮共识投票确认后完成上链。
因此,在 GST 之后,新的正确主节点在修改交易批次时**要么根据规则删除至少一个交易,要么可以确定当前交易批次一定有能够被仲裁为全 1 的证据**,能够保证系统活性。
### 扩展优化一:仲裁意见的补充广播——处理拜占庭主节点延迟提案
拜占庭主节点可能为了阻止正常交易完成仲裁而恶意延迟发送提案给正确节点(尤其是特殊仲裁节点),使得正确节点无法在 *tmrP,r* 超时前及时收到提案从而无法正常执行交易并进行仲裁。
针对上述问题,设计仲裁意见的补充广播机制:
- 对于每个节点,当收到来自当前轮次主节点广播的提案消息,不管处于什么阶段,只要该提案通过所有验证并满足解锁条件,就会正常执行该提案包含的交易并获取读写集,同时对提案中需要自己仲裁的交易进行仲裁。此时,有以下两种情况:
- 如果节点在当前轮次还未广播过 PREVOTE 投票消息,则正常进行投票并表达仲裁意见;
- 即使节点在当前轮次已广播过 PREVOTE 空票,仍可再进行一次 PREVOTE 投票,需要附带交易批次的哈希值以及对交易的仲裁意见。
对于后者中的补充投票,不应作为共识推进的依据,但可以作为仲裁的证据,帮助交易正常完成仲裁。
### 扩展优化二:基于消息冲突的拜占庭错误检测——无视冲突的仲裁意见
拜占庭节点可能恶意发送冲突的仲裁意见给不同的正确节点,误导正确节点对交易得到冲突的仲裁结果。
实际上,依赖 **Gossip communication** 性质,所有正确节点最终一定能看到相同的所有消息。因此,设计基于消息冲突的拜占庭错误检测机制:
- 当收到来自同一个节点的两个冲突的消息(高度、轮次、类型相同,但包含的哈希或仲裁意见冲突),则判断该节点为拜占庭节点,并将该节点的所有仲裁意见视为**认可意见**,避免其破坏全 1 共识的达成。
## 仲裁策略的定义与实现
仲裁策略是一种记录在链上的仲裁结果判断标准,使得所有共识节点能够针对每个交易的合法性得到一致的仲裁结果。依赖仲裁策略,单个共识节点能够整合多个仲裁意见得出仲裁结果。仲裁策略明确了:
- 针对合约:执行过程经过了该合约的交易需要通过该仲裁策略进行仲裁;
- 针对键值对(扩展):执行过程涉及了该键值对的交易需要通过该仲裁策略进行仲裁;
- 仲裁节点:具有仲裁权的共识节点;
- 仲裁策略树:包含仲裁成功策略树和仲裁失败策略树,分别表示交易被仲裁成功和仲裁失败的具体条件,即多个仲裁节点对同一笔交易发表的仲裁意见如何组合形成仲裁结果。
### 仲裁策略树
仲裁策略树在配置文件中以逻辑表达式形式定义,例如:
- AND('Node1', 'Node2', 'Node3')
- OR('Node4', 'Node5')
- OutOf(3, 'Node1', 'Node2', 'Node3', 'Node4')
- OutOf(1, 'Node1', AND('Node2', OR('Node4', 'Node5')))
解析后表示为逻辑树,该树包含:
- 逻辑树节点(Logic Tree Node):AND / OR / OutOf(*k*);
- 条件树节点(Condition Tree Node):单一条件的判断
- 可为共识节点ID,如果该共识节点表达了对应的仲裁意见则该条件达成(仲裁成功策略树需要**认可**的仲裁意见,而仲裁失败策略树需要**反对**的仲裁意见);
- 也可为逻辑树节点,代表逻辑子树,如果该逻辑子树条件达成则该条件达成。
#### 仲裁策略统一化
为确保仲裁策略求值方式一致,所有逻辑树节点被统一转换为 OutOf(*k*) 结构:
| 原表达式 | 统一化结果 |
|---------------------------|---------------------------|
| AND(C1, ..., C*n*) | OutOf(*n*, C1, ..., C*n*) |
| OR(C1, ..., C*n*) | OutOf(1, C1, ..., C*n*) |
| OutOf(*k*, C1, ..., C*n*) | OutOf(*k*, C1, ..., C*n*) |
统一化后,逻辑树仅包含 OutOf(*k*) 树节点,从而保证仲裁策略具体唯一、明确、可执行的计算语义。
#### 仲裁结果计算
每个交易都由对应的仲裁策略树计算仲裁结果,计算过程从仲裁策略树的叶节点开始,自底向上计算,直到根节点的条件达成:
- 如果其仲裁成功策略树的根节点条件达成,则该交易仲裁成功;
- 如果其仲裁失败策略树的根节点条件达成,则该交易仲裁失败。
具体地,对于任意一笔交易,每当节点收到来自其仲裁节点对其的仲裁意见时,
- 根据具体的仲裁意见(认可/反对),对应的仲裁(成功/失败)策略树中由该仲裁节点ID表示的条件树节点的条件达成;
- 而后判断上述条件树节点的父逻辑树节点(统一为 OutOf(*k*) 树节点),统计该逻辑树节点下的子节点条件达成数量,
- 如果数量大于等于k,则该逻辑树节点条件达成,再继续判断该逻辑树节点的父逻辑树节点;
- 否则停止计算,当节点收到下一个仲裁意见时再进行上述计算过程。
#### 自动构建仲裁失败策略树
配置文件中只需要定义仲裁成功策略树的逻辑表达式,在构建仲裁成功策略树的同时,系统会自动构建对应的仲裁失败策略树。
- 对于成功条件 CSuccess = OutOf(*k*, C1, ..., C*n*),其语义为:如果 *n* 个子条件中有至少 *k* 个条件达成,则该交易仲裁成功。
- 当这 *n* 个子条件中有至少 *n-k+1* 个的互补(¬)条件达成,则一定不可能成功。因此,该成功条件对应的失败条件可表示为 CFailure = OutOf(*n-k+1*, ¬C1, ..., ¬C*n*)。
按照以上推导,系统能够根据仲裁成功策略树自动构建出对应的仲裁失败策略树。一个仲裁策略包含的仲裁成功策略树和仲裁失败策略树一定为**互补条件**,一旦其中一棵树条件达成,即可完成仲裁结果的判定,无需等待所有的仲裁意见,从而减少无效计算并加速仲裁过程。
#### 具体示例
例如存在某种金融服务的交易仲裁需要央行同意,或者银行A和银行B同时同意以满足监管需求,仲裁策略的逻辑表达式为 OutOf(1, 'PBC', AND('BankA', 'BankB')),最终构建的仲裁逻辑树如下图所示。
## 仲裁服务
仲裁服务对接架构如下图所示。
该架构将链上执行与链外仲裁解耦,让复杂多变的机构自定义业务仲裁逻辑安全地外置,使得金融场景中的风控、合规、审计能够以可控的方式融入链上共识流程。
**架构层级:**
- 链上层:共识节点,发起仲裁请求;
- 仲裁服务层:endorse-service,负责路由与协调;
- 客户端层:各语言 SDK 客户端,执行具体业务仲裁逻辑。
**通信协议:**
- 上游协议:长安链 ↔ endorse-service;
- 下游协议:endorse-service ↔ 客户端。
**工作流程:**
1. 共识节点执行交易后,将交易和读写集发送给 endorse-service;
2. endorse-service 根据合约名称路由到对应的客户端;
3. 客户端调用业务仲裁逻辑,返回仲裁结果;
4. endorse-service 将结果返回给共识节点;