2018-01-21 18:19:38 -05:00
# 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
2018-01-25 05:02:26 +00:00
using (and turns on) the Consensus Reactor.
2018-01-21 18:19:38 -05:00
## 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
}
```
2018-06-13 14:05:17 +02:00
## Architecture and algorithm
2018-01-21 18:19:38 -05:00
2018-08-27 15:33:46 +08:00
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.
2018-05-29 16:49:02 +04:00
2018-06-13 14:05:17 +02:00

### Data structures
These are the core data structures necessarily to provide the Blockchain Reactor logic.
2018-08-27 15:33:46 +08:00
Requester data structure is used to track assignment of request for `block` at position `height` to a peer with id equals to `peerID` .
2018-06-13 14:05:17 +02:00
```go
type Requester {
2018-08-27 15:33:46 +08:00
mtx Mutex
2018-06-13 14:05:17 +02:00
block Block
2018-08-27 15:33:46 +08:00
height int64
2018-11-26 15:31:11 -05:00
peerID p2p.ID
redoChannel chan p2p.ID //redo may send multi-time; peerId is used to identify repeat
2018-06-13 14:05:17 +02:00
}
```
2018-08-27 15:33:46 +08:00
2018-11-26 15:31:11 -05:00
Pool is a 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.
2018-06-13 14:05:17 +02:00
```go
type Pool {
2018-11-26 15:31:11 -05:00
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
2018-06-13 14:05:17 +02:00
}
```
2018-08-27 15:33:46 +08:00
Peer data structure stores for each peer current `height` and number of pending requests sent to the peer (`numPending` ), etc.
2018-06-13 14:05:17 +02:00
```go
type Peer struct {
2018-11-26 15:31:11 -05:00
id p2p.ID
height int64
numPending int32
timeout *time.Timer
didTimeout bool
2018-06-13 14:05:17 +02:00
}
```
2018-08-27 15:33:46 +08:00
BlockRequest is internal data structure used to denote current mapping of request for a block at some `height` to a peer (`PeerID` ).
2018-06-13 14:05:17 +02:00
```go
type BlockRequest {
Height int64
2018-08-27 15:33:46 +08:00
PeerID p2p.ID
2018-06-13 14:05:17 +02:00
}
```
### Receive routine of Blockchain Reactor
2018-08-27 15:33:46 +08:00
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.
2018-06-13 14:05:17 +02:00
```go
handleMsg(pool, m):
upon receiving bcBlockRequestMessage m from peer p:
2018-08-27 15:33:46 +08:00
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
2018-06-13 14:05:17 +02:00
requester.block = m
2018-08-27 15:33:46 +08:00
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()
2018-06-13 14:05:17 +02:00
upon receiving bcStatusRequestMessage m from peer p:
2018-08-27 15:33:46 +08:00
try to send bcStatusResponseMessage(pool.store.Height)
2018-06-13 14:05:17 +02:00
upon receiving bcStatusResponseMessage m from peer p:
2018-08-27 15:33:46 +08:00
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()
2018-06-13 14:05:17 +02:00
onTimeout(p):
2018-08-27 15:33:46 +08:00
send error message to pool error channel
peer = pool.peers[p]
peer.didTimeout = true
2018-06-13 14:05:17 +02:00
```
### Requester tasks
2018-08-27 15:33:46 +08:00
Requester task is responsible for fetching a single block at position `height` .
2018-06-13 14:05:17 +02:00
```go
fetchBlock(height, pool):
2018-11-26 15:31:11 -05:00
while true do {
2018-08-27 15:33:46 +08:00
peerID = nil
2018-06-13 14:05:17 +02:00
block = nil
2018-08-27 15:33:46 +08:00
peer = pickAvailablePeer(height)
2018-11-26 15:31:11 -05:00
peerID = peer.id
2018-06-13 14:05:17 +02:00
enqueue BlockRequest(height, peerID) to pool.requestsChannel
2018-08-27 15:33:46 +08:00
redo = false
while !redo do
select {
2018-06-13 14:05:17 +02:00
upon receiving Quit message do
2018-08-27 15:33:46 +08:00
return
2018-11-26 15:31:11 -05:00
upon receiving redo message with id on redoChannel do
if peerID == id {
mtx.Lock()
pool.numPending++
redo = true
mtx.UnLock()
}
2018-08-27 15:33:46 +08:00
}
2018-11-26 15:31:11 -05:00
}
2018-06-13 14:05:17 +02:00
pickAvailablePeer(height):
2018-08-27 15:33:46 +08:00
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
2018-06-13 14:05:17 +02:00
```
2018-08-27 15:33:46 +08:00
2018-06-13 14:05:17 +02:00
sleep for requestIntervalMS
2018-08-27 15:33:46 +08:00
2018-06-13 14:05:17 +02:00
### Task for creating Requesters
This task is responsible for continuously creating and starting Requester tasks.
2018-08-27 15:33:46 +08:00
2018-06-13 14:05:17 +02:00
```go
createRequesters(pool):
2018-08-27 15:33:46 +08:00
while true do
if !pool.isRunning then break
if pool.numPending < maxPendingRequests or size ( pool . requesters ) < maxTotalRequesters then
2018-06-13 14:05:17 +02:00
pool.mtx.Lock()
nextHeight = pool.height + size(pool.requesters)
2018-08-27 15:33:46 +08:00
requester = create new requester for height nextHeight
pool.requesters[nextHeight] = requester
pool.numPending += 1 // atomic increment
start requester task
pool.mtx.Unlock()
else
2018-06-13 14:05:17 +02:00
sleep requestIntervalMS
2018-08-27 15:33:46 +08:00
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
2018-06-13 14:05:17 +02:00
peer.didTimeout = true
2018-08-27 15:33:46 +08:00
if peer.didTimeout then
for each requester in pool.requesters do
if requester.getPeerID() == peer then
2018-06-13 14:05:17 +02:00
enqueue msg on requestor's redoChannel
2018-08-27 15:33:46 +08:00
delete(pool.peers, peerID)
pool.mtx.Unlock()
2018-06-13 14:05:17 +02:00
```
2018-08-27 15:33:46 +08:00
### Main blockchain reactor controller task
2018-06-13 14:05:17 +02:00
```go
main(pool):
2018-08-27 15:33:46 +08:00
create trySyncTicker with interval trySyncIntervalMS
create statusUpdateTicker with interval statusUpdateIntervalSeconds
2018-11-26 15:31:11 -05:00
create switchToConsensusTicker with interval switchToConsensusIntervalSeconds
2018-08-27 15:33:46 +08:00
while true do
select {
2018-06-13 14:05:17 +02:00
upon receiving BlockRequest(Height, Peer) on pool.requestsChannel:
2018-08-27 15:33:46 +08:00
try to send bcBlockRequestMessage(Height) to Peer
2018-06-13 14:05:17 +02:00
upon receiving error(peer) on errorsChannel:
2018-08-27 15:33:46 +08:00
stop peer for error
2018-06-13 14:05:17 +02:00
upon receiving message on statusUpdateTickerChannel:
2018-08-27 15:33:46 +08:00
broadcast bcStatusRequestMessage(bcR.store.Height) // message sent in a separate routine
2018-06-13 14:05:17 +02:00
upon receiving message on switchToConsensusTickerChannel:
2018-08-27 15:33:46 +08:00
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
2018-06-13 14:05:17 +02:00
pool.mtx.Unlock()
if haveSomePeers & & receivedBlockOrTimedOut & & ourChainIsLongestAmongPeers then
2018-08-27 15:33:46 +08:00
switch to consensus mode
2018-06-13 14:05:17 +02:00
upon receiving message on trySyncTickerChannel:
2018-08-27 15:33:46 +08:00
for i = 0; i < 10 ; i + + do
pool.mtx.Lock()
2018-06-13 14:05:17 +02:00
firstBlock = pool.requesters[pool.height].block
secondBlock = pool.requesters[pool.height].block
if firstBlock == nil or secondBlock == nil then continue
pool.mtx.Unlock()
2018-08-27 15:33:46 +08:00
verify firstBlock using LastCommit from secondBlock
if verification failed
pool.mtx.Lock()
2018-06-13 14:05:17 +02:00
peerID = pool.requesters[pool.height].peerID
redoRequestsForPeer(peerId)
delete(pool.peers, peerID)
2018-08-27 15:33:46 +08:00
stop peer peerID for error
pool.mtx.Unlock()
else
2018-06-13 14:05:17 +02:00
delete(pool.requesters, pool.height)
save firstBlock to store
2018-08-27 15:33:46 +08:00
pool.height++
execute firstBlock
2018-06-13 14:05:17 +02:00
}
2018-08-27 15:33:46 +08:00
2018-06-13 14:05:17 +02:00
redoRequestsForPeer(pool, peerId):
2018-08-27 15:33:46 +08:00
for each requester in pool.requesters do
if requester.getPeerID() == peerID
enqueue msg on redoChannel for requester
2018-06-13 14:05:17 +02:00
```
2018-08-27 15:33:46 +08:00
2018-05-29 16:49:02 +04:00
## 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.