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 共識(shí)算法中關(guān)于授權(quán)簽名者列表生成的快照信息炫刷,以及授權(quán)簽名者對(duì)給定區(qū)塊頭列表如何進(jìn)行具體簽名的規(guī)則。
假設(shè)授權(quán)簽名者列表的長(zhǎng)度為 K,當(dāng)前進(jìn)行投票的區(qū)塊編號(hào)為 N苦银,給定區(qū)塊頭中的投票簽名者在有序授權(quán)簽名者列表中的偏移為 P什往,偏移從 0 開始左敌。
快照中包含的主要信息有:
- 創(chuàng)建快照時(shí)的區(qū)塊編號(hào)
- 創(chuàng)建快照時(shí)的區(qū)塊哈希
- 授權(quán)簽名者集合
- 最近 K/2 + 1 個(gè)區(qū)塊中各區(qū)塊編號(hào)對(duì)應(yīng)的簽名者集合
- 按區(qū)塊編號(hào)順序投票的投票列表
- 以及各被投票簽名者的得票計(jì)數(shù)器始苇。
授權(quán)簽名者的具體簽名規(guī)則:
- 待應(yīng)用簽名的區(qū)塊頭列表需要滿足要求:區(qū)塊的編號(hào)是連續(xù)的绰沥。
- K 個(gè)簽名者各自在最近連續(xù)的 K/2 + 1 個(gè)區(qū)塊最多只能投出一票瓦宜。
- 第 P 個(gè)簽名者只能在滿足 N % K == P 條件的區(qū)塊中進(jìn)行投票蔚万。
- 對(duì)于一個(gè)投票,得票數(shù)需要超過 K/2临庇,不包括 K/2反璃。
??? 第 1 個(gè)疑問:大多數(shù)時(shí)候在區(qū)塊頭中并不會(huì)進(jìn)行投票,而區(qū)塊頭列表又需要滿足連續(xù)性這個(gè)條件苔巨,但是看代碼中對(duì)于不包含投票的區(qū)塊頭并沒有直接過濾的操作版扩。
??? 第 2 個(gè)疑問:根據(jù)授權(quán)簽名者的具體簽名規(guī)則,在知道 K 的時(shí)候侄泽,能夠推斷出在區(qū)塊 N 中進(jìn)行投票的簽名者為 P礁芦。這在 PoA 聯(lián)盟鏈中會(huì)不會(huì)導(dǎo)致安全漏洞。
!!! 一個(gè) BUG:在投票解除授權(quán)簽名者時(shí)悼尾,存在一個(gè)問題柿扣。當(dāng)授權(quán)簽名者列表中只剩下一個(gè)簽名者,且該簽名者投票解除自己的授權(quán)時(shí)闺魏,會(huì)觸發(fā)此問題未状,導(dǎo)致授權(quán)簽名者列表為空,引起之后用授權(quán)簽名者列表長(zhǎng)度作分母時(shí)的代碼報(bào)除 0 錯(cuò)誤析桥。
- 真正有問題的代碼司草,具體代碼見方法 Snapshot.apply() 中的 delete(snap.Signers, header.Coinbase)。
- 觸發(fā)問題的代碼泡仗,具體代碼見方法 Snapshot.inturn() 中的 return (number % uint64(len(signers))) == uint64(offset)
定義了多種數(shù)據(jù)結(jié)構(gòu)埋虹,如:
- 數(shù)據(jù)結(jié)構(gòu) Vote 用于描述一次具體的投票信息。
- 數(shù)據(jù)結(jié)構(gòu) Tally 用于描述一個(gè)簡(jiǎn)單的投票計(jì)數(shù)器娩怎。
- 數(shù)據(jù)結(jié)構(gòu) Snapshot 用于描述指定時(shí)間點(diǎn)的授權(quán)投票狀態(tài)搔课。
- 數(shù)據(jù)結(jié)構(gòu) signers 用于描述授權(quán)簽名者列表的封裝器,并實(shí)現(xiàn)了排序接口截亦。數(shù)據(jù)結(jié)構(gòu) singers 支持對(duì)授權(quán)簽名者列表進(jìn)行升序排序爬泥,因此可以計(jì)算出給定簽名者在整個(gè)授權(quán)簽名者列表的有序偏移 P柬讨。
1. type Vote struct
數(shù)據(jù)結(jié)構(gòu) Vote 表示授權(quán)簽名者為了修改授權(quán)列表而進(jìn)行的一次投票。
- Signer common.Address: 投票的授權(quán)簽名者
- Block uint6: 投票的區(qū)塊編號(hào)(投票過期)
- Address common.Address: 被投票的帳戶袍啡,以更改其授權(quán)
- Authorize bool: 表示是否授權(quán)或取消對(duì)已投票帳戶的授權(quán)
2. type Tally struct
數(shù)據(jù)結(jié)構(gòu) Tally 是一個(gè)簡(jiǎn)單的投票計(jì)數(shù)器踩官,以保持當(dāng)前的投票得分。投票反對(duì)該提案不計(jì)算在內(nèi)葬馋,因?yàn)樗韧诓煌镀薄?/p>
- Authorize bool: 投票是關(guān)于授權(quán)還是踢某人
- Votes int: 到目前為止希望通過提案的投票數(shù)
3. type Snapshot struct
數(shù)據(jù)結(jié)構(gòu) Snapshot 表示指定時(shí)間點(diǎn)的授權(quán)投票狀態(tài)卖鲤。
config *params.CliqueConfig: 共識(shí)引擎參數(shù)以微調(diào)行為
sigcache *lru.ARCCache: 緩存最近的塊簽名以加速函數(shù) ecrecover()
Number uint64: 創(chuàng)建快照的區(qū)塊編號(hào)
Hash common.Hash: 創(chuàng)建快照的區(qū)塊哈希
Signers map[common.Address]struct{}: 這一刻的授權(quán)簽名者集合
Recents map[uint64]common.Address: 一組最近的簽名者集,用于防止 spam 攻擊畴嘶。分別記錄最近 k/2 + 1 次的區(qū)塊編號(hào)對(duì)應(yīng)的簽名者蛋逾。
Votes []*Vote: 按區(qū)塊編號(hào)順序投票的投票列表
Tally map[common.Address]Tally: 目前的投票計(jì)數(shù)器,以避免重新計(jì)算
通過構(gòu)造函數(shù)
newSnapshot() 使用指定的啟動(dòng)參數(shù)創(chuàng)建新快照窗悯。這種方法不會(huì)初始化最近的簽名者集区匣,所以只能用于創(chuàng)世塊。通過函數(shù) loadSnapshot() 從數(shù)據(jù)庫加載已經(jīng)存在的快照蒋院。
通過方法 store() 將快照插入數(shù)據(jù)庫亏钩。
通過方法 copy() 會(huì)創(chuàng)建快照的深層副本,但不會(huì)創(chuàng)建單獨(dú)的投票欺旧。
通過方法 validVote() 返回在給定的快照上下文中投出的特定投票是否有意義(例如姑丑,不要嘗試添加已經(jīng)授權(quán)的簽名者)。
通過方法 cast() 往投票計(jì)數(shù)器 Snapshot.tally 中增加新的投票辞友。
通過方法 uncast() 從投票計(jì)數(shù)器 Snapshot.tally 中移除之前的一次投票栅哀。
通過方法 apply() 通過將給定的區(qū)塊頭列表應(yīng)用于原始的快照來生成新的授權(quán)快照。
通過方法 signers() 按升序返回授權(quán)簽名者列表称龙。
通過方法 inturn() 返回簽名者在給定區(qū)塊高度是否是 in-turn 的留拾。
4. type signers []common.Address
封裝器 signers 實(shí)現(xiàn)了排序接口,以允許排序地址列表鲫尊。
- 通過方法 Len() 返回列表中元素的個(gè)數(shù)痴柔。
- 通過方法 Less() 比較列表中第 i 個(gè)元素是否比第 j 個(gè)元素的小,如果是返回 true疫向。
- 通過方法 Swap() 交換列表中第 i 個(gè)元素和第 j 個(gè)元素咳蔚。
Appendix B. 詳細(xì)批注
1. type Vote struct
數(shù)據(jù)結(jié)構(gòu) Vote 表示授權(quán)簽名者為了修改授權(quán)列表而進(jìn)行的一次投票。
- Signer common.Address: 投票的授權(quán)簽名者
- Block uint6: 投票的區(qū)塊編號(hào)(投票過期)
- Address common.Address: 被投票的帳戶搔驼,以更改其授權(quán)
- Authorize bool: 表示是否授權(quán)或取消對(duì)已投票帳戶的授權(quán)
2. type Tally struct
數(shù)據(jù)結(jié)構(gòu) Tally 是一個(gè)簡(jiǎn)單的投票計(jì)數(shù)器谈火,以保持當(dāng)前的投票得分。投票反對(duì)該提案不計(jì)算在內(nèi)匙奴,因?yàn)樗韧诓煌镀薄?/p>
- Authorize bool: 投票是關(guān)于授權(quán)還是踢某人
- Votes int: 到目前為止希望通過提案的投票數(shù)
3. type Snapshot struct
數(shù)據(jù)結(jié)構(gòu) Snapshot 表示指定時(shí)間點(diǎn)的授權(quán)投票狀態(tài)堆巧。
config *params.CliqueConfig: 共識(shí)引擎參數(shù)以微調(diào)行為
sigcache *lru.ARCCache: 緩存最近的塊簽名以加速函數(shù) ecrecover()
Number uint64: 創(chuàng)建快照的區(qū)塊編號(hào)
Hash common.Hash: 創(chuàng)建快照的區(qū)塊哈希
Signers map[common.Address]struct{}: 這一刻的授權(quán)簽名者集合
Recents map[uint64]common.Address: 一組最近的簽名者集妄荔,用于防止 spam 攻擊泼菌。分別記錄最近 k/2 + 1 次的區(qū)塊編號(hào)對(duì)應(yīng)的簽名者谍肤。
Votes []*Vote: 按區(qū)塊編號(hào)順序投票的投票列表
Tally map[common.Address]Tally: 目前的投票計(jì)數(shù)器,以避免重新計(jì)算
1. func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot
構(gòu)造函數(shù)
newSnapshot() 使用指定的啟動(dòng)參數(shù)創(chuàng)建新快照哗伯。這種方法不會(huì)初始化最近的簽名者集荒揣,所以只能用于創(chuàng)世塊。
2. func loadSnapshot(config *params.CliqueConfig, sigcache lru.ARCCache, db ethdb.Database, hash common.Hash) (Snapshot, error)
函數(shù) loadSnapshot() 從數(shù)據(jù)庫加載已經(jīng)存在的快照焊刹。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 調(diào)用方法 db.Get() 從數(shù)據(jù)庫加載 JSON 數(shù)據(jù)流
- 調(diào)用方法 json.Unmarshal() 從 JSON 數(shù)據(jù)流中解碼出對(duì)象 clique.Snapshot
- 與方法 Snapshot.store() 的功能相反系任。
3. func (s *Snapshot) store(db ethdb.Database) error
方法 store() 將快照插入數(shù)據(jù)庫。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 調(diào)用方法 json.Marshal() 將對(duì)象 clique.Snapshot 編碼成 JSON 數(shù)據(jù)流虐块。
- 調(diào)用方法 db.Put() 將 JSON 數(shù)據(jù)流插入數(shù)據(jù)庫俩滥。
- 與函數(shù) loadSnapshot() 的功能相反。
4. func (s *Snapshot) copy() *Snapshot
方法 copy() 會(huì)創(chuàng)建快照的深層副本贺奠,但不會(huì)創(chuàng)建單獨(dú)的投票霜旧。
5. func (s *Snapshot) validVote(address common.Address, authorize bool) bool
方法 validVote() 返回在給定的快照上下文中投出的特定投票是否有意義(例如,不要嘗試添加已經(jīng)授權(quán)的簽名者)儡率。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 當(dāng) authorize 為 true 時(shí)挂据,則 address 應(yīng)該不存在于 Snapshot.Signers;且當(dāng) authorize 為 false 時(shí)儿普,則 address 應(yīng)該存在于 Snapshot.Signers崎逃。這兩種情況都是有效的投票,否則為無效的投票眉孩。
- 也就是當(dāng)投出剔除授權(quán)簽名者个绍,該簽名者應(yīng)該存在于授權(quán)簽名者列表。當(dāng)投出新增授權(quán)簽名者時(shí)勺像,該簽名者應(yīng)該不存在于授權(quán)簽名者列表障贸。
- 判定算法有點(diǎn)繞
- 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() 往投票計(jì)數(shù)器 Snapshot.tally 中增加新的投票。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 調(diào)用方法 Snapshot.validVote() 驗(yàn)證投票的有效性吟宦。
- 需要考慮對(duì)指定地址的投票是全新的篮洁,還是只是增加得票數(shù)即可。
7. func (s *Snapshot) uncast(address common.Address, authorize bool) bool
方法 uncast() 從投票計(jì)數(shù)器 Snapshot.tally 中移除之前的一次投票殃姓。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 需要確保此次投票和之前的投票一致袁波。
- 返還投票時(shí)需要考慮返還后指定地址的得票數(shù)是否為 0.
8. func (s Snapshot) apply(headers []types.Header) (*Snapshot, error)
方法 apply() 通過將給定的區(qū)塊頭列表應(yīng)用于原始的快照來生成新的授權(quán)快照。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
如果 len(headers) == 0蜗侈,則直接返回篷牌。允許傳入空 headers 以獲得更清晰的代碼。
檢查區(qū)塊頭列表的完整性踏幻。即區(qū)塊頭列表中的區(qū)塊頭必須是連續(xù)的枷颊,且是根據(jù)區(qū)塊編號(hào)升序排序的。
除了參數(shù) headers 必須是連續(xù)且升序之外,第一個(gè)區(qū)塊頭的區(qū)塊編號(hào)也必須是當(dāng)前快照所處的區(qū)塊編號(hào)的下一個(gè)區(qū)塊夭苗, 即 headers[0].Number.Uint64() != s.Number+1信卡。
-
通過方法 Snapshot.copy() 創(chuàng)建要返回的新的快照,并在此新快照上依次應(yīng)用參數(shù)區(qū)塊頭列表中的區(qū)塊頭 header题造。
- 檢查當(dāng)前區(qū)塊頭是否為檢查點(diǎn)區(qū)塊傍菇,如果是則清除所有的投票信息。
- 從最近的簽名者列表(snap.Recents)中刪除最舊的簽名者以允許它再次簽名界赔。
- 具體規(guī)則為:Snapshot.Recents 最多只會(huì)記錄 K/2 + 1 個(gè)最近的簽名者簽名記錄丢习,也就是簽名者在最近 K/2 + 1 個(gè)區(qū)塊中只能簽名一次。具體的計(jì)算規(guī)則是:假設(shè)當(dāng)前區(qū)塊的編號(hào)為 N淮悼,會(huì)刪除 Snapshot.Recents 中第 N - (K/2 + 1) 個(gè)元素咐低,之后 Snapshot.Recents 中的第 1 個(gè)元素為 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 個(gè)區(qū)塊的編號(hào)為 0瞧挤,由 0 到 limit - 1 正好包含 (limit - 1) - 0 + 1 = (K/2 + 1 - 1) - 0 + 1 = k/2 + 1 個(gè)區(qū)塊锡宋。
- 調(diào)用函數(shù) ecrecover() 從區(qū)塊頭中恢復(fù)出簽名者 signer。
- 檢查簽名者 signer 是否存在于授權(quán)簽名者列表(snap.Signers)特恬,不存在返回 clique.errUnauthorized执俩。
- 檢查簽名者 singer 是否在最近 K/2 + 1 個(gè)區(qū)塊中已經(jīng)簽名過,即是否已經(jīng)存在于最近的簽名者列表(snap.Recents)中癌刽。已經(jīng)簽名過則返回 clique.errUnauthorized役首。
- 更新最近的簽名者列表(snap.Recents),snap.Recents[number] = signer显拜。
- 對(duì)于授權(quán)的區(qū)塊頭衡奥,丟棄簽名者以前的任何投票 vote。
- 通過方法 snap.uncast(vote.Address, vote.Authorize) 從投票計(jì)數(shù)器(Snapshot.Tally)移除該投票远荠。
- 通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 從 Snapshot.Votes 中移除該投票矮固。
- 從區(qū)塊頭 types.Header.Nonce 中計(jì)算是授權(quán)(nonceAuthVote)還是解除授權(quán)(nonceDropVote)投票,無效 Nonce 值則返回 clique.errInvalidVote譬淳。
- 通過方法 Snapshot.cast() 更新投票計(jì)數(shù)器(Snapshot.Tally)档址。如果成功,則往 snap.Votes 添加新的投票邻梆。
- 如果區(qū)塊頭 header 中的投票被通過守伸,則更新授權(quán)簽名者列表。一次投票被通過的條件是浦妄,得票數(shù)大于等于 K/2 + 1尼摹,其中 K 為授權(quán)簽名者個(gè)數(shù)见芹。
- 如果投票是授權(quán)簽名者,則 snap.Signers[header.Coinbase] = struct{}{}
- 如果投票是解除授權(quán)簽名者蠢涝,則:
- delete(snap.Signers, header.Coinbase)辆童。
- 簽名者列表縮小,刪除任何剩余的最近的簽名者列表(snap.Recents)緩存惠赫,這個(gè)操作是為了維持與 K/2 + 1 相關(guān)的這個(gè)規(guī)則。
- 丟棄授權(quán)簽名者以前的任何投票故黑,即調(diào)用 snap.uncast 更新 snap.Votes儿咱,和通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 更新 snap.Votes。注意具體實(shí)現(xiàn)時(shí)的 i-- 操作场晶,這是由于 snap.Votes 的長(zhǎng)度已經(jīng)縮小了 1.
- 丟棄剛剛更改的帳戶(header.coinbase)的所有先前投票
- 通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 修改 snap.Votes混埠。同時(shí)注意與上述相似的 i-- 操作。
- 通過 delete(snap.Tally, header.Coinbase) 直接從 snap.Tally 刪除 header.Coinbase 的整個(gè)計(jì)數(shù)器诗轻。
更新當(dāng)前快照創(chuàng)建時(shí)的區(qū)塊編號(hào)钳宪。即將原快照創(chuàng)建時(shí)的區(qū)塊編號(hào)加上參數(shù) headers 中 types.Header 的個(gè)數(shù),具體實(shí)現(xiàn)為 snap.Number += uint64(len(headers))
更新當(dāng)前快照創(chuàng)建時(shí)的區(qū)塊哈希扳炬。即參數(shù) headers 中最后一個(gè) types.Header 的哈希吏颖。snap.Hash = headers[len(headers)-1].Hash()
9. func (s *Snapshot) signers() []common.Address
方法 signers() 按升序返回授權(quán)簽名者列表。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 通過方法 sort.Sort() 按升序排序授權(quán)簽名者列表恨樟。
10. func (s *Snapshot) inturn(number uint64, signer common.Address) bool
方法 inturn() 返回簽名者在給定區(qū)塊高度是否是 in-turn 的半醉。
這里可以理解 in-turn 為授權(quán)簽名者列表對(duì)于給定區(qū)塊判定采用哪個(gè)簽名者的規(guī)則。
假設(shè)區(qū)塊編號(hào)為 N劝术,也就是區(qū)塊的高度為 N缩多。授權(quán)簽名者列表的長(zhǎng)度為 K。簽名者在授權(quán)簽名者列表中的順序?yàn)?P养晋,從 0 開始偏移衬吆。則如果 (N % K) == P 就返回 true,表示 in-turn绳泉。
主要的實(shí)現(xiàn)細(xì)節(jié)如下:
- 即實(shí)現(xiàn)上面的規(guī)則逊抡。
4. type signers []common.Address
封裝器 signers 實(shí)現(xiàn)了排序接口,以允許排序地址列表零酪。
(1) func (s signers) Len() int
方法 Len() 返回列表中元素的個(gè)數(shù)秦忿。
(2) func (s signers) Less(i, j int) bool
方法 Less() 比較列表中第 i 個(gè)元素是否比第 j 個(gè)元素的小,如果是返回 true蛾娶。
(3) func (s signers) Swap(i, j int)
方法 Swap() 交換列表中第 i 個(gè)元素和第 j 個(gè)元素灯谣。
Reference
Contributor
- Windstamp, https://github.com/windstamp