|
package v1
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// BlockPool keeps track of the fast sync peers, block requests and block responses.
|
|
type BlockPool struct {
|
|
logger log.Logger
|
|
// Set of peers that have sent status responses, with height bigger than pool.Height
|
|
peers map[p2p.ID]*BpPeer
|
|
// Set of block heights and the corresponding peers from where a block response is expected or has been received.
|
|
blocks map[int64]p2p.ID
|
|
|
|
plannedRequests map[int64]struct{} // list of blocks to be assigned peers for blockRequest
|
|
nextRequestHeight int64 // next height to be added to plannedRequests
|
|
|
|
Height int64 // height of next block to execute
|
|
MaxPeerHeight int64 // maximum height of all peers
|
|
toBcR bcReactor
|
|
}
|
|
|
|
// NewBlockPool creates a new BlockPool.
|
|
func NewBlockPool(height int64, toBcR bcReactor) *BlockPool {
|
|
return &BlockPool{
|
|
Height: height,
|
|
MaxPeerHeight: 0,
|
|
peers: make(map[p2p.ID]*BpPeer),
|
|
blocks: make(map[int64]p2p.ID),
|
|
plannedRequests: make(map[int64]struct{}),
|
|
nextRequestHeight: height,
|
|
toBcR: toBcR,
|
|
}
|
|
}
|
|
|
|
// SetLogger sets the logger of the pool.
|
|
func (pool *BlockPool) SetLogger(l log.Logger) {
|
|
pool.logger = l
|
|
}
|
|
|
|
// ReachedMaxHeight check if the pool has reached the maximum peer height.
|
|
func (pool *BlockPool) ReachedMaxHeight() bool {
|
|
return pool.Height >= pool.MaxPeerHeight
|
|
}
|
|
|
|
func (pool *BlockPool) rescheduleRequest(peerID p2p.ID, height int64) {
|
|
pool.logger.Info("reschedule requests made to peer for height ", "peerID", peerID, "height", height)
|
|
pool.plannedRequests[height] = struct{}{}
|
|
delete(pool.blocks, height)
|
|
pool.peers[peerID].RemoveBlock(height)
|
|
}
|
|
|
|
// Updates the pool's max height. If no peers are left MaxPeerHeight is set to 0.
|
|
func (pool *BlockPool) updateMaxPeerHeight() {
|
|
var newMax int64
|
|
for _, peer := range pool.peers {
|
|
peerHeight := peer.Height
|
|
if peerHeight > newMax {
|
|
newMax = peerHeight
|
|
}
|
|
}
|
|
pool.MaxPeerHeight = newMax
|
|
}
|
|
|
|
// UpdatePeer adds a new peer or updates an existing peer with a new base and height.
|
|
// If a peer is short it is not added.
|
|
func (pool *BlockPool) UpdatePeer(peerID p2p.ID, base int64, height int64) error {
|
|
|
|
peer := pool.peers[peerID]
|
|
|
|
if peer == nil {
|
|
if height < pool.Height {
|
|
pool.logger.Info("Peer height too small",
|
|
"peer", peerID, "height", height, "fsm_height", pool.Height)
|
|
return errPeerTooShort
|
|
}
|
|
// Add new peer.
|
|
peer = NewBpPeer(peerID, base, height, pool.toBcR.sendPeerError, nil)
|
|
peer.SetLogger(pool.logger.With("peer", peerID))
|
|
pool.peers[peerID] = peer
|
|
pool.logger.Info("added peer", "peerID", peerID, "base", base, "height", height, "num_peers", len(pool.peers))
|
|
} else {
|
|
// Check if peer is lowering its height. This is not allowed.
|
|
if height < peer.Height {
|
|
pool.RemovePeer(peerID, errPeerLowersItsHeight)
|
|
return errPeerLowersItsHeight
|
|
}
|
|
// Update existing peer.
|
|
peer.Base = base
|
|
peer.Height = height
|
|
}
|
|
|
|
// Update the pool's MaxPeerHeight if needed.
|
|
pool.updateMaxPeerHeight()
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetNoBlock records that the peer does not have a block for height and
|
|
// schedules a new request for that height from another peer.
|
|
func (pool *BlockPool) SetNoBlock(peerID p2p.ID, height int64) {
|
|
peer := pool.peers[peerID]
|
|
if peer == nil {
|
|
return
|
|
}
|
|
peer.SetNoBlock(height)
|
|
|
|
pool.rescheduleRequest(peerID, height)
|
|
}
|
|
|
|
// Cleans and deletes the peer. Recomputes the max peer height.
|
|
func (pool *BlockPool) deletePeer(peer *BpPeer) {
|
|
if peer == nil {
|
|
return
|
|
}
|
|
peer.Cleanup()
|
|
delete(pool.peers, peer.ID)
|
|
|
|
if peer.Height == pool.MaxPeerHeight {
|
|
pool.updateMaxPeerHeight()
|
|
}
|
|
}
|
|
|
|
// RemovePeer removes the blocks and requests from the peer, reschedules them and deletes the peer.
|
|
func (pool *BlockPool) RemovePeer(peerID p2p.ID, err error) {
|
|
peer := pool.peers[peerID]
|
|
if peer == nil {
|
|
return
|
|
}
|
|
pool.logger.Info("removing peer", "peerID", peerID, "error", err)
|
|
|
|
// Reschedule the block requests made to the peer, or received and not processed yet.
|
|
// Note that some of the requests may be removed further down.
|
|
for h := range pool.peers[peerID].blocks {
|
|
pool.rescheduleRequest(peerID, h)
|
|
}
|
|
|
|
oldMaxPeerHeight := pool.MaxPeerHeight
|
|
// Delete the peer. This operation may result in the pool's MaxPeerHeight being lowered.
|
|
pool.deletePeer(peer)
|
|
|
|
// Check if the pool's MaxPeerHeight has been lowered.
|
|
// This may happen if the tallest peer has been removed.
|
|
if oldMaxPeerHeight > pool.MaxPeerHeight {
|
|
// Remove any planned requests for heights over the new MaxPeerHeight.
|
|
for h := range pool.plannedRequests {
|
|
if h > pool.MaxPeerHeight {
|
|
delete(pool.plannedRequests, h)
|
|
}
|
|
}
|
|
// Adjust the nextRequestHeight to the new max plus one.
|
|
if pool.nextRequestHeight > pool.MaxPeerHeight {
|
|
pool.nextRequestHeight = pool.MaxPeerHeight + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pool *BlockPool) removeShortPeers() {
|
|
for _, peer := range pool.peers {
|
|
if peer.Height < pool.Height {
|
|
pool.RemovePeer(peer.ID, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pool *BlockPool) removeBadPeers() {
|
|
pool.removeShortPeers()
|
|
for _, peer := range pool.peers {
|
|
if err := peer.CheckRate(); err != nil {
|
|
pool.RemovePeer(peer.ID, err)
|
|
pool.toBcR.sendPeerError(err, peer.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MakeNextRequests creates more requests if the block pool is running low.
|
|
func (pool *BlockPool) MakeNextRequests(maxNumRequests int) {
|
|
heights := pool.makeRequestBatch(maxNumRequests)
|
|
if len(heights) != 0 {
|
|
pool.logger.Info("makeNextRequests will make following requests",
|
|
"number", len(heights), "heights", heights)
|
|
}
|
|
|
|
for _, height := range heights {
|
|
h := int64(height)
|
|
if !pool.sendRequest(h) {
|
|
// If a good peer was not found for sending the request at height h then return,
|
|
// as it shouldn't be possible to find a peer for h+1.
|
|
return
|
|
}
|
|
delete(pool.plannedRequests, h)
|
|
}
|
|
}
|
|
|
|
// Makes a batch of requests sorted by height such that the block pool has up to maxNumRequests entries.
|
|
func (pool *BlockPool) makeRequestBatch(maxNumRequests int) []int {
|
|
pool.removeBadPeers()
|
|
// At this point pool.requests may include heights for requests to be redone due to removal of peers:
|
|
// - peers timed out or were removed by switch
|
|
// - FSM timed out on waiting to advance the block execution due to missing blocks at h or h+1
|
|
// Determine the number of requests needed by subtracting the number of requests already made from the maximum
|
|
// allowed
|
|
numNeeded := maxNumRequests - len(pool.blocks)
|
|
for len(pool.plannedRequests) < numNeeded {
|
|
if pool.nextRequestHeight > pool.MaxPeerHeight {
|
|
break
|
|
}
|
|
pool.plannedRequests[pool.nextRequestHeight] = struct{}{}
|
|
pool.nextRequestHeight++
|
|
}
|
|
|
|
heights := make([]int, 0, len(pool.plannedRequests))
|
|
for k := range pool.plannedRequests {
|
|
heights = append(heights, int(k))
|
|
}
|
|
sort.Ints(heights)
|
|
return heights
|
|
}
|
|
|
|
func (pool *BlockPool) sendRequest(height int64) bool {
|
|
for _, peer := range pool.peers {
|
|
if peer.NumPendingBlockRequests >= maxRequestsPerPeer {
|
|
continue
|
|
}
|
|
if peer.Base > height || peer.Height < height || peer.NoBlock(height) {
|
|
continue
|
|
}
|
|
|
|
err := pool.toBcR.sendBlockRequest(peer.ID, height)
|
|
if err == errNilPeerForBlockRequest {
|
|
// Switch does not have this peer, remove it and continue to look for another peer.
|
|
pool.logger.Error("switch does not have peer..removing peer selected for height", "peer",
|
|
peer.ID, "height", height)
|
|
pool.RemovePeer(peer.ID, err)
|
|
continue
|
|
}
|
|
|
|
if err == errSendQueueFull {
|
|
pool.logger.Error("peer queue is full", "peer", peer.ID, "height", height)
|
|
continue
|
|
}
|
|
|
|
pool.logger.Info("assigned request to peer", "peer", peer.ID, "height", height)
|
|
|
|
pool.blocks[height] = peer.ID
|
|
peer.RequestSent(height)
|
|
|
|
return true
|
|
}
|
|
pool.logger.Error("could not find peer to send request for block at height", "height", height)
|
|
return false
|
|
}
|
|
|
|
// AddBlock validates that the block comes from the peer it was expected from and stores it in the 'blocks' map.
|
|
func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) error {
|
|
peer, ok := pool.peers[peerID]
|
|
if !ok {
|
|
pool.logger.Error("block from unknown peer", "height", block.Height, "peer", peerID)
|
|
return errBadDataFromPeer
|
|
}
|
|
if wantPeerID, ok := pool.blocks[block.Height]; ok && wantPeerID != peerID {
|
|
pool.logger.Error("block received from wrong peer", "height", block.Height,
|
|
"peer", peerID, "expected_peer", wantPeerID)
|
|
return errBadDataFromPeer
|
|
}
|
|
|
|
return peer.AddBlock(block, blockSize)
|
|
}
|
|
|
|
// BlockData stores the peer responsible to deliver a block and the actual block if delivered.
|
|
type BlockData struct {
|
|
block *types.Block
|
|
peer *BpPeer
|
|
}
|
|
|
|
// BlockAndPeerAtHeight retrieves the block and delivery peer at specified height.
|
|
// Returns errMissingBlock if a block was not found
|
|
func (pool *BlockPool) BlockAndPeerAtHeight(height int64) (bData *BlockData, err error) {
|
|
peerID := pool.blocks[height]
|
|
peer := pool.peers[peerID]
|
|
if peer == nil {
|
|
return nil, errMissingBlock
|
|
}
|
|
|
|
block, err := peer.BlockAtHeight(height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &BlockData{peer: peer, block: block}, nil
|
|
|
|
}
|
|
|
|
// FirstTwoBlocksAndPeers returns the blocks and the delivery peers at pool's height H and H+1.
|
|
func (pool *BlockPool) FirstTwoBlocksAndPeers() (first, second *BlockData, err error) {
|
|
first, err = pool.BlockAndPeerAtHeight(pool.Height)
|
|
second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1)
|
|
if err == nil {
|
|
err = err2
|
|
}
|
|
return
|
|
}
|
|
|
|
// InvalidateFirstTwoBlocks removes the peers that sent us the first two blocks, blocks are removed by RemovePeer().
|
|
func (pool *BlockPool) InvalidateFirstTwoBlocks(err error) {
|
|
first, err1 := pool.BlockAndPeerAtHeight(pool.Height)
|
|
second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1)
|
|
|
|
if err1 == nil {
|
|
pool.RemovePeer(first.peer.ID, err)
|
|
}
|
|
if err2 == nil {
|
|
pool.RemovePeer(second.peer.ID, err)
|
|
}
|
|
}
|
|
|
|
// ProcessedCurrentHeightBlock performs cleanup after a block is processed. It removes block at pool height and
|
|
// the peers that are now short.
|
|
func (pool *BlockPool) ProcessedCurrentHeightBlock() {
|
|
peerID, peerOk := pool.blocks[pool.Height]
|
|
if peerOk {
|
|
pool.peers[peerID].RemoveBlock(pool.Height)
|
|
}
|
|
delete(pool.blocks, pool.Height)
|
|
pool.logger.Debug("removed block at height", "height", pool.Height)
|
|
pool.Height++
|
|
pool.removeShortPeers()
|
|
}
|
|
|
|
// RemovePeerAtCurrentHeights checks if a block at pool's height H exists and if not, it removes the
|
|
// delivery peer and returns. If a block at height H exists then the check and peer removal is done for H+1.
|
|
// This function is called when the FSM is not able to make progress for some time.
|
|
// This happens if either the block H or H+1 have not been delivered.
|
|
func (pool *BlockPool) RemovePeerAtCurrentHeights(err error) {
|
|
peerID := pool.blocks[pool.Height]
|
|
peer, ok := pool.peers[peerID]
|
|
if ok {
|
|
if _, err := peer.BlockAtHeight(pool.Height); err != nil {
|
|
pool.logger.Info("remove peer that hasn't sent block at pool.Height",
|
|
"peer", peerID, "height", pool.Height)
|
|
pool.RemovePeer(peerID, err)
|
|
return
|
|
}
|
|
}
|
|
peerID = pool.blocks[pool.Height+1]
|
|
peer, ok = pool.peers[peerID]
|
|
if ok {
|
|
if _, err := peer.BlockAtHeight(pool.Height + 1); err != nil {
|
|
pool.logger.Info("remove peer that hasn't sent block at pool.Height+1",
|
|
"peer", peerID, "height", pool.Height+1)
|
|
pool.RemovePeer(peerID, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cleanup performs pool and peer cleanup
|
|
func (pool *BlockPool) Cleanup() {
|
|
for id, peer := range pool.peers {
|
|
peer.Cleanup()
|
|
delete(pool.peers, id)
|
|
}
|
|
pool.plannedRequests = make(map[int64]struct{})
|
|
pool.blocks = make(map[int64]p2p.ID)
|
|
pool.nextRequestHeight = 0
|
|
pool.Height = 0
|
|
pool.MaxPeerHeight = 0
|
|
}
|
|
|
|
// NumPeers returns the number of peers in the pool
|
|
func (pool *BlockPool) NumPeers() int {
|
|
return len(pool.peers)
|
|
}
|
|
|
|
// NeedsBlocks returns true if more blocks are required.
|
|
func (pool *BlockPool) NeedsBlocks() bool {
|
|
return len(pool.blocks) < maxNumRequests
|
|
}
|