Skip to content

Coxy Haskell Cardano Blockchain

Bernard Sibanda edited this page Jul 24, 2026 · 1 revision

Table of Contents

  1. 🔐 Build One — Core Types, Cryptography, Transactions, and the UTxO Ledger (v0.1)
  2. 🧱 Build Two — Blocks, the Mempool, and Block Production (v0.2)
  3. ⚖️ Build Three — Slots, Stake, Leader Selection, and Basic Consensus (v0.3)
  4. 🗄️ Build Four — Persistent Chain Store, Forks, Rollback, and Restart Recovery (v0.4)
  5. 🌐 Build Five — Peer-to-Peer Synchronisation with Bounded Queues (v0.5)
  6. 🖥️ Build Six — The Supervised Node Runtime (v0.6)
  7. 🔌 Build Seven — JSON-RPC and Real-Time WebSocket Services (v0.7)
  8. 🔎 Build Eight — Rollback-Aware Indexing and the Explorer API (v0.8)
  9. 📜 Build Nine — Typed Scripts, Execution Budgets, and Signed Oracles (v0.9)
  10. 👛 Build Ten — Deterministic Wallets, Transaction Building, and the Wallet API (v1.0)
  11. 🏛️ Build Eleven — Stake, Governance, and Encrypted Wallet Storage (v1.1)
  12. 💰 Build Twelve — Economics, Genesis, Protocol Parameters, and Era Transitions (v1.2)
  13. 📊 Build Thirteen — Operator CLI, Structured Logging, Metrics, and Safe Maintenance (v1.3)
  14. 🔒 Build Fourteen — Real TCP Transport, Secure Channels, and Peer Management (v1.4)
  15. 🧬 Build Fifteen — Canonical Protocol Codec, Capability Negotiation, and Network Simulation (v1.5)
  16. 🧪 Build Sixteen — Property Testing, Model Checking, Fuzzing, and Benchmarks (v1.6)
  17. ✅ Build Seventeen — Executable Formal Specifications and Release Qualification (v1.7)
  18. 🚀 Build Eighteen — Coordinated Upgrades, Cluster Deployment, and Disaster Recovery (v1.8)
  19. 🤖 Build Nineteen — Remote Upgrade Agents, Control API, and Protected Secrets (v1.9)
  20. 📡 Build Twenty — Mutual-TLS Control Transport, Artifact Transfer, and Deployment Adapters (v2.0)
  21. 🛡️ Build Twenty-One — High-Availability Control Plane and Cross-Region Recovery (v2.1)
  22. 🧭 From COXY Training Code to the Real Cardano Ecosystem
  23. 🧰 Complete Build, Test, and Troubleshooting Workflow
  24. 🤝 Working Professionally with Cardano Open-Source Communities
  25. 📚 Glossary of Terms
  26. 🔗 Official Cardano Learning and Contribution Resources

Company: Coxygen Global
Developer: Bernard Sibanda
Publication date: 24 July 2026
Version: 2.1.0.0

Educational scope. This project is a modular Haskell training blockchain. It uses real cryptographic libraries and executable engineering practices, but it is not Cardano, Bitcoin, Ouroboros, Plutus, TLS, Raft, or a production wallet. Students must understand the boundary between a teaching model and a production protocol before deploying or contributing code.

How to use this tutorial

This tutorial is organised as twenty-one cumulative builds. A student should complete them in order because each build reuses invariants, types, storage formats, and operational practices established earlier. Every build contains an explanation, an architecture flow, excerpts taken from the actual version 2.1 source tree, a practical laboratory, a test checkpoint, professional security notes, and a bridge to the corresponding Cardano open-source area.

The source snippets are intentionally selective. Students must open the referenced module and read the complete function, its imports, its callers, and its tests. Copying a snippet without understanding the surrounding types is not considered completion.

Learning Outcomes

After completing all builds, a student should be able to explain and demonstrate how a typed UTxO ledger validates signed transactions; how blocks, slots, stake, forks, chain storage, and P2P synchronization fit together; how wallets, scripts, oracles, staking, governance, indexing, APIs, monitoring, deployment, disaster recovery, and control-plane high availability are engineered; and how to transfer those skills into the real Cardano open-source ecosystem.

Prerequisites and supported training environment

  • Students should know basic Haskell syntax, algebraic data types, pattern matching, Either, Maybe, lists, maps, records, type classes, and IO.
  • Students should be able to use Git, GitHub, a Linux shell, Cabal, and a text editor such as Visual Studio Code.
  • The project was designed around GHC 8.10.7. Use the compiler version specified by the repository and do not silently upgrade dependencies during a training assessment.
  • Use a disposable training directory. Never place real wallet recovery phrases, production Cardano keys, cloud credentials, or certificate-authority keys inside this project.
  • Complete every cabal test run before moving to the next build. A working demonstration without passing tests is incomplete.

Initial setup

unzip coxy-training-chain-v2.1.0.zip
cd coxy-training-chain-v2.1.0

ghc --version
cabal --version
cabal update
cabal clean
cabal build all
cabal test all --test-show-details=direct
cabal run coxy-training-chain-demo

Build Sequence at a Glance

