以太坊PoA共识协议基本介绍----Clique
1. 开发PoA的动机
基于PoW的共识协议,有一个很明显的缺点,就是使用PoW共识协议的区块链网络的安全非常依赖于参与这个网络的所有正常节点的算力。所以一个刚启动的区块链网络,或者是一些测试网络(比如以太坊有一些testnet),由于网络中总算力还比较小,所以很容易遭受到51%攻击。要解决这个问题,有两个方法,一个是PoS(proof-of-stake),还有就是PoA(proof-of-authority)。
2. 设计PoA协议的约束
在PoA协议中,所有的区块都是被网络中某些被授权的签发者(authorized signer)构造并发布的。所有的节点接收到区块的时候,都能通过这个签发者的信息来验证区块的有效性。这里的难点是,如何维护一个authorized signer的列表, 并且及时更新这个列表。
一种简单的方法是,维护在一个智能合约的存储中。但是轻量级的同步方式在同步的时候是无法访问状态的(这里没理解是啥意思,为啥同步的时候不能访问到智能合约的状态呢)
所以,signer的列表只能被全部包含在区块头中
那么,我们要改变以太坊区块头的结构??不行,扩展太麻烦,要让网络中所有的节点升级,否则会造成分叉,而且还可能会造成安全问题 (感觉这里体现出了去中心化应用的一个弊端,迭代升级是比较困难的)
因此,要在不改变块头结构的情况下,将list of signer的信息包含在区块头中,只能重复利用一下当前块头的一些字段来实现投票和签名的功能咯
区块头结构:
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time uint64 `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
/*
TODO (MariusVanDerWijden) Add this field once needed
// Random was added during the merge and contains the BeaconState randomness
Random common.Hash `json:"random" rlp:"optional"`
*/
}
把区块头中的extraData字段扩充为65字节(因为secp256k1函数的输出是65字节),并作为区块打包者signer的签名(secp256k1函数)。区块接收者可以通过这里进行验签。那么同时miner字段(即Coinbase)就没用了,因为可以通过验签来得到miner的地址。
而且PoA不用挖矿,所以nonce字段也没用了。因此就用miner字段和nonce字段来投票好了。
在常规出块期间,nonce和miner字段都置为0即可。
如果一个singer想要发起投票,那它就会把miner字段设置为它要投票的地址,nonce字段设置为全0就是希望把它提出signer列表,全1(0xfff…fff)就是它希望把这个地址加入signer列表。其他客户端可以在处理区块数据的同时,统计投票信息。
也有epoch的概念,一个epoch代表一段连续的固定数量的区块。比如从第0个到29999个区块,就是一个epoch, 同理第 30000到地59999个区块又是一个epoch。每个epoch结束的时候就会更新一次signer list, 相当于checkpoint操作。新加入的节点同步状态时,只需要同步最近的checkpoint block所包含的signer list即可,不需要重放所有的投票过程
3. 具体细节
定义常量
定义了如下的全局常量:
EPOC_LENGTH : 一个epoch的区块数量。即每隔这么多区块之后,会重置还没有被处理的投票 (建议和以太坊主链ethash epoch保持一致–30000)
BLOCK_PERIOD : 最短的出块时间差 (以太坊主链是15s)
EXTRA_VANITY :为signer保留的extra-data字段的固定前缀字节数,建议32
EXTRA_SEAL : 为signer保留的extra-data字段固定后缀字节数–65字节(secp256k1函数)。 创世区块的这里填充0
NONCE_AUTH : 0xffffffffffffffff, 投票加入一个地址到signer list
NONCE_DROP: 0x0000000000000000 , 投票从signer list踢出一个地址
UNCLE_HASH : 永远是Keccak256(RLP([])), 因为叔父节点在PoA中不会出现
DIFF_NOTURN : 包含out-of-turn签名的区块的 区块分数(难度)。就是说,区块签发的工作应该由signer list里的signer轮流签发,如果某个signer插队,那该区块的difficulty字段就为DIFF_NOTURN = 1
DIFF_INTURN: 包含in-turn签名的区块的 区块分数(难度)。如果区块签发的工作由signer list里的signer正常轮流签发,那么该区块的difficulty字段就为DIFF_INTURN = 2
还有每个区块都有的一些常量:
BLOCK_NUMBER : 当前区块的区块高度,创世区块为0
SIGNER_COUNT: 在链中的特定实例上有效的授权签名者数量
SIGNER_INDEX : (从0开始)该区块的signer在有序列表中索引位置
SIGNER_LIMIT :每个signer只能签名一次(发布一次区块) 的连续区块的数量。比如区块号为n的区块是由signer1签署发布的,那么在接下来的 SIGNER_LIMIT - 1个区块,都不能再由signer1签署发布。这个值必须为 floor(SIGNER_COUNT / 2) + 1。
复用ethash(即以太坊挖矿算法) 区块头结构的字段:
beneficiary / miner : 原来的矿工地址字段,现在用来投票。 不投票的时候设置为0。一个epoch结束时的checkpoint区块,必须设置为0.
nonce : 原来挖矿用的随机数,现在被用来投票。可被赋值为NONCE_DROP , NONCE_AUTH 。 一个epoch结束时的checkpoint区块,必须设置为0.
extraData : checkpointing,签名以及signer vanity(没搞懂啥意思)的联合字段
开头的EXTRA_VANITY会包含任意signer的vanity data(这个data没搞懂是啥).
结尾的EXTRA_SEAL字段是区块颁发者signer的签名
checkpoint block的区块头必须在中间包含signer的有序列表(N * 20 字节)
mixHash : 用于分叉保护逻辑的保留字段
ommersHash : 必须为UNCLE_HASH
timestamp: 必须至少为父区块的timestamp + BLOCK_PERIOD
difficulty : 包含区块的分数用于判断链的质量(没搞懂啥意思)
如果 BLOCK_NUMBER % SIGNER_COUNT != SIGNER_INDEX ,那么 difficulty = DIFF_NOTURN。 否则为 DIFF_INTURN。 也就是说,区块签发工作是由signer list里的signer轮流签发,如果有个signer插队,那么difficulty就会被设置为DIFF_NOTURN。
签发区块
一个signer签发区块的时候,首先要用secp256k1函数对这个区块头数据(除了extraData最后那65字节,因为这个时候还没有)进行签名。得到65字节的签名数据再接到extraData的最后65字节中(就是上面提到的EXTRA_SEAL字段)
为了防止恶意节点破坏网络,每个signer在连续SIGNER_LIMIT个区块中,只能签发一次。没有固定签发顺序,但是顺序签发的权重更大(DIFF_INTURN 为 2, DIFF_NOTURN为1)
签发策略
只要signers符合上诉规则,就可以签发并发布他们认为符合的区块。同时遵守以下规则:
如果一个signer允许签发区块(在signer list 中并且最近没签发过)
1. 计算最优的签发时间(父区块timestamp + BLOCK_PERIOD)
2. 如果这个signer是in-turn的,即正好应该轮到它(BLOCK_NUMBER % SIGNER_COUNT == SIGNER_INDEX), 那就可以立即签名并广播
3. 否则,就要推迟rand(SIGNER_COUNT * 500ms)的时间
这样可以尽可能保证都是轮流签发的
投票机制
对于一个非epoch结束时的block
- signers在签发一个区块时,可以发起一个投票,目的是改变signer list的状态。
- 对每个投票目标来说,单个signer的最新提议才会被保留。也就说单个signer重复投票是没用的。
- 投票统计过程与链增长过程并发进行
- 当某个提议的投票数达到半数以上的signer承认(即SIGNER_LIMIT),这个提议会立即生效
- 为了客户端实现简单起见,无效的提议不会被惩罚
一个提议一旦生效,与之相关的所有投票都会被丢弃(无论是赞同的还是反对的)
级联投票
当把一个signer剔除signer list的投票生效的时候,会出现一种复杂的情况。比如某一个区块发布出来了,这个区块头中的投票信息,正好可以使剔除某个signer的提议生效,于是这个signer被剔除了,同时signer的数量减一了,那么就会导致提议通过的所需最小投票数也减少一个,从而会导致其他一些原本未达到生效要求票数的提议提前生效了。这些生效的提议可能又会导致更多的提议提前生效,产生级联效应。
在Clique协议中,会显示的禁止这种级联效应。规定某一个区块的投票结果,只能影响该投票的目标的提议是否生效,不会去检查其它提议
投票策略
一个简单有效的策略是允许用户配置在signers上的提议(比如 “add 0x…”, “drop 0x…”). 然后在签发区块的时候,signer会随机选取一个提议并发布。这可以使多个提议可以并发地投票
看到这里,我就有以下疑问了:
提议到底使怎么发起的?用户配置是指配置文件吗?那用户还得不停改配置吗?那收到其他signer的投票的时候怎么跟票呢??什么情况下会发起加入新节点的投票,什么情况下会发起剔除节点的投票呢?如果是用户自己定义的话,那用户肯定不希望自己的节点被剔除,也肯定不希望别人的节点加入吧???比如我自己有个机器,加入了这个网络,我想成为signer,我得等别人投我票?可是别人为什么要投我呢,感觉没有投别人账户的动机呀???
例子
//block 表示被一个特定的账户签发的单个区块
// 这个账户可能发起了投票,也可能没有发起
type block struct {
signer string // 签发这个区块的账户
voted string // 可选值,如果signer发起投票就将这个值设置为目标账户
auth bool // 可选值,如果signer发起投票,true为投票加入,false为投票踢出
checkpoint []string // 如果这个区块是epoch区块,这里为signers的有序列表
}
// 定义不同的投票场景用来测试
tests := []struct {
epoch uint64 // 定义多少个block为一个epoch (unset = 30000)
signers []string // 在创世区块中定义的初始signers
blocks []block // 用于存储被签发的区块
results []string // 用于存储经历了所有区块后signers list 的最终状态
failure error // 如果某个区块不满足要求,就会失败
}{
{
// 场景1 : 只有一个signer, 没有投票
signers: []string{"A"},
blocks: []block{
{signer: "A"}
},
results: []string{"A"},
}, {
// Single signer, voting to add two others (only accept first, second needs 2 votes)
//场景2 : 单个signer, 投票加入两个其他的signer(只接受了一个, 因为第二个需要两票才能通过)
signers: []string{"A"},
blocks: []block{
{signer: "A", voted: "B", auth: true},
{signer: "B"},
{signer: "A", voted: "C", auth: true},
},
results: []string{"A", "B"},
}, {
// 场景3 : 两个signer, 投票加入三个其他的signer(只接受了前两个, 因为第三个需要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"},
}, {
// 场景4 : 单个signer, 踢出自己
signers: []string{"A"},
blocks: []block{
{signer: "A", voted: "A", auth: false},
},
results: []string{},
}, {
// 场景5 : 两个signer,踢出对方,实际上需要双方同意才能踢出对方,没满足条件的情况
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "B", auth: false},
},
results: []string{"A", "B"},
}, {
// 场景6 : 两个signer,踢出对方,实际上需要双方同意才能踢出对方,满足条件的情况
signers: []string{"A", "B"},
blocks: []block{
{signer: "A", voted: "B", auth: false},
{signer: "B", voted: "B", auth: false},
},
results: []string{"A"},
}, {
// 场景7 : 三个signer, 其中两个踢出第三个
signers: []string{"A", "B", "C"},
blocks: []block{
{signer: "A", voted: "C", auth: false},
{signer: "B", voted: "C", auth: false},
},
results: []string{"A", "B"},
}, {
//场景8 : 四个signer, 其中两个达成一致并不能踢出任何一个
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"},
}, {
//场景9 : 四个signer, 其中三个达成一致可以踢出另外一个
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"},
}, {
//场景10 : 同一个signer的重复投票是没用的
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"},
}, {
//场景11 : 可以并发第发起多个投票
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"},
}, {
//场景12 : 踢出投票,同一个signer的重复投票也是没用的
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"},
}, {
//场景12 : 踢出投票, 也可以并发发起多个投票, 并且一个被踢出后, 提议通过的最少要求票数也减少了
signers: []string{"A", "B", "C", "D"},
blocks: []block{
{signer: "A", voted: "C"
{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"},
}, {
//场景13 : 如果一个signer被踢出了, 那么它之前的投票也作废了
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"},
}, {
//场景14 : 同上
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"},
}, {
//场景15 : 不允许级联投票处理
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"},
}, {
//场景16 : 只能处理被投票者相关的投票结果
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"},
}, {
//场景17 : 同上, 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"},
},{
// 场景19 : epoch block会丢弃所有未处理的投票
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"},
}, {
// 场景20 : 非signer账户不能发布区块
signers: []string{"A"},
blocks: []block{
{signer: "B"},
},
failure: errUnauthorizedSigner,
}, {
// 场景21 : 同一个signer不能连续发布区块
signers: []string{"A", "B"},
blocks []block{
{signer: "A"},
{signer: "A"},
},
failure: errRecentlySigned,
}, {
// 场景22 : checkpoint block也不能重置 连续发布区块的规则
epoch: 3,
signers: []string{"A", "B", "C"},
blocks: []block{
{signer: "A"},
{signer: "B"},
{signer: "A", checkpoint: []string{"A", "B", "C"}},
{signer: "A"},
},
failure: errRecentlySigned,
},,
}
原文链接:https://eips.ethereum.org/EIPS/eip-225