mirror of
https://github.com/fluencelabs/tendermint
synced 2025-07-31 20:21:56 +00:00
Revert "delete everything" (includes everything non-go-crypto)
This reverts commit 96a3502
This commit is contained in:
BIN
docs/spec/reactors/block_sync/img/bc-reactor.png
Normal file
BIN
docs/spec/reactors/block_sync/img/bc-reactor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
46
docs/spec/reactors/block_sync/impl.md
Normal file
46
docs/spec/reactors/block_sync/impl.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Blockchain Reactor
|
||||
|
||||
* coordinates the pool for syncing
|
||||
* coordinates the store for persistence
|
||||
* coordinates the playing of blocks towards the app using a sm.BlockExecutor
|
||||
* handles switching between fastsync and consensus
|
||||
* it is a p2p.BaseReactor
|
||||
* starts the pool.Start() and its poolRoutine()
|
||||
* registers all the concrete types and interfaces for serialisation
|
||||
|
||||
### poolRoutine
|
||||
|
||||
* listens to these channels:
|
||||
* pool requests blocks from a specific peer by posting to requestsCh, block reactor then sends
|
||||
a &bcBlockRequestMessage for a specific height
|
||||
* pool signals timeout of a specific peer by posting to timeoutsCh
|
||||
* switchToConsensusTicker to periodically try and switch to consensus
|
||||
* trySyncTicker to periodically check if we have fallen behind and then catch-up sync
|
||||
* if there aren't any new blocks available on the pool it skips syncing
|
||||
* tries to sync the app by taking downloaded blocks from the pool, gives them to the app and stores
|
||||
them on disk
|
||||
* implements Receive which is called by the switch/peer
|
||||
* calls AddBlock on the pool when it receives a new block from a peer
|
||||
|
||||
## Block Pool
|
||||
|
||||
* responsible for downloading blocks from peers
|
||||
* makeRequestersRoutine()
|
||||
* removes timeout peers
|
||||
* starts new requesters by calling makeNextRequester()
|
||||
* requestRoutine():
|
||||
* picks a peer and sends the request, then blocks until:
|
||||
* pool is stopped by listening to pool.Quit
|
||||
* requester is stopped by listening to Quit
|
||||
* request is redone
|
||||
* we receive a block
|
||||
* gotBlockCh is strange
|
||||
|
||||
## Block Store
|
||||
|
||||
* persists blocks to disk
|
||||
|
||||
# TODO
|
||||
|
||||
* How does the switch from bcR to conR happen? Does conR persist blocks to disk too?
|
||||
* What is the interaction between the consensus and blockchain reactors?
|
307
docs/spec/reactors/block_sync/reactor.md
Normal file
307
docs/spec/reactors/block_sync/reactor.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
Reference in New Issue
Block a user