上篇我们已经分析到了节点投票后,即提出了一个提案(proposals)后,被打包在了区块头中,现在我们分析下,每个节点如何根据这些提案更新签名者(signers)列表。也就是让这些提案生效的过程。
在这之前,我们要先了解一个结构体-Snapshot, Snapshot是一个快照,不仅是一个缓存,而且存储了最近签名者的map,表示某个时间点上的投票的授权状态。
// Snapshot is the state of the authorization voting at a given point in time.
type Snapshot struct {
config *params.CliqueConfig // Consensus engine parameters to fine tune behavior
sigcache *lru.ARCCache // Cache of recent block signatures to speed up ecrecover
Number uint64 `json:"number"` // Block number where the snapshot was created
Hash common.Hash `json:"hash"` // Block hash where the snapshot was created
Signers map[common.Address]struct{} `json:"signers"` // Set of authorized signers at this moment
Recents map[uint64]common.Address `json:"recents"` // Set of recent signers for spam protections
Votes []*Vote `json:"votes"` // List of votes cast in chronological order
Tally map[common.Address]Tally `json:"tally"` // Current vote tally to avoid recalculating
}
我们先说下Signers和Recents两个参数
Signers: 当下认证签名者的列表
Recents: 最近担当过数字签名算法的signer 的地址,也就是最近签过名的signer地址
快照Snapshot对象中存在投票的Votes和记票的Tally对象,现在我们再看下Votes和Tally结构:
// Vote represents a single vote that an authorized signer made to modify the
// list of authorizations.
type Vote struct {
Signer common.Address `json:"signer"` // Authorized signer that cast this vote
Block uint64 `json:"block"` // Block number the vote was cast in (expire old votes)
Address common.Address `json:"address"` // Account being voted on to change its authorization
Authorize bool `json:"authorize"` // Whether to authorize or deauthorize the voted account
}
// Tally is a simple vote tally to keep the current score of votes. Votes that
// go against the proposal aren't counted since it's equivalent to not voting.
type Tally struct {
Authorize bool `json:"authorize"` // Whether the vote is about authorizing or kicking someone
Votes int `json:"votes"` // Number of votes until now wanting to pass the proposal
}
Vote代表了一个独立的投票,这个投票可以授权一个签名者,更改授权列表。
Signer: 已授权的签名者(通过投票)
Block:投票区块号
Address:被投票的账户,修改它的授权
Authorize:对一个被投票账户是否授权或解授权
Tally是一个简单的用来保存当前投票分数的计分器
Authorize:授权true或移除false
Votes:该提案已获票数
有了上面了解,我们继续,当某个节点收到其他节点广播过来的区块或生成区块时,以太坊会创建一个快照(Snapshot),在Snapshot构造函数中,会涉及到对proposal的处理apply方法。我们先从snapshot函数看起,内容较多,我们直接将分析写在代码中。
// snapshot retrieves the authorization snapshot at a given point in time.
func (c *Clique) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*Snapshot, error) {
//首先定义了两个变量headers和snap,分别用来存储某个快照阶段的区块头和快照对象。
var (
headers []*types.Header
snap *Snapshot
)
//如果snap对象为nil,则一直循环从内存或磁盘上查找快照数据创建snapshot对象。
for snap == nil {
// 如果在内存中查找到了,则直接使用该对象。
if s, ok := c.recents.Get(hash); ok {
snap = s.(*Snapshot)
break
}
// 如果该区块号正好在一个检查点上,则直接从磁盘中获取,如果找到,直接使用.
if number%checkpointInterval == 0 {
if s, err := loadSnapshot(c.config, c.signatures, c.db, hash); err == nil {
log.Trace("Loaded voting snapshot from disk", "number", number, "hash", hash)
snap = s
break
}
}
// 如果是创世区块或者在一个周期的开始并且检查点没有父区块则创建一个快照
if number == 0 || (number%c.config.Epoch == 0 && chain.GetHeaderByNumber(number-1) == nil) {
checkpoint := chain.GetHeaderByNumber(number)
if checkpoint != nil {
hash := checkpoint.Hash()
//此处是从区块头(header)中的Extra字段解出签名者地址
signers := make([]common.Address, (len(checkpoint.Extra)-extraVanity-extraSeal)/common.AddressLength)
for i := 0; i < len(signers); i++ {
copy(signers[i][:], checkpoint.Extra[extraVanity+i*common.AddressLength:])
}
snap = newSnapshot(c.config, c.signatures, number, hash, signers)
if err := snap.store(c.db); err != nil {
return nil, err
}
log.Info("Stored checkpoint snapshot to disk", "number", number, "hash", hash)
break
}
}
// 没有针对这个区块头的快照,则收集区块头并向后移动,直到找到快照或到创世区块。
var header *types.Header
if len(parents) > 0 {
// // 如果我们有明确的父类,从这里一直查找。
header = parents[len(parents)-1]
if header.Hash() != hash || header.Number.Uint64() != number {
return nil, consensus.ErrUnknownAncestor
}
parents = parents[:len(parents)-1]
} else {
// 如果没有明确父类(或者没有更多的),则从数据库中查找
header = chain.GetHeader(hash, number)
if header == nil {
return nil, consensus.ErrUnknownAncestor
}
}
headers = append(headers, header)
number, hash = number-1, header.ParentHash
}
// // 找到了先前的快照,那么将所有pending的区块头都放在它的上面。
for i := 0; i < len(headers)/2; i++ {
headers[i], headers[len(headers)-1-i] = headers[len(headers)-1-i], headers[i]
}
///通过区块头生成一个新的snapshot对象
snap, err := snap.apply(headers)
if err != nil {
return nil, err
}
c.recents.Add(snap.Hash, snap)//将当前快照区块的hash存到recents中。
// 如果正好在一个快照检查点上,则将此快照保存到磁盘上。
if snap.Number%checkpointInterval == 0 && len(headers) > 0 {
if err = snap.store(c.db); err != nil {
return nil, err
}
log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)
}
return snap, err
}
我将代码的分析写在了上面的代码注释中,也可以看到上面的代码通过调用snap的apply方法会重新生成一个snapshot对象,会的参数就是当前pending的区块头。现在我们开始进入这个方法,并将分析写在注释中了。
//apply 通过传过来的区块头并利用当前快照对象生成一个新的快照。
func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) {
// 判断参数有效性,如果区块头数量为0,则直接返回当前snapshot对象
if len(headers) == 0 {
return s, nil
}
// 检查区块头列表数据有效性,下一个区块的区块号是否等于当前区块的区块号加1
for i := 0; i < len(headers)-1; i++ {
if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 {
return nil, errInvalidVotingChain
}
}
//区块头列表的第一个区块的区块号是否等于创建快照的区块号加1
if headers[0].Number.Uint64() != s.Number+1 {
return nil, errInvalidVotingChain
}
// copy一个新的快照对象
snap := s.copy()
//遍历区块头列表,并将投票数据填充到新建的快照对象中
for _, header := range headers {
//如果当前区块是一个检查点,则删除当前区块上所有投票数据 s.config.Epoch 代表一个投票周期。一般是30000个块一个周期,是通过配置获得的
number := header.Number.Uint64()
if number%s.config.Epoch == 0 {
snap.Votes = nil
snap.Tally = make(map[common.Address]Tally)
}
// 找出最早的签名者
//如果区块号大于等于签名者个数一半加1,则从最近签名者列表中删除,这样他就可以继续签名了
if limit := uint64(len(snap.Signers)/2 + 1); number >= limit {
delete(snap.Recents, number-limit)
}
// 从区块头或缓存中取出签名者地址
signer, err := ecrecover(header, s.sigcache)
if err != nil {
return nil, err
}
//检查是否是签名者.
if _, ok := snap.Signers[signer]; !ok {
return nil, errUnauthorizedSigner
}
//检查是否在最近签名者列表中,是否重复签名
for _, recent := range snap.Recents {
if recent == signer {
return nil, errRecentlySigned
}
}
//以区块号为key将此签名者记入最近签名者中.
snap.Recents[number] = signer
//遍历快照的投票数据
for i, vote := range snap.Votes {
//如果此投票是投给当前区块的, 则先清除该签名者的投票
if vote.Signer == signer && vote.Address == header.Coinbase {
// 从缓存计数器中移除该投票
snap.uncast(vote.Address, vote.Authorize)
// 按下标删除第i个投票
snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)
break // only one vote allowed
}
}
// 统计此区块的投票数
var authorize bool
switch {
case bytes.Equal(header.Nonce[:], nonceAuthVote):
authorize = true
case bytes.Equal(header.Nonce[:], nonceDropVote):
authorize = false
default:
return nil, errInvalidVote
}
//将投票数据存入快照投票列表中
if snap.cast(header.Coinbase, authorize) {
snap.Votes = append(snap.Votes, &Vote{
Signer: signer,
Block: number,
Address: header.Coinbase,
Authorize: authorize,
})
}
// 获得当前区块的投票统计,判断投票数是否超过一半投票者,如果通过,更新签名者列表
if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 {
//投票结果授权某人为签名者
if tally.Authorize {
//增加一个签名者
snap.Signers[header.Coinbase] = struct{}{}
} else {
//移除某个签名者
delete(snap.Signers, header.Coinbase)
// 从最近签名者列表中踢出
if limit := uint64(len(snap.Signers)/2 + 1); number >= limit {
delete(snap.Recents, number-limit)
}
// 遍历快照中以前的投票数据
for i := 0; i < len(snap.Votes); i++ {
//如果快照中有此签名者,也删除
if snap.Votes[i].Signer == header.Coinbase {
snap.uncast(snap.Votes[i].Address, snap.Votes[i].Authorize)
snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)
i--
}
}
}
for i := 0; i < len(snap.Votes); i++ {
//丢弃刚刚更改的帐户的任何以前的投票
if snap.Votes[i].Address == header.Coinbase {
snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)
i--
}
}
delete(snap.Tally, header.Coinbase)
}
}
//更新快照的区块号和区块hash
snap.Number += uint64(len(headers))
snap.Hash = headers[len(headers)-1].Hash()
return snap, nil
}
以上代码的逻辑 就是对签名者(signer)的管理,从先recents中删除最老的签名者,并将当前区块的签名者加入到recent缓存中。当投票总数超过一半签名者的时候,就会根据投票情况,重新更新签名名列表(加入或移除)。当此区块被其他节点同步后,就达到了一个统一的共识。投票就真正生效了。
到这里,基本上POA共识就分析完了。以上分析有自己的想法和从其他地方参考的,如果有不同意见,欢迎留言。