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 共識算法中關于授權簽名者列表生成的快照信息,以及授權簽名者對給定區(qū)塊頭列表如何進行具體簽名的規(guī)則。
假設授權簽名者列表的長度為 K,當前進行投票的區(qū)塊編號為 N,給定區(qū)塊頭中的投票簽名者在有序授權簽名者列表中的偏移為 P,偏移從 0 開始。
快照中包含的主要信息有:
- 創(chuàng)建快照時的區(qū)塊編號
- 創(chuàng)建快照時的區(qū)塊哈希
- 授權簽名者集合
- 最近 K/2 + 1 個區(qū)塊中各區(qū)塊編號對應的簽名者集合
- 按區(qū)塊編號順序投票的投票列表
- 以及各被投票簽名者的得票計數(shù)器。
授權簽名者的具體簽名規(guī)則:
- 待應用簽名的區(qū)塊頭列表需要滿足要求:區(qū)塊的編號是連續(xù)的。
- K 個簽名者各自在最近連續(xù)的 K/2 + 1 個區(qū)塊最多只能投出一票。
- 第 P 個簽名者只能在滿足 N % K == P 條件的區(qū)塊中進行投票。
- 對于一個投票,得票數(shù)需要超過 K/2,不包括 K/2。
??? 第 1 個疑問:大多數(shù)時候在區(qū)塊頭中并不會進行投票,而區(qū)塊頭列表又需要滿足連續(xù)性這個條件,但是看代碼中對于不包含投票的區(qū)塊頭并沒有直接過濾的操作。
??? 第 2 個疑問:根據(jù)授權簽名者的具體簽名規(guī)則,在知道 K 的時候,能夠推斷出在區(qū)塊 N 中進行投票的簽名者為 P。這在 PoA 聯(lián)盟鏈中會不會導致安全漏洞。
!!! 一個 BUG:在投票解除授權簽名者時,存在一個問題。當授權簽名者列表中只剩下一個簽名者,且該簽名者投票解除自己的授權時,會觸發(fā)此問題,導致授權簽名者列表為空,引起之后用授權簽名者列表長度作分母時的代碼報除 0 錯誤。
- 真正有問題的代碼,具體代碼見方法 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 用于描述一個簡單的投票計數(shù)器。
- 數(shù)據(jù)結(jié)構(gòu) Snapshot 用于描述指定時間點的授權投票狀態(tài)。
- 數(shù)據(jù)結(jié)構(gòu) signers 用于描述授權簽名者列表的封裝器,并實現(xiàn)了排序接口。數(shù)據(jù)結(jié)構(gòu) singers 支持對授權簽名者列表進行升序排序,因此可以計算出給定簽名者在整個授權簽名者列表的有序偏移 P。
1. type Vote struct
數(shù)據(jù)結(jié)構(gòu) Vote 表示授權簽名者為了修改授權列表而進行的一次投票。
- Signer common.Address: 投票的授權簽名者
- Block uint6: 投票的區(qū)塊編號(投票過期)
- Address common.Address: 被投票的帳戶,以更改其授權
- Authorize bool: 表示是否授權或取消對已投票帳戶的授權
2. type Tally struct
數(shù)據(jù)結(jié)構(gòu) Tally 是一個簡單的投票計數(shù)器,以保持當前的投票得分。投票反對該提案不計算在內(nèi),因為它等同于不投票。
- Authorize bool: 投票是關于授權還是踢某人
- Votes int: 到目前為止希望通過提案的投票數(shù)
3. type Snapshot struct
數(shù)據(jù)結(jié)構(gòu) Snapshot 表示指定時間點的授權投票狀態(tài)。
config *params.CliqueConfig: 共識引擎參數(shù)以微調(diào)行為
sigcache *lru.ARCCache: 緩存最近的塊簽名以加速函數(shù) ecrecover()
Number uint64: 創(chuàng)建快照的區(qū)塊編號
Hash common.Hash: 創(chuàng)建快照的區(qū)塊哈希
Signers map[common.Address]struct{}: 這一刻的授權簽名者集合
Recents map[uint64]common.Address: 一組最近的簽名者集,用于防止 spam 攻擊。分別記錄最近 k/2 + 1 次的區(qū)塊編號對應的簽名者。
Votes []*Vote: 按區(qū)塊編號順序投票的投票列表
Tally map[common.Address]Tally: 目前的投票計數(shù)器,以避免重新計算
通過構(gòu)造函數(shù)
newSnapshot() 使用指定的啟動參數(shù)創(chuàng)建新快照。這種方法不會初始化最近的簽名者集,所以只能用于創(chuàng)世塊。通過函數(shù) loadSnapshot() 從數(shù)據(jù)庫加載已經(jīng)存在的快照。
通過方法 store() 將快照插入數(shù)據(jù)庫。
通過方法 copy() 會創(chuàng)建快照的深層副本,但不會創(chuàng)建單獨的投票。
通過方法 validVote() 返回在給定的快照上下文中投出的特定投票是否有意義(例如,不要嘗試添加已經(jīng)授權的簽名者)。
通過方法 cast() 往投票計數(shù)器 Snapshot.tally 中增加新的投票。
通過方法 uncast() 從投票計數(shù)器 Snapshot.tally 中移除之前的一次投票。
通過方法 apply() 通過將給定的區(qū)塊頭列表應用于原始的快照來生成新的授權快照。
通過方法 signers() 按升序返回授權簽名者列表。
通過方法 inturn() 返回簽名者在給定區(qū)塊高度是否是 in-turn 的。
4. type signers []common.Address
封裝器 signers 實現(xiàn)了排序接口,以允許排序地址列表。
- 通過方法 Len() 返回列表中元素的個數(shù)。
- 通過方法 Less() 比較列表中第 i 個元素是否比第 j 個元素的小,如果是返回 true。
- 通過方法 Swap() 交換列表中第 i 個元素和第 j 個元素。
Appendix B. 詳細批注
1. type Vote struct
數(shù)據(jù)結(jié)構(gòu) Vote 表示授權簽名者為了修改授權列表而進行的一次投票。
- Signer common.Address: 投票的授權簽名者
- Block uint6: 投票的區(qū)塊編號(投票過期)
- Address common.Address: 被投票的帳戶,以更改其授權
- Authorize bool: 表示是否授權或取消對已投票帳戶的授權
2. type Tally struct
數(shù)據(jù)結(jié)構(gòu) Tally 是一個簡單的投票計數(shù)器,以保持當前的投票得分。投票反對該提案不計算在內(nèi),因為它等同于不投票。
- Authorize bool: 投票是關于授權還是踢某人
- Votes int: 到目前為止希望通過提案的投票數(shù)
3. type Snapshot struct
數(shù)據(jù)結(jié)構(gòu) Snapshot 表示指定時間點的授權投票狀態(tài)。
config *params.CliqueConfig: 共識引擎參數(shù)以微調(diào)行為
sigcache *lru.ARCCache: 緩存最近的塊簽名以加速函數(shù) ecrecover()
Number uint64: 創(chuàng)建快照的區(qū)塊編號
Hash common.Hash: 創(chuàng)建快照的區(qū)塊哈希
Signers map[common.Address]struct{}: 這一刻的授權簽名者集合
Recents map[uint64]common.Address: 一組最近的簽名者集,用于防止 spam 攻擊。分別記錄最近 k/2 + 1 次的區(qū)塊編號對應的簽名者。
Votes []*Vote: 按區(qū)塊編號順序投票的投票列表
Tally map[common.Address]Tally: 目前的投票計數(shù)器,以避免重新計算
1. func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot
構(gòu)造函數(shù)
newSnapshot() 使用指定的啟動參數(shù)創(chuàng)建新快照。這種方法不會初始化最近的簽名者集,所以只能用于創(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)存在的快照。
主要的實現(xiàn)細節(jié)如下:
- 調(diào)用方法 db.Get() 從數(shù)據(jù)庫加載 JSON 數(shù)據(jù)流
- 調(diào)用方法 json.Unmarshal() 從 JSON 數(shù)據(jù)流中解碼出對象 clique.Snapshot
- 與方法 Snapshot.store() 的功能相反。
3. func (s *Snapshot) store(db ethdb.Database) error
方法 store() 將快照插入數(shù)據(jù)庫。
主要的實現(xiàn)細節(jié)如下:
- 調(diào)用方法 json.Marshal() 將對象 clique.Snapshot 編碼成 JSON 數(shù)據(jù)流。
- 調(diào)用方法 db.Put() 將 JSON 數(shù)據(jù)流插入數(shù)據(jù)庫。
- 與函數(shù) loadSnapshot() 的功能相反。
4. func (s *Snapshot) copy() *Snapshot
方法 copy() 會創(chuàng)建快照的深層副本,但不會創(chuàng)建單獨的投票。
5. func (s *Snapshot) validVote(address common.Address, authorize bool) bool
方法 validVote() 返回在給定的快照上下文中投出的特定投票是否有意義(例如,不要嘗試添加已經(jīng)授權的簽名者)。
主要的實現(xiàn)細節(jié)如下:
- 當 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() 往投票計數(shù)器 Snapshot.tally 中增加新的投票。
主要的實現(xiàn)細節(jié)如下:
- 調(diào)用方法 Snapshot.validVote() 驗證投票的有效性。
- 需要考慮對指定地址的投票是全新的,還是只是增加得票數(shù)即可。
7. func (s *Snapshot) uncast(address common.Address, authorize bool) bool
方法 uncast() 從投票計數(shù)器 Snapshot.tally 中移除之前的一次投票。
主要的實現(xiàn)細節(jié)如下:
- 需要確保此次投票和之前的投票一致。
- 返還投票時需要考慮返還后指定地址的得票數(shù)是否為 0.
8. func (s Snapshot) apply(headers []types.Header) (*Snapshot, error)
方法 apply() 通過將給定的區(qū)塊頭列表應用于原始的快照來生成新的授權快照。
主要的實現(xiàn)細節(jié)如下:
如果 len(headers) == 0,則直接返回。允許傳入空 headers 以獲得更清晰的代碼。
檢查區(qū)塊頭列表的完整性。即區(qū)塊頭列表中的區(qū)塊頭必須是連續(xù)的,且是根據(jù)區(qū)塊編號升序排序的。
除了參數(shù) headers 必須是連續(xù)且升序之外,第一個區(qū)塊頭的區(qū)塊編號也必須是當前快照所處的區(qū)塊編號的下一個區(qū)塊, 即 headers[0].Number.Uint64() != s.Number+1。
-
通過方法 Snapshot.copy() 創(chuàng)建要返回的新的快照,并在此新快照上依次應用參數(shù)區(qū)塊頭列表中的區(qū)塊頭 header。
- 檢查當前區(qū)塊頭是否為檢查點區(qū)塊,如果是則清除所有的投票信息。
- 從最近的簽名者列表(snap.Recents)中刪除最舊的簽名者以允許它再次簽名。
- 具體規(guī)則為:Snapshot.Recents 最多只會記錄 K/2 + 1 個最近的簽名者簽名記錄,也就是簽名者在最近 K/2 + 1 個區(qū)塊中只能簽名一次。具體的計算規(guī)則是:假設當前區(qū)塊的編號為 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 個區(qū)塊的編號為 0,由 0 到 limit - 1 正好包含 (limit - 1) - 0 + 1 = (K/2 + 1 - 1) - 0 + 1 = k/2 + 1 個區(qū)塊。
- 調(diào)用函數(shù) ecrecover() 從區(qū)塊頭中恢復出簽名者 signer。
- 檢查簽名者 signer 是否存在于授權簽名者列表(snap.Signers),不存在返回 clique.errUnauthorized。
- 檢查簽名者 singer 是否在最近 K/2 + 1 個區(qū)塊中已經(jīng)簽名過,即是否已經(jīng)存在于最近的簽名者列表(snap.Recents)中。已經(jīng)簽名過則返回 clique.errUnauthorized。
- 更新最近的簽名者列表(snap.Recents),snap.Recents[number] = signer。
- 對于授權的區(qū)塊頭,丟棄簽名者以前的任何投票 vote。
- 通過方法 snap.uncast(vote.Address, vote.Authorize) 從投票計數(shù)器(Snapshot.Tally)移除該投票。
- 通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 從 Snapshot.Votes 中移除該投票。
- 從區(qū)塊頭 types.Header.Nonce 中計算是授權(nonceAuthVote)還是解除授權(nonceDropVote)投票,無效 Nonce 值則返回 clique.errInvalidVote。
- 通過方法 Snapshot.cast() 更新投票計數(shù)器(Snapshot.Tally)。如果成功,則往 snap.Votes 添加新的投票。
- 如果區(qū)塊頭 header 中的投票被通過,則更新授權簽名者列表。一次投票被通過的條件是,得票數(shù)大于等于 K/2 + 1,其中 K 為授權簽名者個數(shù)。
- 如果投票是授權簽名者,則 snap.Signers[header.Coinbase] = struct{}{}
- 如果投票是解除授權簽名者,則:
- delete(snap.Signers, header.Coinbase)。
- 簽名者列表縮小,刪除任何剩余的最近的簽名者列表(snap.Recents)緩存,這個操作是為了維持與 K/2 + 1 相關的這個規(guī)則。
- 丟棄授權簽名者以前的任何投票,即調(diào)用 snap.uncast 更新 snap.Votes,和通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 更新 snap.Votes。注意具體實現(xiàn)時的 i-- 操作,這是由于 snap.Votes 的長度已經(jīng)縮小了 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 的整個計數(shù)器。
更新當前快照創(chuàng)建時的區(qū)塊編號。即將原快照創(chuàng)建時的區(qū)塊編號加上參數(shù) headers 中 types.Header 的個數(shù),具體實現(xiàn)為 snap.Number += uint64(len(headers))
更新當前快照創(chuàng)建時的區(qū)塊哈希。即參數(shù) headers 中最后一個 types.Header 的哈希。snap.Hash = headers[len(headers)-1].Hash()
9. func (s *Snapshot) signers() []common.Address
方法 signers() 按升序返回授權簽名者列表。
主要的實現(xiàn)細節(jié)如下:
- 通過方法 sort.Sort() 按升序排序授權簽名者列表。
10. func (s *Snapshot) inturn(number uint64, signer common.Address) bool
方法 inturn() 返回簽名者在給定區(qū)塊高度是否是 in-turn 的。
這里可以理解 in-turn 為授權簽名者列表對于給定區(qū)塊判定采用哪個簽名者的規(guī)則。
假設區(qū)塊編號為 N,也就是區(qū)塊的高度為 N。授權簽名者列表的長度為 K。簽名者在授權簽名者列表中的順序為 P,從 0 開始偏移。則如果 (N % K) == P 就返回 true,表示 in-turn。
主要的實現(xiàn)細節(jié)如下:
- 即實現(xiàn)上面的規(guī)則。
4. type signers []common.Address
封裝器 signers 實現(xiàn)了排序接口,以允許排序地址列表。
(1) func (s signers) Len() int
方法 Len() 返回列表中元素的個數(shù)。
(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
Contributor
- Windstamp, https://github.com/windstamp