Introduction

Welcome to the Cardano Blueprint, a project that aims to serve as a knowledge foundation about how the Cardano protocol works. Blueprints are implementation independent assets, diagrams, specifications, test data, etc. that will enable a wide developer audience to understand the protocol and build Cardano components.

Mission

Create technical documents for creating and underpinning a Cardano node implementation, without needing to reverse engineer existing implementations.

graph TB
    COM((Community))
    BP(Blueprints)
    N1[Node Implementation 1]
    N2[Node Implementation 2]
    C[Ecosystem Clients]

    COM --> BP
    BP --> N1
    BP --> N2
    BP --> C

Why it’s needed

The Cardano Node was developed over the last 8+ years at Input Output Group (IO) to become the reference implementation of the Ouroboros consensus protocols, extended UTxO (eUTxO) ledger model and Plutus smart contract language.

All of these things are documented, but the documentation is spread across multiple repositories, in different formats, some in very dense formal methods syntax, and some mixed with implementation details of the Haskell node.

This project aims to produce a ‘greenfield’ set of blueprints for Cardano, which are:

  • Collected together in a single place
  • Expressed in a single, universal format (markdown)
  • Written for ordinary software developers
  • Abstracted from any particular implementation

The audience includes:

  • Developers in the node teams who are new or may need information outside their current area
  • Developers in other teams in IO and external partners, wishing to integrate with the node
  • Developers of future alternative nodes and clients
  • Anyone wanting to understand Cardano at a deeper technical level

Hosting this project on Github means that it can become a community effort with all the usual processes of a good Open Source software project - Pull Requests, reviews, issues, branches, release tags…

What makes a good blueprint

A good blueprint should be:

  • Abstract - it should define protocols and behaviour, not code
  • Accessible - it should be written so any competent software engineer with some knowledge of the field can understand it - think about the level of a typical Internet RFC. It should use diagrams (in Mermaid) to help understanding.
  • Complete - it should contain all the information required to implement the component, not refer to any external source (which may go out of date).
  • Minimal - it should define the required functionality of an implementation and leave implementation details to implementors.
  • Current - it should be kept up to date with any changes - ideally leading and informing them rather than the other way round.

There is more specific guidance on language and format in the Style Guide.

What about Cardano Improvement Proposals (CIPs)?

The Cardano Improvement Proposal (CIP) process is the standard way that new features are proposed, discussed and ratified for the Cardano network, and it does this job well.

We did consider whether these blueprints could be CIPs themselves, but were concerned that the sheer volume of information could overwhelm the technical and personal capacity of the CIP process, particularly in the initial bootstrap phase. Also, the single-layer nature of the CIP documents could be restrictive for the highly-connected tree of documentation we envisage.

That said, this project will of course tightly integrate with the CIP process:

  • CIPs will of course remain the place for new features and discussion

  • After an initial bootstrap phase, when we are retroactively distilling existing knowledge, we intend to build a review and editing process mirroring the CIP one

  • This project may itself be presented as a CIP, given the Cardano community final say over its status

  • The CIP process could include proposed changes to the Blueprints

Network

The network layer of the Cardano protocol handles two aspects of communication:

  • Node-to-node (N2N) - transmission of data between network nodes
  • Node-to-client (N2C) - integration of application clients to a single node
graph LR
    A(Node)
    B(Node)
    C(Node)
    X[Client]
    Y[Client]
    A <-->|N2N| C
    A <-->|N2N| B
    B <-->|N2N| C
    X <-->|N2C| A
    C <-->|N2C| Y

The network protocols consist of a multiplexing layer which carries one or more mini-protocols, according to the type of connection - for example:

graph TB
    HS(Handshake)
    CS(ChainSync)
    BF(BlockFetch)
    Mux[Multiplexing]
    Con([Raw Connection])

    HS <--> Mux
    CS <--> Mux
    BF <--> Mux
    Mux <--> Con

Shared mini-protocols

These protocols are used in both N2N and N2C modes:

Node-to-node mini-protocols

These protocols are only used for node-to-node communication:

  • Block Fetch - for transferring chain blocks between nodes
  • Transaction Submission - for propagating transactions between nodes
  • Keep Alive - for maintaining and measuring timing of the connection
  • Peer Sharing - for exchanging peer information to create the peer-to-peer (P2P) network

Node-to-client mini-protocols

These protocols are only used for node-to-client communication:

Dummy mini-protocols

