|
|
- # Blockchain Reactor
-
- The Blockchain Reactor's high level responsibility is to enable peers who are
- far behind the current state of the consensus to quickly catch up by downloading
- many blocks in parallel, verifying their commits, and executing them against the
- ABCI application.
-
- Tendermint full nodes run the Blockchain Reactor as a service to provide blocks
- to new nodes. New nodes run the Blockchain Reactor in "fast_sync" mode,
- where they actively make requests for more blocks until they sync up.
- Once caught up, "fast_sync" mode is disabled and the node switches to
- using (and turns on) the Consensus Reactor.
-
- ## Message Types
-
- ```go
- const (
- msgTypeBlockRequest = byte(0x10)
- msgTypeBlockResponse = byte(0x11)
- msgTypeNoBlockResponse = byte(0x12)
- msgTypeStatusResponse = byte(0x20)
- msgTypeStatusRequest = byte(0x21)
- )
- ```
-
- ```go
- type bcBlockRequestMessage struct {
- Height int64
- }
-
- type bcNoBlockResponseMessage struct {
- Height int64
- }
-
- type bcBlockResponseMessage struct {
- Block Block
- }
-
- type bcStatusRequestMessage struct {
- Height int64
-
- type bcStatusResponseMessage struct {
- Height int64
- }
- ```
-
- ## Architecture and algorithm
-
- The Blockchain reactor is organised as a set of concurrent tasks:
- - Receive routine of Blockchain Reactor
- - Task for creating Requesters
- - Set of Requesters tasks and
- - Controller task.
-
- ![Blockchain Reactor Architecture Diagram](img/bc-reactor.png)
-
- ### Data structures
-
- These are the core data structures necessarily to provide the Blockchain Reactor logic.
-
- Requester data structure is used to track assignment of request for `block` at position `height` to a
- peer with id equals to `peerID`.
-
- ```go
- type Requester {
- mtx Mutex
- block Block
- height int64
-
peerID p2p.ID
- redoChannel chan struct{}
- }
- ```
- Pool is core data structure that stores last executed block (`height`), assignment of requests to peers (`requesters`),
- current height for each peer and number of pending requests for each peer (`peers`), maximum peer height, etc.
-
- ```go
- type Pool {
- mtx Mutex
- requesters map[int64]*Requester
-
height int64
- peers map[p2p.ID]*Peer
-
maxPeerHeight int64
-
numPending int32
- store BlockStore
-
requestsChannel chan<- BlockRequest
-
errorsChannel chan<- peerError
- }
- ```
-
- Peer data structure stores for each peer current `height` and number of pending requests sent to
- the peer (`numPending`), etc.
-
- ```go
- type Peer struct {
- id p2p.ID
- height int64
- numPending int32
- timeout *time.Timer
- didTimeout bool
- }
- ```
-
- BlockRequest is internal data structure used to denote current mapping of request for a block at some `height` to
- a peer (`PeerID`).
-
- ```go
- type BlockRequest {
- Height int64
- PeerID p2p.ID
- }
- ```
-
- ### Receive routine of Blockchain Reactor
-
- It is executed upon message reception on the BlockchainChannel inside p2p receive routine. There is a separate p2p
- receive routine (and therefore receive routine of the Blockchain Reactor) executed for each peer. Note that
- try to send will not block (returns immediately) if outgoing buffer is full.
-
- ```go
- handleMsg(pool, m):
- upon receiving bcBlockRequestMessage m from peer p:
- block = load block for height m.Height from pool.store
- if block != nil then
- try to send BlockResponseMessage(block) to p
- else
- try to send bcNoBlockResponseMessage(m.Height) to p
-
- upon receiving bcBlockResponseMessage m from peer p:
- pool.mtx.Lock()
- requester = pool.requesters[m.Height]
- if requester == nil then
- error("peer sent us a block we didn't expect")
- continue
-
- if requester.block == nil and requester.peerID == p then
- requester.block = m
- pool.numPending -= 1 // atomic decrement
- peer = pool.peers[p]
- if peer != nil then
- peer.numPending--
- if peer.numPending == 0 then
- peer.timeout.Stop()
- // NOTE: we don't send Quit signal to the corresponding requester task!
- else
- trigger peer timeout to expire after peerTimeout
- pool.mtx.Unlock()
-
-
- upon receiving bcStatusRequestMessage m from peer p:
- try to send bcStatusResponseMessage(pool.store.Height)
-
- upon receiving bcStatusResponseMessage m from peer p:
- pool.mtx.Lock()
- peer = pool.peers[p]
- if peer != nil then
- peer.height = m.height
- else
- peer = create new Peer data structure with id = p and height = m.Height
- pool.peers[p] = peer
-
- if m.Height > pool.maxPeerHeight then
- pool.maxPeerHeight = m.Height
- pool.mtx.Unlock()
-
- onTimeout(p):
- send error message to pool error channel
- peer = pool.peers[p]
- peer.didTimeout = true
- ```
-
- ### Requester tasks
-
- Requester task is responsible for fetching a single block at position `height`.
-
- ```go
- fetchBlock(height, pool):
- while true do
- peerID = nil
- block = nil
- peer = pickAvailablePeer(height)
- peerId = peer.id
-
- enqueue BlockRequest(height, peerID) to pool.requestsChannel
- redo = false
- while !redo do
- select {
- upon receiving Quit message do
- return
- upon receiving message on redoChannel do
- mtx.Lock()
- pool.numPending++
- redo = true
- mtx.UnLock()
- }
-
- pickAvailablePeer(height):
- selectedPeer = nil
- while selectedPeer = nil do
- pool.mtx.Lock()
- for each peer in pool.peers do
- if !peer.didTimeout and peer.numPending < maxPendingRequestsPerPeer and peer.height >= height then
- peer.numPending++
- selectedPeer = peer
- break
- pool.mtx.Unlock()
-
- if selectedPeer = nil then
- sleep requestIntervalMS
-
- return selectedPeer
- ```
- sleep for requestIntervalMS
- ### Task for creating Requesters
-
- This task is responsible for continuously creating and starting Requester tasks.
- ```go
- createRequesters(pool):
- while true do
- if !pool.isRunning then break
- if pool.numPending < maxPendingRequests or size(pool.requesters) < maxTotalRequesters then
- pool.mtx.Lock()
- nextHeight = pool.height + size(pool.requesters)
- requester = create new requester for height nextHeight
- pool.requesters[nextHeight] = requester
- pool.numPending += 1 // atomic increment
- start requester task
- pool.mtx.Unlock()
- else
- sleep requestIntervalMS
- pool.mtx.Lock()
- for each peer in pool.peers do
- if !peer.didTimeout && peer.numPending > 0 && peer.curRate < minRecvRate then
- send error on pool error channel
- peer.didTimeout = true
- if peer.didTimeout then
- for each requester in pool.requesters do
- if requester.getPeerID() == peer then
- enqueue msg on requestor's redoChannel
- delete(pool.peers, peerID)
- pool.mtx.Unlock()
- ```
-
-
- ### Main blockchain reactor controller task
- ```go
- main(pool):
- create trySyncTicker with interval trySyncIntervalMS
- create statusUpdateTicker with interval statusUpdateIntervalSeconds
- create switchToConsensusTicker with interbal switchToConsensusIntervalSeconds
-
- while true do
- select {
- upon receiving BlockRequest(Height, Peer) on pool.requestsChannel:
- try to send bcBlockRequestMessage(Height) to Peer
-
- upon receiving error(peer) on errorsChannel:
- stop peer for error
-
- upon receiving message on statusUpdateTickerChannel:
- broadcast bcStatusRequestMessage(bcR.store.Height) // message sent in a separate routine
-
- upon receiving message on switchToConsensusTickerChannel:
- pool.mtx.Lock()
- receivedBlockOrTimedOut = pool.height > 0 || (time.Now() - pool.startTime) > 5 Seconds
- ourChainIsLongestAmongPeers = pool.maxPeerHeight == 0 || pool.height >= pool.maxPeerHeight
- haveSomePeers = size of pool.peers > 0
- pool.mtx.Unlock()
- if haveSomePeers && receivedBlockOrTimedOut && ourChainIsLongestAmongPeers then
- switch to consensus mode
-
- upon receiving message on trySyncTickerChannel:
- for i = 0; i < 10; i++ do
- pool.mtx.Lock()
- firstBlock = pool.requesters[pool.height].block
- secondBlock = pool.requesters[pool.height].block
- if firstBlock == nil or secondBlock == nil then continue
- pool.mtx.Unlock()
- verify firstBlock using LastCommit from secondBlock
- if verification failed
- pool.mtx.Lock()
- peerID = pool.requesters[pool.height].peerID
- redoRequestsForPeer(peerId)
- delete(pool.peers, peerID)
- stop peer peerID for error
- pool.mtx.Unlock()
- else
- delete(pool.requesters, pool.height)
- save firstBlock to store
- pool.height++
- execute firstBlock
- }
-
- redoRequestsForPeer(pool, peerId):
- for each requester in pool.requesters do
- if requester.getPeerID() == peerID
- enqueue msg on redoChannel for requester
- ```
-
- ## Channels
-
- Defines `maxMsgSize` for the maximum size of incoming messages,
- `SendQueueCapacity` and `RecvBufferCapacity` for maximum sending and
- receiving buffers respectively. These are supposed to prevent amplification
- attacks by setting up the upper limit on how much data we can receive & send to
- a peer.
-
- Sending incorrectly encoded data will result in stopping the peer.
|