Skip to main content

Interaction with the Ledger Layer

The ouroboros-consensus implementation is intimately related to the cardano-ledger implementation. Consensus uses the ledger implementation as a pure function for validating transactions and blocks.

The cardano-ledger implementation makes a clear distinction between Byron and the rest of the eras, much like the Consensus layer does. All the other eras are derivatives of the Shelley era, and as such, we will refer to them as Shelley eras. The Byron implementation is encapsulated in cardano-ledger-byron.

Some of the basic types used in all the ledger implementations for Shelley eras are also encapsulated in cardano-ledger-core.

Small-steps transition system

The Ledger implementation is based on the small-steps library, which exposes the function applySTS that uses the class STS to define the state, signal, environment, logged events and failures that a rule has. It follows the operational semantics described in the different specification documents listed in their README. Evaluating a ledger rule is essentially running an STS transition that produces a new state or throws failures.

The relation between rules can be seen in section 14 of the Shelley spec. Later eras extend internally some of those rules, but from the Consensus perspective, understanding the Shelley rules is enough.

Types defined by the Ledger layer

note

cardano-ledger exposes very basic types from cardano-slotting (such as SlotNo, EpochNo, ...) and cardano-crypto-* packages (such as HASH, VRF, ...), which are part of the cardano-base repository, but we will omit those in this discussion.

This table describes how the types defined in the Ledger code are wrapped in Consensus, and how those types are then aggregated in different type families used all over the Consensus codebase:

Ledger era(s)Ledger typeIn ConsensusAggregating type family
ByronABlockOrBoundaryByronBlock
ByronABlockOrBoundaryHdrByronHeaderHeader blk
Shelley erasBlock eraShelleyBlock proto era
TPraos erasBHeader cShelleyHeaderHeader blk
ByronTxGenTx ByronBlockGenTx blk
Shelley erasTx l eraShelleyTxGenTx blk
Shelley erasValidated txShelleyValidatedTxValidated (GenTx blk)
Shelley erasNewEpochState eraShelleyLedgerStateLedgerState blk mk

Note that the header for TPraos eras (Shelley, Allegra, Mary, Alonzo) is defined in cardano-ledger (cardano-protocol-tpraos) but the header for Praos eras (Babbage, Conway, ...) is in ouroboros-consensus-protocol, which is why it is not reflected in this table.

It is clearly visible that the type families are indexed (or transitively indexed) by the block types. The block types are combined into the CardanoBlock which is a HardForkBlock, providing a dispatcher for each of the eras depending on the component of the n-ary sum on the value. The CardanoBlock is another block, and as such, it also has instances for the type families which will be n-ary sums or telescopes over the types for the particular blocks defined in the ledger.

Ledger state ticking and forecasting

Before a block can be applied to a Ledger state, the state has to be transported through time up to the slot in which the block was forged. There are mutations in the Ledger state that are enacted just by the passing of time, such as epoch transitions (which imply rewards), or pulsing calculations that are spread over time.

For Shelley eras, ticking a Ledger State is implemented as an evaluation of the TICK transition rule. Ledger exposes the function applyTick to run this rule.

When evaluating the validity of a header in Consensus (which is checked before attempting to validate the body of the block), parts of the Ledger state (the LedgerView) need to be transported to the slot of the header, very much like a restricted version of ticking the Ledger state. This is done by means of the futureLedgerView function which in the end is an evaluation of the TICKF transition rule.

Transaction validation

We will only talk about Shelley eras' transactions as it is expected there are no more Byron transactions floating in the network. Byron transactions implement the same instances in Consensus, but call different functions on the Ledger.

Applying a transaction to a ledger state means running the MEMPOOL transition rule.

A transaction is said to be valid on top of a ledger state if applying the transaction on said ledger state succeeds. Validity of a transaction is not permanent, as transactions can be applied to different ledger states, and succeed only on some.

Consensus checks the validity of transactions when adding them to the mempool. Application of transactions is abstracted in the class LedgerSupportsMempool.

For the Shelley eras, cardano-ledger-shelley exposes a function applyTx which returns either an error (ApplyTxError era) if the transaction is invalid on the given ledger state, or a new state (LedgerState era) and a thin newtype wrapper that tags the transaction as valid (Validated (Tx era)).

Note that even if a transaction is applied to a different ledger state, there are some checks that cannot change their outcome, such as checking cryptographic hashes. For this purpose, Ledger exposes the function reapplyTx which has a type very similar to the one of applyTx.

