Inside cardano-node: A Deep Dive Into the Software That Powers Cardano
Every Cardano stake pool, relay node, and full node on the network runs the same piece of software: cardano-node. It is the beating heart of a blockchain that secures billions of dollars in value and has maintained continuous operation since the Shelley era launched in 2020.
But what does this codebase actually look like on the inside? How is a ~104,000-line Haskell project structured to support a blockchain that has hard-forked through eight distinct protocol eras — without ever stopping the chain?
This post is a deep dive into the cardano-node repository at version 10.6.2. We will walk through its architecture, examine the patterns that make it tick, and highlight what makes this project genuinely interesting from a software engineering perspective.
The Integration Layer
The first thing to understand about cardano-node is what it is not. It is not a monolithic application that implements consensus, networking, and ledger rules from scratch. Instead, it is an integration layer — a relatively thin orchestration package that composes four large external components:
- ouroboros-consensus — The Ouroboros Praos consensus engine and the Hard Fork Combinator
- ouroboros-network — Peer-to-peer networking, typed protocols, multiplexing
- cardano-ledger — Ledger rules for every era (Byron through Conway)
- cardano-api — High-level API for transaction building and node interaction
The node itself wires these together, handles configuration, manages the tracing/monitoring pipeline, and exposes the executables that operators actually run.
This separation is a deliberate architectural choice. Each component lives in its own repository with its own release cadence, test suites, and maintainers. The node repository pins specific versions of each via CHaP (the Cardano Haskell Package repository) — Cardano's own curated Hackage alternative.
Repository Structure: Nine Packages in a Monorepo
The repository is a Cabal-based Haskell monorepo containing nine packages. Each has a focused responsibility:
Core
cardano-node— The main node library and executable. Configuration parsing, protocol instantiation, shutdown handling, and therunNodeentry point.cardano-submit-api— A lightweight REST server (built on Servant and Warp) that accepts signed transactions via HTTP POST and submits them to the local node.
Tracing & Monitoring
trace-dispatcher— The core tracing library. Arrow-basedcontra-tracercombinators for filtering, routing, formatting, and dispatching trace messages.trace-forward— Typed protocols for forwarding trace objects and data points from the node to an external acceptor process.trace-resources— Cross-platform system resource monitoring (CPU, memory, network, disk) with platform-specific implementations for Linux, macOS, and Windows.cardano-tracer— A standalone monitoring service that receives forwarded traces and exposes them via Prometheus, EKG, log files, systemd journal, and an optional RTView web dashboard.
Tooling & Testing
cardano-testnet— A framework for spinning up local multi-node testnets programmatically. Used extensively in integration and property-based tests.cardano-node-chairman— A consensus verification tool that connects to multiple running nodes and asserts they agree on the same chain.cardano-node-capi— An FFI C library that wraps the node, enabling embedding in applications written in other languages.
Beyond these, the bench/ directory houses a full benchmarking suite including tx-generator (for stress-testing transaction throughput), locli (log analysis), plutus-scripts-bench (Plutus script performance), and cardano-profile (workload profiling).
How the Node Starts: From main to Block Production
The entry point is refreshingly simple. In cardano-node.hs:
main :: IO ()
main = do
Crypto.cryptoInit
toplevelExceptionHandler $ do
cmd <- customExecParser p opts
case cmd of
RunCmd args -> runNode args
VersionCmd -> runVersionCommandCrypto.cryptoInit initialises the custom libsodium fork that Cardano uses for its cryptographic primitives (VRF, KES, BLS). This must happen before any crypto operations — a pattern you will see repeated throughout the codebase as a defensive measure.
The runNode function in Cardano.Node.Run is where things get interesting. It follows a carefully ordered startup sequence:
-
Install a SIGTERM handler that converts the signal into an async exception, ensuring graceful shutdown on Unix systems.
-
Merge configuration sources using the
PartialNodeConfigurationmonoid. CLI flags, the YAML config file, and default values are composed with<>(the Semigroup append), where later sources override earlier ones. This is a clean pattern for layered configuration — the same idea as CSS specificity, but for node settings. -
Check VRF key file permissions. The node enforces that VRF secret keys are readable only by the owner — a hard requirement, not a suggestion. If permissions are wrong, the node refuses to start.
-
Instantiate the consensus protocol by reading genesis files for every era (Byron, Shelley, Alonzo, Conway, and optionally Dijkstra) and assembling them into a
SomeConsensusProtocol. This is an existentially-typed value that hides the specific block type while requiring all necessary type class instances. -
Initialise the tracing system, either the legacy
iohk-monitoringbackend or the newertrace-dispatcherpipeline (now the default). -
Read the P2P topology file and configure local root peers, public root peers, bootstrap peers, and ledger peer settings.
-
Launch the node kernel via
Node.runfrom ouroboros-consensus, which starts chain selection, block fetching, mempool management, and the mini-protocol servers.
The Hard Fork Combinator: One Binary, Eight Eras
Perhaps the most architecturally fascinating aspect of Cardano is how it handles protocol upgrades. Most blockchains treat hard forks as disruptive events — all node operators must upgrade before a specific block height, or risk being forked off the network. Ethereum's hard forks (The Merge, Shanghai, Dencun) each required coordinated fleet-wide upgrades. If enough operators fail to upgrade in time, the chain splits.
Cardano takes a fundamentally different approach with the Hard Fork Combinator (HFC) — a piece of consensus-layer machinery, described in a published paper, that lets the node understand every historical era simultaneously within a single binary. The key insight is that the combinator treats each era's consensus protocol as a composable building block. Rather than replacing one protocol with another, it sequences them — creating a composite protocol that knows when to switch between eras based on on-chain state.
This design has a profound consequence: the node software can be updated well in advance of a hard fork. Operators upgrade at their leisure, and the actual transition happens automatically when on-chain governance votes the protocol version forward. There is no flag day, no countdown timer, no risk of operators being caught off guard.
The protocol version numbers in the codebase tell the story:
Version 0 — Byron (Ouroboros Classic)
Version 1 — Byron (Ouroboros BFT)
Version 2 — Shelley
Version 3 — Allegra
Version 4 — Mary
Version 5 — Alonzo
Version 6 — Alonzo (intra-era hard fork)
Version 7 — Babbage
Version 8 — Babbage (intra-era hard fork)
Version 9 — Conway (bootstrap phase)
Version 10 — Conway
Version 11 — Dijkstra (next era, experimental)
Each hard fork is triggered when the protocol version in the on-chain ledger state reaches the corresponding number. This means hard forks are initiated by on-chain governance, not by node operators restarting software. The node simply watches the ledger state and transitions automatically.
In the source code, the mkSomeConsensusProtocolCardano function assembles the full multi-era protocol by reading separate genesis files for each era and configuring hard fork triggers:
Consensus.cardanoHardForkTriggers =
CardanoHardForkTriggers' {
triggerHardForkShelley = ... ,
triggerHardForkAllegra = ... ,
triggerHardForkMary = ... ,
triggerHardForkAlonzo = ... ,
triggerHardForkBabbage = ... ,
triggerHardForkConway = ... ,
triggerHardForkDijkstra = ... ,
}Each trigger is either CardanoTriggerHardForkAtDefaultVersion (the production setting, which watches the on-chain protocol version) or CardanoTriggerHardForkAtEpoch (used in testing to force a transition at a specific epoch).
The practical consequence is remarkable: a node syncing from genesis today will replay Byron blocks from 2017, transition through every intermediate era, and arrive at Conway — all without stopping, restarting, or switching binaries. The node literally time-travels through Cardano's entire protocol history on every sync.
What makes this technically difficult is that each era has different block formats, different transaction types, different ledger rules, and sometimes different consensus mechanisms entirely (Byron used Ouroboros Classic/BFT, while Shelley and later use Praos). The HFC must maintain type-safe boundaries between eras while allowing seamless transitions. This is where Haskell's type system earns its keep — the era transitions are encoded at the type level, making it impossible to accidentally apply Alonzo ledger rules to a Shelley-era block.
Dijkstra: The Next Era
Eagle-eyed readers will notice that the codebase already contains scaffolding for the next era, named Dijkstra (after Edsger W. Dijkstra, continuing Cardano's tradition of naming eras after computer science pioneers). The Cardano.Node.Protocol.Dijkstra module defines an emptyDijkstraGenesis with initial protocol parameters including reference script size limits. The hard fork trigger is wired in but gated behind an npcExperimentalHardForksEnabled flag. When disabled, the node advertises protocol version 10.7; when enabled, it jumps to 11.0 — signalling readiness for Dijkstra.
The Tracing Architecture: Observability as a First-Class Concern
Most software projects bolt on logging as an afterthought. The cardano-node dedicates four entire packages to observability, and the design is worth studying on its own merits.
The foundation is trace-dispatcher, which builds on contra-tracer — a contravariant tracing library. If you are not familiar with contravariant functors, the intuition is this: a regular functor transforms the output of a computation, while a contravariant functor transforms the input. A Tracer IO msg is a sink that consumes messages of type msg. You compose tracers by transforming what they accept, not what they produce.
The trace-dispatcher extends this with a rich combinator language:
- Severity levels —
setSeverity,withSeverity,filterTraceBySeverity - Privacy controls —
privately,allPublic,allConfidential - Routing —
routingTracesends different message types to different backends - Frequency limiting —
FrequencyLimiterthrottles high-volume traces - Folding —
foldTraceMaccumulates state across trace messages (used for forging statistics) - Naming —
appendPrefixNamebuilds hierarchical namespace paths
Messages flow through this pipeline and emerge as structured JSON, human-readable text, EKG metrics, or Prometheus counters — depending on configuration. The entire pipeline is composable, type-safe, and testable.
The trace-forward package takes this further by defining typed protocols (using the same framework as the networking layer) for streaming trace objects from the node to an external cardano-tracer process. This means monitoring is decoupled from the node process — the tracer can be restarted, upgraded, or replaced without affecting the node.
The trace-resources package is a delightful piece of systems programming. It provides platform-specific resource monitoring with separate implementations for Linux (reading /proc), macOS (C FFI bindings to Mach kernel APIs), and Windows (Win32 API calls). Each platform module exposes the same Haskell interface but uses completely different underlying mechanisms.
UTxO-HD and the LedgerDB
One of the more pressing engineering challenges in cardano-node is managing the growing UTxO set — the set of all unspent transaction outputs. Unlike Ethereum's account-based model where balance is a single number per address, Cardano's Extended UTxO model tracks every individual output. When a transaction spends outputs and creates new ones, the spent outputs must be removed and the new ones added. The entire set must be queryable for transaction validation.
This set grows over time. As more transactions occur and more addresses hold funds, the UTxO set expands. On mainnet, it currently consists of millions of entries, and every one of them must be accessible during block validation. For the first few years, the entire set was held in memory — simple and fast, but increasingly expensive as the chain grows.
The LedgerDB subsystem now offers two storage backends to address this:
- V2InMemory — The original approach. Keeps the full UTxO set in RAM. Fast and simple, but requires substantial memory — 24GB is the current recommendation for mainnet, and this number grows with the chain.
- V1LMDB — The newer disk-backed approach, using LMDB (Lightning Memory-mapped Database) to store UTxO diffs on disk. Rather than holding the full set in RAM, it maintains a sliding window of recent changes and reads older state from disk via memory-mapped files. This dramatically reduces memory requirements at the cost of some I/O overhead.
The LMDB configuration is thoughtfully documented in the source. The default map size is 16GB (the maximum database size on disk), with 10 internal databases reserved and 16 reader slots provisioned. These numbers are explicitly chosen to be future-proof while avoiding over-allocation.
This is an active area of engineering. The long-term health of the network depends on the UTxO set remaining manageable as adoption grows — without requiring node operators to continually upgrade to machines with more RAM.
P2P Networking and Topology
In its early days, Cardano used a federated networking model — nodes connected to a static list of relays, and the network topology was essentially hand-managed. This worked for bootstrap, but it created central points of failure and made the network dependent on IOG-operated infrastructure. The transition to a fully peer-to-peer networking model was a multi-year effort that culminated in the Eclipse release, making Cardano one of the few proof-of-stake networks with genuine P2P peer discovery.
The topology configuration in cardano-node now reflects this. The NetworkTopology data type captures:
- Local root peers — Peers that the node will actively maintain connections to, with configurable hot and warm valency targets. These can be marked as
trustablefor bootstrap mode. - Public root peers — Well-known entry points to the network, used for initial peer discovery.
- Ledger peers — Peers discovered from the on-chain stake distribution. The
useLedgerAfterSlotsetting controls when the node starts trusting the ledger for peer information. - Bootstrap peers — Used during the initial chain sync to find trustworthy peers before the node has enough chain history to verify the ledger's peer data.
- Peer snapshot files — Serialised snapshots of the ledger peer set, allowing faster bootstrapping in Genesis consensus mode.
The node also supports SIGHUP-based topology reloading — sending SIGHUP to the node process causes it to re-read the topology file and update peer connections without restarting. This is especially valuable for stake pool operators who need to adjust their network configuration on live production nodes.
Build System: Nix and Cabal
The repository supports two build paths:
Cabal is the standard Haskell build tool. The project uses cabal.project to pin an exact Hackage index-state for reproducibility, and pulls Cardano-specific packages from CHaP.
Nix is the preferred approach for CI and deployment. The flake.nix defines the full build, pulling in haskell.nix (IOG's Nix-to-Cabal bridge), hackage.nix, and CHaP. It includes a binary cache at cache.iog.io to avoid rebuilding GHC and all dependencies from source.
The GHC RTS (runtime system) flags in the Cabal file reveal careful performance tuning:
-threaded -rtsopts
"-with-rtsopts=-T -I0 -A16m -N2 --disable-delayed-os-memory-return"
-Tenables runtime statistics collection-I0disables the idle GC (the node is never truly idle)-A16msets the allocation area to 16MB (reducing minor GC frequency)-N2uses 2 OS capabilities (or-N1on ARM)--disable-delayed-os-memory-returnensures freed memory is returned to the OS promptly — critical for a long-running daemon
The Wider Stack: The Components That Make Up a Node
We have so far focused on the cardano-node repository itself — but as noted at the outset, the node is an integration layer. The real architectural weight lives in a constellation of companion repositories under the IntersectMBO GitHub organisation. These are not optional add-ons; they are the node. Understanding their internal architecture is essential to understanding how Cardano works.
cardano-ledger — The Rule Book
The cardano-ledger repository is arguably the most critical codebase in the entire ecosystem. It implements the ledger rules — the state transition logic that decides whether a transaction is valid, how UTxO sets are updated, how staking rewards are calculated, how governance votes are tallied, and how protocol parameters change. Every block the node validates passes through this code.
Architecture: State transition systems all the way down
The ledger's architecture is built on a framework called small-steps, which implements formal state transition systems directly in Haskell. Every ledger rule — from the top-level "tick" that advances the chain by one slot to the individual UTxO validation that checks a single transaction — is expressed as an instance of the STS type class:
class STS a where
type State a -- The state being transitioned
type Signal a -- The input triggering the transition
type Environment a -- Read-only context
type PredicateFailure a -- What can go wrong
transitionRules :: [TransitionRule a]Rules compose hierarchically through an Embed type class. When the top-level TICK rule processes a new slot, it calls NEWEPOCH, which calls EPOCH, which calls SNAP (snapshot), POOLREAP (pool retirement), and other sub-rules. When the BBODY rule processes a block, it calls LEDGERS, which calls LEDGER for each transaction, which in turn calls UTXOW (witness validation) and DELEGS (certificate processing). Each level translates failures and events from child rules into its own types, building a complete audit trail.
This is not boilerplate design — it is a direct translation of the formal specification papers into executable code. The same rule names and state types appear in both the academic papers and the Haskell source.
Era polymorphism
The ledger handles eight distinct protocol eras through a system of type classes and type families. An Era type class defines each era's identity — its name, its predecessor, and its protocol version range. A family of associated type classes (EraTx, EraTxBody, EraTxOut, EraScript, EraPParams, etc.) defines what transactions, outputs, scripts, and parameters look like in each era.
The critical mechanism is the EraRule type family, which maps abstract rule names to concrete implementations per era:
type instance EraRule "LEDGER" ShelleyEra = ShelleyLEDGER ShelleyEra
type instance EraRule "LEDGER" ConwayEra = ConwayLEDGER ConwayEra
type instance EraRule "POOL" ConwayEra = ShelleyPOOL ConwayEra -- inheritedEach new era can override specific rules while inheriting others unchanged. Conway, for example, replaces the LEDGER, UTXO, and DELEGS rules (adding governance) while keeping the POOL rule from Shelley untouched. Obsolete rules are explicitly marked with VoidEraRule, making it a compile-time error to invoke them in a newer era.
When a hard fork occurs, the TranslateEra type class migrates every piece of ledger state — UTxO sets, delegation maps, protocol parameters, governance proposals — from the previous era's types to the new era's types. This translation is where era-specific genesis configurations (like Conway's initial committee and constitution) get injected.
Conway-era governance
The Conway era (the current era on mainnet) introduced a particularly complex addition: on-chain governance via CIP-1694. The ledger now tracks governance proposals, DRep (Delegated Representative) registrations, committee membership, and a three-body ratification system where governance actions must pass threshold votes from three independent groups — the Constitutional Committee, DReps, and Stake Pool Operators — before being enacted. Each voter group has its own threshold, and the thresholds vary by action type (parameter changes, hard fork initiation, treasury withdrawals, constitutional amendments, etc.).
Formal verification
The ledger rules are specified formally before being implemented. The formal-ledger-specifications repository contains machine-checked specifications written in Agda — a dependently-typed language used for theorem proving. These are not documentation; they are executable specifications against which the Haskell implementation is tested for conformance via the cardano-ledger-conformance package. This spec-first, verify-against approach is unusual in the blockchain world and is one of the strongest arguments for the Haskell ecosystem's formal methods tooling.
ouroboros-consensus — The Decision Engine
The ouroboros-consensus repository implements the consensus layer — the logic that decides which chain to follow, when to produce blocks, and how to manage persistent storage of the blockchain. If the ledger is the rule book, consensus is the referee.
ChainDB: A three-database storage engine
The ChainDB is not a single database but a coordinated system of three:
-
ImmutableDB — An append-only store for finalised blocks. Blocks are organised into chunks (corresponding to epochs), with secondary indices for efficient lookup. Once a block is buried deep enough in the chain (past the security parameter k), it is moved here and can never be rolled back. This database supports efficient streaming via iterators — essential for chain sync with peers.
-
VolatileDB — A hash-indexed store for recent blocks that might still be rolled back. Unlike the ImmutableDB (which is ordered by slot), the VolatileDB indexes blocks by hash because multiple competing forks may exist simultaneously. It maintains a successor map (
filterByPredecessor) that allows the chain selection algorithm to efficiently construct all possible chains from the current immutable tip. -
LedgerDB — Maintains the ledger state at the chain tip plus the past k ledger states needed for rollbacks. This is where the V2InMemory and V1LMDB backends discussed earlier come into play. It also manages snapshots — persisting ledger state to disk periodically so the node does not have to replay the entire chain from genesis on restart.
Three background threads coordinate the databases: one copies blocks from the VolatileDB to the ImmutableDB as they become immutable, one garbage-collects stale blocks from the VolatileDB, and one triggers ledger snapshots and flushing.
Block production: The VRF lottery
Every slot (one second on Cardano), each stake pool runs a VRF (Verifiable Random Function) lottery to determine if it should produce a block. The process works as follows:
- The pool constructs a VRF input by combining the current slot number with the epoch nonce (a random value derived from the chain's history).
- It evaluates the VRF using its private VRF signing key, producing a certified random output.
- It checks whether this output falls below a threshold determined by the pool's relative stake and the active slot coefficient (the
fparameter that controls how many slots have blocks on average). - If the threshold is met, the pool has "won" the slot and produces a block. The VRF proof is included in the block header, allowing any other node to verify the leadership claim without knowing the private key.
The consensus layer also tracks epoch nonces across transitions — the candidate nonce stops evolving a certain number of slots before the epoch boundary (the randomness stabilisation window), ensuring the next epoch's nonce is determined before anyone can try to manipulate it.
Chain selection and fork choice
When a node receives competing chains, it must decide which one to follow. The chain selection rule is deceptively simple: prefer the longer chain. But the implementation has important subtleties:
- Candidates are only considered if they extend from the current immutable tip (no deep reorganisations).
- Equal-length chains are resolved by a tiebreaker function on the block headers.
- A Limit on Eagerness (LoE) bounds how far ahead of the validated ledger state the node will follow an unvalidated chain — preventing an attacker from feeding the node an arbitrarily long invalid chain.
- Blocks are validated against the ledger sequentially, and a candidate that fails validation is truncated at the last valid block — the valid prefix may still be adopted if it is preferred over the current chain.
The Mempool
The mempool is an entirely in-memory, ordered list of transactions waiting to be included in a block. Its default capacity is twice the maximum transaction capacity of a single block, scaling automatically with protocol parameter changes.
What makes the mempool design interesting is its fairness model. Transactions from remote peers and local clients are handled with different priority — local submissions (from the node operator's own wallet or CLI) receive equal weight that may exceed the per-remote-peer allocation, preventing remote traffic from starving local submissions. A timeout system discards transactions that take too long to validate and can disconnect peers that consistently submit expensive-to-validate transactions.
When the chain tip changes (a new block arrives or a rollback occurs), the mempool re-validates all its transactions against the new ledger state, automatically evicting any that have become invalid.
The Hard Fork Combinator internals
The HFC's implementation — which we discussed from the cardano-node perspective earlier — uses a data structure called a Telescope to track the current era and all past eras. Think of it as a heterogeneous linked list where each element is either a Past (containing the start and end boundaries of a completed era) or the Current (containing the active era's state). When a hard fork occurs, the telescope extends — translating the current era's final state into the next era's initial state and pushing the old era into the past.
The CardanoBlock type that the node actually processes is defined as:
type CardanoBlock c = HardForkBlock (CardanoEras c)where CardanoEras is a type-level list of all eight eras. The HFC dispatches every operation — validation, serialisation, chain selection, queries — to the correct era-specific implementation based on which era a given block belongs to.
ouroboros-network — The Nervous System
The ouroboros-network repository implements the networking and diffusion layer — everything involved in getting blocks and transactions between nodes across the internet. It is a substantial codebase in its own right, containing twelve sub-packages and a formal specification document.
Mini-protocols: Type-safe network conversations
Cardano's network communication is built on mini-protocols — small, focused state machines that each handle one aspect of node-to-node interaction. The node runs multiple mini-protocols simultaneously over a single TCP connection:
- ChainSync — A downstream node follows an upstream node's chain. The consumer requests headers, the producer streams them. This is how nodes learn about new blocks without downloading the full block bodies.
- BlockFetch — Downloads ranges of block bodies from peers. Uses a pull-based strategy where the node decides which blocks to fetch from which peers, optimising for throughput and redundancy.
- TxSubmission2 — Distributes transactions across the network. The protocol reverses the usual client/server relationship — the "server" pushes transaction IDs and the "client" requests the ones it has not seen.
- KeepAlive — Maintains connections and detects stale peers.
- PeerSharing — Allows peers to share knowledge of other peers for dynamic topology discovery.
For local applications (wallets, CLI, db-sync) connecting via Unix domain sockets, there are separate node-to-client protocols: LocalTxSubmission (submit transactions with full validation feedback), LocalStateQuery (query the current ledger state at a specific chain point), and LocalTxMonitor (monitor the mempool).
typed-protocols: Deadlock-free by construction
Each mini-protocol is defined using the typed-protocols framework, which uses Haskell's type system to make protocol violations impossible to compile. The key idea is that every protocol state has an explicit agency assignment — at any given moment, exactly one party (client or server) is allowed to send a message. The framework provides type-level proofs (exclusion lemmas) that no state allows both parties to send simultaneously, and no state combines agency with termination. This makes deadlocks structurally impossible.
The framework also supports pipelining — sending multiple requests before waiting for responses — with type-level tracking of the pending response queue. This hides network latency during chain sync and block fetch, where the node may have hundreds of outstanding block requests in flight simultaneously.
The multiplexer
The network-mux package runs all mini-protocols over a single TCP connection. Each data segment on the wire carries a mini-protocol ID, a mode bit (initiator or responder), a timestamp, and up to 64KB of payload. The multiplexer is abstracted over the underlying transport — the same code works over TCP sockets, Unix domain sockets, in-memory queues, and attenuated channels (for testing with simulated network conditions). The package even includes a Wireshark plugin for debugging protocol-level issues on the wire.
Peer governance: Cold, warm, and hot
The P2P networking layer classifies every known peer into one of three states:
- Cold — Known but not connected. The node has learned about this peer (from DNS, ledger data, or peer sharing) but has not established a connection.
- Warm — Connected with an active bearer, but only running measurement protocols (KeepAlive). The connection is established and healthy, but not yet doing useful consensus work.
- Hot — Fully active, running all consensus mini-protocols (ChainSync, BlockFetch, TxSubmission). These are the peers the node is actually syncing with.
The Peer Selection Governor (PSG) promotes and demotes peers to maintain target numbers at each level. Cold peers are promoted to warm by establishing connections; warm peers are promoted to hot by activating consensus protocols; underperforming hot peers are demoted back to warm; and stale warm peers are demoted to cold.
The Peer Churn Governor works alongside the PSG to resist eclipse and partition attacks. It scores hot peers by their block provisioning performance (how many blocks they deliver during a measurement window), refreshes approximately 20% of peers hourly once the node is synced, and applies "tepidity flags" to mark unreliable peers. During initial sync, it limits active connections to avoid overwhelming the node's bandwidth.
plutus — The Smart Contract Platform
The plutus repository is the largest codebase in the ecosystem. It implements the Plutus smart contract platform — the language, compiler, and on-chain evaluator that powers Cardano's programmable transaction validation.
The language: Plutus Core
Plutus Core is based on System F-omega-mu — a variant of the polymorphic lambda calculus extended with higher-order type operators and iso-recursive types. The choice of foundation is deliberate: lambda calculus dates to the 1930s, making it one of the most studied and stable foundations in computer science. This stability matters for a blockchain — scripts stored on-chain today must evaluate identically decades from now.
The on-chain language is intentionally minimal. There are no loops, no mutable variables, no algebraic data types at the surface level. Recursive types are handled through explicit wrap/unwrap operations. All evaluation is strict (unlike Haskell itself). This minimalism makes the language easy to reason about formally and straightforward to implement alternative evaluators for.
The compiler pipeline
Smart contract developers write in Plutus Tx (recently rebranded as Plinth), a subset of Haskell. The compilation pipeline has four stages:
- Haskell to GHC Core — The standard GHC compiler processes the source code normally, producing GHC's internal Core representation.
- GHC Core to Plutus IR (PIR) — A GHC compiler plugin (the
plutus-tx-plugin) intercepts the Core representation and transforms it into Plutus IR, a high-level intermediate language that still has algebraic data types and recursive bindings. - PIR to Typed Plutus Core (TPLC) — Data types are eliminated via Scott encoding (for example,
Boolbecomesforall r. r -> r -> r— a function that takes two arguments and returns one of them). Recursive bindings are converted to explicit fixpoint combinators. - TPLC to Untyped Plutus Core (UPLC) — Type annotations are erased. Polymorphic arguments get
delay/forcewrappers to preserve strictness semantics. Variables are converted to de Bruijn indices (numeric distances to binding sites rather than names). The result is serialised using the flat binary format and included in transactions.
The CEK machine: Deterministic evaluation
Scripts are evaluated on-chain by a CEK machine — a well-studied abstract machine from programming language theory. The name describes its three components:
- Control — The current term being evaluated.
- Environment — Variable-to-value mappings, implemented as random-access lists for efficient de Bruijn index lookup.
- Kontinuation — The evaluation context as a stack of frames (what to do after the current term is evaluated).
The machine alternates between a computing phase (evaluating terms, pushing frames) and a returning phase (popping frames, applying values). Every step is costed — both CPU and memory usage are tracked via an ExBudget counter. If a script exhausts its budget, evaluation terminates immediately and the transaction fails. This makes execution fees fully deterministic and predictable before submission.
A slippage optimisation updates the budget counter only every ~200 steps rather than on every single operation, reducing accounting overhead while bounding the maximum overshoot. For debugging, a steppable variant exposes the full machine state at each transition.
Cost models: Deterministic fees
Every built-in function — from integer addition to cryptographic signature verification — has a cost model that maps input sizes to CPU and memory units. These models are generated through a rigorous pipeline: Criterion benchmarks measure real execution times across varying input sizes, R scripts fit linear models to the benchmark data, and the resulting coefficients are encoded as 64-bit integers representing picoseconds. This ensures platform-reproducible determinism — the same script costs exactly the same fee on every node, regardless of hardware.
The cost model parameters are protocol parameters, meaning they can be updated through on-chain governance without a hard fork.
The constitution script
One of the more remarkable artefacts in the repository is cardano-constitution — a Plutus smart contract that implements the Cardano network's constitution. Under Conway-era governance, protocol parameter changes must satisfy constitutional guardrails (minimum and maximum bounds, rate-of-change limits, etc.). These guardrails are enforced by a Plutus V3 script that validates every governance action. The network's constitution is, quite literally, executable code on chain.
Conformance testing
The plutus-conformance package provides an official test suite that any Plutus Core evaluator must pass — not just the Haskell reference implementation, but also alternative evaluators like Aiken's Rust-based implementation and the Agda mechanised metatheory. Each test case specifies a UPLC program, its expected output, and its expected CPU/memory budget. This enables a multi-implementation ecosystem while guaranteeing behavioural equivalence.
cardano-api, cardano-cli, and cardano-base
Three additional repositories complete the stack:
The cardano-api provides a high-level Haskell API that abstracts over the raw ledger, consensus, and network types. Rather than requiring users to work with era-specific types directly, the API provides a unified interface for building transactions, querying node state, and managing keys — handling the complexity of the multi-era world behind the scenes.
The cardano-cli is the command-line tool that most node operators interact with daily — building transactions, querying the chain tip, registering stake pools, submitting governance actions, and managing keys. Built on top of cardano-api, it was split into its own repository in 2023 to allow independent release cycles.
The cardano-base provides foundational libraries shared across the entire stack: abstract cryptographic interfaces for VRF, KES, and DSIGN signature schemes (cardano-crypto-class), the concrete Praos-specific crypto implementations with libsodium bindings (cardano-crypto-praos), CBOR serialisation (cardano-binary), and slot/epoch arithmetic (cardano-slotting). Nearly every other repository in the ecosystem depends on cardano-base.
Supporting infrastructure
Several other repositories play essential supporting roles:
- io-sim — An IO simulator that allows the consensus and networking layers to be tested in a deterministic simulated environment. The
io-classessub-package provides abstract interfaces for IO, STM, timers, and concurrency — the consensus code is written against these abstractions rather than real IO, meaning entire protocol interactions can be replayed deterministically in tests. - lsm-tree — A Haskell implementation of Log-Structured Merge Trees, being developed as the next-generation on-disk storage backend for the UTxO set (the successor to the LMDB approach discussed earlier).
- cardano-haskell-packages (CHaP) — The custom Cabal package repository that hosts versioned releases of all Cardano Haskell packages. This is how cardano-node resolves its external dependencies — rather than pinning git commits, each dependency publishes releases to CHaP with proper versioning.
The full picture
All told, the software stack that produces a running cardano-node binary spans at least a dozen major repositories, hundreds of thousands of lines of Haskell, and contributions from hundreds of engineers across the IOG and Intersect ecosystem. The cardano-node repository is the tip of an iceberg — the integration point where consensus, networking, ledger rules, smart contract evaluation, cryptographic primitives, and formal specifications all converge into the single binary that every node operator runs.
Notable Engineering Patterns
With the full stack now mapped — from ledger rules to consensus to networking to smart contracts — it is worth highlighting a few patterns in the cardano-node codebase itself that stand out as particularly interesting:
Existential Typing for Protocol Abstraction
The SomeConsensusProtocol type uses an existential wrapper to hide the specific block type while requiring all necessary type class constraints:
data SomeConsensusProtocol where
SomeConsensusProtocol
:: ( Protocol IO blk
, HasKESMetricsData blk
, HasKESInfo blk
, TraceConstraints blk
)
=> BlockType blk
-> ProtocolInfoArgs blk
-> SomeConsensusProtocolIn plain English: the node needs to work with different block types depending on the era (Byron blocks look nothing like Conway blocks), but the code that runs the node should not need to know which specific block type is in use. The SomeConsensusProtocol type solves this by saying: "I contain some block type — I will not tell you which one — but I guarantee it supports running the protocol, reporting KES metrics, providing KES key info, and being traced." The rest of the node can work with this value generically, and the compiler enforces that every required capability is present. It is a textbook application of existential types in production Haskell.
Thunk Detection with nothunks
The codebase uses the nothunks library and a compile-time flag (-funexpected_thunks) to detect space leaks at runtime. In a long-running process like a blockchain node, an unevaluated thunk in the wrong data structure can accumulate into a memory leak over days or weeks. The NoThunks type class provides assertions that critical data structures are fully evaluated.
Monoidal Configuration Composition
The PartialNodeConfiguration type is a record where every field is wrapped in Last (from Data.Monoid). This gives it a Semigroup instance where the rightmost value wins:
nc <- makeNodeConfiguration $
defaultPartialNodeConfiguration
<> configYamlPc
<> cmdPcDefaults, config file values, and CLI overrides compose naturally with <>. No ad-hoc merging logic, no special-casing — just monoid laws.
Testing Infrastructure
These patterns are not academic exercises — they exist because the stakes demand them. For software that underpins a multi-billion dollar network, testing is not optional — and the cardano-node repository takes it seriously at multiple levels:
Unit tests use the tasty framework with both HUnit (traditional assertion-based tests) and QuickCheck (randomised property checking). QuickCheck is a Haskell invention that has since been ported to nearly every other language — it generates thousands of random inputs and verifies that properties hold across all of them, which is far more thorough than hand-written test cases.
Property-based tests go further using hedgehog (and hedgehog-extras) to generate random configurations, network topologies, and transaction sequences. Rather than testing "does this specific transaction succeed?", property-based tests verify invariants like "no valid transaction should ever cause the ledger to enter an inconsistent state" — across thousands of randomly generated scenarios.
Integration tests via cardano-testnet are where things get genuinely impressive. This framework programmatically spins up real multi-node networks — complete with block-producing nodes, relay nodes, and a functioning consensus protocol — all running locally. Tests then submit transactions, register stake pools, vote on governance actions, execute Plutus smart contracts, and verify outcomes against expected chain state. The test suite covers Conway-era governance features including DRep registration and activity, treasury withdrawals, constitutional committee proposals, and parameter updates. This is not mock testing — these are real nodes running real consensus.
The chairman tool connects to multiple live nodes simultaneously and asserts they converge on the same chain — a form of consensus smoke test that verifies the fundamental property any blockchain must have: agreement.
Benchmarks use criterion for micro-benchmarks and a full tx-generator for end-to-end throughput testing. The tx-generator can sustain controlled transaction loads against a test cluster, measuring throughput, latency, and resource consumption under realistic conditions.
Why Haskell?
Having walked through the architecture, it is worth stepping back to address the elephant in the room: the decision to build all of this in Haskell. It is one of the most discussed choices in the blockchain space, and after reading through 104,000 lines of the codebase, both the strengths and the costs are clearly visible.
The choice was made early by IOG (then IOHK), led by Charles Hoskinson and chief scientist Philip Wadler — himself a contributor to the Haskell language standard. The rationale was rooted in the project's academic origins and its ambition to build a blockchain platform that could be formally verified.
What Haskell brings to the table
Strong static typing catches entire categories of bugs at compile time. The type system is expressive enough to encode protocol invariants directly — the existential types in SomeConsensusProtocol, the phantom type parameters in the era system, and the type-level era indices in the Hard Fork Combinator are all examples of using types to make illegal states unrepresentable. In a system that secures billions of dollars, this matters.
Purity and explicit effects make reasoning about code easier. In Haskell, a function that performs IO must declare it in its type signature. This means you can look at a type like GenesisConfig -> Either GenesisError ProtocolInfo and know it is a pure computation — no network calls, no disk access, no hidden state mutation. For a codebase where subtle side effects could mean consensus failures, this is a genuine advantage.
Lazy evaluation, while sometimes a footgun, enables elegant compositional patterns. The tracer combinators, the monoidal configuration system, and the Hard Fork Combinator's era-indexed type machinery all leverage Haskell's ability to compose abstractions cleanly. The contra-tracer library is a particularly good example — contravariant functors are a natural fit for tracing, but they are awkward to express in most mainstream languages.
The ecosystem for formal methods is strong. Cardano's ledger rules are specified in a formal notation and then implemented in Haskell, with the gap between spec and implementation being as small as the language allows. This is not a theoretical exercise — the ledger specs are published academic papers, and the implementation closely mirrors them.
The costs — and they are significant
The barrier to entry is exceptionally high. This is the elephant in the room. Haskell is not a language most software engineers encounter in their careers. The TIOBE index consistently ranks it outside the top 20, and the Stack Overflow Developer Survey shows it used by a tiny fraction of professional developers. The concepts that make Haskell powerful — monads, type classes, existential quantification, higher-kinded types, GADTs — require months of dedicated study for an experienced developer coming from Python, Java, or even Rust. This is not a complaint about individual ability; it is a structural reality about the language's learning curve. A competent engineer who could meaningfully contribute to Ethereum's Go client or Solana's Rust client within weeks would likely need months before being productive in the cardano-node codebase.
The contributor community suffers directly. Open-source ecosystems thrive on a long tail of contributors who fix bugs, improve documentation, add features, and review code. When the language itself is a gate that excludes 95%+ of the developer population, that ecosystem shrinks dramatically. Compare the contributor counts: Ethereum's Geth (Go) has over 900 contributors, Solana's validator (Rust) has over 400, while cardano-node has around 130 — despite Cardano being comparable in market cap and ambition. This is not a coincidence. The language choice directly constrains the size of the community that can participate in the network's most critical software.
Build tooling compounds the problem. Even once you learn enough Haskell to read the code, getting a working development environment is its own challenge. The dual Nix/Cabal build system is powerful but adds significant onboarding friction. Getting set up requires either Nix (which has its own steep learning curve) or careful Cabal configuration with the right GHC version and CHaP repository. By contrast, cargo build or go build gets you a working binary in seconds. Every layer of tooling complexity further narrows the pool of people who will persevere through setup to actually contribute.
Runtime performance requires deep GHC expertise. Haskell's lazy evaluation means that naive code can accumulate unevaluated thunks, leading to space leaks that manifest as gradual memory growth over days or weeks. The nothunks library and the -funexpected_thunks flag exist precisely because this is a known operational risk. The GHC RTS flags shown earlier (-I0, -A16m, --disable-delayed-os-memory-return) are evidence of careful tuning required to keep the garbage collector well-behaved in a long-running process. Languages like Rust, which offer zero-cost abstractions and manual memory management without a garbage collector, simply do not have this class of problem.
Runtime overhead has real operational consequences. Haskell's garbage-collected runtime, combined with lazy evaluation and persistent data structures, results in higher baseline memory consumption compared to systems languages. A fully synced cardano-node on mainnet recommends 24GB of RAM with the in-memory UTxO backend — a figure that would raise eyebrows in the Rust or Go world, where equivalent workloads typically fit in a fraction of the memory. This is not just a number on a spec sheet. Higher memory requirements mean stake pool operators need more expensive hardware, which raises the cost floor for running a pool, which in turn affects decentralisation. In a network that aspires to thousands of independent stake pools, every gigabyte of unnecessary RAM is a barrier to participation. The LMDB backend trades memory for disk I/O, but it introduces its own complexity and performance trade-offs. GHC's stop-the-world garbage collection pauses — however brief — also introduce latency spikes that must be carefully managed in a system where block propagation time matters for consensus.
Library ecosystem gaps create maintenance burden. While Hackage has deep libraries for parsing, type theory, and formal methods, it lags behind ecosystems like Rust's crates.io or Node's npm for systems programming, networking, and general-purpose tooling. The Cardano project has had to build and maintain a substantial amount of custom infrastructure — CHaP (an entire package repository), haskell.nix (a Nix-to-Cabal bridge), custom FFI bindings for cryptographic libraries — that would come off the shelf in other languages. Every piece of custom infrastructure is more code to maintain and more surface area for bugs.
The verdict in practice
The honest assessment is that Haskell was a defensible choice for correctness but an expensive one for ecosystem growth. The type system and purity guarantees have likely prevented entire classes of consensus bugs, and the Hard Fork Combinator — an architecturally novel piece of software — would be substantially harder to build with type safety in a less expressive type system.
But it is hard to ignore the counterfactual. Rust offers a type system that, while less expressive than Haskell's, is powerful enough for most of these guarantees — and it comes with a massive, growing developer community, excellent tooling, predictable performance characteristics, and no garbage collector. Ethereum's transition to Rust-based clients (Reth, Lighthouse) has been accompanied by a surge in contributor activity. It is reasonable to ask whether Cardano would have a larger, more vibrant core development community today if the node had been written in Rust from the start.
The Cardano developer community has pragmatically recognised this by increasingly adopting Rust and TypeScript for tooling, wallets, and dApps, while reserving Haskell for the core protocol layer. Whether this split represents the right long-term equilibrium — or whether the core itself should eventually transition — is a question the community will need to grapple with as the project matures.
The Ecosystem by the Numbers
Since cardano-node is an integration layer, looking at its commit count in isolation is misleading. The real picture emerges when you consider the full ecosystem of repositories that together produce a running node.
Across the eight core repositories — cardano-node, cardano-ledger, ouroboros-consensus, ouroboros-network, plutus, cardano-api, cardano-cli, and cardano-base — the ecosystem has accumulated over 56,000 unique commits from more than 300 contributors, all written in 100% Haskell. (The raw commit total across all repos is higher, around 77,000, but cardano-node, cardano-api, and cardano-cli share git history since the latter two were split out in 2023.)
Where the work happens
The commit distribution reveals which repositories carry the most weight:
- cardano-ledger (11,900 commits) — the single busiest repository, reflecting the constant evolution of ledger rules across eight eras. Every new era brings new transaction types, new validation rules, new protocol parameters, and new governance mechanics — all implemented and tested here.
- plutus (11,841) — nearly as active as the ledger, driven by the complexity of building and maintaining a smart contract platform with its own compiler, evaluator, cost models, and formal metatheory.
- ouroboros-consensus (10,614) — the consensus engine, where the Hard Fork Combinator, ChainDB, block production, and chain selection logic live.
- cardano-node (10,378) — the integration layer itself, including tracing, configuration, and the benchmarking suite.
- ouroboros-network (10,164) — the P2P networking stack, mini-protocol framework, and peer management.
- cardano-base (1,147) — the foundational crypto and serialisation libraries, smaller but touched by nearly every other repo.
The oldest repository is plutus, which dates to November 2016 — predating the Cardano mainnet launch by almost a year. The ledger and network repos followed in 2018. The node itself appeared in May 2019, and the api/cli repositories were carved out in 2023 as the codebase matured enough to warrant separate release cycles.
The people behind the code
When you aggregate contributors across the ecosystem, the picture is one of deep specialisation with key engineers serving as bridges between repositories:
- lehins (6,839 commits) — by far the most prolific contributor in the ecosystem, overwhelmingly focused on cardano-ledger. This is the person who implements the ledger rules that every Cardano transaction must satisfy.
- coot (5,211) — the dominant force in the networking and consensus layers, responsible for much of the P2P transition and protocol plumbing.
- dcoutts (4,592) — one of the principal architects of the Haskell ecosystem itself (co-author of Cabal, GHC contributor), working across the node, consensus, and networking layers. A genuine cross-cutting architect.
- mrBliss (2,983) — deep work on both consensus and networking, particularly the ChainDB and diffusion layer integration.
- michaelpj (2,512) — the leading Plutus contributor, responsible for much of the smart contract compiler and evaluator.
- nc6 (2,423) and teodanciu (2,139) — major ledger contributors alongside lehins, shaping the formal specification and Haskell implementation of each era's rules.
A handful of engineers — notably dcoutts, erikd, and newhoggy — contribute meaningfully across four or more repositories, providing the cross-cutting architectural knowledge that holds a system this distributed together. But most contributors are specialists in a single layer, reflecting the depth of expertise each component demands.
What the numbers reveal
The concentration pattern is striking. Across the ecosystem, the top 20 contributors account for the vast majority of commits. This is not unusual for a Haskell project of this complexity — the language's steep learning curve means that new contributors take longer to become productive, and the depth of domain knowledge required (consensus theory, formal ledger specifications, typed protocol design) further narrows the pool.
The 5,700 GitHub stars across the ecosystem might seem modest compared to Ethereum's Geth (47,000+) or Solana's validator (13,000+), but stars are a trailing indicator of community size rather than code quality. The real indicator is the sustained commit activity across all eight repositories — this is a codebase that is actively developed, not just maintained.
Contributing and Governance
The cardano-node repository is maintained under the Intersect member-based organisation (having transitioned from IOG's direct stewardship). It uses a CODEOWNERS file for review routing and follows a structured release process through the RELEASE.md workflow. Development is Nix-first, and contributions are expected to work with both the Nix and Cabal build paths.
The copyright headers in the Cabal files reflect the project's lineage: "2019-2023 Input Output Global Inc (IOG), 2023-2026 Intersect." — a tangible record of the decentralisation of Cardano's development itself.
Why This Matters for Delegators
What you have just read is a tour through one of the most ambitious software engineering projects in the blockchain space — a formally-specified, type-safe, multi-era consensus system written in hundreds of thousands of lines of Haskell, maintained by hundreds of contributors, and securing billions of dollars in value without interruption.
As a delegator, you are trusting your stake to a pool operator who runs this software 24/7. Understanding what the node actually does — how it validates blocks through hierarchical state transition systems, manages peers through a three-tier governance model, handles upgrades seamlessly via the Hard Fork Combinator, and monitors itself through a type-safe tracing pipeline — gives you a better framework for evaluating pool operators.
At Sandstone, we run our infrastructure on the latest stable node releases, monitor via the cardano-tracer pipeline, and maintain our topology for optimal connectivity to the network. When you delegate to Sandstone, you are delegating to operators who understand this software at a deep level — because running a stake pool is more than uptime. It is about knowing what is happening inside the machine.
Delegate to SAND today and support a pool built on deep technical understanding.
- Pool Ticker: SAND
- Follow us on X for updates
Happy staking.