EIP-225: Clique 权威证明共识协议
Authors | Péter Szilágyi <peterke@gmail.com> |
---|---|
Created | 2017-03-06 |
Table of Contents
摘要
Clique 是一种权威证明共识协议。它遵循以太坊主网的设计,因此可以轻松添加到任何客户端。
动机
以太坊的第一个官方测试网是 Morden。它从 2015 年 7 月运行到大约 2016 年 11 月,由于累积的垃圾数据以及 Geth 和 Parity 之间的一些测试网共识问题,最终被废弃,取而代之的是测试网重启。
Ropsten 因此诞生,清除了所有垃圾数据并以全新的状态开始。它运行良好,直到 2017 年 2 月底,当时恶意行为者决定滥用低 PoW,并逐渐将区块 gas 限制提高到 90 亿(从正常的 470 万),此时发送巨大的交易会严重破坏整个网络。甚至在此之前,攻击者还尝试了多次极长的重组,导致不同客户端甚至不同版本之间的网络分裂。
这些攻击的根本原因是 PoW 网络的安全性仅取决于其背后的计算能力。从零开始重新启动一个新的测试网并不能解决任何问题,因为攻击者可以一次又一次地发起相同的攻击。Parity 团队决定采用紧急解决方案,即回滚大量区块,并制定一个软分叉规则,禁止 gas 限制超过某个阈值。
虽然此解决方案在短期内可能有效:
- 它不够优雅:以太坊应该具有动态区块限制
- 它不具备可移植性:其他客户端需要自己实现新的分叉逻辑
- 它与同步模式不兼容:快速客户端和轻客户端都无法使用
- 它只是延长了攻击:垃圾数据仍然可以无限地稳步推送进来
Parity 的解决方案虽然不完美,但仍然可行。我想提出一个更长期的替代解决方案,该解决方案涉及更多,但应该足够简单,以便在合理的时间内推出。
标准化的权威证明
如上所述,权威证明无法在没有价值的网络中安全运行。以太坊的长期目标是基于 Casper 的权益证明,但这是一项繁重的研究,因此我们无法在短期内依靠它来解决当前的问题。然而,有一种解决方案足够容易实现,并且足以正确修复测试网,即权威证明方案。
此处描述的 PoA 协议的主要设计目标是,它应该非常易于实现并嵌入到任何现有的以太坊客户端中,同时允许使用现有的同步技术(快速、轻量级、warp),而无需客户端开发人员向关键软件添加自定义逻辑。
设计约束
通常有两种同步区块链的方法:
- 经典方法是获取创世区块并逐个处理所有交易。经过尝试和验证,但在以太坊复杂网络中,计算成本很快变得非常高昂。
- 另一种方法是仅下载区块头链并验证其有效性,之后可以从网络下载任意最近的状态并根据最近的区块头进行检查。
PoA 方案基于这样的想法,即区块只能由受信任的签名者生成。因此,客户端看到的每个区块(或区块头)都可以与受信任的签名者列表进行匹配。这里的挑战是如何维护一个可以随时更改的授权签名者列表?显而易见的答案(将其存储在以太坊合约中)也是错误的答案:快速同步、轻量级同步和 warp 同步在同步期间无法访问状态。
维护授权签名者列表的协议必须完全包含在区块头中。
下一个显而易见的想法是更改区块头的结构,以便删除 PoW 的概念,并引入新的字段来适应投票机制。这也是错误的答案:在多个实现中更改如此核心的数据结构将是一场开发、维护和安全方面的噩梦。
维护授权签名者列表的协议必须完全适应当前的数据模型。
因此,根据上述内容,我们无法使用 EVM 进行投票,而必须求助于区块头。并且我们无法更改区块头字段,而必须求助于当前可用的字段。没有太多的回旋余地。
重新利用区块头字段进行签名和投票
目前仅用作有趣元数据的最明显的字段是区块头中 32 字节的 extra-data 部分。矿工通常将他们的客户端和版本放在那里,但有些人用替代的“消息”填充它。该协议会将此字段扩展到 65 字节,用于 secp256k1 矿工签名。这将允许任何人获得一个区块,并根据授权签名者列表对其进行验证。它还使区块头中的 miner 部分变得过时(因为地址可以从签名中推导出来)。
注意,更改区块头字段的长度是一种非侵入性操作,因为所有代码(例如 RLP 编码、哈希)都与此无关,因此客户端不需要自定义逻辑。
以上足以验证链,但我们如何更新动态签名者列表?答案是我们可以重新利用新废弃的 miner 字段和 PoA 废弃的 nonce 字段来创建一个投票协议:
- 在常规区块期间,这两个字段都将设置为零。
- 如果签名者希望对授权签名者列表进行更改,它将:
- 将 miner 设置为它希望投票的签名者
- 将 nonce 设置为
0
或0xff...f
以投票赞成添加或踢出
任何同步链的客户端都可以在区块处理期间“统计”投票,并通过多数票来维护一个动态变化的授权签名者列表。
为了避免无限期地统计投票,并且为了允许定期清除过时的提案,我们可以重用 ethash 中的 epoch 概念,其中每个 epoch 转换都会清除所有待处理的投票。此外,这些 epoch 转换还可以充当无状态检查点,其中包含区块头 extra-data 中的当前授权签名者列表。这允许客户端仅根据检查点哈希进行同步,而无需重播在该点之前对链执行的所有投票。它还允许创世区块头完全定义链,其中包含初始签名者列表。
攻击媒介:恶意签名者
可能会发生恶意用户被添加到签名者列表,或者签名者密钥/机器被泄露。在这种情况下,协议需要能够防御重组和垃圾邮件。拟议的解决方案是,给定一个 N 个授权签名者的列表,任何签名者只能在每 K 个区块中生成 1 个区块。这可确保损害受到限制,并且剩余的矿工可以将恶意用户投票除名。
攻击媒介:审查签名者
另一个有趣的攻击媒介是,如果签名者(或签名者组)试图审查投票将其从授权列表中删除的区块。为了解决这个问题,我们将签名者允许的生成频率限制为 N/2 中的 1 个。这确保了恶意签名者需要控制至少 51% 的签名帐户,在这种情况下,游戏已经结束。
攻击媒介:垃圾邮件签名者
最后一个小攻击媒介是恶意签名者在他们生成的每个区块中注入新的投票提案。由于节点需要统计所有投票才能创建实际的授权签名者列表,因此他们需要及时跟踪所有投票。如果不限制投票窗口,这可能会缓慢但无限地增长。解决方案是在 W 个区块之后放置一个移动窗口,之后投票将被视为过时。一个合理的窗口可能是 1-2 个 epoch。 我们将其称为一个 epoch。
攻击媒介:并发区块
如果授权签名者的数量为 N,并且我们允许每个签名者在 K 个区块中生成 1 个区块,则在任何时间点都允许 N-K+1 个矿工生成。为了避免这些矿工争夺区块,每个签名者都会在发布新区块的时间上添加一个小的随机“偏移量”。这确保了小分叉很少见,但偶尔仍然会发生(如在主网上)。如果签名者被发现滥用其权力并造成混乱,则可以将其投票除名。
规范
我们定义以下常量:
EPOCH_LENGTH
:检查点和重置待处理投票之后的区块数。- 建议测试网设置为
30000
,以保持与主网ethash
epoch 的相似性。
- 建议测试网设置为
BLOCK_PERIOD
:两个连续区块的时间戳之间的最小差值。- 建议测试网设置为
15s
,以保持与主网ethash
目标的相似性。
- 建议测试网设置为
EXTRA_VANITY
:为签名者个性化保留的额外数据前缀字节的固定数量。- 建议为
32 字节
,以保留当前的额外数据限额和/或用途。
- 建议为
EXTRA_SEAL
:为签名者密封保留的额外数据后缀字节的固定数量。65 字节
固定,因为签名基于标准secp256k1
曲线。- 在创世区块上填充零。
NONCE_AUTH
:用于投票添加新签名者的 Magic nonce 数字0xffffffffffffffff
。NONCE_DROP
:用于投票删除签名者的 Magic nonce 数字0x0000000000000000
。UNCLE_HASH
:始终为Keccak256(RLP([]))
,因为叔块在 PoW 之外毫无意义。DIFF_NOTURN
:包含轮流外签名的区块的区块分数(难度)。- 建议为
1
,因为它只需要是一个任意的基线常量。
- 建议为
DIFF_INTURN
:包含轮流内签名的区块的区块分数(难度)。- 建议为
2
,以显示对轮流外签名的轻微偏好。
- 建议为
我们还定义了以下每个区块的常量:
BLOCK_NUMBER
:链中的区块高度,其中创世区块的高度为区块0
。SIGNER_COUNT
:在链中的特定实例有效的授权签名者数量。SIGNER_INDEX
:区块签名者在当前授权签名者的排序列表中的从零开始的索引。SIGNER_LIMIT
:签名者只能签署一个区块的连续区块的数量。- 必须是
floor(SIGNER_COUNT / 2) + 1
才能在链上强制执行多数共识。
- 必须是
我们重新利用 ethash
区块头字段,如下所示:
beneficiary
/miner
:用于提议修改授权签名者列表的地址。- 通常应填充零,仅在投票时修改。
- 仍然允许任意值(即使是没有意义的值,例如投票删除非签名者),以避免客户端实现中围绕投票机制的额外复杂性。
- 必须在检查点(即 epoch 转换)区块上填充零。
- 事务执行必须使用实际的区块签名者(请参阅
extraData
)作为COINBASE
操作码,并且事务费用必须归因于签名者帐户。
nonce
:签名者关于beneficiary
字段定义的帐户的提案。- 应为
NONCE_DROP
,以提议取消授权beneficiary
作为现有签名者。 - 应为
NONCE_AUTH
,以提议授权beneficiary
作为新签名者。 - 必须在检查点(即 epoch 转换)区块上填充零。
- 必须不占用除上述两个值之外的任何其他值(目前)。
- 应为
extraData
:签名者个性化、检查点和签名者签名的组合字段。- 前
EXTRA_VANITY
字节(固定)可以包含任意签名者个性化数据。 - 最后
EXTRA_SEAL
字节(固定)是签名者签署区块头的签名。 - 检查点区块必须包含签名者列表(
N*20 字节
)在两者之间,否则省略。 - 检查点区块 extra-data 部分中的签名者列表必须按升序字节顺序排序。
- 前
mixHash
:保留用于分叉保护逻辑,类似于 DAO 期间的额外数据。- 必须在正常操作期间填充零。
ommersHash
:必须为UNCLE_HASH
,因为叔块在 PoW 之外毫无意义。timestamp
:必须至少为父时间戳 +BLOCK_PERIOD
。difficulty
:包含区块的独立分数以得出链的质量。- 如果
BLOCK_NUMBER % SIGNER_COUNT != SIGNER_INDEX
,则必须为DIFF_NOTURN
- 如果
BLOCK_NUMBER % SIGNER_COUNT == SIGNER_INDEX
,则必须为DIFF_INTURN
- 如果
授权区块
要授权网络中的区块,签名者需要对包含除签名本身之外的所有内容的区块 sighash 进行签名。这意味着此哈希包含区块头的每个字段(包括 nonce
和 mixDigest
),以及 extraData
,但 65 字节的签名后缀除外。这些字段按照它们在黄皮书中的定义顺序进行哈希处理。请注意,此 sighash 与最终区块哈希不同,后者还包括签名。
sighash 使用标准 secp256k1
曲线进行签名,并将生成的 65 字节签名(R
、S
、V
,其中 V
为 0
或 1
)嵌入到 extraData
中作为尾随 65 字节后缀。
为确保恶意签名者(密钥泄露)无法在网络中造成严重破坏,每个签名者最多只能在 SIGNER_LIMIT
个连续区块中签名一次。顺序不是固定的,但轮流内签名比轮流外签名更重要(DIFF_INTURN
)比 DIFF_NOTURN
)。
授权策略
只要签名者符合上述规范,他们就可以按照他们认为合适的方式授权和分配区块。但是,以下建议的策略将减少网络流量和小分叉,因此它是一个建议的功能:
- 如果允许签名者签署区块(在授权列表中并且最近没有签名)。
- 计算下一个区块的最佳签名时间(父时间戳 +
BLOCK_PERIOD
)。 - 如果签名者在轮流内,请等待确切的时间到达,立即签名并广播。
- 如果签名者在轮流外,请将签名延迟
rand(SIGNER_COUNT * 500ms)
。
- 计算下一个区块的最佳签名时间(父时间戳 +
这个小策略将确保轮流内的签名者(其区块权重更高)在签名和传播方面比轮流外的签名者略有优势。此外,该方案允许随着签名者数量的增加进行一些扩展。
对签名者进行投票
每个 epoch 转换(包括创世区块)都充当无状态检查点,有能力的客户端应该能够从中进行同步,而无需任何先前的状态。这意味着 epoch 区块头不得包含投票,所有未解决的投票都将被丢弃,并且统计从头开始。
对于所有非 epoch 转换区块:
- 签名者可以在自己的每个区块中投一票,以提议更改授权列表。
- 每个目标受益人仅保留来自单个签名者的最新提案。
- 随着链的进展,实时统计投票(允许并发提案)。
- 达到多数共识的提案
SIGNER_LIMIT
立即生效。 - 为简化客户端实施,不应对无效提案进行处罚。
生效的提案需要丢弃该提案的所有待处理投票(包括赞成和反对),并从头开始。
级联投票
在取消签名者授权期间可能会出现一个复杂的极端情况。当以前授权的签名者被删除时,批准提案所需的签名者数量可能会减少一个。这可能会导致一个或多个待处理提案达到多数共识,其执行可能会进一步级联到传递新提案。
当多个冲突提案同时通过时(例如,添加新签名者与删除现有签名者),处理此方案并不明显,其中评估顺序可能会大大改变最终授权列表的结果。由于签名者可以在他们生成的每个区块中反转自己的投票,因此哪个提案将是“第一个”并不那么明显。
为避免级联执行可能造成的陷阱,Clique 提案明确禁止级联效应。 换句话说:只有当前区块头/投票的“受益人”可以被添加到/从授权列表中删除。如果这导致其他提案达成共识,则这些提案将在再次“触及”其各自的受益人时执行(假设当时多数共识仍然成立)。
投票策略
由于区块链可能存在小的重组,因此“投掷即忘”的简单投票机制可能不是最佳的,因为包含单个投票的区块可能最终不会出现在最终链上。
一个简单但有效的策略是允许用户在签名者上配置“提案”(例如,“添加 0x…”、“删除 0x…”)。然后,签名代码可以为它签名的每个区块选择一个随机提案并注入它。这确保了链上最终会注意到多个并发提案以及重组。
此列表可能会在一定数量的区块/epoch 后过期,但重要的是要意识到“看到”一个提案通过并不意味着它不会被重组,因此在提案通过后不应立即将其删除。
测试用例
// block 表示由特定帐户签名的单个区块,其中
// 该帐户可能已进行也可能未进行 Clique 投票。
type block struct {
signer string // 签署此特定区块的帐户
voted string // 如果签名者投票添加/删除某人,则为可选值
auth bool // 投票是授权(或取消授权)
checkpoint []string // 如果这是 epoch 区块,则为授权签名者的列表
}
// 定义各种投票场景以进行测试
tests := []struct {
epoch uint64 // epoch 中的区块数(未设置 = 30000)
signers []string // 创世中的授权签名者的初始列表
blocks []block // 签名区块的链,可能会影响授权
results []string // 所有区块后的授权签名者的最终列表
failure error // 根据规则,某些区块无效时的失败
}{
{
// 单个签名者,未投任何票
signers: []string{"A"},
blocks: []block{
{signer: "A"}
},
results: []string{"A"},
}, {
// 单个签名者,投票添加其他两个(仅接受第一个,第二个需要 2 票)
signers: []string{"A"},
blocks: []block{
{signer: "A", voted: "B", auth: true},
{signer: "B"},
{signer: "A", voted: "C", auth: true},
},
results: []string{"A", "B"},
}, {
// 两个签名者,投票添加其他三个(仅接受前两个,第三个已经需要 3 票)
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "C", auth: true},
{signer: "B", voted: "C", auth: true},
{signer: "A", voted: "D", auth: true},
{signer: "B", voted: "D", auth: true},
{signer: "C"},
{signer: "A", voted: "E", auth: true},
{signer: "B", voted: "E", auth: true},
},
results: []string{"A", "B", "C", "D"},
}, {
// 单个签名者,删除自身(很奇怪,但是通过显式允许它会减少一个极端情况)
signers: []string{"A"},
blocks: []block{
{signer: "A", voted: "A", auth: false},
},
results: []string{},
}, {
// 两个签名者,实际上需要双方同意才能删除其中任何一个(未满足)
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "B", auth: false},
},
results: []string{"A", "B"},
}, {
// 两个签名者,实际上需要双方同意才能删除其中任何一个(已满足)
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "B", auth: false},
{signer: "B", voted: "B", auth: false},
},
results: []string{"A"},
}, {
// 三个签名者,其中两个决定删除第三个
signers: []string{"A", "B", "C"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B", voted: "C", auth: false},
},
results: []string{"A", "B"},
}, {
// 四个签名者,两个人的共识不足以删除任何人
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B", voted: "C", auth: false},
},
results: []string{"A", "B", "C", "D"},
}, {
// 四个签名者,三个人的共识已经足以删除某人
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "D", auth: false},
{signer: "B", voted: "D", auth: false},
{signer: "C", voted: "D", auth: false},
},
results: []string{"A", "B", "C"},
}, {
// 授权对每个签名者对每个目标进行一次计数
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "C", auth: true},
{signer: "B"},
{signer: "A", voted: "C", auth: true},
{signer: "B"},
{signer: "A", voted: "C", auth: true},
},
results: []string{"A", "B"},
}, {
// 允许同时授权多个帐户
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "C", auth: true},
{signer: "B"},
{signer: "A", voted: "D", auth: true},
{signer: "B"},
{signer: "A"},
{signer: "B", voted: "D", auth: true},
{signer: "A"},
{signer: "B", voted: "C", auth: true},
},
results: []string{"A", "B", "C", "D"},
}, {
// 取消授权对每个签名者对每个目标进行一次计数
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "B", auth: false},
{signer: "B"},
{signer: "A", voted: "B", auth: false},
{signer: "B"},
{signer: "A", voted: "B", auth: false},
},
results: []string{"A", "B"},
}, {
// 允许同时取消授权多个帐户
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B"},
{signer: "C"},
{signer: "A", voted: "D", auth: false},
{signer: "B"},
{signer: "C"},
{signer: "A"},
{signer: "B", voted: "D", auth: false},
{signer: "C", voted: "D", auth: false},
{signer: "A"},
{signer: "B", voted: "C", auth: false},
},
results: []string{"A", "B"},
}, {
// 来自已取消授权的签名者的投票会立即被丢弃(取消授权投票)
signers: []string{"A", "B", "C"},
blocks: []block{
{signer: "C", voted: "B", auth: false},
{signer: "A", voted: "C", auth: false},
{signer: "B", voted: "C", auth: false},
{signer: "A", voted: "B", auth: false},
},
results: []string{"A", "B"},
}, {
// 来自已取消授权的签名者的投票会立即被丢弃(授权投票)
signers: []string{"A", "B", "C"},
blocks: []block{
{signer: "C", voted: "D", auth: true},
{signer: "A", voted: "C", auth: false},
{signer: "B", voted: "C", auth: false},
{signer: "A", voted: "D", auth: true},
},
results: []string{"A", "B"},
}, {
// 不允许级联更改,只能更改被投票的帐户
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B"},
{signer: "C"},
{signer: "A", voted: "D", auth: false},
{signer: "B", voted: "C", auth: false},
{signer: "C"},
{signer: "A"},
{signer: "B", voted: "D", auth: false},
{signer: "C", voted: "D", auth: false},
},
results: []string{"A", "B", "C"},
}, {
// 超出范围(通过取消授权)达成共识的更改会在触摸时执行
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B"},
{signer: "C"},
{signer: "A", voted: "D", auth: false},
{signer: "B", voted: "C", auth: false},
{signer: "C"},
{signer: "A"},
{signer: "B", voted: "D", auth: false},
{signer: "C", voted: "D", auth: false},
{signer: "A"},
{signer: "C", voted: "C", auth: true},
},
results: []string{"A", "B"},
}, {
// 超出范围(通过取消授权)达成共识的更改可能会在第一次触摸时超出共识
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B"},
{signer: "C"},
{signer: "A", voted: "D", auth: false},
{signer: "B", voted: "C", auth: false},
{signer: "C"},
{signer: "A"},
{signer: "B", voted: "D", auth: false},
{signer: "C", voted: "D", auth: false},
{signer: "A"},
{signer: "B", voted: "C", auth: true},
},
results: []string{"A", "B", "C"},
}, {
// 确保待处理的投票不会在授权状态更改后仍然存在。
// 只有当签名者被快速添加、删除然后重新添加(或相反)时,才会出现此极端情况,
// 而其中一个原始投票者已被删除。 如果在系统的某个位置保留了过去的投票,
// 这将干扰最终的签名者结果。
signers: []string{"A", "B", "C", "D", "E"},
blocks: []block{
{signer: "A", voted: "F", auth: true}, // 授权 F,需要 3 票
{signer: "B", voted: "F", auth: true},
{signer: "C", voted: "F", auth: true},
{signer: "D", voted: "F", auth: false}, // 取消授权 F,需要 4 票(将 A 先前的投票“保持不变”)
{signer: "E", voted: "F", auth: false},
{signer: "B", voted: "F", auth: false},
{signer: "C", voted: "F", auth: false},
{signer: "D", voted: "F", auth: true}, // 几乎授权 F,需要 2/3 票
{signer: "E", voted: "F", auth: true},
{signer: "B", voted: "A", auth: false}, // 取消授权 A,需要 3 票
{signer: "C", voted: "A", auth: false},
{signer: "D", voted: "A", auth: false},
{signer: "B", voted: "F", auth: true}, // 完成授权 F,需要 3/3 票
},
results: []string{"B", "C", "D", "E", "F"},
}, {
// Epoch 转换会重置所有投票,以允许链检查点
epoch: 3,
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "C", auth: true},
{signer: "B"},
{signer: "A", checkpoint: []string{"A", "B"}},
{signer: "B", voted: "C", auth: true},
},
results: []string{"A", "B"},
}, {
// 未经授权的签名者不应能够签名区块
signers: []string{"A"},
blocks: []block{
{signer: "B"},
},
failure: errUnauthorizedSigner,
}, {
// 最近签名的授权签名者不应能够再次签名
signers: []string{"A", "B"},
blocks []block{
{signer: "A"},
{signer: "A"},
},
failure: errRecentlySigned,
}, {
// 在批量导入的检查点区块上,最近的签名不应重置
epoch: 3,
signers: []string{"A", "B", "C"},
blocks: []block{
{signer: "A"},
{signer: "B"},
{signer: "A", checkpoint: []string{"A", "B", "C"}},
{signer: "A"},
},
failure: errRecentlySigned,
},,
}
实现
参考实现是 go-ethereum 的一部分,并且自 2017 年 4 月以来一直作为 Rinkeby 测试网背后的共识引擎。
版权
在 CC0 下放弃版权及相关权利。
Citation
Please cite this document as:
Péter Szilágyi <peterke@gmail.com>, "EIP-225: Clique 权威证明共识协议," Ethereum Improvement Proposals, no. 225, March 2017. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-225.