These protocols are only used for testing and experimentation:

1

ChainSync is shared between N2N and N2C, but shares full blocks in N2C as opposed to just headers in N2N

Network: Multiplexing

The multiplexing layer is a simple binary protocol which runs on top of the raw connection (TCP or local socket) and provides:

  • Multiplexing of multiple mini-protocols1 over a single connection
  • Framing and segmentation of messages within a stream connection
  • Timing information for latency measurement

This shows the arrangement for a typical node-to-node (N2N) connection:

graph LR
  subgraph n1 [Node 1]
    direction LR
    HS1(Handshake)
    CS1(ChainSync)
    BF1(BlockFetch)
    Mux1[Multiplexer]

    HS1 <--> Mux1
    CS1 <--> Mux1
    BF1 <--> Mux1
  end

  subgraph n2 [Node 2]
    direction LR
    Mux2[Multiplexer]
    HS2(Handshake)
    CS2(ChainSync)
    BF2(BlockFetch)

    Mux2 <--> HS2
    Mux2 <--> CS2
    Mux2 <--> BF2
  end

  Mux1 <==> Mux2

Packet format

A multiplexer packet consists of an 8-byte header followed by up to 65535 bytes of payload. Multiple payload segments can be combined to form a full message.

packet-beta
    0-31: "Transmission time"
    32: "M"
    33-47: "Mini-protocol ID"
    48-63: "Payload length N"
    64-95: "Payload (variable length N)"
FieldSizeMeaning
Transmission time32Monotonic time stamp (µsec, lowest 32 bits)
M1Mode: 0 from initiator, 1 = from responder
Mini-protocol ID15Mini-protocol ID (see below)
Payload length16Segment payload length (N) in bytes
PayloadNRaw payload data

All fields are network/big-endian byte order.

warning

How are multi-segment messages delimited? - there is no ‘start of message’ flag or ‘N of M’ counter

1

Although the multiplexer is only used with mini-protocols in Cardano, it’s actually completely agnostic as to data format.

Network: Mini-protocols

The Cardano mini-protocols are a set of protocols that each provides a particular aspect of the communication between nodes (node-to-node or N2N) or between a node and an application client (node-to-client or N2C). They run over a multiplexer which allows multiple mini-protocols to share the same underlying TCP or local socket connection.

Each mini-protocol is represented by a state machine and a set of messages that can be passed between the parties.

State machines

The progress of the communication is defined by a state machine, which is replicated at each end. The transitions of the state machine are messages being sent/received. As well as defining which messages are valid to send and receive in each state, the state machine also defines which side has agency - that is, should be the one to send the next message.

The initiator or a connection is the one that requested the connection be opened - the client in a simple client/server model.

The responder or is the one that responds to the connection request - the server, in other words.

In every case it is the initiator (client) which has agency first. In many cases the initiator and responder take turns to have agency (send messages), but in some cases where one party must wait for a response, the other will keep agency and send a follow-up message later.

We can draw this state machine in the standard way, with circles and arrows, but with the addition of an indicator of which side has agency. This one is for the minimal example mini-protocol, Ping Pong:

stateDiagram

    [*] --> StIdle
    StIdle --> StBusy: MsgPing
    StBusy --> StIdle: MsgPong
    StIdle --> [*]: MsgDone

    direction LR

    classDef initiator color:#080
    classDef responder color:#008, text-decoration: underline
    class StIdle initiator
    class StBusy responder

It has been the convention to mark states where the initiator has agency in green and the responder in blue, as here, but we also underline it for responder agency in case colours aren’t clear.

As a double check, we can show the agency for each state as a table as well:

StateAgency
StIdleInitiator
StBusyResponder

By convention state names have an St prefix, while messages have Msg, to avoid confusion.

We can also show the transitions of the state machine as a table, and indicate what data is passed with each message, although Ping Pong doesn’t carry any:

From stateMessageParametersto state
StIdleMsgPing-StBusy
StBusyMsgPong-StIdle
StIdleMsgDone-End

Message formats

The messages of the mini-protocols are encoded in CBOR, a compact binary encoding of JSON. The syntax of valid messages is expressed in CDDL (Concise Data Definition Language).

Here’s the CDDL for the node-to-node handshake protocol:

;
; NodeToNode Handshake, v7 to v10
;

handshakeMessage
    = msgProposeVersions
    / msgAcceptVersion
    / msgRefuse

