Go-ethereum 源码解析之 consensus/clique/snapshot.go

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

  1. https://github.com/ethereum/go-ethereum/blob/master/consensus/clique/snapshot.go

Contributor

  1. Windstamp, https://github.com/windstamp
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容