|
|
- # 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.
|