msgProposeVersions = [0, versionTable]
msgAcceptVersion   = [1, versionNumber, nodeToNodeVersionData]
msgRefuse          = [2, refuseReason]

versionTable = { * versionNumber => nodeToNodeVersionData }

versionNumber = 7 / 8 / 9 / 10

nodeToNodeVersionData = [ networkMagic, initiatorOnlyDiffusionMode ]

; range between 0 and 0xffffffff
networkMagic = 0..4294967295
initiatorOnlyDiffusionMode = bool

refuseReason
    = refuseReasonVersionMismatch
    / refuseReasonHandshakeDecodeError
    / refuseReasonRefused

refuseReasonVersionMismatch      = [0, [ *versionNumber ] ]
refuseReasonHandshakeDecodeError = [1, versionNumber, tstr]
refuseReasonRefused              = [2, versionNumber, tstr]

Network: Handshake mini-protocol

Mini-protocol number: 0

The Handshake mini-protocol is used to establish a connection and negotiate protocol versions and parameters between the initiator (client) and responder (server). There are two versions, one for node-to-node (N2N) and one for node-to-client (N2C), which differ only in the parameters.

State machine

stateDiagram

    [*] --> StPropose
    StPropose --> StConfirm: MsgProposeVersions
    StConfirm --> [*]: MsgAcceptVersion
    StConfirm --> [*]: MsgReplyVersion
    StConfirm --> [*]: MsgRefuse

    direction LR

    classDef initiator color:#080
    classDef responder color:#008, text-decoration: underline
    class StPropose initiator
    class StConfirm responder

State agencies

StateAgency
StProposeInitiator
StConfirmResponder

State transitions

From stateMessageParametersto state
StProposeMsgProposeVersionsversionTableStConfirm
StConfirmMsgReplyVersionversionTableEnd
StConfirmMsgAcceptVersion(versionNumber, versionData)End
StConfirmMsgRefusereasonEnd

TCP simultaneous open

In the rare case when both sides try to connect to each other at the same time, it’s possible to get a “TCP simultaneous open” where you end up with a single socket, not two. In this case, both sides will think they are the initiator so will send a MsgProposeVersions, and this protocol handles this by treating the received one in StConfirm state as a MsgReplyVersion, which has the same CBOR encoding.

warning

Why does the message need to change name? The state machine would be valid with an StConfirm -- MsgProposeVersions --> End arc.

note

Also, is the negotiation always deemed successful in this case? What if one side can’t accept the other’s version? (there is talk of resetting the connection)

warning

MsgReplyVersion is no longer mentioned in the CDDL - is this therefore out of date?

Messages

The MsgProposeVersions message is sent by the initiator to propose a set of possible versions and protocol parameters. versionTable is a map of version numbers to associated parameters - bear in mind that different versions may have different sets of parameters. The version number keys must be unique and in ascending order.

note

This seems an arbitrary constraint which could easily be avoided by implementations, although deterministic CBOR encoding would enforce it.

The MsgAcceptVersion message is returned by the responder to confirm a mutually acceptable version and set of parameters.

The MsgRefuse message is returned by the responder to indicate there is no acceptable version match, or other reason. If it is a version mismatch it returns a set of version numbers that it could have accepted.

warning

The content of MsgRefuse is inconsistent between the paper and CDDL - check the above.

Message size limits

Because the Handshake protocol operates before the multiplexer is fully set up, the messages must not be split into segments, and this imposes a size limit of 5760 bytes.

warning

This seems like a protocol level mix, and since the negotiated parameters don’t seem to affect the mux config (and could be changed dynamically even if they did), it’s not clear why this constraint is needed.

note

Why 5076 when the mux protocol can handle 65535? Implementation detail?

Timeouts

The maximum time to wait for a message in StPropose (for the responder) or StConfirm (for the initiator) is 10 seconds. After this the connection should be torn down.

CDDL

Here’s the CDDL for the latest node-to-node handshake protocol:

;
; NodeToNode Handshake (>=v13)
;
handshakeMessage
    = msgProposeVersions
    / msgAcceptVersion
    / msgRefuse
    / msgQueryReply

msgProposeVersions = [0, versionTable]
msgAcceptVersion   = [1, versionNumber, nodeToNodeVersionData]
msgRefuse          = [2, refuseReason]
msgQueryReply      = [3, versionTable]

versionTable = { * versionNumber => nodeToNodeVersionData }