<style> .table-responsive { width: 100%; overflow-x: auto; margin: 1.5rem 0; border: 1px solid #d8dee4; border-radius: 10px; } .tutorial-table { width: 100%; min-width: 760px; border-collapse: collapse; font-family: Arial, Helvetica, sans-serif; font-size: 0.95rem; line-height: 1.5; background: #ffffff; } .tutorial-table th, .tutorial-table td { padding: 11px 13px; border: 1px solid #d8dee4; text-align: left; vertical-align: top; } .tutorial-table thead th { background: #0033ad; color: #ffffff; font-weight: 700; } .tutorial-table tbody tr:nth-child(even) { background: #f6f8fa; } .tutorial-table tbody tr:hover { background: #eef4ff; } .tutorial-table code { display: inline-block; margin: 1px; padding: 2px 5px; border-radius: 4px; background: #edf2f7; color: #1a202c; font-size: 0.9em; } .build-sequence-table { min-width: 1000px; } .build-sequence-table th:nth-child(1), .build-sequence-table td:nth-child(1), .build-sequence-table th:nth-child(2), .build-sequence-table td:nth-child(2) { white-space: nowrap; } @media (max-width: 768px) { .tutorial-table { font-size: 0.88rem; } .tutorial-table th, .tutorial-table td { padding: 9px 10px; } } </style>
Build Version Primary modules Capability added
Build One v0.1 Types.hs, Crypto.hs, Transaction.hs, Ledger.hs Core Types, Cryptography, Transactions, and the UTxO Ledger
Build Two v0.2 Block.hs, Mempool.hs, BlockProducer.hs Blocks, the Mempool, and Block Production
Build Three v0.3 Consensus.hs Slots, Stake, Leader Selection, and Basic Consensus
Build Four v0.4 ChainStore.hs Persistent Chain Store, Forks, Rollback, and Restart Recovery
Build Five v0.5 P2P.hs Peer-to-Peer Synchronisation with Bounded Queues
Build Six v0.6 Node.hs The Supervised Node Runtime
Build Seven v0.7 RPC.hs, WebSocket.hs JSON-RPC and Real-Time WebSocket Services
Build Eight v0.8 Indexer.hs, ExplorerAPI.hs Rollback-Aware Indexing and the Explorer API
Build Nine v0.9 Script.hs, Oracle.hs Typed Scripts, Execution Budgets, and Signed Oracles
Build Ten v1.0 Wallet.hs, WalletAPI.hs Deterministic Wallets, Transaction Building, and the Wallet API
Build Eleven v1.1 Stake.hs, Governance.hs, WalletStore.hs Stake, Governance, and Encrypted Wallet Storage
Build Twelve v1.2 Economics.hs, Genesis.hs, ProtocolParams.hs Economics, Genesis, Protocol Parameters, and Era Transitions
Build Thirteen v1.3 CLI.hs, Logging.hs, Metrics.hs Operator CLI, Structured Logging, Metrics, and Safe Maintenance
Build Fourteen v1.4 Transport.hs, TLS.hs, PeerManager.hs Real TCP Transport, Secure Channels, and Peer Management
Build Fifteen v1.5 ProtocolCodec.hs, VersionNegotiation.hs, NetworkSimulation.hs Canonical Protocol Codec, Capability Negotiation, and Network Simulation
Build Sixteen v1.6 PropertyTesting.hs, ModelChecking.hs, Benchmark.hs Property Testing, Model Checking, Fuzzing, and Benchmarks
Build Seventeen v1.7 FormalSpec.hs, AdversarialTesting.hs, ReleaseQualification.hs Executable Formal Specifications and Release Qualification
Build Eighteen v1.8 UpgradeCoordinator.hs, ClusterDeployment.hs, DisasterRecovery.hs Coordinated Upgrades, Cluster Deployment, and Disaster Recovery
Build Nineteen v1.9 UpgradeAgent.hs, ClusterControlAPI.hs, SecretsBackend.hs Remote Upgrade Agents, Control API, and Protected Secrets
Build Twenty v2.0 ControlTransport.hs, AgentControlAPI.hs, ArtifactTransfer.hs, DeploymentAdapter.hs, VirtualHostIntegration.hs Mutual-TLS Control Transport, Artifact Transfer, and Deployment Adapters
Build Twenty-One v2.1 ControlPlaneHA.hs, DurableControlStore.hs, HAControlRuntime.hs, HAClusterControlAPI.hs, CertificateLifecycle.hs, RolloutApproval.hs, CrossRegionRecovery.hs High-Availability Control Plane and Cross-Region Recovery

Core Engineering Rules Used in Every Build One. Make invalid states difficult to represent. Use domain-specific types instead of passing unrelated strings and integers through the system.

  1. Keep pure validation separate from effects. Pure functions are easier to test, replay, model-check, and compare with reference specifications.
  2. Treat encoding as protocol logic. A hash or signature is meaningful only when all participants serialize the same semantic value identically.
  3. Validate at every trust boundary. Peers, API clients, operators, artifacts, storage files, and recovered snapshots are all untrusted until checked.
  4. Bound every external resource. Limit frame sizes, request bodies, page sizes, queues, script budgets, recursion, collection sizes, retries, and concurrent connections.
  5. Design for rollback and restart. A blockchain and its operational control plane must recover from forks, process failures, machine failures, and partial deployments.
  6. Preserve evidence. Logs, metrics, tests, replay seeds, audit chains, qualification reports, manifests, and runbooks turn a demonstration into an engineering process.

1. 🔐 Build One — Core Types, Cryptography, Transactions, and the UTxO Ledger

Release stage: v0.1
Primary modules: Types.hs, Crypto.hs, Transaction.hs, Ledger.hs

Purpose

This build establishes the smallest trustworthy core of the training blockchain. It defines the data that every later subsystem shares, uses real SHA-256 hashing and Ed25519 signatures, constructs deterministic transaction identifiers, and applies transactions to a UTxO ledger without allowing value creation or double spending.

Learning objectives

  • Algebraic data types model addresses, coins, transaction references, outputs, and validation errors explicitly.
  • Cryptographic keys are separated into signing and verification roles, and signatures are checked against the exact transaction body.
  • The ledger consumes existing UTxOs and creates new UTxOs only when signatures, ownership, uniqueness, fees, and value conservation all pass.

Architecture flow

Signing key → verification key → address
        ↓
Unsigned transaction → deterministic body bytes → signature
        ↓
Ledger validation → consume inputs → create outputs → new ledger state

How this build works

This build establishes the smallest trustworthy core of the training blockchain. It defines the data that every later subsystem shares, uses real SHA-256 hashing and Ed25519 signatures, constructs deterministic transaction identifiers, and applies transactions to a UTxO ledger without allowing value creation or double spending. Algebraic data types model addresses, coins, transaction references, outputs, and validation errors explicitly. Cryptographic keys are separated into signing and verification roles, and signatures are checked against the exact transaction body. The ledger consumes existing UTxOs and creates new UTxOs only when signatures, ownership, uniqueness, fees, and value conservation all pass.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Key generation, hashing, signing, verification, and address derivation

Source: src/Coxy/Crypto.hs, lines 24–65 in the version 2.1 training tree.

newtype SigningKey = SigningKey Ed25519.SecretKey

-- | Generate a fresh key using the operating system's cryptographic RNG.
generateSigningKey :: IO SigningKey
generateSigningKey = SigningKey <$> Ed25519.generateSecretKey

-- | Construct a deterministic key from exactly 32 bytes.
-- This is useful for tests and teaching fixtures. Production wallets should
-- derive keys from an audited recovery-phrase and HD-wallet implementation.
signingKeyFromSeed :: ByteString -> Either String SigningKey
signingKeyFromSeed seed =
  case Ed25519.secretKey seed of
    CryptoPassed secret -> Right (SigningKey secret)
    CryptoFailed err    -> Left (show err)

verificationKey :: SigningKey -> VerificationKey
verificationKey (SigningKey secret) =
  VerificationKey
    (convert (Ed25519.toPublic secret) :: ByteString)

signBytes :: SigningKey -> ByteString -> SignatureBytes
signBytes (SigningKey secret) message =
  let publicKey = Ed25519.toPublic secret
      signature = Ed25519.sign secret publicKey message
  in SignatureBytes (convert signature :: ByteString)

verifyBytes :: VerificationKey -> ByteString -> SignatureBytes -> Bool
verifyBytes (VerificationKey publicBytes) message (SignatureBytes signatureBytes) =
  case (Ed25519.publicKey publicBytes, Ed25519.signature signatureBytes) of
    (CryptoPassed publicKey, CryptoPassed signature) ->
      Ed25519.verify publicKey message signature
    _ ->
      False

-- | SHA-256 encoded as lowercase hexadecimal text.
hashBytes :: ByteString -> String
hashBytes bytes =
  show (hash bytes :: Digest SHA256)

-- | Training address format: a readable prefix followed by the first
-- 40 hexadecimal characters of SHA-256(public-key-bytes).
addressFromVerificationKey :: VerificationKey -> Address

Genesis creation and the first stages of transaction validation

Source: src/Coxy/Ledger.hs, lines 39–95 in the version 2.1 training tree.

genesisLedger :: [(TxOutRef, TxOutput)] -> Ledger
genesisLedger genesisOutputs =
  Ledger
    { ledgerUtxos = Map.fromList genesisOutputs
    , ledgerAppliedTxs = Set.empty
    , ledgerOracleRegistry = Map.empty
    , ledgerOracleReports = Map.empty
    }

validateTransaction :: Ledger -> SignedTransaction -> Either ValidationError ()
validateTransaction ledger signedTransaction =
  validateTransactionAt
    (txNonce (signedTxBody signedTransaction))
    ledger
    signedTransaction

validateTransactionAt
  :: Word64
  -> Ledger
  -> SignedTransaction
  -> Either ValidationError ()
validateTransactionAt currentSlot ledger signedTransaction = do
  let transaction = signedTxBody signedTransaction
      txId = transactionId transaction
      utxos = ledgerUtxos ledger
      signerAddress =
        addressFromVerificationKey (signedTxVerificationKey signedTransaction)

  if Set.member txId (ledgerAppliedTxs ledger)
    then Left (TransactionAlreadyApplied txId)
    else Right ()

  if null (txInputs transaction) then Left EmptyInputSet else Right ()
  if null (txOutputs transaction) then Left EmptyOutputSet else Right ()

  case findDuplicateInput (txInputs transaction) of
    Just duplicate -> Left (DuplicateInput duplicate)
    Nothing -> Right ()

  case find ((<= 0) . unCoin . txOutputValue) (txOutputs transaction) of
    Just output -> Left (NonPositiveOutput (txOutputValue output))
    Nothing -> Right ()

  case firstInvalidOutputToken (txOutputs transaction) of
    Just (asset, quantity) -> Left (NonPositiveTokenQuantity asset quantity)
    Nothing -> Right ()

  case firstDatumWithoutScript (txOutputs transaction) of
    Just index -> Left (DatumWithoutScript index)
    Nothing -> Right ()

  if unCoin (txFee transaction) < 0
    then Left (NegativeFee (txFee transaction))
    else Right ()

  if verifyTransactionSignature signedTransaction
    then Right ()

Student practical laboratory

  1. Read the type definitions before reading the validation function, because every later invariant is expressed through these types.
  2. Run the demonstration and record Alice’s balance before and after the signed payment.
  3. Change one output amount after signing and confirm that signature verification rejects the transaction.
  4. Attempt to apply the same signed transaction twice and confirm that the consumed input cannot be spent again.

Use the following workflow after each change:

# Work in a branch dedicated to Build One
git switch -c training/build-01

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano also uses a UTxO-based ledger, but its Extended UTxO model adds scripts, datums, redeemers, multi-assets, protocol parameters, certificates, and era-specific ledger rules. The discipline learned here—explicit types, deterministic serialization, signature verification, and value conservation—transfers directly to Cardano ledger and wallet work.

COXY module Cardano area What to study next
Crypto, Transaction, Ledger cardano-ledger and cardano-api UTxO rules, transaction bodies, witnesses, value conservation

Security and professional practice

Never invent cryptographic algorithms for production. Use maintained libraries, keep private keys out of logs, and treat deterministic encoding as part of the consensus surface.

Completion checkpoint

A student completes Build One only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Types.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

2. 🧱 Build Two — Blocks, the Mempool, and Block Production

Release stage: v0.2
Primary modules: Block.hs, Mempool.hs, BlockProducer.hs

Purpose

This build connects valid transactions to signed blocks. The mempool admits transactions that are currently valid, reserves their inputs to prevent conflicting pending spends, sorts them by fee, and supplies a bounded set to the block producer. A block commits to its body, links to its parent, and is signed by its producer.

Learning objectives

  • The mempool is temporary node state rather than part of the ledger, so it must be revalidated whenever the selected chain changes.
  • A block header commits to the previous block, the transaction-body hash, the producer key, the slot, and the block number.
  • Block production and block validation remain separate so a producer cannot bypass the same checks imposed on received blocks.

Architecture flow

Wallet submission → mempool validation → conflict reservation
        ↓
Fee-priority selection → candidate block body
        ↓
Body hash + parent hash + slot → producer signature → validated block

How this build works

This build connects valid transactions to signed blocks. The mempool admits transactions that are currently valid, reserves their inputs to prevent conflicting pending spends, sorts them by fee, and supplies a bounded set to the block producer. A block commits to its body, links to its parent, and is signed by its producer. The mempool is temporary node state rather than part of the ledger, so it must be revalidated whenever the selected chain changes. A block header commits to the previous block, the transaction-body hash, the producer key, the slot, and the block number. Block production and block validation remain separate so a producer cannot bypass the same checks imposed on received blocks.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Mempool state and admission rules

Source: src/Coxy/Mempool.hs, lines 26–78 in the version 2.1 training tree.

data Mempool = Mempool
  { mempoolTransactions :: Map.Map TxId SignedTransaction
  , mempoolInputOwners  :: Map.Map TxOutRef TxId
  }
  deriving (Eq, Show)

data MempoolError
  = MempoolTransactionInvalid TxId ValidationError
  | MempoolDuplicateTransaction TxId
  | MempoolInputConflict TxOutRef TxId
  deriving (Eq, Show)

emptyMempool :: Mempool
emptyMempool =
  Mempool
    { mempoolTransactions = Map.empty
    , mempoolInputOwners = Map.empty
    }

mempoolSize :: Mempool -> Int
mempoolSize = Map.size . mempoolTransactions

mempoolTxIds :: Mempool -> [TxId]
mempoolTxIds = Map.keys . mempoolTransactions

-- | Highest fee first, with transaction ID as a deterministic tie-breaker.
mempoolTransactionsByFee :: Mempool -> [SignedTransaction]
mempoolTransactionsByFee =
  sortBy compareByFee . Map.elems . mempoolTransactions
  where
    compareByFee left right =
      case compare (transactionFee right) (transactionFee left) of
        EQ -> compare (transactionIdentifier left) (transactionIdentifier right)
        ordering -> ordering

    transactionFee = txFee . signedTxBody
    transactionIdentifier = transactionId . signedTxBody

-- | Backwards-compatible admission using the transaction nonce as a training
-- slot. Runtime nodes should call 'submitTransactionAt' with their clock slot.
submitTransaction
  :: Ledger
  -> SignedTransaction
  -> Mempool
  -> Either MempoolError Mempool
submitTransaction ledger transaction =
  submitTransactionAt (txNonce (signedTxBody transaction)) ledger transaction

submitTransactionAt
  :: Word64
  -> Ledger
  -> SignedTransaction
  -> Mempool

Block-production configuration and produce/apply workflow

Source: src/Coxy/BlockProducer.hs, lines 15–82 in the version 2.1 training tree.

data BlockProducerConfig = BlockProducerConfig
  { maxTransactionsPerBlock :: Int
  }
  deriving (Eq, Show)

data BlockProductionError
  = InvalidBlockCapacity Int
  | NoTransactionsAvailable
  | ProducedBlockRejected BlockValidationError
  deriving (Eq, Show)

-- | Select pending transactions by descending fee and sign a candidate block.
produceBlock
  :: BlockProducerConfig
  -> SigningKey
  -> Word64
  -> ChainState
  -> Mempool
  -> Either BlockProductionError Block
produceBlock config producerKey slot state pool
  | maxTransactionsPerBlock config <= 0 =
      Left (InvalidBlockCapacity (maxTransactionsPerBlock config))
  | null selectedTransactions =
      Left NoTransactionsAvailable
  | otherwise =
      Right
        ( mkSignedBlock
            producerKey
            (chainHeight state + 1)
            slot
            (chainTip state)
            selectedTransactions
        )
  where
    selectedTransactions =
      take
        (maxTransactionsPerBlock config)
        (mempoolTransactionsByFee pool)

-- | Produce, validate, and commit one block. Included transactions are removed
-- and the remainder is revalidated against the new ledger state.
produceAndApplyBlock
  :: BlockProducerConfig
  -> SigningKey
  -> Word64
  -> ChainState
  -> Mempool
  -> Either BlockProductionError (Block, ChainState, Mempool)
produceAndApplyBlock config producerKey slot state pool = do
  block <- produceBlock config producerKey slot state pool
  updatedState <-
    case applyBlock state block of
      Left err      -> Left (ProducedBlockRejected err)
      Right success -> Right success

  let includedIds =
        map (transactionId . signedTxBody) (blockTransactions block)
      withoutIncluded = removeTransactions includedIds pool
      updatedPool =
        revalidateMempoolAt slot (chainLedger updatedState) withoutIncluded

  Right (block, updatedState, updatedPool)

Student practical laboratory

  1. Submit two independent transactions with different fees and inspect the mempool ordering.
  2. Create two transactions that spend the same input and confirm that only the first can reserve it.
  3. Produce a block with a one-transaction capacity and verify that the highest-fee transaction is selected.
  4. Modify a block body after signing and confirm that the body commitment or producer signature fails.

Use the following workflow after each change:

# Work in a branch dedicated to Build Two
git switch -c training/build-02

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano nodes also maintain a local transaction pool and produce blocks under protocol limits. Real Cardano selection additionally considers transaction size, execution units, reference inputs, script collateral, validity intervals, and ledger-era rules.

COXY module Cardano area What to study next
Mempool and BlockProducer cardano-node consensus integration mempool admission, block limits, transaction selection

Security and professional practice

A mempool is attacker-controlled input. Enforce size limits, conflict detection, validation costs, and eviction policies before using this design beyond a classroom environment.

Completion checkpoint

A student completes Build Two only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Block.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

3. ⚖️ Build Three — Slots, Stake, Leader Selection, and Basic Consensus

Release stage: v0.3
Primary modules: Consensus.hs

Purpose

This build introduces a deterministic proof-of-stake training model. Registered producers hold positive stake, time is represented by slots, a public ticket selects a leader in proportion to stake, and competing valid chains are compared using a deterministic selection rule.

Learning objectives

  • The producer registry is keyed by verification key so registration order cannot influence the schedule.
  • Stake-weighted selection maps a deterministic ticket into cumulative stake intervals.
  • Consensus validation checks both producer registration and whether the producer was the selected leader for the block slot.

Architecture flow

Network seed + registry commitment + slot
        ↓ SHA-256 ticket
Ticket mod total stake → stake interval → selected producer
        ↓
Leader signs block → peers verify leadership and block validity

How this build works

This build introduces a deterministic proof-of-stake training model. Registered producers hold positive stake, time is represented by slots, a public ticket selects a leader in proportion to stake, and competing valid chains are compared using a deterministic selection rule. The producer registry is keyed by verification key so registration order cannot influence the schedule. Stake-weighted selection maps a deterministic ticket into cumulative stake intervals. Consensus validation checks both producer registration and whether the producer was the selected leader for the block slot.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Registered producers, stake, and consensus configuration

Source: src/Coxy/Consensus.hs, lines 72–108 in the version 2.1 training tree.

data RegisteredProducer = RegisteredProducer
  { registeredProducerName  :: String
  , registeredProducerKey   :: VerificationKey
  , registeredProducerStake :: Stake
  }
  deriving (Eq, Show)

-- | Producers are keyed by verification key so registration order cannot
-- influence the deterministic schedule.
newtype ProducerRegistry = ProducerRegistry
  (Map.Map VerificationKey RegisteredProducer)
  deriving (Eq, Show)

-- | Public parameters for the training lottery. The seed is deliberately
-- simple and public. A production proof-of-stake protocol needs secure epoch
-- randomness, VRFs, and stronger adversarial analysis.
data ConsensusConfig = ConsensusConfig
  { consensusNetworkId   :: String
  , consensusGenesisSeed :: String
  }
  deriving (Eq, Show)

-- | Consensus-specific failures are kept separate from block and transaction
-- failures so a node can report the exact stage that rejected data.
data ConsensusError
  = InvalidProducerStake VerificationKey Stake
  | DuplicateProducer VerificationKey
  | EmptyProducerRegistry
  | UnregisteredProducer VerificationKey
  | WrongSlotLeader Slot VerificationKey VerificationKey
  | SlotNotAfterChainTip Slot Slot
  | ConsensusBlockProductionFailed BlockProductionError
  | EmptyChainCandidate String
  | CandidateBaseMismatch String
  | CandidateSlotNotIncreasing String Slot Slot
  | CandidateBlockRejected String BlockValidationError
  | NoChainCandidates

Stake-weighted leader selection and leader validation

Source: src/Coxy/Consensus.hs, lines 212–255 in the version 2.1 training tree.

selectSlotLeader
  :: ConsensusConfig
  -> ProducerRegistry
  -> Slot
  -> Either ConsensusError RegisteredProducer
selectSlotLeader config registry slot = do
  ticket <- leaderTicket config registry slot
  let Stake total = totalRegisteredStake registry
      position = ticket `mod` total
  selectByStake position (registeredProducers registry)
  where
    selectByStake _ [] = Left EmptyProducerRegistry
    selectByStake position (producer : remaining)
      | position < producerAmount = Right producer
      | otherwise = selectByStake (position - producerAmount) remaining
      where
        producerAmount = unStake (registeredProducerStake producer)

-- | Check that a block was made by the registered leader for its slot.
-- Cryptographic header-signature and ledger validation remain in Coxy.Block.
validateBlockLeader
  :: ConsensusConfig
  -> ProducerRegistry
  -> Block
  -> Either ConsensusError ()
validateBlockLeader config registry block = do
  let header = blockHeader block
      slot = Slot (blockHeaderSlot header)
      actualProducer = blockHeaderProducer header

  if producerIsRegistered actualProducer registry
    then Right ()
    else Left (UnregisteredProducer actualProducer)

  expected <- selectSlotLeader config registry slot
  let expectedProducer = registeredProducerKey expected

  if actualProducer == expectedProducer
    then Right ()
    else Left (WrongSlotLeader slot expectedProducer actualProducer)

-- | Two different blocks compete when they claim the same height, slot, and
-- parent. The block hash comparison ensures they are not identical copies.
blocksCompete :: Block -> Block -> Bool

Student practical laboratory

  1. Register three producers with a 60/30/10 stake split and generate leaders for one hundred slots.
  2. Count the selected slots and explain why the observed distribution is approximate rather than exact.
  3. Try to produce a block with a nonleader key and confirm rejection.
  4. Create equal-height competing chains and verify the deterministic hash tie-break.

Use the following workflow after each change:

# Work in a branch dedicated to Build Three
git switch -c training/build-03

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Ouroboros uses cryptographic leader proofs, epoch randomness, evolving keys, operational certificates, chain-density rules, and extensive adversarial analysis. This build teaches the control flow, but it is not an implementation of Ouroboros.

COXY module Cardano area What to study next
Consensus Ouroboros consensus slots, epochs, VRFs, KES, chain selection

Security and professional practice

Public deterministic randomness is predictable. It is suitable for teaching only and must never be presented as secure proof-of-stake consensus.

Completion checkpoint

A student completes Build Three only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Consensus.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

4. 🗄️ Build Four — Persistent Chain Store, Forks, Rollback, and Restart Recovery

Release stage: v0.4
Primary modules: ChainStore.hs

Purpose

This build gives the node durable chain memory. It stores every valid block by hash, preserves competing forks, selects the best eligible tip, reports rollback and roll-forward paths, advances an immutable point, and reconstructs state after restart.

Learning objectives

  • A fork-aware store must validate a block against its own parent state rather than only against the currently selected tip.
  • A chain change is represented explicitly by a common ancestor, rollback hashes, and roll-forward hashes.
  • The immutable point prevents selection or manual rollback across history that the node has declared final for operational purposes.

Architecture flow

Received block → find parent state → validate and store
        ↓
Compare eligible tips → choose best tip
        ↓
Compute common ancestor → rollback old branch → roll forward new branch
        ↓
Persist metadata and blocks → recover on restart

How this build works

This build gives the node durable chain memory. It stores every valid block by hash, preserves competing forks, selects the best eligible tip, reports rollback and roll-forward paths, advances an immutable point, and reconstructs state after restart. A fork-aware store must validate a block against its own parent state rather than only against the currently selected tip. A chain change is represented explicitly by a common ancestor, rollback hashes, and roll-forward hashes. The immutable point prevents selection or manual rollback across history that the node has declared final for operational purposes.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Chain-store state, chain-change report, and failures

Source: src/Coxy/ChainStore.hs, lines 98–145 in the version 2.1 training tree.

data ChainStore = ChainStore
  { chainStoreConfig         :: ChainStoreConfig
  , chainStoreGenesis        :: ChainState
  , chainStoreBlocks         :: Map.Map BlockHash StoredBlock
  , chainStoreSelectedTip    :: BlockHash
  , chainStoreImmutablePoint :: ChainPoint
  }
  deriving (Eq, Show)

-- | Describes the rollback and roll-forward path when the selected branch
-- changes. Rollback hashes are ordered from the old tip towards the common
-- ancestor. Roll-forward hashes are ordered from the common ancestor towards
-- the new tip.
data ChainChange = ChainChange
  { chainChangeOldTip         :: BlockHash
  , chainChangeNewTip         :: BlockHash
  , chainChangeCommonAncestor :: ChainPoint
  , chainChangeRolledBack     :: [BlockHash]
  , chainChangeRolledForward  :: [BlockHash]
  }
  deriving (Eq, Show)

data ChainStoreError
  = InvalidGenesisState ChainState
  | DuplicateStoredBlock BlockHash
  | UnknownBlockParent BlockHash BlockHash
  | StoredBlockConsensusFailure BlockHash ConsensusError
  | StoredBlockValidationFailure BlockHash BlockValidationError
  | UnknownStoredBlock BlockHash
  | StoredChainCycle BlockHash
  | RollbackTargetNotAncestor BlockHash BlockHash
  | RollbackPastImmutablePoint ChainPoint BlockHash
  | ImmutablePointNotOnSelectedChain ChainPoint BlockHash
  | StoreIoFailure FilePath String
  | StoreDecodeFailure FilePath String
  | StoreChecksumMismatch FilePath String String
  | StoreBlockHashMismatch FilePath BlockHash BlockHash
  | StoreMetadataVersionMismatch Word16
  | StoreBlockVersionMismatch Word16
  | StoreImmutableDepthMismatch Word64 Word64
  | StoreGenesisMismatch String String
  | StoreSelectedTipMissing BlockHash
  | StoreImmutablePointMissing ChainPoint
  | StoreUnresolvedBlocks [BlockHash]
  | StoreMetadataMissing FilePath
  deriving (Eq, Show)

-- | Construct an empty store rooted at the supplied genesis state.

Insertion, fork selection, and rollback entry points

Source: src/Coxy/ChainStore.hs, lines 253–315 in the version 2.1 training tree.

insertAndSelectBlock
  :: ConsensusConfig
  -> ProducerRegistry
  -> Block
  -> ChainStore
  -> Either ChainStoreError (ChainStore, ChainChange)
insertAndSelectBlock consensus registry block store = do
  inserted <- insertBlock consensus registry block store
  selectBestStoredTip inserted

-- | Select the valid stored tip with greatest height. Equal-height forks use
-- the lexicographically smaller hash. A candidate is eligible only when it
-- descends from the current immutable point.
selectBestStoredTip
  :: ChainStore
  -> Either ChainStoreError (ChainStore, ChainChange)
selectBestStoredTip store = do
  eligible <-
    filterMRight
      (descendsFromPoint store (chainStoreImmutablePoint store))
      (genesisBlockHash : Map.keys (chainStoreBlocks store))

  best <-
    case eligible of
      [] ->
        Left
          ( ImmutablePointNotOnSelectedChain
              (chainStoreImmutablePoint store)
              (chainStoreSelectedTip store)
          )
      firstTip : remainingTips ->
        Right (foldl' (preferTip store) firstTip remainingTips)

  let oldTip = chainStoreSelectedTip store
  change <- chainChangeBetween store oldTip best
  let selected = store { chainStoreSelectedTip = best }
  advanced <- advanceImmutablePoint selected
  Right (advanced, change)

-- | Manually move the selected tip to one of its ancestors. Stored blocks are
-- never deleted, so a later chain-selection pass may roll forward again. The
-- immutable point is monotonic and cannot be crossed.
rollbackSelectedTo
  :: BlockHash
  -> ChainStore
  -> Either ChainStoreError (ChainStore, ChainChange)
rollbackSelectedTo target store = do
  _ <- stateAtHash target store
  let current = chainStoreSelectedTip store

  targetIsAncestor <- isAncestorHash store target current
  unless targetIsAncestor
    (Left (RollbackTargetNotAncestor target current))

  targetContainsImmutable <-
    descendsFromPoint store (chainStoreImmutablePoint store) target
  unless targetContainsImmutable
    (Left (RollbackPastImmutablePoint (chainStoreImmutablePoint store) target))

  change <- chainChangeBetween store current target
  Right (store { chainStoreSelectedTip = target }, change)

selectedChainState :: ChainStore -> Either ChainStoreError ChainState

Student practical laboratory

  1. Build two branches from genesis and confirm that both remain stored.
  2. Extend the shorter branch until it becomes preferred and inspect the reported rollback path.
  3. Advance the immutable point, then try to select a fork that diverges before it.
  4. Stop and restart the program, recover the store, and compare height, tip, and immutable point.

Use the following workflow after each change:

# Work in a branch dedicated to Build Four
git switch -c training/build-04

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano nodes maintain volatile and immutable chain storage and must handle short forks safely. Production storage is considerably more sophisticated, but the concepts of parent-based validation, rollback, immutable history, and recovery are directly relevant.

COXY module Cardano area What to study next
ChainStore ChainDB volatile/immutable storage, rollbacks, snapshots

Security and professional practice

Durability requires atomic writes, checksums, format versioning, corruption detection, and tested recovery. A successful write call alone is not proof of recoverability.

Completion checkpoint

A student completes Build Four only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how ChainStore.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

5. 🌐 Build Five — Peer-to-Peer Synchronisation with Bounded Queues

Release stage: v0.5
Primary modules: P2P.hs

Purpose

This build defines authenticated node identities and the logical messages required to synchronise forks. Peers exchange signed handshakes, tips, header locators, bounded header batches, block requests, block batches, transactions, and keep-alive messages through bounded queues.

Learning objectives

  • A signed handshake binds the network, protocol version, node identity, nonce, and advertised tip.
  • Header-first synchronisation identifies a common ancestor before downloading full block bodies.
  • Inbound and outbound queues are bounded so an untrusted peer cannot consume unlimited memory.

Architecture flow

Signed handshake → authenticated peer
        ↓
Tip announcement → locator-based header request
        ↓
Linked header batch → missing block request
        ↓
Validated block batch → ChainStore selection and possible rollback

How this build works

This build defines authenticated node identities and the logical messages required to synchronise forks. Peers exchange signed handshakes, tips, header locators, bounded header batches, block requests, block batches, transactions, and keep-alive messages through bounded queues. A signed handshake binds the network, protocol version, node identity, nonce, and advertised tip. Header-first synchronisation identifies a common ancestor before downloading full block bodies. Inbound and outbound queues are bounded so an untrusted peer cannot consume unlimited memory.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Node identity, handshake, tip, header, block, and P2P message types

Source: src/Coxy/P2P.hs, lines 106–180 in the version 2.1 training tree.

data NodeIdentity = NodeIdentity
  { nodeIdentityId   :: NodeId
  , nodeIdentityName :: String
  , nodeIdentityKey  :: VerificationKey
  }
  deriving (Eq, Show, Read)

-- | Compact description of a node's currently selected chain tip.
data TipAnnouncement = TipAnnouncement
  { announcedTipHeight :: Word64
  , announcedTipSlot   :: Maybe Word64
  , announcedTipHash   :: BlockHash
  }
  deriving (Eq, Show, Read)

-- | Signed first-contact message. The signature binds the public identity,
-- network, protocol version, nonce, and advertised chain tip.
data PeerHandshake = PeerHandshake
  { handshakeNetworkId       :: String
  , handshakeProtocolVersion :: Word16
  , handshakeIdentity        :: NodeIdentity
  , handshakeNonce           :: Word64
  , handshakeTip             :: TipAnnouncement
  , handshakeSignature       :: SignatureBytes
  }
  deriving (Eq, Show, Read)

-- | Bitcoin-style locator request. Locators are ordered from the requester's
-- preferred cursor or tip backwards towards genesis.
data HeaderRequest = HeaderRequest
  { headerRequestLocators :: [BlockHash]
  , headerRequestLimit    :: Int
  }
  deriving (Eq, Show, Read)

-- | A header plus the hash of its complete signed block. The next header can
-- therefore prove linkage before the complete block body is downloaded.
data HeaderAnnouncement = HeaderAnnouncement
  { announcedHeaderHash  :: BlockHash
  , announcedBlockHeader :: BlockHeader
  }
  deriving (Eq, Show, Read)

-- | Headers following a common ancestor selected from the request locators.
data HeaderBatch = HeaderBatch
  { headerBatchAncestor :: BlockHash
  , headerBatchHeaders  :: [HeaderAnnouncement]
  }
  deriving (Eq, Show, Read)

newtype BlockRequest = BlockRequest
  { requestedBlockHashes :: [BlockHash]
  }
  deriving (Eq, Show, Read)

-- | Messages exchanged after both sides have authenticated the peer identity.
-- Coxy.Transport carries these values over bounded TCP frames using the
-- canonical, versioned codec implemented by Coxy.ProtocolCodec.
data P2PMessage
  = HandshakeAccepted TipAnnouncement
  | TipAnnouncementMessage TipAnnouncement
  | HeaderRequestMessage HeaderRequest
  | HeaderBatchMessage HeaderBatch
  | BlockRequestMessage BlockRequest
  | BlockBatchMessage [Block]
  | TransactionMessage SignedTransaction
  | KeepAlivePing Word64
  | KeepAlivePong Word64
  | ProtocolErrorMessage String
  deriving (Eq, Show, Read)

data QueueDirection
  = InboundQueue
  | OutboundQueue
  deriving (Eq, Show)

Handshake creation and verification

Source: src/Coxy/P2P.hs, lines 348–420 in the version 2.1 training tree.

createHandshake config identity signingKey nonce tip = do
  validateP2PConfig config
  validateNodeIdentity identity
  unless
    (verificationKey signingKey == nodeIdentityKey identity)
    (Left (IdentitySigningKeyMismatch (nodeIdentityId identity)))
  let unsigned =
        PeerHandshake
          { handshakeNetworkId = p2pNetworkId config
          , handshakeProtocolVersion = p2pProtocolVersion config
          , handshakeIdentity = identity
          , handshakeNonce = nonce
          , handshakeTip = tip
          , handshakeSignature = signBytes signingKey ByteString8.empty
          }
      signature = signBytes signingKey (handshakeBytes unsigned)
  Right (unsigned { handshakeSignature = signature })

createHandshakeForNode
  :: SigningKey
  -> Word64
  -> P2PNode
  -> Either P2PError PeerHandshake
createHandshakeForNode signingKey nonce node = do
  tip <- currentTip node
  createHandshake
    (p2pConfig node)
    (p2pIdentity node)
    signingKey
    nonce
    tip

verifyHandshake
  :: P2PConfig
  -> NodeIdentity
  -> PeerHandshake
  -> Either P2PError PeerState
verifyHandshake config localIdentity handshake = do
  let remoteIdentity = handshakeIdentity handshake
      expectedNetwork = p2pNetworkId config
      actualNetwork = handshakeNetworkId handshake
      expectedVersion = p2pProtocolVersion config
      actualVersion = handshakeProtocolVersion handshake
      remoteId = nodeIdentityId remoteIdentity

  validateNodeIdentity remoteIdentity

  when
    (remoteId == nodeIdentityId localIdentity)
    (Left (SelfConnection remoteId))

  unless
    (actualNetwork == expectedNetwork)
    (Left (HandshakeNetworkMismatch expectedNetwork actualNetwork))

  unless
    (actualVersion == expectedVersion)
    (Left (HandshakeProtocolMismatch expectedVersion actualVersion))

  unless
    ( verifyBytes
        (nodeIdentityKey remoteIdentity)
        (handshakeBytes handshake)
        (handshakeSignature handshake)
    )
    (Left (InvalidHandshakeSignature remoteId))

  Right
    PeerState
      { peerRemoteIdentity = remoteIdentity
      , peerRemoteTip = handshakeTip handshake
      , peerHandshakeNonce = handshakeNonce handshake
      , peerAwaitingHeaders = False

Student practical laboratory

  1. Connect two simulated peers with different branches and run synchronisation until their selected tips match.
  2. Reduce the header and block batch limits to one and observe multi-round catch-up.
  3. Fill an inbound queue to capacity and confirm that the next message is rejected.
  4. Tamper with the handshake network identifier or signature and confirm rejection.

Use the following workflow after each change:

# Work in a branch dedicated to Build Five
git switch -c training/build-05

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano networking separates several mini-protocols, including chain synchronization, block fetch, transaction submission, keep-alive, and peer sharing. This training message set prepares students to read those protocol state machines.

COXY module Cardano area What to study next
P2P ouroboros-network chain-sync, block-fetch, tx-submission, peer sharing

Security and professional practice

Authentication does not make peer data valid. Every received block and transaction must still pass consensus and ledger validation.

Completion checkpoint

A student completes Build Five only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how P2P.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

6. 🖥️ Build Six — The Supervised Node Runtime

Release stage: v0.6
Primary modules: Node.hs

Purpose

This build combines the previously separate components into a single supervised node runtime. The node advances a slot clock, processes bounded inbound work, maintains peer liveness, revalidates its mempool, produces a block only when it is the selected leader, persists state periodically, and shuts down cleanly.

Learning objectives

  • A node is a coordinated collection of state machines rather than one large loop.
  • Relay nodes run without a producer signing key, while producer nodes create blocks only in their assigned slots.
  • Lifecycle states, metrics, persistence intervals, and graceful shutdown make failure behaviour observable and testable.

Architecture flow

Slot tick → process inbound messages → update peer health
        ↓
Revalidate mempool → select slot leader
        ↓
Produce/apply block when eligible → announce tip
        ↓
Schedule persistence and keep-alives → supervised shutdown

How this build works

This build combines the previously separate components into a single supervised node runtime. The node advances a slot clock, processes bounded inbound work, maintains peer liveness, revalidates its mempool, produces a block only when it is the selected leader, persists state periodically, and shuts down cleanly. A node is a coordinated collection of state machines rather than one large loop. Relay nodes run without a producer signing key, while producer nodes create blocks only in their assigned slots. Lifecycle states, metrics, persistence intervals, and graceful shutdown make failure behaviour observable and testable.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Combined node state and lifecycle

Source: src/Coxy/Node.hs, lines 187–230 in the version 2.1 training tree.

data NodeState = NodeState
  { nodeConfig             :: NodeConfig
  , nodeConsensusConfig    :: ConsensusConfig
  , nodeProducerRegistry   :: ProducerRegistry
  , nodeProducerSigningKey :: Maybe SigningKey
  , nodeMempool            :: Mempool
  , nodeP2PState           :: P2PNode
  , nodeLifecycle          :: NodeLifecycle
  , nodeCurrentSlot        :: Slot
  , nodeLastKeepAliveSlot  :: Slot
  , nodeLastPersistedSlot  :: Slot
  , nodePeerLiveness       :: Map.Map NodeId PeerLiveness
  , nodeMetrics            :: NodeMetrics
  , nodeLastProducedBlock  :: Maybe BlockHash
  , nodeLastChainChange    :: Maybe ChainChange
  }

-- | Thread-safe handle used by applications and tests. Networking remains an
-- explicit transport concern: callers deliver and take peer envelopes.
data NodeHandle = NodeHandle
  { nodeHandleState      :: MVar NodeState
  , nodeHandleThread     :: ThreadId
  , nodeHandleCompletion :: MVar ()
  }

newNodeState
  :: NodeConfig
  -> ConsensusConfig
  -> ProducerRegistry
  -> Maybe SigningKey
  -> P2PNode
  -> Either NodeError NodeState
newNodeState config consensus registry producerKey p2p = do
  validateNodeConfig config
  let consensusNetwork = consensusNetworkId consensus
      peerNetwork = p2pNetworkId (p2pConfig p2p)
  unless
    (consensusNetwork == peerNetwork)
    (Left (NodeNetworkMismatch consensusNetwork peerNetwork))
  when
    (null (registeredProducers registry))
    (Left (InvalidNodeConfiguration "producer registry must not be empty"))
  selected <- firstStore (selectedChainState (p2pChainStore p2p))
  case producerKey of

Per-slot runtime processing and block-production integration

Source: src/Coxy/Node.hs, lines 342–410 in the version 2.1 training tree.

advanceNodeSlot :: Slot -> NodeState -> Either NodeError NodeState
advanceNodeSlot nextSlot state = do
  requireRunningOrStarting state
  let currentSlot = nodeCurrentSlot state
  unless
    (nextSlot > currentSlot)
    (Left (NodeSlotNotIncreasing currentSlot nextSlot))
  let advanced =
        synchronisePeerLiveness
          ( state
              { nodeLifecycle = NodeRunning
              , nodeCurrentSlot = nextSlot
              }
          )
  inboundProcessed <- processNodeInbound advanced
  timedOut <- disconnectTimedOutPeers inboundProcessed
  selected <- nodeSelectedChainState timedOut
  let revalidated =
        timedOut
          { nodeMempool =
              revalidateMempoolAt (unSlot nextSlot)
                (chainLedger selected) (nodeMempool timedOut)
          }
  produced <- produceForCurrentSlot revalidated
  sendScheduledKeepAlives produced

persistenceDue :: NodeState -> Bool
persistenceDue state =
  nodeLifecycle state == NodeStopping
    || slotDistance
         (nodeLastPersistedSlot state)
         (nodeCurrentSlot state)
       >= nodePersistenceIntervalSlots (nodeConfig state)

persistNodeState :: NodeState -> IO (Either NodeError NodeState)
persistNodeState state = do
  result <- persistChainStore (p2pChainStore (nodeP2PState state))
  pure $
    case result of
      Left err -> Left (NodeChainStoreFailure err)
      Right () ->
        let metrics = nodeMetrics state
        in Right
            state
              { nodeLastPersistedSlot = nodeCurrentSlot state
              , nodeMetrics =
                  metrics
                    { metricPersistenceWrites =
                        metricPersistenceWrites metrics + 1
                    }
              }

-- | Persist all accepted blocks and transition to the terminal stopped state.
shutdownNodeState :: NodeState -> IO (Either NodeError NodeState)
shutdownNodeState state = do
  persisted <- persistNodeState (state { nodeLifecycle = NodeStopping })
  pure ((\updated -> updated { nodeLifecycle = NodeStopped }) <$> persisted)

startNodeRuntime :: NodeState -> IO NodeHandle
startNodeRuntime initialState = do
  stateVar <- newMVar (initialState { nodeLifecycle = NodeRunning })
  completion <- newEmptyMVar
  threadId <-
    forkFinally
      (supervisorLoop stateVar)
      (supervisorFinished stateVar completion)
  pure
    NodeHandle
      { nodeHandleState = stateVar

Student practical laboratory

  1. Run one producer node and one relay node and compare their lifecycle and production behaviour.
  2. Advance slots until the producer is selected and verify that a pending transaction is placed in a block.
  3. Configure a short keep-alive interval and simulate a silent peer until timeout.
  4. Request graceful shutdown and confirm that persistence occurs before the node reaches the stopped state.

Use the following workflow after each change:

# Work in a branch dedicated to Build Six
git switch -c training/build-06

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

The production cardano-node similarly integrates consensus, ledger validation, chain storage, networking, mempool processing, and operational monitoring. Reading a supervised training runtime helps students navigate the real node architecture without treating it as an indivisible black box.

COXY module Cardano area What to study next
Node cardano-node architecture supervision, protocol threads, shutdown and persistence

Security and professional practice

Supervision must distinguish recoverable worker failures from state-corrupting failures. A production runtime also needs resource isolation, signal handling, backpressure, and careful exception boundaries.

Completion checkpoint

A student completes Build Six only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Node.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

7. 🔌 Build Seven — JSON-RPC and Real-Time WebSocket Services

Release stage: v0.7
Primary modules: RPC.hs, WebSocket.hs

Purpose

This build exposes the running node to applications. JSON-RPC provides status, chain, block, mempool, transaction-submission, persistence, and shutdown operations. WebSockets publish real-time block, rollback, transaction, and peer-status events through an STM broadcast hub.

Learning objectives

  • Read-only methods and privileged administration methods have different authorization requirements.
  • Transaction submission decodes the signed body and recalculates identifiers rather than trusting caller-provided hashes.
  • Event streams use monotonic sequence numbers so clients can detect gaps and reconnect safely.

Architecture flow

HTTP JSON-RPC request → decode and validate → read or update NodeHandle
WebSocket subscription → topic filter → sequenced event hub → client update

How this build works

This build exposes the running node to applications. JSON-RPC provides status, chain, block, mempool, transaction-submission, persistence, and shutdown operations. WebSockets publish real-time block, rollback, transaction, and peer-status events through an STM broadcast hub. Read-only methods and privileged administration methods have different authorization requirements. Transaction submission decodes the signed body and recalculates identifiers rather than trusting caller-provided hashes. Event streams use monotonic sequence numbers so clients can detect gaps and reconnect safely.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

RPC configuration, request handling, and method dispatch

Source: src/Coxy/RPC.hs, lines 166–215 in the version 2.1 training tree.

data RpcServerConfig = RpcServerConfig
  { rpcHost            :: String
  , rpcPort            :: Int
  , rpcAdminToken      :: Maybe ByteString.ByteString
  , rpcMaxRequestBytes :: Int64
  }
  deriving (Eq, Show)

defaultRpcServerConfig :: RpcServerConfig
defaultRpcServerConfig =
  RpcServerConfig
    { rpcHost = "127.0.0.1"
    , rpcPort = 9080
    , rpcAdminToken = Nothing
    , rpcMaxRequestBytes = 1024 * 1024
    }

data RpcRequest = RpcRequest
  { rpcRequestVersion :: Text
  , rpcRequestMethod  :: Text
  , rpcRequestParams  :: Maybe Value
  , rpcRequestId      :: Maybe Value
  }
  deriving (Eq, Show)

instance FromJSON RpcRequest where
  parseJSON = withObject "RpcRequest" $ \value ->
    RpcRequest
      <$> value .: "jsonrpc"
      <*> value .: "method"
      <*> value .:? "params"
      <*> value .:? "id"

data RpcError = RpcError
  { rpcErrorCode    :: Int
  , rpcErrorMessage :: Text
  , rpcErrorData    :: Maybe Value
  }
  deriving (Eq, Show)

data RpcResponse
  = RpcSuccess
      { rpcResponseId     :: Value
      , rpcResponseResult :: Value
      }
  | RpcFailure
      { rpcResponseId    :: Value
      , rpcResponseError :: RpcError
      }
  deriving (Eq, Show)

Event topics, envelopes, and broadcast subscriptions

Source: src/Coxy/WebSocket.hs, lines 108–165 in the version 2.1 training tree.

data EventTopic
  = BlockTopic
  | RollbackTopic
  | TransactionTopic
  | PeerTopic
  | ScriptTopic
  | OracleTopic
  deriving (Eq, Ord, Show)

instance ToJSON EventTopic where
  toJSON topic = Aeson.String (topicText topic)

instance FromJSON EventTopic where
  parseJSON = withText "EventTopic" $ \value ->
    case value of
      "block" -> pure BlockTopic
      "rollback" -> pure RollbackTopic
      "transaction" -> pure TransactionTopic
      "peer" -> pure PeerTopic
      "script" -> pure ScriptTopic
      "oracle" -> pure OracleTopic
      _ -> fail "topic must be block, rollback, transaction, peer, script, or oracle"

-- | Runtime events exposed to browser, wallet, explorer, and monitoring
-- clients. Values are deliberately summaries rather than internal mutable
-- state.
data NodeEvent
  = BlockAppliedEvent
      { eventBlockHash        :: BlockHash
      , eventBlockHeight      :: Word64
      , eventBlockSlot        :: Word64
      , eventBlockTransactions :: Int
      }
  | ChainRollbackEvent
      { eventRollbackOldTip         :: BlockHash
      , eventRollbackNewTip         :: BlockHash
      , eventRollbackCommonAncestor :: ChainPoint
      , eventRolledBack              :: [BlockHash]
      , eventRolledForward           :: [BlockHash]
      }
  | TransactionAcceptedEvent
      { eventTransactionId     :: TxId
      , eventTransactionSource :: Text
      }
  | PeerStatusEvent
      { eventPeerId       :: NodeId
      , eventPeerName     :: Text
      , eventPeerStatus   :: Text
      , eventPeerCount    :: Int
      }
  | ScriptExecutionEvent
      { eventScriptTxId  :: TxId
      , eventScriptHash  :: ScriptHash
      , eventScriptInput :: TxOutRef
      , eventScriptResult :: ScriptExecutionResult
      }
  | OracleRegistrationEvent
      { eventOracleTxId  :: TxId

Student practical laboratory

  1. Call node.status and chain.tip from curl and compare the returned height and hash.
  2. Submit a valid signed transaction through tx.submit and watch it appear in the mempool.
  3. Subscribe to block and transaction topics from a browser client.
  4. Call an administrative method without a bearer token, with a wrong token, and with the correct token.

Use the following workflow after each change:

# Work in a branch dedicated to Build Seven
git switch -c training/build-07

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano applications commonly use node-to-client protocols and application gateways such as Ogmios rather than inventing an unauthenticated public node API. The lessons about method separation, validation, event sequencing, and access control transfer directly.

COXY module Cardano area What to study next
RPC/WebSocket node-to-client, Ogmios local socket access, typed queries, event streams

Security and professional practice

Bind administrative services to trusted interfaces, require TLS at remote boundaries, limit request size and rate, and never expose signing keys through an RPC process.

Completion checkpoint

A student completes Build Seven only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how RPC.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

8. 🔎 Build Eight — Rollback-Aware Indexing and the Explorer API

Release stage: v0.8
Primary modules: Indexer.hs, ExplorerAPI.hs

Purpose

This build creates query-friendly projections of the selected chain. The indexer resolves transaction inputs, maintains address and token histories, current UTxOs, producer statistics, and script activity. The Explorer API exposes paginated, deterministic queries for wallets and explorers.

Learning objectives

  • The chain store is authoritative for validity, while the query database is a replaceable projection optimized for application questions.
  • A rollback-aware index removes data from an abandoned branch and reconstructs the selected branch.
  • Pagination, stable ordering, and bounded page sizes are required for predictable clients and resource safety.

Architecture flow

Selected ChainStore path → replay blocks → resolve inputs and outputs
        ↓
Address/token/producer/script indexes → persistent query snapshot
        ↓
Explorer HTTP query → stable pagination → wallet or browser client

How this build works

This build creates query-friendly projections of the selected chain. The indexer resolves transaction inputs, maintains address and token histories, current UTxOs, producer statistics, and script activity. The Explorer API exposes paginated, deterministic queries for wallets and explorers. The chain store is authoritative for validity, while the query database is a replaceable projection optimized for application questions. A rollback-aware index removes data from an abandoned branch and reconstructs the selected branch. Pagination, stable ordering, and bounded page sizes are required for predictable clients and resource safety.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

The query database and its major index families

Source: src/Coxy/Indexer.hs, lines 253–310 in the version 2.1 training tree.

data QueryDatabase = QueryDatabase
  { queryDbTip                :: ChainPoint
  , queryDbBlocksByHash       :: Map.Map BlockHash IndexedBlock
  , queryDbBlockOrder         :: [BlockHash]
  , queryDbBlockByHeight      :: Map.Map Word64 BlockHash
  , queryDbTransactionsById   :: Map.Map TxId IndexedTransaction
  , queryDbTransactionOrder   :: [TxId]
  , queryDbAddressHistory     :: Map.Map Address [AddressActivity]
  , queryDbAddressUtxos       :: Map.Map Address (Map.Map TxOutRef IndexedOutput)
  , queryDbTokenHistory       :: Map.Map AssetId [TokenActivity]
  , queryDbTokenUtxos         :: Map.Map AssetId (Map.Map TxOutRef IndexedOutput)
  , queryDbProducerStats      :: Map.Map ProducerId ProducerStats
  , queryDbScriptHistory      :: Map.Map ScriptHash [ScriptActivity]
  , queryDbScriptUtxos        :: Map.Map ScriptHash (Map.Map TxOutRef IndexedOutput)
  , queryDbDatumHistory       :: Map.Map ScriptHash [DatumActivity]
  , queryDbRedeemerHistory    :: Map.Map ScriptHash [RedeemerActivity]
  , queryDbScriptResults      :: Map.Map ScriptHash [ScriptResultActivity]
  , queryDbOracleRegistrations :: Map.Map OracleId IndexedOracleRegistration
  , queryDbOracleReports      :: Map.Map String [IndexedOracleReport]
  }
  deriving (Eq, Show, Read)

data IndexerConfig = IndexerConfig
  { indexerDatabaseFile            :: FilePath
  , indexerPollIntervalMicroseconds :: Int
  }
  deriving (Eq, Show)

defaultIndexerConfig :: IndexerConfig
defaultIndexerConfig =
  IndexerConfig
    { indexerDatabaseFile = "data/indexer/query.db"
    , indexerPollIntervalMicroseconds = 100000
    }

data IndexerError
  = IndexerChainStoreFailure ChainStoreError
  | IndexerLedgerFailure TxId ValidationError
  | IndexerMissingInput TxOutRef
  | IndexerTipMismatch BlockHash BlockHash
  | IndexerInvalidPollInterval Int
  | IndexerIoFailure FilePath String
  | IndexerDecodeFailure FilePath String
  | IndexerChecksumMismatch FilePath String String
  | IndexerRuntimeFailure String
  deriving (Eq, Show)

data IndexerHandle = IndexerHandle
  { indexerHandleConfig     :: IndexerConfig
  , indexerHandleNode       :: NodeHandle
  , indexerHandleDatabase   :: MVar QueryDatabase
  , indexerHandleLastError  :: MVar (Maybe IndexerError)
  , indexerHandleStop       :: MVar ()
  , indexerHandleCompletion :: MVar ()
  }

emptyQueryDatabase :: ChainStore -> Either IndexerError QueryDatabase
emptyQueryDatabase store = do

Explorer API configuration and paginated application entry point

Source: src/Coxy/ExplorerAPI.hs, lines 105–150 in the version 2.1 training tree.

data PageRequest = PageRequest
  { requestedPage     :: Int
  , requestedPageSize :: Int
  }
  deriving (Eq, Show)

data Page value = Page
  { pageItems       :: [value]
  , pageNumber      :: Int
  , pageSize        :: Int
  , pageTotalItems  :: Int
  , pageTotalPages  :: Int
  , pageHasNext     :: Bool
  , pageHasPrevious :: Bool
  }
  deriving (Eq, Show)

data TokenSummary = TokenSummary
  { tokenSummaryAsset         :: AssetId
  , tokenSummaryCurrentSupply :: Integer
  , tokenSummaryUtxoCount     :: Int
  , tokenSummaryActivityCount :: Int
  }
  deriving (Eq, Show)

data ScriptSummary = ScriptSummary
  { scriptSummaryHash          :: ScriptHash
  , scriptSummaryUtxoCount     :: Int
  , scriptSummaryActivityCount :: Int
  }
  deriving (Eq, Show)

data AddressSummary = AddressSummary
  { addressSummaryAddress       :: Address
  , addressSummaryCoinBalance   :: Coin
  , addressSummaryTokenBalances :: TokenBundle
  , addressSummaryUtxoCount     :: Int
  , addressSummaryActivityCount :: Int
  }
  deriving (Eq, Show)

data ExplorerApiConfig = ExplorerApiConfig
  { explorerHost            :: String
  , explorerPort            :: Int
  , explorerDefaultPageSize :: Int
  , explorerMaximumPageSize :: Int

Student practical laboratory

  1. Index a chain containing payments to two addresses and query each address summary and history.
  2. Create a fork, switch the selected tip, and confirm that abandoned transactions disappear from the query database.
  3. Query blocks and transactions with page sizes of one, two, and an excessive value.
  4. Corrupt the persisted query snapshot and confirm that recovery rejects or rebuilds it.

Use the following workflow after each change:

# Work in a branch dedicated to Build Eight
git switch -c training/build-08

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano ecosystems use indexers such as cardano-db-sync, Kupo, and specialized project indexers. This build teaches why rollback handling and selected-chain projection are essential before students depend on indexed data.

COXY module Cardano area What to study next
Indexer/ExplorerAPI cardano-db-sync, Kupo rollback-aware indexing, PostgreSQL, chain synchronization

Security and professional practice

An indexer can be stale even when it is internally consistent. Expose indexed tip information and compare it with the node tip in readiness checks.

Completion checkpoint

A student completes Build Eight only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Indexer.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

9. 📜 Build Nine — Typed Scripts, Execution Budgets, and Signed Oracles

Release stage: v0.9
Primary modules: Script.hs, Oracle.hs

Purpose

This build extends outputs with typed datums and transactions with redeemers and witnesses. Deterministic scripts enforce signatures, deadlines, hash locks, multisignature thresholds, and oracle predicates. Oracle operators register keys and sign fresh, quorum-backed reports.

Learning objectives

  • Datum and redeemer values use an explicit recursive data type instead of untyped strings.
  • Every script reports deterministic CPU and memory use and fails when its declared budget is insufficient.
  • Oracle trust is constrained by registrations, feed permissions, signatures, sequence numbers, freshness windows, and quorum rules.

Architecture flow

Script output + inline datum
        ↓ spend attempt with redeemer and witness
Deterministic evaluator → result + trace + budget used
        ↓
Optional signed oracle reports → freshness/quorum/predicate checks

How this build works

This build extends outputs with typed datums and transactions with redeemers and witnesses. Deterministic scripts enforce signatures, deadlines, hash locks, multisignature thresholds, and oracle predicates. Oracle operators register keys and sign fresh, quorum-backed reports. Datum and redeemer values use an explicit recursive data type instead of untyped strings. Every script reports deterministic CPU and memory use and fails when its declared budget is insufficient. Oracle trust is constrained by registrations, feed permissions, signatures, sequence numbers, freshness windows, and quorum rules.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Typed script data, datum, and redeemer values

Source: src/Coxy/Types.hs, lines 122–155 in the version 2.1 training tree.

data ScriptData
  = ScriptInteger Integer
  | ScriptBytes ByteString
  | ScriptText String
  | ScriptBool Bool
  | ScriptList [ScriptData]
  | ScriptMap [(ScriptData, ScriptData)]
  | ScriptUnit
  deriving (Eq, Ord, Show, Read)

newtype Datum = Datum
  { unDatum :: ScriptData
  }
  deriving (Eq, Ord, Show, Read)

newtype Redeemer = Redeemer
  { unRedeemer :: ScriptData
  }
  deriving (Eq, Ord, Show, Read)

-- | Deterministic training execution units. Both dimensions must fit.
data ExecutionBudget = ExecutionBudget
  { executionCpu    :: Word64
  , executionMemory :: Word64
  }
  deriving (Eq, Ord, Show, Read)

-- | A stable result that can be persisted by the indexer.
data ScriptExecutionResult = ScriptExecutionResult
  { scriptResultAccepted   :: Bool
  , scriptResultBudgetUsed :: ExecutionBudget
  , scriptResultTrace      :: [String]
  }
  deriving (Eq, Show, Read)

Script programs, execution context, and deterministic evaluation entry points

Source: src/Coxy/Script.hs, lines 173–240 in the version 2.1 training tree.

evaluateScriptWitness
  :: OracleRegistry
  -> Word64
  -> ByteString.ByteString
  -> TxOutRef
  -> VerificationKey
  -> ScriptWitness
  -> Either ScriptError ScriptExecutionResult
evaluateScriptWitness registry currentSlot transactionBytes ref primaryKey witness = do
  let program = scriptWitnessProgram witness
      used = executionCost program witness
      limit = scriptWitnessBudget witness
  if budgetFits used limit
    then Right ()
    else Left (ScriptBudgetExceeded used limit)
  traceLines <- evaluateProgram program
  Right
    ScriptExecutionResult
      { scriptResultAccepted = True
      , scriptResultBudgetUsed = used
      , scriptResultTrace =
          [ "script=" ++ unScriptHash (scriptHash program)
          , "datum=" ++ show (scriptWitnessDatum witness)
          , "redeemer=" ++ show (scriptWitnessRedeemer witness)
          ] ++ traceLines
      }
  where
    authorizationPayload = scriptAuthorizationBytes transactionBytes ref
    validSupplemental =
      [ scriptSignatureKey signature
      | signature <- scriptWitnessSignatures witness
      , verifyBytes
          (scriptSignatureKey signature)
          authorizationPayload
          (scriptSignatureBytes signature)
      ]
    authorisedKeys = Set.fromList (primaryKey : validSupplemental)
    authorisedAddresses =
      Set.fromList (map addressFromVerificationKey (Set.toList authorisedKeys))

    evaluateProgram program =
      case program of
        RequireSignature requiredAddress ->
          if Set.member requiredAddress authorisedAddresses
            then Right ["required signature accepted"]
            else Left (ScriptSignatureMissing requiredAddress)

        RequireDeadline deadline ->
          if currentSlot >= deadline
            then Right
              [ "deadline reached at slot "
                  ++ show currentSlot
              ]
            else Left (ScriptDeadlineNotReached deadline currentSlot)

        RequireHashLock expectedHash -> do
          secret <-
            case unRedeemer (scriptWitnessRedeemer witness) of
              ScriptBytes bytes -> Right bytes
              ScriptText text -> Right (ByteString8.pack text)
              _ -> Left ScriptHashLockRedeemerInvalid
          let actualHash = hashBytes secret
          if actualHash == expectedHash
            then Right ["hash-lock preimage accepted"]
            else Left (ScriptHashLockMismatch expectedHash actualHash)

        RequireMultiSignature threshold allowedKeys -> do
          let required = fromIntegral threshold :: Integer

Oracle identities, observations, signatures, and reports

Source: src/Coxy/Oracle.hs, lines 32–75 in the version 2.1 training tree.

data OracleError
  = OracleIdMismatch OracleId OracleId
  | OracleRegistrationSignatureInvalid OracleId
  | OracleRegistrationHasNoFeeds OracleId
  | OracleRegistrationDuplicateFeed OracleId String
  | OracleAlreadyRegistered OracleId
  | OracleKeyAlreadyRegistered OracleId
  | OracleUnknown OracleId
  | OracleNotTrusted OracleId
  | OracleFeedNotAllowed OracleId String
  | OracleDuplicateSignature OracleId
  | OracleSignatureInvalid OracleId
  | OracleInvalidQuorum Integer Integer
  | OracleQuorumNotReached Integer Integer
  | OracleReportFromFuture Word64 Word64
  | OracleReportStale Word64 Word64
  | OracleReportSequenceNotIncreasing String Word64 Word64
  deriving (Eq, Show)

oracleIdFromVerificationKey :: VerificationKey -> OracleId
oracleIdFromVerificationKey =
  OracleId . hashBytes . unVerificationKey

scriptDataBytes :: ScriptData -> ByteString.ByteString
scriptDataBytes value = ByteString8.pack (encodeData value)
  where
    field text = show (length text) ++ ":" ++ text
    bytesField bytes = field (show (ByteString.unpack bytes))
    encodeData datum =
      case datum of
        ScriptInteger number -> field "integer" ++ field (show number)
        ScriptBytes bytes -> field "bytes" ++ bytesField bytes
        ScriptText text -> field "text" ++ field text
        ScriptBool flag -> field "bool" ++ field (show flag)
        ScriptList values ->
          field "list" ++ field (show (length values))
            ++ concatMap (field . encodeData) values
        ScriptMap entries ->
          field "map" ++ field (show (length entries))
            ++ concatMap encodeEntry entries
        ScriptUnit -> field "unit"
    encodeEntry (key, item) = field (encodeData key) ++ field (encodeData item)

oracleRegistrationBytes :: OracleRegistration -> ByteString.ByteString

Student practical laboratory

  1. Lock an output with a hash-lock program and spend it with the correct and incorrect secret.
  2. Use a deadline script before and after its permitted slot.
  3. Construct a two-of-three multisignature witness and remove one signature.
  4. Register three oracle operators, assemble a two-signature report, then test stale, future, duplicate, and untrusted reports.

Use the following workflow after each change:

# Work in a branch dedicated to Build Nine
git switch -c training/build-09

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Plutus validators consume datum, redeemer, and script context under execution-unit limits. Cardano oracles are application protocols rather than a privileged ledger feature, so their trust, signatures, update policy, and failure modes must be designed explicitly.

COXY module Cardano area What to study next
Script/Oracle Plutus and application protocols datum, redeemer, script context, execution units, oracle trust

Security and professional practice

Determinism is mandatory for consensus scripts. Never read wall-clock time, network data, random input, or mutable external state during validation.

Completion checkpoint

A student completes Build Nine only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Script.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

10. 👛 Build Ten — Deterministic Wallets, Transaction Building, and the Wallet API

Release stage: v1.0
Primary modules: Wallet.hs, WalletAPI.hs

Purpose

This build gives users a structured way to derive training keys, discover addresses, select coin and token inputs, estimate fees, create change, build script and oracle transactions, separate online construction from offline signing, and retrieve wallet history through the Explorer index.

Learning objectives

  • Derivation paths separate payment, oracle, external, and internal roles so key usage is explicit.
  • Selection must satisfy base coin, every native token, fees, and minimum change requirements without accidentally spending script-controlled outputs.
  • An unsigned plan can be created on an online machine and signed later by a seed-holding process that verifies the expected path and body.

Architecture flow

Training seed + derivation path → wallet key and address
        ↓
Explorer UTxOs → deterministic selection → fee and change
        ↓
Unsigned transaction plan → offline signing → RPC submission
        ↓
Rollback-aware wallet history

How this build works

This build gives users a structured way to derive training keys, discover addresses, select coin and token inputs, estimate fees, create change, build script and oracle transactions, separate online construction from offline signing, and retrieve wallet history through the Explorer index. Derivation paths separate payment, oracle, external, and internal roles so key usage is explicit. Selection must satisfy base coin, every native token, fees, and minimum change requirements without accidentally spending script-controlled outputs. An unsigned plan can be created on an online machine and signed later by a seed-holding process that verifies the expected path and body.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Training seed, derivation path, and wallet key derivation

Source: src/Coxy/Wallet.hs, lines 108–185 in the version 2.1 training tree.

  | otherwise = Left (WalletSeedLengthInvalid (ByteString.length bytes))

-- | Deterministically hash arbitrary text into a 32-byte training seed.
walletSeedFromText :: String -> WalletSeed
walletSeedFromText textValue =
  WalletSeed (sha256Bytes (ByteString8.pack textValue))

walletSeedFingerprint :: WalletSeed -> String
walletSeedFingerprint (WalletSeed seed) = take 16 (hashBytes seed)

-- | Export the 32-byte training seed for encrypted WalletStore persistence.
-- Callers must never log or transmit this value.
walletSeedBytes :: WalletSeed -> ByteString.ByteString
walletSeedBytes (WalletSeed seed) = seed

data WalletPurpose
  = PaymentPurpose
  | StakePurpose
  | GovernancePurpose
  | OraclePurpose
  deriving (Eq, Ord, Show, Read)

data WalletRole
  = ExternalRole
  | InternalRole
  deriving (Eq, Ord, Show, Read)

data DerivationPath = DerivationPath
  { derivationAccount :: Word32
  , derivationPurpose :: WalletPurpose
  , derivationRole    :: WalletRole
  , derivationIndex   :: Word32
  }
  deriving (Eq, Ord, Show, Read)

data WalletKey = WalletKey
  { walletKeyPathInternal    :: DerivationPath
  , walletKeySigningInternal :: SigningKey
  , walletKeyVerificationInternal :: VerificationKey
  , walletKeyAddressInternal :: Address
  }

walletKeyPath :: WalletKey -> DerivationPath
walletKeyPath = walletKeyPathInternal

walletKeyAddress :: WalletKey -> Address
walletKeyAddress = walletKeyAddressInternal

walletKeyVerificationKey :: WalletKey -> VerificationKey
walletKeyVerificationKey = walletKeyVerificationInternal

deriveWalletKey :: WalletSeed -> DerivationPath -> Either WalletError WalletKey
deriveWalletKey (WalletSeed rootSeed) path = do
  signingKey <-
    firstWalletDerivation
      ( signingKeyFromSeed
          ( sha256Bytes
              ( ByteString.concat
                  [ ByteString8.pack "coxy-training-wallet-v1|"
                  , rootSeed
                  , ByteString8.pack ("|" ++ encodePath path)
                  ]
              )
          )
      )
  let publicKey = verificationKey signingKey
  Right
    WalletKey
      { walletKeyPathInternal = path
      , walletKeySigningInternal = signingKey
      , walletKeyVerificationInternal = publicKey
      , walletKeyAddressInternal = addressFromVerificationKey publicKey
      }

encodePath :: DerivationPath -> String
encodePath path =
  show (derivationAccount path)
    ++ "/"

Payment-plan construction from paths or watch-only addresses

Source: src/Coxy/Wallet.hs, lines 570–640 in the version 2.1 training tree.

buildPaymentPlan
  :: WalletSeed
  -> QueryDatabase
  -> PaymentBuildRequest
  -> Either WalletError WalletTransactionPlan
buildPaymentPlan seed database request = do
  sourceKey <- deriveWalletKey seed (paymentBuildSourcePath request)
  changeKey <- deriveWalletKey seed (paymentBuildChangePath request)
  plan <-
    buildPaymentPlanForAddress
      database
      AddressPaymentBuildRequest
        { addressPaymentSourceAddress = walletKeyAddress sourceKey
        , addressPaymentChangeAddress = walletKeyAddress changeKey
        , addressPaymentTargets = paymentBuildTargets request
        , addressPaymentFeePolicy = paymentBuildFeePolicy request
        , addressPaymentNonce = paymentBuildNonce request
        }
  Right (plan { walletPlanPrimaryPath = Just (paymentBuildSourcePath request) })

buildPaymentPlanForAddress
  :: QueryDatabase
  -> AddressPaymentBuildRequest
  -> Either WalletError WalletTransactionPlan
buildPaymentPlanForAddress database request = do
  targets <- validateTargets (addressPaymentTargets request)
  if null targets then Left WalletPaymentTargetsEmpty else Right ()
  let targetCoin = sumCoins (map paymentTargetCoin targets)
      targetTokens = sumTokenBundles (map paymentTargetTokens targets)
      candidates =
        ordinaryAddressOutputs database (addressPaymentSourceAddress request)
  selection <-
    selectWalletInputs
      (addressPaymentFeePolicy request)
      targetCoin
      targetTokens
      (length targets)
      0
      0
      candidates
  let targetOutputs = map paymentTargetToOutput targets
      changeOutput =
        makeChangeOutput
          (addressPaymentChangeAddress request)
          (walletSelectionChangeCoin selection)
          (walletSelectionChangeTokens selection)
      outputs = targetOutputs ++ maybeToList changeOutput
      refs = map indexedOutputRef (walletSelectionInputs selection)
  body <-
    firstTransactionError
      ( mkTransaction
          refs
          outputs
          (walletSelectionFee selection)
          (fromIntegral (addressPaymentNonce request))
      )
  Right
    WalletTransactionPlan
      { walletPlanKind = WalletPaymentPlan
      , walletPlanUnsigned = body
      , walletPlanPrimaryAddress = addressPaymentSourceAddress request
      , walletPlanPrimaryPath = Nothing
      , walletPlanSelectedInputs = walletSelectionInputs selection
      , walletPlanFee = walletSelectionFee selection
      , walletPlanChangeOutput = changeOutput
      }

data ScriptSpendRequest = ScriptSpendRequest
  { scriptSpendPrimaryPath     :: DerivationPath
  , scriptSpendChangePath      :: DerivationPath
  , scriptSpendInput           :: TxOutRef

Wallet API configuration and route handling

Source: src/Coxy/WalletAPI.hs, lines 101–150 in the version 2.1 training tree.

  )
import Network.Wai.Handler.Warp
  ( defaultSettings
  , runSettings
  , setHost
  , setPort
  )
import Text.Read (readMaybe)

data WalletApiConfig = WalletApiConfig
  { walletApiHost                    :: String
  , walletApiPort                    :: Int
  , walletApiPrivateToken            :: Maybe ByteString.ByteString
  , walletApiEnablePrivateOperations :: Bool
  , walletApiCorsOrigin              :: Maybe ByteString.ByteString
  , walletApiMaxRequestBytes         :: Int64
  }
  deriving (Eq, Show)

defaultWalletApiConfig :: WalletApiConfig
defaultWalletApiConfig =
  WalletApiConfig
    { walletApiHost = "127.0.0.1"
    , walletApiPort = 9083
    , walletApiPrivateToken = Nothing
    , walletApiEnablePrivateOperations = False
    , walletApiCorsOrigin = Nothing
    , walletApiMaxRequestBytes = 1024 * 1024
    }

data WalletApiError
  = WalletApiBadRequest String
  | WalletApiPrivateOperationsDisabled
  | WalletApiAuthenticationFailed
  | WalletApiWalletFailure WalletError
  | WalletApiRuntimeFailure String
  deriving (Eq, Show)

data WalletServerHandle = WalletServerHandle
  { walletServerThread     :: ThreadId
  , walletServerCompletion :: MVar ()
  }

walletApplication :: WalletApiConfig -> IndexerHandle -> Application
walletApplication config indexer request respond
  | requestMethod request == methodOptions =
      respond (responseLBS status204 (commonHeaders config) LazyByteString.empty)
  | requestMethod request == methodGet = do
      database <- readQueryDatabase indexer
      routeGet database

Student practical laboratory

  1. Derive external and internal addresses from a fixed seed and confirm reproducibility.
  2. Fund several UTxOs and inspect deterministic coin and token selection.
  3. Build a payment plan on a watch-only path, transfer it to an offline signer, and verify the returned signature.
  4. Query wallet history before and after a chain rollback.

Use the following workflow after each change:

# Work in a branch dedicated to Build Ten
git switch -c training/build-10

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Real Cardano wallets use standards such as BIP-39 and CIP-1852 and must handle stake keys, multi-assets, min-UTxO rules, collateral, certificates, governance, hardware signing, and network parameters. This module is a conceptual bridge, not a compatible Cardano wallet.

COXY module Cardano area What to study next
Wallet/WalletAPI cardano-wallet and CIP-30 clients CIP-1852 derivation, balancing, collateral, hardware signing

Security and professional practice

The training API accepts raw seed material only in protected local exercises. Production wallets must use audited derivation standards, secure memory, hardware-backed storage where possible, and strong user confirmation.

Completion checkpoint

A student completes Build Ten only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Wallet.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

11. 🏛️ Build Eleven — Stake, Governance, and Encrypted Wallet Storage

Release stage: v1.1
Primary modules: Stake.hs, Governance.hs, WalletStore.hs

Purpose

This build introduces stake credentials, signed registration and delegation certificates, reward accounting, DRep and committee participation, governance-action lifecycles, encrypted local wallet persistence, recovery backups, and hardware or offline signing interfaces.

Learning objectives

  • Stake operations are authenticated certificates whose ordering and epoch constraints are validated before state changes.
  • Governance separates proposal, voting, ratification, enactment, rejection, and expiry, with different voter roles and threshold rules.
  • Wallet secrets are stored using authenticated encryption and password-based key derivation, while public signing requests can leave the secure boundary.

Architecture flow

Stake key → registration → pool delegation → epoch snapshot → rewards
Governance key → DRep/committee registration → signed votes → ratify/enact
Wallet seed → encrypted store/backup → offline or hardware signer

How this build works

This build introduces stake credentials, signed registration and delegation certificates, reward accounting, DRep and committee participation, governance-action lifecycles, encrypted local wallet persistence, recovery backups, and hardware or offline signing interfaces. Stake operations are authenticated certificates whose ordering and epoch constraints are validated before state changes. Governance separates proposal, voting, ratification, enactment, rejection, and expiry, with different voter roles and threshold rules. Wallet secrets are stored using authenticated encryption and password-based key derivation, while public signing requests can leave the secure boundary.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Stake credentials, certificates, rewards, and stake state

Source: src/Coxy/Stake.hs, lines 45–105 in the version 2.1 training tree.

  deriving (Eq, Ord, Show, Read)

newtype StakeCredential = StakeCredential
  { unStakeCredential :: String
  }
  deriving (Eq, Ord, Show, Read)

newtype StakePoolId = StakePoolId
  { unStakePoolId :: String
  }
  deriving (Eq, Ord, Show, Read)

-- | Signed stake operations. Every certificate carries the public key whose
-- hash forms the stake credential. The signature covers all fields except the
-- signature itself.
data StakeCertificate
  = RegisterStake
      { stakeCertificateCredential :: StakeCredential
      , stakeCertificateKey        :: VerificationKey
      , stakeCertificateEpoch      :: EpochNo
      , stakeCertificateSignature  :: SignatureBytes
      }
  | DelegateStake
      { stakeCertificateCredential :: StakeCredential
      , stakeCertificateKey        :: VerificationKey
      , stakeCertificatePool       :: StakePoolId
      , stakeCertificateEpoch      :: EpochNo
      , stakeCertificateSignature  :: SignatureBytes
      }
  | DeregisterStake
      { stakeCertificateCredential :: StakeCredential
      , stakeCertificateKey        :: VerificationKey
      , stakeCertificateEpoch      :: EpochNo
      , stakeCertificateSignature  :: SignatureBytes
      }
  | WithdrawStakeRewards
      { stakeCertificateCredential :: StakeCredential
      , stakeCertificateKey        :: VerificationKey
      , stakeCertificateAmount     :: Coin
      , stakeCertificateEpoch      :: EpochNo
      , stakeCertificateSignature  :: SignatureBytes
      }
  deriving (Eq, Show, Read)

data StakeAccount = StakeAccount
  { stakeAccountCredential       :: StakeCredential
  , stakeAccountKey              :: VerificationKey
  , stakeAccountRegisteredAt     :: EpochNo
  , stakeAccountDelegatedPool    :: Maybe StakePoolId
  , stakeAccountRewardBalance    :: Coin
  , stakeAccountTotalWithdrawn   :: Coin
  }
  deriving (Eq, Show, Read)

data StakeState = StakeState
  { stakeStateAccounts        :: Map StakeCredential StakeAccount
  , stakeStateLastRewardEpoch :: Maybe EpochNo
  }
  deriving (Eq, Show, Read)

data PoolRewardPot = PoolRewardPot

Governance actions, voter roles, choices, and lifecycle state

Source: src/Coxy/Governance.hs, lines 86–140 in the version 2.1 training tree.

data GovernanceActionKind
  = ParameterChange String ScriptData
  | TreasuryWithdrawal Address Coin
  | ConstitutionUpdate String
  | CommitteeUpdate [CommitteeId]
  | InformationAction String
  deriving (Eq, Ord, Show, Read)

data GovernanceActionStatus
  = GovernanceDraft
  | GovernanceVoting
  | GovernanceRatified
  | GovernanceEnacted
  | GovernanceRejected
  | GovernanceExpired
  deriving (Eq, Ord, Show, Read)

-- | Basis-point thresholds. 10,000 basis points equals 100 percent.
data GovernanceThresholds = GovernanceThresholds
  { governanceDRepApprovalBps          :: Word32
  , governanceDRepParticipationBps     :: Word32
  , governanceCommitteeApprovalBps     :: Word32
  , governanceCommitteeMinimumVotes    :: Word32
  }
  deriving (Eq, Show, Read)

defaultGovernanceThresholds :: GovernanceThresholds
defaultGovernanceThresholds =
  GovernanceThresholds
    { governanceDRepApprovalBps = 5000
    , governanceDRepParticipationBps = 2000
    , governanceCommitteeApprovalBps = 5000
    , governanceCommitteeMinimumVotes = 1
    }

data DRepRegistration = DRepRegistration
  { drepRegistrationId        :: DRepId
  , drepRegistrationName      :: String
  , drepRegistrationKey       :: VerificationKey
  , drepRegistrationEpoch     :: EpochNo
  , drepRegistrationSignature :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data CommitteeRegistration = CommitteeRegistration
  { committeeRegistrationId        :: CommitteeId
  , committeeRegistrationName      :: String
  , committeeRegistrationKey       :: VerificationKey
  , committeeRegistrationTermEnds  :: EpochNo
  , committeeRegistrationSignature :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data GovernanceVote = GovernanceVote
  { governanceVoteActionId :: GovernanceActionId

Encrypted wallet-store creation, opening, and password handling

Source: src/Coxy/WalletStore.hs, lines 210–275 in the version 2.1 training tree.

  | WalletStoreKdfInvalid KdfConfig
  | WalletStoreSaltLengthInvalid Int
  | WalletStoreNonceLengthInvalid Int
  | WalletStoreCipherInitializationFailed String
  | WalletStoreAeadInitializationFailed String
  | WalletStoreAuthenticationFailed
  | WalletStorePayloadInvalid
  | WalletStoreSeedInvalid String
  | WalletStoreIoError String
  | WalletStoreFileInvalid
  | WalletStoreVersionUnsupported Word32
  | WalletStoreBackupFingerprintMismatch String String
  | WalletStoreSigningAddressMismatch
  | WalletStoreSigningRequestMismatch
  | WalletStoreSignatureInvalid
  | WalletStoreWalletError WalletError
  | WalletStoreOfflineChecksumInvalid
  | WalletStoreOfflineResponseMismatch
  deriving (Eq, Show)

createWalletStore
  :: KdfConfig
  -> String
  -> WalletProfile
  -> IO (Either WalletStoreError WalletStoreEnvelope)
createWalletStore kdf password profile = do
  salt <- getRandomBytes 16
  nonce <- getRandomBytes 12
  pure (encryptProfile storeAssociatedData kdf salt nonce password profile)

openWalletStore
  :: String
  -> WalletStoreEnvelope
  -> Either WalletStoreError WalletProfile
openWalletStore = decryptProfile storeAssociatedData

saveWalletStore
  :: FilePath
  -> WalletStoreEnvelope
  -> IO (Either WalletStoreError ())
saveWalletStore path envelope =
  atomicWrite path (ByteString8.pack (show envelope))

loadWalletStore
  :: FilePath
  -> IO (Either WalletStoreError WalletStoreEnvelope)
loadWalletStore path = readStructuredFile path

changeWalletStorePassword
  :: KdfConfig
  -> String
  -> String
  -> WalletStoreEnvelope
  -> IO (Either WalletStoreError WalletStoreEnvelope)
changeWalletStorePassword kdf oldPassword newPassword envelope =
  case openWalletStore oldPassword envelope of
    Left err -> pure (Left err)
    Right profile -> createWalletStore kdf newPassword profile

createRecoveryBackup
  :: KdfConfig
  -> String
  -> WalletProfile
  -> Word64
  -> IO (Either WalletStoreError WalletBackupEnvelope)
createRecoveryBackup kdf password profile createdAt = do

Student practical laboratory

  1. Register a stake credential, delegate it to a pool, distribute an epoch reward, and withdraw the reward.
  2. Create a governance action, register voters, cast DRep and committee votes, and move the action through its lifecycle.
  3. Create an encrypted wallet store, open it with the correct and wrong passwords, and tamper with its ciphertext.
  4. Create an offline signing bundle and verify that the online side can validate the response without receiving the seed.

Use the following workflow after each change:

# Work in a branch dedicated to Build Eleven
git switch -c training/build-11

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano supports stake registration, delegation, rewards, DReps, constitutional committee voting, and governance actions through ledger certificates and transactions. Students should compare these training transitions with current Cardano ledger specifications and CIPs.

COXY module Cardano area What to study next
Stake/Governance ledger certificates and Conway governance delegation, rewards, DReps, committee, governance actions

Security and professional practice

The cryptographic envelope is training infrastructure rather than an audited keystore. Password handling, backup protection, key deletion, hardware integration, and recovery ceremonies require operational discipline beyond code correctness.

Completion checkpoint

A student completes Build Eleven only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Stake.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

12. 💰 Build Twelve — Economics, Genesis, Protocol Parameters, and Era Transitions

Release stage: v1.2
Primary modules: Economics.hs, Genesis.hs, ProtocolParams.hs

Purpose

This build defines the network’s monetary and constitutional starting point. It accounts for maximum and circulating supply, treasury, reserves, fees, rewards, and refundable deposits; schedules protocol updates; records epoch snapshots; validates genesis distribution; and generates reproducible bootstrap files.

Learning objectives

  • The economic invariant requires every coin to be accounted for in circulation, treasury, reserves, fee pot, reward pot, or deposits.
  • Protocol parameters are versioned state, and governed updates activate at explicit epochs instead of changing behaviour silently.
  • Genesis hashing normalizes allocations, producers, updates, and transitions so equivalent configurations produce the same network identity.

Architecture flow

Genesis allocations + monetary policy + producers + parameters
        ↓ validation and canonical normalization
Genesis hash → initial ledger/economics/consensus state
        ↓ epochs
Fees + reserve release → treasury/rewards → snapshots and parameter updates

How this build works

This build defines the network’s monetary and constitutional starting point. It accounts for maximum and circulating supply, treasury, reserves, fees, rewards, and refundable deposits; schedules protocol updates; records epoch snapshots; validates genesis distribution; and generates reproducible bootstrap files. The economic invariant requires every coin to be accounted for in circulation, treasury, reserves, fee pot, reward pot, or deposits. Protocol parameters are versioned state, and governed updates activate at explicit epochs instead of changing behaviour silently. Genesis hashing normalizes allocations, producers, updates, and transitions so equivalent configurations produce the same network identity.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Network eras, protocol parameters, and parameter-state history

Source: src/Coxy/ProtocolParams.hs, lines 92–150 in the version 2.1 training tree.

data ProtocolParameters = ProtocolParameters
  { protocolVersion  :: ProtocolVersion
  , protocolFees     :: FeeParameters
  , protocolDeposits :: DepositParameters
  , protocolEpoch    :: EpochParameters
  }
  deriving (Eq, Show, Read)

data DepositKind
  = StakeCredentialDeposit
  | StakePoolDeposit
  | DRepDeposit
  | GovernanceActionDeposit
  deriving (Eq, Ord, Show, Read)

-- | Optional fields allow governance to update one parameter without copying
-- unrelated values into the proposal.
data ProtocolParameterUpdate = ProtocolParameterUpdate
  { updateProtocolVersion  :: Maybe ProtocolVersion
  , updateFeeParameters    :: Maybe FeeParameters
  , updateDepositParameters :: Maybe DepositParameters
  , updateEpochParameters  :: Maybe EpochParameters
  }
  deriving (Eq, Show, Read)

data ScheduledProtocolUpdate = ScheduledProtocolUpdate
  { scheduledUpdateEpoch  :: EpochNo
  , scheduledUpdateSource :: String
  , scheduledUpdateValue  :: ProtocolParameterUpdate
  }
  deriving (Eq, Show, Read)

data EraTransition = EraTransition
  { eraTransitionFrom           :: NetworkEra
  , eraTransitionTo             :: NetworkEra
  , eraTransitionEpoch          :: EpochNo
  , eraTransitionMinimumVersion :: ProtocolVersion
  }
  deriving (Eq, Show, Read)

data ProtocolParameterState = ProtocolParameterState
  { parameterStateCurrentEra       :: NetworkEra
  , parameterStateCurrent          :: ProtocolParameters
  , parameterStateLastEpoch        :: EpochNo
  , parameterStatePendingUpdates   :: Map EpochNo ScheduledProtocolUpdate
  , parameterStatePendingEras      :: Map EpochNo EraTransition
  , parameterStateHistory          :: [(EpochNo, ProtocolParameters)]
  , parameterStateEraHistory       :: [(EpochNo, NetworkEra)]
  }
  deriving (Eq, Show, Read)

data ProtocolParameterError
  = ProtocolCoinParameterNegative String Coin
  | ProtocolVersionMajorZero
  | ProtocolEpochParameterZero String
  | ProtocolSecurityParameterTooLarge Word64 Word64
  | ProtocolTransactionLimitExceedsBlockLimit Word64 Word64
  | ProtocolTransactionTooLarge Word64 Word64
  | ProtocolUpdateInPast EpochNo EpochNo

Economic state and monetary accounting

Source: src/Coxy/Economics.hs, lines 120–180 in the version 2.1 training tree.

data EconomicState = EconomicState
  { economicPolicy             :: MonetaryPolicy
  , economicCirculatingSupply  :: Coin
  , economicTreasury           :: Coin
  , economicReserves           :: Coin
  , economicFeePot             :: Coin
  , economicRewardPot          :: Coin
  , economicDeposits           :: Map DepositId DepositRecord
  , economicLastEpoch          :: Maybe EpochNo
  , economicEpochSnapshots     :: Map EpochNo EpochSnapshot
  }
  deriving (Eq, Show, Read)

data EconomicError
  = EconomicCoinNegative String Coin
  | EconomicMaximumSupplyNonPositive Coin
  | EconomicBasisPointsInvalid String Word32
  | EconomicInitialSupplyExceedsMaximum Coin Coin Coin
  | EconomicInvariantBroken Coin Coin
  | EconomicInsufficientCirculation Coin Coin
  | EconomicInsufficientTreasury Coin Coin
  | EconomicInsufficientRewardPot Coin Coin
  | EconomicDepositAlreadyExists DepositId
  | EconomicDepositNotFound DepositId
  | EconomicDepositAmountMismatch DepositId Coin Coin
  | EconomicTransactionFeeBelowMinimum Coin Coin
  | EconomicGovernanceActionNotEnacted
  | EconomicGovernanceActionUnsupported
  | EconomicProtocolError ProtocolParameterError
  | EconomicEpochNotIncreasing EpochNo EpochNo
  | EconomicNoDelegatedStake
  | EconomicRewardAllocationNegative StakePoolId Coin
  deriving (Eq, Show, Read)

defaultMonetaryPolicy :: MonetaryPolicy
defaultMonetaryPolicy =
  MonetaryPolicy
    { monetaryMaximumSupply = Coin 45000000000
    , monetaryInitialTreasury = Coin 1000000000
    , monetaryExpansionRateBps = 50
    , monetaryTreasuryCutBps = 2000
    , monetaryReserveFloor = Coin 0
    }

initialiseEconomics
  :: MonetaryPolicy
  -> Coin
  -> Either EconomicError EconomicState
initialiseEconomics policy circulating = do
  validateMonetaryPolicy policy
  validateNonNegative "initial circulating supply" circulating
  let maximumSupply = monetaryMaximumSupply policy
      treasury = monetaryInitialTreasury policy
      committed = addCoin circulating treasury
  if committed > maximumSupply
    then
      Left
        ( EconomicInitialSupplyExceedsMaximum
            circulating
            treasury
            maximumSupply

Genesis configuration and generated bundle

Source: src/Coxy/Genesis.hs, lines 111–165 in the version 2.1 training tree.

data GenesisConfig = GenesisConfig
  { genesisNetworkId        :: String
  , genesisSystemStart      :: Word64
  , genesisConsensusSeed    :: String
  , genesisInitialEra       :: NetworkEra
  , genesisProtocolParameters :: ProtocolParameters
  , genesisMonetaryPolicy   :: MonetaryPolicy
  , genesisAllocations      :: [GenesisAllocation]
  , genesisProducers        :: [RegisteredProducer]
  , genesisProtocolUpdates  :: [GenesisProtocolUpdate]
  , genesisEraTransitions   :: [GenesisEraTransition]
  }
  deriving (Eq, Show)

data GenesisBundle = GenesisBundle
  { genesisBundleHash          :: String
  , genesisBundleLedger        :: Ledger
  , genesisBundleChainState    :: ChainState
  , genesisBundleEconomics     :: EconomicState
  , genesisBundleParameters    :: ProtocolParameterState
  , genesisBundleRegistry      :: ProducerRegistry
  , genesisBundleConsensus     :: ConsensusConfig
  }
  deriving (Eq, Show)

data BootstrapManifest = BootstrapManifest
  { bootstrapManifestVersion     :: Word64
  , bootstrapManifestGenesisHash :: String
  , bootstrapManifestFiles       :: Map FilePath String
  }
  deriving (Eq, Show, Read)

data GenesisError
  = GenesisNetworkIdEmpty
  | GenesisConsensusSeedEmpty
  | GenesisNoAllocations
  | GenesisNoProducers
  | GenesisAllocationLabelEmpty
  | GenesisDuplicateAllocationLabel String
  | GenesisAllocationNonPositive String Coin
  | GenesisTokenQuantityNonPositive String AssetId Integer
  | GenesisDuplicateProducerKey VerificationKey
  | GenesisDuplicateProtocolUpdate EpochNo
  | GenesisDuplicateEraTransition EpochNo
  | GenesisProtocolFailure ProtocolParameterError
  | GenesisEconomicsFailure String
  | GenesisConsensusFailure String
  | GenesisKeyFailure String
  | GenesisBootstrapIoFailure FilePath String
  | GenesisBootstrapMismatch FilePath String String
  deriving (Eq, Show)

defaultTrainingGenesisConfig :: Either GenesisError GenesisConfig
defaultTrainingGenesisConfig = do
  aliceKey <- keyFromByte 1

Student practical laboratory

  1. Generate bootstrap files twice from the same configuration and compare every byte and SHA-256 digest.
  2. Attempt a genesis allocation that exceeds maximum supply and confirm rejection.
  3. Collect fees and deposits, close an epoch, and verify the monetary invariant.
  4. Schedule a protocol update and an era transition, then attempt to activate them too early or out of sequence.

Use the following workflow after each change:

# Work in a branch dedicated to Build Twelve
git switch -c training/build-12

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano networks are defined by genesis files, era-specific parameters, monetary rules, protocol versions, and hard-fork transitions. Students should learn to treat these as consensus-critical configuration, not ordinary application settings.

COXY module Cardano area What to study next
Genesis/Economics/Params genesis and protocol parameters network magic, eras, hard forks, monetary accounting

Security and professional practice

A changed genesis hash creates a different network. Never reuse a database with incompatible genesis or protocol history, and verify bootstrap artifacts before node startup.

Completion checkpoint

A student completes Build Twelve only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Economics.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

13. 📊 Build Thirteen — Operator CLI, Structured Logging, Metrics, and Safe Maintenance

Release stage: v1.3
Primary modules: CLI.hs, Logging.hs, Metrics.hs

Purpose

This build turns the program into an operable service. It adds validated configuration, structured JSON logs, Prometheus-style metrics, liveness and readiness endpoints, bootstrap import, database inspection, snapshot export and import, guarded maintenance commands, and an atomic runtime lock.

Learning objectives

  • Liveness answers whether the process is functioning, while readiness answers whether it is safe to receive real work.
  • Structured logs use stable fields so operators can search events across nodes and correlate failures.
  • Destructive maintenance is replaced with verified staging, network confirmations, backups, checksums, and lock checks.

Architecture flow

operator.json + environment secrets → validated configuration
Node/indexer state → metrics snapshot → /metrics and health endpoints
Stopped database → verified snapshot/inspection/maintenance commands

How this build works

This build turns the program into an operable service. It adds validated configuration, structured JSON logs, Prometheus-style metrics, liveness and readiness endpoints, bootstrap import, database inspection, snapshot export and import, guarded maintenance commands, and an atomic runtime lock. Liveness answers whether the process is functioning, while readiness answers whether it is safe to receive real work. Structured logs use stable fields so operators can search events across nodes and correlate failures. Destructive maintenance is replaced with verified staging, network confirmations, backups, checksums, and lock checks.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Operator command types and command parsing

Source: src/Coxy/CLI.hs, lines 156–225 in the version 2.1 training tree.

data OperatorCommand
  = CommandHelp
  | CommandConfigInit FilePath
  | CommandConfigValidate FilePath
  | CommandBootstrapImport FilePath FilePath
  | CommandDatabaseInspect FilePath
  | CommandSnapshotExport FilePath FilePath
  | CommandSnapshotVerify FilePath
  | CommandSnapshotImport FilePath FilePath Bool String
  | CommandCleanupTemporary FilePath
  | CommandResetDatabase FilePath String
  | CommandHealthCheck FilePath
  deriving (Eq, Show)

data DatabaseReport = DatabaseReport
  { databaseReportRoot              :: FilePath
  , databaseReportMetadataPresent   :: Bool
  , databaseReportRuntimeLocked     :: Bool
  , databaseReportBlockFiles        :: Int
  , databaseReportQueryDbPresent    :: Bool
  , databaseReportTemporaryFiles    :: [FilePath]
  , databaseReportTotalFiles        :: Int
  , databaseReportTotalBytes        :: Integer
  , databaseReportRecoveryStatus    :: String
  , databaseReportSelectedHeight    :: Maybe Word64
  , databaseReportSelectedTip       :: Maybe String
  , databaseReportImmutableHeight   :: Maybe Word64
  , databaseReportForkTips          :: Maybe Int
  }
  deriving (Eq, Show, Read)

data SnapshotFile = SnapshotFile
  { snapshotFilePath   :: FilePath
  , snapshotFileSha256 :: String
  , snapshotFileBytes  :: Integer
  }
  deriving (Eq, Show, Read)

data SnapshotManifest = SnapshotManifest
  { snapshotManifestFormat    :: String
  , snapshotManifestNetworkId :: String
  , snapshotManifestCreatedAt :: String
  , snapshotManifestFiles     :: [SnapshotFile]
  }
  deriving (Eq, Show, Read)

defaultOperatorConfig :: OperatorConfig
defaultOperatorConfig =
  OperatorConfig
    { operatorNetworkId = "coxy-training"
    , operatorDataDirectory = "data/rpc-node"
    , operatorBootstrapDirectory = "config/bootstrap-example"
    , operatorImmutableDepth = 20
    , operatorSlotLengthMicroseconds = 1000000
    , operatorKeepAliveIntervalSlots = 10
    , operatorPeerTimeoutSlots = 30
    , operatorPersistenceIntervalSlots = 10
    , operatorMaxInboundMessagesPerSlot = 32
    , operatorMaxTransactionsPerBlock = 100
    , operatorP2PInboundQueueCapacity = 128
    , operatorP2POutboundQueueCapacity = 128
    , operatorP2PMaxLocatorHashes = 32
    , operatorP2PMaxHeadersPerMessage = 16
    , operatorP2PMaxBlocksPerMessage = 16
    , operatorTransportHost = "0.0.0.0"
    , operatorTransportPort = 3001
    , operatorTransportBacklog = 64
    , operatorTransportMaximumFrameBytes = 4 * 1024 * 1024
    , operatorTransportStaticPeers = []
    , operatorPeerTargetOutbound = 4

Structured log levels, entries, and logger interface

Source: src/Coxy/Logging.hs, lines 49–100 in the version 2.1 training tree.

data LogLevel
  = LogDebug
  | LogInfo
  | LogWarning
  | LogError
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

data LogDestination
  = LogToStdout
  | LogToFile FilePath
  deriving (Eq, Show, Read)

data LoggingConfig = LoggingConfig
  { loggingMinimumLevel :: LogLevel
  , loggingDestination  :: LogDestination
  }
  deriving (Eq, Show, Read)

defaultLoggingConfig :: LoggingConfig
defaultLoggingConfig =
  LoggingConfig
    { loggingMinimumLevel = LogInfo
    , loggingDestination = LogToStdout
    }

data LoggerSink = LoggerSink
  { loggerSinkHandle    :: Handle
  , loggerSinkCloseWhenDone :: Bool
  }

-- | A logger serialises all writes through one MVar so concurrent node,
-- indexer, RPC, and maintenance threads cannot interleave JSON lines.
data Logger = Logger
  { loggerConfigValue :: LoggingConfig
  , loggerSinkValue   :: MVar LoggerSink
  }

parseLogLevel :: String -> Maybe LogLevel
parseLogLevel input =
  case map toLowerAscii input of
    "debug" -> Just LogDebug
    "info" -> Just LogInfo
    "warning" -> Just LogWarning
    "warn" -> Just LogWarning
    "error" -> Just LogError
    _ -> Nothing

newLogger :: LoggingConfig -> IO (Either String Logger)
newLogger config = do
  sinkResult <-
    case loggingDestination config of
      LogToStdout -> pure (Right (LoggerSink stdout False))

Metrics snapshot and health status types

Source: src/Coxy/Metrics.hs, lines 108–165 in the version 2.1 training tree.

data HealthReport = HealthReport
  { healthReportState  :: HealthState
  , healthReportChecks :: [HealthCheck]
  }
  deriving (Eq, Show, Read)

data MetricsConfig = MetricsConfig
  { metricsHost :: String
  , metricsPort :: Int
  }
  deriving (Eq, Show, Read)

defaultMetricsConfig :: MetricsConfig
defaultMetricsConfig =
  MetricsConfig
    { metricsHost = "127.0.0.1"
    , metricsPort = 9090
    }

data MetricsServerHandle = MetricsServerHandle
  { metricsServerThread     :: ThreadId
  , metricsServerCompletion :: MVar ()
  }

collectRuntimeMetrics
  :: NodeState
  -> Maybe QueryDatabase
  -> [MetricSample]
collectRuntimeMetrics state database =
  chainSamples
    ++ nodeSamples
    ++ queueSamples
    ++ indexerSamples
  where
    p2p = nodeP2PState state
    store = p2pChainStore p2p
    selected = selectedChainState store
    selectedHeight = either (const 0) chainHeight selected
    selectedSlot =
      either
        (const 0)
        (maybe 0 id . chainLastSlot)
        selected
    metrics = nodeMetrics state
    chainSamples =
      [ gauge "coxy_chain_height" "Selected chain height." selectedHeight
      , gauge "coxy_chain_last_slot" "Last selected chain slot." selectedSlot
      , gauge "coxy_chain_stored_blocks" "Blocks retained across all forks." (storedBlockCount store)
      , gauge "coxy_chain_fork_tips" "Known fork tips." (length (forkTips store))
      , gauge "coxy_chain_immutable_height" "Current immutable height." (immutableHeight store)
      , gauge "coxy_mempool_transactions" "Transactions waiting in the mempool." (mempoolSize (nodeMempool state))
      , gauge "coxy_p2p_connected_peers" "Authenticated connected peers." (Map.size (p2pPeers p2p))
      , gauge "coxy_node_current_slot" "Current node clock slot." (unSlot (nodeCurrentSlot state))
      , gauge "coxy_node_lifecycle" "Node lifecycle encoded as a numeric state." (lifecycleValue (nodeLifecycle state))
      ]
    nodeSamples =
      [ counter "coxy_node_local_transactions_accepted_total" "Locally accepted transactions." (metricLocalTransactionsAccepted metrics)
      , counter "coxy_node_peer_transactions_accepted_total" "Peer transactions accepted." (metricPeerTransactionsAccepted metrics)

Student practical laboratory

  1. Create an operator configuration, introduce an invalid port collision, and run configuration validation.
  2. Start the service, read JSON logs, scrape /metrics, and compare liveness with readiness.
  3. Export and verify a snapshot, alter one file, and confirm checksum failure.
  4. Attempt database maintenance while the runtime lock exists, then repeat safely after shutdown.

Use the following workflow after each change:

# Work in a branch dedicated to Build Thirteen
git switch -c training/build-13

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Stake-pool and node operators depend on repeatable CLI workflows, monitoring, logs, health checks, snapshots, and safe upgrades. These operational skills are as important as Haskell programming when contributing to infrastructure repositories.

COXY module Cardano area What to study next
CLI/Logging/Metrics cardano-cli and node operations configuration, observability, snapshots, health checks

Security and professional practice

Metrics and logs can leak addresses, topology, tokens, or secrets. Define a data-classification policy and keep credentials out of configuration files and log contexts.

Completion checkpoint

A student completes Build Thirteen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how CLI.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

14. 🔒 Build Fourteen — Real TCP Transport, Secure Channels, and Peer Management

Release stage: v1.4
Primary modules: Transport.hs, TLS.hs, PeerManager.hs

Purpose

This build replaces queue-only transport with real TCP sockets and versioned frames. It introduces an authenticated encrypted training channel, peer discovery, a persistent address book, connection limits, exponential backoff, ban scoring, and deterministic network fault injection.

Learning objectives

  • Framing separates message boundaries from the TCP byte stream and enforces magic, version, type, size, and checksum rules.
  • The training secure channel authenticates node identities and derives directional encryption keys before application records are exchanged.
  • Peer management treats connection attempts, discovery, retries, bans, inbound limits, and outbound targets as persistent policy.

Architecture flow

TCP connect/accept → framed authenticated handshake
        ↓
Directional encrypted records → canonical P2P messages
        ↓
Peer manager → discovery, limits, backoff, bans, address-book persistence

How this build works

This build replaces queue-only transport with real TCP sockets and versioned frames. It introduces an authenticated encrypted training channel, peer discovery, a persistent address book, connection limits, exponential backoff, ban scoring, and deterministic network fault injection. Framing separates message boundaries from the TCP byte stream and enforces magic, version, type, size, and checksum rules. The training secure channel authenticates node identities and derives directional encryption keys before application records are exchanged. Peer management treats connection attempts, discovery, retries, bans, inbound limits, and outbound targets as persistent policy.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Frame types, transport configuration, and secure connection state

Source: src/Coxy/Transport.hs, lines 155–210 in the version 2.1 training tree.

data RawFrameType
  = ClientHelloFrame
  | ServerHelloFrame
  | ClientFinishFrame
  | SecurePayloadFrame
  deriving (Eq, Ord, Show, Read)

data TransportConfig = TransportConfig
  { transportListenHost       :: String
  , transportListenPort       :: Int
  , transportListenBacklog    :: Int
  , transportMaximumFrameBytes :: Int
  }
  deriving (Eq, Show, Read)

defaultTransportConfig :: TransportConfig
defaultTransportConfig =
  TransportConfig
    { transportListenHost = "127.0.0.1"
    , transportListenPort = 3001
    , transportListenBacklog = 64
    , transportMaximumFrameBytes = 4 * 1024 * 1024
    }

data TransportListener = TransportListener
  { listenerConfig :: TransportConfig
  , listenerSocket :: Socket
  }

data SecureConnection = SecureConnection
  { secureConnectionSocket          :: Socket
  , secureConnectionRemoteEndpoint  :: PeerEndpoint
  , secureConnectionRemoteHandshake :: PeerHandshake
  , secureConnectionSession         :: MVar SecureSession
  , secureConnectionSendLock        :: MVar ()
  , secureConnectionClosed          :: MVar Bool
  , secureConnectionCapabilities    :: NegotiatedCapabilities
  }

connectionRemoteEndpoint :: SecureConnection -> PeerEndpoint
connectionRemoteEndpoint = secureConnectionRemoteEndpoint

connectionRemoteHandshake :: SecureConnection -> PeerHandshake
connectionRemoteHandshake = secureConnectionRemoteHandshake

connectionSessionId :: SecureConnection -> IO String
connectionSessionId connection =
  secureSessionId <$> readMVar (secureConnectionSession connection)

connectionNegotiatedCapabilities :: SecureConnection -> NegotiatedCapabilities
connectionNegotiatedCapabilities = secureConnectionCapabilities

data TransportError
  = InvalidTransportConfiguration String
  | TransportAddressResolutionFailed String
  | TransportIoFailure String

Authenticated secure-channel configuration and handshake records

Source: src/Coxy/TLS.hs, lines 165–225 in the version 2.1 training tree.

data SecureSession = SecureSession
  { secureSessionIdInternal              :: String
  , secureSessionSendKey                 :: ByteString
  , secureSessionReceiveKey              :: ByteString
  , secureSessionSendSequenceInternal    :: Word64
  , secureSessionReceiveSequenceInternal :: Word64
  , secureSessionMaximumPlaintext        :: Int
  }
  deriving (Eq)

instance Show SecureSession where
  show session =
    "SecureSession { id = "
      ++ show (secureSessionIdInternal session)
      ++ ", sendSequence = "
      ++ show (secureSessionSendSequenceInternal session)
      ++ ", receiveSequence = "
      ++ show (secureSessionReceiveSequenceInternal session)
      ++ " }"

secureSessionId :: SecureSession -> String
secureSessionId = secureSessionIdInternal

secureSessionSendSequence :: SecureSession -> Word64
secureSessionSendSequence = secureSessionSendSequenceInternal

secureSessionReceiveSequence :: SecureSession -> Word64
secureSessionReceiveSequence = secureSessionReceiveSequenceInternal

data ProtectedPayload = ProtectedPayload
  { protectedPayloadSequence   :: Word64
  , protectedPayloadAuthTag    :: ByteString
  , protectedPayloadCiphertext :: ByteString
  }
  deriving (Eq, Show, Read)

data TLSError
  = InvalidPreSharedKeyLength Int
  | InvalidMaximumPlaintextBytes Int
  | IdentityKeyMismatch
  | ClientHelloChallengeLength Int
  | ServerHelloChallengeLength Int
  | ClientChallengeMismatch
  | InvalidClientHelloSignature
  | InvalidServerHelloSignature
  | InvalidServerPskAuthenticator
  | ClientFinishSessionMismatch String String
  | InvalidClientPskAuthenticator
  | InvalidClientFinishSignature
  | UnderlyingHandshakeFailure P2PError
  | PlaintextTooLarge Int Int
  | CiphertextTooLarge Int Int
  | UnexpectedSecureSequence Word64 Word64
  | InvalidAuthenticationTagLength Int
  | SecureAuthenticationFailed
  | SecureCipherInitializationFailed String
  | SecureAeadInitializationFailed String
  | SecureHelloDecodeFailed String
  deriving (Eq, Show)

createClientHello

Address-book and peer-manager runtime state

Source: src/Coxy/PeerManager.hs, lines 143–210 in the version 2.1 training tree.

data PeerManagerConfig = PeerManagerConfig
  { peerManagerAddressBookFile             :: FilePath
  , peerManagerTargetOutboundConnections   :: Int
  , peerManagerMaximumInboundConnections   :: Int
  , peerManagerMaximumOutboundConnections  :: Int
  , peerManagerDiscoveryBatchSize          :: Int
  , peerManagerInitialBackoffSeconds       :: Word64
  , peerManagerMaximumBackoffSeconds       :: Word64
  , peerManagerBanThreshold                :: Int
  , peerManagerBanDurationSeconds          :: Word64
  , peerManagerSupervisorDelayMicroseconds :: Int
  , peerManagerConnectTimeoutMicroseconds  :: Int
  }
  deriving (Eq, Show, Read)

defaultPeerManagerConfig :: PeerManagerConfig
defaultPeerManagerConfig =
  PeerManagerConfig
    { peerManagerAddressBookFile = "data/rpc-node/peers/address-book.dat"
    , peerManagerTargetOutboundConnections = 4
    , peerManagerMaximumInboundConnections = 32
    , peerManagerMaximumOutboundConnections = 8
    , peerManagerDiscoveryBatchSize = 32
    , peerManagerInitialBackoffSeconds = 5
    , peerManagerMaximumBackoffSeconds = 3600
    , peerManagerBanThreshold = 100
    , peerManagerBanDurationSeconds = 3600
    , peerManagerSupervisorDelayMicroseconds = 250000
    , peerManagerConnectTimeoutMicroseconds = 5000000
    }

data PeerManagerState = PeerManagerState
  { peerManagerStateConfig              :: PeerManagerConfig
  , peerManagerStateRecords             :: Map.Map PeerEndpoint PeerRecord
  , peerManagerStateInboundConnections  :: Int
  , peerManagerStateOutboundConnections :: Int
  }
  deriving (Eq, Show, Read)

data PeerManagerError
  = InvalidPeerManagerConfiguration String
  | InvalidPeerEndpoint PeerEndpoint
  | PeerConnectionLimitReached ConnectionDirection Int
  | DuplicateConnectedNode NodeId
  | UnknownPeerEndpoint PeerEndpoint
  | AddressBookIoFailure FilePath String
  | AddressBookDecodeFailure FilePath
  | AddressBookVersionMismatch Word16
  | AddressBookChecksumMismatch String String
  | PeerManagerTransportFailure TransportError
  | PeerManagerNodeFailure NodeError
  | PeerManagerP2PFailure String
  | PeerManagerRuntimeFailure String
  deriving (Eq, Show)

newPeerManagerState
  :: PeerManagerConfig
  -> Either PeerManagerError PeerManagerState
newPeerManagerState config = do
  validatePeerManagerConfig config
  Right
    PeerManagerState
      { peerManagerStateConfig = config
      , peerManagerStateRecords = Map.empty
      , peerManagerStateInboundConnections = 0
      , peerManagerStateOutboundConnections = 0
      }

Student practical laboratory

  1. Run two nodes over loopback TCP and exchange a keep-alive and tip announcement.
  2. Send a truncated, oversized, or checksum-corrupted frame and inspect the transport error.
  3. Fail repeated connection attempts and observe deterministic backoff and ban-score changes.
  4. Use the fault profile to drop, duplicate, corrupt, and truncate selected frames.

Use the following workflow after each change:

# Work in a branch dedicated to Build Fourteen
git switch -c training/build-14

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Production Cardano networking uses carefully specified protocols and maintained cryptographic transport components. The training secure channel is deliberately labelled as nonproduction; students should learn why protocol framing, identity, resource limits, and peer policy are distinct concerns.

COXY module Cardano area What to study next
Transport/PeerManager ouroboros-network transport TCP, mux, peer selection, resource limits

Security and professional practice

Do not deploy custom cryptography as TLS. Use maintained TLS 1.3 or Noise implementations, validate certificates or peer keys, and keep protocol parsing aggressively bounded.

Completion checkpoint

A student completes Build Fourteen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how Transport.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

15. 🧬 Build Fifteen — Canonical Protocol Codec, Capability Negotiation, and Network Simulation

Release stage: v1.5
Primary modules: ProtocolCodec.hs, VersionNegotiation.hs, NetworkSimulation.hs

Purpose

This build makes network encoding consensus-safe and compatibility-aware. It replaces application Show/Read transport with canonical binary encoding, versions mini-protocols independently, negotiates capabilities after authentication, and simulates deterministic multi-node latency, loss, partitions, forks, healing, and convergence.

Learning objectives

  • Canonical encoding requires one byte representation for each value, strict ordering, minimal integers, bounded nesting, and rejection of trailing data.
  • Mini-protocol versioning lets chain sync, block fetch, transaction submission, keep-alive, peer sharing, and version data evolve independently.
  • A pure simulator makes network failures repeatable, allowing convergence properties to be tested without wall-clock timing or real sockets.

Architecture flow

Typed protocol value → canonical bytes → framed secure record
Authenticated peers → capability offers → highest compatible mini-protocol versions
Simulation seed + link policies → scheduled deliveries → partitions/healing → convergence

How this build works

This build makes network encoding consensus-safe and compatibility-aware. It replaces application Show/Read transport with canonical binary encoding, versions mini-protocols independently, negotiates capabilities after authentication, and simulates deterministic multi-node latency, loss, partitions, forks, healing, and convergence. Canonical encoding requires one byte representation for each value, strict ordering, minimal integers, bounded nesting, and rejection of trailing data. Mini-protocol versioning lets chain sync, block fetch, transaction submission, keep-alive, peer sharing, and version data evolve independently. A pure simulator makes network failures repeatable, allowing convergence properties to be tested without wall-clock timing or real sockets.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Codec limits, canonical message families, and encoding entry points

Source: src/Coxy/ProtocolCodec.hs, lines 89–125 in the version 2.1 training tree.

encodeP2PMessageCanonical :: P2PMessage -> ByteString
encodeP2PMessageCanonical value =
  encodeTopLevel p2pFamily (putP2PMessage value)

decodeP2PMessageCanonical
  :: CodecLimits
  -> ByteString
  -> Either ProtocolCodecError P2PMessage
decodeP2PMessageCanonical limits =
  decodeTopLevel limits p2pFamily (getP2PMessage limits)

encodeCapabilityOfferCanonical :: CapabilityOffer -> ByteString
encodeCapabilityOfferCanonical value =
  encodeTopLevel capabilityOfferFamily (putCapabilityOffer value)

decodeCapabilityOfferCanonical
  :: CodecLimits
  -> ByteString
  -> Either ProtocolCodecError CapabilityOffer
decodeCapabilityOfferCanonical limits =
  decodeTopLevel limits capabilityOfferFamily (getCapabilityOffer limits)

encodeNegotiatedCapabilitiesCanonical :: NegotiatedCapabilities -> ByteString
encodeNegotiatedCapabilitiesCanonical value =
  encodeTopLevel negotiatedCapabilitiesFamily (putNegotiatedCapabilities value)

decodeNegotiatedCapabilitiesCanonical
  :: CodecLimits
  -> ByteString
  -> Either ProtocolCodecError NegotiatedCapabilities
decodeNegotiatedCapabilitiesCanonical limits =
  decodeTopLevel limits negotiatedCapabilitiesFamily (getNegotiatedCapabilities limits)

encodeNegotiationResponseCanonical :: NegotiationResponse -> ByteString
encodeNegotiationResponseCanonical value =
  encodeTopLevel negotiationResponseFamily (putNegotiationResponse value)

Mini-protocols, version ranges, and capability offers

Source: src/Coxy/VersionNegotiation.hs, lines 33–90 in the version 2.1 training tree.

data MiniProtocol
  = ChainSyncProtocol
  | BlockFetchProtocol
  | TransactionSubmissionProtocol
  | KeepAliveProtocol
  | PeerSharingProtocol
  | VersionDataProtocol
  deriving (Eq, Ord, Enum, Bounded, Show, Read)

data VersionRange = VersionRange
  { versionRangeMinimum :: ProtocolVersion
  , versionRangeMaximum :: ProtocolVersion
  }
  deriving (Eq, Ord, Show, Read)

-- | Public capability advertisement. Required protocols must be present in the
-- final intersection or negotiation fails.
data CapabilityOffer = CapabilityOffer
  { capabilityNetworkId          :: String
  , capabilitySoftwareName       :: String
  , capabilitySoftwareVersion    :: String
  , capabilityProtocols          :: Map MiniProtocol VersionRange
  , capabilityRequiredProtocols  :: Set MiniProtocol
  , capabilityMaximumMessageBytes :: Word32
  }
  deriving (Eq, Show, Read)

-- | Result agreed by both peers. The version map contains only protocols that
-- overlap. The maximum message size is the lower of the two advertised limits.
data NegotiatedCapabilities = NegotiatedCapabilities
  { negotiatedNetworkId           :: String
  , negotiatedProtocols           :: Map MiniProtocol ProtocolVersion
  , negotiatedMaximumMessageBytes :: Word32
  , negotiatedLocalSoftware       :: String
  , negotiatedRemoteSoftware      :: String
  }
  deriving (Eq, Show, Read)

data VersionNegotiationError
  = InvalidVersionRange MiniProtocol VersionRange
  | EmptySoftwareName
  | EmptySoftwareVersion
  | EmptyCapabilityNetworkId
  | InvalidMaximumMessageBytes Word32
  | RequiredProtocolNotAdvertised MiniProtocol
  | CapabilityNetworkMismatch String String
  | RequiredProtocolUnavailable MiniProtocol
  | NoCompatibleMiniProtocols
  deriving (Eq, Show, Read)


data NegotiationResponse
  = NegotiationAccepted CapabilityOffer
  | NegotiationRejected VersionNegotiationError
  deriving (Eq, Show, Read)

data CompatibilityResult
  = Compatible NegotiatedCapabilities

Simulation nodes, links, scheduled messages, and counters

Source: src/Coxy/NetworkSimulation.hs, lines 90–145 in the version 2.1 training tree.

data NetworkSimulation = NetworkSimulation
  { simulationTick              :: Word64
  , simulationSeed              :: Word64
  , simulationNodes             :: Map SimulationNodeId SimNode
  , simulationLinks             :: Map (SimulationNodeId, SimulationNodeId) LinkPolicy
  , simulationPartition         :: Maybe [Set SimulationNodeId]
  , simulationPendingMessages   :: [ScheduledMessage]
  , simulationNextSequence      :: Word64
  , simulationDeliveredMessages :: Word64
  , simulationDroppedMessages   :: Word64
  }
  deriving (Eq, Show, Read)

data SimulationError
  = EmptySimulation
  | DuplicateSimulationNode SimulationNodeId
  | UnknownSimulationNode SimulationNodeId
  | InvalidLinkLoss Word16
  | InvalidPartition [SimulationNodeId]
  | MissingSimulationParent SimulationNodeId SimBlockHash
  | SimulationDidNotBecomeIdle Int
  | SimulationDidNotConverge Int
  deriving (Eq, Show)

genesisSimBlockHash :: SimBlockHash
genesisSimBlockHash = SimBlockHash (replicate 64 '0')

defaultLinkPolicy :: LinkPolicy
defaultLinkPolicy =
  LinkPolicy
    { linkEnabled = True
    , linkLatencyTicks = 1
    , linkLossBasisPoints = 0
    }

newNetworkSimulation
  :: Word64
  -> [SimulationNodeId]
  -> Either SimulationError NetworkSimulation
newNetworkSimulation seed nodeIds
  | null nodeIds = Left EmptySimulation
  | Set.size (Set.fromList nodeIds) /= length nodeIds =
      Left (DuplicateSimulationNode (firstDuplicate nodeIds))
  | otherwise =
      Right
        NetworkSimulation
          { simulationTick = 0
          , simulationSeed = seed
          , simulationNodes =
              Map.fromList
                [ (nodeId, emptyNode nodeId)
                | nodeId <- nodeIds
                ]
          , simulationLinks = Map.empty
          , simulationPartition = Nothing
          , simulationPendingMessages = []

Student practical laboratory

  1. Round-trip every major P2P message through the canonical codec and verify byte-for-byte re-encoding.
  2. Append trailing bytes or create nonminimal integers and confirm decoder rejection.
  3. Negotiate peers with partially overlapping versions and inspect selected versions and message limits.
  4. Partition a three-node simulation, create independent forks, heal the partition, and run convergence rounds.

Use the following workflow after each change:

# Work in a branch dedicated to Build Fifteen
git switch -c training/build-15

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Cardano network protocols depend on canonical CBOR, version negotiation, and independently evolving mini-protocols. This build gives students the vocabulary needed to read protocol codecs and compatibility tables in production repositories.

COXY module Cardano area What to study next
ProtocolCodec/Negotiation CBOR and protocol versioning canonical encoding, mini-protocol versions, compatibility

Security and professional practice

A decoder is an attack surface. Validate lengths before allocation, reject ambiguous encodings, cap recursion and collection sizes, and fuzz every externally reachable parser.

Completion checkpoint

A student completes Build Fifteen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how ProtocolCodec.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

16. 🧪 Build Sixteen — Property Testing, Model Checking, Fuzzing, and Benchmarks

Release stage: v1.6
Primary modules: PropertyTesting.hs, ModelChecking.hs, Benchmark.hs

Purpose

This build moves testing beyond hand-written examples. It generates state-machine command sequences with QuickCheck, records deterministic replay plans, fuzzes canonical codecs, explores bounded fork and rollback state spaces, verifies resource limits, and measures reproducible performance workloads.

Learning objectives

  • Property tests assert invariants over many generated executions instead of checking only predetermined examples.
  • Model checking explores a bounded graph of reachable states and verifies rollback paths and convergence after healing.
  • Benchmarks use deterministic workloads and checksums so performance changes can be compared without silently changing the work performed.

Architecture flow

Seed → generated commands → implementation state machine → invariant checks
Bounded actions → breadth-first model exploration → violation trace
Deterministic workload → timed samples → checksum + baseline comparison

How this build works

This build moves testing beyond hand-written examples. It generates state-machine command sequences with QuickCheck, records deterministic replay plans, fuzzes canonical codecs, explores bounded fork and rollback state spaces, verifies resource limits, and measures reproducible performance workloads. Property tests assert invariants over many generated executions instead of checking only predetermined examples. Model checking explores a bounded graph of reachable states and verifies rollback paths and convergence after healing. Benchmarks use deterministic workloads and checksums so performance changes can be compared without silently changing the work performed.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

State-machine commands and deterministic replay plan

Source: src/Coxy/PropertyTesting.hs, lines 46–105 in the version 2.1 training tree.

data StateMachineCommand
  = ForgeCommand SimulationNodeId String
  | PartitionCommand [[SimulationNodeId]]
  | HealCommand
  | SetLatencyCommand SimulationNodeId SimulationNodeId Word64
  | SetPacketLossCommand SimulationNodeId SimulationNodeId Word16
  | AdvanceTicksCommand Int
  | AntiEntropyCommand
  deriving (Eq, Show, Read)

data StateMachineStep = StateMachineStep
  { stateMachineStepIndex   :: Int
  , stateMachineStepCommand :: StateMachineCommand
  , stateMachineStepTick    :: Word64
  , stateMachineStepTips    :: [(SimulationNodeId, SimBlockHash)]
  }
  deriving (Eq, Show, Read)

data ProtocolInvariantViolation
  = EmptyNodeSetInvariant
  | SelectedTipUnknown SimulationNodeId SimBlockHash
  | SelectedTipNotBest SimulationNodeId SimBlockHash SimBlockHash
  | BlockHashKeyMismatch SimulationNodeId SimBlockHash SimBlockHash
  | BlockParentUnknown SimulationNodeId SimBlockHash SimBlockHash
  | BlockHeightMismatch SimulationNodeId SimBlockHash Word64 Word64
  | LinkEndpointUnknown SimulationNodeId SimulationNodeId
  | LinkLossOutOfRange SimulationNodeId SimulationNodeId Word16
  | PartitionMembershipInvalid [SimulationNodeId]
  | DuplicateScheduledSequence Word64
  | ScheduledEndpointUnknown SimulationNodeId SimulationNodeId
  deriving (Eq, Show, Read)

data StateMachineFailure = StateMachineFailure
  { stateMachineFailureIndex      :: Int
  , stateMachineFailureCommand    :: Maybe StateMachineCommand
  , stateMachineFailureReason     :: String
  , stateMachineFailureViolations :: [ProtocolInvariantViolation]
  }
  deriving (Eq, Show, Read)

data StateMachineResult = StateMachineResult
  { stateMachineFinalSimulation :: NetworkSimulation
  , stateMachineTrace           :: [StateMachineStep]
  }
  deriving (Eq, Show, Read)

data CodecFuzzMutation
  = FlipBitMutation Int Int
  | TruncateMutation Int
  | AppendMutation ByteString
  | OverwriteMutation Int Word8
  deriving (Eq, Show, Read)

data CodecFuzzCase = CodecFuzzCase
  { codecFuzzCaseIndex    :: Int
  , codecFuzzCaseMutation :: CodecFuzzMutation
  , codecFuzzCaseBytes    :: ByteString
  }
  deriving (Eq, Show, Read)

Model actions, exploration configuration, and reports

Source: src/Coxy/ModelChecking.hs, lines 77–125 in the version 2.1 training tree.

data ModelCheckReport = ModelCheckReport
  { modelCheckedStates        :: Int
  , modelCheckedTransitions   :: Int
  , modelMaximumDepthReached  :: Int
  , modelRollbackTransitions  :: Int
  , modelHealingChecks        :: Int
  , modelUniqueFingerprints   :: [String]
  }
  deriving (Eq, Show, Read)

defaultModelCheckConfig :: ModelCheckConfig
defaultModelCheckConfig =
  ModelCheckConfig
    { modelCheckMaximumDepth = 5
    , modelCheckMaximumStates = 5000
    , modelCheckConvergenceRounds = 20
    , modelCheckIdleTickLimit = 10000
    }

modelStateFingerprint :: NetworkSimulation -> String
modelStateFingerprint = hashBytes . ByteString8.pack . show

observeRollbackTransition
  :: SimulationNodeId
  -> NetworkSimulation
  -> NetworkSimulation
  -> Either String RollbackTransition
observeRollbackTransition nodeId before after = do
  oldNode <- lookupModelNode nodeId before
  newNode <- lookupModelNode nodeId after
  let oldTip = simNodeSelectedTip oldNode
      newTip = simNodeSelectedTip newNode
      allBlocks =
        Map.union
          (simNodeKnownBlocks newNode)
          (simNodeKnownBlocks oldNode)
  oldPath <- pathToGenesis allBlocks oldTip
  newPath <- pathToGenesis allBlocks newTip
  common <-
    case find (`elem` newPath) oldPath of
      Nothing -> Left "tips do not share genesis"
      Just value -> Right value
  let rolledBack = takeWhile (/= common) oldPath
      forwardFromTip = takeWhile (/= common) newPath
  Right
    RollbackTransition
      { rollbackNode = nodeId
      , rollbackOldTip = oldTip
      , rollbackNewTip = newTip

Benchmark configuration and result model

Source: src/Coxy/Benchmark.hs, lines 58–115 in the version 2.1 training tree.

data BenchmarkResult = BenchmarkResult
  { benchmarkResultName                :: String
  , benchmarkResultOperations          :: Int
  , benchmarkResultSamples             :: Int
  , benchmarkResultMinimumNanoseconds   :: Word64
  , benchmarkResultMaximumNanoseconds   :: Word64
  , benchmarkResultMeanNanoseconds      :: Double
  , benchmarkResultOperationsPerSecond :: Double
  , benchmarkResultChecksum             :: Word64
  }
  deriving (Eq, Show, Read)

data BenchmarkReport = BenchmarkReport
  { benchmarkReportConfig      :: BenchmarkConfig
  , benchmarkReportEnvironment :: BenchmarkEnvironment
  , benchmarkReportResults     :: [BenchmarkResult]
  }
  deriving (Eq, Show, Read)

data BenchmarkBaselineEntry = BenchmarkBaselineEntry
  { benchmarkBaselineName            :: String
  , benchmarkBaselineMeanNanoseconds :: Double
  }
  deriving (Eq, Show, Read)

data BenchmarkRegression = BenchmarkRegression
  { benchmarkRegressionName       :: String
  , benchmarkRegressionBaseline   :: Double
  , benchmarkRegressionObserved   :: Double
  , benchmarkRegressionPercentage :: Double
  }
  deriving (Eq, Show, Read)

defaultBenchmarkConfig :: BenchmarkConfig
defaultBenchmarkConfig =
  BenchmarkConfig
    { benchmarkWarmupIterations = 3
    , benchmarkMeasuredIterations = 10
    , benchmarkSamples = 7
    , benchmarkWorkloadScale = 100
    }

benchmarkCase :: String -> IO Word64 -> BenchmarkCase
benchmarkCase = BenchmarkCase

defaultBenchmarkCases :: BenchmarkConfig -> [BenchmarkCase]
defaultBenchmarkCases config =
  [ benchmarkCase
      "protocol-codec-roundtrip"
      (codecRoundTripWorkload scale)
  , benchmarkCase
      "network-simulation-convergence"
      (networkSimulationWorkload (max 1 (scale `div` 10)))
  , benchmarkCase
      "fork-rollback-model-check"
      (modelCheckingWorkload (max 2 (min 4 (scale `div` 25))))
  ]
  where

Student practical laboratory

  1. Run the same replay plan twice and verify identical command sequences and final fingerprints.
  2. Introduce a deliberate bad fork-choice rule in a branch and observe the shortest failing trace.
  3. Run a codec fuzz campaign and inspect rejected, canonical, and incorrectly accepted mutations.
  4. Create a benchmark baseline, change the workload scale, and explain why checksums and operation counts matter.

Use the following workflow after each change:

# Work in a branch dedicated to Build Sixteen
git switch -c training/build-16

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Large Cardano repositories use property testing, simulation, conformance testing, and benchmarking extensively because distributed ledger bugs often appear only in long or adversarial sequences. Students should become comfortable reading generators, shrinkers, models, and invariant reports.

COXY module Cardano area What to study next
Property/Model/Benchmark QuickCheck and simulation suites generators, shrinkers, IOSim-style deterministic testing

Security and professional practice

A passing bounded model is evidence, not proof. Document exploration limits, preserve failing seeds, and prevent benchmarks from becoming release theatre by fixing workloads and environments.

Completion checkpoint

A student completes Build Sixteen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how PropertyTesting.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

17. ✅ Build Seventeen — Executable Formal Specifications and Release Qualification

Release stage: v1.7
Primary modules: FormalSpec.hs, AdversarialTesting.hs, ReleaseQualification.hs

Purpose

This build turns protocol expectations into executable specifications and release gates. It compares independent reference models with implementations, exercises Byzantine scenarios, runs soak and mutation campaigns, evaluates coverage and performance thresholds, and signs release manifests only after qualification succeeds.

Learning objectives

  • A refinement check compares two independently expressed meanings of the same rule, reducing the chance that tests merely repeat implementation logic.
  • Adversarial scenarios cover equivocation, eclipse isolation, withholding, downgrade attempts, malformed data, flooding, and fork races.
  • A signed release manifest binds version, source revision, qualification report, artifacts, sizes, and SHA-256 hashes.

Architecture flow

Executable requirements + reference models → refinement report
Byzantine/soak/mutation/model/benchmark evidence → qualification gates
Passing report + artifact hashes → Ed25519-signed release manifest

How this build works

This build turns protocol expectations into executable specifications and release gates. It compares independent reference models with implementations, exercises Byzantine scenarios, runs soak and mutation campaigns, evaluates coverage and performance thresholds, and signs release manifests only after qualification succeeds. A refinement check compares two independently expressed meanings of the same rule, reducing the chance that tests merely repeat implementation logic. Adversarial scenarios cover equivocation, eclipse isolation, withholding, downgrade attempts, malformed data, flooding, and fork races. A signed release manifest binds version, source revision, qualification report, artifacts, sizes, and SHA-256 hashes.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Executable specification and refinement result structures

Source: src/Coxy/FormalSpec.hs, lines 43–95 in the version 2.1 training tree.

data SpecificationCategory
  = CodecSpecification
  | NegotiationSpecification
  | ForkChoiceSpecification
  | SynchronisationSpecification
  | RollbackSpecification
  | ResourceSpecification
  deriving (Eq, Ord, Enum, Bounded, Show, Read)

-- | A named executable requirement. The function is deliberately kept
-- abstract so callers execute it through 'runExecutableSpecifications'.
data ExecutableSpecification = ExecutableSpecification
  { executableSpecificationName     :: String
  , executableSpecificationCategory :: SpecificationCategory
  , executeSpecification            :: Either String ()
  }

data SpecificationResult = SpecificationResult
  { specificationResultName     :: String
  , specificationResultCategory :: SpecificationCategory
  , specificationResultPassed   :: Bool
  , specificationResultDetails  :: String
  }
  deriving (Eq, Show, Read)

data RefinementResult = RefinementResult
  { refinementResultName    :: String
  , refinementResultPassed  :: Bool
  , refinementResultDetails :: String
  }
  deriving (Eq, Show, Read)

data FormalSpecReport = FormalSpecReport
  { formalSpecResults       :: [SpecificationResult]
  , formalRefinementResults :: [RefinementResult]
  }
  deriving (Eq, Show, Read)

defaultFormalSpecifications :: [ExecutableSpecification]
defaultFormalSpecifications =
  [ ExecutableSpecification
      "canonical-codec-roundtrip"
      CodecSpecification
      specificationCanonicalCodec
  , ExecutableSpecification
      "canonical-codec-rejects-trailing-bytes"
      CodecSpecification
      specificationRejectTrailingBytes
  , ExecutableSpecification
      "capability-negotiation-is-symmetric"
      NegotiationSpecification
      specificationNegotiationSymmetry
  , ExecutableSpecification

Byzantine scenarios and soak configuration

Source: src/Coxy/AdversarialTesting.hs, lines 36–90 in the version 2.1 training tree.

data ByzantineScenario
  = EquivocatingForkScenario
  | EclipseIsolationScenario
  | WithholdingRecoveryScenario
  | InvalidBlockInjectionScenario
  | CapabilityDowngradeScenario
  | CodecTamperingScenario
  | AnnouncementFloodScenario
  | EqualHeightRaceScenario
  deriving (Eq, Ord, Enum, Bounded, Show, Read)

data ByzantineScenarioResult = ByzantineScenarioResult
  { byzantineScenario       :: ByzantineScenario
  , byzantineScenarioPassed :: Bool
  , byzantineScenarioDetails :: String
  , byzantineDeliveredMessages :: Word64
  , byzantineDroppedMessages   :: Word64
  }
  deriving (Eq, Show, Read)

data AdversarialReport = AdversarialReport
  { adversarialScenarioResults :: [ByzantineScenarioResult]
  }
  deriving (Eq, Show, Read)

data SoakConfig = SoakConfig
  { soakSeed              :: Word64
  , soakSteps             :: Int
  , soakSize              :: Int
  , soakNodeCount         :: Int
  , soakConvergenceRounds :: Int
  }
  deriving (Eq, Show, Read)

data SoakReport = SoakReport
  { soakCommandsExecuted   :: Int
  , soakTraceLength        :: Int
  , soakFinalTick          :: Word64
  , soakDeliveredMessages  :: Word64
  , soakDroppedMessages    :: Word64
  , soakFinalFingerprint   :: String
  , soakConvergedAfterHeal :: Bool
  , soakInvariantViolations :: [ProtocolInvariantViolation]
  }
  deriving (Eq, Show, Read)

data ProtocolMutant
  = PreferShortestChainMutant
  | PreferLargestHashMutant
  | IgnorePartitionMutant
  | AcceptTrailingCodecBytesMutant
  | SelectLowestProtocolVersionMutant
  | IgnoreParentHeightMutant
  | IgnorePacketLossMutant
  deriving (Eq, Ord, Enum, Bounded, Show, Read)

Qualification configuration, evidence, and release result

Source: src/Coxy/ReleaseQualification.hs, lines 137–205 in the version 2.1 training tree.

data ReleaseQualificationConfig = ReleaseQualificationConfig
  { qualificationReleaseVersion        :: String
  , qualificationNetworkId             :: String
  , qualificationSourceRevision        :: String
  , qualificationModelCheckConfig       :: ModelCheckConfig
  , qualificationSoakConfig             :: SoakConfig
  , qualificationBenchmarkConfig        :: BenchmarkConfig
  , qualificationBenchmarkBaseline      :: [BenchmarkBaselineEntry]
  , qualificationAllowedRegressionPct   :: Double
  , qualificationRequireBaseline        :: Bool
  , qualificationCoverageGates          :: CoverageGates
  , qualificationCoverageEvidence       :: CoverageEvidence
  }
  deriving (Eq, Show, Read)

data QualificationCheck = QualificationCheck
  { qualificationCheckName    :: String
  , qualificationCheckPassed  :: Bool
  , qualificationCheckDetails :: String
  }
  deriving (Eq, Show, Read)

data QualificationReport = QualificationReport
  { qualificationReportReleaseVersion :: String
  , qualificationReportNetworkId      :: String
  , qualificationReportSourceRevision :: String
  , qualificationFormalReport         :: FormalSpecReport
  , qualificationAdversarialReport    :: AdversarialReport
  , qualificationSoakReport           :: Either String SoakReport
  , qualificationMutationReport       :: MutationReport
  , qualificationModelReport          :: Either String ModelCheckReport
  , qualificationCompatibility        :: [CompatibilityEntry]
  , qualificationBenchmarkReport      :: BenchmarkReport
  , qualificationBenchmarkRegressions :: [BenchmarkRegression]
  , qualificationCoverageResults      :: [CoverageGateResult]
  , qualificationChecks               :: [QualificationCheck]
  }
  deriving (Eq, Show, Read)

defaultReleaseQualificationConfig :: ReleaseQualificationConfig
defaultReleaseQualificationConfig =
  ReleaseQualificationConfig
    { qualificationReleaseVersion = "2.0.0"
    , qualificationNetworkId = "coxy-training"
    , qualificationSourceRevision = "uncommitted"
    , qualificationModelCheckConfig = defaultModelCheckConfig
    , qualificationSoakConfig = defaultSoakConfig
    , qualificationBenchmarkConfig = defaultBenchmarkConfig
    , qualificationBenchmarkBaseline = []
    , qualificationAllowedRegressionPct = 10
    , qualificationRequireBaseline = True
    , qualificationCoverageGates = defaultCoverageGates
    , qualificationCoverageEvidence = CoverageEvidence Nothing Nothing
    }

quickReleaseQualificationConfig :: ReleaseQualificationConfig
quickReleaseQualificationConfig =
  defaultReleaseQualificationConfig
    { qualificationModelCheckConfig =
        defaultModelCheckConfig
          { modelCheckMaximumDepth = 3
          , modelCheckMaximumStates = 2000
          }
    , qualificationSoakConfig =
        defaultSoakConfig
          { soakSteps = 250
          , soakSize = 20
          , soakNodeCount = 3
          , soakConvergenceRounds = 30

Student practical laboratory

  1. Run the quick qualification pipeline and inspect every gate rather than reading only the final pass/fail.
  2. Change one reference rule or implementation rule and verify that refinement detects the mismatch.
  3. Run mutation testing and identify which tests kill each deliberately wrong rule.
  4. Create a release manifest, change an artifact byte, and verify that the manifest no longer validates.

Use the following workflow after each change:

# Work in a branch dedicated to Build Seventeen
git switch -c training/build-17

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Production open-source releases depend on reproducible CI, compatibility evidence, security testing, performance tracking, signed artifacts, and transparent review. This build prepares students to participate responsibly rather than submitting untested code.

COXY module Cardano area What to study next
Formal/Adversarial/Qualification formal specifications and release CI refinement, conformance, security review, signed releases

Security and professional practice

Coverage percentages do not prove correctness. Require meaningful assertions, adversarial cases, reproducible evidence, independent review, and protected release-signing keys.

Completion checkpoint

A student completes Build Seventeen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how FormalSpec.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

18. 🚀 Build Eighteen — Coordinated Upgrades, Cluster Deployment, and Disaster Recovery

Release stage: v1.8
Primary modules: UpgradeCoordinator.hs, ClusterDeployment.hs, DisasterRecovery.hs

Purpose

This build expands from one node to a small training network. It creates signed upgrade plans, enforces rolling batches and producer quorum, generates reproducible container and systemd packages, replicates snapshots, runs failover drills, records key-rotation ceremonies, and provides operator runbooks.

Learning objectives

  • An upgrade plan is immutable and auditable, with explicit target versions, qualification evidence, batches, health gates, and rollback state.
  • Deployment packages exclude secrets and include manifests that commit to every generated file.
  • Disaster recovery selects verified replicas under recovery-point and recovery-time objectives instead of trusting the nearest copy.

Architecture flow

Signed release + cluster inventory → ordered rolling plan
Drain → install → verify → healthy or rollback
Snapshot → independent replicas → verification → failover drill → recovery report

How this build works

This build expands from one node to a small training network. It creates signed upgrade plans, enforces rolling batches and producer quorum, generates reproducible container and systemd packages, replicates snapshots, runs failover drills, records key-rotation ceremonies, and provides operator runbooks. An upgrade plan is immutable and auditable, with explicit target versions, qualification evidence, batches, health gates, and rollback state. Deployment packages exclude secrets and include manifests that commit to every generated file. Disaster recovery selects verified replicas under recovery-point and recovery-time objectives instead of trusting the nearest copy.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Upgrade plan, node stages, and coordinator state

Source: src/Coxy/UpgradeCoordinator.hs, lines 92–155 in the version 2.1 training tree.

data UpgradePlan = UpgradePlan
  { upgradePlanId                :: String
  , upgradePlanNetworkId         :: String
  , upgradePlanCurrentVersion    :: String
  , upgradePlanTargetVersion     :: String
  , upgradePlanQualificationHash :: String
  , upgradePlanPolicy            :: UpgradePolicy
  , upgradePlanMembers           :: [ClusterMember]
  , upgradePlanBatches           :: [[String]]
  }
  deriving (Eq, Show, Read)

data UpgradePlanError
  = EmptyUpgradePlanId
  | EmptyUpgradeNetworkId
  | EmptyUpgradeVersion
  | UpgradeVersionUnchanged String
  | MissingQualificationHash
  | EmptyUpgradeCluster
  | DuplicateClusterMember String
  | EmptyUpgradeBatch Int
  | UnknownUpgradeMember String
  | DuplicateUpgradeMember String
  | MissingUpgradeMember String
  | UpgradeBatchTooLarge Int Int
  | ProducerQuorumViolation Int Int
  | InvalidUpgradePolicy String
  | ClusterMemberVersionMismatch String String String
  | MixedVersionsDisallowed Int
  | MixedVersionCompatibilityFailure VersionNegotiationError
  | InvalidUpgradeReleaseSignature
  | UpgradeReleaseNetworkMismatch String String
  | UpgradeReleaseVersionMismatch String String
  | UpgradeReleaseQualificationMismatch String String
  | InvalidUpgradeTransition String String
  | UpgradeMemberNotInCurrentBatch String
  | UpgradeMemberStateMismatch String MemberUpgradeStatus
  | UpgradeCannotResume String
  deriving (Eq, Show, Read)

-- | Per-node state used by the coordinator.
data MemberUpgradeStatus
  = MemberPending
  | MemberDraining
  | MemberInstalling
  | MemberVerifying
  | MemberHealthy
  | MemberFailed String
  | MemberRolledBack
  deriving (Eq, Show, Read)

data UpgradePhase
  = UpgradePlanned
  | UpgradeRunning Int
  | UpgradePaused String
  | UpgradeComplete
  | UpgradeAborted String
  deriving (Eq, Show, Read)

data UpgradeEvent = UpgradeEvent
  { upgradeEventSequence :: Word64
  , upgradeEventMember   :: Maybe String
  , upgradeEventAction   :: String
  , upgradeEventDetails  :: String

Cluster roles and reproducible deployment package model

Source: src/Coxy/ClusterDeployment.hs, lines 38–85 in the version 2.1 training tree.

data DeploymentNode = DeploymentNode
  { deploymentNodeName          :: String
  , deploymentNodeRole          :: DeploymentRole
  , deploymentNodeHost          :: String
  , deploymentNodeTransportPort :: Int
  , deploymentNodeMetricsPort   :: Int
  , deploymentNodeDataDirectory :: FilePath
  }
  deriving (Eq, Ord, Show, Read)

-- | Reproducible packaging input. Secrets are intentionally absent.
data ClusterDeployment = ClusterDeployment
  { clusterDeploymentName           :: String
  , clusterDeploymentNetworkId      :: String
  , clusterDeploymentReleaseVersion :: String
  , clusterDeploymentImage          :: String
  , clusterDeploymentExecutable     :: FilePath
  , clusterDeploymentUser           :: String
  , clusterDeploymentNodes          :: [DeploymentNode]
  }
  deriving (Eq, Show, Read)

data DeploymentPackage = DeploymentPackage
  { deploymentPackageRoot  :: FilePath
  , deploymentPackageFiles :: [(FilePath, String)]
  }
  deriving (Eq, Show, Read)

data DeploymentError
  = EmptyClusterName
  | EmptyClusterNetwork
  | EmptyClusterVersion
  | EmptyContainerImage
  | EmptyDeploymentNodes
  | DuplicateDeploymentNode String
  | DuplicateTransportEndpoint String Int
  | DuplicateMetricsEndpoint String Int
  | InvalidDeploymentPort String Int
  | MissingProducerNode
  | MissingRelayNode
  | UnsafeDeploymentPath FilePath
  | DeploymentFileMissing FilePath
  | DeploymentChecksumMismatch FilePath String String
  | InvalidDeploymentManifest String
  deriving (Eq, Show, Read)

defaultTrainingCluster :: ClusterDeployment
defaultTrainingCluster =

Replica targets, recovery policy, and drill results

Source: src/Coxy/DisasterRecovery.hs, lines 48–105 in the version 2.1 training tree.

data ReplicaTarget = ReplicaTarget
  { replicaTargetName     :: String
  , replicaTargetLocation :: String
  , replicaTargetPriority :: Int
  }
  deriving (Eq, Ord, Show, Read)

-- | Latest health evidence for one replica.
data ReplicaHealth = ReplicaHealth
  { replicaHealthTarget         :: ReplicaTarget
  , replicaHealthAvailable      :: Bool
  , replicaHealthVerified       :: Bool
  , replicaHealthSnapshotHash   :: String
  , replicaHealthChainHeight    :: Word64
  , replicaHealthLastVerifiedAt :: Word64
  }
  deriving (Eq, Show, Read)

-- | One snapshot replicated to multiple independent targets.
data ReplicatedSnapshot = ReplicatedSnapshot
  { replicatedSnapshotNetworkId   :: String
  , replicatedSnapshotGenesisHash :: String
  , replicatedSnapshotHeight      :: Word64
  , replicatedSnapshotHash        :: String
  , replicatedSnapshotManifest    :: SnapshotManifest
  , replicatedSnapshotTargets     :: [ReplicaTarget]
  }
  deriving (Eq, Show, Read)

data DisasterRecoveryError
  = EmptyRecoveryNetwork
  | EmptyRecoveryGenesisHash
  | InsufficientReplicaTargets Int Int
  | DuplicateReplicaTarget String
  | InvalidReplicaTarget String
  | SnapshotNetworkMismatch String String
  | InvalidSnapshotFormat String
  | ReplicaHashMismatch String String String
  | NoHealthyFailoverReplica
  | FailoverDataLossExceeded Word64 Word64
  | FailoverRecoveryTimeExceeded Word64 Word64
  | EmptyKeyCeremonyId
  | InvalidKeyRotationThreshold Int Int
  | DuplicateKeyParticipant VerificationKey
  | DuplicateKeyParticipantName String
  | UnknownKeyParticipant VerificationKey
  | DuplicateKeyApproval VerificationKey
  | InsufficientKeyApprovals Int Int
  | KeyFingerprintUnchanged
  | KeyCeremonyStateError KeyRotationStatus
  | InvalidKeyApproval VerificationKey
  deriving (Eq, Show, Read)

data FailoverDrillConfig = FailoverDrillConfig
  { failoverRequiredVerifiedReplicas :: Int
  , failoverMaximumRecoveryTicks     :: Word64
  , failoverMaximumDataLossBlocks    :: Word64
  }

Student practical laboratory

  1. Generate a five-node package and verify the deployment manifest.
  2. Create a rollout that upgrades query and relay nodes before producers and confirm the maximum-unavailable rule.
  3. Inject a failed readiness check and practice pause, rollback, repair, and resume.
  4. Replicate a snapshot to several targets, corrupt one copy, and run the failover selection drill.

Use the following workflow after each change:

# Work in a branch dedicated to Build Eighteen
git switch -c training/build-18

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Stake-pool and service operators need staged upgrades, topology-aware availability, rollback plans, verified backups, and runbooks. These skills transfer to real Cardano testnet and production operations even though the training deployment format is custom.

COXY module Cardano area What to study next
Deployment/Recovery stake-pool and infrastructure operations rolling upgrades, backups, runbooks, key ceremonies

Security and professional practice

Never perform a first-time recovery during a real incident. Schedule drills, isolate staging restores, verify genesis and network identity, and protect cold and operational keys separately.

Completion checkpoint

A student completes Build Eighteen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how UpgradeCoordinator.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

19. 🤖 Build Nineteen — Remote Upgrade Agents, Control API, and Protected Secrets

Release stage: v1.9
Primary modules: UpgradeAgent.hs, ClusterControlAPI.hs, SecretsBackend.hs

Purpose

This build introduces authenticated remote control. Agents accept short-lived signed commands, persist resumable checkpoints, and report execution. A role-aware control API dispatches commands and gates rollout progress. A versioned secret backend restricts recipients, records acknowledgements, rotates values, and preserves audit history.

Learning objectives

  • Command signatures bind command ID, target node, action, validity window, nonce, rollout plan, and controller key.
  • Nonce monotonicity and duplicate command identifiers prevent replay even when transport requests are retried.
  • Secrets have names, scopes, versions, recipient policies, signatures, revocation states, and delivery acknowledgements.

Architecture flow

Authorized operator → signed command → controller validation
        ↓
Registered agent → replay checks → persistent execution steps
        ↓
Health report → rollout gate + hash-chained audit
Secret version → authorized recipient → acknowledgement/rotation/revocation

How this build works

This build introduces authenticated remote control. Agents accept short-lived signed commands, persist resumable checkpoints, and report execution. A role-aware control API dispatches commands and gates rollout progress. A versioned secret backend restricts recipients, records acknowledgements, rotates values, and preserves audit history. Command signatures bind command ID, target node, action, validity window, nonce, rollout plan, and controller key. Nonce monotonicity and duplicate command identifiers prevent replay even when transport requests are retried. Secrets have names, scopes, versions, recipient policies, signatures, revocation states, and delivery acknowledgements.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Agent actions, signed commands, and execution records

Source: src/Coxy/UpgradeAgent.hs, lines 33–95 in the version 2.1 training tree.

data AgentAction
  = DrainNode
  | InstallRelease FilePath String
  | InstallVerifiedRelease FilePath String String
  | RestartNode
  | VerifyReadiness
  | RollbackRelease String
  | PersistNode
  deriving (Eq, Ord, Show, Read)

data AgentCommand = AgentCommand
  { agentCommandId        :: String
  , agentCommandPlanId    :: String
  , agentCommandNode      :: String
  , agentCommandAction    :: AgentAction
  , agentCommandIssuedAt  :: Word64
  , agentCommandExpiresAt :: Word64
  , agentCommandNonce     :: Word64
  }
  deriving (Eq, Ord, Show, Read)

data SignedAgentCommand = SignedAgentCommand
  { signedAgentCommandBody      :: AgentCommand
  , signedAgentCommandSigner    :: VerificationKey
  , signedAgentCommandSignature :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data AgentStep = AgentStep
  { agentStepNumber  :: Int
  , agentStepName    :: String
  , agentStepDetails :: String
  }
  deriving (Eq, Ord, Show, Read)

data AgentExecution = AgentExecution
  { agentExecutionCommand   :: AgentCommand
  , agentExecutionNextStep  :: Int
  , agentExecutionSteps     :: [AgentStep]
  , agentExecutionCompleted :: Bool
  , agentExecutionFailure   :: Maybe String
  }
  deriving (Eq, Show, Read)

data AgentState = AgentState
  { agentStateNodeName       :: String
  , agentStateTrustedKeys    :: [VerificationKey]
  , agentStateHighestNonces  :: Map VerificationKey Word64
  , agentStateExecutions     :: Map String AgentExecution
  , agentStateAuditSequence  :: Word64
  }
  deriving (Eq, Show, Read)

data AgentError
  = EmptyAgentCommandId
  | UnsafeAgentCommandId String
  | AgentNodeMismatch String String
  | AgentCommandExpired Word64 Word64
  | AgentCommandFromFuture Word64 Word64
  | UntrustedAgentSigner
  | InvalidAgentSignature
  | ReplayedAgentNonce Word64 Word64
  | DuplicateAgentCommand String

Control roles, agent records, and rollout policy

Source: src/Coxy/ClusterControlAPI.hs, lines 60–120 in the version 2.1 training tree.

import Network.Wai
  ( Application
  , Request
  , pathInfo
  , requestHeaders
  , requestMethod
  , responseLBS
  , responseStream
  , strictRequestBody
  )
import Text.Read (readMaybe)

data ControlRole = ControlViewer | ControlOperator | ControlAdministrator
  deriving (Eq, Ord, Show, Read)

data ControlPrincipal = ControlPrincipal
  { controlPrincipalName  :: String
  , controlPrincipalToken :: String
  , controlPrincipalRole  :: ControlRole
  }
  deriving (Eq, Ord, Show, Read)

data AuditEntry = AuditEntry
  { auditEntrySequence     :: Word64
  , auditEntryTimestamp    :: Word64
  , auditEntryPrincipal    :: String
  , auditEntryAction       :: String
  , auditEntryTarget       :: String
  , auditEntryResult       :: String
  , auditEntryPreviousHash :: String
  , auditEntryHash         :: String
  }
  deriving (Eq, Ord, Show, Read)

data RolloutPolicy = RolloutPolicy
  { rolloutMaximumUnavailable :: Int
  , rolloutRequireReadiness   :: Bool
  , rolloutPauseOnFailure     :: Bool
  }
  deriving (Eq, Ord, Show, Read)

data RolloutNode = RolloutNode
  { rolloutNodeName       :: String
  , rolloutNodeRole       :: DeploymentRole
  , rolloutNodeHealthy    :: Bool
  , rolloutNodeCompleted  :: Bool
  , rolloutNodeInProgress :: Bool
  }
  deriving (Eq, Ord, Show, Read)

data RolloutState = RolloutState
  { rolloutStatePlanId  :: String
  , rolloutStatePolicy  :: RolloutPolicy
  , rolloutStateBatches :: [[String]]
  , rolloutStateBatch   :: Int
  , rolloutStateNodes   :: Map String RolloutNode
  , rolloutStatePaused  :: Maybe String
  }
  deriving (Eq, Show, Read)

data RemoteAgentEndpoint = RemoteAgentEndpoint

Secret scopes, versions, recipients, and delivery state

Source: src/Coxy/SecretsBackend.hs, lines 34–88 in the version 2.1 training tree.

newtype SecretName = SecretName String
  deriving (Eq, Ord, Show, Read)

data SecretScope
  = ClusterSecret
  | NodeSecret String
  | RoleSecret String
  deriving (Eq, Ord, Show, Read)

data SecretVersion = SecretVersion
  { secretVersionNumber    :: Word64
  , secretVersionCreatedAt :: Word64
  , secretVersionRevokedAt :: Maybe Word64
  }
  deriving (Eq, Ord, Show, Read)

data SecretEnvelope = SecretEnvelope
  { secretEnvelopeName       :: SecretName
  , secretEnvelopeScope      :: SecretScope
  , secretEnvelopeVersion    :: SecretVersion
  , secretEnvelopeCiphertext :: ByteString
  , secretEnvelopeDigest     :: String
  , secretEnvelopeSigner     :: VerificationKey
  , secretEnvelopeSignature  :: SignatureBytes
  , secretEnvelopeRecipients :: [String]
  , secretEnvelopeAcks       :: [String]
  }
  deriving (Eq, Show, Read)

data SecretsBackend = SecretsBackend
  { secretsBackendNetworkId :: String
  , secretsBackendKeyHash   :: String
  , secretsBackendEntries   :: Map SecretName [SecretEnvelope]
  , secretsBackendSequence  :: Word64
  }
  deriving (Eq, Show, Read)

data SecretsError
  = EmptySecretName
  | EmptySecretValue
  | InvalidSecretSignature
  | SecretDigestMismatch
  | SecretNotFound SecretName
  | SecretVersionNotFound SecretName Word64
  | SecretRevoked SecretName Word64
  | SecretRecipientNotAllowed String
  | SecretAlreadyAcknowledged String
  | SecretsBackendKeyMismatch
  | InvalidSecretsBackendFile FilePath
  | SecretsBackendChecksumMismatch
  deriving (Eq, Show, Read)

emptySecretsBackend :: String -> ByteString -> SecretsBackend
emptySecretsBackend networkId masterKey =
  SecretsBackend networkId (hashBytes masterKey) Map.empty 0

Student practical laboratory

  1. Create and sign a command, then test wrong node, expired time, repeated nonce, duplicate ID, and untrusted signer cases.
  2. Persist an execution after one step, restart the agent state, and resume from the unfinished step.
  3. Use viewer and operator tokens against the control API and confirm role boundaries.
  4. Rotate a shared secret, verify that the old version is revoked, and record delivery acknowledgements.

Use the following workflow after each change:

# Work in a branch dedicated to Build Nineteen
git switch -c training/build-19

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Remote fleet management is operational infrastructure around a node rather than a ledger feature. The professional lessons are universal: command-level signatures, least privilege, replay protection, resumability, auditability, and secret lifecycle management.

COXY module Cardano area What to study next
Agents/Control/Secrets secure fleet management least privilege, replay protection, secret rotation, audit trails

Security and professional practice

A control plane is a high-value target. Separate controller keys from API tokens, minimize agent privileges, require short validity windows, and design every command to be safe under retries.

Completion checkpoint

A student completes Build Nineteen only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how UpgradeAgent.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

20. 📡 Build Twenty — Mutual-TLS Control Transport, Artifact Transfer, and Deployment Adapters

Release stage: v2.0
Primary modules: ControlTransport.hs, AgentControlAPI.hs, ArtifactTransfer.hs, DeploymentAdapter.hs, VirtualHostIntegration.hs

Purpose

This build connects the remote control model to real host boundaries. Nginx and curl provide mutual TLS, agents stream command progress, artifacts are uploaded in resumable verified chunks, systemd and container adapters activate releases without shell-string construction, and isolated virtual-host tests exercise the complete deployment path.

Learning objectives

  • Transport authentication and command signatures are independent layers, so compromise of one credential does not silently remove every check.
  • Artifact verification occurs per chunk and again for the complete finalized file before an installation action can reference it.
  • Deployment adapters use argument-safe process invocation, allow-listed services, versioned release directories, readiness checks, and rollback steps.

Architecture flow

Controller certificate ⇄ agent certificate through mTLS proxy
Signed command → agent API → streamed progress events
Artifact metadata → verified chunks → finalized digest
Adapter → activate systemd/container release → readiness → completion

How this build works

This build connects the remote control model to real host boundaries. Nginx and curl provide mutual TLS, agents stream command progress, artifacts are uploaded in resumable verified chunks, systemd and container adapters activate releases without shell-string construction, and isolated virtual-host tests exercise the complete deployment path. Transport authentication and command signatures are independent layers, so compromise of one credential does not silently remove every check. Artifact verification occurs per chunk and again for the complete finalized file before an installation action can reference it. Deployment adapters use argument-safe process invocation, allow-listed services, versioned release directories, readiness checks, and rollback steps.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Remote-agent endpoints, mTLS client configuration, and transport errors

Source: src/Coxy/ControlTransport.hs, lines 75–130 in the version 2.1 training tree.

  { authenticatedPeerSubject     :: String
  , authenticatedPeerFingerprint :: String
  }
  deriving (Eq, Ord, Show, Read)

data AgentProgressStatus
  = ProgressAccepted
  | ProgressStarted
  | ProgressStepCompleted
  | ProgressCompleted
  | ProgressFailed
  deriving (Eq, Ord, Show, Read)

data AgentProgressEvent = AgentProgressEvent
  { progressEventSequence  :: Word64
  , progressEventTimestamp :: Word64
  , progressEventNode      :: String
  , progressEventCommandId :: String
  , progressEventStep      :: Maybe Int
  , progressEventStatus    :: AgentProgressStatus
  , progressEventMessage   :: String
  }
  deriving (Eq, Ord, Show, Read)

newtype AgentProgressHub = AgentProgressHub (TChan AgentProgressEvent)

data ControlTransportError
  = MTLSVerificationMissing
  | MTLSVerificationFailed String
  | MTLSSubjectMissing
  | MTLSFingerprintMissing
  | MTLSSubjectNotAllowed String
  | MTLSFingerprintNotAllowed String
  | InvalidRemotePathSegment String
  | InvalidMutualTLSClientConfig String
  | CurlProcessFailed Int String
  | CurlInvocationFailed String
  | CurlOutputInvalid String
  | ProgressStreamEnded
  | ProgressEventInvalid String
  deriving (Eq, Show, Read)

newAgentProgressHub :: IO AgentProgressHub
newAgentProgressHub = AgentProgressHub <$> newBroadcastTChanIO

publishAgentProgress :: AgentProgressHub -> AgentProgressEvent -> IO ()
publishAgentProgress (AgentProgressHub channel) event =
  atomically (writeTChan channel event)

subscribeAgentProgress :: AgentProgressHub -> IO (TChan AgentProgressEvent)
subscribeAgentProgress (AgentProgressHub channel) = atomically (dupTChan channel)

validateMutualTLSProxyHeaders
  :: MutualTLSProxyPolicy
  -> RequestHeaders
  -> Either ControlTransportError AuthenticatedControlPeer

Artifact metadata, chunks, completion, and validation failures

Source: src/Coxy/ArtifactTransfer.hs, lines 34–95 in the version 2.1 training tree.

newtype ArtifactId = ArtifactId
  { unArtifactId :: String
  }
  deriving (Eq, Ord, Show, Read)

data ArtifactManifest = ArtifactManifest
  { artifactManifestId         :: ArtifactId
  , artifactManifestFileName   :: FilePath
  , artifactManifestSize       :: Word64
  , artifactManifestSha256     :: String
  , artifactManifestChunkSize  :: Int
  , artifactManifestChunkCount :: Int
  }
  deriving (Eq, Ord, Show, Read)

data ArtifactUploadStatus = ArtifactUploadStatus
  { artifactUploadManifest       :: ArtifactManifest
  , artifactUploadReceivedChunks :: [Int]
  , artifactUploadMissingChunks  :: [Int]
  , artifactUploadFinalized      :: Bool
  , artifactUploadFinalPath      :: Maybe FilePath
  }
  deriving (Eq, Ord, Show, Read)

data ArtifactError
  = EmptyArtifactId
  | UnsafeArtifactId String
  | UnsafeArtifactFileName FilePath
  | EmptyArtifactPayload
  | InvalidArtifactChunkSize Int
  | InvalidArtifactChunkCount Int
  | ArtifactTooLarge Word64 Word64
  | ArtifactChunkSizeLimit Int Int
  | ArtifactChunkCountLimit Int Int
  | ArtifactAlreadyStarted ArtifactId
  | ArtifactNotStarted ArtifactId
  | ArtifactManifestMismatch ArtifactId
  | ArtifactChunkOutOfRange Int Int
  | ArtifactChunkTooLarge Int Int
  | ArtifactChunkSizeMismatch Int Int Int
  | ArtifactChunkDigestMismatch Int String String
  | ArtifactChunkMissing [Int]
  | ArtifactSizeMismatch Word64 Word64
  | ArtifactDigestMismatch String String
  | ArtifactManifestCorrupt FilePath
  | ArtifactFinalizedAlready ArtifactId
  | ArtifactFinalPathOutsideRoot FilePath
  | ArtifactFinalPathInvalid FilePath
  | ArtifactNotFinalized ArtifactId
  deriving (Eq, Show, Read)

createArtifactManifest
  :: ArtifactId
  -> FilePath
  -> Int
  -> ByteString
  -> Either ArtifactError ArtifactManifest
createArtifactManifest artifactId fileName chunkSize payload = do
  validateArtifactId artifactId
  validateFileName fileName
  require (not (ByteString.null payload)) EmptyArtifactPayload
  require (chunkSize > 0) (InvalidArtifactChunkSize chunkSize)

Systemd, Docker, Podman, and mock deployment adapters

Source: src/Coxy/DeploymentAdapter.hs, lines 64–115 in the version 2.1 training tree.

data DeploymentAdapter
  = SystemdAdapter SystemdAdapterConfig
  | ContainerAdapter ContainerAdapterConfig
  | MockAdapter MockAdapterConfig
  deriving (Eq, Ord, Show, Read)

data AdapterOperation
  = RunProcess FilePath [String]
  | VerifyArtifact FilePath String
  | CopyArtifact FilePath FilePath
  | ReplacePath FilePath FilePath
  | MakeExecutable FilePath
  | WriteTextFile FilePath String
  | AppendTextFile FilePath String
  deriving (Eq, Ord, Show, Read)

data AdapterStep = AdapterStep
  { adapterStepNumber    :: Int
  , adapterStepName      :: String
  , adapterStepOperation :: AdapterOperation
  }
  deriving (Eq, Ord, Show, Read)

data AdapterStepResult = AdapterStepResult
  { adapterResultStep   :: AdapterStep
  , adapterResultOutput :: String
  }
  deriving (Eq, Ord, Show, Read)

data AdapterError
  = EmptyAdapterUnit
  | UnsafeAdapterPath FilePath
  | ArtifactFileMissing FilePath
  | ArtifactDigestInvalid FilePath String String
  | AdapterProcessFailed FilePath [String] Int String
  | AdapterFileOperationFailed String
  | UnsupportedAdapterAction String
  deriving (Eq, Show, Read)

planDeploymentAction :: DeploymentAdapter -> AgentAction -> Either AdapterError [AdapterStep]
planDeploymentAction adapter action =
  case adapter of
    SystemdAdapter config -> planSystemd config action
    ContainerAdapter config -> planContainer config action
    MockAdapter config -> planMock config action

planSystemd :: SystemdAdapterConfig -> AgentAction -> Either AdapterError [AdapterStep]
planSystemd config action = do
  require (not (null (systemdUnitName config))) EmptyAdapterUnit
  requireAbsolute (systemdExecutable config)
  requireAbsolute (systemdReleaseRoot config)
  requireAbsolute (systemdCurrentLink config)

Student practical laboratory

  1. Generate the disposable training PKI and verify each certificate chain.
  2. Deliver a signed command through the mutual-TLS boundary and consume its progress stream.
  3. Interrupt a multi-chunk upload, inspect missing chunks, resume, and finalize it.
  4. Run the isolated multi-host integration executable, then repeat on disposable hosts with the real proxy and chosen adapter.

Use the following workflow after each change:

# Work in a branch dedicated to Build Twenty
git switch -c training/build-20

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

Real infrastructure commonly combines TLS, signed artifacts, service managers, containers, readiness gates, and audit logs. Students should learn to distinguish ledger trust from deployment trust and to use maintained PKI and orchestration components.

COXY module Cardano area What to study next
mTLS/Artifacts/Adapters production deployment pipelines PKI, artifact signing, systemd, containers, readiness gates

Security and professional practice

The Haskell service deliberately delegates TLS to maintained tools. Protect CA keys, check certificate revocation, disable proxy buffering for streams, constrain root-level adapters, and never install an artifact by filename alone.

Completion checkpoint

A student completes Build Twenty only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how ControlTransport.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

21. 🛡️ Build Twenty-One — High-Availability Control Plane and Cross-Region Recovery

Release stage: v2.1
Primary modules: ControlPlaneHA.hs, DurableControlStore.hs, HAControlRuntime.hs, HAClusterControlAPI.hs, CertificateLifecycle.hs, RolloutApproval.hs, CrossRegionRecovery.hs

Purpose

The final build removes the controller as a single operational point of failure. It replicates controller state, elects one leader, commits changes only after quorum acknowledgement, stores hash-chained events and audits durably, manages certificate revocation and renewal, requires delegated rollout approvals, and tests recovery across regions.

Learning objectives

  • Only one healthy leader may accept writes, while followers serve replicated reads and catch up through committed events.
  • Durable event and audit records use sequential indexes, previous hashes, payload hashes, and idempotent duplicate handling.
  • Rollout authority is separated across roles and time-bounded delegations, while regional recovery rejects divergent or insufficiently current snapshots.

Architecture flow

Controller quorum → election term → one leader
Write proposal → follower acknowledgements → committed replicated event
        ↓
Durable event/audit/snapshot → follower catch-up
Certificate and approval policy → authorized rollout
Region failure → verified replica selection → new leader → convergence

How this build works

The final build removes the controller as a single operational point of failure. It replicates controller state, elects one leader, commits changes only after quorum acknowledgement, stores hash-chained events and audits durably, manages certificate revocation and renewal, requires delegated rollout approvals, and tests recovery across regions. Only one healthy leader may accept writes, while followers serve replicated reads and catch up through committed events. Durable event and audit records use sequential indexes, previous hashes, payload hashes, and idempotent duplicate handling. Rollout authority is separated across roles and time-bounded delegations, while regional recovery rejects divergent or insufficiently current snapshots.

The build preserves the earlier invariants rather than replacing them. New data enters through explicit constructors and is validated before it can affect the selected ledger or operational state. Errors remain typed so tests and operators can identify the exact stage that rejected an action. This pattern is essential in distributed systems because a generic failure such as “invalid data” is not enough to diagnose whether the problem was encoding, authentication, consensus, ledger rules, persistence, authorization, or deployment state.

Actual source code excerpts

Controller identities, roles, replicated events, and HA cluster state

Source: src/Coxy/ControlPlaneHA.hs, lines 49–120 in the version 2.1 training tree.

data ControllerRole
  = ControllerFollower
  | ControllerCandidate
  | ControllerLeader
  deriving (Eq, Ord, Show, Read)

data HAConfig = HAConfig
  { haRequiredQuorum       :: Int
  , haElectionTimeoutTicks :: Word64
  , haMaximumClockSkew     :: Word64
  }
  deriving (Eq, Show, Read)

defaultHAConfig :: HAConfig
defaultHAConfig =
  HAConfig
    { haRequiredQuorum = 2
    , haElectionTimeoutTicks = 10
    , haMaximumClockSkew = 2
    }

data ControllerReplica = ControllerReplica
  { controllerReplicaId             :: ControllerId
  , controllerReplicaRegion         :: RegionId
  , controllerReplicaRole           :: ControllerRole
  , controllerReplicaTerm           :: Word64
  , controllerReplicaVotedFor       :: Maybe ControllerId
  , controllerReplicaHealthy        :: Bool
  , controllerReplicaLastHeartbeat  :: Word64
  , controllerReplicaCommittedIndex :: Word64
  , controllerReplicaAppliedIndex   :: Word64
  , controllerReplicaState          :: ClusterControlState
  }
  deriving (Eq, Show, Read)

data ReplicatedControlEvent = ReplicatedControlEvent
  { replicatedEventIndex            :: Word64
  , replicatedEventTerm             :: Word64
  , replicatedEventLeader           :: ControllerId
  , replicatedEventTimestamp        :: Word64
  , replicatedEventPreviousHash     :: String
  , replicatedEventStateHash        :: String
  , replicatedEventHash             :: String
  , replicatedEventState            :: ClusterControlState
  , replicatedEventAcknowledgements :: Set ControllerId
  , replicatedEventCommitted        :: Bool
  }
  deriving (Eq, Show, Read)

data ControllerCluster = ControllerCluster
  { controllerClusterConfig   :: HAConfig
  , controllerClusterMembers  :: Map ControllerId ControllerReplica
  , controllerClusterLeaderId :: Maybe ControllerId
  , controllerClusterTerm     :: Word64
  , controllerClusterTick     :: Word64
  , controllerClusterEvents   :: Map Word64 ReplicatedControlEvent
  }
  deriving (Eq, Show, Read)

data HAError
  = InvalidHAConfig String
  | DuplicateController ControllerId
  | UnknownController ControllerId
  | UnhealthyController ControllerId
  | NoControllerQuorum Int Int
  | ControllerNotLeader ControllerId
  | NoElectedLeader
  | LeaderAlreadyElected ControllerId
  | ElectionTermRegression Word64 Word64
  | ElectionTimestampRegression Word64 Word64
  | ReplicatedEventNotFound Word64
  | ReplicatedEventAlreadyCommitted Word64

Durable event, audit, and snapshot storage

Source: src/Coxy/DurableControlStore.hs, lines 33–95 in the version 2.1 training tree.

data DurableStoreConfig = DurableStoreConfig
  { durableStoreRoot            :: FilePath
  , durableStoreEventDirectory  :: FilePath
  , durableStoreAuditDirectory  :: FilePath
  , durableStoreSnapshotFile    :: FilePath
  }
  deriving (Eq, Ord, Show, Read)

defaultDurableStoreConfig :: FilePath -> DurableStoreConfig
defaultDurableStoreConfig root =
  DurableStoreConfig
    { durableStoreRoot = root
    , durableStoreEventDirectory = root </> "events"
    , durableStoreAuditDirectory = root </> "audit"
    , durableStoreSnapshotFile = root </> "controller.snapshot"
    }

data DurableStoreError
  = DurableStoreMissing FilePath
  | DurableStoreInvalidEnvelope FilePath
  | DurableStoreChecksumMismatch FilePath
  | DurableStoreDecodeFailure FilePath
  | DurableStoreEventConflict Word64
  | DurableStoreEventGap Word64 Word64
  | DurableStoreEventChainBroken Word64
  | DurableStoreAuditConflict Word64
  | DurableStoreAuditGap Word64 Word64
  | DurableStoreAuditChainBroken Word64
  | DurableStoreSnapshotInvalid FilePath
  deriving (Eq, Show, Read)

data DurableStoreReport = DurableStoreReport
  { durableStoreReportEvents       :: Int
  , durableStoreReportAuditEntries :: Int
  , durableStoreReportLastEvent    :: Word64
  , durableStoreReportLastAudit    :: Word64
  , durableStoreReportSnapshot     :: Bool
  }
  deriving (Eq, Show, Read)

initialiseDurableStore :: DurableStoreConfig -> IO ()
initialiseDurableStore config = do
  createDirectoryIfMissing True (durableStoreRoot config)
  createDirectoryIfMissing True (durableStoreEventDirectory config)
  createDirectoryIfMissing True (durableStoreAuditDirectory config)

appendDurableControlEvent
  :: DurableStoreConfig
  -> ReplicatedControlEvent
  -> IO (Either DurableStoreError ())
appendDurableControlEvent config event = do
  initialiseDurableStore config
  existingResult <- loadDurableControlEvents config
  case existingResult of
    Left err -> pure (Left err)
    Right existing ->
      case findEvent (replicatedEventIndex event) existing of
        Just stored
          | stored == event -> pure (Right ())
          | otherwise -> pure (Left (DurableStoreEventConflict (replicatedEventIndex event)))
        Nothing -> case validateNextEvent existing event of
          Left err -> pure (Left err)
          Right () -> do

Certificate roles, metadata, revocation, and renewal

Source: src/Coxy/CertificateLifecycle.hs, lines 51–115 in the version 2.1 training tree.

data CertificatePurpose
  = ControllerCertificate
  | UpgradeAgentCertificate
  | OperatorCertificate
  deriving (Eq, Ord, Show, Read)

data CertificateStatus
  = CertificateActive
  | CertificateRevoked Word64 String
  | CertificateSuperseded CertificateSerial
  | CertificateExpired
  deriving (Eq, Ord, Show, Read)

data ManagedCertificate = ManagedCertificate
  { managedCertificateSerial       :: CertificateSerial
  , managedCertificateSubject      :: String
  , managedCertificateSubjectKey   :: VerificationKey
  , managedCertificateFingerprint  :: CertificateFingerprint
  , managedCertificateIssuerKey    :: VerificationKey
  , managedCertificatePurpose      :: CertificatePurpose
  , managedCertificateNotBefore    :: Word64
  , managedCertificateNotAfter     :: Word64
  , managedCertificateStatus       :: CertificateStatus
  , managedCertificateSignature    :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data CertificateRevocation = CertificateRevocation
  { certificateRevocationSerial       :: CertificateSerial
  , certificateRevocationFingerprint  :: CertificateFingerprint
  , certificateRevocationTimestamp    :: Word64
  , certificateRevocationReason       :: String
  , certificateRevocationAuthority    :: VerificationKey
  , certificateRevocationSignature    :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data CertificateRenewalRequest = CertificateRenewalRequest
  { renewalRequestId             :: String
  , renewalRequestOldSerial      :: CertificateSerial
  , renewalRequestNewSerial      :: CertificateSerial
  , renewalRequestSubject        :: String
  , renewalRequestNewSubjectKey  :: VerificationKey
  , renewalRequestNewFingerprint :: CertificateFingerprint
  , renewalRequestNotBefore      :: Word64
  , renewalRequestNotAfter       :: Word64
  , renewalRequestSigner         :: VerificationKey
  , renewalRequestSignature      :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data RenewalCandidate = RenewalCandidate
  { renewalCandidateSerial      :: CertificateSerial
  , renewalCandidateSubject     :: String
  , renewalCandidatePurpose     :: CertificatePurpose
  , renewalCandidateExpiresAt   :: Word64
  , renewalCandidateRemaining   :: Word64
  }
  deriving (Eq, Ord, Show, Read)

data CertificateRegistry = CertificateRegistry
  { certificateRegistryAuthority       :: VerificationKey
  , certificateRegistryCertificates    :: Map CertificateSerial ManagedCertificate
  , certificateRegistryActiveBySubject :: Map String CertificateSerial
  , certificateRegistryRevocations     :: [CertificateRevocation]

Approval roles, delegation, quorum policy, and authorization

Source: src/Coxy/RolloutApproval.hs, lines 39–120 in the version 2.1 training tree.

data ApprovalRole
  = ReleaseManagerRole
  | SecurityOfficerRole
  | OperationsLeadRole
  | RegionalOperatorRole String
  deriving (Eq, Ord, Show, Read)

data ApprovalPrincipal = ApprovalPrincipal
  { approvalPrincipalName :: String
  , approvalPrincipalRole :: ApprovalRole
  , approvalPrincipalKey  :: VerificationKey
  }
  deriving (Eq, Ord, Show, Read)

data ApprovalPolicy = ApprovalPolicy
  { approvalPolicyMinimumApprovals :: Int
  , approvalPolicyRequiredRoles    :: Map ApprovalRole Int
  , approvalPolicyAllowDelegation  :: Bool
  , approvalPolicyMaximumAge       :: Word64
  }
  deriving (Eq, Show, Read)

defaultApprovalPolicy :: ApprovalPolicy
defaultApprovalPolicy =
  ApprovalPolicy
    { approvalPolicyMinimumApprovals = 2
    , approvalPolicyRequiredRoles = Map.fromList
        [ (ReleaseManagerRole, 1)
        , (SecurityOfficerRole, 1)
        ]
    , approvalPolicyAllowDelegation = True
    , approvalPolicyMaximumAge = 3600
    }

data DelegationGrant = DelegationGrant
  { delegationGrantId          :: String
  , delegationGrantDelegator   :: String
  , delegationGrantDelegate    :: String
  , delegationGrantPlanPrefix  :: Maybe String
  , delegationGrantNotBefore   :: Word64
  , delegationGrantNotAfter    :: Word64
  , delegationGrantSigner      :: VerificationKey
  , delegationGrantSignature   :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data RolloutApproval = RolloutApproval
  { rolloutApprovalPlanId             :: String
  , rolloutApprovalQualificationHash  :: String
  , rolloutApprovalTargetVersion      :: String
  , rolloutApprovalIssuedAt           :: Word64
  , rolloutApprovalExpiresAt          :: Word64
  , rolloutApprovalApprover           :: String
  , rolloutApprovalEffectivePrincipal :: String
  , rolloutApprovalDelegationId       :: Maybe String
  , rolloutApprovalSigner             :: VerificationKey
  , rolloutApprovalSignature          :: SignatureBytes
  }
  deriving (Eq, Show, Read)

data RolloutAuthorization = RolloutAuthorization
  { rolloutAuthorizationPlanId       :: String
  , rolloutAuthorizationApprovedAt   :: Word64
  , rolloutAuthorizationApprovalHash :: String
  , rolloutAuthorizationApprovers    :: [String]
  , rolloutAuthorizationRoles        :: [ApprovalRole]
  }
  deriving (Eq, Show, Read)

data ApprovalState = ApprovalState
  { approvalStatePrincipals   :: Map String ApprovalPrincipal
  , approvalStateDelegations  :: Map String DelegationGrant
  , approvalStateApprovals    :: Map String [RolloutApproval]
  , approvalStateConsumed     :: Set String
  }
  deriving (Eq, Show, Read)

data ApprovalError
  = InvalidApprovalPolicy String
  | EmptyApprovalPrincipal
  | DuplicateApprovalPrincipal String
  | ApprovalKeyAlreadyRegistered VerificationKey

Regional replicas, recovery objectives, and recovery reports

Source: src/Coxy/CrossRegionRecovery.hs, lines 24–85 in the version 2.1 training tree.

data RegionControlReplica = RegionControlReplica
  { regionReplicaRegion          :: RegionId
  , regionReplicaPriority        :: Int
  , regionReplicaAvailable       :: Bool
  , regionReplicaVerified        :: Bool
  , regionReplicaCommittedIndex  :: Word64
  , regionReplicaLastEventHash   :: String
  , regionReplicaSnapshotHash    :: String
  , regionReplicaRecoveryTicks   :: Word64
  , regionReplicaClusterSnapshot :: ControllerCluster
  }
  deriving (Eq, Show, Read)

data CrossRegionConfig = CrossRegionConfig
  { crossRegionPrimaryRegion       :: RegionId
  , crossRegionMinimumReplicas     :: Int
  , crossRegionMaximumDataLoss     :: Word64
  , crossRegionMaximumRecoveryTime :: Word64
  }
  deriving (Eq, Show, Read)

defaultCrossRegionConfig :: RegionId -> CrossRegionConfig
defaultCrossRegionConfig primary =
  CrossRegionConfig
    { crossRegionPrimaryRegion = primary
    , crossRegionMinimumReplicas = 2
    , crossRegionMaximumDataLoss = 1
    , crossRegionMaximumRecoveryTime = 30
    }

data CrossRegionRecoveryPlan = CrossRegionRecoveryPlan
  { recoveryPlanFailedRegion       :: RegionId
  , recoveryPlanSelectedRegion     :: RegionId
  , recoveryPlanSourceIndex        :: Word64
  , recoveryPlanPrimaryIndex       :: Word64
  , recoveryPlanExpectedDataLoss   :: Word64
  , recoveryPlanExpectedTicks      :: Word64
  , recoveryPlanSnapshotHash       :: String
  }
  deriving (Eq, Show, Read)

data CrossRegionRecoveryReport = CrossRegionRecoveryReport
  { recoveryReportPlan              :: CrossRegionRecoveryPlan
  , recoveryReportPromotedLeader    :: ControllerId
  , recoveryReportRecoveredIndex    :: Word64
  , recoveryReportDataLoss          :: Word64
  , recoveryReportRecoveryTicks     :: Word64
  , recoveryReportConvergedRegions  :: [RegionId]
  , recoveryReportPassed            :: Bool
  , recoveryReportChecks            :: [(String, Bool)]
  }
  deriving (Eq, Show, Read)

data CrossRegionError
  = InvalidCrossRegionConfig String
  | InsufficientRegionReplicas Int Int
  | DuplicateRegionReplica RegionId
  | RegionReplicaUnavailable RegionId
  | RegionReplicaUnverified RegionId
  | RegionSnapshotInvalid RegionId
  | RegionSnapshotHashMismatch RegionId
  | RegionEventHashMismatch RegionId RegionId Word64

Student practical laboratory

  1. Elect a leader in a three-controller cluster, commit a state update, and confirm quorum acknowledgement.
  2. Make the leader unavailable, elect a replacement, restore the old controller, and perform follower catch-up.
  3. Tamper with a durable event, audit record, or controller snapshot and verify failure.
  4. Revoke a certificate, request renewal with a rotated key, collect delegated rollout approvals, and run a cross-region recovery drill.

Use the following workflow after each change:

# Work in a branch dedicated to Build Twenty-One
git switch -c training/build-21

# Build every component so module-boundary errors are visible
cabal build all

# Run the complete test suite, then focus on the relevant module tests
cabal test all --test-show-details=direct

# Run the training demonstration or service when the build requires integration
cabal run coxy-training-chain-demo

Bridge to the real Cardano stack

High availability for operator control systems is separate from blockchain consensus, but it protects upgrades, certificates, artifacts, and fleet state. The training model is intentionally simpler than Raft, Paxos, etcd, cloud KMS, or enterprise PKI, yet it teaches the invariants students must recognize.

COXY module Cardano area What to study next
Control-plane HA reliable operations infrastructure quorum consensus, durable logs, certificate lifecycle, regional recovery

Security and professional practice

A deterministic teaching election is not a production consensus protocol. Real HA control planes require battle-tested consensus, secure clocks, protected quorum membership, monitored replication lag, independent backups, and regular regional failover exercises.

Completion checkpoint

A student completes Build Twenty-One only after the project compiles, the complete test suite passes, the laboratory failure cases are demonstrated, and the student can explain how ControlPlaneHA.hs depends on the earlier modules without reading a prepared answer. Record the exact commands, test result, Git commit, and one unresolved question in the learning log.

22. 🧭 From COXY Training Code to the Real Cardano Ecosystem

The training chain is valuable because it makes each subsystem small enough to inspect. The next professional step is not to rename the modules and call the result Cardano. The next step is to use the concepts as a map while reading, building, testing, and contributing to real Cardano repositories.

A staged transition plan

Stage A — Operate a local Cardano development network

Build cardano-node using the repository-supported toolchain, generate a disposable local network, create payment and stake keys, submit transactions, inspect UTxOs, register a stake pool, and practise rollback-safe restarts. Keep this network isolated and use only test keys.

Stage B — Use cardano-api and cardano-cli

Compare the training Transaction, Ledger, ProtocolParams, and Genesis types with the real API types. Learn era witnesses, network IDs, transaction bodies, witnesses, values, certificates, validity intervals, execution units, collateral, reference inputs, and governance procedures.

Stage C — Study Plutus and off-chain balancing

Rebuild one training script as a real Plutus validator. Write emulator or ledger tests, compile the validator, calculate its address, create datum and redeemer values, construct a balanced testnet transaction, and inspect script execution and failure messages.

Stage D — Use production-grade indexing interfaces

Run or query a rollback-aware indexer such as cardano-db-sync or Kupo. Compare their chain synchronization and rollback behaviour with Indexer.hs. Use Ogmios or another maintained node gateway when a WebSocket or JSON application interface is needed.

Stage E — Contribute through reviewed issues and pull requests

Start with documentation, tests, reproducible bug reports, small refactors, or an issue labelled for new contributors. Read the repository contribution guide, format and lint exactly as required, add tests for every changed behaviour, explain compatibility implications, and respond professionally to review.

Concept mapping

Training concept Real Cardano direction Evidence a student should produce
UTxO ledger and signatures cardano-ledger, cardano-api A testnet transaction built, signed, submitted, and independently queried
Slots and stake selection ouroboros-consensus A written comparison of the training lottery with VRF/KES-based leader checks
ChainStore and rollback ChainDB and node synchronization A local-network rollback or restart exercise with before/after tip evidence
P2P and transport ouroboros-network A message-flow diagram for chain-sync, block-fetch, and tx-submission
Scripts and budgets Plutus and ledger script evaluation A validator test covering success, failure, and execution-unit limits
Wallet selection cardano-wallet or cardano-api balancing A multi-asset transaction with change and fee explanation
Indexer cardano-db-sync, Kupo A rollback-aware address or contract query
Governance Conway ledger and governance CIPs A testnet governance workflow or precise transaction plan
Release operations cardano-node release and operator practice A reproducible build, test report, signed checksum, and rollback plan

23. 🧰 Complete Build, Test, and Troubleshooting Workflow

Standard verification sequence

git status --short
cabal clean
cabal build all
cabal test all --test-show-details=direct
cabal run coxy-training-chain-demo
cabal run coxy-training-chain-benchmark -- --quick
cabal run coxy-training-chain-qualify -- --quick --line-coverage 100 --branch-coverage 100

Not every executable is needed for every lesson, but every release stage must build together because shared types and storage formats cross module boundaries. When a test fails, do not immediately weaken the assertion. Reproduce the smallest failing case, identify the violated invariant, inspect the exact typed error, and add a regression test before applying the fix.

Common troubleshooting sequence

  1. Confirm the exact GHC and Cabal versions.
  2. Run cabal clean after changing package constraints or generated modules.
  3. Read the first compiler error rather than the final cascade of dependent errors.
  4. Check constructor changes across all pattern matches and codec versions.
  5. Check that storage-format changes retain explicit version handling and migration tests.
  6. Check that a signature or hash uses the same canonical bytes at construction and verification.
  7. Check current slot, chain tip, immutable point, and network identifier when consensus tests fail.
  8. Check queue limits, timeouts, and peer authentication when integration tests stall.
  9. Preserve QuickCheck seeds, model traces, and fault profiles for deterministic replay.
  10. Never delete a database to hide a recovery bug; reproduce it on a copied training directory.

Student evidence folder

Each student should maintain a folder containing a setup report, compiler versions, test logs, screenshots or terminal captures of successful and rejected cases, replay artifacts, benchmark reports, a glossary written in the student’s own words, and links to Git commits. This evidence makes assessment reproducible and prepares the student for professional code review.

24. 🤝 Working Professionally with Cardano Open-Source Communities

Professional participation begins with respect for repository scope and maintainer time. Search existing issues and discussions before opening a new report. Provide exact versions, operating system, commands, expected behaviour, actual behaviour, minimal reproduction steps, and relevant logs with secrets removed.

For a pull request, keep the change focused, explain the invariant or defect, include tests, update documentation, and avoid unrelated formatting. Use meaningful commits and sign them when the project requests it. Do not force-push during active review without explaining why, and do not treat review questions as personal criticism.

Security findings require responsible disclosure. Do not publish exploitable details, production keys, or live-node weaknesses in a public issue before following the project security policy. Training exercises must use pre-production, preview, local networks, or isolated virtual hosts rather than real funds.

Students should participate in public technical discussions only after reading the relevant specification, CIP, issue history, and code. A useful contribution can be a corrected tutorial, a failing test, a benchmark, a compatibility report, or a clear explanation. Open source rewards evidence and cooperation more than confident speculation.

25. 📚 Glossary of Terms

Address

A network-specific identifier that receives value or identifies a script-controlled output. In this training project, addresses are derived from verification keys or explicitly assigned to scripts.

Address book

Persistent peer-manager state containing known endpoints, retry information, bans, and discovery metadata.

Address discovery

The wallet process of deriving addresses in sequence and querying history until a configured unused-address gap is reached.

Artifact

A versioned release file, container image archive, configuration package, or other deployment input whose size and SHA-256 digest are verified.

Atomic write

A storage technique that writes a temporary file and replaces the destination only after the new content is complete.

Audit trail

An ordered record of important actions. In later builds, each entry commits to the previous entry hash so tampering is detectable.

Authentication

Proof that a peer, operator, controller, oracle, or transaction signer controls an expected credential.

Authorization

The policy decision that determines which authenticated identity may perform a requested operation.

Backoff

A delay that increases after repeated connection or deployment failures to avoid tight retry loops.

Ban score

A cumulative penalty assigned to a peer for malformed frames, authentication failures, protocol violations, or other abusive behaviour.

Benchmark

A reproducible timed workload used to detect performance changes while keeping operation counts and checksums stable.

Block

A signed collection of transactions with a header that links to a parent block and commits to its body.

Block body hash

The cryptographic commitment to the ordered transactions contained in a block.

Block producer

A registered participant selected to create a block for a slot.

Bootstrap files

Reproducible network, genesis, protocol-parameter, and manifest files used to create or verify a network.

Byzantine behaviour

Arbitrary or malicious participant behaviour, including equivocation, malformed messages, withholding, flooding, and downgrade attacks.

Canonical encoding

A binary encoding in which each valid value has exactly one accepted byte representation.

Capability negotiation

The process by which peers select mutually supported versions and limits for independently versioned mini-protocols.

Certificate revocation

A signed declaration that a certificate or fingerprint must no longer be accepted.

Chain selection

The deterministic rule that chooses one valid stored branch as the current selected chain.

Chain tip

The hash and height of the final block on the currently selected branch.

ChainStore

The fork-aware persistent component that stores blocks, parent states, the selected tip, and the immutable point.

Change output

A wallet output that returns selected input value not used by payments or fees.

Checkpoint

A durable record of completed deployment or agent steps, allowing safe resume after restart.

Ciphertext

Encrypted bytes produced by an authenticated-encryption algorithm.

Coin

The base monetary unit in the training ledger.

Collateral

Additional transaction value used by real Cardano script transactions to cover failed script execution. The training project discusses but does not reproduce every Cardano collateral rule.

Committee

A governance body whose registered members cast votes under committee-specific thresholds and terms.

Common ancestor

The last block shared by two competing chain branches.

Consensus

The rules that determine valid block producers and the preferred valid chain.

Consensus-critical

Data or logic that every honest node must interpret identically to remain on the same chain.

Controller

A service that authorizes, records, and coordinates remote agent commands and cluster rollouts.

Controller quorum

The minimum number of replicated controllers that must acknowledge a state update before it is committed.

Cryptographic hash

A fixed-length digest that changes unpredictably when its input changes and is used for commitments and identifiers.

Datum

Typed data committed to a script-controlled output and supplied to the validator when that output is spent.

Delegation

Assignment of stake influence to a stake pool or governance voting power to a DRep without transferring ownership.

Deployment adapter

A restricted interface that activates, verifies, restarts, and rolls back a systemd or container release.

Determinism

The property that the same valid inputs always produce the same output, independent of machine or timing.

Digital signature

A cryptographic proof created with a private signing key and verified with the corresponding public key.

DRep

A delegated representative who receives governance voting power from stake credentials.

Epoch

A group of slots used for stake snapshots, rewards, parameter activation, and governance timing.

Event stream

A sequenced real-time feed of blocks, rollbacks, transactions, peers, scripts, or oracle activity.

Execution budget

The maximum deterministic CPU and memory units a script may consume.

Explorer

A user interface or API that queries indexed blockchain data by block, transaction, address, token, producer, or script.

Failover

Transfer of service responsibility from a failed primary node, controller, or region to a verified replacement.

Fee

Value paid by a transaction and accounted for under protocol rules.

Follower

A nonleader controller replica that serves reads, acknowledges proposals, and catches up committed state.

Fork

Two or more valid branches that descend from a common ancestor.

Fork choice

Another name for the chain-selection rule.

Formal specification

A precise description of protocol behaviour. In this project the specifications are executable, but they are not machine-checked mathematical proofs.

Frame

A bounded transport record containing magic, version, type, length, checksum, and payload.

Freshness window

The maximum permitted age of an oracle report, command, certificate, or other time-sensitive object.

Genesis

The canonical configuration and initial state from which a blockchain network begins.

Governance action

A proposal that moves through draft, voting, ratification, enactment, rejection, or expiry.

Graceful shutdown

A controlled stop that rejects new work, completes necessary persistence, closes services, and reaches a known lifecycle state.

Handshake

The first authenticated exchange through which peers prove identities and agree on network and protocol information.

Hardware signer

An external device or process that signs approved transaction bytes without exposing its private key to the online builder.

Health check

A test of whether a service is live, ready, synchronized, or otherwise safe to use.

Height

The number assigned to a block relative to genesis.

Immutable point

A selected-chain point before which the training node refuses rollback or alternative fork selection.

Indexer

A service that converts selected-chain blocks into query-optimized, rollback-aware records.

Input reservation

A mempool record preventing two pending transactions from claiming the same UTxO.

JSON-RPC

A request and response protocol that identifies methods, parameters, results, errors, and request IDs in JSON.

Key derivation

Deterministic creation of role-specific keys from a root seed and derivation path.

Key rotation

Replacement of a key or certificate while preserving an auditable transition from the previous credential.

Leader

The controller replica elected to accept control-plane writes, or the block producer selected for a slot, depending on context.

Ledger

The validated state that records unspent outputs, applied transactions, oracle state, and other consensus-controlled information.

Liveness

Evidence that a process or system is running and responding.

Manifest

A file that lists artifacts and commits to their sizes and hashes, sometimes with a release signature.

Mempool

A node-local collection of validated transactions waiting for block inclusion.

Mini-protocol

An independently versioned network protocol dedicated to one activity such as chain sync or block fetch.

Model checking

Systematic bounded exploration of reachable states and transitions to find invariant violations.

Monetary invariant

The rule that every coin remains accounted for across circulation, treasury, reserves, fees, rewards, and deposits.

mTLS

Mutual Transport Layer Security, in which both sides present and verify certificates.

Multisignature

An authorization rule that requires a threshold number of valid signatures from a defined key set.

Node

The supervised runtime that combines networking, consensus, chain storage, ledger validation, mempool, and services.

Node identity

A stable identity derived from a verification key and used during authenticated peer or control-plane communication.

Noise

A family of secure-channel protocol patterns. The training channel is described as Noise-style but is not a production Noise implementation.

Nonce

A number used once or monotonically increased to prevent replay and distinguish otherwise similar messages.

Operational certificate

A Cardano mechanism authorizing a hot block-production key under a colder credential. It is discussed in the transition material, not reproduced completely here.

Oracle

A signed application protocol that supplies external observations to on-chain scripts.

Output

A new UTxO containing an address, base coin, tokens, and optional script information.

P2P

Peer-to-peer networking in which nodes exchange blockchain information directly.

Pagination

Dividing a large query result into bounded, stable pages.

Partition

A network failure or policy that prevents groups of nodes from communicating.

Peer discovery

Learning candidate peer endpoints from static configuration or authenticated peers.

Peer manager

The subsystem that supervises connections, address persistence, backoff, limits, discovery, and bans.

Persistence

Writing state to durable storage so it can be recovered after process or host restart.

Plutus

Cardano’s smart-contract platform and associated languages, compiler, core language, and ledger integration.

Private key

Secret signing material that must never be logged or shared.

Producer registry

The mapping of authorized block-producer verification keys to names and stake.

Property testing

Generation of many inputs or command sequences to check general invariants.

Protocol parameter

A consensus-controlled value such as fee coefficients, size limits, deposits, or protocol version.

Public key

A shareable key used to verify signatures.

Query database

The persistent, replaceable projection used by explorer and wallet clients.

Quorum

The minimum number or weight of approvals required for a decision or committed state.

Readiness

Evidence that a service can safely accept work, including synchronization and dependency checks.

Recovery point objective (RPO)

The maximum acceptable amount of data or progress lost during recovery.

Recovery time objective (RTO)

The maximum acceptable time required to restore service.

Redeemer

Typed input supplied by a transaction to select or parameterize a script action.

Refinement check

A comparison showing that an implementation agrees with an independent reference model for a defined behaviour.

Relay node

A node that participates in networking and validation but does not hold a block-production signing key.

Replay attack

Reuse of a previously valid message, command, handshake, or signature in an unauthorized context.

Reserves

Supply held outside circulation and released under epoch economic rules.

Rollback

Moving the selected chain backward to a common ancestor before following a different branch.

Rollout

An ordered multi-node release deployment governed by availability, health, approval, and rollback policy.

Script

Deterministic validation logic that controls whether a script UTxO may be spent.

Script context

Transaction and ledger information made available to a validator.

Seed

Root secret or deterministic simulation value. Cryptographic wallet seeds and public simulation seeds have very different security requirements.

Selected chain

The valid stored branch currently preferred by the chain-selection rule.

Slot

A discrete unit of consensus time in which a block producer may be selected.

Snapshot

A verified copy of chain, index, controller, or other state used for inspection or recovery.

Stake

Value used to weight block-production or governance influence without transferring ownership.

Stake pool

A registered Cardano block-production operator to which holders may delegate stake.

State machine

A system described by explicit states, commands, preconditions, transitions, and invariants.

Structured logging

Logging with stable machine-readable fields rather than only free-form text.

Supervisor

A runtime component that starts workers, observes failures, coordinates shutdown, and preserves lifecycle state.

TLS

Transport Layer Security, the standard protocol used to encrypt and authenticate network connections.

Token bundle

A mapping from native asset identifiers to quantities carried by a UTxO.

Transaction

A signed ledger operation that consumes inputs, creates outputs, pays a fee, and may include scripts, certificates, or oracle actions.

Transaction ID

A deterministic hash of the canonical transaction body.

Treasury

Network-controlled funds accumulated and withdrawn under economic and governance rules.

UTxO

An unspent transaction output that can be referenced exactly once as a transaction input.

Validation

Deterministic checking that data and state transitions satisfy all applicable rules.

Version negotiation

Selection of compatible protocol versions and limits before application messages are exchanged.

VRF

A verifiable random function used by Ouroboros-style protocols to produce private leader checks with public proofs.

Wallet

Software that derives keys and addresses, discovers funds, constructs transactions, signs, submits, and displays history.

WebSocket

A persistent two-way connection used for real-time subscriptions.

Witness

Cryptographic or script information supplied to authorize a transaction input.

Zero trust

An operational principle that authenticates, authorizes, validates, and limits every interaction rather than trusting network location alone.

26. 🔗 Official Cardano Learning and Contribution Resources

The following official repositories and documentation should be used for the transition exercises. URLs are written in full so the tutorial remains usable outside a chat interface.

Final graduation checkpoint

A student is ready to move from the COXY training chain into real Cardano contribution work when the student can build and test the complete project, explain every build boundary, reproduce at least one failure using a saved seed or trace, operate a disposable Cardano development network, submit and query a real test transaction, complete one Plutus or indexing exercise, and produce a small evidence-based contribution that follows an upstream repository’s contribution rules.

Coxygen Global — Coxy Haskell Cardano Blockchain
Developer: Bernard Sibanda — 24 July 2026

Clone this wiki locally