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:
- Handshake - for connection and version negotiation
- Chain Synchronization - for synchronization of changes to the Cardano chain1
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:
- Local State Query - for querying ledger state
- Local Tx Submission - for submitting transactions locally
- Local Tx Monitor - for monitoring transactions
Dummy mini-protocols
These protocols are only used for testing and experimentation:
- Ping-Pong - a simple presence test
- Request-Response - a generic mechanism for exchanging data
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)"
Field | Size | Meaning |
---|---|---|
Transmission time | 32 | Monotonic time stamp (µsec, lowest 32 bits) |
M | 1 | Mode: 0 from initiator, 1 = from responder |
Mini-protocol ID | 15 | Mini-protocol ID (see below) |
Payload length | 16 | Segment payload length (N) in bytes |
Payload | N | Raw 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
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:
State | Agency |
---|---|
StIdle | Initiator |
StBusy | Responder |
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 state | Message | Parameters | to state |
---|---|---|---|
StIdle | MsgPing | - | StBusy |
StBusy | MsgPong | - | StIdle |
StIdle | MsgDone | - | 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
State | Agency |
---|---|
StPropose | Initiator |
StConfirm | Responder |
State transitions
From state | Message | Parameters | to state |
---|---|---|---|
StPropose | MsgProposeVersions | versionTable | StConfirm |
StConfirm | MsgReplyVersion | versionTable | End |
StConfirm | MsgAcceptVersion | (versionNumber, versionData) | End |
StConfirm | MsgRefuse | reason | End |
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.
Example footnote
Other stuff
The footnote should appear below. If not, we need to contribute this to mdbook
.