You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

534 lines
20 KiB

  1. # ADR 040: Blockchain Reactor Refactor
  2. ## Changelog
  3. 19-03-2019: Initial draft
  4. ## Context
  5. The Blockchain Reactor's high level responsibility is to enable peers who are far behind the current state of the
  6. blockchain to quickly catch up by downloading many blocks in parallel from its peers, verifying block correctness, and
  7. executing them against the ABCI application. We call the protocol executed by the Blockchain Reactor `fast-sync`.
  8. The current architecture diagram of the blockchain reactor can be found here:
  9. ![Blockchain Reactor Architecture Diagram](img/bc-reactor.png)
  10. The current architecture consists of dozens of routines and it is tightly depending on the `Switch`, making writing
  11. unit tests almost impossible. Current tests require setting up complex dependency graphs and dealing with concurrency.
  12. Note that having dozens of routines is in this case overkill as most of the time routines sits idle waiting for
  13. something to happen (message to arrive or timeout to expire). Due to dependency on the `Switch`, testing relatively
  14. complex network scenarios and failures (for example adding and removing peers) is very complex tasks and frequently lead
  15. to complex tests with not deterministic behavior ([#3400]). Impossibility to write proper tests makes confidence in
  16. the code low and this resulted in several issues (some are fixed in the meantime and some are still open):
  17. [#3400], [#2897], [#2896], [#2699], [#2888], [#2457], [#2622], [#2026].
  18. ## Decision
  19. To remedy these issues we plan a major refactor of the blockchain reactor. The proposed architecture is largely inspired
  20. by ADR-30 and is presented on the following diagram:
  21. ![Blockchain Reactor Refactor Diagram](img/bc-reactor-refactor.png)
  22. We suggest a concurrency architecture where the core algorithm (we call it `Controller`) is extracted into a finite
  23. state machine. The active routine of the reactor is called `Executor` and is responsible for receiving and sending
  24. messages from/to peers and triggering timeouts. What messages should be sent and timeouts triggered is determined mostly
  25. by the `Controller`. The exception is `Peer Heartbeat` mechanism which is `Executor` responsibility. The heartbeat
  26. mechanism is used to remove slow and unresponsive peers from the peer list. Writing of unit tests is simpler with
  27. this architecture as most of the critical logic is part of the `Controller` function. We expect that simpler concurrency
  28. architecture will not have significant negative effect on the performance of this reactor (to be confirmed by
  29. experimental evaluation).
  30. ### Implementation changes
  31. We assume the following system model for "fast sync" protocol:
  32. * a node is connected to a random subset of all nodes that represents its peer set. Some nodes are correct and some
  33. might be faulty. We don't make assumptions about ratio of faulty nodes, i.e., it is possible that all nodes in some
  34. peer set are faulty.
  35. * we assume that communication between correct nodes is synchronous, i.e., if a correct node `p` sends a message `m` to
  36. a correct node `q` at time `t`, then `q` will receive message the latest at time `t+Delta` where `Delta` is a system
  37. parameter that is known by network participants. `Delta` is normally chosen to be an order of magnitude higher than
  38. the real communication delay (maximum) between correct nodes. Therefore if a correct node `p` sends a request message
  39. to a correct node `q` at time `t` and there is no the corresponding reply at time `t + 2*Delta`, then `p` can assume
  40. that `q` is faulty. Note that the network assumptions for the consensus reactor are different (we assume partially
  41. synchronous model there).
  42. The requirements for the "fast sync" protocol are formally specified as follows:
  43. - `Correctness`: If a correct node `p` is connected to a correct node `q` for a long enough period of time, then `p`
  44. - will eventually download all requested blocks from `q`.
  45. - `Termination`: If a set of peers of a correct node `p` is stable (no new nodes are added to the peer set of `p`) for
  46. - a long enough period of time, then protocol eventually terminates.
  47. - `Fairness`: A correct node `p` sends requests for blocks to all peers from its peer set.
  48. As explained above, the `Executor` is responsible for sending and receiving messages that are part of the `fast-sync`
  49. protocol. The following messages are exchanged as part of `fast-sync` protocol:
  50. ``` go
  51. type Message int
  52. const (
  53. MessageUnknown Message = iota
  54. MessageStatusRequest
  55. MessageStatusResponse
  56. MessageBlockRequest
  57. MessageBlockResponse
  58. )
  59. ```
  60. `MessageStatusRequest` is sent periodically to all peers as a request for a peer to provide its current height. It is
  61. part of the `Peer Heartbeat` mechanism and a failure to respond timely to this message results in a peer being removed
  62. from the peer set. Note that the `Peer Heartbeat` mechanism is used only while a peer is in `fast-sync` mode. We assume
  63. here existence of a mechanism that gives node a possibility to inform its peers that it is in the `fast-sync` mode.
  64. ``` go
  65. type MessageStatusRequest struct {
  66. SeqNum int64 // sequence number of the request
  67. }
  68. ```
  69. `MessageStatusResponse` is sent as a response to `MessageStatusRequest` to inform requester about the peer current
  70. height.
  71. ``` go
  72. type MessageStatusResponse struct {
  73. SeqNum int64 // sequence number of the corresponding request
  74. Height int64 // current peer height
  75. }
  76. ```
  77. `MessageBlockRequest` is used to make a request for a block and the corresponding commit certificate at a given height.
  78. ``` go
  79. type MessageBlockRequest struct {
  80. Height int64
  81. }
  82. ```
  83. `MessageBlockResponse` is a response for the corresponding block request. In addition to providing the block and the
  84. corresponding commit certificate, it contains also a current peer height.
  85. ``` go
  86. type MessageBlockResponse struct {
  87. Height int64
  88. Block Block
  89. Commit Commit
  90. PeerHeight int64
  91. }
  92. ```
  93. In addition to sending and receiving messages, and `HeartBeat` mechanism, controller is also managing timeouts
  94. that are triggered upon `Controller` request. `Controller` is then informed once a timeout expires.
  95. ``` go
  96. type TimeoutTrigger int
  97. const (
  98. TimeoutUnknown TimeoutTrigger = iota
  99. TimeoutResponseTrigger
  100. TimeoutTerminationTrigger
  101. )
  102. ```
  103. The `Controller` can be modelled as a function with clearly defined inputs:
  104. * `State` - current state of the node. Contains data about connected peers and its behavior, pending requests,
  105. * received blocks, etc.
  106. * `Event` - significant events in the network.
  107. producing clear outputs:
  108. * `State` - updated state of the node,
  109. * `MessageToSend` - signal what message to send and to which peer
  110. * `TimeoutTrigger` - signal that timeout should be triggered.
  111. We consider the following `Event` types:
  112. ``` go
  113. type Event int
  114. const (
  115. EventUnknown Event = iota
  116. EventStatusReport
  117. EventBlockRequest
  118. EventBlockResponse
  119. EventRemovePeer
  120. EventTimeoutResponse
  121. EventTimeoutTermination
  122. )
  123. ```
  124. `EventStatusResponse` event is generated once `MessageStatusResponse` is received by the `Executor`.
  125. ``` go
  126. type EventStatusReport struct {
  127. PeerID ID
  128. Height int64
  129. }
  130. ```
  131. `EventBlockRequest` event is generated once `MessageBlockRequest` is received by the `Executor`.
  132. ``` go
  133. type EventBlockRequest struct {
  134. Height int64
  135. PeerID p2p.ID
  136. }
  137. ```
  138. `EventBlockResponse` event is generated upon reception of `MessageBlockResponse` message by the `Executor`.
  139. ``` go
  140. type EventBlockResponse struct {
  141. Height int64
  142. Block Block
  143. Commit Commit
  144. PeerID ID
  145. PeerHeight int64
  146. }
  147. ```
  148. `EventRemovePeer` is generated by `Executor` to signal that the connection to a peer is closed due to peer misbehavior.
  149. ``` go
  150. type EventRemovePeer struct {
  151. PeerID ID
  152. }
  153. ```
  154. `EventTimeoutResponse` is generated by `Executor` to signal that a timeout triggered by `TimeoutResponseTrigger` has
  155. expired.
  156. ``` go
  157. type EventTimeoutResponse struct {
  158. PeerID ID
  159. Height int64
  160. }
  161. ```
  162. `EventTimeoutTermination` is generated by `Executor` to signal that a timeout triggered by `TimeoutTerminationTrigger`
  163. has expired.
  164. ``` go
  165. type EventTimeoutTermination struct {
  166. Height int64
  167. }
  168. ```
  169. `MessageToSend` is just a wrapper around `Message` type that contains id of the peer to which message should be sent.
  170. ``` go
  171. type MessageToSend struct {
  172. PeerID ID
  173. Message Message
  174. }
  175. ```
  176. The Controller state machine can be in two modes: `ModeFastSync` when
  177. a node is trying to catch up with the network by downloading committed blocks,
  178. and `ModeConsensus` in which it executes Tendermint consensus protocol. We
  179. consider that `fast sync` mode terminates once the Controller switch to
  180. `ModeConsensus`.
  181. ``` go
  182. type Mode int
  183. const (
  184. ModeUnknown Mode = iota
  185. ModeFastSync
  186. ModeConsensus
  187. )
  188. ```
  189. `Controller` is managing the following state:
  190. ``` go
  191. type ControllerState struct {
  192. Height int64 // the first block that is not committed
  193. Mode Mode // mode of operation
  194. PeerMap map[ID]PeerStats // map of peer IDs to peer statistics
  195. MaxRequestPending int64 // maximum height of the pending requests
  196. FailedRequests []int64 // list of failed block requests
  197. PendingRequestsNum int // total number of pending requests
  198. Store []BlockInfo // contains list of downloaded blocks
  199. Executor BlockExecutor // store, verify and executes blocks
  200. }
  201. ```
  202. `PeerStats` data structure keeps for every peer its current height and a list of pending requests for blocks.
  203. ``` go
  204. type PeerStats struct {
  205. Height int64
  206. PendingRequest int64 // a request sent to this peer
  207. }
  208. ```
  209. `BlockInfo` data structure is used to store information (as part of block store) about downloaded blocks: from what peer
  210. a block and the corresponding commit certificate are received.
  211. ``` go
  212. type BlockInfo struct {
  213. Block Block
  214. Commit Commit
  215. PeerID ID // a peer from which we received the corresponding Block and Commit
  216. }
  217. ```
  218. The `Controller` is initialized by providing an initial height (`startHeight`) from which it will start downloading
  219. blocks from peers and the current state of the `BlockExecutor`.
  220. ``` go
  221. func NewControllerState(startHeight int64, executor BlockExecutor) ControllerState {
  222. state = ControllerState {}
  223. state.Height = startHeight
  224. state.Mode = ModeFastSync
  225. state.MaxRequestPending = startHeight - 1
  226. state.PendingRequestsNum = 0
  227. state.Executor = executor
  228. initialize state.PeerMap, state.FailedRequests and state.Store to empty data structures
  229. return state
  230. }
  231. ```
  232. The core protocol logic is given with the following function:
  233. ``` go
  234. func handleEvent(state ControllerState, event Event) (ControllerState, Message, TimeoutTrigger, Error) {
  235. msg = nil
  236. timeout = nil
  237. error = nil
  238. switch state.Mode {
  239. case ModeConsensus:
  240. switch event := event.(type) {
  241. case EventBlockRequest:
  242. msg = createBlockResponseMessage(state, event)
  243. return state, msg, timeout, error
  244. default:
  245. error = "Only respond to BlockRequests while in ModeConsensus!"
  246. return state, msg, timeout, error
  247. }
  248. case ModeFastSync:
  249. switch event := event.(type) {
  250. case EventBlockRequest:
  251. msg = createBlockResponseMessage(state, event)
  252. return state, msg, timeout, error
  253. case EventStatusResponse:
  254. return handleEventStatusResponse(event, state)
  255. case EventRemovePeer:
  256. return handleEventRemovePeer(event, state)
  257. case EventBlockResponse:
  258. return handleEventBlockResponse(event, state)
  259. case EventResponseTimeout:
  260. return handleEventResponseTimeout(event, state)
  261. case EventTerminationTimeout:
  262. // Termination timeout is triggered in case of empty peer set and in case there are no pending requests.
  263. // If this timeout expires and in the meantime no new peers are added or new pending requests are made
  264. // then `fast-sync` mode terminates by switching to `ModeConsensus`.
  265. // Note that termination timeout should be higher than the response timeout.
  266. if state.Height == event.Height && state.PendingRequestsNum == 0 { state.State = ConsensusMode }
  267. return state, msg, timeout, error
  268. default:
  269. error = "Received unknown event type!"
  270. return state, msg, timeout, error
  271. }
  272. }
  273. }
  274. ```
  275. ``` go
  276. func createBlockResponseMessage(state ControllerState, event BlockRequest) MessageToSend {
  277. msgToSend = nil
  278. if _, ok := state.PeerMap[event.PeerID]; !ok { peerStats = PeerStats{-1, -1} }
  279. if state.Executor.ContainsBlockWithHeight(event.Height) && event.Height > peerStats.Height {
  280. peerStats = event.Height
  281. msg = BlockResponseMessage{
  282. Height: event.Height,
  283. Block: state.Executor.getBlock(eventHeight),
  284. Commit: state.Executor.getCommit(eventHeight),
  285. PeerID: event.PeerID,
  286. CurrentHeight: state.Height - 1,
  287. }
  288. msgToSend = MessageToSend { event.PeerID, msg }
  289. }
  290. state.PeerMap[event.PeerID] = peerStats
  291. return msgToSend
  292. }
  293. ```
  294. ``` go
  295. func handleEventStatusResponse(event EventStatusResponse, state ControllerState) (ControllerState, MessageToSend, TimeoutTrigger, Error) {
  296. if _, ok := state.PeerMap[event.PeerID]; !ok {
  297. peerStats = PeerStats{ -1, -1 }
  298. } else {
  299. peerStats = state.PeerMap[event.PeerID]
  300. }
  301. if event.Height > peerStats.Height { peerStats.Height = event.Height }
  302. // if there are no pending requests for this peer, try to send him a request for block
  303. if peerStats.PendingRequest == -1 {
  304. msg = createBlockRequestMessages(state, event.PeerID, peerStats.Height)
  305. // msg is nil if no request for block can be made to a peer at this point in time
  306. if msg != nil {
  307. peerStats.PendingRequests = msg.Height
  308. state.PendingRequestsNum++
  309. // when a request for a block is sent to a peer, a response timeout is triggered. If no corresponding block is sent by the peer
  310. // during response timeout period, then the peer is considered faulty and is removed from the peer set.
  311. timeout = ResponseTimeoutTrigger{ msg.PeerID, msg.Height, PeerTimeout }
  312. } else if state.PendingRequestsNum == 0 {
  313. // if there are no pending requests and no new request can be placed to the peer, termination timeout is triggered.
  314. // If termination timeout expires and we are still at the same height and there are no pending requests, the "fast-sync"
  315. // mode is finished and we switch to `ModeConsensus`.
  316. timeout = TerminationTimeoutTrigger{ state.Height, TerminationTimeout }
  317. }
  318. }
  319. state.PeerMap[event.PeerID] = peerStats
  320. return state, msg, timeout, error
  321. }
  322. ```
  323. ``` go
  324. func handleEventRemovePeer(event EventRemovePeer, state ControllerState) (ControllerState, MessageToSend, TimeoutTrigger, Error) {
  325. if _, ok := state.PeerMap[event.PeerID]; ok {
  326. pendingRequest = state.PeerMap[event.PeerID].PendingRequest
  327. // if a peer is removed from the peer set, its pending request is declared failed and added to the `FailedRequests` list
  328. // so it can be retried.
  329. if pendingRequest != -1 {
  330. add(state.FailedRequests, pendingRequest)
  331. }
  332. state.PendingRequestsNum--
  333. delete(state.PeerMap, event.PeerID)
  334. // if the peer set is empty after removal of this peer then termination timeout is triggered.
  335. if state.PeerMap.isEmpty() {
  336. timeout = TerminationTimeoutTrigger{ state.Height, TerminationTimeout }
  337. }
  338. } else { error = "Removing unknown peer!" }
  339. return state, msg, timeout, error
  340. ```
  341. ``` go
  342. func handleEventBlockResponse(event EventBlockResponse, state ControllerState) (ControllerState, MessageToSend, TimeoutTrigger, Error)
  343. if state.PeerMap[event.PeerID] {
  344. peerStats = state.PeerMap[event.PeerID]
  345. // when expected block arrives from a peer, it is added to the store so it can be verified and if correct executed after.
  346. if peerStats.PendingRequest == event.Height {
  347. peerStats.PendingRequest = -1
  348. state.PendingRequestsNum--
  349. if event.PeerHeight > peerStats.Height { peerStats.Height = event.PeerHeight }
  350. state.Store[event.Height] = BlockInfo{ event.Block, event.Commit, event.PeerID }
  351. // blocks are verified sequentially so adding a block to the store does not mean that it will be immediately verified
  352. // as some of the previous blocks might be missing.
  353. state = verifyBlocks(state) // it can lead to event.PeerID being removed from peer list
  354. if _, ok := state.PeerMap[event.PeerID]; ok {
  355. // we try to identify new request for a block that can be asked to the peer
  356. msg = createBlockRequestMessage(state, event.PeerID, peerStats.Height)
  357. if msg != nil {
  358. peerStats.PendingRequests = msg.Height
  359. state.PendingRequestsNum++
  360. // if request for block is made, response timeout is triggered
  361. timeout = ResponseTimeoutTrigger{ msg.PeerID, msg.Height, PeerTimeout }
  362. } else if state.PeerMap.isEmpty() || state.PendingRequestsNum == 0 {
  363. // if the peer map is empty (the peer can be removed as block verification failed) or there are no pending requests
  364. // termination timeout is triggered.
  365. timeout = TerminationTimeoutTrigger{ state.Height, TerminationTimeout }
  366. }
  367. }
  368. } else { error = "Received Block from wrong peer!" }
  369. } else { error = "Received Block from unknown peer!" }
  370. state.PeerMap[event.PeerID] = peerStats
  371. return state, msg, timeout, error
  372. }
  373. ```
  374. ``` go
  375. func handleEventResponseTimeout(event, state) {
  376. if _, ok := state.PeerMap[event.PeerID]; ok {
  377. peerStats = state.PeerMap[event.PeerID]
  378. // if a response timeout expires and the peer hasn't delivered the block, the peer is removed from the peer list and
  379. // the request is added to the `FailedRequests` so the block can be downloaded from other peer
  380. if peerStats.PendingRequest == event.Height {
  381. add(state.FailedRequests, pendingRequest)
  382. delete(state.PeerMap, event.PeerID)
  383. state.PendingRequestsNum--
  384. // if peer set is empty, then termination timeout is triggered
  385. if state.PeerMap.isEmpty() {
  386. timeout = TimeoutTrigger{ state.Height, TerminationTimeout }
  387. }
  388. }
  389. }
  390. return state, msg, timeout, error
  391. }
  392. ```
  393. ``` go
  394. func createBlockRequestMessage(state ControllerState, peerID ID, peerHeight int64) MessageToSend {
  395. msg = nil
  396. blockHeight = -1
  397. r = find request in state.FailedRequests such that r <= peerHeight // returns `nil` if there are no such request
  398. // if there is a height in failed requests that can be downloaded from the peer send request to it
  399. if r != nil {
  400. blockNumber = r
  401. delete(state.FailedRequests, r)
  402. } else if state.MaxRequestPending < peerHeight {
  403. // if height of the maximum pending request is smaller than peer height, then ask peer for next block
  404. state.MaxRequestPending++
  405. blockHeight = state.MaxRequestPending // increment state.MaxRequestPending and then return the new value
  406. }
  407. if blockHeight > -1 { msg = MessageToSend { peerID, MessageBlockRequest { blockHeight } }
  408. return msg
  409. }
  410. ```
  411. ``` go
  412. func verifyBlocks(state State) State {
  413. done = false
  414. for !done {
  415. block = state.Store[height]
  416. if block != nil {
  417. verified = verify block.Block using block.Commit // return `true` is verification succeed, 'false` otherwise
  418. if verified {
  419. block.Execute() // executing block is costly operation so it might make sense executing asynchronously
  420. state.Height++
  421. } else {
  422. // if block verification failed, then it is added to `FailedRequests` and the peer is removed from the peer set
  423. add(state.FailedRequests, height)
  424. state.Store[height] = nil
  425. if _, ok := state.PeerMap[block.PeerID]; ok {
  426. pendingRequest = state.PeerMap[block.PeerID].PendingRequest
  427. // if there is a pending request sent to the peer that is just to be removed from the peer set, add it to `FailedRequests`
  428. if pendingRequest != -1 {
  429. add(state.FailedRequests, pendingRequest)
  430. state.PendingRequestsNum--
  431. }
  432. delete(state.PeerMap, event.PeerID)
  433. }
  434. done = true
  435. }
  436. } else { done = true }
  437. }
  438. return state
  439. }
  440. ```
  441. In the proposed architecture `Controller` is not active task, i.e., it is being called by the `Executor`. Depending on
  442. the return values returned by `Controller`,`Executor` will send a message to some peer (`msg` != nil), trigger a
  443. timeout (`timeout` != nil) or deal with errors (`error` != nil).
  444. In case a timeout is triggered, it will provide as an input to `Controller` the corresponding timeout event once
  445. timeout expires.
  446. ## Status
  447. Draft.
  448. ## Consequences
  449. ### Positive
  450. - isolated implementation of the algorithm
  451. - improved testability - simpler to prove correctness
  452. - clearer separation of concerns - easier to reason
  453. ### Negative
  454. ### Neutral