versionNumber = 13 / 14

nodeToNodeVersionData = [ networkMagic, initiatorOnlyDiffusionMode, peerSharing, query ]

; range between 0 and 0xffffffff
networkMagic = 0..4294967295
initiatorOnlyDiffusionMode = bool
; range between 0 and 1
peerSharing = 0..1
query = bool

refuseReason
    = refuseReasonVersionMismatch
    / refuseReasonHandshakeDecodeError
    / refuseReasonRefused

refuseReasonVersionMismatch      = [0, [ *versionNumber ] ]
refuseReasonHandshakeDecodeError = [1, versionNumber, tstr]
refuseReasonRefused              = [2, versionNumber, tstr]

And the node-to-client version:

;
; NodeToClient Handshake
;

handshakeMessage
    = msgProposeVersions
    / msgAcceptVersion
    / msgRefuse
    / msgQueryReply

msgProposeVersions = [0, versionTable]
msgAcceptVersion   = [1, versionNumber, nodeToClientVersionData]
msgRefuse          = [2, refuseReason]
msgQueryReply      = [3, versionTable]

; Entries must be sorted by version number. For testing, this is handled in `handshakeFix`.
versionTable = { * versionNumber => nodeToClientVersionData }


; as of version 2 (which is no longer supported) we set 15th bit to 1
;               16    / 17    / 18    / 19
versionNumber = 32784 / 32785 / 32786 / 32787

; As of version 15 and higher
nodeToClientVersionData = [networkMagic, query]

networkMagic = uint
query        = bool

refuseReason
    = refuseReasonVersionMismatch
    / refuseReasonHandshakeDecodeError
    / refuseReasonRefused

refuseReasonVersionMismatch      = [0, [ *versionNumber ] ]
refuseReasonHandshakeDecodeError = [1, versionNumber, tstr]
refuseReasonRefused              = [2, versionNumber, tstr]

Ledger

Cardano uses the Extended Unspent Transaction Output (EUTxO) ledger model…

Styleguide

note

Expand this as needed with examples, tips, glossary and general language used. Try not to replicate upstream docs, e.g. for mermaid or math markup.

Overall style

The blueprints should be well written.

It might seem this should go without saying, but it’s put here to emphasise its importance. When writing you should always be keeping the reader in mind, thinking what is the best way to pass on the knowledge you have given the current state of knowledge they have. This is a hard thing to do, but it is worth aiming for!

People take in information in different ways. Some like literate prose, many like visual diagrams and pseudo-code, and a few like mathematical formulae. If you can do all of these, so much the better, but good diagrams are often the place to start.

Other tips:

  • Create one file per major topic
  • Divide text up into logical, hierarchical sections
  • Break up long paragraphs into bulleted lists
  • Use code blocks to give short examples
  • Use backquote to set off identifiers - e.g. message or state names

Language

Text should be written in American English, at the reading level of a competent software developer - which is often very high, but bear in mind that English may not be their first language. Use technical language by all means, but there is no need to be egregiously erudite in your elucidation.

It may not be necessary to talk about humans in much of this documentation, but if you do, please use gender-neutral pronouns - ‘they’ and ‘their’:

A user must keep the keys to their wallet safe.

If you find this difficult, cast it into the plural:

Users must keep the keys to their wallets safe.

Normative vs declarative style

Some standards documentation is very SHOUTY about MUST, SHALL and so on. Even if it doesn’t SHOUT it can still be rather clumsy to read. Instead, we want to use a declarative style. We can express what an implementation has to do conform to the specification as a simple descriptive fact - e.g.

The tester discards widgets with broken flibbits.

rather than the ‘normative’

The tester SHALL discard widgets with broken flibbits.

So in this style there is an implicit ‘must’. If something is optional, this can be said explicitly:

The package may use extra sproggles if required.

It’s still OK to use ‘must’ to emphasise cases which are absolutely critical, but still, please don’t SHOUT.

Diagrams & Maths

We can use mermaid diagrams (live editor):

graph LR;
    A-->B-->D;
    A-->C;

and maths using katex:

Alerts

warning

We can use the github flavored callouts, documented here

note

A friendly note in github. How about code blocks?

cargo install mdbook-alerts

Footnotes

Additional information that would complicate the read-flow can be put into footnotes 1.

1

Example footnote

Other stuff

The footnote should appear below. If not, we need to contribute this to mdbook.

Contribution Guidelines