package v1
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
"github.com/tendermint/tendermint/p2p"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// Blockchain Reactor State
|
|
type bcReactorFSMState struct {
|
|
name string
|
|
|
|
// called when transitioning out of current state
|
|
handle func(*BcReactorFSM, bReactorEvent, bReactorEventData) (next *bcReactorFSMState, err error)
|
|
// called when entering the state
|
|
enter func(fsm *BcReactorFSM)
|
|
|
|
// timeout to ensure FSM is not stuck in a state forever
|
|
// the timer is owned and run by the fsm instance
|
|
timeout time.Duration
|
|
}
|
|
|
|
func (s *bcReactorFSMState) String() string {
|
|
return s.name
|
|
}
|
|
|
|
// BcReactorFSM is the datastructure for the Blockchain Reactor State Machine
|
|
type BcReactorFSM struct {
|
|
logger log.Logger
|
|
mtx sync.Mutex
|
|
|
|
startTime time.Time
|
|
|
|
state *bcReactorFSMState
|
|
stateTimer *time.Timer
|
|
pool *BlockPool
|
|
|
|
// interface used to call the Blockchain reactor to send StatusRequest, BlockRequest, reporting errors, etc.
|
|
toBcR bcReactor
|
|
}
|
|
|
|
// NewFSM creates a new reactor FSM.
|
|
func NewFSM(height int64, toBcR bcReactor) *BcReactorFSM {
|
|
return &BcReactorFSM{
|
|
state: unknown,
|
|
startTime: time.Now(),
|
|
pool: NewBlockPool(height, toBcR),
|
|
toBcR: toBcR,
|
|
}
|
|
}
|
|
|
|
// bReactorEventData is part of the message sent by the reactor to the FSM and used by the state handlers.
|
|
type bReactorEventData struct {
|
|
peerID p2p.ID
|
|
err error // for peer error: timeout, slow; for processed block event if error occurred
|
|
base int64 // for status response
|
|
height int64 // for status response; for processed block event
|
|
block *types.Block // for block response
|
|
stateName string // for state timeout events
|
|
length int // for block response event, length of received block, used to detect slow peers
|
|
maxNumRequests int // for request needed event, maximum number of pending requests
|
|
}
|
|
|
|
// Blockchain Reactor Events (the input to the state machine)
|
|
type bReactorEvent uint
|
|
|
|
const (
|
|
// message type events
|
|
startFSMEv = iota + 1
|
|
statusResponseEv
|
|
blockResponseEv
|
|
processedBlockEv
|
|
makeRequestsEv
|
|
stopFSMEv
|
|
|
|
// other events
|
|
peerRemoveEv = iota + 256
|
|
stateTimeoutEv
|
|
)
|
|
|
|
func (msg *bcReactorMessage) String() string {
|
|
var dataStr string
|
|
|
|
switch msg.event {
|
|
case startFSMEv:
|
|
dataStr = ""
|
|
case statusResponseEv:
|
|
dataStr = fmt.Sprintf("peer=%v base=%v height=%v", msg.data.peerID, msg.data.base, msg.data.height)
|
|
case blockResponseEv:
|
|
dataStr = fmt.Sprintf("peer=%v block.height=%v length=%v",
|
|
msg.data.peerID, msg.data.block.Height, msg.data.length)
|
|
case processedBlockEv:
|
|
dataStr = fmt.Sprintf("error=%v", msg.data.err)
|
|
case makeRequestsEv:
|
|
dataStr = ""
|
|
case stopFSMEv:
|
|
dataStr = ""
|
|
case peerRemoveEv:
|
|
dataStr = fmt.Sprintf("peer: %v is being removed by the switch", msg.data.peerID)
|
|
case stateTimeoutEv:
|
|
dataStr = fmt.Sprintf("state=%v", msg.data.stateName)
|
|
default:
|
|
dataStr = fmt.Sprintf("cannot interpret message data")
|
|
}
|
|
|
|
return fmt.Sprintf("%v: %v", msg.event, dataStr)
|
|
}
|
|
|
|
func (ev bReactorEvent) String() string {
|
|
switch ev {
|
|
case startFSMEv:
|
|
return "startFSMEv"
|
|
case statusResponseEv:
|
|
return "statusResponseEv"
|
|
case blockResponseEv:
|
|
return "blockResponseEv"
|
|
case processedBlockEv:
|
|
return "processedBlockEv"
|
|
case makeRequestsEv:
|
|
return "makeRequestsEv"
|
|
case stopFSMEv:
|
|
return "stopFSMEv"
|
|
case peerRemoveEv:
|
|
return "peerRemoveEv"
|
|
case stateTimeoutEv:
|
|
return "stateTimeoutEv"
|
|
default:
|
|
return "event unknown"
|
|
}
|
|
|
|
}
|
|
|
|
// states
|
|
var (
|
|
unknown *bcReactorFSMState
|
|
waitForPeer *bcReactorFSMState
|
|
waitForBlock *bcReactorFSMState
|
|
finished *bcReactorFSMState
|
|
)
|
|
|
|
// timeouts for state timers
|
|
const (
|
|
waitForPeerTimeout = 3 * time.Second
|
|
waitForBlockAtCurrentHeightTimeout = 10 * time.Second
|
|
)
|
|
|
|
// errors
|
|
var (
|
|
// internal to the package
|
|
errNoErrorFinished = errors.New("fast sync is finished")
|
|
errInvalidEvent = errors.New("invalid event in current state")
|
|
errMissingBlock = errors.New("missing blocks")
|
|
errNilPeerForBlockRequest = errors.New("peer for block request does not exist in the switch")
|
|
errSendQueueFull = errors.New("block request not made, send-queue is full")
|
|
errPeerTooShort = errors.New("peer height too low, old peer removed/ new peer not added")
|
|
errSwitchRemovesPeer = errors.New("switch is removing peer")
|
|
errTimeoutEventWrongState = errors.New("timeout event for a state different than the current one")
|
|
errNoTallerPeer = errors.New("fast sync timed out on waiting for a peer taller than this node")
|
|
|
|
// reported eventually to the switch
|
|
// handle return
|
|
errPeerLowersItsHeight = errors.New("fast sync peer reports a height lower than previous")
|
|
// handle return
|
|
errNoPeerResponseForCurrentHeights = errors.New("fast sync timed out on peer block response for current heights")
|
|
errNoPeerResponse = errors.New("fast sync timed out on peer block response") // xx
|
|
errBadDataFromPeer = errors.New("fast sync received block from wrong peer or block is bad") // xx
|
|
errDuplicateBlock = errors.New("fast sync received duplicate block from peer")
|
|
errBlockVerificationFailure = errors.New("fast sync block verification failure") // xx
|
|
errSlowPeer = errors.New("fast sync peer is not sending us data fast enough") // xx
|
|
|
|
)
|
|
|
|
func init() {
|
|
unknown = &bcReactorFSMState{
|
|
name: "unknown",
|
|
handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) {
|
|
switch ev {
|
|
case startFSMEv:
|
|
// Broadcast Status message. Currently doesn't return non-nil error.
|
|
fsm.toBcR.sendStatusRequest()
|
|
return waitForPeer, nil
|
|
|
|
case stopFSMEv:
|
|
return finished, errNoErrorFinished
|
|
|
|
default:
|
|
return unknown, errInvalidEvent
|
|
}
|
|
},
|
|
}
|
|
|
|
waitForPeer = &bcReactorFSMState{
|
|
name: "waitForPeer",
|
|
timeout: waitForPeerTimeout,
|
|
enter: func(fsm *BcReactorFSM) {
|
|
// Stop when leaving the state.
|
|
fsm.resetStateTimer()
|
|
},
|
|
handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) {
|
|
switch ev {
|
|
case stateTimeoutEv:
|
|
if data.stateName != "waitForPeer" {
|
|
fsm.logger.Error("received a state timeout event for different state",
|
|
"state", data.stateName)
|
|
return waitForPeer, errTimeoutEventWrongState
|
|
}
|
|
// There was no statusResponse received from any peer.
|
|
// Should we send status request again?
|
|
return finished, errNoTallerPeer
|
|
|
|
case statusResponseEv:
|
|
if err := fsm.pool.UpdatePeer(data.peerID, data.base, data.height); err != nil {
|
|
if fsm.pool.NumPeers() == 0 {
|
|
return waitForPeer, err
|
|
}
|
|
}
|
|
if fsm.stateTimer != nil {
|
|
fsm.stateTimer.Stop()
|
|
}
|
|
return waitForBlock, nil
|
|
|
|
case stopFSMEv:
|
|
if fsm.stateTimer != nil {
|
|
fsm.stateTimer.Stop()
|
|
}
|
|
return finished, errNoErrorFinished
|
|
|
|
default:
|
|
return waitForPeer, errInvalidEvent
|
|
}
|
|
},
|
|
}
|
|
|
|
waitForBlock = &bcReactorFSMState{
|
|
name: "waitForBlock",
|
|
timeout: waitForBlockAtCurrentHeightTimeout,
|
|
enter: func(fsm *BcReactorFSM) {
|
|
// Stop when leaving the state.
|
|
fsm.resetStateTimer()
|
|
},
|
|
handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) {
|
|
switch ev {
|
|
|
|
case statusResponseEv:
|
|
err := fsm.pool.UpdatePeer(data.peerID, data.base, data.height)
|
|
if fsm.pool.NumPeers() == 0 {
|
|
return waitForPeer, err
|
|
}
|
|
if fsm.pool.ReachedMaxHeight() {
|
|
return finished, err
|
|
}
|
|
return waitForBlock, err
|
|
|
|
case blockResponseEv:
|
|
fsm.logger.Debug("blockResponseEv", "H", data.block.Height)
|
|
err := fsm.pool.AddBlock(data.peerID, data.block, data.length)
|
|
if err != nil {
|
|
// A block was received that was unsolicited, from unexpected peer, or that we already have it.
|
|
// Ignore block, remove peer and send error to switch.
|
|
fsm.pool.RemovePeer(data.peerID, err)
|
|
fsm.toBcR.sendPeerError(err, data.peerID)
|
|
}
|
|
if fsm.pool.NumPeers() == 0 {
|
|
return waitForPeer, err
|
|
}
|
|
return waitForBlock, err
|
|
|
|
case processedBlockEv:
|
|
if data.err != nil {
|
|
first, second, _ := fsm.pool.FirstTwoBlocksAndPeers()
|
|
fsm.logger.Error("error processing block", "err", data.err,
|
|
"first", first.block.Height, "second", second.block.Height)
|
|
fsm.logger.Error("send peer error for", "peer", first.peer.ID)
|
|
fsm.toBcR.sendPeerError(data.err, first.peer.ID)
|
|
fsm.logger.Error("send peer error for", "peer", second.peer.ID)
|
|
fsm.toBcR.sendPeerError(data.err, second.peer.ID)
|
|
// Remove the first two blocks. This will also remove the peers
|
|
fsm.pool.InvalidateFirstTwoBlocks(data.err)
|
|
} else {
|
|
fsm.pool.ProcessedCurrentHeightBlock()
|
|
// Since we advanced one block reset the state timer
|
|
fsm.resetStateTimer()
|
|
}
|
|
|
|
// Both cases above may result in achieving maximum height.
|
|
if fsm.pool.ReachedMaxHeight() {
|
|
return finished, nil
|
|
}
|
|
|
|
return waitForBlock, data.err
|
|
|
|
case peerRemoveEv:
|
|
// This event is sent by the switch to remove disconnected and errored peers.
|
|
fsm.pool.RemovePeer(data.peerID, data.err)
|
|
if fsm.pool.NumPeers() == 0 {
|
|
return waitForPeer, nil
|
|
}
|
|
if fsm.pool.ReachedMaxHeight() {
|
|
return finished, nil
|
|
}
|
|
return waitForBlock, nil
|
|
|
|
case makeRequestsEv:
|
|
fsm.makeNextRequests(data.maxNumRequests)
|
|
return waitForBlock, nil
|
|
|
|
case stateTimeoutEv:
|
|
if data.stateName != "waitForBlock" {
|
|
fsm.logger.Error("received a state timeout event for different state",
|
|
"state", data.stateName)
|
|
return waitForBlock, errTimeoutEventWrongState
|
|
}
|
|
// We haven't received the block at current height or height+1. Remove peer.
|
|
fsm.pool.RemovePeerAtCurrentHeights(errNoPeerResponseForCurrentHeights)
|
|
fsm.resetStateTimer()
|
|
if fsm.pool.NumPeers() == 0 {
|
|
return waitForPeer, errNoPeerResponseForCurrentHeights
|
|
}
|
|
if fsm.pool.ReachedMaxHeight() {
|
|
return finished, nil
|
|
}
|
|
return waitForBlock, errNoPeerResponseForCurrentHeights
|
|
|
|
case stopFSMEv:
|
|
if fsm.stateTimer != nil {
|
|
fsm.stateTimer.Stop()
|
|
}
|
|
return finished, errNoErrorFinished
|
|
|
|
default:
|
|
return waitForBlock, errInvalidEvent
|
|
}
|
|
},
|
|
}
|
|
|
|
finished = &bcReactorFSMState{
|
|
name: "finished",
|
|
enter: func(fsm *BcReactorFSM) {
|
|
fsm.logger.Info("Time to switch to consensus reactor!", "height", fsm.pool.Height)
|
|
fsm.toBcR.switchToConsensus()
|
|
fsm.cleanup()
|
|
},
|
|
handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) {
|
|
return finished, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// Interface used by FSM for sending Block and Status requests,
|
|
// informing of peer errors and state timeouts
|
|
// Implemented by BlockchainReactor and tests
|
|
type bcReactor interface {
|
|
sendStatusRequest()
|
|
sendBlockRequest(peerID p2p.ID, height int64) error
|
|
sendPeerError(err error, peerID p2p.ID)
|
|
resetStateTimer(name string, timer **time.Timer, timeout time.Duration)
|
|
switchToConsensus()
|
|
}
|
|
|
|
// SetLogger sets the FSM logger.
|
|
func (fsm *BcReactorFSM) SetLogger(l log.Logger) {
|
|
fsm.logger = l
|
|
fsm.pool.SetLogger(l)
|
|
}
|
|
|
|
// Start starts the FSM.
|
|
func (fsm *BcReactorFSM) Start() {
|
|
_ = fsm.Handle(&bcReactorMessage{event: startFSMEv})
|
|
}
|
|
|
|
// Handle processes messages and events sent to the FSM.
|
|
func (fsm *BcReactorFSM) Handle(msg *bcReactorMessage) error {
|
|
fsm.mtx.Lock()
|
|
defer fsm.mtx.Unlock()
|
|
fsm.logger.Debug("FSM received", "event", msg, "state", fsm.state)
|
|
|
|
if fsm.state == nil {
|
|
fsm.state = unknown
|
|
}
|
|
next, err := fsm.state.handle(fsm, msg.event, msg.data)
|
|
if err != nil {
|
|
fsm.logger.Error("FSM event handler returned", "err", err,
|
|
"state", fsm.state, "event", msg.event)
|
|
}
|
|
|
|
oldState := fsm.state.name
|
|
fsm.transition(next)
|
|
if oldState != fsm.state.name {
|
|
fsm.logger.Info("FSM changed state", "new_state", fsm.state)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (fsm *BcReactorFSM) transition(next *bcReactorFSMState) {
|
|
if next == nil {
|
|
return
|
|
}
|
|
if fsm.state != next {
|
|
fsm.state = next
|
|
if next.enter != nil {
|
|
next.enter(fsm)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called when entering an FSM state in order to detect lack of progress in the state machine.
|
|
// Note the use of the 'bcr' interface to facilitate testing without timer expiring.
|
|
func (fsm *BcReactorFSM) resetStateTimer() {
|
|
fsm.toBcR.resetStateTimer(fsm.state.name, &fsm.stateTimer, fsm.state.timeout)
|
|
}
|
|
|
|
func (fsm *BcReactorFSM) isCaughtUp() bool {
|
|
return fsm.state == finished
|
|
}
|
|
|
|
func (fsm *BcReactorFSM) makeNextRequests(maxNumRequests int) {
|
|
fsm.pool.MakeNextRequests(maxNumRequests)
|
|
}
|
|
|
|
func (fsm *BcReactorFSM) cleanup() {
|
|
fsm.pool.Cleanup()
|
|
}
|
|
|
|
// NeedsBlocks checks if more block requests are required.
|
|
func (fsm *BcReactorFSM) NeedsBlocks() bool {
|
|
fsm.mtx.Lock()
|
|
defer fsm.mtx.Unlock()
|
|
return fsm.state.name == "waitForBlock" && fsm.pool.NeedsBlocks()
|
|
}
|
|
|
|
// FirstTwoBlocks returns the two blocks at pool height and height+1
|
|
func (fsm *BcReactorFSM) FirstTwoBlocks() (first, second *types.Block, err error) {
|
|
fsm.mtx.Lock()
|
|
defer fsm.mtx.Unlock()
|
|
firstBP, secondBP, err := fsm.pool.FirstTwoBlocksAndPeers()
|
|
if err == nil {
|
|
first = firstBP.block
|
|
second = secondBP.block
|
|
}
|
|
return
|
|
}
|
|
|
|
// Status returns the pool's height and the maximum peer height.
|
|
func (fsm *BcReactorFSM) Status() (height, maxPeerHeight int64) {
|
|
fsm.mtx.Lock()
|
|
defer fsm.mtx.Unlock()
|
|
return fsm.pool.Height, fsm.pool.MaxPeerHeight
|
|
}
|