Go-ethereum 源码解析之 consensus/clique/snapshot.go
package clique
import (
"bytes"
"encoding/json"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/params"
lru "github.com/hashicorp/golang-lru"
)
// 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
}
// 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 implements the sort interface to allow sorting a list of addresses
type signers []common.Address
func (s signers) Len() int { return len(s) }
func (s signers) Less(i, j int) bool { return bytes.Compare(s[i][:], s[j][:]) < 0 }
func (s signers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// newSnapshot creates a new snapshot with the specified startup parameters. This
// method does not initialize the set of recent signers, so only ever use if for
// the genesis block.
func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot {
snap := &Snapshot{
config: config,
sigcache: sigcache,
Number: number,
Hash: hash,
Signers: make(map[common.Address]struct{}),
Recents: make(map[uint64]common.Address),
Tally: make(map[common.Address]Tally),
}
for _, signer := range signers {
snap.Signers[signer] = struct{}{}
}
return snap
}
Appendix A. 总体批注
文件 clique/snapshot.go 主要是用于描述 Clique 共识算法中关于授权签名者列表生成的快照信息,以及授权签名者对给定区块头列表如何进行具体签名的规则。
假设授权签名者列表的长度为 K,当前进行投票的区块编号为 N,给定区块头中的投票签名者在有序授权签名者列表中的偏移为 P,偏移从 0 开始。
快照中包含的主要信息有:
- 创建快照时的区块编号
- 创建快照时的区块哈希
- 授权签名者集合
- 最近 K/2 + 1 个区块中各区块编号对应的签名者集合
- 按区块编号顺序投票的投票列表
- 以及各被投票签名者的得票计数器。
授权签名者的具体签名规则:
- 待应用签名的区块头列表需要满足要求:区块的编号是连续的。
- K 个签名者各自在最近连续的 K/2 + 1 个区块最多只能投出一票。
- 第 P 个签名者只能在满足 N % K == P 条件的区块中进行投票。
- 对于一个投票,得票数需要超过 K/2,不包括 K/2。
??? 第 1 个疑问:大多数时候在区块头中并不会进行投票,而区块头列表又需要满足连续性这个条件,但是看代码中对于不包含投票的区块头并没有直接过滤的操作。
??? 第 2 个疑问:根据授权签名者的具体签名规则,在知道 K 的时候,能够推断出在区块 N 中进行投票的签名者为 P。这在 PoA 联盟链中会不会导致安全漏洞。
!!! 一个 BUG:在投票解除授权签名者时,存在一个问题。当授权签名者列表中只剩下一个签名者,且该签名者投票解除自己的授权时,会触发此问题,导致授权签名者列表为空,引起之后用授权签名者列表长度作分母时的代码报除 0 错误。
- 真正有问题的代码,具体代码见方法 Snapshot.apply() 中的 delete(snap.Signers, header.Coinbase)。
- 触发问题的代码,具体代码见方法 Snapshot.inturn() 中的 return (number % uint64(len(signers))) == uint64(offset)
定义了多种数据结构,如:
- 数据结构 Vote 用于描述一次具体的投票信息。
- 数据结构 Tally 用于描述一个简单的投票计数器。
- 数据结构 Snapshot 用于描述指定时间点的授权投票状态。
- 数据结构 signers 用于描述授权签名者列表的封装器,并实现了排序接口。数据结构 singers 支持对授权签名者列表进行升序排序,因此可以计算出给定签名者在整个授权签名者列表的有序偏移 P。
1. type Vote struct
数据结构 Vote 表示授权签名者为了修改授权列表而进行的一次投票。
- Signer common.Address: 投票的授权签名者
- Block uint6: 投票的区块编号(投票过期)
- Address common.Address: 被投票的帐户,以更改其授权
- Authorize bool: 表示是否授权或取消对已投票帐户的授权
2. type Tally struct
数据结构 Tally 是一个简单的投票计数器,以保持当前的投票得分。投票反对该提案不计算在内,因为它等同于不投票。
- Authorize bool: 投票是关于授权还是踢某人
- Votes int: 到目前为止希望通过提案的投票数
3. type Snapshot struct
数据结构 Snapshot 表示指定时间点的授权投票状态。
config *params.CliqueConfig: 共识引擎参数以微调行为
sigcache *lru.ARCCache: 缓存最近的块签名以加速函数 ecrecover()
Number uint64: 创建快照的区块编号
Hash common.Hash: 创建快照的区块哈希
Signers map[common.Address]struct{}: 这一刻的授权签名者集合
Recents map[uint64]common.Address: 一组最近的签名者集,用于防止 spam 攻击。分别记录最近 k/2 + 1 次的区块编号对应的签名者。
Votes []*Vote: 按区块编号顺序投票的投票列表
Tally map[common.Address]Tally: 目前的投票计数器,以避免重新计算
通过构造函数
newSnapshot() 使用指定的启动参数创建新快照。这种方法不会初始化最近的签名者集,所以只能用于创世块。通过函数 loadSnapshot() 从数据库加载已经存在的快照。
通过方法 store() 将快照插入数据库。
通过方法 copy() 会创建快照的深层副本,但不会创建单独的投票。
通过方法 validVote() 返回在给定的快照上下文中投出的特定投票是否有意义(例如,不要尝试添加已经授权的签名者)。
通过方法 cast() 往投票计数器 Snapshot.tally 中增加新的投票。
通过方法 uncast() 从投票计数器 Snapshot.tally 中移除之前的一次投票。
通过方法 apply() 通过将给定的区块头列表应用于原始的快照来生成新的授权快照。
通过方法 signers() 按升序返回授权签名者列表。
通过方法 inturn() 返回签名者在给定区块高度是否是 in-turn 的。
4. type signers []common.Address
封装器 signers 实现了排序接口,以允许排序地址列表。
- 通过方法 Len() 返回列表中元素的个数。
- 通过方法 Less() 比较列表中第 i 个元素是否比第 j 个元素的小,如果是返回 true。
- 通过方法 Swap() 交换列表中第 i 个元素和第 j 个元素。
Appendix B. 详细批注
1. type Vote struct
数据结构 Vote 表示授权签名者为了修改授权列表而进行的一次投票。
- Signer common.Address: 投票的授权签名者
- Block uint6: 投票的区块编号(投票过期)
- Address common.Address: 被投票的帐户,以更改其授权
- Authorize bool: 表示是否授权或取消对已投票帐户的授权
2. type Tally struct
数据结构 Tally 是一个简单的投票计数器,以保持当前的投票得分。投票反对该提案不计算在内,因为它等同于不投票。
- Authorize bool: 投票是关于授权还是踢某人
- Votes int: 到目前为止希望通过提案的投票数
3. type Snapshot struct
数据结构 Snapshot 表示指定时间点的授权投票状态。
config *params.CliqueConfig: 共识引擎参数以微调行为
sigcache *lru.ARCCache: 缓存最近的块签名以加速函数 ecrecover()
Number uint64: 创建快照的区块编号
Hash common.Hash: 创建快照的区块哈希
Signers map[common.Address]struct{}: 这一刻的授权签名者集合
Recents map[uint64]common.Address: 一组最近的签名者集,用于防止 spam 攻击。分别记录最近 k/2 + 1 次的区块编号对应的签名者。
Votes []*Vote: 按区块编号顺序投票的投票列表
Tally map[common.Address]Tally: 目前的投票计数器,以避免重新计算
1. func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot
构造函数
newSnapshot() 使用指定的启动参数创建新快照。这种方法不会初始化最近的签名者集,所以只能用于创世块。
2. func loadSnapshot(config *params.CliqueConfig, sigcache lru.ARCCache, db ethdb.Database, hash common.Hash) (Snapshot, error)
函数 loadSnapshot() 从数据库加载已经存在的快照。
主要的实现细节如下:
- 调用方法 db.Get() 从数据库加载 JSON 数据流
- 调用方法 json.Unmarshal() 从 JSON 数据流中解码出对象 clique.Snapshot
- 与方法 Snapshot.store() 的功能相反。
3. func (s *Snapshot) store(db ethdb.Database) error
方法 store() 将快照插入数据库。
主要的实现细节如下:
- 调用方法 json.Marshal() 将对象 clique.Snapshot 编码成 JSON 数据流。
- 调用方法 db.Put() 将 JSON 数据流插入数据库。
- 与函数 loadSnapshot() 的功能相反。
4. func (s *Snapshot) copy() *Snapshot
方法 copy() 会创建快照的深层副本,但不会创建单独的投票。
5. func (s *Snapshot) validVote(address common.Address, authorize bool) bool
方法 validVote() 返回在给定的快照上下文中投出的特定投票是否有意义(例如,不要尝试添加已经授权的签名者)。
主要的实现细节如下:
- 当 authorize 为 true 时,则 address 应该不存在于 Snapshot.Signers;且当 authorize 为 false 时,则 address 应该存在于 Snapshot.Signers。这两种情况都是有效的投票,否则为无效的投票。
- 也就是当投出剔除授权签名者,该签名者应该存在于授权签名者列表。当投出新增授权签名者时,该签名者应该不存在于授权签名者列表。
- 判定算法有点绕
- return (signer && !authorize) || (!signer && authorize)
func (s *Snapshot) validVote(address common.Address, authorize bool) bool {
_, signer := s.Signers[address]
return (signer && !authorize) || (!signer && authorize)
}
6. func (s *Snapshot) cast(address common.Address, authorize bool) bool
方法 cast() 往投票计数器 Snapshot.tally 中增加新的投票。
主要的实现细节如下:
- 调用方法 Snapshot.validVote() 验证投票的有效性。
- 需要考虑对指定地址的投票是全新的,还是只是增加得票数即可。
7. func (s *Snapshot) uncast(address common.Address, authorize bool) bool
方法 uncast() 从投票计数器 Snapshot.tally 中移除之前的一次投票。
主要的实现细节如下:
- 需要确保此次投票和之前的投票一致。
- 返还投票时需要考虑返还后指定地址的得票数是否为 0.
8. func (s Snapshot) apply(headers []types.Header) (*Snapshot, error)
方法 apply() 通过将给定的区块头列表应用于原始的快照来生成新的授权快照。
主要的实现细节如下:
如果 len(headers) == 0,则直接返回。允许传入空 headers 以获得更清晰的代码。
检查区块头列表的完整性。即区块头列表中的区块头必须是连续的,且是根据区块编号升序排序的。
除了参数 headers 必须是连续且升序之外,第一个区块头的区块编号也必须是当前快照所处的区块编号的下一个区块, 即 headers[0].Number.Uint64() != s.Number+1。
-
通过方法 Snapshot.copy() 创建要返回的新的快照,并在此新快照上依次应用参数区块头列表中的区块头 header。
- 检查当前区块头是否为检查点区块,如果是则清除所有的投票信息。
- 从最近的签名者列表(snap.Recents)中删除最旧的签名者以允许它再次签名。
- 具体规则为:Snapshot.Recents 最多只会记录 K/2 + 1 个最近的签名者签名记录,也就是签名者在最近 K/2 + 1 个区块中只能签名一次。具体的计算规则是:假设当前区块的编号为 N,会删除 Snapshot.Recents 中第 N - (K/2 + 1) 个元素,之后 Snapshot.Recents 中的第 1 个元素为 N - (K/2 + 1) + 1,在 N - (K/2 + 1) + 1 和 N 之间存在 (N - (N - (K/2 + 1) + 1) + 1) = K/2 + 1。之所以是 number >= limit,这里 limit = K/2 + 1,是由于第 1 个区块的编号为 0,由 0 到 limit - 1 正好包含 (limit - 1) - 0 + 1 = (K/2 + 1 - 1) - 0 + 1 = k/2 + 1 个区块。
- 调用函数 ecrecover() 从区块头中恢复出签名者 signer。
- 检查签名者 signer 是否存在于授权签名者列表(snap.Signers),不存在返回 clique.errUnauthorized。
- 检查签名者 singer 是否在最近 K/2 + 1 个区块中已经签名过,即是否已经存在于最近的签名者列表(snap.Recents)中。已经签名过则返回 clique.errUnauthorized。
- 更新最近的签名者列表(snap.Recents),snap.Recents[number] = signer。
- 对于授权的区块头,丢弃签名者以前的任何投票 vote。
- 通过方法 snap.uncast(vote.Address, vote.Authorize) 从投票计数器(Snapshot.Tally)移除该投票。
- 通过 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 从 Snapshot.Votes 中移除该投票。
- 从区块头 types.Header.Nonce 中计算是授权(nonceAuthVote)还是解除授权(nonceDropVote)投票,无效 Nonce 值则返回 clique.errInvalidVote。
- 通过方法 Snapshot.cast() 更新投票计数器(Snapshot.Tally)。如果成功,则往 snap.Votes 添加新的投票。
- 如果区块头 header 中的投票被通过,则更新授权签名者列表。一次投票被通过的条件是,得票数大于等于 K/2 + 1,其中 K 为授权签名者个数。
- 如果投票是授权签名者,则 snap.Signers[header.Coinbase] = struct{}{}
- 如果投票是解除授权签名者,则:
- delete(snap.Signers, header.Coinbase)。
- 签名者列表缩小,删除任何剩余的最近的签名者列表(snap.Recents)缓存,这个操作是为了维持与 K/2 + 1 相关的这个规则。
- 丢弃授权签名者以前的任何投票,即调用 snap.uncast 更新 snap.Votes,和通过 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 更新 snap.Votes。注意具体实现时的 i-- 操作,这是由于 snap.Votes 的长度已经缩小了 1.
- 丢弃刚刚更改的帐户(header.coinbase)的所有先前投票
- 通过 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 修改 snap.Votes。同时注意与上述相似的 i-- 操作。
- 通过 delete(snap.Tally, header.Coinbase) 直接从 snap.Tally 删除 header.Coinbase 的整个计数器。
更新当前快照创建时的区块编号。即将原快照创建时的区块编号加上参数 headers 中 types.Header 的个数,具体实现为 snap.Number += uint64(len(headers))
更新当前快照创建时的区块哈希。即参数 headers 中最后一个 types.Header 的哈希。snap.Hash = headers[len(headers)-1].Hash()
9. func (s *Snapshot) signers() []common.Address
方法 signers() 按升序返回授权签名者列表。
主要的实现细节如下:
- 通过方法 sort.Sort() 按升序排序授权签名者列表。
10. func (s *Snapshot) inturn(number uint64, signer common.Address) bool
方法 inturn() 返回签名者在给定区块高度是否是 in-turn 的。
这里可以理解 in-turn 为授权签名者列表对于给定区块判定采用哪个签名者的规则。
假设区块编号为 N,也就是区块的高度为 N。授权签名者列表的长度为 K。签名者在授权签名者列表中的顺序为 P,从 0 开始偏移。则如果 (N % K) == P 就返回 true,表示 in-turn。
主要的实现细节如下:
- 即实现上面的规则。
4. type signers []common.Address
封装器 signers 实现了排序接口,以允许排序地址列表。
(1) func (s signers) Len() int
方法 Len() 返回列表中元素的个数。
(2) func (s signers) Less(i, j int) bool
方法 Less() 比较列表中第 i 个元素是否比第 j 个元素的小,如果是返回 true。
(3) func (s signers) Swap(i, j int)
方法 Swap() 交换列表中第 i 个元素和第 j 个元素。
Reference
- https://github.com/ethereum/go-ethereum/blob/master/consensus/clique/snapshot.go
Contributor
- Windstamp, https://github.com/windstamp