配合代码食用(Geth v1.9.0 stable)
背景:
以太坊目前有ethash和clique两个共识引擎,其中ethash是用于正式网络的PoW(proof-of-work)共识引擎,clique是用于测试网络的PoA(proof-of-authority)共识引擎
介绍:
clique是依靠授权节点(signers)顺序产生block,可以由已授权的signer选举(投票超过50%)加入新的signer和踢出已授权signer的联盟链共识引擎
使用:
通过编译好的puppeth程序创建共识为poa的genesis.json文件,geth执行init操作读取json文件,然后启动私链节点
代码分析:
consenesus/clique/clique.go
1.常量和变量意义:
checkpointInterval = 1024 ,每隔1024块保存投票快照到数据库
inmemorySnapshots = 128 ,保存在内存中的快照数量
inmemorySignatures = 4096 ,保存在内存中的最近区块的签名者数量
wiggleTime = 500 * time.Millisecond ,用于非顺序出块人出块延迟时间计算,在0 ~ (signerCount/2+1)*wiggleTime范围内随机取一个值作为延迟时间
epochLength = uint64(30000) ,每隔30000块清空所有投票
extraVanity = 32 ,extra-data保留32个字节的前缀
extraSeal = 65 ,extra-data为区块signer保留65个字节的后缀
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") ,投票加入一个签名者使用的nonce
nonceDropVote = hexutil.MustDecode("0x0000000000000000") ,投票踢出一个签名者使用的nonce
diffInTurn = big.NewInt(2) ,出块人是顺序出块人时的区块难度值
diffNoTurn = big.NewInt(1) ,出块人不是顺序出块人时的区块难度值
2.重要方法:
Prepare
给header.Coinbase赋值(投票目标地址)
给header.Nonce赋值(投票类型为加入或提出签名者)
给header.Difficulty赋值(是顺序出块人为2,否则为1)
给header.Extra赋值(32字节前缀+所有签名者地址+65字节后缀用于区块签名)
给header.MixDigest赋值为空,摘要,在pow中用于防篡改校验(VerifySeal)
给header.Time赋值(等于parent.Time+Period或等于now>parent.Time+Period)
Finalize
给header.Root赋值
给header.UncleHash赋值为空(poa没有叔区块)
构建block返回
Seal
判断在最近出块记录中,则不允许出块(维持一个大小为signercount/2+1的signer队列Recents,用于判断最近是否出过块)
计算应该delay的时间
拷贝签名到header.Extra后65字节
delay到出块时间后将sealed block放入worker.resultCh
VerifySeal
验证header.Number不为0
验证区块签名者在签名者列表中
验证区块签名者最近没出过块
验证header.Difficulty和轮次是否匹配
snapshot
获取基于某个块的Snapshot
优先从内存中获取
如果内存中没有,且恰好number是checkpointInterval的整数倍,从数据库取
如果恰好number是epochLength的整数倍,创建一个Snapshot并保存
还是没有从上一块(number-1)取
取到了Snapshot,如果是从number块前面的块取的,要snap.apply(headers)
Snapshot保存到缓存中
如果恰好number是checkpointInterval的整数倍且有执行apply,保存Snapshot到数据库
VerifyHeader
验证header有效性
APIs
获取共识引擎提供的RPC接口
consenesus/clique/snapshot.go
1.结构体:
type Snapshot struct { // Snapshot是基于某个区块高度的投票认证状态
config *params.CliqueConfig // 配置参数
sigcache *lru.ARCCache // 缓存最近区块签名地址,用于快速得到签名地址
Number uint64 // Snapshot创建时的区块高度
Hash common.Hash // Snapshot创建时的区块哈希
Signers map[common.Address]struct{} // 已认证签名者集合
Recents map[uint64]common.Address // 最近已签名区块的签名者集合 //数量为 SIGNER_COUNT/2+1 ,可保证即使存在恶意signer,他最多只能攻击连续块 SIGNER_COUNT/2+1 中的1个
Votes []*Vote // 按时间排序的投票集合
Tally map[common.Address]Tally // 当前投票记录避免重复计算
}
type Tally struct { // 投票记录
Authorize bool `json:"authorize"` // 投票是加入或者提出某个账户
Votes int `json:"votes"` // 想要通过的提议当前的投票数
}
type Vote struct {
Signer common.Address // 投这个票的已认证签名者
Block uint64 // 投这个票的区块高度(太旧的投票是过期投票)
Address common.Address // 被投的账户
Authorize bool // 是认证还是解除认证这个被投票的账户
}
2.重要方法:
apply
验证headers有效性和连续性
snap := s.copy()
遍历headers:
每隔epochLength块清空Votes和Tally
删除Recents中最旧的一个singer使其能够再次签名
获取header的签名者,判断如果不在签名者列表中或者在Recents列表中,return,否则将签名者放入Recents中
遍历Votes,丢弃掉之前这个header的签名者投给同一个账户(header.Coinbase)的投票
投票,即在Snapshot.Tally和Snapshot.Votes中添加记录
根据投票记录Tally判断如果当前被投票地址header.Coinbase票数大于当前签名者列表长度/2:
如果提议为加入新的签名者,签名者列表加入新签名者header.Coinbase;如果提议为踢出已认证签名者,将其从签名者列表删除,然后删除Recents中最旧的一个,丢弃所有该签名者投的票
丢弃之前所有投给该签名者的票和记录
更新snap.Number和snap.Hash到最新
consenesus/clique/api.go
1.结构体:
type API struct { //API是一个面向用户的RPC接口对象,用于控制signer和poa投票机制
chain consensus.ChainReader
clique *Clique
}
2.clique共识引擎提供的RPC接口有:
func (api *API) GetSnapshot(number *rpc.BlockNumber) (*Snapshot, error) ,基于区块高度为number的块得到状态快照(state snapshot)
func (api *API) GetSnapshotAtHash(hash common.Hash) (*Snapshot, error) ,基于区块哈希为hash的块得到状态快照
func (api *API) GetSigners(number *rpc.BlockNumber) ([]common.Address, error) ,基于区块高度为number的块得到签名者列表
func (api *API) GetSignersAtHash(hash common.Hash) ([]common.Address, error) ,基于区块哈希为hash的块得到签名者列表
func (api *API) Proposals() map[common.Address]bool ,获取当前所有提议
func (api *API) Propose(address common.Address, auth bool) ,提议加入新的认证签名者或踢出现有认证签名者
func (api *API) Discard(address common.Address) ,丢弃已有的一个提议
小结
在以太坊上增加dpos共识引擎可以参考poa,这个后面再聊~