package statesync
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
tmsync "github.com/tendermint/tendermint/libs/sync"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
)
|
|
|
|
// snapshotKey is a snapshot key used for lookups.
|
|
type snapshotKey [sha256.Size]byte
|
|
|
|
// snapshot contains data about a snapshot.
|
|
type snapshot struct {
|
|
Height uint64
|
|
Format uint32
|
|
Chunks uint32
|
|
Hash []byte
|
|
Metadata []byte
|
|
|
|
trustedAppHash []byte // populated by light client
|
|
}
|
|
|
|
// Key generates a snapshot key, used for lookups. It takes into account not only the height and
|
|
// format, but also the chunks, hash, and metadata in case peers have generated snapshots in a
|
|
// non-deterministic manner. All fields must be equal for the snapshot to be considered the same.
|
|
func (s *snapshot) Key() snapshotKey {
|
|
// Hash.Write() never returns an error.
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(fmt.Sprintf("%v:%v:%v", s.Height, s.Format, s.Chunks)))
|
|
hasher.Write(s.Hash)
|
|
hasher.Write(s.Metadata)
|
|
var key snapshotKey
|
|
copy(key[:], hasher.Sum(nil))
|
|
return key
|
|
}
|
|
|
|
// snapshotPool discovers and aggregates snapshots across peers.
|
|
type snapshotPool struct {
|
|
stateProvider StateProvider
|
|
|
|
tmsync.Mutex
|
|
snapshots map[snapshotKey]*snapshot
|
|
snapshotPeers map[snapshotKey]map[p2p.NodeID]p2p.NodeID
|
|
|
|
// indexes for fast searches
|
|
formatIndex map[uint32]map[snapshotKey]bool
|
|
heightIndex map[uint64]map[snapshotKey]bool
|
|
peerIndex map[p2p.NodeID]map[snapshotKey]bool
|
|
|
|
// blacklists for rejected items
|
|
formatBlacklist map[uint32]bool
|
|
peerBlacklist map[p2p.NodeID]bool
|
|
snapshotBlacklist map[snapshotKey]bool
|
|
}
|
|
|
|
// newSnapshotPool creates a new snapshot pool. The state source is used for
|
|
func newSnapshotPool(stateProvider StateProvider) *snapshotPool {
|
|
return &snapshotPool{
|
|
stateProvider: stateProvider,
|
|
snapshots: make(map[snapshotKey]*snapshot),
|
|
snapshotPeers: make(map[snapshotKey]map[p2p.NodeID]p2p.NodeID),
|
|
formatIndex: make(map[uint32]map[snapshotKey]bool),
|
|
heightIndex: make(map[uint64]map[snapshotKey]bool),
|
|
peerIndex: make(map[p2p.NodeID]map[snapshotKey]bool),
|
|
formatBlacklist: make(map[uint32]bool),
|
|
peerBlacklist: make(map[p2p.NodeID]bool),
|
|
snapshotBlacklist: make(map[snapshotKey]bool),
|
|
}
|
|
}
|
|
|
|
// Add adds a snapshot to the pool, unless the peer has already sent recentSnapshots
|
|
// snapshots. It returns true if this was a new, non-blacklisted snapshot. The
|
|
// snapshot height is verified using the light client, and the expected app hash
|
|
// is set for the snapshot.
|
|
func (p *snapshotPool) Add(peerID p2p.NodeID, snapshot *snapshot) (bool, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
appHash, err := p.stateProvider.AppHash(ctx, snapshot.Height)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
snapshot.trustedAppHash = appHash
|
|
key := snapshot.Key()
|
|
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
|
|
switch {
|
|
case p.formatBlacklist[snapshot.Format]:
|
|
return false, nil
|
|
case p.peerBlacklist[peerID]:
|
|
return false, nil
|
|
case p.snapshotBlacklist[key]:
|
|
return false, nil
|
|
case len(p.peerIndex[peerID]) >= recentSnapshots:
|
|
return false, nil
|
|
}
|
|
|
|
if p.snapshotPeers[key] == nil {
|
|
p.snapshotPeers[key] = make(map[p2p.NodeID]p2p.NodeID)
|
|
}
|
|
p.snapshotPeers[key][peerID] = peerID
|
|
|
|
if p.peerIndex[peerID] == nil {
|
|
p.peerIndex[peerID] = make(map[snapshotKey]bool)
|
|
}
|
|
p.peerIndex[peerID][key] = true
|
|
|
|
if p.snapshots[key] != nil {
|
|
return false, nil
|
|
}
|
|
p.snapshots[key] = snapshot
|
|
|
|
if p.formatIndex[snapshot.Format] == nil {
|
|
p.formatIndex[snapshot.Format] = make(map[snapshotKey]bool)
|
|
}
|
|
p.formatIndex[snapshot.Format][key] = true
|
|
|
|
if p.heightIndex[snapshot.Height] == nil {
|
|
p.heightIndex[snapshot.Height] = make(map[snapshotKey]bool)
|
|
}
|
|
p.heightIndex[snapshot.Height][key] = true
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// Best returns the "best" currently known snapshot, if any.
|
|
func (p *snapshotPool) Best() *snapshot {
|
|
ranked := p.Ranked()
|
|
if len(ranked) == 0 {
|
|
return nil
|
|
}
|
|
return ranked[0]
|
|
}
|
|
|
|
// GetPeer returns a random peer for a snapshot, if any.
|
|
func (p *snapshotPool) GetPeer(snapshot *snapshot) p2p.NodeID {
|
|
peers := p.GetPeers(snapshot)
|
|
if len(peers) == 0 {
|
|
return ""
|
|
}
|
|
return peers[rand.Intn(len(peers))] // nolint:gosec // G404: Use of weak random number generator
|
|
}
|
|
|
|
// GetPeers returns the peers for a snapshot.
|
|
func (p *snapshotPool) GetPeers(snapshot *snapshot) []p2p.NodeID {
|
|
key := snapshot.Key()
|
|
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
|
|
peers := make([]p2p.NodeID, 0, len(p.snapshotPeers[key]))
|
|
for _, peer := range p.snapshotPeers[key] {
|
|
peers = append(peers, peer)
|
|
}
|
|
|
|
// sort results, for testability (otherwise order is random, so tests randomly fail)
|
|
sort.Slice(peers, func(a int, b int) bool {
|
|
return strings.Compare(string(peers[a]), string(peers[b])) < 0
|
|
})
|
|
|
|
return peers
|
|
}
|
|
|
|
// Ranked returns a list of snapshots ranked by preference. The current heuristic is very naïve,
|
|
// preferring the snapshot with the greatest height, then greatest format, then greatest number of
|
|
// peers. This can be improved quite a lot.
|
|
func (p *snapshotPool) Ranked() []*snapshot {
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
|
|
if len(p.snapshots) == 0 {
|
|
return []*snapshot{}
|
|
}
|
|
|
|
numPeers := make([]int, 0, len(p.snapshots))
|
|
for key := range p.snapshots {
|
|
numPeers = append(numPeers, len(p.snapshotPeers[key]))
|
|
}
|
|
sort.Ints(numPeers)
|
|
median := len(numPeers) / 2
|
|
if len(numPeers)%2 == 0 {
|
|
median = (numPeers[median-1] + numPeers[median]) / 2
|
|
} else {
|
|
median = numPeers[median]
|
|
}
|
|
|
|
commonCandidates := make([]*snapshot, 0, len(p.snapshots)/2)
|
|
uncommonCandidates := make([]*snapshot, 0, len(p.snapshots)/2)
|
|
for key := range p.snapshots {
|
|
if len(p.snapshotPeers[key]) > median {
|
|
commonCandidates = append(commonCandidates, p.snapshots[key])
|
|
continue
|
|
}
|
|
|
|
uncommonCandidates = append(uncommonCandidates, p.snapshots[key])
|
|
}
|
|
|
|
sort.Slice(commonCandidates, p.sorterFactory(commonCandidates))
|
|
sort.Slice(uncommonCandidates, p.sorterFactory(uncommonCandidates))
|
|
|
|
return append(commonCandidates, uncommonCandidates...)
|
|
}
|
|
|
|
func (p *snapshotPool) sorterFactory(candidates []*snapshot) func(int, int) bool {
|
|
return func(i, j int) bool {
|
|
a := candidates[i]
|
|
b := candidates[j]
|
|
|
|
switch {
|
|
case a.Height > b.Height:
|
|
return true
|
|
case a.Height < b.Height:
|
|
return false
|
|
case len(p.snapshotPeers[a.Key()]) > len(p.snapshotPeers[b.Key()]):
|
|
return true
|
|
case a.Format > b.Format:
|
|
return true
|
|
case a.Format < b.Format:
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reject rejects a snapshot. Rejected snapshots will never be used again.
|
|
func (p *snapshotPool) Reject(snapshot *snapshot) {
|
|
key := snapshot.Key()
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
|
|
p.snapshotBlacklist[key] = true
|
|
p.removeSnapshot(key)
|
|
}
|
|
|
|
// RejectFormat rejects a snapshot format. It will never be used again.
|
|
func (p *snapshotPool) RejectFormat(format uint32) {
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
|
|
p.formatBlacklist[format] = true
|
|
for key := range p.formatIndex[format] {
|
|
p.removeSnapshot(key)
|
|
}
|
|
}
|
|
|
|
// RejectPeer rejects a peer. It will never be used again.
|
|
func (p *snapshotPool) RejectPeer(peerID p2p.NodeID) {
|
|
if len(peerID) == 0 {
|
|
return
|
|
}
|
|
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
|
|
p.removePeer(peerID)
|
|
p.peerBlacklist[peerID] = true
|
|
}
|
|
|
|
// RemovePeer removes a peer from the pool, and any snapshots that no longer have peers.
|
|
func (p *snapshotPool) RemovePeer(peerID p2p.NodeID) {
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
p.removePeer(peerID)
|
|
}
|
|
|
|
// removePeer removes a peer. The caller must hold the mutex lock.
|
|
func (p *snapshotPool) removePeer(peerID p2p.NodeID) {
|
|
for key := range p.peerIndex[peerID] {
|
|
delete(p.snapshotPeers[key], peerID)
|
|
if len(p.snapshotPeers[key]) == 0 {
|
|
p.removeSnapshot(key)
|
|
}
|
|
}
|
|
|
|
delete(p.peerIndex, peerID)
|
|
}
|
|
|
|
// removeSnapshot removes a snapshot. The caller must hold the mutex lock.
|
|
func (p *snapshotPool) removeSnapshot(key snapshotKey) {
|
|
snapshot := p.snapshots[key]
|
|
if snapshot == nil {
|
|
return
|
|
}
|
|
|
|
delete(p.snapshots, key)
|
|
delete(p.formatIndex[snapshot.Format], key)
|
|
delete(p.heightIndex[snapshot.Height], key)
|
|
for peerID := range p.snapshotPeers[key] {
|
|
delete(p.peerIndex[peerID], key)
|
|
}
|
|
delete(p.snapshotPeers, key)
|
|
}
|