PoA共识引擎算法实现分析
clique中一些概念和定义
- EPOCH_LENGTH : epoch长度是30000个block, 每次进入新的epoch,前面的投票都被清空,重新开始记录,这里的投票是指加入或移除signer
- BLOCK_PERIOD : 出块时间, 默认是15s
-
UNCLE_HASH : 总是
Keccak256(RLP([]))
,因为没有uncle - SIGNER_COUNT : 每个block都有一个signers的数量
-
SIGNER_LIMIT : 等于
(SIGNER_COUNT / 2) + 1
. 每个singer只能签名连续SIGNER_LIMIT个block中的1个- 比如有5个signer:ABCDE, 对4个block进行签名, 不允许签名者为ABAC, 因为A在连续3个block中签名了2次
-
NONCE_AUTH : 表示投票类型是加入新的signer; 值=
0xffffffffffffffff
-
NONCE_DROP : 表示投票类型是踢除旧的的signer; 值=
0x0000000000000000
- EXTRA_VANITY : 代表block头中Extra字段中的保留字段长度: 32字节
- EXTRA_SEAL : 代表block头中Extra字段中的存储签名数据的长度: 65字节
- IN-TURN/OUT-OF-TURN : 每个block都有一个in-turn的signer, 其他signers是out-of-turn, in-turn的signer的权重大一些, 出块的时间会快一点, 这样可以保证该高度的block被in-turn的signer挖到的概率很大.
clique中最重要的两个数据结构:
- 共识引擎的结构:
type Clique struct {
config *params.CliqueConfig // 系统配置参数
db ethdb.Database // 数据库: 用于存取检查点快照
recents *lru.ARCCache //保存最近block的快照, 加速reorgs
signatures *lru.ARCCache //保存最近block的签名, 加速挖矿
proposals map[common.Address]bool //当前signer提出的proposals列表
signer common.Address // signer地址
signFn SignerFn // 签名函数
lock sync.RWMutex // 读写锁
}
- snapshot的结构:
type Snapshot struct {
config *params.CliqueConfig // 系统配置参数
sigcache *lru.ARCCache // 保存最近block的签名缓存,加速ecrecover
Number uint64 // 创建快照时的block号
Hash common.Hash // 创建快照时的block hash
Signers map[common.Address]struct{} // 此刻的授权的signers
Recents map[uint64]common.Address // 最近的一组signers, key=blockNumber
Votes []*Vote // 按时间顺序排列的投票列表
Tally map[common.Address]Tally // 当前的投票计数,以避免重新计算
}
除了这两个结构, 对block头的部分字段进行了复用定义, ethereum的block头定义:
type Header struct {
ParentHash common.Hash
UncleHash common.Hash
Coinbase common.Address
Root common.Hash
TxHash common.Hash
ReceiptHash common.Hash
Bloom Bloom
Difficulty *big.Int
Number *big.Int
GasLimit *big.Int
GasUsed *big.Int
Time *big.Int
Extra []byte
MixDigest common.Hash
Nonce BlockNonce
}
- 创世块中的Extra字段包括:
- 32字节的前缀(extraVanity)
- 所有signer的地址
- 65字节的后缀(extraSeal): 保存signer的签名
- 其他block的Extra字段只包括extraVanity和extraSeal
- Time字段表示产生block的时间间隔是:blockPeriod(15s)
- Nonce字段表示进行一个投票: 添加( nonceAuthVote:
0xffffffffffffffff
)或者移除( nonceDropVote:0x0000000000000000
)一个signer - Coinbase字段存放 被投票 的地址
- 举个栗子: signerA的一个投票:加入signerB, 那么Coinbase存放B的地址
- Difficulty字段的值: 1-是 本block的签名者 (in turn), 2- 非本block的签名者 (out of turn)
下面对比较重要的函数详细分析实现流程
Snapshot.apply(headers)
创建一个新的授权signers的快照, 将从上一个snapshot开始的区块头中的proposals更新到最新的snapshot上
- 对入参headers进行完整性检查: 因为可能传入多个区块头, block号必须连续
- 遍历所有的header, 如果block号刚好处于epoch的起始(number%Epoch == 0),将snapshot中的Votes和Tally复位( 丢弃历史全部数据 )
- 对于每一个header,从签名中恢复得到 signer
- 如果该signer在snap.Recents中, 说明 最近已经有过签名 , 不允许再次签名, 直接 返回 结束
-
记录 该signer是该block的签名者:
snap.Recents[number] = signer
- 统计header.Coinbase的投票数,如果 超过signers总数的50%
- 执行加入或移除操作
- 删除snap.Recents中的一个signer记录: key=number- (uint64(len(snap.Signers)/2 + 1)), 表示释放该signer,下次可以对block进行签名了
- 清空被移除的Coinbase的投票
- 移除snap.Votes中该Conibase的所有投票记录
- 移除snap.Tally中该Conibase的所有投票数记录
共识引擎clique的初始化
在 Ethereum.StartMining
中,如果Ethereum.engine配置为clique.Clique, 根据当前节点的矿工地址(默认是acounts[0]), 配置clique的 签名者 : clique.Authorize(eb, wallet.SignHash)
,其中 签名函数 是SignHash,对给定的hash进行签名.
获取给定时间点的一个快照 Clique.snapshot
- 先查找Clique.recents中是否有缓存, 有的话就返回该snapshot
- 在查找持久化存储中是否有缓存, 有的话就返回该snapshot
- 如果是创世块
- 从Extra中取出所有的signers
newSnapshot(Clique.config, Clique.signatures, 0, genesis.Hash(), signers)
- signatures是最近的签名快照
- signers是所有的初始signers
- 把snapshot加入到Clique.recents中, 并持久化到db中
- 其他普通块
- 沿着父块hash一直往回找是否有snapshot, 如果没找到就记录该区块头
- 如果找到最近的snapshot, 将前面记录的headers 都
applay
到该snapshot上 - 保存该最新的snapshot到缓存Clique.recents中, 并持久化到db中
Clique.Prepare(chain , header)
Prepare是共识引擎接口之一. 该函数配置header中共识相关的参数(Cionbase, Difficulty, Extra, MixDigest, Time)
- 对于非epoch的block(
number % Epoch != 0
):
- 得到Clique.proposals中的投票数据(例:A加入C, B踢除D)
- 根据snapshot的signers分析投票数否有效(例: C原先没有在signers中, 加入投票有效, D原先在signers中,踢除投票有效)
- 从被投票的地址列表(C,D)中, 随机选择一个地址 ,作为该header的Coinbase,设置Nonce为加入(
0xffffffffffffffff
)或者踢除(0x0000000000000000
) -
Clique.signer
如果是本轮的签名者(in-turn), 设置header.Difficulty = diffInTurn(1), 否则就是diffNoTurn(2) - 配置header.Extra的数据为[
extraVanity
+snap中的全部signers
+extraSeal
] - MixDigest需要配置为nil
- 配置时间戳:Time为父块的时间+15s
重点: Clique.Seal(chain, block , stop)
Seal也是共识引擎接口之一. 该函数用clique.signer对block的进行签名. 在pow]算法中, 该函数进行hash运算来解"难题".
- 如果signer没有在snapshot的signers中,不允许对block进行签名
- 如果不是本block的签名者,延时一定的时间(随机)后再签名, 如果是本block的签名者, 立即签名.
- 签名结果放在Extra的extraSeal的65字节中
Clique.VerifySeal(chain, header)
VerifySeal也是共识引擎接口之一.
- 从header的签名中恢复账户地址,改地址要求在snapshot的signers中
- 检查header中的Difficulty是否匹配(in turn或out of turn)
Clique.Finalize
Finalize也是共识引擎接口之一. 该函数生成一个block, 没有叔块处理,也没有奖励机制
-
header.Root
: 状态根保持原状 -
header.UncleHash
: 为nil -
types.NewBlock(header, txs, nil, receipts)
: 封装并返回最终的block
API.Propose(addr, auth)
添加一个proposal: 调用者对addr的投票, auth表示加入还是踢出
API.Discard(addr)
删除一个proposal