Both of these functions are wrapped by homonym functions in Consensus, which operate on Consensus types and prepare the environment and context to call the Ledger functions with the appropriate arguments and Ledger types.

Block validation

A block B is said to be valid if applying the block on top of its predecessor B' is successful. In particular if some ancestor block (or even B') fails to apply, then the ledger state at the predecessor block (at B') cannot exist and it would be absurd to ask whether B is valid or not.

In Shelley, applying a block to a ledger state means running the BBODY transition rule.

Consensus checks the validity of blocks when selecting chains in ChainSelection.

For Byron, Consensus calls either validateBlock or validateBoundary from cardano-ledger-byron depending on whether a regular block or an EBB is being validated.

For the Shelley eras, cardano-ledger-shelley exposes a function applyBlock which returns a new state (NewEpochState era), a list of failures (empty if the block is valid) and a list of generated events.

Note that in contrast to transactions, blocks can only be applied on top of their parent block, which cannot ever change, so a valid block is valid always. This introduces a duality between applyBlock and reapplyBlock, in which reapplyBlock skips as many checks as possible and cannot fail.

We use reapplyBlock to apply the blocks from Genesis (or the snapshot we start from) to the immutable tip when initializing consensus, as we know those blocks have been previously validated by us.

note

When a volatile block has been validated, it would be correct to reapply it if we ever want to adopt that block again. Consensus at the moment does not do this optimization, and applies it as if it was a new block.

Storing the UTxOs on disk

The UTxO set is by far the largest structure in the Ledger State. It contains the millions of associations of TxIn to TxOut. Consensus, being the first layer that uses impure code, is the one responsible for storing and managing such set so that it can be stored in persistent storage, lowering the memory requirements of a running node.

The ledger states that Consensus carries around have had their UTxO sets stripped out. When a ledger rule is invoked, Consensus asks the ledger for the needed entries of the UTxO set (with neededTxInsForBlock for Shelley eras' blocks, and with allInputsTxBodyF for transactions) and injects only those into the Ledger state before invoking the ledger rules.

Once the rules return a new ledger state, Consensus extracts the new UTxO set, calculates the difference with the input set and pushes those differences to the UTxO store. In particular, if the node is using the InMemory backend (which consists of a UTxO set in a TVar), it will clone the UTxO set from the parent ledger state and apply those differences to the cloned UTxO set. If the node is using the LSM-trees backend, it will duplicate the handle for the parent ledger state and push the differences to the cloned handle.

Codecs

cardano-ledger provides instances for serializing all the relevant types it exposes. It uses the classes EncCBOR and DecCBOR to produce Encoding and Decoder values which depend on a Version to decide on the particular codec format. This allows different eras to encode values in different ways.

Most of the ledger types are wrapped in a Hard Fork Combinator construct in Consensus, where a tag for the era is prepended thus allowing Consensus to express/infer the era of the underlying data.

From the Consensus side, the particular era codec is invoked from within the ToCBOR and FromCBOR instances for a particular value, where the era is known, by using toEraCBOR/toPlainEncoding or eraDecoder/toPlainDecoder.

As such, the serialized form of a block is just the toEraCBOR @era of the block, wrapped in a tag that indicates the era of the block. The serialized form of a transaction is toEraCBOR @era of the transaction, wrapped in a tag that indicates the era of the transaction.

Translation among eras

The Ledger layer is structured in separate eras. Each era is its own package, implementing all the interfaces required for such an era, and linking it to the previous era (PreviousEra era).

Some of the Ledger types will need to be transported from one era to the next one, in particular the most relevant one is the Ledger State. When Consensus ticks through an era boundary, the state has to be translated to the new era in order to apply the block after the era boundary (which will belong to the new era too). For this purpose, cardano-ledger defines the TranslateEra class, which is capable of translating types from the previous era to the current one, using a TranslationContext. Transactions (Tx era) sometimes need to also be translated to a later era, when the transaction received from the network is in an older era than the current selection.

Some types do not require a translation context and can always be converted to a later era. Instead of "translate", this is called "upgrade". A notable example is TxOuts which are upgraded through upgradeTxOut.

Queries

cardano-ledger also exposes an API to perform queries into the Ledger State, to offer information to local clients such as wallets. Queries are discussed extensively in the next section.