Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ChainSync

Mini-protocol number: 2

ChainSync is the miniprotocol used to transmit chains of headers. It is a pull-based miniprotocol: data is transmitted only upon explicit request from the client.

The purpose of ChainSync is to enable the client to acquire and validate the headers of the server’s selected chain, and if it is better than the client’s current selection, direct BlockFetch to download the corresponding blocks.

tip

There usually is one ChainSync client per-peer connected to the node, such that the chain state of each peer is tracked independently.

The connection is abruptly terminated if the peer misbehaves. In particular, actions considered as misbehaviour are (not exclusively):

  • The peer violates the state machine of the protocol,
  • The server sends an invalid header,
  • The server announces a fork that is more than k blocks deep from the client’s current selection.

warning

TODO: Make this list exhaustive

State machine

The state machine for ChainSync is as follows:

stateDiagram
    direction LR
    [*] --> StIdle
    StIdle --> [*]: MsgDone
    StIdle --> StIntersect: MsgFindIntersect
    StIdle --> StCanAwait: MsgRequestNext
    StIntersect --> StIdle: MsgIntersectNotFound
    StIntersect --> StIdle: MsgIntersectFound
    StCanAwait --> StIdle: MsgRollForward
    StCanAwait --> StIdle: MsgRollBackward
    StCanAwait --> StMustReply: MsgAwaitReply
    StMustReply --> StIdle: MsgRollForward
    StMustReply --> StIdle: MsgRollBackward

    classDef initiator color:#080
    classDef responder color:#008, text-decoration: underline
    class StIdle initiator
    class StCanAwait responder
    class StIntersect responder
    class StMustReply responder

State agencies

StateAgency
StIdleInitiator
StIntersectResponder
StCanAwaitResponder
StMustReplyResponder

State transitions

From stateMessageParametersTo state
StIdleMsgRequestNextStCanAwait
StIdleMsgFindIntersect[point]StIntersect
StIdleMsgDoneEnd
StCanAwaitMsgAwaitReplyStMustReply
StCanAwaitMsgRollForwardheader, tipStIdle
StCanAwaitMsgRollBackwardpoint_old, tipStIdle
StMustReplyMsgRollForwardheader, tipStIdle
StMustReplyMsgRollBackwardpoint_old, tipStIdle
StIntersectMsgIntersectFoundpoint_intersect, tipStIdle
StIntersectMsgIntersectNotFoundtipStIdle

ChainSync pipelining or pipelined diffusion

Not to be confused with protocol pipelining. The original design of ChainSync was extended with pipelining capabilities: a server can transmit a tentative header on top of the selected chain, and the invalidity of such header (or the associated body) will not cause the connection to terminate. If the client wants (by considering such header as the best known chain) it can request the body of the block via BlockFetch as usually done for any block.

This optimization is used to shorten the time it takes to diffuse chains on the network, as otherwise nodes would only announce blocks after they validated it, causing each hop through the network to be bottle-necked by validation times.

warning

TODO: expand pipelining explanation, possibly with diagrams

There are some important considerations to take into account regarding pipelined diffusion:

  • Nodes can pipeline only one header which must be on top of its current selection,
  • If the server then validates the pipelined block and finds out it was invalid, it is encouraged to announce it promptly to its clients.

warning

TODO: Are these hard requirements?

More information can be found here.

Access pattern of ChainSync

ChainSync involves potentially serving the whole chain, both the immutable part and the volatile part (the current node’s selection). As the current selection is bound to be rolled back, the ChainSync protocol has capabilities for announcing such rollbacks to clients and following rollbacks of servers.

  • For the immutable part of the chain: ChainSync accesses blocks in a sequential manner, a simple iterator over such chain would suffice.
  • For the volatile part of the chain: ChainSync accesses the current selection in a sequential manner but such selection is bound to change if a new chain is selected. The abstraction used to implement the access to the selection must be able to follow such rollbacks.
  • Blocks that become immutable usually are written to the disk as they are not used for following the current chain once the node is caught up, following the description in the k security parameter section. The implementation of ChainSync should be able to identify this situation, as blocks might be gone from the volatile part of the chain as the selection advances. This does not need to be made explicit for clients but it has to be taken into account on the implementation of the server.

Codecs

The messages depicted in the state machine follow this CDDL specification:

;; messages.cddl
chainSyncMessage
    = msgRequestNext
    / msgAwaitReply
    / msgRollForward
    / msgRollBackward
    / msgFindIntersect
    / msgIntersectFound
    / msgIntersectNotFound
    / chainSyncMsgDone

msgRequestNext         = [0]
msgAwaitReply          = [1]
msgRollForward         = [2, header.header, tip]
msgRollBackward        = [3, point, tip]
msgFindIntersect       = [4, [* point]]
msgIntersectFound      = [5, point, tip]
msgIntersectNotFound   = [6, tip]
chainSyncMsgDone       = [7]

tip = [ point, base.blockNo ]

point = []                         ; the genesis point
      / [ base.slotNo, base.hash ]

;# import base as base
;# import header as header

The header is a tag-encoded value that contains CBOR-in-CBOR headers for the particular era:

;; header.cddl
header
 = base.ns7<byronHeader,
            serialisedShelleyHeader<shelley.header>,
            serialisedShelleyHeader<allegra.header>,
            serialisedShelleyHeader<mary.header>,
            serialisedShelleyHeader<alonzo.header>,
            serialisedShelleyHeader<babbage.header>,
            serialisedShelleyHeader<conway.header>>

byronHeader = [byronRegularIdx, #6.24(bytes .cbor byron.blockhead)]
            / [byronBoundaryIdx, #6.24(bytes .cbor byron.ebbhead)]

byronBoundaryIdx = [0, base.word32]
byronRegularIdx = [1, base.word32]

serialisedShelleyHeader<era> = #6.24(bytes .cbor era)

;# include byron as byron
;# include shelley as shelley
;# include allegra as allegra
;# include mary as mary
;# include alonzo as alonzo
;# include babbage as babbage
;# include conway as conway
;# import base as base