Repository navigation
Coxy Haskell Cardano Blockchain
- 🔐 Build One — Core Types, Cryptography, Transactions, and the UTxO Ledger (v0.1)
- 🧱 Build Two — Blocks, the Mempool, and Block Production (v0.2)
- ⚖️ Build Three — Slots, Stake, Leader Selection, and Basic Consensus (v0.3)
- 🗄️ Build Four — Persistent Chain Store, Forks, Rollback, and Restart Recovery (v0.4)
- 🌐 Build Five — Peer-to-Peer Synchronisation with Bounded Queues (v0.5)
- 🖥️ Build Six — The Supervised Node Runtime (v0.6)
- 🔌 Build Seven — JSON-RPC and Real-Time WebSocket Services (v0.7)
- 🔎 Build Eight — Rollback-Aware Indexing and the Explorer API (v0.8)
- 📜 Build Nine — Typed Scripts, Execution Budgets, and Signed Oracles (v0.9)
- 👛 Build Ten — Deterministic Wallets, Transaction Building, and the Wallet API (v1.0)
- 🏛️ Build Eleven — Stake, Governance, and Encrypted Wallet Storage (v1.1)
- 💰 Build Twelve — Economics, Genesis, Protocol Parameters, and Era Transitions (v1.2)
- 📊 Build Thirteen — Operator CLI, Structured Logging, Metrics, and Safe Maintenance (v1.3)
- 🔒 Build Fourteen — Real TCP Transport, Secure Channels, and Peer Management (v1.4)
- 🧬 Build Fifteen — Canonical Protocol Codec, Capability Negotiation, and Network Simulation (v1.5)
- 🧪 Build Sixteen — Property Testing, Model Checking, Fuzzing, and Benchmarks (v1.6)
- ✅ Build Seventeen — Executable Formal Specifications and Release Qualification (v1.7)
- 🚀 Build Eighteen — Coordinated Upgrades, Cluster Deployment, and Disaster Recovery (v1.8)
- 🤖 Build Nineteen — Remote Upgrade Agents, Control API, and Protected Secrets (v1.9)
- 📡 Build Twenty — Mutual-TLS Control Transport, Artifact Transfer, and Deployment Adapters (v2.0)
- 🛡️ Build Twenty-One — High-Availability Control Plane and Cross-Region Recovery (v2.1)
- 🧭 From COXY Training Code to the Real Cardano Ecosystem
- 🧰 Complete Build, Test, and Troubleshooting Workflow
- 🤝 Working Professionally with Cardano Open-Source Communities
- 📚 Glossary of Terms
- 🔗 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.
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.
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.
- Students should know basic Haskell syntax, algebraic data types, pattern matching,
Either,Maybe, lists, maps, records, type classes, andIO. - 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 testrun before moving to the next build. A working demonstration without passing tests is incomplete.
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 | 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.
- Keep pure validation separate from effects. Pure functions are easier to test, replay, model-check, and compare with reference specifications.
- Treat encoding as protocol logic. A hash or signature is meaningful only when all participants serialize the same semantic value identically.
- Validate at every trust boundary. Peers, API clients, operators, artifacts, storage files, and recovered snapshots are all untrusted until checked.
- Bound every external resource. Limit frame sizes, request bodies, page sizes, queues, script budgets, recursion, collection sizes, retries, and concurrent connections.
- Design for rollback and restart. A blockchain and its operational control plane must recover from forks, process failures, machine failures, and partial deployments.
- Preserve evidence. Logs, metrics, tests, replay seeds, audit chains, qualification reports, manifests, and runbooks turn a demonstration into an engineering process.
Release stage: v0.1
Primary modules: Types.hs, Crypto.hs, Transaction.hs, Ledger.hs
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.
Signing key → verification key → address
↓
Unsigned transaction → deterministic body bytes → signature
↓
Ledger validation → consume inputs → create outputs → new ledger state
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.
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 -> AddressSource: 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 ()- Read the type definitions before reading the validation function, because every later invariant is expressed through these types.
- Run the demonstration and record Alice’s balance before and after the signed payment.
- Change one output amount after signing and confirm that signature verification rejects the transaction.
- 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-demoCardano 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 |
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.
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.
Release stage: v0.2
Primary modules: Block.hs, Mempool.hs, BlockProducer.hs
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.
Wallet submission → mempool validation → conflict reservation
↓
Fee-priority selection → candidate block body
↓
Body hash + parent hash + slot → producer signature → validated block
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.
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
-> MempoolSource: 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)- Submit two independent transactions with different fees and inspect the mempool ordering.
- Create two transactions that spend the same input and confirm that only the first can reserve it.
- Produce a block with a one-transaction capacity and verify that the highest-fee transaction is selected.
- 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-demoCardano 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 |
A mempool is attacker-controlled input. Enforce size limits, conflict detection, validation costs, and eviction policies before using this design beyond a classroom environment.
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.
Release stage: v0.3
Primary modules: Consensus.hs
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.
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
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.
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
| NoChainCandidatesSource: 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- Register three producers with a 60/30/10 stake split and generate leaders for one hundred slots.
- Count the selected slots and explain why the observed distribution is approximate rather than exact.
- Try to produce a block with a nonleader key and confirm rejection.
- 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-demoOuroboros 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 |
Public deterministic randomness is predictable. It is suitable for teaching only and must never be presented as secure proof-of-stake consensus.
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.
Release stage: v0.4
Primary modules: ChainStore.hs
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.
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
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.
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.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- Build two branches from genesis and confirm that both remain stored.
- Extend the shorter branch until it becomes preferred and inspect the reported rollback path.
- Advance the immutable point, then try to select a fork that diverges before it.
- 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-demoCardano 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 |
Durability requires atomic writes, checksums, format versioning, corruption detection, and tested recovery. A successful write call alone is not proof of recoverability.
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.
Release stage: v0.5
Primary modules: P2P.hs
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.
Signed handshake → authenticated peer
↓
Tip announcement → locator-based header request
↓
Linked header batch → missing block request
↓
Validated block batch → ChainStore selection and possible rollback
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.
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)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- Connect two simulated peers with different branches and run synchronisation until their selected tips match.
- Reduce the header and block batch limits to one and observe multi-round catch-up.
- Fill an inbound queue to capacity and confirm that the next message is rejected.
- 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-demoCardano 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 |
Authentication does not make peer data valid. Every received block and transaction must still pass consensus and ledger validation.
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.
Release stage: v0.6
Primary modules: Node.hs
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.
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
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.
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 ofSource: 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- Run one producer node and one relay node and compare their lifecycle and production behaviour.
- Advance slots until the producer is selected and verify that a pending transaction is placed in a block.
- Configure a short keep-alive interval and simulate a silent peer until timeout.
- 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-demoThe 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 |
Supervision must distinguish recoverable worker failures from state-corrupting failures. A production runtime also needs resource isolation, signal handling, backpressure, and careful exception boundaries.
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.
Release stage: v0.7
Primary modules: RPC.hs, WebSocket.hs
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.
HTTP JSON-RPC request → decode and validate → read or update NodeHandle
WebSocket subscription → topic filter → sequenced event hub → client update
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.
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)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- Call node.status and chain.tip from curl and compare the returned height and hash.
- Submit a valid signed transaction through tx.submit and watch it appear in the mempool.
- Subscribe to block and transaction topics from a browser client.
- 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-demoCardano 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 |
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.
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.
Release stage: v0.8
Primary modules: Indexer.hs, ExplorerAPI.hs
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.
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
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.
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 = doSource: 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- Index a chain containing payments to two addresses and query each address summary and history.
- Create a fork, switch the selected tip, and confirm that abandoned transactions disappear from the query database.
- Query blocks and transactions with page sizes of one, two, and an excessive value.
- 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-demoCardano 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 |
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.
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.
Release stage: v0.9
Primary modules: Script.hs, Oracle.hs
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.
Script output + inline datum
↓ spend attempt with redeemer and witness
Deterministic evaluator → result + trace + budget used
↓
Optional signed oracle reports → freshness/quorum/predicate checks
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.
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)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 :: IntegerSource: 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- Lock an output with a hash-lock program and spend it with the correct and incorrect secret.
- Use a deadline script before and after its permitted slot.
- Construct a two-of-three multisignature witness and remove one signature.
- 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-demoPlutus 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 |
Determinism is mandatory for consensus scripts. Never read wall-clock time, network data, random input, or mutable external state during validation.
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.
Release stage: v1.0
Primary modules: Wallet.hs, WalletAPI.hs
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.
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
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.
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)
++ "/"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 :: TxOutRefSource: 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- Derive external and internal addresses from a fixed seed and confirm reproducibility.
- Fund several UTxOs and inspect deterministic coin and token selection.
- Build a payment plan on a watch-only path, transfer it to an offline signer, and verify the returned signature.
- 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-demoReal 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 |
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.
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.
Release stage: v1.1
Primary modules: Stake.hs, Governance.hs, WalletStore.hs
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.
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
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.
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 = PoolRewardPotSource: 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 :: GovernanceActionIdSource: 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- Register a stake credential, delegate it to a pool, distribute an epoch reward, and withdraw the reward.
- Create a governance action, register voters, cast DRep and committee votes, and move the action through its lifecycle.
- Create an encrypted wallet store, open it with the correct and wrong passwords, and tamper with its ciphertext.
- 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-demoCardano 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 |
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.
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.
Release stage: v1.2
Primary modules: Economics.hs, Genesis.hs, ProtocolParams.hs
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.
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
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.
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 EpochNoSource: 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
maximumSupplySource: 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- Generate bootstrap files twice from the same configuration and compare every byte and SHA-256 digest.
- Attempt a genesis allocation that exceeds maximum supply and confirm rejection.
- Collect fees and deposits, close an epoch, and verify the monetary invariant.
- 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-demoCardano 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 |
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.
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.
Release stage: v1.3
Primary modules: CLI.hs, Logging.hs, Metrics.hs
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.
operator.json + environment secrets → validated configuration
Node/indexer state → metrics snapshot → /metrics and health endpoints
Stopped database → verified snapshot/inspection/maintenance commands
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.
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 = 4Source: 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))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)- Create an operator configuration, introduce an invalid port collision, and run configuration validation.
- Start the service, read JSON logs, scrape /metrics, and compare liveness with readiness.
- Export and verify a snapshot, alter one file, and confirm checksum failure.
- 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-demoStake-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 |
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.
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.
Release stage: v1.4
Primary modules: Transport.hs, TLS.hs, PeerManager.hs
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.
TCP connect/accept → framed authenticated handshake
↓
Directional encrypted records → canonical P2P messages
↓
Peer manager → discovery, limits, backoff, bans, address-book persistence
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.
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 StringSource: 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)
createClientHelloSource: 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
}
- Run two nodes over loopback TCP and exchange a keep-alive and tip announcement.
- Send a truncated, oversized, or checksum-corrupted frame and inspect the transport error.
- Fail repeated connection attempts and observe deterministic backoff and ban-score changes.
- 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-demoProduction 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 |
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.
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.
Release stage: v1.5
Primary modules: ProtocolCodec.hs, VersionNegotiation.hs, NetworkSimulation.hs
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.
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
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.
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)
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 NegotiatedCapabilitiesSource: 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 = []- Round-trip every major P2P message through the canonical codec and verify byte-for-byte re-encoding.
- Append trailing bytes or create nonminimal integers and confirm decoder rejection.
- Negotiate peers with partially overlapping versions and inspect selected versions and message limits.
- 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-demoCardano 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 |
A decoder is an attack surface. Validate lengths before allocation, reject ambiguous encodings, cap recursion and collection sizes, and fuzz every externally reachable parser.
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.
Release stage: v1.6
Primary modules: PropertyTesting.hs, ModelChecking.hs, Benchmark.hs
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.
Seed → generated commands → implementation state machine → invariant checks
Bounded actions → breadth-first model exploration → violation trace
Deterministic workload → timed samples → checksum + baseline comparison
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.
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)
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 = newTipSource: 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- Run the same replay plan twice and verify identical command sequences and final fingerprints.
- Introduce a deliberate bad fork-choice rule in a branch and observe the shortest failing trace.
- Run a codec fuzz campaign and inspect rejected, canonical, and incorrectly accepted mutations.
- 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-demoLarge 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 |
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.
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.
Release stage: v1.7
Primary modules: FormalSpec.hs, AdversarialTesting.hs, ReleaseQualification.hs
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.
Executable requirements + reference models → refinement report
Byzantine/soak/mutation/model/benchmark evidence → qualification gates
Passing report + artifact hashes → Ed25519-signed release manifest
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.
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
, ExecutableSpecificationSource: 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)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- Run the quick qualification pipeline and inspect every gate rather than reading only the final pass/fail.
- Change one reference rule or implementation rule and verify that refinement detects the mismatch.
- Run mutation testing and identify which tests kill each deliberately wrong rule.
- 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-demoProduction 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 |
Coverage percentages do not prove correctness. Require meaningful assertions, adversarial cases, reproducible evidence, independent review, and protected release-signing keys.
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.
Release stage: v1.8
Primary modules: UpgradeCoordinator.hs, ClusterDeployment.hs, DisasterRecovery.hs
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.
Signed release + cluster inventory → ordered rolling plan
Drain → install → verify → healthy or rollback
Snapshot → independent replicas → verification → failover drill → recovery report
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.
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 :: StringSource: 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 =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
}- Generate a five-node package and verify the deployment manifest.
- Create a rollout that upgrades query and relay nodes before producers and confirm the maximum-unavailable rule.
- Inject a failed readiness check and practice pause, rollback, repair, and resume.
- 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-demoStake-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 |
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.
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.
Release stage: v1.9
Primary modules: UpgradeAgent.hs, ClusterControlAPI.hs, SecretsBackend.hs
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.
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
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.
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 StringSource: 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 = RemoteAgentEndpointSource: 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- Create and sign a command, then test wrong node, expired time, repeated nonce, duplicate ID, and untrusted signer cases.
- Persist an execution after one step, restart the agent state, and resume from the unfinished step.
- Use viewer and operator tokens against the control API and confirm role boundaries.
- 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-demoRemote 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 |
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.
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.
Release stage: v2.0
Primary modules: ControlTransport.hs, AgentControlAPI.hs, ArtifactTransfer.hs, DeploymentAdapter.hs, VirtualHostIntegration.hs
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.
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
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.
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 AuthenticatedControlPeerSource: 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)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)- Generate the disposable training PKI and verify each certificate chain.
- Deliver a signed command through the mutual-TLS boundary and consume its progress stream.
- Interrupt a multi-chunk upload, inspect missing chunks, resume, and finalize it.
- 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-demoReal 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 |
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.
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.
Release stage: v2.1
Primary modules: ControlPlaneHA.hs, DurableControlStore.hs, HAControlRuntime.hs, HAClusterControlAPI.hs, CertificateLifecycle.hs, RolloutApproval.hs, CrossRegionRecovery.hs
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.
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
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.
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 Word64Source: 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 () -> doSource: 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]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 VerificationKeySource: 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- Elect a leader in a three-controller cluster, commit a state update, and confirm quorum acknowledgement.
- Make the leader unavailable, elect a replacement, restore the old controller, and perform follower catch-up.
- Tamper with a durable event, audit record, or controller snapshot and verify failure.
- 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-demoHigh 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 |
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.
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.
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.
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.
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.
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.
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.
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.
| 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 |
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 100Not 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.
- Confirm the exact GHC and Cabal versions.
- Run
cabal cleanafter changing package constraints or generated modules. - Read the first compiler error rather than the final cascade of dependent errors.
- Check constructor changes across all pattern matches and codec versions.
- Check that storage-format changes retain explicit version handling and migration tests.
- Check that a signature or hash uses the same canonical bytes at construction and verification.
- Check current slot, chain tip, immutable point, and network identifier when consensus tests fail.
- Check queue limits, timeouts, and peer authentication when integration tests stall.
- Preserve QuickCheck seeds, model traces, and fault profiles for deterministic replay.
- Never delete a database to hide a recovery bug; reproduce it on a copied training directory.
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.
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.
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.
Persistent peer-manager state containing known endpoints, retry information, bans, and discovery metadata.
The wallet process of deriving addresses in sequence and querying history until a configured unused-address gap is reached.
A versioned release file, container image archive, configuration package, or other deployment input whose size and SHA-256 digest are verified.
A storage technique that writes a temporary file and replaces the destination only after the new content is complete.
An ordered record of important actions. In later builds, each entry commits to the previous entry hash so tampering is detectable.
Proof that a peer, operator, controller, oracle, or transaction signer controls an expected credential.
The policy decision that determines which authenticated identity may perform a requested operation.
A delay that increases after repeated connection or deployment failures to avoid tight retry loops.
A cumulative penalty assigned to a peer for malformed frames, authentication failures, protocol violations, or other abusive behaviour.
A reproducible timed workload used to detect performance changes while keeping operation counts and checksums stable.
A signed collection of transactions with a header that links to a parent block and commits to its body.
The cryptographic commitment to the ordered transactions contained in a block.
A registered participant selected to create a block for a slot.
Reproducible network, genesis, protocol-parameter, and manifest files used to create or verify a network.
Arbitrary or malicious participant behaviour, including equivocation, malformed messages, withholding, flooding, and downgrade attacks.
A binary encoding in which each valid value has exactly one accepted byte representation.
The process by which peers select mutually supported versions and limits for independently versioned mini-protocols.
A signed declaration that a certificate or fingerprint must no longer be accepted.
The deterministic rule that chooses one valid stored branch as the current selected chain.
The hash and height of the final block on the currently selected branch.
The fork-aware persistent component that stores blocks, parent states, the selected tip, and the immutable point.
A wallet output that returns selected input value not used by payments or fees.
A durable record of completed deployment or agent steps, allowing safe resume after restart.
Encrypted bytes produced by an authenticated-encryption algorithm.
The base monetary unit in the training ledger.
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.
A governance body whose registered members cast votes under committee-specific thresholds and terms.
The last block shared by two competing chain branches.
The rules that determine valid block producers and the preferred valid chain.
Data or logic that every honest node must interpret identically to remain on the same chain.
A service that authorizes, records, and coordinates remote agent commands and cluster rollouts.
The minimum number of replicated controllers that must acknowledge a state update before it is committed.
A fixed-length digest that changes unpredictably when its input changes and is used for commitments and identifiers.
Typed data committed to a script-controlled output and supplied to the validator when that output is spent.
Assignment of stake influence to a stake pool or governance voting power to a DRep without transferring ownership.
A restricted interface that activates, verifies, restarts, and rolls back a systemd or container release.
The property that the same valid inputs always produce the same output, independent of machine or timing.
A cryptographic proof created with a private signing key and verified with the corresponding public key.
A delegated representative who receives governance voting power from stake credentials.
A group of slots used for stake snapshots, rewards, parameter activation, and governance timing.
A sequenced real-time feed of blocks, rollbacks, transactions, peers, scripts, or oracle activity.
The maximum deterministic CPU and memory units a script may consume.
A user interface or API that queries indexed blockchain data by block, transaction, address, token, producer, or script.
Transfer of service responsibility from a failed primary node, controller, or region to a verified replacement.
Value paid by a transaction and accounted for under protocol rules.
A nonleader controller replica that serves reads, acknowledges proposals, and catches up committed state.
Two or more valid branches that descend from a common ancestor.
Another name for the chain-selection rule.
A precise description of protocol behaviour. In this project the specifications are executable, but they are not machine-checked mathematical proofs.
A bounded transport record containing magic, version, type, length, checksum, and payload.
The maximum permitted age of an oracle report, command, certificate, or other time-sensitive object.
The canonical configuration and initial state from which a blockchain network begins.
A proposal that moves through draft, voting, ratification, enactment, rejection, or expiry.
A controlled stop that rejects new work, completes necessary persistence, closes services, and reaches a known lifecycle state.
The first authenticated exchange through which peers prove identities and agree on network and protocol information.
An external device or process that signs approved transaction bytes without exposing its private key to the online builder.
A test of whether a service is live, ready, synchronized, or otherwise safe to use.
The number assigned to a block relative to genesis.
A selected-chain point before which the training node refuses rollback or alternative fork selection.
A service that converts selected-chain blocks into query-optimized, rollback-aware records.
A mempool record preventing two pending transactions from claiming the same UTxO.
A request and response protocol that identifies methods, parameters, results, errors, and request IDs in JSON.
Deterministic creation of role-specific keys from a root seed and derivation path.
Replacement of a key or certificate while preserving an auditable transition from the previous credential.
The controller replica elected to accept control-plane writes, or the block producer selected for a slot, depending on context.
The validated state that records unspent outputs, applied transactions, oracle state, and other consensus-controlled information.
Evidence that a process or system is running and responding.
A file that lists artifacts and commits to their sizes and hashes, sometimes with a release signature.
A node-local collection of validated transactions waiting for block inclusion.
An independently versioned network protocol dedicated to one activity such as chain sync or block fetch.
Systematic bounded exploration of reachable states and transitions to find invariant violations.
The rule that every coin remains accounted for across circulation, treasury, reserves, fees, rewards, and deposits.
Mutual Transport Layer Security, in which both sides present and verify certificates.
An authorization rule that requires a threshold number of valid signatures from a defined key set.
The supervised runtime that combines networking, consensus, chain storage, ledger validation, mempool, and services.
A stable identity derived from a verification key and used during authenticated peer or control-plane communication.
A family of secure-channel protocol patterns. The training channel is described as Noise-style but is not a production Noise implementation.
A number used once or monotonically increased to prevent replay and distinguish otherwise similar messages.
A Cardano mechanism authorizing a hot block-production key under a colder credential. It is discussed in the transition material, not reproduced completely here.
A signed application protocol that supplies external observations to on-chain scripts.
A new UTxO containing an address, base coin, tokens, and optional script information.
Peer-to-peer networking in which nodes exchange blockchain information directly.
Dividing a large query result into bounded, stable pages.
A network failure or policy that prevents groups of nodes from communicating.
Learning candidate peer endpoints from static configuration or authenticated peers.
The subsystem that supervises connections, address persistence, backoff, limits, discovery, and bans.
Writing state to durable storage so it can be recovered after process or host restart.
Cardano’s smart-contract platform and associated languages, compiler, core language, and ledger integration.
Secret signing material that must never be logged or shared.
The mapping of authorized block-producer verification keys to names and stake.
Generation of many inputs or command sequences to check general invariants.
A consensus-controlled value such as fee coefficients, size limits, deposits, or protocol version.
A shareable key used to verify signatures.
The persistent, replaceable projection used by explorer and wallet clients.
The minimum number or weight of approvals required for a decision or committed state.
Evidence that a service can safely accept work, including synchronization and dependency checks.
The maximum acceptable amount of data or progress lost during recovery.
The maximum acceptable time required to restore service.
Typed input supplied by a transaction to select or parameterize a script action.
A comparison showing that an implementation agrees with an independent reference model for a defined behaviour.
A node that participates in networking and validation but does not hold a block-production signing key.
Reuse of a previously valid message, command, handshake, or signature in an unauthorized context.
Supply held outside circulation and released under epoch economic rules.
Moving the selected chain backward to a common ancestor before following a different branch.
An ordered multi-node release deployment governed by availability, health, approval, and rollback policy.
Deterministic validation logic that controls whether a script UTxO may be spent.
Transaction and ledger information made available to a validator.
Root secret or deterministic simulation value. Cryptographic wallet seeds and public simulation seeds have very different security requirements.
The valid stored branch currently preferred by the chain-selection rule.
A discrete unit of consensus time in which a block producer may be selected.
A verified copy of chain, index, controller, or other state used for inspection or recovery.
Value used to weight block-production or governance influence without transferring ownership.
A registered Cardano block-production operator to which holders may delegate stake.
A system described by explicit states, commands, preconditions, transitions, and invariants.
Logging with stable machine-readable fields rather than only free-form text.
A runtime component that starts workers, observes failures, coordinates shutdown, and preserves lifecycle state.
Transport Layer Security, the standard protocol used to encrypt and authenticate network connections.
A mapping from native asset identifiers to quantities carried by a UTxO.
A signed ledger operation that consumes inputs, creates outputs, pays a fee, and may include scripts, certificates, or oracle actions.
A deterministic hash of the canonical transaction body.
Network-controlled funds accumulated and withdrawn under economic and governance rules.
An unspent transaction output that can be referenced exactly once as a transaction input.
Deterministic checking that data and state transitions satisfy all applicable rules.
Selection of compatible protocol versions and limits before application messages are exchanged.
A verifiable random function used by Ouroboros-style protocols to produce private leader checks with public proofs.
Software that derives keys and addresses, discovers funds, constructs transactions, signs, submits, and displays history.
A persistent two-way connection used for real-time subscriptions.
Cryptographic or script information supplied to authorize a transaction input.
An operational principle that authenticates, authorizes, validates, and limits every interaction rather than trusting network location alone.
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.
- Cardano Developer Portal: https://developers.cardano.org/
- Cardano node repository: https://github.com/IntersectMBO/cardano-node
- Cardano ledger repository: https://github.com/IntersectMBO/cardano-ledger
- Ouroboros consensus repository: https://github.com/IntersectMBO/ouroboros-consensus
- Ouroboros network repository: https://github.com/IntersectMBO/ouroboros-network
- Cardano API documentation and source within cardano-node: https://github.com/IntersectMBO/cardano-node
- Plutus repository: https://github.com/IntersectMBO/plutus
- Cardano Improvement Proposals: https://github.com/cardano-foundation/CIPs
- cardano-wallet repository: https://github.com/cardano-foundation/cardano-wallet
- cardano-db-sync repository: https://github.com/IntersectMBO/cardano-db-sync
- Ogmios repository: https://github.com/CardanoSolutions/ogmios
- Kupo repository: https://github.com/CardanoSolutions/kupo
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
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: