# Cardano Developer Portal > Documentation for building on Cardano: getting started, core concepts, governance, stake pool operations, and community contribution. This file contains all documentation content in a single document following the llmstxt.org standard. ## Cardano for Ethereum Developers Coming from Ethereum, Cardano will look and feel different: different account model, different smart contract paradigm, different tooling. This page maps the key differences, translates the Solidity habits that carry over (and the ones that don't), and ends with concrete next steps for building. ## What Makes Cardano Different from Ethereum? ### Account Model When developing on Cardano, the most significant difference you will encounter is the account model design. Understanding why Cardano's model was designed differently helps make sense of everything else. Unlike Ethereum, Cardano is designed around the **[Extended UTxO (eUTxO) model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo)** rather than an account-based model. On Ethereum, each address maintains a balance stored in global state. Transactions update these balances directly, and smart contracts hold and modify their own storage. ![eUTxO vs Account Model](./curriculum/fundamentals/core-concepts/img/eutxo-vs-account-model.jpg) On Cardano, there is no global mutable contract storage like on Ethereum. Instead, both value and application state are carried in discrete Unspent Transaction Outputs (UTxOs). While the ledger itself maintains global state (such as the UTxO set, protocol parameters, and staking state), smart contracts do not read from or write to shared storage. Everything a validator is allowed to reason about must be provided explicitly by the transaction through its inputs and outputs. A helpful mental model is to think of UTxOs like physical bills rather than a single bank balance. Your wallet doesn’t hold “100 ADA” as one number; it holds individual UTxOs whose values sum to your balance. When you spend, you consume entire UTxOs and create new ones as outputs. For example, if you have a single 100 ADA UTxO and want to send 10 ADA, the transaction consumes the 100 ADA UTxO and creates two new ones: one with 10 ADA for the recipient and one with 90 ADA as change. Smart contracts on Cardano follow the same model. Validators (Cardano's term for smart contracts) have no internal, mutable storage. There is no state living inside a contract. Instead, application state lives in **datums**, which are arbitrary data attached to UTxOs, similar to a struct stored alongside the UTxO's value. A validator's job is not to update state, but to check that a transaction correctly transforms one set of UTxOs (and their datums) into the next. ### Counter Example To make the contrast concrete, here is a counter implemented in Solidity on Ethereum, followed by the same counter implemented in [Aiken](/docs/developers/curriculum/smart-contracts/choose-a-language) on Cardano. #### Ethereum Counter Smart Contract ```js contract Counter { uint256 private count; function increment() public { count += 1; } function decrement() public { count -= 1; } function getCount() public view returns (uint256) { return count; } } ``` In Solidity, `count` is stored directly in the contract's storage and modified in place. The contract itself owns this state. When `increment()` or `decrement()` is called, the contract executes code that mutates its internal storage as part of transaction execution. #### Cardano Counter in Aiken On Cardano, the mental model is fundamentally different. There is no contract-owned storage that gets updated by executing code. Instead, state (the counter value) lives in a datum attached to a UTxO. The validator does not perform the state change; it only validates that a proposed state transition encoded in the transaction is correct. In other words: - Off-chain code constructs a transaction describing the desired state change - On-chain code (the validator) verifies that the transaction follows the rules First, we define the shape of the state we want to track. This is the datum attached to the UTxO holding the counter value: ```aiken pub type CounterDatum { count: Int, } ``` We define the set of actions (similar to endpoints of the spending smart contract we will write later). This intent is provided to the validator via a **redeemer**, which is data supplied by whoever builds the transaction, telling the validator which action to perform: ```aiken pub type CounterAction { Increment Decrement } ``` Building a counter on Cardano means designing valid state transitions. You construct a transaction with the correct inputs (the UTxO holding current state) and outputs (a new UTxO with updated state) that satisfy the validator's rules. When someone wants to increment the counter, they build a transaction that consumes the current state and produces the new state: ```mermaid flowchart LR subgraph Input A["UTxO at script addressDatum: {count: 5}Value: 2 ADA"] end subgraph Transaction T["Redeemer: Increment"] end subgraph Output B["UTxO at script addressDatum: {count: 6}Value: 2 ADA"] end A --> Transaction --> B ``` The transaction: 1. Supplies a redeemer specifying the desired action (Increment or Decrement) 2. Spends the UTxO containing the current counter value in its datum (input) 3. Creates a new UTxO at the same address with the updated datum (output) :::note Helper functions The validator below uses helper functions like `find_continuing_output` and `get_datum`. These are not built into Aiken, but are common patterns you implement or import from [utility libraries](https://packages.aiken-lang.org/). ::: Here's the validator: ```aiken validator counter_validator { spend( datum_opt: Option, redeemer: CounterAction, _input: OutputReference, tx: Transaction, ) { // --- Input state (current counter) --- // The state lives in the datum of the UTxO being spent. expect Some(input_datum) = datum_opt // --- Transition Logic --- // The redeemer tells the validator which state transition logic to apply. when redeemer is { Increment -> { // --- Output state (next counter) --- // A valid state transition must produce a continuing output UTxO at the same smart contract address. // That output carries the next state in its datum. expect Some(output) = find_continuing_output(tx) expect output_datum: CounterDatum = get_datum(output) // --- Transition rule --- // The datum attached to the output UTxO must contain the next correct state of the counter. output_datum.count == input_datum.count + 1 } Decrement -> { expect Some(output) = find_continuing_output(tx) expect output_datum: CounterDatum = get_datum(output) output_datum.count == input_datum.count - 1 } } } else(_) { // Non-spending purpose (e.g., minting) is not supported by this validator. fail } } ``` To explore more real-world smart contracts written in Aiken, see the [Contract library](/templates/contracts). ## How Do Transactions Work on Cardano? A Cardano transaction transforms UTxOs: it spends existing ones and creates new ones. ![UTxO Transaction Flow](./curriculum/fundamentals/core-concepts/img/utxo-transaction-flow.png) The key components: - **Inputs**: UTxOs being spent - **Outputs**: New UTxOs being created, each with an address, value, and optionally a datum attached to it - **Signatures**: Authorize spending by verifying that the transaction is signed by the required keys to spend the input UTxOs - **Redeemers**: Data passed to validators when spending UTxOs locked at validator script addresses - **Validity interval**: A time window when the transaction is valid - **Mint/burn**: Token operations, if any The validity interval deserves special attention. On Ethereum, smart contracts can read block.timestamp during execution. Cardano validators do not have access to a notion of “current time.” Instead, time-based constraints are enforced using the transaction’s declared validity interval. Every transaction specifies a lower and/or upper bound on when it may be included. The ledger rejects transactions outside this window before any scripts run. Validators can then rely on the declared bounds themselves. ```mermaid gantt dateFormat X axisFormat %s section Validity Before valid :done, 0, 1 Valid window :active, 1, 3 After valid :done, 3, 4 ``` To enforce "action X only after date Y," store Y in the datum and check that the transaction's lower bound ≥ Y. Another significant difference from Ethereum is composition. On Ethereum, a transaction has a single entry point and composition is typically expressed through internal contract calls (routers, aggregators, multicalls). On Cardano, a single transaction can spend from multiple script addresses and mint tokens under multiple policies. Each validator runs independently; all must pass, and the entire transaction succeeds or fails atomically. No router contracts are required. In the UTxO model, the [transaction](/docs/developers/curriculum/fundamentals/core-concepts/transactions) itself is the **local state**. Every input, every output, every signature, every piece of data the validator needs is contained within the transaction. There's no querying external state and as a result there are no surprises from concurrent modifications. When a validator runs, it receives this complete local state as context. For example, the transaction's `extra_signatories` field lists every verification key hash that signed it. The validator can inspect all inputs being spent, all outputs being created, and all metadata attached. Given the same transaction inputs and outputs, a validator will always produce the same result because everything it needs is self-contained. ## How Do Fees Work on Cardano? Cardano uses a deterministic fee model. Fees are calculated using a fixed formula based on transaction size (a × tx_size + b) and, for smart contract transactions, known execution budgets (CPU and memory units). Fees are fully calculable before submission, with no gas price auctions or bidding. One important detail: every UTxO must contain a minimum amount of ADA (called **minUTxO**). This prevents dust attacks and ensures UTxOs are economically meaningful. The minimum depends on the UTxO's size, so more data in the datum means more ADA required. Client SDKs handle this automatically when building transactions. ## Common Patterns For a deeper dive into how smart contracts work on Cardano, see the [Smart Contracts overview](/docs/developers/curriculum/smart-contracts/overview). Below are common patterns you'll encounter when building validators. ### Adding Authorization: The `onlyOwner` Pattern Let's extend our counter example to require owner authorization. On Ethereum, you'd use a modifier: ```js address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function increment() public onlyOwner { count += 1; } ``` On Cardano, we would check if the defined owner signed the transaction. The `key_signed` [helper function](https://packages.aiken-lang.org/) checks whether a specific key hash appears in `tx.extra_signatories`, the list of keys that signed this transaction: ```aiken validator counter_with_owner(owner: VerificationKeyHash) { spend( datum_opt: Option, redeemer: CounterAction, _input: OutputReference, tx: Transaction, ) { expect Some(input_datum) = datum_opt // Check if owner signed let is_owner_signed = key_signed(tx.extra_signatories, owner) when redeemer is { Increment -> { expect Some(output) = find_continuing_output(tx) expect output_datum: CounterDatum = get_datum(output) // Check if the owner signed the transaction and the counter was incremented correctly // If both conditions are true, the transaction is allowed. is_owner_signed && (input_datum.count + 1 == output_datum.count) } Decrement -> { // Similar logic for decrementing. } } } ``` The `owner` parameter is baked into the script at compile time. Different owners produce different script hashes, meaning different addresses: ```aiken validator counter_with_owner(owner: VerificationKeyHash) { ``` This is Cardano's equivalent of Solidity's constructor arguments, but with an important difference: the parameter is compiled directly into the script bytecode. Even if two contracts have identical logic, compiling with different `owner` verification keys produces different bytecode: ```mermaid flowchart LR subgraph Compile ["Same script, different params"] A["Script + Owner A"] -->|hash| B["Address A"] C["Script + Owner B"] -->|hash| D["Address B"] end ``` Since the script address is derived by hashing this bytecode, each owner gets their own unique address. This means each owner's counter state lives in UTxOs at a completely separate address, providing isolation between different instances of the same contract logic. ### Adding Time Locks What if we want the counter to only work before a certain deadline? On Ethereum, you'd check `block.timestamp`. On Cardano, we use validity intervals. Here's a more complete example based on our vesting contract pattern: ```aiken pub type CounterDatum { count: Int, owner: ByteArray, deadline: Int, // POSIX timestamp in milliseconds } validator timed_counter { spend( datum_opt: Option, _redeemer: Data, _input: OutputReference, tx: Transaction, ) { expect Some(datum) = datum_opt let is_owner_signed = key_signed(tx.extra_signatories, datum.owner) let is_not_expired = valid_before(tx.validity_range, datum.deadline) is_owner_signed && is_not_expired } else(_) { fail } } ``` The `valid_before` function checks that the transaction's validity interval ends before the deadline. ## Native Tokens vs ERC-20/721 On Ethereum, tokens are smart contracts. Creating an ERC-20 means deploying a contract, and every transfer is a contract call that costs gas. The `approve` + `transferFrom` pattern is necessary for dApps to spend your tokens. On Cardano, [native tokens](/docs/developers/curriculum/native-tokens/overview) are built into the ledger itself. Remember that UTxOs are the fundamental unit that all network logic applies to. Each UTxO can carry multiple values: ADA plus any number of [native tokens](/docs/developers/curriculum/native-tokens/overview). When you spend a UTxO, you're moving all the value it contains in one atomic operation. This means transferring tokens is no different from transferring ADA. There's no `approve` pattern, no separate contract calls, no extra gas. A single transaction can move ADA and dozens of different tokens across multiple UTxOs, all with the same predictable fee. ### Native Scripts For simple minting rules, you don't need to write any smart contract code. Cardano has **native scripts**, a minimal scripting language built into the ledger with six simple constructors: `sig` (require signature), `all` (all conditions), `any` (any condition), `atLeast` (n-of-m), `before` (time lock), and `after` (time lock). For example, to create a token that requires your signature and can only be minted before a deadline: ```typescript const nativeScript: NativeScript = { type: "all", scripts: [ { type: "before", slot: "99999999" }, { type: "sig", keyHash: yourPubKeyHash }, ], }; ``` The ledger validates these rules directly. This covers most basic token use cases: single-owner minting, multisig minting, time-locked minting. ### Validators When you need complex business logic like conditional minting based on other UTxOs, oracle data, or custom validation, you write a minting policy smart contract: ```aiken validator my_token(owner: VerificationKeyHash) { mint(_redeemer: Data, _policy_id: PolicyId, tx: Transaction) { key_signed(tx.extra_signatories, owner) } else(_) { fail } } ``` This gives you full programmability by allowing you to check transaction inputs/outputs, reference other UTxOs, and enforce arbitrary conditions. Native tokens work just like ADA in transactions. The only difference is that minting and burning require a policy. ## Putting It Together: A Ticketing System Let's see how these concepts combine in a real use case. This ticketing system is used for a conference registration. It issues unique NFT tickets where each purchase increments a counter and mints a token named `TICKET0`, `TICKET1`, and so on. ### Transaction Structure Here's what a ticket purchase transaction looks like: ```mermaid flowchart LR subgraph Inputs A["Buyer funds(ADA for payment)"] B["Current state(ticket_counter: 7)"] end subgraph TX["buy_ticket"] T[" "] end subgraph Outputs C["Ticket + change(TICKET7 NFT)"] D["New state(ticket_counter: 8)"] E["Payment(ADA to treasury)"] end A --> TX B --> TX TX --> C TX --> D TX --> E ``` The transaction spends two UTxOs as inputs: the buyer's funds (from their wallet) and the current protocol state (sitting at the script address with `ticket_counter: 7` in its datum). It creates three new UTxOs as outputs: the minted ticket plus change goes back to the buyer's wallet, the updated state (with `ticket_counter: 8`) returns to the script address to continue the protocol, and the payment goes to the treasury (organizers) address. Everything happens atomically. If any part fails, nothing happens. First, the types. We define an `AssetClass` to identify tokens, datums for state, and redeemers for actions: ```aiken use cardano/assets.{AssetName, PolicyId} pub type AssetClass { policy: PolicyId, name: AssetName, } pub type TicketerDatum { ticket_counter: Int, } pub type TicketerRedeemer { BuyTicket } pub type TicketPolicyRedeemer { MintTicket BurnTicket } ``` Now the validator itself, which handles both spending (state updates) and minting (ticket creation): ```aiken validator ticketer( admin_token: AssetClass, blind_price: Int, normal_price: Int, switch_slot: Int, treasury: Address, max_tickets: Int, ) { spend(datum: Option, redeemer: TicketerRedeemer, utxo: OutputReference, tx: Transaction) { expect Some(datum) = datum let TicketerDatum { ticket_counter } = datum expect ticket_counter < max_tickets expect [ticketer_output] = list.filter(outputs, fn(o) { o.address == ticketer_input.output.address }) expect ticketer_datum: TicketerDatum = ticketer_output.datum let must_update_datum = ticketer_datum.ticket_counter == ticket_counter + 1 let current_price = if interval.is_entirely_before(tx.validity_range, switch_slot) { blind_price } else { normal_price } let must_pay_treasury = list.any(outputs, fn(o) { o.address == treasury && quantity_of(o.value, ada_policy_id, ada_asset_name) >= current_price }) let ticket_name = concat("TICKET", from_string(string.from_int(ticket_counter))) let must_mint_ticket = tx.mint == from_asset(policy_id, ticket_name, 1) must_update_datum? && must_pay_treasury? && must_mint_ticket? } mint(redeemer: TicketPolicyRedeemer, policy_id: PolicyId, tx: Transaction) { when redeemer is { MintTicket -> { list.any(tx.inputs, fn(input) { input.output.address.payment_credential == Script(policy_id) }) } BurnTicket -> { list.all(tokens(tx.mint, policy_id), fn(pair) { pair.2nd < 0 }) } } } } ``` Let's break down what's happening. ### Parameterized Scripts ```aiken validator ticketer( admin_token: AssetClass, blind_price: Int, normal_price: Int, switch_slot: Int, treasury: Address, max_tickets: Int, ) { ``` Configuration is encoded as script [parameters](https://aiken-lang.org/language-tour/validators#parameters) (not stored in contract state (datum)). You can use parameter values as hardcoded constants for the validator. The validator is compiled as a parameterized script, and when parameters are applied off-chain, they become part of the resulting script bytes. Because the script hash is computed from those bytes, different parameters result in different script hashes and therefore different contract addresses. ### State Lives in Datums ```aiken spend(datum: Option, redeemer: TicketerRedeemer, UTxO: OutputReference, tx: Transaction) { expect Some(datum) = datum let TicketerDatum { ticket_counter } = datum ``` The `ticket_counter` isn't stored in the contract. It's attached to the UTxO being spent. Each purchase consumes the old state UTxO and creates a new one with an incremented counter. The validator receives this state as input, not from internal storage. ```aiken expect ticketer_datum: TicketerDatum = ticketer_output.datum let must_update_datum = ticketer_datum.ticket_counter == ticket_counter + 1 ``` The validator doesn't increment the counter. It checks that whoever built the transaction incremented it correctly. The off-chain code does the work; the on-chain code validates the result. If the output datum doesn't have exactly `ticket_counter + 1`, validation fails. ### Finding the Continuing Output ```aiken expect [ticketer_output] = list.filter(outputs, fn(o) { o.address == ticketer_input.output.address }) ``` When a stateful contract updates, the new state must go back to the same script address. This line finds the output that "continues" the contract by filtering for outputs sent to the same address as the input. The `expect [ticketer_output]` pattern asserts there's exactly one such output. If zero or multiple outputs match, validation fails. This is how you ensure the protocol state isn't duplicated or lost. ### Verifying Payments ```aiken let must_pay_treasury = list.any(outputs, fn(o) { o.address == treasury && quantity_of(o.value, ada_policy_id, ada_asset_name) >= current_price }) ``` The validator scans transaction outputs to verify payment. It checks that at least one output goes to the treasury address with the required ADA amount. This pattern of iterating outputs to find a matching address and minimum value is how you enforce payments on Cardano. The transaction builder decides which UTxOs to use and how to structure outputs; the validator just confirms the result meets requirements. ### Time via Validity Intervals ```aiken let current_price = if interval.is_entirely_before(tx.validity_range, switch_slot) { blind_price } else { normal_price } ``` The validator checks if the transaction's validity range falls before or after `switch_slot`. Early bird pricing is enforced by the ledger rejecting transactions submitted after the deadline, before the script even runs. The validator just needs to check which price tier applies. ### Multiple Validators in one transaction ```aiken mint(redeemer: TicketPolicyRedeemer, policy_id: PolicyId, tx: Transaction) { when redeemer is { MintTicket -> { list.any(tx.inputs, fn(input) { input.output.address.payment_credential == Script(policy_id) }) ``` The `spend` validator handles state updates while the `mint` validator controls ticket creation. For minting, the validator just checks that the spend validator is also running in this transaction, which ensures state is properly updated. Both validators run independently; if either fails, the whole transaction is rejected atomically. :::info How often do validators run? Validators run **per script purpose**, not once per transaction: - **Spending validators** run once for each script-locked input being spent - **Minting policies** run once per policy ID (not per asset or per input) - **Staking scripts** run once per certificate or withdrawal that requires a script A single transaction can trigger multiple executions of the same spending validator (with different datums/redeemers), plus minting policy executions, plus staking scripts. This is a key difference from Ethereum, where one contract call means one execution. ::: ### Admin Token for Authentication The `admin_token` parameter solves a Cardano-specific problem: anyone can create (send) UTxOs to any address. Without the admin token, an attacker could create fake state UTxOs with manipulated counters. Our validator checks that the state UTxO contains this unique token, identifying it as the legitimate protocol state rather than a random UTxO at the same address. ### Combining Validations ```aiken must_update_datum? && must_pay_treasury? && must_mint_ticket? ``` All conditions must pass for the transaction to succeed. Each check is a boolean, and the final expression combines them. The `?` suffix is Aiken syntax that traces the variable name on failure, useful for debugging which condition failed. This declarative style of building up named boolean checks and combining them at the end makes validators easier to read and audit. This is what production Cardano development looks like: declarative transactions where you specify exactly what should happen, and validators that approve or reject based on whether you followed the rules. ## Developer Environment Ready to start building? Here's the tooling landscape. ### Programming Languages Ethereum developers primarily use Solidity. On Cardano, currently the [most popular language](https://cardano-foundation.github.io/state-of-the-developer-ecosystem/2025/#what-do-you-use-or-plan-to-use-for-writing-plutus-script-validators-smart-contracts) is **Aiken**: a purpose-built language with Rust-like syntax, strong static typing, and its own toolchain (compiler, test framework, formatter, LSP). Aiken compiles directly to UPLC (Untyped Plutus Core), Cardano's native bytecode. Alternatives include [OpShin](https://opshin.dev) (Python syntax), [Scalus](https://scalus.org) (Scala), [Pebble](https://pluts.harmoniclabs.tech/) (TypeScript DSL). See the [Smart Contracts overview](/docs/developers/curriculum/smart-contracts/overview) for the full list. ### Tools | Ethereum | Cardano | |----------|---------| | Hardhat | [Aiken CLI](https://aiken-lang.org/installation-instructions) (`aiken build`, `aiken check`) | | Remix | [Aiken Playground](https://play.aiken-lang.org) | | Web3.js, ethers.js | [Client SDKs](/docs/developers/curriculum/start-building/choose-your-tools) like **Mesh SDK** (TypeScript) | | Ganache, Foundry | [Local devnets](/docs/developers/curriculum/start-building/local-testing#local-devnets) like [Yaci DevKit](https://devkit.yaci.xyz/) | | Infura, Alchemy | [Query APIs](/docs/developers/curriculum/production/connecting-to-the-chain#query-apis) like [Blockfrost](https://blockfrost.dev/), [Maestro](https://www.gomaestro.org/), [Koios](https://koios.rest/) | | Etherscan | [Explorers](https://explorer.cardano.org/) | | MetaMask | [Wallets](https://cardano.org/apps/?tags=wallet) | ### Client SDKs Client SDKs handle transaction building, wallet integration, UTxO selection, and fee calculation. They're equivalent to ethers.js or web3.js but for Cardano. See the full [Client SDKs documentation](/docs/developers/curriculum/start-building/choose-your-tools) for detailed guides. | Language | SDK | |----------|-----| | TypeScript | [Typescript SDKs](/docs/developers/curriculum/start-building/choose-your-tools)| | Python | [PyCardano](/tools/?tags=sdk) | | Rust | [Pallas](/tools/?tags=sdk) | | Go | [Apollo](/tools/?tags=sdk) | | C# | [Chrysalis](/tools/?tags=sdk) | ### Development Workflow 1. Write validators in Aiken (`.ak` files) 2. Build with `aiken build`, which generates `plutus.json` (the "blueprint") 3. Test with `aiken check` (built-in test framework) 4. Import compiled scripts into your off-chain app 5. Deploy by sending UTxOs to the script address Unlike Ethereum, scripts don't require deployment to exist. The script hash determines the address, and the same script always produces the same address. However, you can publish scripts on-chain as **reference scripts** (CIP-33) so transactions can reference them instead of including the full script code each time, reducing fees. ## What's Different with Smart Contract Development? The examples showed *how* the models differ. Here's *why* those differences matter. The eUTxO model offers strong guarantees, but those guarantees come with tradeoffs. Understanding these early helps avoid frustration when building more complex applications. ### Parallelization Because application state is carried in UTxOs rather than shared contract storage, transactions that operate on different UTxOs can often be processed in parallel. There is no single mutable variable that all users must contend over. For example, if 100 users each control their own counter UTxO, all 100 can update their counters simultaneously without blocking one another. ### Deterministic Validation and No Reentrancy Validators only inspect the transaction they validate: its inputs, outputs, datums, redeemers, signers, and validity range. They do not depend on execution order or shared mutable state, and always evaluate to true or false. This deterministic model eliminates reentrancy-style vulnerabilities common in account-based systems. The classic reentrancy pattern, where a contract is re-entered mid-execution while mutating shared storage, does not apply. Each UTxO is consumed atomically, and validators cannot be re-invoked during execution. Because validator behavior is predictable from the transaction itself, contracts are easier to test and formally verify. ### More Off-chain Complexity On Ethereum, much of the application logic lives inside the contract itself. On Cardano, the validator only verifies correctness of the state transition. The work of constructing the correct state transition happens off-chain. This is what Client SDKs handle: ```mermaid flowchart LR subgraph Off-Chain ["Off-Chain (Client SDK)"] A[Query UTxOs] B[Select inputs] C[Compute new datums] D[Build transaction] E[Calculate fees] end subgraph On-Chain ["On-Chain (Validator)"] F{Check rules} G[✓ Valid] H[✗ Invalid] end A --> B --> C --> D --> E --> F F -->|pass| G F -->|fail| H ``` But Client SDKs are just one piece of the puzzle. A typical Cardano dApp is not just a smart contract. It combines off-chain components like transaction construction, state management, and indexers with on-chain validators: ![Typical Cardano DApp Architecture](./img/typical-cardano-dapp.jpeg) Off-chain components handle state tracking, transaction building, UTxO selection, mempool monitoring, and blockchain indexing. On-chain validators are lean and only verify that transactions follow the rules. In practice, Cardano development intentionally shifts complexity from on-chain to off-chain correctness. ### State Management There is no implicit “current state” stored in a contract. State must be modeled explicitly using UTxOs and datums. Patterns like counters, registries, or mappings require designing how state is split across UTxOs and how those UTxOs evolve over time. This makes state transitions explicit and auditable, but it can feel verbose compared to mutating a variable in contract storage on Ethereum. ### Concurrency Requires Design The eUTxO model enables parallelism, but requires care to avoid contention. If many users need to update a single piece of state (for example, a single global counter), they will contend for the same UTxO: ```mermaid flowchart LR subgraph Contention ["❌ Contention (single UTxO)"] direction LR U1["User A"] --> S1["Shared UTxO"] U2["User B"] --> S1 U3["User C"] --> S1 S1 -.->|"only one succeeds"| R1["?"] end ``` ```mermaid flowchart LR subgraph Parallel ["✓ Parallel (per-user UTxOs)"] direction LR A1["User A"] --> A2["UTxO A"] --> A3["UTxO A'"] B1["User B"] --> B2["UTxO B"] --> B3["UTxO B'"] C1["User C"] --> C2["UTxO C"] --> C3["UTxO C'"] end ``` Avoiding contention requires architectural patterns such as sharded state, per-user UTxOs, or batching. These are design decisions with their own tradeoffs. ### No Contract Calls Ethereum contracts can call other contracts. Cardano validators do not call other validators. Instead, you compose multiple validations within a single transaction. All referenced validators run independently and must pass for the transaction to succeed. ### No Mapping Type Ethereum's `mapping(address => uint)` has no direct equivalent. Instead, you use the UTxO pattern: create one UTxO per entry, with the datum containing the key and value. To look up an entry, query for UTxOs at the script address with matching key in datum. This is more parallelizable since multiple users can update their entries simultaneously without contention. ### Smart Contract Security The eUTxO model has its own security considerations that differ from account-based systems. [Smart Contract Vulnerabilities](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview) serves as a reference for common issues and mitigations. **Example: Double Satisfaction** Since validators run independently for each input and all see the same transaction outputs, a careless validator can be "satisfied" multiple times by the same output: ```mermaid flowchart TB subgraph tx ["Attacker's Transaction"] direction TB subgraph inputs ["Inputs (attacker claims both)"] A["UTxO 1: Swap offer10 tokens for 5 ADA"] B["UTxO 2: Swap offer10 tokens for 5 ADA"] end subgraph outputs ["Outputs (only pays once)"] C["5 ADA to seller"] D["20 tokens to attacker"] end end A -.->|"checks: 5 ADA paid? ✓"| C B -.->|"checks: 5 ADA paid? ✓"| C ``` Both validators ask "is there an output paying 5 ADA to the seller?" Both see the same output and pass. The attacker claims 20 tokens but only pays 5 ADA instead of 10. The fix is tagging outputs uniquely so each validator looks for its specific output. This is just one of many eUTxO-specific patterns. The documentation covers these vulnerabilities, and you can practice exploiting them in the [Cardano CTF](/docs/developers/curriculum/smart-contracts/security/ctf). ## Quick Reference: Ethereum to Cardano A few Solidity habits carry over, but the ones that matter are not one-to-one: - **There is no contract storage.** Solidity keeps mutable state inside the contract; Cardano attaches immutable data (a **datum**) to a UTxO. You never update a datum in place, you consume the UTxO and create a new one with the new value, so a `mapping(addr => uint)` becomes one UTxO per entry rather than a single mutable map. - **There is no `msg.sender` or `msg.value`.** A validator inspects the transaction itself: check `tx.extra_signatories` for who signed, and read input and output values explicitly. - **Logic moves from runtime to build time.** A `constructor` becomes a parameterized script whose parameters are baked in at compile time. `require(condition)` becomes Aiken's `expect`, and `modifier onlyOwner` becomes an explicit `key_signed(signers, owner)` check. - **The interface is a blueprint, not an ABI.** Tools read [`plutus.json`](https://cips.cardano.org/cip/CIP-0057) the way they read an ABI. Instead of view functions and events, you query UTxOs directly through a provider and use transaction metadata or an indexer for event-style history. ## Next steps - [Start Building](/docs/developers/curriculum/start-building/overview): Module 2 of the curriculum. Pick your tools, get test ADA, and send your first transaction with the same SDKs used throughout. - [eUTXO](/docs/developers/curriculum/fundamentals/core-concepts/eutxo): the model behind every difference on this page, if you jumped straight here. - [Write a validator](/docs/developers/curriculum/smart-contracts/write-a-validator): hands-on Aiken when you reach Module 5; [aiken-lang.org](https://aiken-lang.org) and the [standard library](https://aiken-lang.github.io/stdlib/) pair well with it. --- ## Masumi Network An agent economy needs three capabilities beyond any single agent's code: decentralized identity, payments between agents, and discovery, the ones the [AI agents overview](/docs/developers/curriculum/dapps/ai-agents/overview) singles out as needing a dedicated protocol. [Masumi](https://www.masumi.network/) is a Cardano protocol implementing all three, so an agent's own wallet and signing stay ordinary SDK work while Masumi handles the parts that need a shared network. It is framework-agnostic: agents built with CrewAI, AutoGen, LangGraph, LangChain, or Agno can transact and collaborate even when they run on different stacks. ## What Masumi provides - **Payments.** Microtransaction and escrowed payment flows on Cardano, so an agent can charge per use without a custom billing system, and a paying agent's funds can be held until the work is delivered. - **Identity.** Each agent gets a [decentralized identifier (DID)](https://www.w3.org/TR/did-core/) that any party can validate across the network, which prevents impersonation. - **Traceability.** Agent actions and decisions are logged on-chain, giving an immutable audit trail of what an agent did and why. - **Discovery.** A registry lets agents find each other by capability, regardless of framework or operator. ![Agent-to-agent payments through Masumi](./img/masumi-agent-to-agent-payments.png) When one agent hires another (a market-research agent buying data from an analysis agent, which in turn pays a third for raw market data), the identities, payments, and logs for the whole chain flow through this infrastructure. ## Getting started The quickest path is the CrewAI template: 1. Install the Masumi node that runs alongside your AI workflow. 2. Start it in parallel with your framework (CrewAI, LangGraph, or another). 3. Add the Masumi integration to your agent with a few lines of code. 4. Deploy: the agent goes live on the network with a verified identity. Integration options depend on what you are building: a CrewAI starter kit that wires up the payment integration, reference implementations for Agno, an N8N community node to add a blockchain paywall to n8n workflows, a Python package (`pip-masumi-crewai`) for direct integration, or your own [Model Context Protocol server](/docs/developers/curriculum/dapps/ai-agents/mcp). ## The network Masumi is several components working together: - **Registry service.** Agent registration and identity management. - **Payment service.** Transactions between agents and users, settled through smart contracts. - **Explorer.** Track transactions, logs, and agent activity. - **Sokosumi.** A marketplace for discovering agents. - **Kodosumi.** A runtime for managing and executing agent services at scale. ## Resources - [Documentation](https://www.masumi.network/dev) - [Masumi Explorer](https://explorer.masumi.network) - [GitHub organization](https://github.com/masumi-network) - [Website](https://www.masumi.network/) - [Discord](https://discord.gg/masumi) Protocol changes are proposed through the [Masumi Improvement Proposals](https://github.com/masumi-network/masumi-improvement-proposals) repository. ## Next steps - [MCP access](/docs/developers/curriculum/dapps/ai-agents/mcp): give an AI assistant Cardano tools, with the signing boundary intact - [Build a dApp](/docs/developers/curriculum/dapps/overview): back to the module, where the agent's wallet and transactions are ordinary dApp building blocks --- ## Connect an AI assistant with MCP There are three ways AI meets Cardano, and you have already seen two. An assistant can help you [write Cardano code](/docs/developers/curriculum/start-building/ai-assisted-development), and an [autonomous agent](/docs/developers/curriculum/dapps/ai-agents/overview) can hold its own wallet and act on-chain without a human. This page is the third: a general assistant like Claude that reads *your* Cardano state and drafts transactions *you* approve and sign. The bridge that makes this work is **MCP**. ## What MCP is The **[Model Context Protocol (MCP)](https://modelcontextprotocol.io)** is an open standard for connecting AI assistants to external systems. It was introduced by Anthropic and is now stewarded by the Linux Foundation's Agentic AI Foundation, so it is not tied to a single vendor. An **MCP server** advertises a set of **tools**, callable functions with typed inputs, that any MCP-compatible client (Claude Desktop, an IDE assistant, your own app) can invoke on your behalf. MCP is complementary to the [agent skills](/docs/developers/curriculum/start-building/ai-assisted-development) covered earlier: a skill is a folder of instructions that shapes *how* the model works, while an MCP server gives it *tools it can call*. You often use both, skills for knowledge, MCP for actions. ## What a Cardano MCP server exposes A Cardano MCP server turns chain and wallet access into tools the assistant can call. In practice they fall into two groups: - **Read tools**: query the UTXOs and balance at an address, resolve an ADA Handle to its address, check staking and rewards, look up a transaction. These let the assistant answer questions about your on-chain state in plain language. - **Write tools**: assemble a transaction from a request ("send 10 ADA to this handle," "delegate to this pool") and submit it once it is signed. ## Your wallet stays the signing authority The important property is that the assistant **proposes**, and you **sign**. A well-designed Cardano MCP server builds an *unsigned* transaction and hands it to your wallet to review and approve, using the same [CIP-30](/docs/developers/curriculum/dapps/connect-a-wallet#what-cip-30-gives-you) build-then-sign boundary you already use in a dApp. Your signing keys never reach the model or the server. ```mermaid flowchart LR A["AI assistant(MCP client)"] -->|"calls a tool"| S["Cardano MCP serverqueries chain, builds tx"] S -->|"unsigned transaction"| W["Your walletyou review and sign"] W -->|"submit"| C["Cardano"] style A fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style S fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style W fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style C fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` Treat any server that holds spending keys it can move without your approval as a custodial service, and weigh it accordingly. For read-only servers the risk is smaller, but the same rule applies: an assistant should never be able to move funds you did not authorize. ## Find an implementation The Cardano MCP ecosystem is early and moving quickly, with wallets and protocols shipping their own servers. Rather than pin a list here, browse the current options, and check each one's license, source, and exactly which tools it exposes, in [Builder Tools](/tools). ## Next steps - [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet): the CIP-30 signing boundary an MCP server relies on - [Set up your AI assistant](/docs/developers/curriculum/start-building/ai-assisted-development): give your coding assistant current Cardano context - [IoT on Cardano](/docs/developers/curriculum/dapps/iot/): the module's hands-on hardware track, and the road to [Ship to Production](/docs/developers/curriculum/production/overview) --- ## AI agents on Cardano This section is about **autonomous agents**: AI systems that act on-chain themselves, holding a wallet and sending transactions without a human in the loop. If instead you want AI to help you *write* Cardano code, see [Set up your AI assistant](/docs/developers/curriculum/start-building/ai-assisted-development). And if you want a general assistant to read your wallet and draft transactions you approve and sign, see [Connect an AI assistant with MCP](/docs/developers/curriculum/dapps/ai-agents/mcp). An autonomous agent is software that perceives, decides, and acts toward a goal. Putting one on Cardano means giving it the ability to move value and record decisions on a ledger no single party controls, which is what makes automated trading, treasury management, governance participation, or paid agent-to-agent services possible without a trusted intermediary. ## What an on-chain agent needs Whatever framework an agent is built in (CrewAI, LangGraph, Agno, or your own), acting on Cardano comes down to four capabilities: - **A wallet and signing.** The agent holds keys and builds, signs, and submits transactions. These are the same mechanics from [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet) and [Transaction building](/docs/developers/curriculum/start-building/transaction-building), driven from the agent's code instead of a UI. - **Payments.** An agent that sells a service needs to charge for it, and one that consumes another agent's service needs to pay. That means per-use microtransactions and, often, funds held in escrow until the work is delivered. - **A verifiable identity.** Other agents and users need to know they are talking to the right agent, not an impersonator. An on-chain [decentralized identifier (DID)](https://www.w3.org/TR/did-core/) gives each agent a credential anyone can check. - **Discovery.** To collaborate, agents have to find each other. A shared on-chain registry lets one agent locate another by capability, regardless of who built or operates it. The wallet and signing parts are ordinary SDK work you have already seen. Identity, payments between agents, and discovery are where a dedicated protocol helps. ## Masumi: the agent-economy protocol [Masumi](/docs/developers/curriculum/dapps/ai-agents/masumi) is a Cardano protocol that provides exactly those: decentralized identity, an escrowed payment layer, and an agent registry, all framework-agnostic. It is the worked example in this section. Start there to see how an agent registers an identity, gets paid, and discovers peers. --- ## Connect a Wallet Connecting a wallet is the front door to almost every dApp: a swap, an NFT mint, a vote, or signing in all start here. On Cardano, browser wallets expose a standard interface called **[CIP-30](https://cips.cardano.org/cip/CIP-0030)**, the dApp-wallet connector. Your app requests access, then asks the wallet for the user's addresses, UTXOs, and signatures. **The keys never leave the user's device**; the wallet prompts the user to approve each signature. This page is about the **browser wallet** (the user's CIP-30 extension or hardware wallet). To create a wallet from a mnemonic or private key in backend code, see [Keys & Wallets › working with wallets in code](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys#working-with-wallets-in-code). ## What CIP-30 gives you Once a user grants access, the wallet API lets you: - Read the user's **addresses** (used, unused, change, and reward/stake address) - List the wallet's **UTXOs** and **balance** - Request a **transaction signature** (the user approves in their wallet) - Request a **data signature** ([CIP-8](https://cips.cardano.org/cip/CIP-0008)) to prove ownership, the basis of [sign-in with wallet](/docs/developers/curriculum/dapps/wallet-authentication) Most Cardano browser wallets implement CIP-30, and hardware wallets work through them via a browser extension. The extension talks to the device, and your code is identical either way. For the current set of wallets, see [cardano.org/apps](https://cardano.org/apps). ## Connect Pick your SDK. Both wrap the raw CIP-30 API; the choice follows [your tools](/docs/developers/curriculum/start-building/choose-your-tools). ```typescript declare const cardano: any // window.cardano // 1. Discover installed CIP-30 wallets const available = Object.keys(cardano).filter((k) => cardano[k]?.enable) // 2. User picks one; request access (prompts the user) const walletApi = await cardano["eternl"].enable() // 3. Wrap it in a signing client const client = Client.make(mainnet).withCip30(walletApi) // 4. Read the user's address const address = Address.toBech32(await client.address()) console.log("Connected:", address) ``` `.withCip30()` gives you signing capability without provider-backed submission on its own. Frontend flows still rely on a backend (or a provider-backed client) to broadcast the signed transaction. That's the architecture below. ```typescript // 1. Discover installed CIP-30 wallets const wallets = MeshCardanoBrowserWallet.getInstalledWallets() // 2. User picks one; request access (prompts the user) const wallet = await MeshCardanoBrowserWallet.enable("eternl") // 3. Read state (Mesh-format helpers; base getUtxos()/getChangeAddress() return CBOR/hex) const changeAddress = await wallet.getChangeAddressBech32() const balance = await wallet.getBalanceMesh() const utxos = await wallet.getUtxosMesh() ``` In React, Mesh also ships a ready-made `` connect button and a `useWallet` hook; see [Mesh React](https://meshjs.dev/react). ## Frontend signs, backend builds and submits The most important architectural rule for dApps: **the frontend should only sign**. Build and submit transactions on a backend that holds the provider connection, using a read-only view of the user's address. This keeps provider keys off the client and gives you one place to validate what you're asking users to sign. When that backend's build logic is packaged as a reusable service behind a standard interface rather than wired into one app, you have a [headless dApp](/docs/developers/curriculum/start-building/transaction-building#headless-dapps). ```mermaid flowchart LR FE["Frontend (CIP-30 wallet)"] -->|"user address"| BE["Backend (provider)"] BE -->|"unsigned tx CBOR"| FE FE -->|"user approves -> witness"| FE2["Merge witness"] FE2 -->|"signed tx CBOR"| BE BE -->|"submit via provider"| CHAIN["Cardano"] ``` 1. Frontend connects the wallet (above) and sends the **user's address** to your backend. 2. Backend builds the transaction (it has the provider) and returns the **unsigned CBOR**. 3. Frontend calls `signTx` (the wallet prompts the user) and merges the witness into the transaction. 4. Frontend hands the **signed CBOR** back to the backend, which submits it through the provider. The frontend half, end to end: ```typescript declare const cardano: any async function signOnFrontend(unsignedTxCbor: string) { // Connect (signing only, no provider on the client) const walletApi = await cardano.eternl.enable() const client = Client.make(mainnet).withCip30(walletApi) // User approves; the wallet returns just its witness set const witnessSet = await client.signTx(unsignedTxCbor) // Merge the witness into the unsigned transaction const signedTxCbor = Transaction.addVKeyWitnessesHex( unsignedTxCbor, TransactionWitnessSet.toCBORHex(witnessSet) ) // Hand the signed CBOR back to the backend to submit through its provider const { txHash } = await fetch("/api/submit-tx", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ signedTxCbor }) }).then((r) => r.json()) as { txHash: string } return txHash } ``` ```typescript async function signOnFrontend(unsignedTxCbor: string) { // Connect (signing only) const wallet = await MeshCardanoBrowserWallet.enable("eternl") // User approves; signTxReturnFullTx merges the witness and returns the full signed tx // (pass true as the second argument for partial / multi-sig signing) const signedTxCbor = await wallet.signTxReturnFullTx(unsignedTxCbor, false) // Hand the signed CBOR back to the backend to submit through its provider const { txHash } = await fetch("/api/submit-tx", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ signedTxCbor }) }).then((r) => r.json()) as { txHash: string } return txHash } ``` For the backend (building and submitting with a provider), see [your first transaction](/docs/developers/curriculum/start-building/your-first-transaction) and [lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend). :::tip Wallet UX Always show a wallet-selection UI, handle user rejection gracefully, and display transaction details before requesting a signature. Cache the user's wallet choice, but never auto-connect without an explicit user action, and never cache signatures. ::: ## Handling errors and edge cases `enable()` rejects with a CIP-30 error code you should handle: ```typescript declare const cardano: any async function connect(walletName: string) { if (!cardano[walletName]) throw new Error(`${walletName} not installed`) try { return await cardano[walletName].enable() } catch (error: any) { if (error.code === 2) console.error("User rejected the connection") else if (error.code === 3) console.error("Account not found") else console.error("Connection failed:", error) throw error } } ``` ## Derivation paths Wallets and hardware devices follow Cardano's [BIP-32 / CIP-1852](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys) derivation paths. You rarely set these yourself (the wallet manages them) but it helps to recognize the shape: | Path | Account | Role | Use | |---|---|---|---| | `m/1852'/1815'/0'/0/0` | 0 | external | First payment address | | `m/1852'/1815'/0'/0/1` | 0 | external | Second address, same account | | `m/1852'/1815'/1'/0/0` | 1 | external | Second account, first address | | `m/1852'/1815'/0'/2/0` | 0 | staking | Staking key | `1852'` = Cardano purpose, `1815'` = ADA coin type, then account / role (`0` external, `1` change, `2` staking) / index. ## Governance: CIP-95 The core CIP-30 API has no governance methods. [CIP-95](https://cips.cardano.org/cip/CIP-0095) adds them as an **optional extension**, which you request when you connect: ```typescript const api = await window.cardano.eternl.enable({ extensions: [{ cip: 95 }] }) if (!api.cip95) { // Connected, but this wallet speaks core CIP-30 only } ``` A wallet without CIP-95 still connects; it just returns no `cip95` namespace. Feature-detect before you put governance actions in front of a user, because plenty of wallets implement the core only. The extension adds `getPubDRepKey()` for the user's DRep key, and `getRegisteredPubStakeKeys()` / `getUnregisteredPubStakeKeys()` for their stake keys. `signTx` and `signData` are extended rather than namespaced, so existing calls keep working and gain the ability to witness Conway certificates and DRep credentials. For what to do with them, see [Governance operations](/docs/developers/curriculum/staking-governance/governance-operations#browser-wallet-apis-cip-95). ## No browser extension? Wallet as a Service Not every user has a browser wallet installed. **Wallet-as-a-Service (WaaS)** lets users create a non-custodial wallet via social login, removing the install step entirely (keys are split with Shamir's Secret Sharing and reconstructed only on the user's device at signing time). See [UTXOS Web3 Services](/docs/developers/curriculum/dapps/wallet-authentication#hosted-sign-in-as-a-service), which also supports [transaction sponsorship](https://docs.utxos.dev/sponsor) so users can transact without holding ADA for fees first. ## Framework integration (React & Svelte) Connecting a wallet in a frontend is the same CIP-30 flow shown above; a framework just needs somewhere to hold the connection state and a button to trigger it. How much you hand-roll depends on the SDK: **Mesh** ships ready-made React and Svelte packages (a connect button, hooks, reactive state), while **Evolution** has no UI package and stays framework-agnostic, so you wire the raw CIP-30 flow from [Connect](#connect) into your own hook, store, or context. The connect button is also its own concern, separable from whichever SDK builds your transactions. [`cardano-connect-with-wallet`](https://github.com/cardano-foundation/cardano-connect-with-wallet) is one option for that, worth knowing about if you are not on React or you need mobile wallet support; the Vite [template](/templates) pairs it with Evolution. [Builder Tools](/tools/?tags=wallet) lists the others. ### React Wrap your app once in `` and import the stylesheet, then drop the pre-built button in: ```tsx // app root (pages/_app.tsx or app/layout.tsx) export default function App({ Component, pageProps }) { return ( ) } ``` ```tsx // persist remembers the wallet choice and auto-connects on return; // onConnected fires once connected; label and isDark style the button {}} /> ``` `` renders the wallet-selection modal and connection flow for you. From any component under the provider, the hooks read live wallet state: | Hook | Returns | |---|---| | `useWallet()` | `{ wallet, connected, connecting, name, state, connect, disconnect, error }`, the instance plus connect/disconnect and a `"NOT_CONNECTED" \| "CONNECTING" \| "CONNECTED"` state | | `useWalletList()` | installed CIP-30 wallets as `{ name, icon, version }[]`, for a custom selector | | `useAddress()` | the connected bech32 address (`accountId` arg, default `0`) | | `useAssets()` | every asset across the wallet's UTXOs as `{ unit, quantity }[]` | | `useLovelace()` | the ADA balance in lovelace, as a string | | `useNetwork()` | network ID: `0` = testnet, `1` = mainnet | ### Svelte Mesh ships the same `` from `@meshsdk/svelte` (Svelte 5). Instead of hooks, you read the reactive `BrowserWalletState` runes, accessed directly inside an `$effect` so reactivity isn't lost: ```svelte {#if BrowserWalletState.connected} Connected to {BrowserWalletState.name} {:else} {/if} ``` `BrowserWalletState` exposes `wallet`, `connected`, `name`, and `connecting`. Read its properties directly (don't destructure) or you lose reactivity. Without these packages (Evolution, or a framework Mesh doesn't ship for) the concept is unchanged and the code is short: call `cardano[name].enable()` on a button click, stash the returned wallet API or `client` in a `useState`/`useContext` (React) or a store (Svelte), and read from it. Same CIP-30 surface, just without the pre-built widget. ## Building for the browser Mesh uses Node built-ins (`Buffer`, `crypto`, `stream`) that modern bundlers no longer polyfill, so your first browser build can fail. This trips up most people on the first build; it is not a wallet bug. Under Next.js / Webpack 5, two fixes are needed (Vite / SvelteKit use the equivalent `rollup-plugin-polyfill-node`): - **Polyfill the Node built-ins.** Add [`node-polyfill-webpack-plugin`](https://www.npmjs.com/package/node-polyfill-webpack-plugin) in `next.config`, and also strip the `node:` scheme with a `NormalModuleReplacementPlugin` over `/^node:/`. The plugin alone does not cover `node:`-prefixed imports, so without the strip the build still fails with `UnhandledSchemeError: ... node:buffer`. - **Pin a working libsodium.** A current Mesh release transitively pulls `libsodium-wrappers-sumo@0.7.x`, whose ESM build is broken, so `next build` fails with `Can't resolve './libsodium-sumo.mjs'`. Add `"overrides": { "libsodium-wrappers-sumo": "^0.8.4" }` to your `package.json`. This is a temporary workaround until Mesh ships the upstream fix (`@cardano-sdk/crypto@0.4.6+` already pins the corrected libsodium). The [mesh-nextjs](https://github.com/cardano-foundation/developer-portal/tree/staging/examples/templates/mesh-nextjs) template carries this whole configuration already; copy its `next.config.ts` and the `overrides` block. Because it tracks upstream releases rather than pinned versions, check the versions it resolves against what you get. ## Next steps - [Sign in with wallet](/docs/developers/curriculum/dapps/wallet-authentication): passwordless authentication with CIP-8 message signing - [Keys & Wallets](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys): the key model behind wallets, and creating wallets in backend code - [Detect incoming payments](/docs/developers/curriculum/dapps/listen-for-payments): confirm ADA payments to an address - [DeFi on Cardano](/docs/developers/curriculum/dapps/defi): what users do once they're connected --- ## DeFi on Cardano Decentralized finance (DeFi) replaces traditional financial intermediaries with smart contract protocols, enabling permissionless trading, lending, and yield generation directly on-chain. For web2 developers, DeFi introduces a paradigm where financial logic lives on-chain, composable like microservices but trustless and permissionless. This page covers the core DeFi primitives and, more importantly, the specific design challenges and solutions that arise when you build them on Cardano's [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo). The mechanics differ enough from Ethereum that copying an EVM design rarely works directly. To see what is already running, browse Cardano's live DeFi apps at [cardano.org/apps](https://cardano.org/apps). If you build web services, the primitives map onto familiar ones: - **DEXes are stock-exchange matching engines**, except the matching logic is public, anyone can be a market maker, and there's no broker between you and the market. - **Liquidity pools are connection pools.** A connection pool keeps pre-established DB connections many requests share; a liquidity pool keeps reserves many traders swap against. Size it wrong and you get congestion (high slippage) or underutilization (low LP returns). - **Oracles are API aggregators.** Like querying five price APIs, discarding outliers, and taking the median, but solved for trustlessness. - **Order batching is batch processing in a message queue.** A consumer (batcher) collects messages (orders), processes them in bulk, and writes results back: SQS + Lambda, with atomicity. - **Impermanent loss is the cost of a cache under write-heavy load.** You pre-allocate liquidity to serve trades efficiently, but if prices move fast, rebalancing costs exceed the benefit. - **Composability is Unix pipes**: `grep | sort | uniq`, except every stage either fully succeeds or fully rolls back. ## The DeFi landscape DeFi protocols replace intermediaries (banks, brokerages, clearinghouses) with deterministic smart contracts. Each intermediary removed eliminates a fee, reduces latency, and removes a trust requirement. The ecosystem spans several categories: - **Decentralized exchanges (DEXes)**: trade tokens without a centralized order book - **Lending and borrowing**: supply assets to earn yield; borrow against collateral - **Stablecoins**: tokens pegged to fiat through algorithmic or collateral-backed mechanisms - **Yield aggregators**: automatically optimize returns across protocols - **Synthetic assets**: on-chain representations of real-world assets - **Insurance**: decentralized coverage against smart contract failures On Cardano this spans DEXes, lending and borrowing protocols, stablecoins, and yield optimizers. Each works within the constraints and advantages of eUTXO, which leads to distinctive architectural patterns. :::tip Where Cardano DeFi stands For a snapshot of which primitives Cardano already has, partly has, and is still missing, see the [Cardano DeFi map](https://cardanodefi.space/). DeFi composes, so the gaps are open opportunities: ship a missing piece and it snaps into everything already there. ::: ## Decentralized exchanges (DEXes) A DEX lets users swap one token for another through smart contracts, without a centralized intermediary holding custody. Unlike Coinbase or Binance, you never lose custody during a trade. ### Order books vs AMMs Traditional exchanges use an **order book**: a structure that matches buy and sell orders at specific prices. ```text Traditional Order Book: +------------------------------------------+ | SELL ORDERS (Asks) | | Sell 100 ADA @ 0.52 | | Sell 250 ADA @ 0.51 | |-------------- SPREAD --------------------| | Buy 300 ADA @ 0.50 | | Buy 150 ADA @ 0.49 | | BUY ORDERS (Bids) | +------------------------------------------+ ``` On-chain order books are expensive because every placement, cancellation, and modification is a transaction. Some Cardano DEXes do implement on-chain order books, representing each order as a distinct UTXO. But the dominant model in DeFi is the **Automated Market Maker (AMM)**. ### How AMMs work An AMM replaces the order book with a formula that prices assets from the ratio of reserves in a **liquidity pool**. Instead of matching individual buyers and sellers, everyone trades against the pool. The most common formula is the **constant product formula**: ```text x * y = k x = quantity of Token A in the pool y = quantity of Token B in the pool k = a constant (must remain the same after every trade) ``` A concrete example. A pool holds 10,000 ADA and 5,000 USDx, so `k = 50,000,000`. A trader buys USDx with 1,000 ADA: ```text New ADA in pool: 10,000 + 1,000 = 11,000 New USDx in pool: k / new_x = 50,000,000 / 11,000 = 4,545.45 USDx received: 5,000 - 4,545.45 = 454.55 USDx Effective price: 1,000 ADA / 454.55 USDx = 2.20 ADA per USDx ``` The trader received ~454.55 USDx instead of the 500 expected at the initial 2 ADA/USDx rate. This difference is **slippage**, and it grows with larger trades relative to pool size: the curve moves the price more dramatically as you drain one side. ```mermaid graph LR A[User deposits 1,000 ADA] --> B[AMM Pool\n11,000 ADA / 4,545 USDx] B --> C[User receives 454.55 USDx] B --> D[k remains 50,000,000] style A fill:#4CAF50,color:#fff style C fill:#2196F3,color:#fff style D fill:#FF9800,color:#fff ``` ### Other AMM formulas The constant product formula is not the only option: - **Constant sum (x + y = k)**: zero slippage, but can be fully drained of one asset. Rare in practice. - **StableSwap (Curve)**: a hybrid optimized for assets that trade near 1:1 (stablecoin pairs); low slippage for balanced trades. - **Concentrated liquidity**: LPs specify price ranges, concentrating capital where it's most useful. High capital efficiency, more complexity. On Cardano, DEXes typically use a constant product AMM, often with a stableswap variant for stable pairs. ## Liquidity pools and providers Liquidity pools hold paired token reserves that traders swap against. **Liquidity providers (LPs)** deposit equal values of both tokens and receive **LP tokens** representing their share. Trading fees accrue to the pool, increasing each LP token's value over time. ```mermaid graph TD LP[Liquidity Provider] -->|Deposits ADA + USDx| Pool[Liquidity Pool\nADA / USDx] Pool -->|Issues| LPT[LP Tokens] LPT --> LP Trader[Trader] -->|Swaps + pays 0.3% fee| Pool Pool -->|Returns swapped token| Trader Pool -->|Fees accumulate| Pool style LP fill:#4CAF50,color:#fff style Trader fill:#2196F3,color:#fff style Pool fill:#FF9800,color:#fff style LPT fill:#9C27B0,color:#fff ``` When an LP withdraws, they burn their LP tokens for a proportional share of the pool, which now includes accumulated fees. That fee accrual is how LPs earn yield. (LP tokens are ordinary [native tokens](/docs/developers/curriculum/native-tokens/overview), minted by the pool's policy.) ### Impermanent loss Impermanent loss (IL) is the difference in value between holding tokens in an AMM pool versus simply holding them in a wallet. When the price ratio of pooled assets diverges from the deposit ratio, the AMM's constant rebalancing leaves LPs holding less of the appreciating asset than they would have by holding outright. ```text Initial deposit: 1,000 ADA + 500 USDx (at 2 ADA per USDx) If held (no LP): 1,000 ADA (now worth 2x) + 500 USDx = 3,000 equivalent As LP after ADA doubles: ~707 ADA + ~707 USDx = ~2,828 equivalent Impermanent loss: ~5.7% ``` It's "impermanent" because if the price returns to the original ratio, the loss disappears; it only becomes permanent when the LP withdraws at a different ratio. Trading fees can offset IL, but in volatile periods IL can exceed fee income. Think of it as a "rebalancing cost": the AMM constantly sells the appreciating asset and buys the depreciating one. ## Lending and borrowing Lending is DeFi's second pillar after DEXes: lenders supply assets to earn interest, borrowers lock collateral to take liquidity without selling what they hold. On Cardano there is no shared mutable "lending market" contract; a lending pool, a loan request, and an active loan are each UTXOs, and every state change is a transaction the validators check. Each of those UTXOs has to be identified by a unique NFT the protocol minted, because anyone can create a UTXO at a script address: without that check, a forged pool or loan is injectable. The mechanics below are protocol-independent. ### Overcollateralization and loan-to-value On a public chain nobody can be pursued into repaying, so loans are **overcollateralized**: the borrower locks collateral worth more than the debt. Two ratios govern a position, both expressed as **loan-to-value (LTV)**, the debt divided by the collateral's current value: - The **borrow LTV** caps what can be borrowed at origination: at 60%, collateral worth 1,000 ADA supports at most 600 ADA worth of principal. - The **liquidation LTV** is the health threshold: if accrued interest grows the debt, or the collateral's price falls, and the ratio crosses it (say 80%), the position becomes liquidatable. The gap between the two is the safety margin. Debt and collateral are usually different assets, so the ratio is computed by pricing both through [oracle feeds](/docs/developers/curriculum/dapps/oracles/overview) against a common denominator, on Cardano naturally lovelace: ```text Collateral: 1,000 ADA = 1,000,000,000 lovelace Debt: 450 USDx, feed: 1 USDx = 1,600,000 = 720,000,000 lovelace LTV = 720 / 1,000 = 72% above the 60% borrow cap (no further borrowing), below the 80% liquidation threshold (safe, for now) ``` ### Liquidation When a position crosses the liquidation threshold or misses a repayment deadline, someone must be able to close it, and in eUTXO that someone is anyone: liquidation is **permissionless**, incentivized by a liquidation fee, and executed in practice by competing bots watching the chain, the same open-market role batchers play on a DEX. What happens to the collateral is a design choice: - **Direct claim.** The lender or liquidator takes the collateral outright. Simple, but any collateral value above the debt is lost to the borrower. - **Dutch auction.** The collateral is offered at a price that starts above the debt and decays in steps over time until a buyer accepts; if the sale clears above the debt, the surplus returns to the borrower. On-chain, the decaying price is a pure function of time computed against the transaction's validity-interval **lower bound**: the earliest moment the transaction could be valid yields the fewest elapsed decay steps and so the highest price, so a buyer can never claim a discount that has not provably elapsed. - **DEX / market liquidation.** Rather than handing the collateral to a claimant or running a bespoke auction, the seized collateral is sold on an existing DEX into the borrowed asset, the debt is repaid from the proceeds, and any surplus returns to the borrower. It needs no auction machinery of its own, but it inherits the DEX's liquidity: a thin pool means heavy slippage, and the sale clears at whatever price the pool gives when the swap is batched, not a price the protocol sets. ### Interest Three interest shapes cover most on-chain lending: - **Even split**: total repayment is principal plus a flat rate, divided across installments: each installment is `P * (1 + r) / n`. - **Amortized**: the classic mortgage formula, each installment `P * i * (1+i)^n / ((1+i)^n - 1)` with per-installment rate `i`; early installments are interest-heavy, later ones repay mostly principal. - **Interest-only / perpetual**: no final deadline; each period the borrower pays the interest accrued on the outstanding principal, and reduces the principal whenever they choose. Which shapes a protocol offers matters less than one property: the validator must be able to recompute the amount owed **deterministically from on-chain data**, the terms in the datum plus the transaction's [validity interval](/docs/developers/curriculum/fundamentals/core-concepts/transactions#validity-intervals-and-time), with no external clock. ### Money math on-chain None of this uses floating point. Plutus has none, deliberately: consensus needs bit-identical results on every node, so on-chain financial math is integer math. - **Scaled integers**: rates are integers against a fixed denominator, a 4.5% rate stored as `450` over `10000`. - **Rationals**: Aiken's `aiken/math/rational` carries numerator and denominator separately through multi-step formulas (the same tool the [Pyth integration](/docs/developers/curriculum/dapps/oracles/pyth) uses for price scaling), so precision is lost only once, at the final comparison. - **Round in one direction, consistently**: round *up* what the user owes, round *down* what the user is owed. A rounding rule that ever favors the caller becomes a crumb-collecting exploit at scale; one that always favors the protocol is merely conservative. ## Oracles: DeFi's data dependency DeFi protocols need real-world data: prices for swaps and liquidations, rates for lending. Smart contracts can't query APIs, so **oracles** post verified data on-chain as datums that contracts read via reference inputs. A compromised oracle can make a lending protocol liquidate incorrectly or a stablecoin lose its peg, which makes oracles one of DeFi's most critical and most vulnerable components. Cardano's reference inputs ([CIP-31](https://cips.cardano.org/cip/CIP-31)) let many transactions read the same oracle UTXO in parallel without contention, a structural advantage for DeFi. The full picture (the oracle problem, multi-oracle validation, and integrating Pyth, the recommended oracle) is on the **[Oracles](/docs/developers/curriculum/dapps/oracles/overview)** page. ## The eUTXO design challenge Cardano's eUTXO model requires DeFi protocols to solve concurrency differently than Ethereum's account model, because a single UTXO can be consumed by only one transaction per block. ### The concurrency problem In an account model, a contract holds a single mutable state and the chain resolves the ordering of many users in a block. In eUTXO, a UTXO can be spent once. If a liquidity pool is one UTXO, only one user can interact with it per block. ```text Block N: User A wants to swap ADA to USDx --+ User B wants to swap ADA to USDx --+--> Only ONE can spend the pool UTXO User C wants to swap USDx to ADA --+ Result: two transactions fail with "UTXO already spent" ``` This is not a bug; it's a fundamental property of the model. Two patterns address it. ### Order batching The most common pattern. Instead of interacting with the pool directly, users submit **order UTXOs** expressing intent ("swap 100 ADA for USDx, max 2% slippage"). A **batcher** (or scooper) collects many orders and executes them against the pool in a single transaction. ```mermaid graph TD U1[User 1: Swap ADA to USDx] --> B[Batcher / Scooper] U2[User 2: Swap USDx to ADA] --> B U3[User 3: Swap ADA to USDx] --> B B --> TX[Single Transaction] TX --> Pool[Updated Pool UTXO] TX --> R1[Result: USDx to User 1] TX --> R2[Result: ADA to User 2] TX --> R3[Result: USDx to User 3] style B fill:#FF9800,color:#fff style TX fill:#4CAF50,color:#fff style Pool fill:#2196F3,color:#fff ``` Orders process atomically, contention drops, and the batcher can optimize execution order. The trade-offs: latency (users wait for the batcher) and trust in the batcher (though the validator enforces correctness). Most Cardano DEXes decentralize the role by letting anyone run a batcher for fees. Batchers lean on the [UTXO indexer pattern](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/utxo-indexers) to map many order inputs to outputs cheaply on-chain. A batcher is a continuously running off-chain service, and it takes on the contention it spares users. It keeps its own view of the pending orders. Because the chain moves underneath it, before assembling a batch it re-checks that each queued order UTxO still exists, drops any that a user has since cancelled or spent, and saves enough state to resume after a crash. When one of its transactions loses the race for the shared pool UTxO, it [rebuilds from fresh chain state and retries](/docs/developers/curriculum/start-building/transaction-building#resilient-submission-retry-safe). It also chooses which orders to include and in what order, following a matching policy (first-come-first-served, largest-first, or whatever maximizes fees). That policy is where a batcher's fairness lives. Swaps are only one kind of order. Production DEXes run deposits and withdrawals through the same queue, and some add richer types: - **Donations**: assets paid into the pool with nothing asked back, spread pro-rata across all LPs. An on-chain incentive primitive other protocols can drive automatically. - **Signed-intent orders**: the order is posted before its exact terms are set, and an executor fills them in later. The owner's signature covers the whole intent and is bound to that specific order UTXO and a validity window, so it cannot be replayed, the same bind-to-context rule used everywhere else. - **Price-record orders**: mint a snapshot of the pool price that other protocols can read without touching the pool. The snapshot is taken at the end of a batch, so sandwiching it only hands an arbitrage opportunity to the next batch. ### Pool sharding Another approach splits the pool across multiple UTXOs, each holding a portion of the liquidity, so several transactions execute concurrently against different shards. ```text Instead of one pool UTXO with 100,000 ADA / 50,000 USDx: +----------------+ +----------------+ +----------------+ | Pool Shard 1 | | Pool Shard 2 | | Pool Shard 3 | | 33,333 ADA | | 33,333 ADA | | 33,334 ADA | | 16,667 USDx | | 16,667 USDx | | 16,666 USDx | +----------------+ +----------------+ +----------------+ Three users can now swap concurrently against different shards. ``` The trade-off is keeping pricing consistent across shards and higher slippage per shard (each holds less liquidity). The flip side of contention is covered as a vulnerability in [UTXO contention](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#utxo-contention). ### Transaction chaining A third approach removes the wait rather than the contention. Because validation is [deterministic](/docs/developers/curriculum/smart-contracts/overview#deterministic-validation), a transaction's id, and so its outputs, are known the moment it is built, before it is confirmed. That lets each interaction build on the previous one's not-yet-settled output, forming a chain ordered by its on-chain input dependencies instead of by an off-chain batcher. Protocols use it to keep a batching pipeline moving without waiting on confirmations, and wallets use it to let a user spend change still sitting in the mempool. The concept and its trade-offs are in [transaction chaining](/docs/developers/curriculum/production/transaction-chaining); the build-side code is in [chaining transactions](/docs/developers/curriculum/start-building/transaction-building#chaining-transactions). ### Batcher-free pools Chaining applied to the pool itself removes the batcher rather than assisting it. There are no order UTXOs and no scooper: each user builds a transaction that spends the pool UTXO directly, performs the swap, and produces the next pool UTXO, and the following user chains onto that output before it confirms. Trades settle as fast as they can be submitted, with no batch cycle to wait on and no operator choosing the sequence. The cost moves rather than disappears: the app now has to track the pool's unconfirmed head and serve it to whoever swaps next, and because many users race to extend the same chain, only one wins each slot while the losers rebuild on fresh state and retry, the [same lost-race handling](/docs/developers/curriculum/start-building/transaction-building#resilient-submission-retry-safe) a batcher runs internally, now pushed out to every client. A pool that takes swaps directly also shapes its validator differently. The script guards only the pool: one pool UTXO in, one out, the [constant product](#how-amms-work) preserved after fees, its identifying token intact. It deliberately does not check that the swapper paid themselves the correct amount out, because a user who shortchanges their own output only harms themselves and the pool cannot be drained while its own invariant holds. Constraining just what the script is responsible for keeps validation cheap, but it leaves correct compensation to whoever builds the transaction, and it is the exact opening a [double satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction) attack aims at, so a direct-interaction pool must be explicit about how many non-pool inputs and outputs it will accept. ### Determinism: the compensating advantage Concurrency is a challenge, but eUTXO's [determinism](/docs/developers/curriculum/smart-contracts/overview#deterministic-validation) is a powerful advantage for DeFi. On Ethereum a transaction can pass local simulation but fail on-chain because another transaction changed state first (the basis of MEV). On Cardano, if a transaction validates locally it produces the exact same result on-chain, assuming its inputs haven't been spent. This also makes Cardano structurally resistant to **front-running**: validators can't insert their own transactions ahead of yours to manipulate price, because every transaction specifies its exact inputs and outputs. ## Composability: money Legos Composability lets you combine multiple DeFi protocols in a single atomic transaction. On Cardano this works by referencing multiple script inputs and outputs in one transaction. A single transaction could: 1. Withdraw collateral from a lending protocol 2. Swap that collateral on a DEX 3. Provide liquidity to a different pool 4. Mint an NFT receipt All atomically: if any step fails, the entire transaction is invalid and no state changes. This is what makes DeFi protocols behave like stackable building blocks. ### DeFi Kernel: a shared standard Composability works best when protocols agree on a common substrate. [DeFi Kernel](https://defikernel.org/) is an open community standard for exactly that: a single order book the size of the whole chain, where any participant can write an order and any participant can fill it, with no batcher, administrator, or permission required. It is not a DEX or a lending protocol but the neutral layer those can sit on, the way the Linux kernel is for operating systems, so they share one liquidity pool instead of each bootstrapping their own. A contract is DeFi-Kernel-compatible if it satisfies three properties, with no committee, whitelist, or token gate: - **Permissionless.** Users sign and submit their own transactions directly to a node. No off-chain operator sits between intent and settlement, so no one can censor a fill or front-run a maker. - **Composable.** Every compatible contract publishes its datum schema in the open, so any other contract or wallet can read it and chain transactions across protocols in one signature. - **Discoverable.** Orders must be findable by anyone running a node, through beacon tokens, deterministic addresses, or another on-chain tagging mechanism. The UTxO set itself is the order book, with no central indexer to trust. Conform to the rules and your contract inherits the ecosystem's liquidity, users, and tooling on day one instead of starting from zero. The standard, the brief, and a live registry of compatible contracts (DEX, lending, options, and synthetics) live at [defikernel.org](https://defikernel.org/); to make a contract discoverable, open a pull request against the [DeFi Kernel Registry](https://github.com/DeFiKernel-Cardano/DeFi-Kernel-Registry-for-Cardano) with your script hashes and datum schema. ## Why flash loans are absent Flash loans on Ethereum let users borrow with no collateral as long as they repay within the same transaction. Cardano's eUTXO model prevents this because each transaction must balance its inputs and outputs **at construction time**. You cannot "borrow" assets mid-transaction. The EVM can check repayment at the end of sequential execution; a Cardano transaction must be fully defined before submission. This is a security advantage: flash loans have been used to manipulate prices and drain DeFi protocols in a single Ethereum transaction. Cardano's model makes those attack vectors much harder. eUTXO still lets you compose debt operations in a single transaction; what it removes is the *uncollateralized* loan. **Refinancing** is the clear example: you can repay an existing loan with liquidity from a new collateralized loan in one transaction, because both loans are ordinary UTXO flows and the transaction balances at construction time like any other. The only thing ruled out is borrowing with nothing at stake on a promise to repay inside the same transaction. ## Yield farming and liquidity mining Yield farming strategically deploys capital across protocols to maximize returns; liquidity mining specifically distributes governance tokens to LPs as extra incentive beyond trading fees. On Cardano this includes providing DEX liquidity (earning fees plus extra protocol tokens), lending on lending platforms, staking LP tokens in farms, and liquidity bootstrapping events. Yields are not magic: they come from trading fees (real activity), token emissions (inflationary, may not hold value), and protocol revenue. Understanding the source of a yield is essential to evaluating its risk. ## DeFi application patterns The primitives above (AMMs, oracles, order batching, composability) combine into the common building blocks of a DeFi app. Each of these is language-agnostic and composes the on-chain techniques from [Design Patterns](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview): - **Reward accrual and claiming.** Distribute rewards proportionally to stake or LP share. Snapshots or time-locks stop last-minute gaming, and many claims are settled in batches using the [linked-list fold](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/linked-list) and a [stake validator](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) for transaction-level checks. - **Token vesting.** Lock tokens and release them on a datum-defined schedule (a cliff, then linear release). Unlock conditions are enforced against the transaction's [validity interval](/docs/developers/curriculum/fundamentals/core-concepts/transactions#validity-intervals-and-time), and each partial claim updates the remaining balance in the datum. Guard the claim against [double satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction). - **P2P offers and atomic swaps.** Represent each offer as its own UTXO carrying the maker's terms (offered asset, asked asset, expiry). A taker spends it directly, or an off-chain matcher fills many offers in one transaction, pairing inputs to outputs with [UTXO indexers](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/utxo-indexers). - **Routing and aggregation.** Off-chain routers compute the best path across pools and submit a single transaction the validators check atomically. The on-chain side leans on the same order-batching and indexing patterns, so no centralized frontend has to be trusted. - **Cross-chain bridges.** Lock assets on the source chain and mint wrapped equivalents on the target (burn-to-unlock in reverse), with a multisig guardian set attesting to each transfer. Bridges depend on off-chain infrastructure and trust assumptions beyond a single chain, so treat them as their own design problem. For production-grade, open-source references of these patterns, see [Anastasia Labs' dApp repositories](https://github.com/Anastasia-Labs/production-grade-dapps). ## Key takeaways - **AMMs replace order books** with formulas (like constant product) that price assets from pool reserves, enabling permissionless trading. - **LPs earn fees but face impermanent loss**: a hidden cost when the pooled price ratio diverges from the deposit ratio. - **Oracles bridge the on-chain/off-chain gap** but introduce trust assumptions; reference inputs let many transactions read them in parallel. - **eUTXO requires DeFi-specific patterns** (order batching, pool sharding) for concurrency, but pays you back with determinism and front-running resistance. - **Composability makes protocols interoperable building blocks**, enabling complex operations in single atomic transactions. ## Next steps - [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet): let users interact with your protocol from the browser - [Oracles](/docs/developers/curriculum/dapps/oracles/overview): the price-feed infrastructure DeFi depends on - [Smart contract security](/docs/developers/curriculum/smart-contracts/security): the attack classes (double satisfaction, contention) that hit DeFi hardest - [Contract library](/templates/contracts): escrow, swap, and production-grade dApp implementations - [Ship to Production](/docs/developers/curriculum/production/overview): infrastructure, reliability, and the scaling patterns a live protocol needs --- ## Gathering Data Before we build the ticker, walk through the APIs it consumes: wallet contents, token prices, NFT floors. :::warning Educational use only The endpoints below are for educational use. Some are not officially supported by their providers. For production, use official (mostly paid) APIs or build your own. ::: ## Checking your wallet You already know several ways to fetch wallet data from earlier in this section: - [Koios](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance) or [Blockfrost](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance) - Workshop 02. - Mesh SDK - [Workshop 03: Build your own API](/docs/developers/curriculum/dapps/iot/input-and-write/02-build-your-own-api). - On-chain explorers like [CardanoScan](https://cardanoscan.io/), [Cexplorer](https://cexplorer.io/), [Adastat](https://adastat.net/), [pool.pm](https://pool.pm/) - paste your address. - Wallet extensions - [Yoroi](https://yoroi-wallet.com/), [Eternl](https://eternl.io/), [Vespr](https://vespr.xyz/), [Begin](https://begin.is/). The next sections add token prices and NFT floors on top. ## Fetching from MinSwap [MinSwap](https://minswap.org/) is a DEX on Cardano. It exposes an endpoint to fetch all tokens (and prices) for a wallet address: ``` https://monorepo-mainnet-prod.minswap.org/v1/portfolio/tokens?address=[WALLETADDRESS]&only_minswap=true&filter_small_value=false ``` Replace `[WALLETADDRESS]` with your wallet address. The response is JSON: ```json { "positions": { "nft_positions": [ { "currency_symbol": "f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a", "token_name": "000de14063617264616e6f7468696e6773", "is_verified": true } ], "asset_positions": [ { "asset": { "currency_symbol": "", "token_name": "", "is_verified": true, "metadata": { "decimals": 6, "name": "Cardano", "ticker": "ADA" } }, "price_usd": 0.3864, "amount": 11.37058, "amount_usd": 4.393592112, "pnl_24h_usd": -0.3466696040696063, "pnl_24h_percent": -7.8903456495828275 }, { "asset": { "currency_symbol": "3d77d63dfa6033be98021417e08e3368cc80e67f8d7afa196aaa0b39", "token_name": "53746172636820546f6b656e", "is_verified": true, "metadata": { "name": "STRCH", "url": "https://starch.one", "ticker": "STRCH", "decimals": 0, "description": "" } }, "price_usd": 3.0215906268916395e-9, "amount": 114081, "amount_usd": 0.00034470608030642515, "pnl_24h_usd": -0.00005476157689164048, "pnl_24h_percent": -15.88645516289135 } ], "lp_asset_positions": [] } } ``` You get NFTs in the wallet, tokens, USD price per token, amounts, 24h USD change, and 24h percent change - enough to render token rows on the ticker. ## Fetching from JPG.store (no longer) [JPG.store](https://www.jpg.store/) is an NFT marketplace on Cardano. Unfortunately they've locked down their API since this workshop was written - no public or paid endpoints are available anymore. ## Cexplorer.io for NFT floors [Cexplorer.io](https://cexplorer.io/) saves the day with a free tier. Sign up, click your wallet address (top right) to reach your profile, click the **API** tab, and create a new project to get an API key. Cexplorer also ships a Node.js SDK at [github.com/vellum-labs/cexplorer-api](https://github.com/vellum-labs/cexplorer-api/tree/main/packages/cexplorer-api). We'll use it to fetch floor prices for NFT collections via: ``` https://api-mainnet-stage.cexplorer.io/v1/policy/detail?id=[POLICYID] ``` Replace `[POLICYID]` with the collection's policy ID. Example response: ```json { "license": "private/dev usage only.", "code": 200, "data": { "id": "f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a", "policy": { "mintc": 320782, "stats": null, "script": { "json": { "type": "sig", "keyHash": "4da965a049dfd15ed1ee19fba6e2974a0b79fc416dd1796a1f97f5e1" }, "type": "timelock" }, "quantity": 310506, "last_mint": "2025-12-01T08:58:06", "first_mint": "2021-12-14T16:00:24" }, "collection": { "url": "adahandle", "name": "ADA Handle", "stats": { "floor": 5000000, "owners": 66766, "volume": 612934424304312, "royalties": { "rate": 0.02, "address": "addr1qye59j5vquaprdmxf0gs2y3n20necqg3dnzxty23x07u7awkchm0w43pg2uczh4vcvdr59teny2996rq4tmq2umjyqvqlhm7d2" } } } }, "tokens": 4, "ex": 0.0174, "debug": false } ``` The ticker uses the collection name, owners, volume, and floor (in lovelace). :::info API key required Include your API key in the `api-key` request header. Without it you'll get 401 Unauthorized or empty data. The free tier has rate limits. ::: ## Other APIs For production tickers, two paid options worth considering: - **[TapTools](https://www.taptools.io/)** - a Cardano analytics platform with detailed token, portfolio, and market data; tiered pricing. [API docs](https://openapi.taptools.io/). - **[Charli3](https://charli3.io/)** - a Cardano oracle / API provider with historical and live token prices, free + paid tiers. [Token API](https://charli3.io/api). ## Next steps You now have all the data the ticker needs. The next lesson assembles it into a multi-screen TFT display. ## Further Resources - [MinSwap](https://minswap.org/) - Cardano DEX. - [Cexplorer.io](https://cexplorer.io/) - explorer + free-tier API. - [JPG.store](https://www.jpg.store/) - NFT marketplace. - [TapTools](https://www.taptools.io/) - analytics platform. - [Charli3](https://charli3.io/) - oracle / token-price API. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/04-cardano-ticker/gathering-data) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-04](https://github.com/CardanoThings/Workshops/tree/main/Workshop-04).* --- ## Building the Ticker Now build the Cardano Ticker: a multi-screen display that rotates through wallet balance, tokens, NFTs, and status, with a stock-market-style scrolling ticker along the bottom. ## What we're building The ticker shows: - Your ADA wallet balance. - All token holdings with prices and 24-hour changes. - NFT collections with floor prices. - System status info. - A scrolling ticker at the bottom with token prices. It rotates between screens every 10 seconds. Data updates periodically - wallet balance every minute, tokens / NFTs every 10 minutes. On startup it connects to WiFi, fetches initial data, shows the first screen. During operation it keeps WiFi alive, updates data on schedule, rotates screens, and animates the bottom ticker. If you've finished the previous workshops, you already know most of this. The project combines: - WiFi connectivity from [Workshop 02 - Fetch Wallet Balance](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance). - Display techniques from [Workshop 02 - Display Data](/docs/developers/curriculum/dapps/iot/read-and-output/02-display-data). - API fetching from [Workshop 02 - Fetch Wallet Balance](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance) and [Workshop 03 - Connect and Read Sensor Data](/docs/developers/curriculum/dapps/iot/input-and-write/01-connect-and-read-sensor-data). :::info Mainnet data This workshop uses mainnet data; the examples use the CardanoThings.io wallet. ::: ## Project structure The CardanoTicker is a multi-file Arduino project: each component has its own `.h` and `.cpp` files. Walk through each one below. Full source: [github.com/CardanoThings/Workshops/tree/main/Workshop-04/examples/CardanoTicker](https://github.com/CardanoThings/Workshops/tree/main/Workshop-04/examples/CardanoTicker). ## Configuration files Before the code, point the project at your wallet and APIs. ### `config.cpp` - your addresses This stores Cardano addresses and API endpoints. Edit it with your own: - **`stakeAddress`** - your stake address (`stake1...`). Used for Koios wallet-balance lookups (you learned about stake addresses in [Workshop 02](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance)). - **`walletAddress`** - your payment address (`addr1...`). Used by MinSwap for tokens and NFTs. - **`cexplorerApiKey`** - your Cexplorer.io API key from the previous lesson. :::info Finding your addresses Both are visible in your wallet (Yoroi, Eternl, Vespr) - the stake address under "Staking" or "Rewards", the payment address as your main receive address. They're also on [CardanoScan](https://cardanoscan.io/) and [Cexplorer](https://cexplorer.io/). ::: `config.cpp`: ```cpp /** * config.cpp - Configuration implementation file * * This file defines the actual values for wallet addresses and API endpoints * that are declared in config.h. Edit these values with your own addresses * and API keys before uploading to your device. */ #include "config.h" // Your Cardano stake address (starts with "stake1..." on mainnet) // Used to fetch your ADA wallet balance from the Koios API // Replace this with your own stake address String stakeAddress = "stake1u8l0y82je0t2wkkpps97rv0q7lf882q0fc24gwjz9nacz0c5gt5k" "3"; // Your Cardano wallet address (starts with "addr1..." on mainnet) // Used to fetch your token and NFT positions from the MinSwap API // Replace this with your own wallet address String walletAddress = "addr1q8xy5cfmccecvvr2z7ns7mzld8qkq73lgwnq7vy3my0s5rl77gw49j7k5advzrqtuxc7p" "a7jww5q7ns42sayyt8msylsx4k2qx"; // Cexplorer API key for accessing NFT floor price data // Get your free API key from: https://cexplorer.io/api // Replace "your-api-key-here" with your actual API key String cexplorerApiKey = "your-api-key-here"; // API endpoint URLs // These point to Cardano blockchain APIs used to fetch wallet data // Koios API endpoint - fetches wallet balance (ADA) // Koios is a Cardano blockchain indexer that provides fast access to blockchain // data const char *koiosApiUrl = "https://api.koios.rest/api/v1/account_info"; // MinSwap API endpoint - fetches token and NFT portfolio data // MinSwap is a decentralized exchange (DEX) that provides portfolio information const char *minswapApiUrl = "https://monorepo-mainnet-prod.minswap.org/v1/portfolio/tokens"; // Cexplorer API endpoint - fetches NFT collection floor prices // Cexplorer provides detailed NFT collection information including floor prices const char *cexplorerApiUrl = "https://api-mainnet-stage.cexplorer.io/v1/policy/detail"; ``` `config.h`: ```cpp /** * config.h - Configuration header file * * This file declares external variables that are defined in config.cpp. * These contain your wallet addresses and API endpoints. * * Important: You need to edit config.cpp with your actual addresses! */ #ifndef CONFIG_H #define CONFIG_H #include // Your Cardano stake address (starts with "stake1..." on mainnet) // This is used to fetch your wallet balance from Koios API extern String stakeAddress; // Your Cardano wallet address (starts with "addr1..." on mainnet) // This is used to fetch your token and NFT positions from MinSwap API extern String walletAddress; // API endpoint URLs // These point to the Cardano blockchain APIs we use to fetch data extern const char *koiosApiUrl; // Koios API - for wallet balance extern const char *minswapApiUrl; // MinSwap API - for tokens and NFTs extern const char *cexplorerApiUrl; // Cexplorer API - for NFT floor prices #endif ``` > Source: [`Workshop-04/examples/CardanoTicker/config.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/config.cpp), [`config.h`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/config.h) ### `secrets.h` - WiFi credentials Stored separately so it can be `.gitignore`d. Copy `secrets.h.example` to `secrets.h` and fill in `WIFI_SSID` / `WIFI_PASSWORD`. ```cpp /** * secrets.h.example - Template file for sensitive configuration values * * This is a template file showing what secrets need to be configured. * To use this file: * 1. Copy this file to secrets.h (secrets.h is in .gitignore and won't be committed) * 2. Fill in your actual WiFi credentials and API keys * 3. Never commit secrets.h to version control! * * IMPORTANT: Keep your secrets.h file private and never share it publicly. */ #ifndef SECRETS_H #define SECRETS_H // Your WiFi network name (SSID) // The name of the WiFi network your device should connect to #define WIFI_SSID "" // Your WiFi network password // The password required to connect to your WiFi network #define WIFI_PASSWORD "" // Cexplorer API key for accessing NFT floor price data // Get your free API key from: https://cexplorer.io/api // This key is used to fetch NFT collection floor prices #define CEXPLORER_API_KEY "" #endif ``` > Source: [`Workshop-04/examples/CardanoTicker/secrets.h.example`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/secrets.h.example) ## WiFi manager and data fetcher The WiFi manager handles connection and auto-reconnect. The data fetcher organises every API call from the previous lesson (Koios + MinSwap + Cexplorer) into one reusable module - fetches periodically, stores results for the screens to read. Both modules use the same techniques as [Workshop 02](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance), just packaged. The data fetcher rate-limits (wallet every 1 minute; tokens / NFTs every 10 minutes) and exposes getters like `getWalletBalance()` and `getToken(i)` that screen files use. `wifi_manager.cpp`: ```cpp /** * wifi_manager.cpp - WiFi connection management implementation * * This file implements WiFi connection management with automatic reconnection. * It stores WiFi credentials and periodically attempts to connect or reconnect * if the connection is lost. */ #include "wifi_manager.h" #include namespace { // Time to wait between reconnection attempts (5 seconds) // Prevents rapid reconnection attempts that could overwhelm the WiFi module const unsigned long WIFI_RETRY_INTERVAL_MS = 5000; // Maximum time to wait for a connection before retrying (12 seconds) // If connection takes longer than this, we assume it failed and retry const unsigned long WIFI_CONNECT_TIMEOUT_MS = 12000; // Stored WiFi credentials (set by wifiManagerSetup) const char *storedSsid = nullptr; const char *storedPassword = nullptr; // Timestamp of the last connection attempt // Used to implement retry intervals and connection timeouts unsigned long lastAttemptMs = 0; /** * Attempt to connect to WiFi * * Disconnects any existing connection, sets WiFi to station mode, * and begins connection with stored credentials. * * @param force If true, attempts connection immediately regardless of retry * interval */ void attemptConnection(bool force) { // Don't attempt connection if SSID is not set or empty if (storedSsid == nullptr || storedSsid[0] == '\0') { return; } const unsigned long now = millis(); // Respect retry interval unless forced (e.g., initial setup) if (!force && (now - lastAttemptMs) < WIFI_RETRY_INTERVAL_MS) { return; } lastAttemptMs = now; Serial.print("WiFi: connecting to "); Serial.println(storedSsid); // Disconnect any existing connection and clear stored credentials WiFi.disconnect(true, true); // Set WiFi to station mode (client mode, not access point) WiFi.mode(WIFI_STA); // Begin connection attempt WiFi.begin(storedSsid, storedPassword); } } // namespace /** * Initialize WiFi manager with credentials * * Stores the WiFi credentials and immediately attempts to connect. * * @param ssid The WiFi network name (SSID) * @param password The WiFi network password */ void wifiManagerSetup(const char *ssid, const char *password) { storedSsid = ssid; storedPassword = password; // Force immediate connection attempt on setup attemptConnection(true); } /** * Monitor and maintain WiFi connection * * Checks connection status and automatically attempts to reconnect * if disconnected. Uses timeout mechanism to detect failed connections. * Should be called repeatedly in the main loop(). */ void wifiManagerLoop() { // If already connected, no action needed if (WiFi.status() == WL_CONNECTED) { return; } const unsigned long now = millis(); // Check if connection attempt has timed out // Also handles case where no attempt has been made yet (lastAttemptMs == 0) const bool timedOut = (now - lastAttemptMs) > WIFI_CONNECT_TIMEOUT_MS || lastAttemptMs == 0; if (timedOut) { // Retry connection (respects retry interval) attemptConnection(false); } } /** * Check if WiFi is currently connected * * @return true if WiFi status is WL_CONNECTED, false otherwise */ bool wifiManagerIsConnected() { return WiFi.status() == WL_CONNECTED; } ``` `wifi_manager.h`: ```cpp /** * wifi_manager.h - Header file for WiFi connection management * * This file declares functions for managing WiFi connectivity on the ESP32. * It handles connection setup, connection monitoring, and provides status * information about the WiFi connection state. */ #ifndef WIFI_MANAGER_H #define WIFI_MANAGER_H #include /** * Initialize WiFi connection * * Sets up WiFi with the provided credentials and attempts to connect. * Call this once in setup() before using other WiFi functions. * * @param ssid The WiFi network name (SSID) * @param password The WiFi network password */ void wifiManagerSetup(const char *ssid, const char *password); /** * Update WiFi connection status * * Monitors the WiFi connection and attempts to reconnect if disconnected. * Call this repeatedly in loop() to maintain connection. */ void wifiManagerLoop(); /** * Check if WiFi is currently connected * * @return true if connected to WiFi, false otherwise */ bool wifiManagerIsConnected(); #endif ``` `data_fetcher.cpp`: ```cpp /** * data_fetcher.cpp - Implementation of blockchain data fetching * * This file contains all the code that talks to Cardano blockchain APIs to * fetch your wallet data. It handles: * - Fetching wallet balance from Koios API * - Fetching token positions from MinSwap API * - Fetching NFT collection data from MinSwap and Cexplorer APIs * - Storing and organizing all this data for display * * Key Concepts: * - HTTP requests: How we talk to APIs over the internet * - JSON parsing: APIs return data in JSON format, we need to extract it * - Rate limiting: We don't fetch too often to avoid hitting API limits */ #include "data_fetcher.h" // Libraries for making HTTP requests and parsing JSON responses #include // Parses JSON data from APIs #include // Makes HTTP requests (GET, POST) to APIs #include // WiFi functionality // Our custom headers #include "config.h" // API URLs and wallet addresses #include "wifi_manager.h" // WiFi connection management // Private namespace - these variables are only accessible within this file namespace { // How often to fetch wallet balance (1 minute = 60,000 milliseconds) // UL = unsigned long (ensures the number is treated as the right type) constexpr unsigned long KOIOS_INTERVAL_MS = 60UL * 1000UL; // How often to fetch token/NFT data (10 minutes = 600,000 milliseconds) // We fetch this less often because it's more data and takes longer constexpr unsigned long PORTFOLIO_INTERVAL_MS = 10UL * 60UL * 1000UL; // Maximum number of NFT policy IDs we can store // Policy ID = unique identifier for an NFT collection // Limited to 8 to match display capacity and API call limits constexpr size_t MAX_POLICY_IDS = 8; // Maximum number of tokens we can store (limited by screen display) constexpr size_t MAX_TOKENS = 8; // Maximum number of NFT collections we can store (limited by screen display) constexpr size_t MAX_NFTS = 8; // Global variables to store fetched data // These persist between function calls (unlike local variables) float walletBalance = 0.0f; // Your ADA balance (in ADA, not Lovelace) int tokenCount = 0; // How many different tokens you own int nftCount = 0; // How many different NFT collections you own // Array to store Policy IDs (one per NFT collection) // We need these to fetch floor prices from Cexplorer API String policyIds[MAX_POLICY_IDS]; int policyIdCount = 0; // How many Policy IDs we've collected // Arrays to store token and NFT data // Arrays are like lists - we can store multiple items TokenInfo tokens[MAX_TOKENS]; // Array of token information NFTInfo nfts[MAX_NFTS]; // Array of NFT collection information // Timestamps to track when we last fetched data // Used to implement rate limiting (don't fetch too often) unsigned long lastKoiosFetch = 0; // When we last fetched wallet balance unsigned long lastPortfolioFetch = 0; // When we last fetched tokens/NFTs // Forward declarations - these functions are defined later in this file // We declare them here so they can be called from other functions void fetchWalletBalance(); // Fetches ADA balance from Koios void fetchMinSwapData(); // Fetches tokens/NFTs from MinSwap void fetchCexplorerData(const String &policyId); // Fetches NFT floor prices } // namespace /** * Initialize the data fetcher * * This function resets all data storage to zero/empty. It's called once at * startup to ensure we start with clean data. * * Think of it like clearing a whiteboard before starting a new lesson. */ void initDataFetcher() { // Reset all counters to zero walletBalance = 0.0f; tokenCount = 0; nftCount = 0; policyIdCount = 0; lastKoiosFetch = 0; lastPortfolioFetch = 0; // Clear all token data arrays // Loop through each position in the array and set it to empty/default values for (size_t i = 0; i < MAX_TOKENS; ++i) { tokens[i].ticker = ""; // Empty string tokens[i].amount = 0.0f; // Zero amount tokens[i].value = 0.0f; // Zero value tokens[i].change24h = 0.0f; // Zero change } // Clear all NFT data arrays for (size_t i = 0; i < MAX_NFTS; ++i) { nfts[i].name = ""; // Empty name nfts[i].amount = 0.0f; // Zero amount nfts[i].floorPrice = 0.0f; // Zero floor price nfts[i].policyId = ""; // Empty policy ID } } /** * Update wallet balance data from Koios API * * This function implements rate limiting - it only fetches data if: * 1. WiFi is connected * 2. Enough time has passed since last fetch (1 minute) * * Rate limiting is important because: * - APIs have limits on how often you can request data * - Fetching too often wastes bandwidth and battery * - Wallet balance doesn't change that frequently anyway */ void updateKoiosData() { // Check if WiFi is connected - we can't fetch data without internet if (!wifiManagerIsConnected()) { return; // Exit early if no WiFi } // Get current time in milliseconds since device started const unsigned long now = millis(); // Rate limiting check: // - If lastKoiosFetch is 0, we've never fetched (allow it) // - Otherwise, only fetch if at least 1 minute has passed if (lastKoiosFetch != 0 && (now - lastKoiosFetch) < KOIOS_INTERVAL_MS) { return; // Not enough time has passed, skip this update } // Record that we're fetching now lastKoiosFetch = now; // Actually fetch the wallet balance from Koios API fetchWalletBalance(); } /** * Update token and NFT portfolio data * * This function fetches your token positions and NFT collections from MinSwap, * then fetches NFT floor prices from Cexplorer. It only runs every 10 minutes * because this data doesn't change as frequently and the API calls take longer. * * Process: * 1. Fetch tokens and NFTs from MinSwap API * 2. Extract Policy IDs from NFT data * 3. Fetch floor prices for each NFT collection from Cexplorer API */ void updatePortfolioData() { // Check WiFi connection first if (!wifiManagerIsConnected()) { return; // Can't fetch without internet } // Rate limiting - only fetch every 10 minutes const unsigned long now = millis(); if (lastPortfolioFetch != 0 && (now - lastPortfolioFetch) < PORTFOLIO_INTERVAL_MS) { return; // Not enough time has passed } // Record fetch time lastPortfolioFetch = now; // Step 1: Fetch tokens and NFTs from MinSwap // This populates the tokens[] and nfts[] arrays, and collects Policy IDs fetchMinSwapData(); // Step 2: Fetch floor prices for each NFT collection from Cexplorer // We loop through all Policy IDs we collected and fetch floor price data // This gives us the "floor price" (lowest selling price) for each collection for (int i = 0; i < policyIdCount; ++i) { fetchCexplorerData(policyIds[i]); } } /** * Getter functions - These provide access to the stored data * * These are simple functions that return the values of our global variables. * Other files (like screen files) call these to get data for display. */ // Return your current ADA wallet balance float getWalletBalance() { return walletBalance; } // Return how many different tokens you own int getTokenCount() { return tokenCount; } // Return how many different NFT collections you own int getNftCount() { return nftCount; } // Return when wallet balance was last fetched (for "Last updated" display) unsigned long getLastKoiosFetchTime() { return lastKoiosFetch; } /** * Get information about a specific token * * @param index Which token to get (0 = first token, 1 = second, etc.) * @return TokenInfo structure, or empty structure if index is invalid * * Example: getToken(0) returns your first token, getToken(1) returns your * second */ TokenInfo getToken(int index) { // Create an empty token structure as default TokenInfo empty = {"", 0.0f, 0.0f, 0.0f}; // Validate index - make sure it's within valid range // index must be >= 0 and < tokenCount if (index < 0 || index >= tokenCount) { return empty; // Invalid index, return empty structure } // Valid index, return the token data return tokens[index]; } /** * Get information about a specific NFT collection * * @param index Which collection to get (0 = first collection, 1 = second, etc.) * @return NFTInfo structure, or empty structure if index is invalid */ NFTInfo getNFT(int index) { // Create an empty NFT structure as default NFTInfo empty = {"", 0.0f, 0.0f, ""}; // Validate index if (index < 0 || index >= nftCount) { return empty; // Invalid index, return empty structure } // Valid index, return the NFT collection data return nfts[index]; } namespace { /** * Fetch wallet balance from Koios API * * Koios is a Cardano blockchain indexer - it provides fast access to blockchain * data without having to query the blockchain directly (which is slow). * * Process: * 1. Create HTTP client * 2. Build JSON request with your stake address * 3. Send POST request to Koios API * 4. Parse JSON response * 5. Extract balance (in Lovelace) and convert to ADA * * Important Cardano concepts: * - Stake Address: Your wallet's staking address (starts with "stake1...") * - Lovelace: The smallest unit of ADA (like cents to dollars) * - 1 ADA = 1,000,000 Lovelace * - We convert Lovelace to ADA for display */ void fetchWalletBalance() { Serial.println(); Serial.println("--- Fetching Wallet Balance from Koios ---"); // Create HTTP client object - this handles internet communication HTTPClient http; // Set the API endpoint URL (defined in config.h) http.begin(koiosApiUrl); // Tell the API we're sending JSON data http.addHeader("Content-Type", "application/json"); // Build the JSON request payload // Koios API expects: {"_stake_addresses":["stake1..."]} // We're asking: "What's the balance for this stake address?" String jsonPayload = "{\"_stake_addresses\":[\""; jsonPayload += stakeAddress; // Your stake address from config.h jsonPayload += "\"]}"; Serial.println("Sending POST request to Koios..."); Serial.print("Payload: "); Serial.println(jsonPayload); // Send the HTTP POST request and get response code // POST means we're sending data (unlike GET which just requests data) int httpResponseCode = http.POST(jsonPayload); // Check if request was successful (response code > 0 means success) if (httpResponseCode > 0) { Serial.print("HTTP Response Code: "); Serial.println(httpResponseCode); // Get the response data (this is JSON text) String response = http.getString(); // Create a JSON document to parse the response // 2048 = maximum size of JSON we expect (in bytes) DynamicJsonDocument doc(2048); // Parse the JSON string into a structured document we can access DeserializationError error = deserializeJson(doc, response); // Check if parsing was successful if (!error) { // Check if response is an array with at least one item if (doc.is() && doc.size() > 0) { // Get the first (and only) account info object JsonObject accountInfo = doc[0]; // Extract balance as a string (APIs often return large numbers as // strings) const char *balanceStr = accountInfo["total_balance"]; long long balanceLovelace = 0; // Convert string to number (atoll = "ASCII to long long") if (balanceStr != nullptr) { balanceLovelace = atoll(balanceStr); } // Convert Lovelace to ADA // Example: 5,000,000 Lovelace / 1,000,000 = 5.0 ADA walletBalance = balanceLovelace / 1000000.0; // Print success message to Serial Monitor Serial.println(); Serial.println("✓ Wallet Balance Fetched Successfully!"); Serial.print("Stake Address: "); Serial.println(accountInfo["stake_address"].as()); Serial.print("Total Balance: "); Serial.print(walletBalance, 6); // Print with 6 decimal places Serial.println(" ADA"); } else { Serial.println("Error: Empty response from Koios API"); } } else { // JSON parsing failed - maybe API returned invalid JSON Serial.print("JSON parsing failed: "); Serial.println(error.c_str()); } } else { // HTTP request failed (network error, timeout, etc.) Serial.print("Error in HTTP request. Response Code: "); Serial.println(httpResponseCode); } // Always close the HTTP connection when done http.end(); } /** * Fetch token and NFT data from MinSwap API * * MinSwap is a DEX (Decentralized Exchange) on Cardano. Their API provides * portfolio information including: * - Token positions (what tokens you own and their values) * - NFT positions (what NFTs you own, grouped by collection) * * Process: * 1. Build URL with your wallet address as a parameter * 2. Send GET request (simpler than POST - just requesting data) * 3. Parse JSON response * 4. Extract tokens and store in tokens[] array * 5. Extract NFTs, group by Policy ID, and store in nfts[] array * 6. Collect Policy IDs for later floor price fetching */ void fetchMinSwapData() { Serial.println(); Serial.println("--- Fetching Tokens and NFTs from MinSwap ---"); // Create HTTP client HTTPClient http; // Build the API URL with query parameters // Query parameters are added after "?" in the URL // Example: // https://api.minswap.org/v1/portfolio/tokens?address=addr1...&only_minswap=true String fullUrl = String(minswapApiUrl); fullUrl += "?address="; fullUrl += walletAddress; // Your wallet address from config.h fullUrl += "&only_minswap=true"; // Only show tokens from MinSwap fullUrl += "&filter_small_value=false"; // Don't filter out small value tokens Serial.print("Requesting: "); Serial.println(fullUrl); // Set the URL and send GET request // GET is simpler than POST - we're just requesting data, not sending data http.begin(fullUrl); Serial.println("Sending GET request to MinSwap..."); int httpResponseCode = http.GET(); if (httpResponseCode > 0) { Serial.print("HTTP Response Code: "); Serial.println(httpResponseCode); String response = http.getString(); DynamicJsonDocument doc(8192); DeserializationError error = deserializeJson(doc, response); if (!error) { Serial.println(); Serial.println("✓ MinSwap Data Fetched Successfully!"); // Check if response contains "positions" data if (doc.containsKey("positions")) { JsonObject positions = doc["positions"]; // Process NFT positions first if (positions.containsKey("nft_positions")) { JsonArray nftArray = positions["nft_positions"]; // Reset NFT storage before processing new data nftCount = 0; policyIdCount = 0; // Process each NFT position in the array // MinSwap returns each NFT as a separate entry, but we want to group // them by collection (Policy ID). So if you own 3 NFTs from the same // collection, we'll count them as one collection with amount = 3. for (int i = 0; i < nftArray.size() && nftCount < static_cast(MAX_NFTS); ++i) { JsonObject nft = nftArray[i]; // Get the Policy ID (also called "currency_symbol" in MinSwap API) // Policy ID is like a collection identifier - all NFTs from the // same collection have the same Policy ID const char *currencySymbol = nft["currency_symbol"]; // Skip if no Policy ID (shouldn't happen, but safety check) if (currencySymbol == nullptr) { continue; // Skip to next NFT } String policyId = String(currencySymbol); // Extract NFT collection name from metadata // MinSwap provides metadata with the collection name String nftName = "Unknown NFT"; if (nft.containsKey("asset")) { JsonObject assetInfo = nft["asset"]; if (assetInfo.containsKey("metadata")) { JsonObject metadata = assetInfo["metadata"]; // The "|" operator means "use this value, or if missing, use // default" nftName = metadata["name"] | "Unknown NFT"; } } // Check if we already have this Policy ID in our array // We want to group NFTs by collection, so we check if we've seen // this Policy ID before int existingIndex = -1; for (int j = 0; j < nftCount; ++j) { if (nfts[j].policyId == policyId) { existingIndex = j; // Found it! Remember which position break; } } if (existingIndex >= 0) { // We already have this collection - just increment the count // Example: If you own 2 Cardano Punks, then find a 3rd one, // we increment amount from 2 to 3 nfts[existingIndex].amount += 1.0f; } else { // New collection we haven't seen before - add it to our array nfts[nftCount].name = nftName; nfts[nftCount].amount = 1.0f; // First NFT from this collection nfts[nftCount].floorPrice = 0.0f; // Will be updated by Cexplorer later nfts[nftCount].policyId = policyId; // Save Policy ID so we can fetch floor price from Cexplorer if (policyIdCount < static_cast(MAX_POLICY_IDS)) { policyIds[policyIdCount] = policyId; ++policyIdCount; } Serial.print(" NFT Collection "); Serial.print(nftCount + 1); Serial.print(": "); Serial.print(nftName); Serial.print(" (Policy ID: "); Serial.print(currencySymbol); Serial.println(")"); ++nftCount; // Move to next position in array } } Serial.print("NFT Collections found: "); Serial.println(nftCount); Serial.print("Extracted "); Serial.print(policyIdCount); Serial.println(" policy ID(s) for Cexplorer API calls"); } // Process token positions (regular tokens, not NFTs) if (positions.containsKey("asset_positions")) { JsonArray assetArray = positions["asset_positions"]; // Count how many tokens we found tokenCount = assetArray.size(); // Limit to maximum we can display (8 tokens) if (tokenCount > static_cast(MAX_TOKENS)) { tokenCount = MAX_TOKENS; } Serial.print("Tokens found: "); Serial.println(tokenCount); // Process each token for (int i = 0; i < assetArray.size() && i < static_cast(MAX_TOKENS); ++i) { JsonObject asset = assetArray[i]; // Check if token has required data if (asset.containsKey("asset")) { JsonObject assetInfo = asset["asset"]; if (assetInfo.containsKey("metadata")) { JsonObject metadata = assetInfo["metadata"]; // Extract token information from JSON // The "|" operator provides default values if data is missing String ticker = metadata["ticker"] | "UNKNOWN"; // Token symbol (e.g., "MIN") String name = metadata["name"] | "Unknown Token"; // Full name float priceUsd = asset["price_usd"] | 0.0f; // Price per token in USD float amount = asset["amount"] | 0.0f; // How many you own float change24h = asset["pnl_24h_percent"] | 0.0f; // 24h price change % // Store token data in our array tokens[i].ticker = ticker; tokens[i].amount = amount; tokens[i].value = priceUsd * amount; // Total value = price × amount tokens[i].change24h = change24h; Serial.print(" Token "); Serial.print(i + 1); Serial.print(": "); Serial.print(ticker); Serial.print(" ("); Serial.print(name); Serial.print(") - Price: $"); Serial.print(priceUsd, 4); Serial.print(", Amount: "); Serial.print(amount, 2); Serial.print(", 24h Change: "); Serial.print(change24h, 2); Serial.println("%"); } } } } } else { Serial.println("Warning: No positions found in MinSwap response"); } } else { Serial.print("JSON parsing failed: "); Serial.println(error.c_str()); } } else { Serial.print("Error in HTTP request. Response Code: "); Serial.println(httpResponseCode); } http.end(); } /** * Fetch NFT collection information from Cexplorer API * * Cexplorer is a Cardano blockchain explorer that provides detailed information * about NFT collections, including: * - Collection name * - Floor price (lowest current selling price) * - Number of owners * - Other statistics * * This function is called once for each NFT Policy ID we found from MinSwap. * * @param policyId The Policy ID of the NFT collection to look up * * Process: * 1. Build URL with Policy ID as query parameter * 2. Send GET request to Cexplorer API * 3. Parse JSON response * 4. Extract collection name and floor price * 5. Update the corresponding NFT entry in our nfts[] array */ void fetchCexplorerData(const String &policyId) { Serial.println(); Serial.println("--- Fetching NFT Info from Cexplorer ---"); Serial.print("Policy ID: "); Serial.println(policyId); // Create HTTP client HTTPClient http; // Build URL with Policy ID as query parameter // Example: https://api.cexplorer.io/v1/policy/detail?id=f0ff48bbb7... String fullUrl = String(cexplorerApiUrl); fullUrl += "?id="; fullUrl += policyId; Serial.print("Requesting: "); Serial.println(fullUrl); // Set URL and prepare request http.begin(fullUrl); // Optional: Add API key header if you have one // Some APIs require authentication, but Cexplorer works without it // http.addHeader("api-key", cexplorerApiKey); Serial.println("Sending GET request to Cexplorer..."); int httpResponseCode = http.GET(); if (httpResponseCode > 0) { Serial.print("HTTP Response Code: "); Serial.println(httpResponseCode); String response = http.getString(); DynamicJsonDocument doc(4096); DeserializationError error = deserializeJson(doc, response); if (!error) { Serial.println(); Serial.println("✓ Cexplorer Data Fetched Successfully!"); // Check if response contains "data" object if (doc.containsKey("data")) { JsonObject data = doc["data"]; // Check if collection information is available if (data.containsKey("collection")) { JsonObject collection = data["collection"]; // Extract collection name // Cexplorer usually has better/more accurate names than MinSwap String collectionName = collection["name"] | "Unknown"; Serial.print("Collection Name: "); Serial.println(collectionName); // Extract floor price (lowest current selling price) float floorPriceAda = 0.0f; if (collection.containsKey("stats")) { JsonObject stats = collection["stats"]; // Floor price comes in Lovelace (smallest ADA unit) long floorLovelace = stats["floor"] | 0; // Convert to ADA (divide by 1,000,000) floorPriceAda = floorLovelace / 1000000.0f; // Also get number of owners (for debugging/logging) int owners = stats["owners"] | 0; Serial.print("Floor Price: "); Serial.print(floorPriceAda, 2); // Print with 2 decimal places Serial.println(" ADA"); Serial.print("Owners: "); Serial.println(owners); } // Now update our NFT array with the collection name and floor price // We need to find which NFT entry has this Policy ID for (int i = 0; i < nftCount && i < static_cast(MAX_NFTS); ++i) { if (nfts[i].policyId == policyId) { // Found the matching NFT collection! // Update with better name from Cexplorer (more accurate than // MinSwap) nfts[i].name = collectionName; // Update floor price if we got one if (floorPriceAda > 0.0f) { nfts[i].floorPrice = floorPriceAda; } break; // Found it, no need to keep searching } } } } else { Serial.println("Warning: No data found in Cexplorer response"); } } else { Serial.print("JSON parsing failed: "); Serial.println(error.c_str()); } } else { Serial.print("Error in HTTP request. Response Code: "); Serial.println(httpResponseCode); if (httpResponseCode == 401) { Serial.println("Error: 401 Unauthorized - Check your Cexplorer API key!"); } } http.end(); } } // namespace ``` `data_fetcher.h`: ```cpp /** * data_fetcher.h - Header file for blockchain data fetching * * This file defines the data structures and functions used to fetch and store * information from Cardano blockchain APIs. * * What is a header file (.h)? * - It declares what functions and structures exist, but doesn't implement them * - The actual code is in data_fetcher.cpp * - Other files can #include this to use these functions */ #ifndef DATA_FETCHER_H #define DATA_FETCHER_H #include /** * TokenInfo - Structure to store information about a Cardano token * * In Cardano, tokens are custom assets (like cryptocurrencies) that can be * created and traded. Examples: MIN (MinSwap token), HOSKY (meme token), etc. * * This structure holds all the information we need to display about a token. */ struct TokenInfo { String ticker; // Short symbol for the token (e.g., "MIN", "ADA") float amount; // How many tokens you own float value; // Total value of your tokens in USD (amount × price) float change24h; // Price change percentage over last 24 hours (can be negative) }; /** * NFTInfo - Structure to store information about an NFT collection * * NFT = Non-Fungible Token (unique digital collectible) * In Cardano, NFTs are grouped by "Policy ID" - think of it as the collection ID. * All NFTs from the same collection share the same Policy ID. * * Example: If you own 3 "Cardano Punks" NFTs, they all have the same Policy ID, * but each individual NFT is unique. */ struct NFTInfo { String name; // Name of the NFT collection (e.g., "Cardano Punks") float amount; // Number of NFTs you own from this collection float floorPrice; // Floor price = lowest price this collection is selling for (in ADA) String policyId; // Policy ID = unique identifier for this NFT collection // Used to match NFTs with their floor price data }; // Function declarations - these are implemented in data_fetcher.cpp /** * Initialize the data fetcher * Sets all counters and arrays to zero/empty */ void initDataFetcher(); /** * Update wallet balance from Koios API * Fetches your ADA balance every minute (if enough time has passed) * Koios is a Cardano blockchain indexer - it provides fast access to blockchain data */ void updateKoiosData(); /** * Update token and NFT data from MinSwap and Cexplorer APIs * Fetches your token positions and NFT collections every 10 minutes * MinSwap is a DEX (Decentralized Exchange) that provides portfolio data * Cexplorer provides NFT collection information and floor prices */ void updatePortfolioData(); // Getter functions - these return the stored data /** * Get your current ADA wallet balance * @return Balance in ADA (1 ADA = 1,000,000 Lovelace) */ float getWalletBalance(); /** * Get the number of different tokens you own * @return Number of unique tokens (max 8) */ int getTokenCount(); /** * Get the number of different NFT collections you own * @return Number of unique NFT collections (max 8) */ int getNftCount(); /** * Get timestamp of when wallet balance was last fetched * Useful for displaying "Last updated: X minutes ago" * @return Timestamp in milliseconds, or 0 if never fetched */ unsigned long getLastKoiosFetchTime(); /** * Get information about a specific token * @param index Which token to get (0 = first token, 1 = second, etc.) * @return TokenInfo structure with token data, or empty if index is invalid */ TokenInfo getToken(int index); /** * Get information about a specific NFT collection * @param index Which collection to get (0 = first collection, 1 = second, etc.) * @return NFTInfo structure with NFT data, or empty if index is invalid */ NFTInfo getNFT(int index); #endif ``` > Source: [`wifi_manager.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/wifi_manager.cpp), [`data_fetcher.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/data_fetcher.cpp) ## Wallet screen After the start screen ("CardanoTicker") on boot, the ticker rotates between four data screens every 10 seconds: wallet, tokens, NFTs, status. The wallet screen shows your ADA balance prominently - same idea as the wallet display from [Workshop 02](/docs/developers/curriculum/dapps/iot/read-and-output/02-display-data). It shows: - **Balance** - ADA balance in size-3 text. - **Stake address** - truncated to fit (first 12 + "..." + last 12 chars). - **Last updated** - relative time ("2m 30s ago" or "just now"). `wallet_screen.cpp`: ```cpp /** * wallet_screen.cpp - Wallet Balance Screen * * This screen displays your Cardano wallet information: * - ADA balance (your main cryptocurrency holdings) * - Stake address (your wallet's staking address) * - Last update time (when balance was last fetched) * * Cardano Concepts: * - ADA: The native cryptocurrency of Cardano (like Bitcoin for Bitcoin network) * - Stake Address: A special address used for staking (earning rewards) * Format: starts with "stake1..." (mainnet) or "stake_test1..." (testnet) * - Staking: Locking ADA to help secure the network and earn rewards */ #include "wallet_screen.h" #include "config.h" #include "data_fetcher.h" #include "screen_helper.h" #include // External reference to TFT display extern TFT_eSPI tft; /** * Draw the wallet balance screen * * This is the first screen shown (index 0). It displays your ADA balance * prominently, along with your stake address and last update time. */ void drawWalletScreen() { // Draw header with title and page indicator // activeIndex = 0 means this is the first screen renderHeader("Wallet", 0); // Clear the content area clearContentArea(); // Set default text color tft.setTextColor(TFT_WHITE, TFT_BLACK); int y = kHeaderHeight + 5; // Start below header // Draw "Balance" label tft.setTextSize(2); // Medium text tft.setCursor(10, y); tft.print("Balance"); // Draw ADA balance in large text tft.setTextSize(3); // Large text for emphasis y += 30; // Move down tft.setCursor(10, y); tft.print(getWalletBalance(), 2); // Print balance with 2 decimal places tft.print("ADA"); // Add "ADA" label // Draw stake address (smaller text) tft.setTextSize(1); // Small text y += 35; // Move down tft.setCursor(10, y); tft.print("Stake Address: "); // Stake addresses are long (like 57 characters), so we truncate for display // Show first 12 characters + "..." + last 12 characters // Example: "stake1u8l0y8...c5gt5k3" instead of full address String truncated = stakeAddress.substring(0, 12); // First 12 chars truncated += "..."; truncated += stakeAddress.substring(stakeAddress.length() - 12); // Last 12 chars tft.print(truncated); // Display last updated time y += 16; // Move down tft.setCursor(10, y); tft.print("Last updated: "); // Get timestamp of when balance was last fetched const unsigned long lastFetch = getLastKoiosFetchTime(); if (lastFetch == 0) { // Never fetched (device just started or WiFi not connected yet) tft.print("Never"); } else { // Calculate time difference const unsigned long now = millis(); // Current time const unsigned long diffMs = now - lastFetch; // Difference in milliseconds const unsigned long diffSec = diffMs / 1000UL; // Convert to seconds // Format time difference in human-readable way if (diffSec < 10) { // Less than 10 seconds ago tft.print("just now"); } else if (diffSec < 60) { // Less than 1 minute ago - show seconds tft.print(diffSec); tft.print("s ago"); } else { // 1 minute or more - show minutes and seconds const unsigned long minutes = diffSec / 60UL; // Total minutes const unsigned long seconds = diffSec % 60UL; // Remaining seconds tft.print(minutes); tft.print("m "); if (seconds > 0) { tft.print(seconds); tft.print("s "); } tft.print("ago"); } } } ``` `wallet_screen.h`: ```cpp /** * wallet_screen.h - Header file for wallet display screen * * This file declares the function to draw the wallet screen, which displays * your Cardano wallet balance in ADA and related information. */ #ifndef WALLET_SCREEN_H #define WALLET_SCREEN_H /** * Draw the wallet screen * * Displays your current ADA wallet balance and related wallet information. * This screen is part of the rotating display cycle. */ void drawWalletScreen(); #endif ``` > Source: [`wallet_screen.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/wallet_screen.cpp) ## Token screen A table of token holdings - one row per token: ticker symbol, amount, total value, 24-hour change. - **Ticker** - the token symbol ("MIN", "HOSKY", "ADA"). - **Amount** - how many you own. - **Value** - total USD value. - **24h change** - coloured green (up) / red (down). `token_screen.cpp`: ```cpp /** * token_screen.cpp - Token Holdings Screen * * This screen displays all the Cardano tokens you own, showing: * - Token ticker symbol (e.g., "MIN", "HOSKY") * - Amount you own * - Total value in USD * - 24-hour price change percentage (green if up, red if down) * * Tokens are custom assets on Cardano blockchain. Unlike ADA (the native * currency), tokens are created by projects and can represent anything * (governance tokens, meme coins, utility tokens, etc.). */ #include "token_screen.h" #include "data_fetcher.h" #include "screen_helper.h" #include // External reference to TFT display (defined in main .ino file) extern TFT_eSPI tft; /** * Draw the token positions screen * * This function renders a table showing all your token holdings. * Each row shows one token with its ticker, amount, value, and price change. */ void drawTokenScreen() { // Draw header with title and page indicator // activeIndex = 1 means this is the second screen (0-indexed) renderHeader("Token Positions", 1); // Clear the content area (erases previous screen's content) clearContentArea(); // Set default text color (white on black) tft.setTextColor(TFT_WHITE, TFT_BLACK); // Start drawing below the header // kHeaderHeight is the height of the header (34px), +5px for spacing int y = kHeaderHeight + 5; // Draw screen title with token count tft.setTextSize(2); // Larger text for title tft.setCursor(10, y); // 10px from left edge tft.print("Tokens(" + String(getTokenCount()) + ")"); // e.g., "Tokens(5)" y += 35; // Move down for next line // Get token count (already limited to MAX_DISPLAY_ITEMS = 8) const int tokenCount = getTokenCount(); const int displayCount = tokenCount; // We can display all tokens (max 8) // Switch to smaller text for the table tft.setTextSize(1); // Draw column headers tft.setTextColor(TFT_DARKGREY, TFT_BLACK); // Gray text for headers tft.setTextSize(1); tft.setCursor(10, y); // "Ticker" column tft.print("Ticker"); tft.setCursor(60, y); // "Amount" column tft.print("Amount"); tft.setCursor(160, y); // "Value" column tft.print("Value"); tft.setCursor(240, y); // "24h Change" column tft.print("24h Change"); y += 16; // Move down to start data rows tft.setTextColor(TFT_WHITE, TFT_BLACK); // Back to white for data // Loop through each token and draw a row for (int i = 0; i < displayCount; ++i) { // Get token data from data fetcher TokenInfo token = getToken(i); // Truncate token name if too long (so it fits on screen) String displayName = token.ticker; if (displayName.length() > 20) { // If longer than 20 characters, show first 15 + "..." displayName = displayName.substring(0, 15) + "..."; } // Draw token ticker (left column) tft.setCursor(10, y); tft.print(displayName); // Draw amount you own (second column) tft.setCursor(60, y); tft.print(token.amount, 2); // Print with 2 decimal places // Draw total value in USD (third column) tft.setCursor(160, y); tft.print("$" + String(token.value, 2)); // e.g., "$123.45" // Draw 24-hour price change (fourth column) tft.setCursor(240, y); // Color code: green for positive change, red for negative if (token.change24h >= 0) { tft.setTextColor(TFT_GREEN, TFT_BLACK); // Green = price went up } else { tft.setTextColor(TFT_RED, TFT_BLACK); // Red = price went down } // Show change with + or - sign // Note: String() already includes the minus sign for negative numbers // So we only need to add "+" for positive numbers // Example: String(5.67, 2) = "5.67" (we add "+" to make "+5.67") // String(-5.67, 2) = "-5.67" (already has minus, no need to add // "-") tft.print((token.change24h >= 0 ? "+" : "") + String(token.change24h, 2)); tft.print("%"); // Reset text color back to white tft.setTextColor(TFT_WHITE, TFT_BLACK); // Move down for next row y += 16; // Safety check: stop if we're running out of screen space // Don't draw over the ticker at the bottom if (y > tft.height() - kTickerHeight - 10) { break; // Exit loop early } } // If there are more tokens than we can display, show a message // (This shouldn't happen since we limit to 8, but good to have) if (tokenCount > displayCount) { y += 4; // Add some spacing tft.setCursor(10, y); tft.print("... and "); tft.print(tokenCount - displayCount); tft.print(" more"); } } ``` `token_screen.h`: ```cpp /** * token_screen.h - Header file for token display screen * * This file declares the function to draw the token screen, which displays * information about the Cardano tokens in your wallet, including token names, * quantities, values, and 24-hour price changes. */ #ifndef TOKEN_SCREEN_H #define TOKEN_SCREEN_H /** * Draw the token screen * * Displays your Cardano token holdings with their values and price changes. * This screen is part of the rotating display cycle. */ void drawTokenScreen(); #endif ``` > Source: [`token_screen.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/token_screen.cpp) ## NFT screen A table of NFT collections - one row per collection: name, count, floor price. - **Name** - collection name ("Cardano Punks", "SpaceBudz"). - **Amount** - how many NFTs you own from the collection. - **Floor price** - current lowest selling price for the collection in ADA. `nft_screen.cpp`: ```cpp /** * nft_screen.cpp - NFT Collection Screen * * This screen displays all your NFT collections, showing: * - Collection name (e.g., "Cardano Punks", "SpaceBudz") * - Number of NFTs you own from that collection * - Floor price (lowest current selling price) in ADA * * Important Cardano NFT concepts: * - NFTs are grouped by "Policy ID" (collection identifier) * - If you own 3 NFTs from the same collection, they're shown as one entry * - Floor price = the cheapest NFT from that collection currently for sale * - Floor price helps you understand the collection's market value */ #include "nft_screen.h" #include "data_fetcher.h" #include "screen_helper.h" #include // External reference to TFT display extern TFT_eSPI tft; /** * Draw the NFT positions screen * * This function renders a table showing all your NFT collections. * Each row shows one collection with its name, how many you own, and floor * price. */ void drawNFTScreen() { // Draw header with title and page indicator // activeIndex = 2 means this is the third screen (0-indexed) renderHeader("NFT Positions", 2); // Clear the content area clearContentArea(); // Set default text color tft.setTextColor(TFT_WHITE, TFT_BLACK); // Start drawing below header int y = kHeaderHeight + 5; // Draw screen title with NFT collection count tft.setTextSize(2); // Larger text tft.setCursor(10, y); tft.print("NFTs(" + String(getNftCount()) + ")"); // e.g., "NFTs(3)" y += 35; // Move down // Get NFT collection count (already limited to MAX_DISPLAY_ITEMS = 8) const int nftCount = getNftCount(); const int displayCount = nftCount; // We can display all collections (max 8) // Switch to smaller text for table tft.setTextSize(1); // Draw column headers tft.setTextColor(TFT_DARKGREY, TFT_BLACK); // Gray for headers tft.setTextSize(1); tft.setCursor(10, y); // "Name" column tft.print("Name"); tft.setCursor(120, y); // "Amount" column tft.print("Amount"); tft.setCursor(200, y); // "Floor Price" column tft.print("Floor Price"); y += 16; // Move down to data rows tft.setTextColor(TFT_WHITE, TFT_BLACK); // Back to white // Loop through each NFT collection and draw a row for (int i = 0; i < displayCount; ++i) { // Get NFT collection data from data fetcher NFTInfo nft = getNFT(i); // Truncate collection name if too long (so it fits on screen) String displayName = nft.name; if (displayName.length() > 18) { // If longer than 18 characters, show first 15 + "..." displayName = displayName.substring(0, 15) + "..."; } // Draw collection name (left column) tft.setCursor(10, y); tft.print(displayName); // Draw number of NFTs you own (middle column) tft.setCursor(120, y); tft.print(nft.amount, 0); // Print as integer (no decimals for count) // Draw floor price (right column) tft.setCursor(200, y); if (nft.floorPrice > 0.0f) { // If we have floor price data, show it in ADA tft.print(String(nft.floorPrice, 2) + " ADA"); // e.g., "50.25 ADA" } else { // If floor price not available yet (still fetching), show "N/A" tft.print("N/A"); } // Move down for next row y += 16; // Safety check: stop if running out of screen space if (y > tft.height() - kTickerHeight - 10) { break; // Exit loop early } } // If there are more collections than we can display, show a message // (This shouldn't happen since we limit to 8, but good to have) if (nftCount > displayCount) { y += 4; // Add spacing tft.setCursor(10, y); tft.print("... and "); tft.print(nftCount - displayCount); tft.print(" more"); } } ``` `nft_screen.h`: ```cpp /** * nft_screen.h - Header file for NFT display screen * * This file declares the function to draw the NFT screen, which displays * information about the NFT collections in your wallet, including collection * names, quantities owned, and floor prices. */ #ifndef NFT_SCREEN_H #define NFT_SCREEN_H /** * Draw the NFT screen * * Displays your NFT collections with their floor prices and quantities. * This screen is part of the rotating display cycle. */ void drawNFTScreen(); #endif ``` > Source: [`nft_screen.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/nft_screen.cpp) ## Status screen Technical info about the device and network - useful for debugging. - **Network status** - "Connected" / "Offline". - **Signal strength** - dBm (closer to 0 is better). - **IP address** - local network address. - **MAC address** - hardware identifier. - **Uptime** - "2d 5h 30m 15s". `status_screen.cpp`: ```cpp /** * status_screen.cpp - System Status Screen * * This screen displays device and network information: * - WiFi connection status * - WiFi signal strength (RSSI) * - IP address (your device's address on the network) * - MAC address (unique hardware identifier) * - Uptime (how long the device has been running) * * This is useful for debugging connection issues and monitoring device health. */ #include "status_screen.h" #include "screen_helper.h" #include "wifi_manager.h" #include #include // External reference to TFT display extern TFT_eSPI tft; /** * Draw the system status screen * * This is the last screen (index 3). It shows technical information about * the device and network connection. */ void drawStatusScreen() { // Draw header with title and page indicator // activeIndex = 3 means this is the fourth (last) screen renderHeader("System", 3); // Clear the content area clearContentArea(); // Set default text color tft.setTextColor(TFT_WHITE, TFT_BLACK); // Gather all status information const bool connected = wifiManagerIsConnected(); // Is WiFi connected? const int32_t rssi = connected ? WiFi.RSSI() : 0; // Signal strength (only if connected) const IPAddress ipAddr = connected ? WiFi.localIP() : IPAddress(0, 0, 0, 0); // IP address const String macAddr = WiFi.macAddress(); // MAC address (always available) // Calculate uptime (how long device has been running) const unsigned long uptimeMs = millis(); // Milliseconds since startup const unsigned long uptimeSec = uptimeMs / 1000UL; // Convert to seconds // Break down uptime into days, hours, minutes, seconds const unsigned long days = uptimeSec / 86400UL; // 86400 seconds = 1 day const unsigned long hours = (uptimeSec % 86400UL) / 3600UL; // Remaining hours const unsigned long minutes = (uptimeSec % 3600UL) / 60UL; // Remaining minutes const unsigned long seconds = uptimeSec % 60UL; // Remaining seconds // Start drawing below header int y = kHeaderHeight + 5; // Draw "Network" label tft.setTextSize(2); tft.setCursor(10, y); tft.print("Network"); // Draw connection status in large text tft.setTextSize(3); // Large text y += 30; tft.setCursor(10, y); tft.print(connected ? "Connected" : "Offline"); // Show status // Draw WiFi signal strength tft.setTextSize(1); // Small text y += 35; tft.setCursor(10, y); tft.print("Signal: "); if (connected) { // RSSI = Received Signal Strength Indicator // Measured in dBm (decibels relative to milliwatt) // Typical range: -30 (excellent) to -90 (poor) // Negative numbers are normal - closer to 0 is better tft.print(String(rssi) + " dBm"); } else { tft.print("N/A"); // Not available if not connected } // Draw IP address y += 16; tft.setCursor(10, y); tft.print("IP: "); // IP address = Internet Protocol address // This is your device's address on your local network // Format: XXX.XXX.XXX.XXX (e.g., 192.168.1.100) tft.print(ipAddr.toString()); // Draw MAC address y += 16; tft.setCursor(10, y); tft.print("MAC: "); // MAC address = Media Access Control address // This is a unique identifier for your device's network hardware // Format: XX:XX:XX:XX:XX:XX (e.g., AA:BB:CC:DD:EE:FF) // Unlike IP address, MAC address never changes tft.print(macAddr); // Draw uptime y += 16; tft.setCursor(10, y); tft.print("Uptime: "); // Display uptime in human-readable format: "Xd Xh Xm Xs" tft.print(days); tft.print("d "); // Days tft.print(hours); tft.print("h "); // Hours tft.print(minutes); tft.print("m "); // Minutes tft.print(seconds); tft.print("s"); // Seconds } ``` `status_screen.h`: ```cpp /** * status_screen.h - Header file for status display screen * * This file declares the function to draw the status screen, which displays * system information such as WiFi connection status, last update times, * and other diagnostic information. */ #ifndef STATUS_SCREEN_H #define STATUS_SCREEN_H /** * Draw the status screen * * Displays system status information including network connectivity, * API fetch times, and other diagnostic data. * This screen is part of the rotating display cycle. */ void drawStatusScreen(); #endif ``` > Source: [`status_screen.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/status_screen.cpp) ## Scrolling ticker The bottom strip continuously scrolls token prices horizontally - stock-market-style. Per token: - **Ticker symbol** in larger text ("MIN"). - **Price per token** in USD ("$0.0123"). - **24h change** colour-coded ("+5.67%"). `ticker.cpp`: ```cpp /** * ticker.cpp - Scrolling Token Ticker * * This file implements a scrolling ticker at the bottom of the screen that * displays token prices continuously. Think of it like a stock market ticker * - it scrolls horizontally showing token symbols, prices, and 24h changes. * * How it works: * 1. We draw all token information into an off-screen buffer (sprite) * 2. We draw the content twice (side by side) to create seamless looping * 3. We scroll the viewport left, and when we reach the end, we loop back * 4. This creates an endless scrolling effect * * Technical concepts: * - Sprite: An off-screen buffer we draw to, then push to screen all at once * - This reduces flicker compared to drawing directly to the screen * - Seamless looping: Drawing content twice so when one copy scrolls off, * the second copy is already visible, creating infinite scroll */ #include "ticker.h" #include "data_fetcher.h" #include #include // External reference to TFT display (defined in main .ino file) extern TFT_eSPI tft; // Create sprite for smooth scrolling // A sprite is an off-screen buffer - we draw to it, then push it to the display // This reduces flicker because we update the whole area at once TFT_eSprite scrollSprite = TFT_eSprite(&tft); // Scroll area configuration const int scrollAreaHeight = 30; // Height of ticker area at bottom (in pixels) // Must match kTickerHeight in screen_helper.h const int yPos = 4; // Vertical position within sprite (4px from top) const int scrollSpeed = 2; // How many pixels to scroll per update // Higher = faster scroll, Lower = slower scroll // Scrolling state variables int scrollX = 0; // Current horizontal scroll position (in pixels) // This tracks how far we've scrolled to the left int contentWidth = 0; // Total width of all token content (in pixels) // Used to calculate when to loop back to start /** * Calculate the price per token * * The data fetcher gives us total value (value) and amount owned (amount). * To show price per token, we divide: price = total_value / amount * * @param token The token information structure * @return Price per token in USD, or 0 if amount is 0 * * Example: If you own 100 tokens worth $50 total, price per token = $0.50 */ static float getTokenPrice(const TokenInfo& token) { // Avoid division by zero (if amount is 0, return 0) return (token.amount > 0.0f) ? (token.value / token.amount) : 0.0f; } /** * Calculate total width of all token content * * This function measures how wide all the token information is when displayed. * We need this to know when to loop the scroll back to the beginning. * * We measure each piece of text individually because different text sizes * and lengths take up different amounts of space. */ void calculateContentWidth() { contentWidth = 0; // Start with zero width const int tokenCount = getTokenCount(); // Loop through each token and measure its width for (int i = 0; i < tokenCount; i++) { TokenInfo token = getToken(i); float price = getTokenPrice(token); // Measure ticker symbol width (larger text, size 2) tft.setTextSize(2); contentWidth += tft.textWidth(token.ticker) + 4; // Add 4px spacing after ticker // Measure price width (smaller text, size 1) tft.setTextSize(1); String priceStr = "$" + String(price, 4); // Format: "$0.1234" contentWidth += tft.textWidth(priceStr) + 4; // Add 4px spacing after price // Measure 24h change width (smaller text, size 1) // Note: String() already includes the minus sign for negative numbers String changeStr = (token.change24h >= 0 ? "+" : "") + String(token.change24h, 2) + "%"; // Format: "+5.67%" or "-2.34%" contentWidth += tft.textWidth(changeStr) + 8; // Add 8px spacing after change (extra space) } // Now contentWidth = total width needed to display all tokens // We'll draw this content twice (side by side) for seamless looping } /** * Draw all token information at a given horizontal position * * This function draws all tokens into the sprite buffer starting at xPos. * We call this twice with different xPos values to create seamless looping. * * @param xPos The horizontal position to start drawing at (can be negative) * * What we draw for each token: * 1. Ticker symbol (large text, e.g., "MIN") * 2. Price per token (small text, e.g., "$0.0123") * 3. 24h change (small text, colored green/red, e.g., "+5.67%") */ void drawContentLine(int xPos) { const int tokenCount = getTokenCount(); // Draw each token in sequence for (int i = 0; i < tokenCount; i++) { TokenInfo token = getToken(i); float price = getTokenPrice(token); // Draw token ticker symbol (larger, more prominent) scrollSprite.setTextSize(2); // Size 2 = larger text scrollSprite.setTextColor(TFT_WHITE, TFT_BLACK); scrollSprite.drawString(token.ticker, xPos, yPos); // Draw at current position xPos += scrollSprite.textWidth(token.ticker) + 4; // Move xPos right by ticker width + spacing // Draw token price (smaller text, slightly lower for visual alignment) scrollSprite.setTextSize(1); // Size 1 = smaller text String priceStr = "$" + String(price, 4); // Format: "$0.1234" scrollSprite.drawString(priceStr, xPos, yPos + 2); // +2px down for alignment xPos += scrollSprite.textWidth(priceStr) + 4; // Move xPos right // Draw 24h price change with color coding // Format: "+5.67%" for positive, "-2.34%" for negative // Note: String() already includes the sign for negative numbers String changeStr = (token.change24h >= 0 ? "+" : "") + String(token.change24h, 2) + "%"; // Format: "+5.67%" or "-2.34%" // Color code: green = price went up, red = price went down if (token.change24h >= 0) { scrollSprite.setTextColor(TFT_GREEN, TFT_BLACK); // Green for gains } else { scrollSprite.setTextColor(TFT_RED, TFT_BLACK); // Red for losses } scrollSprite.drawString(changeStr, xPos, yPos + 2); // Draw change xPos += scrollSprite.textWidth(changeStr) + 8; // Move xPos right (extra spacing) // After this loop, xPos has moved to the right of all tokens // This is how we know where to draw the second copy for seamless looping } } /** * Initialize the ticker display * * This function sets up the scrolling ticker. It's called once at startup * in setup(). It creates the sprite buffer and calculates content width. */ void initTicker() { // Fill entire screen with black (clean slate) tft.fillScreen(TFT_BLACK); // Configure sprite for smooth scrolling // 16-bit color depth = 65,536 colors (full color support) // Higher color depth = better quality but uses more memory scrollSprite.setColorDepth(16); // Create the sprite buffer // Width = full screen width, Height = ticker area height (30px) // This creates an off-screen buffer we can draw to scrollSprite.createSprite(tft.width(), scrollAreaHeight); // Calculate how wide all the token content is // This tells us when to loop the scroll back to the beginning calculateContentWidth(); Serial.println("Token scroll display initialized!"); } /** * Update the ticker display (call this in loop) * * This function is called repeatedly from loop() to create the scrolling * animation. Each call: * 1. Clears the sprite * 2. Draws content at current scroll position * 3. Draws content again (offset) for seamless looping * 4. Pushes sprite to screen * 5. Updates scroll position * * Seamless looping trick: * - We draw the content twice, side by side * - When the first copy scrolls off the left, the second copy is already visible * - When we reach the end, we reset scrollX to 0 and it looks continuous */ void updateTicker() { // Clear the sprite buffer with black (erase previous frame) scrollSprite.fillSprite(TFT_BLACK); // Draw first copy of content // -scrollX means we're scrolling left (negative X moves content left) // Example: if scrollX = 50, we draw at x = -50, which shows content // that's 50 pixels to the left (already scrolled) drawContentLine(-scrollX); // Draw second copy of content for seamless looping // We offset it by contentWidth so it appears right after the first copy // When first copy scrolls off left, second copy is already visible drawContentLine(-scrollX + contentWidth); // Push the sprite to the display // This updates the screen all at once (reduces flicker) // Position: x=0 (left edge), y=bottom of screen minus ticker height scrollSprite.pushSprite(0, tft.height() - scrollAreaHeight); // Update scroll position (move left by scrollSpeed pixels) scrollX += scrollSpeed; // Check if we've scrolled past all the content // If so, reset to 0 to create the loop effect if (scrollX >= contentWidth) { scrollX = 0; // Loop back to beginning } // Small delay to control scroll speed // Without this, scrolling would be too fast // 30ms = ~33 updates per second (smooth animation) delay(30); } ``` `ticker.h`: ```cpp /** * ticker.h - Header file for scrolling ticker * * This file declares the functions for the scrolling token price ticker * that appears at the bottom of the screen. */ #ifndef TICKER_H #define TICKER_H #include /** * Initialize the ticker display * * Sets up the sprite buffer and calculates content width. * Call this once in setup(). */ void initTicker(); /** * Update the ticker display * * Draws the scrolling ticker and updates the scroll position. * Call this repeatedly in loop() to create smooth scrolling animation. */ void updateTicker(); #endif ``` > Source: [`ticker.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-04/examples/CardanoTicker/ticker.cpp) ## Putting it all together 1. **Download the code** from [the GitHub repo](https://github.com/CardanoThings/Workshops/tree/main/Workshop-04/examples/CardanoTicker). 2. **Configure addresses** - edit `config.cpp` with your stake address and wallet address. 3. **Set up WiFi** - copy `secrets.h.example` to `secrets.h` and fill in. 4. **Get an API key** - Cexplorer.io key into `config.cpp`. 5. **Install libraries** - TFT_eSPI, ArduinoJson, WiFi (you already have these from earlier workshops). 6. **Upload and run** - flash to your ESP32 and watch. :::info Library requirements You should already have all of these from previous workshops: - **TFT_eSPI** - Workshop 02 (display). - **ArduinoJson** - Workshop 02 (JSON parsing). - **WiFi** - built into ESP32. - **HTTPClient** - built into ESP32. ::: ## Next steps Some directions: - Special effects when your balance changes. - Another screen for your own NFT project. - For more advanced graphics, look into [LVGL](https://lvgl.io/) - beautiful UIs for any MCU/MPU/display. ![Cardano Ticker on the CYD - 1](../img/CardanoTicker1.jpg) ![Cardano Ticker on the CYD - 2](../img/CardanoTicker2.jpg) ![Cardano Ticker on the CYD - 3](../img/CardanoTicker3.jpg) ![Cardano Ticker on the CYD - 4](../img/CardanoTicker4.jpg) ![Cardano Ticker on the CYD - 5](../img/CardanoTicker5.jpg) ## Further Resources - [TFT_eSPI Library](https://github.com/Bodmer/TFT_eSPI) - graphics for ESP8266 / ESP32 TFT displays. - [ArduinoJson Library](https://arduinojson.org/) - JSON parsing. - [Koios API Documentation](https://api.koios.rest/) - free Cardano API. - [MinSwap](https://minswap.org/) - Cardano DEX (token + NFT positions). - [Cexplorer.io](https://cexplorer.io/) - explorer + free-tier API for NFT collection data. - [LVGL](https://lvgl.io/) - advanced embedded GUI library. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/04-cardano-ticker/building-the-ticker) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-04](https://github.com/CardanoThings/Workshops/tree/main/Workshop-04).* --- ## Workshop 04: Cardano Ticker This workshop builds a complete Cardano ticker: a multi-screen TFT display that rotates between your ADA balance, your token holdings with prices from MinSwap, your NFT collections with floor prices from Cexplorer, and a status screen - plus a stock-market-style scrolling ticker along the bottom. The example uses **mainnet data** (the original CardanoThings wallet). API endpoints used here are intended for educational purposes; a production ticker would need official paid APIs or a self-hosted source. > Source code: [github.com/CardanoThings/Workshops/tree/main/Workshop-04](https://github.com/CardanoThings/Workshops/tree/main/Workshop-04) ## Steps 1. **[Gathering Data](./01-gathering-data.md)** - APIs and endpoints used: Koios for stake balance, MinSwap portfolio API for token prices, Cexplorer for NFT collection floor prices. Includes a note on TapTools and Charli3 for production use. 2. **[Building the Ticker](./02-building-the-ticker.md)** - Walk through a multi-file Arduino project: WiFi manager, data fetcher, four data screens (wallet, tokens, NFTs, status) and the scrolling ticker. ## What you'll need - The board with a screen from the [Hardware reference](/docs/developers/curriculum/dapps/iot/hardware/), as set up in Workshop 02. - A free [Cexplorer.io](https://cexplorer.io/) API key. - Libraries already installed from earlier workshops: TFT_eSPI, ArduinoJson, WiFi, HTTPClient. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/04-cardano-ticker) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-04](https://github.com/CardanoThings/Workshops/tree/main/Workshop-04).* --- ## AHT10 Temperature & Humidity Sensor (I2C) A high-precision digital **temperature and humidity sensor** from Aosong with an I2C interface. Used in [Workshop 03 - Connect and Read Sensor Data](/docs/developers/curriculum/dapps/iot/input-and-write/01-connect-and-read-sensor-data) to read environmental data and post it on-chain as transaction metadata or NFTs. ![AHT10 sensor](./img/aht10.webp) ## Features - Temperature: -40°C to +85°C - Accuracy ±0.3°C (typical), resolution 0.01°C - Humidity: 0-100% RH - Accuracy ±2% RH (typical), resolution 0.024% RH - I2C interface (SDA, SCL) - Operating voltage: 1.8V - 6.0V (3.3V recommended) - Low power consumption - **Factory calibrated** - no user calibration needed - Fast response time - Compact SMD package I2C address: `0x38` (fixed). ## Resources - [AHT10 datasheet](https://datasheet4u.com/datasheets/ASAIR/AHT10/1545327) - [Adafruit AHTX0 library](https://github.com/adafruit/Adafruit_AHTX0) - supports AHT10 and AHT20. - [Adafruit AHT20 tutorial](https://learn.adafruit.com/adafruit-aht20/arduino) - same library, similar API. --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware/aht10-temperature-humidity-sensor-i2c) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Cheap Yellow Display (CYD) The **Cheap Yellow Display (CYD)** is an affordable ESP32-based development board with an integrated 2.8" TFT display and resistive touch panel. It's the default board for any lesson in this section that needs a screen - Workshops 02, 04, and 05. ![CYD top view](./img/cyd-01.webp) ![CYD back view](./img/cyd-02.webp) ## Features - ESP32-WROOM-32 microcontroller (240 MHz dual-core) - 2.8" 320×240 ILI9341 TFT LCD display - Resistive touch panel (XPT2046) - MicroSD card slot - 3.5 mm audio jack with speaker amplifier - LiPo battery connector (with charging circuit) - 30+ GPIO pins - WiFi and Bluetooth - USB-C for programming and power ## Resources - [ESP32 CYD - Getting Started Guide](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) - community repo with the canonical `User_Setup.h` for `TFT_eSPI`. - [ESP32 CYD Pins reference](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/PINS.md) - [ESP32 CYD on Random Nerd Tutorials](https://randomnerdtutorials.com/cheap-yellow-display-esp32-2432s028r/) - [ESP32 CYD Case STL files](https://www.thingiverse.com/thing:6653040) - 3D-printable enclosure. --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware/cheap-yellow-display-cyd) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## ESP32-C3 The **ESP32-C3** is a cost-effective, RISC-V based microcontroller with WiFi and Bluetooth 5 (LE). It's the default board for every workshop in this section - modern architecture, low power, USB-C, and good Arduino IDE support. ![ESP32-C3 top](./img/esp32c3-01.webp) ![ESP32-C3 back](./img/esp32c3-02.webp) ![ESP32-C3 side](./img/esp32c3-03.webp) ## Features - RISC-V 32-bit single-core processor (up to 160 MHz) - 400 KB SRAM, 384 KB ROM - 2.4 GHz WiFi (802.11 b/g/n) - Bluetooth 5 (LE only) - 22 GPIOs, ADC, I2C, SPI, UART - USB-to-UART bridge for programming - USB-C for power / programming - Lower power consumption than the original ESP32 - Built-in USB support :::info ESP32-C3 quirks worth knowing - **No 5 GHz WiFi** - Connects only to 2.4 GHz networks. - **WiFi power workaround.** The Super Mini variant often needs `WiFi.setTxPower(WIFI_POWER_8_5dBm);` to connect reliably. See [Troubleshooting](/docs/developers/curriculum/dapps/iot/troubleshooting) for the full note. - **Default I2C pins** are `SDA = GPIO 8`, `SCL = GPIO 9`. ::: ## Resources - [ESP32-C3 Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf) - [ESP32-C3 Technical Reference Manual](https://www.espressif.com/sites/default/files/documentation/esp32-c3_technical_reference_manual_en.pdf) - [ESP32-C3 Super Mini pinout diagram](https://i0.wp.com/randomnerdtutorials.com/wp-content/uploads/2025/05/ESP32-C3-Super-Mini-Pinout-f.png?w=918&quality=100&strip=all&ssl=1) - [ESP32-C3 Super Mini case STL files](https://www.printables.com/model/1137008-esp32-c3c6h2s3-super-mini-case) - [ESP32-C3 on Random Nerd Tutorials](https://randomnerdtutorials.com/getting-started-esp32-c3-super-mini/) - [ESP32io.com](https://esp32io.com/) - wider ESP32 tutorial portal. --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware/esp32-c3) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## 1.3" OLED Display (SH1106, I2C) A compact **1.3" monochrome OLED display** with the SH1106 controller and I2C interface. Used in [Workshop 03 - Connect and Read Sensor Data](/docs/developers/curriculum/dapps/iot/input-and-write/01-connect-and-read-sensor-data) as an alternative to the CYD's TFT for showing sensor readings on a smaller, lower-power screen. ![1.3 inch OLED top](./img/oled-01.webp) ![1.3 inch OLED back](./img/oled-02.webp) ![1.3 inch OLED side](./img/oled-03.webp) ![1.3 inch OLED in use](./img/oled-04.webp) ## Features - 1.3" diagonal display - 128×64 pixel resolution - SH1106 controller - I2C interface (SDA, SCL) - 3.3V or 5V operation - High-contrast monochrome OLED - Wide viewing angle, no backlight needed - Low power consumption :::info Note on the SSD1306 vs SH1106 There's a near-identical 0.96" OLED that uses the **SSD1306** controller. Library and addresses differ slightly. The CardanoThings workshops use the SH1106 variant, but for the SSD1306 see the inline reference block in [Workshop 02 - Display Data](/docs/developers/curriculum/dapps/iot/read-and-output/02-display-data). ::: ## Resources - [SH1106 datasheet](https://www.velleman.eu/downloads/29/infosheets/sh1106_datasheet.pdf) - [Adafruit SH1106 library](https://github.com/winneymj/SH1106) - [U8glib library](https://github.com/olikraus/u8glib) - alternate driver supporting SH1106 and many others. - [Instructables - SH1106 1.3" OLED tutorial](https://www.instructables.com/How-to-Interface-With-OLED-13-Inch-LCD128x64/) --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware/oled-display-sh1106-13inch-i2c) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Hardware Reference Everything the workshops in this section use, in one place: what to get, what each part is called, and what substitutes work. Each component also has its own page with specs, pinouts, quirks, and datasheets. ## What you need Parts are named the way a supplier lists them, so the name below is the search term. These are commodity components sold by many vendors, so this page names the part rather than a shop. | Part | Search for | Needed by | | --- | --- | --- | | Microcontroller | **`ESP32-C3`** development board with USB-C. The workshops use the "Super Mini" form factor. | All workshops | | Board with screen | **`ESP32-2432S028R`**, widely sold as the **Cheap Yellow Display** or **CYD**: an ESP32 with a built-in 2.8" TFT and resistive touch. | Workshops 02, 04, 05 | | Display | 1.3" 128x64 monochrome I2C OLED with an **`SH1106`** controller. An **`SSD1306`** module is a common substitute and needs only a driver change in the sketch. | Workshop 03 (alternative to the CYD screen) | | Sensor | **`AHT10`** temperature and humidity sensor, I2C breakout. The **`AHT20`** is a drop-in upgrade. | Workshop 03 | | Actuator | Single-channel relay module with opto-isolation, rated for **3.3V logic**. | Workshop 02 | | Actuator | **`WS2812B`** addressable RGB LED ring, 12 LEDs. Sold as NeoPixel-compatible. | Workshop 02 | | Cabling | Breadboard, jumper wires, and a **USB data cable** (many cheap cables are charge-only and will not program the board). | All workshops | An ESP8266 or an original ESP32 works for most lessons, with pin numbers and occasionally a library differing from what the sketches use. ## Component reference ### Boards - **[Cheap Yellow Display (CYD)](./cheap-yellow-display-cyd.md)** - ESP32 with an integrated 2.8" TFT touchscreen. Used in Workshops 02, 04, and 05. - **[ESP32-C3](./esp32-c3.md)** - RISC-V SoC with WiFi and Bluetooth 5 (LE). The default board across all five workshops, and the page carries the WiFi transmit-power workaround the Super Mini variant often needs. ### Displays - **[1.3" OLED Display (SH1106, I2C)](./oled-display-sh1106-13inch-i2c.md)** - compact monochrome OLED, used in Workshop 03 as an alternative to the CYD's TFT. ### Sensors - **[AHT10 Temperature & Humidity Sensor (I2C)](./aht10-temperature-humidity-sensor-i2c.md)** - used in Workshop 03 to read environmental data and put it on-chain. ### Actuators - **[Relay Module 3V, 1 Channel](./relay-module-3v-1channel.md)** - switches AC or DC loads from a 3.3V microcontroller. Used in Workshop 02's "Light up the Tree" lesson. - **[WS2812B LED Ring (12 LEDs)](./ws2812b-led-ring-12.md)** - addressable RGB ring used to build the Epoch Clock in Workshop 02. If a board will not accept an upload or the serial monitor prints nothing, [Troubleshooting](/docs/developers/curriculum/dapps/iot/troubleshooting) covers the cable, driver, and baud-rate causes before you suspect the hardware. --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Relay Module (3V, 1 Channel) A low-voltage **3V single-channel relay module** with opto-isolation. Switches AC or DC loads safely from a 3.3V microcontroller (ESP32) without needing a level shifter. This is the part used in [Workshop 02 - Light up the Tree](/docs/developers/curriculum/dapps/iot/read-and-output/03-light-up-the-tree). ![Relay module top](./img/relay-01.webp) ![Relay module side](./img/relay-02.webp) ## Features - 3.3V operation (compatible with ESP32) - Single channel - Normally Open (NO) and Normally Closed (NC) contacts - Max load: **10A / 250VAC** or **10A / 30VDC** - Opto-isolated input for safety - LED indicator for relay status - Active LOW trigger - No external driver circuit needed :::danger High-voltage warning A relay can switch mains-voltage loads (110V / 220V), which can cause serious injury or death if mishandled. Read the safety section in [Workshop 02 - Light up the Tree](/docs/developers/curriculum/dapps/iot/read-and-output/03-light-up-the-tree) before wiring anything to mains. If unsure, stick to low-voltage LED loads. ::: ## Resources - [ESP32 IO - Relay tutorial](https://esp32io.com/tutorials/esp32-relay) - [Random Nerd - ESP32 relay module web server](https://randomnerdtutorials.com/esp32-relay-module-ac-web-server/) --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware/relay-module-3v-1channel) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## WS2812B LED Ring (12 LEDs) A circular **WS2812B LED ring with 12 individually addressable RGB LEDs** (NeoPixel-compatible). Each LED gives 24-bit colour control and the ring is daisy-chainable. Used in [Workshop 02 - Epoch Clock](/docs/developers/curriculum/dapps/iot/read-and-output/04-epoch-clock). ![WS2812B ring top](./img/led-ring-01.webp) ![WS2812B ring back](./img/led-ring-02.webp) ![WS2812B ring side](./img/led-ring-03.webp) ## Features - 12 WS2812B addressable RGB LEDs in a circle - 24-bit colour depth (16.7 million colours) - Single-wire data interface (DIN / DOUT) - 5V operation (3.3V logic compatible) - Cascadable via DOUT - Refresh rate up to 800 Hz - No external resistors needed - Perfect for circular displays and progress indicators :::danger Current draw warning At full white, each LED can draw up to **60 mA**. A 12-LED ring at full white can draw **720 mA** - more than most USB ports can supply. Run at low brightness when on USB power, or use an external 5V supply rated for ≥ 1 A. Full safety guidance is in [Workshop 02 - Epoch Clock](/docs/developers/curriculum/dapps/iot/read-and-output/04-epoch-clock). ::: ## Resources - [WS2812B datasheet](https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf) - [Adafruit NeoPixel library](https://github.com/adafruit/Adafruit_NeoPixel) - [FastLED library](https://github.com/FastLED/FastLED) - alternate library, more performant for large arrays. - [Adafruit NeoPixel Überguide](https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library-use) --- *Adapted from the [CardanoThings](https://cardanothings.io/hardware/ws2812b-led-ring-12) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Connect and Read Sensor Data Wire up your first sensor - an AHT10 temperature and humidity sensor over I2C - read it, and optionally show the readings on an OLED. ## Hardware requirements - ESP32-C3 (or ESP32) microcontroller. - AHT10 temperature and humidity sensor (I2C, factory-calibrated). - Optional: 1.3" SH1106 OLED display (I2C). - Breadboard and jumper wires. ## Introduction to the AHT10 The AHT10 is a high-precision digital temperature and humidity sensor from Aosong. Compared to basic sensors like the DHT-22, it's more accurate, lower-power, and factory-calibrated. - Temperature: -40°C to +85°C, ±0.3°C. - Humidity: 0-100% RH, ±2% RH. - I2C interface (only two wires for data). - 3.3V (compatible with ESP32). - Fast response, no calibration needed. The AHT10 talks I2C - simpler than the single-wire protocol on the DHT-22. I2C uses two wires (SDA + SCL) and supports multiple devices on the same bus. The Adafruit AHTX0 library covers both AHT10 and AHT20. ## Wiring the AHT10 The module typically has four pins: 1. **VCC** → 3.3V. 2. **GND** → GND. 3. **SDA** → GPIO 8 (standard SDA on ESP32). 4. **SCL** → GPIO 9 (standard SCL on ESP32). I2C needs pull-up resistors (4.7 kΩ - 10 kΩ) on SDA and SCL. Most AHT10 breakout modules include them. If yours doesn't, add them externally between the data/clock pins and VCC. :::info ESP32-C3 pinout reference Need a pinout reference for wiring? See the interactive ESP32-C3 pinout at [cardanothings.io](https://cardanothings.io), the official [ESP32-C3 datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf), or your board's specific schematic. Common pin protocols on the C3: **SPI** uses MOSI / MISO / SCK / SS-CS; **I2C** uses SDA / SCL (typically GPIO 8 / 9); **UART** uses TX / RX. ::: ### Install the library 1. Open Arduino IDE. 2. **Sketch → Include Library → Manage Libraries**. 3. Search for **Adafruit AHT10**. 4. Install - and accept its dependencies (Adafruit BusIO + Adafruit Unified Sensor) when prompted. :::info The library handles all the I2C protocol details. `getEvent()` returns both temperature and humidity in one call. It uses the standard ESP32 I2C pins (GPIO 8/9) automatically. Source: [github.com/adafruit/Adafruit_AHTX0](https://github.com/adafruit/Adafruit_AHTX0). ::: ## Basic sensor read Before adding a display, verify the sensor with serial-monitor output. This sketch reads every 500 ms and prints temperature + humidity. ```cpp // Include required libraries #include // Create sensor object Adafruit_AHT10 aht; void setup() { // Initialize serial communication for debugging (115200 baud rate) Serial.begin(115200); Serial.println("Adafruit AHT10 demo!"); // Initialize AHT10 sensor // begin() returns true if sensor is found, false if not found if (!aht.begin()) { Serial.println("Could not find AHT10? Check wiring"); while (1) delay(10); // Halt execution if sensor not found } Serial.println("AHT10 found"); } void loop() { // Create sensor event structures to hold readings sensors_event_t humidity, temp; // Read both temperature and humidity simultaneously // getEvent() populates temp and humidity objects with fresh data aht.getEvent(&humidity, &temp); // Print temperature reading to serial monitor Serial.print("Temperature: "); Serial.print(temp.temperature); Serial.println(" degrees C"); // Print humidity reading to serial monitor Serial.print("Humidity: "); Serial.print(humidity.relative_humidity); Serial.println("% rH"); // Wait 500ms before next reading delay(500); } ``` > Source: [`Workshop-03/examples/sensor-example/sensor-example.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/sensor-example/sensor-example.ino) If the serial monitor doesn't show readings, check wiring and pull-up resistors. :::info Other temperature sensors The AHT10 is one option among many. Adapting the code for others: - **SHT21 / SHT31** - similar I2C, different addresses (0x40 / 0x44). - **DHT11 / DHT22** - single-wire, different library and wiring. - **BME280** - also measures pressure, address 0x76 / 0x77. - **HTU21D** - similar to SHT21, address 0x40. Don't know what address your sensor uses? Run the I2C scanner sketch below to enumerate connected devices. :::
**Reference: I2C device scanner sketch** A small utility that scans every I2C address (0x00-0x7F) and prints which ones respond. Useful any time you don't know your sensor's or display's address, or when an I2C device isn't behaving and you want to confirm the chip is actually on the bus. No extra libraries needed - uses the built-in `Wire` library. **I2C pins:** - ESP32-C3: `SDA = GPIO 8`, `SCL = GPIO 9` - ESP32-CYD: `SDA = GPIO 27`, `SCL = GPIO 22` (check your board) ```cpp /* * ESP32 I2C Scanner * * Scans all I2C addresses and reports which devices respond. * Use this when you don't know your device's I2C address * or to verify connections. * * Compatible with: ESP32-C3, ESP32 CYD, and all ESP32 boards. */ #include // I2C pins - adjust these for your board // ESP32-C3: SDA = GPIO 8, SCL = GPIO 9 // ESP32 CYD: SDA = GPIO 27, SCL = GPIO 22 (may vary) #define I2C_SDA 8 #define I2C_SCL 9 void setup() { Serial.begin(115200); delay(1000); Serial.println(); Serial.println("========================================"); Serial.println("I2C Device Scanner"); Serial.println("========================================"); Serial.println(); Wire.begin(I2C_SDA, I2C_SCL); Serial.print("Scanning I2C bus on SDA=GPIO"); Serial.print(I2C_SDA); Serial.print(", SCL=GPIO"); Serial.println(I2C_SCL); Serial.println(); } void loop() { int devicesFound = 0; Serial.println("Scanning..."); // Scan all 128 possible I2C addresses (0x00 to 0x7F) for (byte address = 1; address < 127; address++) { Wire.beginTransmission(address); byte error = Wire.endTransmission(); if (error == 0) { Serial.print("Device found at address 0x"); if (address < 16) Serial.print("0"); Serial.print(address, HEX); Serial.println(); devicesFound++; } else if (error == 4) { Serial.print("Unknown error at address 0x"); if (address < 16) Serial.print("0"); Serial.print(address, HEX); Serial.println(); } } Serial.println(); if (devicesFound == 0) { Serial.println("No I2C devices found."); Serial.println("Check your wiring:"); Serial.println("- Is VCC connected to 3.3V?"); Serial.println("- Is GND connected to GND?"); Serial.println("- Are SDA and SCL connected correctly?"); } else { Serial.print("Found "); Serial.print(devicesFound); Serial.print(" device"); if (devicesFound > 1) Serial.print("s"); Serial.println(); } Serial.println("========================================"); Serial.println(); delay(5000); } ```
## Adding a 1.3" OLED display (SH1106) The SH1106-controlled 1.3" OLED also speaks I2C - so it shares the same SDA/SCL bus as the sensor. I2C supports multiple devices on one bus as long as they have different addresses. ### Wiring the OLED (sharing the I2C bus) 1. **VCC** → 3.3V (shared with the AHT10). 2. **GND** → GND (shared). 3. **SDA** → GPIO 8 (shared SDA). 4. **SCL** → GPIO 9 (shared SCL). :::info Complete wiring **Power and ground** - AHT10 VCC + OLED VCC → ESP32 3.3V. - AHT10 GND + OLED GND → ESP32 GND. **I2C bus sharing** - AHT10 → address `0x38` on GPIO 8/9. - 1.3" SH1106 OLED → address `0x3C` (or `0x3D`) on the same GPIO 8/9. I2C supports multi-device because each device has a unique address. The microcontroller addresses each one individually. **I2C addresses** - AHT10: `0x38` (fixed). - 1.3" OLED (SH1106): `0x3C` or `0x3D` (check the display docs or use an I2C scanner). ::: ## Displaying sensor data This sketch reads the AHT10 and renders temperature + humidity on the OLED, refreshing every 2 seconds. ```cpp // Include necessary libraries #include // I2C communication library (built-in) #include // Adafruit AHT10 library #include // Adafruit graphics library #include // Adafruit SH1106 OLED library (for 1.3" OLED) // OLED display settings #define SCREEN_WIDTH 128 // OLED display width in pixels #define SCREEN_HEIGHT 64 // OLED display height in pixels #define OLED_RESET -1 // Reset pin (not used, set to -1) #define SCREEN_ADDRESS 0x3C // I2C address (usually 0x3C or 0x3D) // Create sensor and display objects Adafruit_AHT10 aht; // Initialize AHT10 sensor Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Variables to store sensor readings float temperature = 0; // Current temperature reading float humidity = 0; // Current humidity reading // Variables for timing sensor reads unsigned long lastRead = 0; // Timestamp of last sensor read const unsigned long readInterval = 2000; // Read every 2 seconds void setup() { // Initialize serial communication for debugging (115200 baud rate) Serial.begin(115200); Serial.println("Adafruit AHT10 demo!"); // Initialize AHT10 sensor if (!aht.begin()) { Serial.println("Could not find AHT10? Check wiring"); while (1) delay(10); // Halt if sensor not found } Serial.println("AHT10 found"); // Initialize OLED display if (!display.begin(SCREEN_ADDRESS)) { Serial.println("SH1106 allocation failed"); for (;;); // Don't proceed, loop forever } Serial.println("OLED Display initialized!"); // Clear display and show startup message display.clearDisplay(); display.setTextSize(1); display.setTextColor(SH110X_WHITE); display.setCursor(0, 0); display.println("Initializing..."); display.display(); delay(1000); // Clear and show ready message display.clearDisplay(); display.setCursor(0, 0); display.println("Ready!"); display.display(); delay(500); } void loop() { // Get current time in milliseconds unsigned long currentMillis = millis(); // Check if enough time has passed since last sensor read if (currentMillis - lastRead >= readInterval) { readSensorData(); // Read from sensor displayData(); // Update display lastRead = currentMillis; // Update last read timestamp } } void readSensorData() { // Create sensor event structures to hold readings sensors_event_t humidity_event, temp_event; // Read both temperature and humidity from sensor // The getEvent() function populates temp and humidity objects with fresh data aht.getEvent(&humidity_event, &temp_event); // Store readings in global variables temperature = temp_event.temperature; // Temperature in Celsius humidity = humidity_event.relative_humidity; // Humidity as percentage (0-100) // Print readings to serial monitor for debugging Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" degrees C"); Serial.print("Humidity: "); Serial.print(humidity); Serial.println("% rH"); } void displayData() { // Clear display buffer display.clearDisplay(); // Display temperature label and value display.setTextSize(1); display.setCursor(0, 0); display.print("Temperature"); display.setCursor(0, 14); display.setTextSize(3); display.print(temperature, 1); // Format to 1 decimal place display.println("C"); // Display humidity label and value display.setTextSize(1); display.setCursor(0, 52); display.print("Humidity: "); display.print(humidity, 1); // Format to 1 decimal place display.println("%"); // Update display to show all changes display.display(); } ``` > Source: [`Workshop-03/examples/display-sensor-data/display-sensor-data.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/display-sensor-data/display-sensor-data.ino) You'll need the Adafruit SH110X library (with Adafruit GFX as a dependency) for the OLED, plus the AHT10 stack you already installed. ### Display layout The OLED shows: - "Temperature" label at the top in small text. - Temperature value in size-3 text with the °C unit. - Humidity label and value at the bottom in small text with the % unit. Customise freely - add timestamps, graphs, dew-point calculations, anything. ### Troubleshooting If you get "Failed to find AHT10 sensor!": - Check VCC/GND/SDA/SCL connections. - Confirm I2C pull-ups are present (usually on the module). - Verify pin assignments (GPIO 8 SDA, GPIO 9 SCL on ESP32). - Confirm the sensor has 3.3V power. - Run an I2C scanner to confirm the sensor is at `0x38`. - Make sure no other I2C devices conflict on the same bus. - Confirm all libraries installed: Adafruit AHT10 + BusIO + Unified Sensor. ## Further Resources - [Adafruit AHT10 Library](https://github.com/adafruit/Adafruit_AHTX0) - for AHT10/AHT20 sensors. - [I2C tutorial](https://www.youtube.com/watch?v=pxhg2Rwm_h8) - protocol overview. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/03-input-and-write/connect-and-read-sensor-data) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-03](https://github.com/CardanoThings/Workshops/tree/main/Workshop-03).* --- ## Build your own API to put data on-chain Build a small Node.js API server that bridges your microcontroller to the chain: it fetches wallet balance, builds and submits transactions, and accepts sensor readings via POST that get attached as transaction metadata. We use [Mesh SDK](https://meshjs.dev/) (open-source TypeScript SDK for Cardano) with the [Koios](https://api.koios.rest/) provider - Koios is free and needs no API key, perfect for development. ## Setting up Node.js **Prerequisites:** - Node.js 14+ and npm. - A text editor (VS Code, Cursor, etc.). **Create the project:** 1. Make a new directory and `cd` into it. 2. `npm init -y` - initialise. 3. `npm install express` - install Express. ### Basic Express server ```javascript // Import required Node.js packages // Create Express application instance const app = express(); // Server port number const PORT = 3000; // GET endpoint for health check // Useful for testing if server is running // URL: http://localhost:3000/health app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Start server and listen on specified port app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); ``` > Source: [`Workshop-03/examples/basic-nodejs-api/basic-api.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/basic-nodejs-api/basic-api.js) Make sure your `package.json` has `"type": "module"` so ESM imports work: ```json { "name": "basic-nodejs-api", "version": "1.0.0", "description": "Basic Node.js API server", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5" } } ``` Run it: ```bash node basic-api.js ``` The server runs at `http://localhost:3000`. Hit `http://localhost:3000/health` in a browser or [Insomnia](https://insomnia.rest/) to verify. ## Adding a POST endpoint Now add a POST endpoint that accepts data, an in-memory store, CORS, and JSON parsing. Install CORS: ```bash npm install cors ``` ```javascript // Import required Node.js packages // Create Express application instance const app = express(); // Server port number const PORT = 3000; // Store received data in memory // In a production app, you would use a database instead let storedData = null; // Middleware: Enable CORS to allow requests from different origins app.use(cors()); // Middleware: Parse JSON request bodies app.use(express.json()); // POST endpoint to receive and store data // URL: http://localhost:3000/data app.post('/data', async (req, res) => { try { // Extract data from request body const data = req.body; // Store the data in a variable storedData = data; // Log received data to console for debugging console.log('Received and stored data:', data); // Return success response res.json({ success: true, message: 'Data received and stored successfully.', data: data }); } catch (error) { // Handle errors and return error response console.error('Error:', error); res.status(500).json({ success: false, error: error.message }); } }); // GET endpoint to retrieve stored data // URL: http://localhost:3000/data app.get('/data', (req, res) => { try { if (storedData === null) { return res.status(404).json({ success: false, message: 'No data has been stored yet. Send a POST request to /data first.' }); } // Return the stored data res.json({ success: true, data: storedData }); } catch (error) { // Handle errors and return error response console.error('Error:', error); res.status(500).json({ success: false, error: error.message }); } }); // GET endpoint for health check // Useful for testing if server is running // URL: http://localhost:3000/health app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Start server and listen on specified port app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); ``` > Source: [`Workshop-03/examples/basic-nodejs-api/server.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/basic-nodejs-api/server.js) **Test:** 1. Start: `node server.js`. 2. POST to `http://localhost:3000/data` with body `{"temperature": 23.5, "humidity": 65.2}`. 3. GET `http://localhost:3000/data` to retrieve. Data is in memory - it disappears on restart. In production, use a database. ## Adding Mesh for blockchain interaction Now bring [Mesh SDK](https://meshjs.dev/) in to interact with the chain. ```bash npm install @meshsdk/core @meshsdk/wallet ``` No API key required - Koios is free. If you hit rate limits, sign up for the free tier at [koios.rest](https://koios.rest/). Use `'preprod'` for testnet, `'api'` for mainnet. For wallet operations, you'll need a mnemonic (seed phrase). The example code uses a `mnemonic` array - fine for examples, but in production always load from environment variables. Use a **testnet** wallet for development. ## Fetching wallet balance with Mesh A standalone script - initialise a wallet from a mnemonic, fetch balance, log it. ```javascript // Import Mesh SDK components // Initialize Koios provider for Preprod Testnet // Koios is free to use and doesn't require an API key // 'preprod' = Preprod testnet, 'api' = Mainnet const provider = new KoiosProvider('preprod'); // Initialize wallet using mnemonic // WARNING: This is for example purposes only! Never hardcode your mnemonic in production code! // In production, always use environment variables: process.env.WALLET_MNEMONIC?.split(' ') || [] // Replace with your actual 12 or 24 word mnemonic phrase from your testnet wallet const mnemonic = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10", "word11", "word12"]; // Create MeshCardanoHeadlessWallet instance // This wallet will be used to interact with the Cardano blockchain // fromMnemonic is async (no separate init() step needed) const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet (Preprod), 1 = mainnet walletAddressType: AddressType.Base, fetcher: provider, // Provider for fetching blockchain data submitter: provider, // Provider for submitting transactions mnemonic // Array of mnemonic words }); // Function to fetch and log wallet balance async function fetchWalletBalance() { try { // Get wallet address // The change address is the address where change from transactions is sent const address = await wallet.getChangeAddressBech32(); console.log('Wallet Address:', address); // Get wallet balance using Mesh's built-in method // Returns an array of assets: [{ unit: 'lovelace', quantity: '...' }, ...] // The first item is always lovelace (ADA), followed by any native tokens const balanceArray = await wallet.getBalanceMesh(); // Extract lovelace from the balance array // Find the item with unit 'lovelace' and get its quantity const lovelaceAsset = balanceArray.find(asset => asset.unit === 'lovelace'); const balanceLovelace = lovelaceAsset ? parseInt(lovelaceAsset.quantity) : 0; // Convert Lovelace to ADA // 1 ADA = 1,000,000 Lovelace const balanceADA = balanceLovelace / 1000000; // Log wallet information to console console.log('Wallet Balance:', balanceADA, 'ADA'); console.log('Balance in Lovelace:', balanceLovelace); } catch (error) { // Handle any errors that occur during balance fetching console.error('Error fetching wallet balance:', error); } } // Call the function to fetch and log wallet balance fetchWalletBalance(); ``` > Source: [`Workshop-03/examples/mesh-basics/wallet-balance.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/mesh-basics/wallet-balance.js) Run with `node wallet-balance.js` - your wallet address and balance print to the console. :::info - Always use testnet wallets for development. - Never expose mnemonics in code. - Koios is free and needs no API key. ::: ## Creating and submitting transactions Now use [`MeshTxBuilder`](https://meshjs.dev/apis/txbuilder/basics) to send tADA to another address with metadata attached. ```javascript // Import Mesh SDK components // Initialize Koios provider for Preprod Testnet // Koios is free to use and doesn't require an API key // 'preprod' = Preprod testnet, 'api' = Mainnet const provider = new KoiosProvider('preprod'); // Initialize wallet using mnemonic // WARNING: This is for example purposes only! Never hardcode your mnemonic in production code! // In production, always use environment variables: process.env.WALLET_MNEMONIC?.split(' ') || [] // Replace with your actual 12 or 24 word mnemonic phrase from your testnet wallet const mnemonic = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10", "word11", "word12"]; // Create MeshCardanoHeadlessWallet instance // This wallet will be used to create and sign transactions // fromMnemonic is async (no separate init() step needed) const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet (Preprod), 1 = mainnet walletAddressType: AddressType.Base, fetcher: provider, // Provider for fetching blockchain data submitter: provider, // Provider for submitting transactions mnemonic // Array of mnemonic words }); // Function to create and submit a transaction with metadata async function sendTransaction() { try { // PingPong wallet address - this wallet will automatically refund the transaction minus fees within 60 seconds // Perfect for testing transactions on the Cardano Preprod testnet // The PingPong wallet sends your funds back automatically, making it ideal for testing const recipientAddress = 'addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w'; // Amount to send in ADA // This will be converted to Lovelace (1 ADA = 1,000,000 Lovelace) const amountADA = 10.0; // Send 10 ADA const amountLovelace = Math.floor(amountADA * 1000000); // Convert to Lovelace // Transaction metadata // Metadata allows you to attach additional data to transactions that is permanently stored on the blockchain // Metadata labels must be numbers between 0 and 65535 // Label 674 = Message (CIP-20 standard for transaction messages) const metadata = { 674: { // Message label (CIP-20 standard) msg: ['Hello from CardanoThings!', 'This is a test transaction with metadata.'] } }; // Log transaction details before creating it console.log('Creating transaction...'); console.log('Recipient:', recipientAddress); console.log('Amount:', amountADA, 'ADA'); console.log('Metadata:', JSON.stringify(metadata, null, 2)); // Get wallet UTXOs (Unspent Transaction Outputs) // UTXOs represent available funds in your wallet that can be spent const utxos = await wallet.getUtxosMesh(); // Get change address // This is where any remaining funds (after transaction amount and fees) will be sent const changeAddress = await wallet.getChangeAddressBech32(); // Initialize MeshTxBuilder // MeshTxBuilder provides low-level APIs for building transactions with fine-grained control // This gives you more control than the higher-level wallet.buildTx() method const txBuilder = new MeshTxBuilder({ fetcher: provider, // Provider for fetching blockchain data needed for transaction building verbose: false // Set to true for detailed debugging information during transaction building }); // Build the transaction using MeshTxBuilder // This approach gives you more control over the transaction structure const unsignedTx = await txBuilder .txOut(recipientAddress, [{ unit: 'lovelace', quantity: amountLovelace.toString() }]) // Output: send lovelace to recipient address .changeAddress(changeAddress) // Address to receive change (remaining funds after transaction) .metadataValue(674, metadata[674]) // Attach message metadata (label 674, CIP-20 standard) .selectUtxosFrom(utxos) // Automatically select UTXOs from the provided list to fund the transaction .complete(); // Finalize the transaction structure and return the unsigned transaction // Sign the transaction with your wallet's private key // This proves that you own the wallet and authorizes the transaction const signedTx = await wallet.signTx(unsignedTx); // Submit the signed transaction to the Cardano network // The transaction will be broadcast to the network and included in the next block const txHash = await wallet.submitTx(signedTx); // Log success message and transaction details console.log('Transaction submitted successfully!'); console.log('Transaction Hash:', txHash); console.log('View on Cardano Explorer:', `https://preprod.cardanoscan.io/transaction/${txHash}`); console.log('Metadata will be visible on the blockchain explorer'); } catch (error) { // Handle any errors that occur during transaction creation or submission console.error('Error creating or submitting transaction:', error); } } // Call the function to create and submit transaction sendTransaction(); ``` > Source: [`Workshop-03/examples/mesh-basics/send-transaction.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/mesh-basics/send-transaction.js) The `recipientAddress` is preset to the CardanoThings PingPong wallet - it bounces test transactions back to you within ~60 seconds, which is convenient for testing flows. Run with `node send-transaction.js` and view the resulting tx on [preprod.cardanoscan.io](https://preprod.cardanoscan.io/). :::tip CardanoThings PingPong wallet The PingPong wallet auto-refunds your transaction (minus fees) within ~60 seconds, so you can iterate without finding a friend with a Preprod wallet. Address: `addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w` Preprod-only. ::: :::warning - Use only testnet addresses (`addr_test1...`) for development. - Each transaction needs a small fee (~0.17 to 0.2 ADA). - Submitted transactions can't be reversed - verify before sending. - Confirmation can take a few seconds to minutes. ::: ## Putting it all together Combine Express + Mesh + Koios into one server. `GET /wallet` returns wallet info; `POST /data` accepts sensor readings and submits a transaction with the data as metadata to the PingPong wallet. ```javascript // Import required Node.js packages // Create Express application instance const app = express(); // Server port number const PORT = 3000; // Initialize Koios provider for Preprod Testnet // Koios is free to use and doesn't require an API key const provider = new KoiosProvider( 'preprod' // Network: 'preprod' for testnet, 'api' for mainnet ); // Initialize wallet using mnemonic // WARNING: This is for example purposes only! Never hardcode your mnemonic in production code! // In production, always use environment variables: process.env.WALLET_MNEMONIC?.split(' ') || [] // Replace with your actual 12 or 24 word mnemonic phrase from your testnet wallet const mnemonic = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10", "word11", "word12"]; // Create MeshCardanoHeadlessWallet instance // This wallet will be used to interact with the Cardano blockchain // fromMnemonic is async (no separate init() step needed) const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet (Preprod), 1 = mainnet walletAddressType: AddressType.Base, fetcher: provider, // Provider for fetching blockchain data submitter: provider, // Provider for submitting transactions mnemonic // Array of mnemonic words }); // Middleware: Enable CORS to allow requests from different origins // This allows your microcontroller to make requests to this API from a different domain app.use(cors()); // Middleware: Parse JSON request bodies // This automatically parses JSON data sent in POST/PUT requests app.use(express.json()); // GET endpoint to retrieve wallet information // Returns wallet address, balance, and network information app.get('/wallet', async (req, res) => { try { // Get wallet address // The change address is the address where change from transactions is sent const address = await wallet.getChangeAddressBech32(); // Get wallet balance using Mesh's built-in method // Returns an array of assets: [{ unit: 'lovelace', quantity: '...' }, ...] // The first item is always lovelace (ADA) const balanceArray = await wallet.getBalanceMesh(); // Extract lovelace from the balance array // Find the item with unit 'lovelace' and get its quantity const lovelaceAsset = balanceArray.find(asset => asset.unit === 'lovelace'); const balanceLovelace = lovelaceAsset ? parseInt(lovelaceAsset.quantity) : 0; // Convert Lovelace to ADA // 1 ADA = 1,000,000 Lovelace const balanceADA = balanceLovelace / 1000000; // Return wallet information as JSON response res.json({ success: true, address: address, // Wallet address balance: { lovelace: balanceLovelace, // Balance in Lovelace ada: balanceADA // Balance in ADA }, network: 'preprod' // Network: preprod testnet }); } catch (error) { // Handle errors and return error response console.error('Error:', error); res.status(500).json({ success: false, error: error.message }); } }); // POST endpoint to receive sensor data and create a transaction // URL: http://localhost:3000/data // Request Body: { temperature: 23.5, humidity: 65.2 } app.post('/data', async (req, res) => { try { // Extract sensor data from request body const { temperature, humidity } = req.body; // Validate required fields if (temperature === undefined || humidity === undefined) { return res.status(400).json({ success: false, error: 'temperature and humidity are required' }); } // Generate timestamp server-side when data is received const timestamp = Date.now(); console.log('Received sensor data:', { temperature, humidity, timestamp }); // PingPong wallet address - this wallet will automatically refund the transaction minus fees within 60 seconds // Perfect for testing transactions on the Cardano Preprod testnet const recipientAddress = 'addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w'; // Amount to send in ADA (convert to Lovelace: 1 ADA = 1,000,000 Lovelace) const amountADA = 10.0; // Send 10 ADA const amountLovelace = Math.floor(amountADA * 1000000); // Create transaction metadata with sensor data // Label 674 = Message (CIP-20 standard for transaction messages) const transactionMetadata = { 674: { // Message label (CIP-20 standard) msg: [ `Sensor Data: Temperature ${temperature}°C, Humidity ${humidity}%RH`, `Timestamp: ${timestamp}` ] } }; // Get wallet UTXOs (Unspent Transaction Outputs) // UTXOs represent available funds in your wallet that can be spent const utxos = await wallet.getUtxosMesh(); // Get change address // This is where any remaining funds (after transaction amount and fees) will be sent const changeAddress = await wallet.getChangeAddressBech32(); // Initialize MeshTxBuilder // MeshTxBuilder provides low-level APIs for building transactions const txBuilder = new MeshTxBuilder({ fetcher: provider, // Provider for fetching blockchain data verbose: false // Set to true for detailed debugging information during transaction building }); // Build the transaction using MeshTxBuilder // This uses the same code pattern as the POST /transaction endpoint const unsignedTx = await txBuilder .txOut(recipientAddress, [{ unit: 'lovelace', quantity: amountLovelace.toString() }]) // Output: send lovelace to recipient .changeAddress(changeAddress) // Address to receive change .metadataValue(674, transactionMetadata[674]) // Attach metadata with sensor data (label 674) .selectUtxosFrom(utxos) // Automatically select UTXOs to fund the transaction .complete(); // Finalize the transaction structure // Sign the transaction with your wallet's private key // This proves that you own the wallet and authorizes the transaction const signedTx = await wallet.signTx(unsignedTx); // Submit the signed transaction to the Cardano network // The transaction will be broadcast to the network and included in the next block const txHash = await wallet.submitTx(signedTx); // Return success response with transaction details res.json({ success: true, message: 'Sensor data received and transaction submitted successfully', txHash: txHash, // Transaction hash (unique identifier) explorerUrl: `https://preprod.cardanoscan.io/transaction/${txHash}`, // Link to view transaction on explorer sensorData: { temperature: temperature, humidity: humidity, timestamp: timestamp // Timestamp generated server-side when data was received } }); } catch (error) { // Handle errors and return error response console.error('Error processing sensor data or submitting transaction:', error); res.status(500).json({ success: false, error: error.message }); } }); // GET endpoint for health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Start server and listen on specified port app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); ``` > Source: [`Workshop-03/examples/mesh-nodejs-api/server.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/mesh-nodejs-api/server.js) Matching `package.json`: ```json { "name": "mesh-api", "version": "1.0.0", "description": "Node.js API with Mesh.js integration", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", "@meshsdk/core": "^1.7.0", "@meshsdk/wallet": "^1.7.0" } } ``` **Test it:** 1. `node server.js`. 2. `GET http://localhost:3000/wallet` - returns address + balance. 3. `POST http://localhost:3000/data` with `{"temperature": 23.5, "humidity": 65.2}` - auto-creates a transaction with the sensor data as CIP-20 message metadata. 4. Use the returned tx hash on [preprod.cardanoscan.io](https://preprod.cardanoscan.io/) to view metadata on-chain. :::warning Make sure your wallet has enough tADA for transaction amounts plus fees (~0.2 ADA per tx). Transactions are irreversible. ::: ## Next steps You now have a working API that takes sensor data and stores it on-chain as metadata. The next lesson upgrades this so each sensor reading becomes an **NFT** instead of just metadata - a unique on-chain digital item that can be collected, traded, displayed. ## Further Resources - [Mesh SDK](https://meshjs.dev/) - the SDK. - [Express.js Documentation](https://expressjs.com/) - the framework. - [Koios](https://api.koios.rest/) - free Cardano API. - [Insomnia](https://insomnia.rest/) - API client. - [Awesome JSON Viewer](https://github.com/rbrahul/Awesome-JSON-Viewer) - readable JSON in the browser. - [REST API Tutorial](https://www.restapitutorial.com/) - REST primer. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/03-input-and-write/build-your-own-api) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-03](https://github.com/CardanoThings/Workshops/tree/main/Workshop-03).* --- ## Mint Sensor Data on-chain The capstone for Workshop 03: turn the metadata-storing flow from the previous lesson into NFT minting. Each sensor reading becomes a unique on-chain item. ## What we're building By the end of this lesson, every time the microcontroller posts sensor data to the API, the API mints an NFT containing that reading. NFTs are permanent - they can't be deleted or modified once minted. **Prerequisites:** - The previous two lessons in this workshop (sensor + Node.js API). - Node.js and npm. - A Preprod testnet wallet with some tADA for fees. ## Understanding NFT minting **Minting** is just the term for "creating an NFT." Each NFT is a unique digital item on-chain - a permanent certificate that "this sensor reading happened at this time." **Policy ID** is a unique identifier for an NFT collection - like a label on a series. You use the same policy ID to mint multiple NFTs in the same collection or to look up the whole collection on a block explorer. The example NFT metadata structure used in this lesson: ```json { "policyId": { "tokenName": { "name": "Sensor Data NFT - 2024-01-15T10:30:00Z", "image": "https://cardanothings.io/nft.png", "mediaType": "image/png", "description": "Temperature and humidity sensor data", "author": "A CardanoThings.io User", "temperature": "23.5", "humidity": "65.2", "timestamp": 1705312200000 } } } ``` `policyId` and `tokenName` are generated automatically by the server. **Where the minting happens** is its own design choice. A microcontroller is too small to hold a wallet or build a transaction, so the signing always happens elsewhere, and there are two ways to arrange it. This lesson runs your own minting API: you own the native policy, hold the keys, and your server builds and submits every transaction, so nothing about the mint depends on a third party. The alternative is to POST to a managed minting service that holds a policy for you and signs on your behalf, which skips running any infrastructure but hands over key ownership and control. Either way the device holds no signing keys, only a credential that triggers the mint - treat that credential as a secret, since anything that can read it off the device can mint under your policy. ## Minting your first NFT This script connects to Preprod, sets up your wallet, builds and signs a minting transaction with [CIP-25](https://cips.cardano.org/cips/cip25/) metadata, and submits it. ```javascript // Import Mesh SDK components needed for minting NFTs // KoiosProvider: Connects to Cardano blockchain to read and submit data // MeshCardanoHeadlessWallet: Represents your wallet and handles signing transactions // MeshTxBuilder: Builds blockchain transactions step by step // ForgeScript: Creates the policy script that controls who can mint NFTs // resolveScriptHash: Converts the policy script into a Policy ID // stringToHex: Converts text names into hexadecimal format for blockchain // Step 1: Set up the blockchain provider // This connects you to the Cardano network // 'preprod' = testnet (free, for testing), 'api' = mainnet (real money) const provider = new KoiosProvider('preprod'); // Step 2: Set up your wallet // IMPORTANT: Replace these words with your actual wallet mnemonic phrase // NEVER share your mnemonic with anyone or commit it to GitHub! // In production, use environment variables: process.env.WALLET_MNEMONIC?.split(' ') const mnemonic = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10", "word11", "word12"]; // Step 3: Create your wallet instance // This wallet will be used to sign transactions and interact with the blockchain // fromMnemonic is async and builds a ready-to-use wallet - no separate init needed const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet (Preprod), 1 = mainnet walletAddressType: AddressType.Base, fetcher: provider, // Provider for reading blockchain data (like your balance) submitter: provider, // Provider for sending transactions to the network mnemonic // Your wallet's mnemonic words }); // Step 5: Get your wallet's UTXOs (Unspent Transaction Outputs) // UTXOs are like coins in your wallet - they represent available funds // We need these to pay for the transaction fees const utxos = await wallet.getUtxosMesh(); // Step 6: Get your change address // This is your wallet address where any leftover funds will be sent back const changeAddress = await wallet.getChangeAddressBech32(); // Step 7: Create a forging script (minting policy) // This script defines who can mint NFTs from this collection // withOneSignature means only your wallet can mint NFTs with this policy const forgingScript = ForgeScript.withOneSignature(changeAddress); // Step 8: Prepare the NFT metadata // This is the information that will be stored in your NFT // It can include name, description, image, and any custom data const demoAssetMetadata = { name: "Sensor Data NFT - 2024-01-15T10:30:00Z", // Name of your NFT image: "https://cardanothings.io/nft.png", // URL to the NFT image mediaType: "image/png", // Type of image file description: "Temperature and humidity sensor data", // Description of the NFT author: "A CardanoThings.io User", // Who created this NFT temperature: "23.5", // Sensor reading: temperature humidity: "65.2", // Sensor reading: humidity timestamp: Date.now(), // When this data was recorded }; // Step 9: Generate the Policy ID // The Policy ID is a unique identifier for your NFT collection // All NFTs minted with the same policy belong to the same collection const policyId = resolveScriptHash(forgingScript); console.log("Policy ID:", policyId); // Step 10: Create a unique token name // Each NFT needs a unique name within the collection // We add a timestamp to make sure each NFT has a different name const tokenName = "TemperatureNFT" + Date.now().toString(); // Step 11: Convert token name to hexadecimal // Blockchain requires names to be in hexadecimal format (base 16) const tokenNameHex = stringToHex(tokenName); // Step 12: Structure the metadata according to CIP-25 standard // CIP-25 is the Cardano standard for NFT metadata // The structure is: { policyId: { tokenName: { metadata } } } const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } }; // Step 13: Create a transaction builder // This tool helps us build the minting transaction step by step const txBuilder = new MeshTxBuilder({ fetcher: provider, // Provider for fetching blockchain data verbose: false, // Set to true for detailed logging (helpful for debugging) }); // Step 14: Build the minting transaction // This creates the transaction that will mint your NFT const unsignedTx = await txBuilder .mint("1", policyId, tokenNameHex) // Mint 1 NFT with the given policy and name .mintingScript(forgingScript) // Use our policy script .metadataValue(721, metadata) // Attach metadata (721 is the CIP-25 standard label) .changeAddress(changeAddress) // Where to send any leftover funds .selectUtxosFrom(utxos) // Which UTXOs to use for payment .complete(); // Finish building the transaction // Step 15: Sign the transaction // Your wallet signs the transaction to prove you authorized it // This is like signing a check - it proves the transaction came from you const signedTx = await wallet.signTx(unsignedTx); // Step 16: Submit the transaction to the blockchain // This sends your transaction to the Cardano network // The network will process it and create your NFT const txHash = await wallet.submitTx(signedTx); // Step 17: Check if the transaction was successful // If txHash exists, the transaction was submitted successfully if (txHash) { console.log("Transaction submitted successfully!"); console.log("Transaction Hash:", txHash); // You can view your transaction on the Cardano explorer console.log("View on Cardano Explorer:", `https://preprod.cardanoscan.io/transaction/${txHash}`); } else { console.error("Transaction submission failed!"); } ``` > Source: [`Workshop-03/examples/mesh-nft-basics/mint-nft.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/mesh-nft-basics/mint-nft.js) Matching `package.json`: ```json { "name": "mesh-nft-basics", "version": "1.0.0", "description": "Basic NFT minting and burning examples using Mesh SDK", "type": "module", "main": "mint-nft.js", "scripts": { "mint": "node mint-nft.js", "burn": "node burn-nft.js" }, "dependencies": { "@meshsdk/core": "^1.7.0", "@meshsdk/wallet": "^1.7.0" } } ``` Replace the mnemonic array with your testnet wallet's mnemonic, ensure your wallet has some tADA, then `node mint-nft.js`. Once the tx confirms (a moment later), view the NFT on [preprod.cardanoscan.io](https://preprod.cardanoscan.io/) by pasting the tx hash, or view the whole collection by pasting the policy ID. Your Yoroi NFTs tab will show it too. ## Posting sensor data from the microcontroller Now wire the AHT10 sensor + Node.js API together: the ESP32 reads the sensor and POSTs to your API every 5 minutes. :::info Send-once flag for testing The sketch has a `sendOnce` flag (defaults to `true`) - sends the data once instead of on a 5-minute loop. Useful while debugging, so you don't burn through transactions. Set to `false` for continuous mode. ::: ```cpp // Include necessary libraries #include // WiFi connectivity #include // HTTP client for API calls #include // JSON parsing and creation #include // Adafruit AHT10 library // Create AHT10 sensor object Adafruit_AHT10 aht; // WiFi credentials - replace with your network details const char* ssid = "Your SSID"; const char* password = "Your Password"; // Your API server URL - replace with your server's IP address const char* apiUrl = "http://YOUR_SERVER_IP:3000/data"; // Variables for timing sensor readings unsigned long lastReading = 0; // Timestamp of last reading const unsigned long readingInterval = 300000; // Read every 5 minutes (300000 milliseconds) // Send once flag - set to true for testing to avoid creating too many transactions const bool sendOnce = true; // If true, send sensor data only once bool dataSent = false; // Track if data has been sent void setup() { // Initialize serial communication for debugging Serial.begin(115200); Serial.println("Temperature Sensor NFT Demo!"); // Initialize AHT10 sensor if (!aht.begin()) { Serial.println("Could not find AHT10? Check wiring"); while (1) delay(10); // Halt if sensor not found } Serial.println("AHT10 found"); // Start WiFi connection WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println("Connected to WiFi"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); } void loop() { // Check if WiFi connection is still active if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); while (WiFi.status() != WL_CONNECTED) { delay(1000); } } // Get current time in milliseconds unsigned long currentMillis = millis(); // Check if enough time has passed since last reading if (currentMillis - lastReading >= readingInterval) { // Only send if sendOnce is false, or if sendOnce is true and data hasn't been sent yet if (!sendOnce || !dataSent) { sendSensorData(); // Read sensor and send to API dataSent = true; // Mark that data has been sent } lastReading = currentMillis; // Update last reading timestamp } } void sendSensorData() { // Create sensor event structures to hold readings sensors_event_t humidity_event, temp_event; // Read both temperature and humidity from sensor // The getEvent() function populates temp and humidity objects with fresh data aht.getEvent(&humidity_event, &temp_event); // Extract temperature and humidity values float temperature = temp_event.temperature; // Temperature in Celsius float humidity = humidity_event.relative_humidity; // Humidity as percentage (0-100) // Print sensor readings to serial monitor Serial.print("Temperature: "); Serial.print(temperature); Serial.println(" degrees C"); Serial.print("Humidity: "); Serial.print(humidity); Serial.println("% rH"); // Only proceed if WiFi is connected if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Initialize HTTP client with API URL http.begin(apiUrl); // Set content type header for JSON request http.addHeader("Content-Type", "application/json"); // Create JSON document to build request payload DynamicJsonDocument doc(512); // Add sensor data to JSON document doc["temperature"] = temperature; // Temperature in Celsius doc["humidity"] = humidity; // Humidity as percentage (0-100) doc["timestamp"] = millis(); // Current time in milliseconds // Serialize JSON document to string String jsonPayload; serializeJson(doc, jsonPayload); Serial.println("Sending data to API..."); Serial.println("Payload: " + jsonPayload); // Send POST request to API int httpResponseCode = http.POST(jsonPayload); // Check if request was successful if (httpResponseCode > 0) { // Get response body String response = http.getString(); Serial.println("HTTP Response Code: " + String(httpResponseCode)); Serial.println("Response: " + response); } else { // Print error if request failed Serial.println("Error in HTTP request"); Serial.println("HTTP Response Code: " + String(httpResponseCode)); } // Close HTTP connection http.end(); } } ``` > Source: [`Workshop-03/examples/post-sensor-data/post-sensor-data.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/post-sensor-data/post-sensor-data.ino) **Configure:** 1. Update `ssid` and `password` for your WiFi. 2. Replace `YOUR_SERVER_IP` with your computer's IP. 3. Make sure the Node.js API server is running and reachable on your network. 4. Upload, open the serial monitor at 115200 baud. :::info Finding your server IP - **Windows:** `ipconfig` → "IPv4 Address" under your active adapter. - **macOS / Linux:** `ifconfig` or `ip addr` → look for `inet` on `en0` (Mac) or `wlan0` (Linux). Both devices must be on the same network. ::: At this point, the previous workshop's API still creates plain transactions with metadata - not NFTs. Time to upgrade the server. ## Putting it all together Replace the previous server with one that mints an NFT per POST. ```javascript // Import required Node.js packages // Create Express application instance const app = express(); // Server port number const PORT = 3000; // Initialize Koios provider for Preprod Testnet // Koios is free to use and doesn't require an API key const provider = new KoiosProvider('preprod'); // Initialize wallet using mnemonic // IMPORTANT: Replace these words with your actual wallet mnemonic phrase // NEVER share your mnemonic with anyone or commit it to GitHub! const mnemonic = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10", "word11", "word12"]; const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet (Preprod), 1 = mainnet walletAddressType: AddressType.Base, fetcher: provider, submitter: provider, mnemonic }); // Middleware: Enable CORS to allow requests from different origins app.use(cors()); // Middleware: Parse JSON request bodies app.use(express.json()); // POST endpoint to receive sensor data and mint NFT // URL: http://localhost:3000/data app.post('/data', async (req, res) => { try { // Extract sensor data from request body const { temperature, humidity, timestamp } = req.body; // Validate required fields if (temperature === undefined || humidity === undefined) { return res.status(400).json({ success: false, error: 'temperature and humidity are required' }); } console.log('Received sensor data:', { temperature, humidity, timestamp }); // Get wallet UTXOs and change address const utxos = await wallet.getUtxosMesh(); const changeAddress = await wallet.getChangeAddressBech32(); // Create forging script for minting // This creates a simple policy that allows minting from the wallet address const forgingScript = ForgeScript.withOneSignature(changeAddress); const policyId = resolveScriptHash(forgingScript); // Create unique token name based on timestamp const tokenName = `SensorData_${timestamp || Date.now()}`; const tokenNameHex = stringToHex(tokenName); // Create NFT metadata following CIP-25 standard (label 721) const assetMetadata = { name: `Sensor Data NFT - ${new Date().toISOString()}`, image: "https://cardanothings.io/nft.png", mediaType: "image/png", description: 'Temperature and humidity sensor data', author: "A CardanoThings.io User", temperature: temperature.toString(), humidity: humidity.toString(), timestamp: timestamp || Date.now() }; // Structure metadata according to CIP-25 standard const metadata = { [policyId]: { [tokenName]: assetMetadata } }; // Initialize MeshTxBuilder const txBuilder = new MeshTxBuilder({ fetcher: provider, verbose: true }); // Build the minting transaction const unsignedTx = await txBuilder .mint("1", policyId, tokenNameHex) // Mint 1 token .mintingScript(forgingScript) // Use the forging script .metadataValue(721, metadata) // Attach NFT metadata (CIP-25 standard) .changeAddress(changeAddress) // Address to receive change .selectUtxosFrom(utxos) // Select UTXOs to fund the transaction .complete(); // Sign the transaction with your wallet const signedTx = await wallet.signTx(unsignedTx); // Submit the transaction to the network const txHash = await wallet.submitTx(signedTx); console.log('NFT minted successfully!'); console.log('Transaction Hash:', txHash); res.json({ success: true, message: 'Sensor data received and NFT minted successfully', txHash: txHash, explorerUrl: `https://preprod.cardanoscan.io/transaction/${txHash}`, policyId: policyId, tokenName: tokenName, metadata: assetMetadata }); } catch (error) { console.error('Error minting NFT:', error); res.status(500).json({ success: false, error: error.message }); } }); // GET endpoint for health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Start server and listen on specified port app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log('POST sensor data to: http://localhost:' + PORT + '/data'); }); ``` > Source: [`Workshop-03/examples/nodejs-nft-api/server.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/nodejs-nft-api/server.js) Matching `package.json`: ```json { "name": "nodes-nft-api", "version": "1.0.0", "description": "Node.js API server that receives sensor data and automatically mints NFTs on Cardano blockchain", "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", "@meshsdk/core": "^1.7.0", "@meshsdk/wallet": "^1.7.0" } } ``` **Setup:** 1. `npm install express cors @meshsdk/core @meshsdk/wallet`. 2. Replace the mnemonic array with your testnet mnemonic. 3. `node server.js`. 4. Make sure the Arduino sketch points at this server's IP and port 3000. :::info **Security:** never commit a mnemonic. Use environment variables and `.gitignore` your `.env`. **Testnet vs Mainnet:** Preprod here is `networkId: 0`. Mainnet is `1` and `new KoiosProvider('api')`. **Fees:** each NFT mint costs ~0.2 tADA on testnet. Make sure your wallet has enough for the volume you intend to send. ::: ## Burning NFTs Sometimes you want to destroy an NFT - clean up test mints, retire a series, etc. Burning permanently removes the NFT (the original transaction stays visible). **Burning is mint with a negative amount.** Mint `-1` of a token and you destroy one. **Important:** - You can only burn NFTs you minted. - Use the exact same policy ID and token name as the original mint. - Burning is permanent. - You still pay a transaction fee. ```javascript // Import Mesh SDK components needed for burning NFTs // IMPORTANT: Replace these words with your actual wallet mnemonic phrase // NEVER share your mnemonic with anyone or commit it to GitHub! const mnemonic = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10", "word11", "word12"]; // The exact name of the token you want to burn // This should match the tokenName used when minting the NFT const tokenName = ""; // Replace with your token name, e.g., "SensorData_1705312200000" // Initialize Koios provider for Preprod Testnet const provider = new KoiosProvider('preprod'); // Create the wallet instance // fromMnemonic is async and builds a ready-to-use wallet - no separate init needed const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet (Preprod), 1 = mainnet walletAddressType: AddressType.Base, fetcher: provider, // Provider for fetching blockchain data submitter: provider, // Provider for submitting transactions mnemonic // Array of mnemonic words }); // Get wallet UTXOs (Unspent Transaction Outputs) - these are like coins in your wallet const utxos = await wallet.getUtxosMesh(); // Get the change address where any leftover funds will be sent back const changeAddress = await wallet.getChangeAddressBech32(); // Create forging script for the policy // This must match the policy used when minting the NFT const forgingScript = ForgeScript.withOneSignature(changeAddress); // Generate the Policy ID from the forging script const policyId = resolveScriptHash(forgingScript); // Convert token name to hexadecimal format (required by blockchain) const tokenNameHex = stringToHex(tokenName); // Initialize transaction builder const txBuilder = new MeshTxBuilder({ fetcher: provider, // Provider for fetching blockchain data verbose: false, // Set to true for detailed debugging information during transaction building }); // Build the burn transaction // Minting "-1" is the same as burning 1 token const unsignedTx = await txBuilder .mint("-1", policyId, tokenNameHex) // Mint -1 token (burns 1 token) .mintingScript(forgingScript) // Use the same policy script .changeAddress(changeAddress) // Address to receive change .selectUtxosFrom(utxos) // Select UTXOs to fund the transaction .complete(); // Sign the transaction with your wallet const signedTx = await wallet.signTx(unsignedTx); // Submit the transaction to the blockchain const txHash = await wallet.submitTx(signedTx); // Log the transaction hash - you can view it on the Cardano explorer if (txHash) { console.log("Transaction submitted successfully!"); console.log("Transaction Hash:", txHash); console.log("View on Cardano Explorer:", `https://preprod.cardanoscan.io/transaction/${txHash}`); } else { console.error("Transaction submission failed!"); } ``` > Source: [`Workshop-03/examples/mesh-nft-basics/burn-nft.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-03/examples/mesh-nft-basics/burn-nft.js) Set `tokenName` to the exact name of the NFT you want to burn (case-sensitive). Run with `node burn-nft.js`. :::info Finding token names - **From the API response** when minting - there's a `tokenName` field. Copy it exactly. - **From your wallet** (Yoroi, Vespr, Eternl) - view your NFTs. - **From an explorer** - view your transaction on [CardanoScan](https://preprod.cardanoscan.io/) and check the asset details. ::: ## Next steps Some directions to explore: - Mint NFTs hourly, daily, or only on threshold crossings (e.g. temperature > X). - Build a website to display your NFTs and sensor data using Koios endpoints like [`/account_assets`](https://preprod.koios.rest/#post-/account_assets) or [`/policy_asset_info`](https://preprod.koios.rest/#get-/policy_asset_info). - Wire other sensors (light, motion) and mint when conditions trigger. - Build more complex apps with [Mesh SDK](https://meshjs.dev/). If running your own minting infrastructure is too much, [NMKR](https://nmkr.io/) is a paid service for NFT minting via API. There's a third-party [tutorial integrating NMKR with an ESP32 Cam](https://github.com/elRaulito/IoT-NMKR-integration-Open-Source-). If you want more, the next workshop builds a Cardano Ticker on the CYD - no soldering, no API server, all on-device. ## Further Resources - [Mesh SDK Documentation](https://meshjs.dev/) - full reference. - [Mesh Minting Guide](https://meshjs.dev/apis/txbuilder/minting) - minting examples (native + Plutus). - [CIP-25 NFT Metadata Standard](https://cips.cardano.org/cips/cip25/) - the spec. - [Preprod CardanoScan](https://preprod.cardanoscan.io/) - block explorer. - [Koios API](https://api.koios.rest/) - free Cardano API. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/03-input-and-write/mint-sensor-data-on-chain) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-03](https://github.com/CardanoThings/Workshops/tree/main/Workshop-03).* --- ## Workshop 03: Input and Write This workshop is about *writing* to the chain. You will read temperature and humidity from an AHT10 sensor, post the readings to a Node.js API, and use [Mesh SDK](https://meshjs.dev/) with the [Koios](https://api.koios.rest/) provider to turn each reading into a Cardano transaction - and eventually into an NFT. > Source code: [github.com/CardanoThings/Workshops/tree/main/Workshop-03](https://github.com/CardanoThings/Workshops/tree/main/Workshop-03) ## Steps 1. **[Connect and Read Sensor Data](./01-connect-and-read-sensor-data.md)** - Wire an AHT10 over I2C, read temperature and humidity, optionally render them on a small SH1106 OLED. 2. **[Build your own API to put data on-chain](./02-build-your-own-api.md)** - A Node.js + Express server that uses Mesh and Koios to fetch wallet balance and submit transactions with sensor data attached as metadata. 3. **[Mint Sensor Data on-chain](./03-mint-sensor-data-on-chain.md)** - Mint each sensor reading as an NFT using the CIP-25 metadata standard. Includes burning. ## What you'll need - Everything from Workshops 01 and 02. - The temperature and humidity sensor from the [Hardware reference](/docs/developers/curriculum/dapps/iot/hardware/), and optionally the OLED for on-device display. - Node.js 14+ and a Cardano testnet wallet with some tADA for transaction fees. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/03-input-and-write) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-03](https://github.com/CardanoThings/Workshops/tree/main/Workshop-03).* --- ## Arduino **Arduino** is an open-source electronics platform that makes it easy to build interactive projects. It's two things: hardware (microcontroller boards) and software (the Arduino IDE) that lets you program those boards to sense and control the physical world via sensors, motors, displays, and more. ## Brief history Arduino was created in 2005 by Hernando Barragán at the Interaction Design Institute Ivrea in Italy. It was developed as a tool for students and designers to prototype interactive objects without extensive electronics or programming knowledge. The platform was inspired by earlier open-source projects like Wiring and Processing. The first Arduino board (Arduino Serial) was released in 2005, followed by the popular Arduino Uno in 2010. Arduino has since grown into a global community with thousands of contributors and millions of users. ## Key features - **User-friendly hardware** - simple microcontroller boards with digital and analog I/O, USB, and power options. - **Arduino IDE** - free, cross-platform editor for writing, compiling, and uploading code. - **Extensive library ecosystem** - pre-written code for sensors, displays, motors, and communication protocols. - **Community support** - large online forums, tutorials, and shared projects. - **Modular** - works with "shields" (add-on boards) for expanded capability. - **Open-source** - all designs and software are open, encouraging customisation and forks. ## Resources - [Arduino official website](https://www.arduino.cc/) - [Arduino IDE download](https://www.arduino.cc/en/software) - [Arduino documentation](https://docs.arduino.cc/) - [Arduino forum](https://forum.arduino.cc/) - [Project Hub](https://create.arduino.cc/projecthub) - community-shared projects. - [PlatformIO](https://platformio.org/) - alternative IDE/build system, popular with experienced devs. --- *Adapted from the [CardanoThings](https://cardanothings.io/introductions/arduino) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## ESP32 / D1 Microcontrollers **ESP32** and **ESP8266** (commonly called "D1" in variants like the D1 Mini) are low-cost, low-power **system-on-chip (SoC) microcontrollers** developed by Espressif Systems. They integrate WiFi and Bluetooth connectivity, which makes them ideal for IoT applications, smart devices, and any wireless project. ## Brief history The **ESP8266** was released by Espressif in 2014 as a WiFi-enabled microcontroller. It changed IoT development overnight by making cheap WiFi connectivity feasible for embedded projects. In 2016, Espressif released the **ESP32** - a more powerful successor with dual-core processing, Bluetooth, and a wider feature set. ESP8266-based boards like the D1 Mini (also released around 2016) became staples in the maker community; ESP32 boards offer more performance for demanding projects. ## Key features - **WiFi connectivity** - built-in 802.11 b/g/n for internet access and network communication. - **Bluetooth** - ESP32 includes Bluetooth Classic and BLE; ESP8266 is WiFi-only. - **Microcontroller capabilities** - GPIO, ADC, PWM, I2C, SPI, UART for interfacing with sensors and peripherals. - **Low power consumption** - sleep modes for battery-powered devices. - **Programming flexibility** - Arduino IDE, MicroPython, ESP-IDF, and other frameworks. - **Integrated antennas** - on-board antennas for wireless, with the option for external ones. - **Community ecosystem** - extensive libraries, tutorials, and community support. The workshops in this section default to the **ESP32-C3** - see the [ESP32-C3 hardware page](/docs/developers/curriculum/dapps/iot/hardware/esp32-c3) for the specific variant used and known quirks. ## Resources - [Espressif official site](https://www.espressif.com/) - [ESP-IDF documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/) - [Arduino Core for ESP32](https://github.com/espressif/arduino-esp32) - [D1 Mini documentation](https://www.wemos.cc/en/latest/d1/d1_mini.html) - [PlatformIO community](https://community.platformio.org/) --- *Adapted from the [CardanoThings](https://cardanothings.io/introductions/esp32-d1-microcontrollers) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Introductions If you're new to embedded development or web APIs, here are quick primers on the supporting tech the workshops in this section use. None of this is Cardano-specific - these are the general-purpose tools you'll layer Cardano on top of. - **[Arduino](./arduino.md)** - the platform you'll write your sketches in. - **[ESP32 / D1 Microcontrollers](./esp32-d1-microcontrollers.md)** - the boards you'll run them on. - **[REST APIs](./rest-apis.md)** - how the workshops talk to Cardano data providers like Koios, Blockfrost, and Maestro. For Cardano itself, see the [Core Concepts](/docs/developers/curriculum/fundamentals/core-concepts/overview) section. --- *Adapted from the [CardanoThings](https://cardanothings.io/introductions) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## REST APIs **REST** (Representational State Transfer) is an architectural style for networked applications, especially web services. It defines a set of constraints and principles for building scalable, stateless, and interoperable APIs that use standard HTTP methods to interact with resources identified by URIs. Every Cardano data provider used in this section - [Koios](https://api.koios.rest/), [Blockfrost](https://blockfrost.io/), [Maestro](https://www.gomaestro.org/) - is a REST API. Understanding the basics makes the workshops easier to follow. ## Brief history REST was introduced by Roy Thomas Fielding in his 2000 doctoral dissertation at UC Irvine, where he co-authored the HTTP specification. Fielding distilled the architectural principles that made the World Wide Web successful into a formal style. REST emerged as a way to standardise web service design, in contrast to more complex approaches like SOAP. It became the dominant style for web APIs through the 2000s and 2010s, powering everything from social media to cloud computing. ## Key principles - **Stateless** - each request contains everything the server needs; no server-side session state. - **Client-server architecture** - clean separation between client and server concerns. - **Cacheable** - responses can be cached to improve performance. - **Uniform interface** - consistent use of HTTP methods (`GET`, `POST`, `PUT`, `DELETE`) and resource URIs. - **Layered system** - the architecture can be composed of hierarchical layers (proxies, gateways, etc.). - **Code on demand (optional)** - servers can extend client functionality by sending executable code. - **Resource-based** - every "thing" is a resource with a unique URI. ## How it shows up in this section - `GET https://preprod.koios.rest/api/v1/tip` - chain tip endpoint, used in [Workshop 01: API Setup](/docs/developers/curriculum/dapps/iot/the-basics/03-api-setup). - `POST /account_info` - wallet balance lookup, used in [Workshop 02: Fetch Wallet Balance](/docs/developers/curriculum/dapps/iot/read-and-output/01-fetch-wallet-balance). - `POST /address_utxos` - UTxO listing, used in [Workshop 05: Building the Backend](/docs/developers/curriculum/dapps/iot/qr-code-payments/05-building-the-backend). All return JSON. The microcontroller parses the JSON with [ArduinoJson](https://arduinojson.org/) and reacts. ## Resources - [RESTful API Tutorial](https://restfulapi.net/) - [REST API Design Guide](https://restfulapi.net/rest-api-design-tutorial-with-example/) - [HTTP Status Codes reference](https://restfulapi.net/http-status-codes/) - [OpenAPI Specification](https://swagger.io/specification/) - the de-facto standard for documenting REST APIs. --- *Adapted from the [CardanoThings](https://cardanothings.io/introductions/rest-apis) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Internet of Things (IoT) on Cardano This section is a hands-on course for connecting microcontrollers to Cardano. Across five workshops you will set up a wallet, fetch on-chain data with an ESP32, drive displays and relays from blockchain events, push sensor data on-chain as transactions and NFTs, build a Cardano price ticker, and finish with a working QR-code payment terminal that listens for confirmations on the Preprod testnet. The material was originally produced as the **CardanoThings** course (Project Catalyst Fund 11). The canonical web version remains at [cardanothings.io](https://cardanothings.io); the canonical source code is at [github.com/CardanoThings/Workshops](https://github.com/CardanoThings/Workshops). ## What you'll build - **Workshop 01 - The Basics.** Wallet on Preprod, Arduino IDE setup, your first Koios API call from an ESP32. - **Workshop 02 - Read and Output.** Fetch wallet balance on a schedule, display data on a TFT, drive a relay, build an Epoch Clock on a WS2812 ring. - **Workshop 03 - Input and Write.** Read an AHT10 sensor, build a Node.js + Mesh API, push sensor data on-chain as transaction metadata and NFTs. - **Workshop 04 - Cardano Ticker.** A multi-screen TFT ticker showing wallet balance, token prices (MinSwap), and NFT floors (Cexplorer). - **Workshop 05 - QR-Code Payments.** A microcontroller-hosted webserver, CIP-13 payment URIs, on-screen QR code, and on-chain payment confirmation via Koios. ## Prerequisites **Hardware** - An ESP32 or ESP8266 microcontroller. Most workshops use the **ESP32 Cheap Yellow Display (CYD)** for its built-in TFT; an **ESP32-C3** with an I2C OLED works as well. - USB data cable (not charge-only). - Workshop-specific extras: relay module (W2), AHT10 sensor (W3), WS2812 LED ring (W2), breadboard and jumper wires. **Software** - [Arduino IDE](https://www.arduino.cc/en/software/) - A Cardano wallet on **Preprod Testnet** ([Yoroi](https://yoroi-wallet.com/) is used in the workshops) - Node.js 14+ for Workshop 03 and 05 backend pieces **Knowledge** - Comfort with C-like syntax (Arduino sketches are C++). - Some JavaScript helps for Workshop 03 onward. - No prior Cardano experience required. ## Workshops 1. [Workshop 01: The Basics](./the-basics/overview.md) - wallet, IDE, first API call. 2. [Workshop 02: Read and Output](./read-and-output/overview.md) - fetch, display, control hardware from chain data. 3. [Workshop 03: Input and Write](./input-and-write/overview.md) - sensor data on-chain, NFT minting from a microcontroller. 4. [Workshop 04: Cardano Ticker](./cardano-ticker/overview.md) - multi-screen ticker for wallet, tokens, NFTs. 5. [Workshop 05: QR-Code Payments](./qr-code-payments/overview.md) - CIP-13 QR codes and on-chain payment confirmation. ## Alongside the workshops - [Introductions](./introductions/overview.md) - Arduino, ESP32 and D1 microcontrollers, and REST APIs, if any of those are new. - [Hardware](./hardware/overview.md) - the boards, displays, sensors, and actuators the workshops use, with specs and where to buy. - [Troubleshooting](./troubleshooting.md) - the upload, driver, and serial-monitor problems that stop a workshop. ## Community projects Builds from the community that extend or remix what's covered in this section: - **[NMKR ESP32-Cam](https://github.com/elRaulito/IoT-NMKR-integration-Open-Source-)** by [elRaulito](https://github.com/elRaulito) - Minting NFTs with NMKR directly from an ESP32-Cam. A drop-in alternative to the self-hosted Mesh approach in [Workshop 03](./input-and-write/overview.md) for builders who'd rather hand minting off to a managed service. - **[StarchMiner Lite](https://github.com/MadOrkestra/StarchMinerLite)** by [Mad Orkestra](https://github.com/madorkestra) - ESP32-based Starch miner. Goes potato. ## Attribution The course was authored by the **CardanoThings** team and funded by **Project Catalyst Fund 11**. Source code lives at [github.com/CardanoThings/Workshops](https://github.com/CardanoThings/Workshops); the original web version is [cardanothings.io](https://cardanothings.io). --- ## Getting Started Foundation for the payment terminal: a small webserver running on the microcontroller, serving HTML/CSS/JS from LittleFS. ## What we're building The full Workshop 05 flow: 1. Create a payment request via a website running directly on the microcontroller. 2. The microcontroller renders a QR code on the TFT display, scannable by a mobile wallet (Yoroi, Vespr, Begin). 3. The user signs and submits the transaction from their phone. 4. The microcontroller polls Koios and waits for the payment to confirm on-chain. 5. On confirmation, it shows a confirmation message on the TFT and updates the backend store. We'll use Koios (already familiar) to check for payments, briefly cover [CIP-13](https://cips.cardano.org/cip/CIP-0013) for payment URIs, and render QR codes scannable by most Cardano mobile wallets. ## Project structure The basic webserver project is laid out as: ``` basic-webserver/ ├── basic-webserver.ino # Main Arduino sketch ├── web_server.h # Web server header ├── web_server.cpp # Web server implementation ├── wifi_manager.h # WiFi manager header ├── wifi_manager.cpp # WiFi manager implementation ├── secrets.h # WiFi credentials (gitignored) ├── secrets.h.example # Template for secrets.h └── data/ # Files served by the webserver └── index.html # Default page at root ``` ### The `data` directory The `data/` directory holds everything served by the webserver - stored on the ESP32's flash via LittleFS. When a client requests a file (e.g. `/index.html`), the server reads it from LittleFS and streams it back. HTML, CSS, JavaScript, images - all go in here. ### LittleFS [LittleFS](https://github.com/littlefs-project/littlefs) is a lightweight filesystem for embedded devices like the ESP32. The webserver code initialises it in `webServerSetup()`: ```cpp if (!LittleFS.begin(true)) { Serial.println("ERROR: LittleFS Mount Failed"); return; } ``` `begin(true)` formats the filesystem if it doesn't exist - handy for first-time setup. :::tip File paths Files in `data/` are served at the root path: - `data/index.html` → `http://[IP]/index.html` or `http://[IP]/` - `data/style.css` → `http://[IP]/style.css` - `data/app.js` → `http://[IP]/app.js` ::: ## Creating the webserver Start with WiFi - most of this is already familiar from Workshop 01. `basic-webserver.ino`: ```cpp // Include necessary libraries #include // Include our custom header files #include "secrets.h" // WiFi credentials (not in git) #include "web_server.h" // HTTP web server for serving files #include "wifi_manager.h" // WiFi connection management void setup() { // Initialize serial communication for debugging // Serial communication lets us send messages to the computer via USB // 115200 is the baud rate (speed of communication) // You can view these messages in the Arduino IDE Serial Monitor Serial.begin(115200); delay(1000); // Give serial monitor time to connect Serial.println("Basic Web Server Example"); Serial.println("========================"); // Set up WiFi connection // WIFI_SSID is your WiFi network name // WIFI_PASSWORD is your WiFi password // These are defined in secrets.h (which you should create from // secrets.h.example) wifiManagerSetup(WIFI_SSID, WIFI_PASSWORD); // Wait for WiFi connection (with timeout) // We need WiFi to serve web pages, so we wait here Serial.println("Waiting for WiFi connection..."); const unsigned long wifiTimeout = 30000; // 30 seconds timeout (in milliseconds) const unsigned long wifiStart = millis(); // Record when we started waiting // Keep checking if WiFi is connected, but don't wait forever // millis() returns the number of milliseconds since the device started while (!wifiManagerIsConnected() && (millis() - wifiStart) < wifiTimeout) { wifiManagerLoop(); // Check WiFi status and try to connect delay(100); // Wait 100ms before checking again (don't waste CPU) } // Start web server if WiFi is connected if (wifiManagerIsConnected()) { webServerSetup(); } else { Serial.println("WiFi connection failed - web server not started"); } } void loop() { // Keep WiFi connection alive and check for reconnection if needed // This needs to be called regularly to maintain the connection wifiManagerLoop(); // Handle web server requests (runs asynchronously, but we call loop for // consistency) if (wifiManagerIsConnected() && !webServerIsRunning()) { // If WiFi just reconnected, start the server webServerSetup(); } webServerLoop(); } ``` `wifi_manager.cpp`: ```cpp /** * wifi_manager.cpp - WiFi connection management implementation * * This file implements WiFi connection management with automatic reconnection. * It stores WiFi credentials and periodically attempts to connect or reconnect * if the connection is lost. */ #include "wifi_manager.h" #include namespace { // Time to wait between reconnection attempts (5 seconds) // Prevents rapid reconnection attempts that could overwhelm the WiFi module const unsigned long WIFI_RETRY_INTERVAL_MS = 5000; // Maximum time to wait for a connection before retrying (12 seconds) // If connection takes longer than this, we assume it failed and retry const unsigned long WIFI_CONNECT_TIMEOUT_MS = 12000; // Stored WiFi credentials (set by wifiManagerSetup) const char *storedSsid = nullptr; const char *storedPassword = nullptr; // Timestamp of the last connection attempt // Used to implement retry intervals and connection timeouts unsigned long lastAttemptMs = 0; /** * Attempt to connect to WiFi * * Disconnects any existing connection, sets WiFi to station mode, * and begins connection with stored credentials. * * @param force If true, attempts connection immediately regardless of retry * interval */ void attemptConnection(bool force) { // Don't attempt connection if SSID is not set or empty if (storedSsid == nullptr || storedSsid[0] == '\0') { return; } const unsigned long now = millis(); // Respect retry interval unless forced (e.g., initial setup) if (!force && (now - lastAttemptMs) < WIFI_RETRY_INTERVAL_MS) { return; } lastAttemptMs = now; Serial.print("WiFi: connecting to "); Serial.println(storedSsid); // Disconnect any existing connection and clear stored credentials WiFi.disconnect(true, true); // Set WiFi to station mode (client mode, not access point) WiFi.mode(WIFI_STA); // Begin connection attempt WiFi.begin(storedSsid, storedPassword); } } // namespace /** * Initialize WiFi manager with credentials * * Stores the WiFi credentials and immediately attempts to connect. * * @param ssid The WiFi network name (SSID) * @param password The WiFi network password */ void wifiManagerSetup(const char *ssid, const char *password) { storedSsid = ssid; storedPassword = password; // Force immediate connection attempt on setup attemptConnection(true); } /** * Monitor and maintain WiFi connection * * Checks connection status and automatically attempts to reconnect * if disconnected. Uses timeout mechanism to detect failed connections. * Should be called repeatedly in the main loop(). */ void wifiManagerLoop() { // If already connected, no action needed if (WiFi.status() == WL_CONNECTED) { return; } const unsigned long now = millis(); // Check if connection attempt has timed out // Also handles case where no attempt has been made yet (lastAttemptMs == 0) const bool timedOut = (now - lastAttemptMs) > WIFI_CONNECT_TIMEOUT_MS || lastAttemptMs == 0; if (timedOut) { // Retry connection (respects retry interval) attemptConnection(false); } } /** * Check if WiFi is currently connected * * @return true if WiFi status is WL_CONNECTED, false otherwise */ bool wifiManagerIsConnected() { return WiFi.status() == WL_CONNECTED; } ``` `wifi_manager.h`: ```cpp /** * wifi_manager.h - Header file for WiFi connection management * * This file declares functions for managing WiFi connectivity on the ESP32. * It handles connection setup, connection monitoring, and provides status * information about the WiFi connection state. */ #ifndef WIFI_MANAGER_H #define WIFI_MANAGER_H #include /** * Initialize WiFi connection * * Sets up WiFi with the provided credentials and attempts to connect. * Call this once in setup() before using other WiFi functions. * * @param ssid The WiFi network name (SSID) * @param password The WiFi network password */ void wifiManagerSetup(const char *ssid, const char *password); /** * Update WiFi connection status * * Monitors the WiFi connection and attempts to reconnect if disconnected. * Call this repeatedly in loop() to maintain connection. */ void wifiManagerLoop(); /** * Check if WiFi is currently connected * * @return true if connected to WiFi, false otherwise */ bool wifiManagerIsConnected(); #endif ``` > Source: [`basic-webserver.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/basic-webserver/basic-webserver.ino), [`wifi_manager.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/basic-webserver/wifi_manager.cpp) The webserver uses the ESP32's built-in `WebServer` library on port 80, serving files from LittleFS. How it works: - **Server initialisation** - bound to port 80, listens for incoming requests. - **File serving** - when a client requests a file, the server checks LittleFS; if found, streams it with the right content type. - **Request handling** - `onNotFound()` catches every request and routes it to the file handler. Any path tries to find a matching file in LittleFS. - **Fallback** - if the file isn't found, serves `index.html` if available, otherwise 404. - **Continuous processing** - `webServerLoop()` must be called regularly from your main `loop()`. It's non-blocking, so other code can keep running. `web_server.cpp`: ```cpp #include "web_server.h" #include #include #include namespace { WebServer server(80); // Web server on port 80 bool serverStarted = false; // Flag to check if server is started // Get MIME type for HTML files String getContentType(String filename) { return "text/html"; } // Handle file requests void handleFileRequest() { String path = server.uri(); // Default to index.html for root path if (path == "/" || path == "") { path = "/index.html"; } // Ensure path starts with / if (!path.startsWith("/")) { path = "/" + path; } // Check if file exists in LittleFS if (LittleFS.exists(path)) { String contentType = getContentType(path); File file = LittleFS.open(path, "r"); if (file) { server.streamFile(file, contentType); file.close(); Serial.print("Served file: "); Serial.println(path); } else { server.send(500, "text/plain", "Error opening file"); Serial.print("Error opening file: "); Serial.println(path); } } else { // File not found - try index.html as fallback if (path != "/index.html" && LittleFS.exists("/index.html")) { File file = LittleFS.open("/index.html", "r"); if (file) { server.streamFile(file, "text/html"); file.close(); Serial.print("File not found, serving index.html: "); Serial.println(path); } else { server.send(404, "text/plain", "File not found"); } } else { // 404 Not Found server.send(404, "text/plain", "File not found"); Serial.print("404 - File not found: "); Serial.println(path); } } } } // namespace void webServerSetup() { // Initialize LittleFS file system if (!LittleFS.begin(true)) { Serial.println("ERROR: LittleFS Mount Failed"); return; } Serial.println("LittleFS mounted successfully"); // List all files in LittleFS (for debugging) File root = LittleFS.open("/"); File file = root.openNextFile(); Serial.println("Files in LittleFS:"); while (file) { Serial.print(" "); Serial.print(file.name()); Serial.print(" ("); Serial.print(file.size()); Serial.println(" bytes)"); file = root.openNextFile(); } // Serve files from root and all subdirectories server.onNotFound(handleFileRequest); // Start the server server.begin(); serverStarted = true; // Print the server's IP address Serial.print("Web server started on http://"); Serial.println(WiFi.localIP()); } // Function to handle incoming client requests void webServerLoop() { // If the server is started, handle incoming client requests if (serverStarted) { server.handleClient(); } } // Function to check if the server is running bool webServerIsRunning() { return serverStarted; } ``` `web_server.h`: ```cpp #ifndef WEB_SERVER_H #define WEB_SERVER_H #include // Initialize the web server (call after WiFi is connected) void webServerSetup(); // Handle server requests (call in loop()) void webServerLoop(); // Check if server is running bool webServerIsRunning(); #endif ``` > Source: [`web_server.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/basic-webserver/web_server.cpp) ## Creating web content The webserver serves files from `data/`. `index.html` is the entry point - open the ESP32's IP in a browser and you'll see this page. Add CSS and JS files later as needed. `data/index.html`: ```html Hello World! Hello World! This is a simple HTML page served by your microcontroller's web server. ``` > Source: [`data/index.html`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/basic-webserver/data/index.html) Once uploaded to LittleFS, this is reachable at `http://[ESP32_IP]/` or `http://[ESP32_IP]/index.html`. ## Uploading files to LittleFS Sketch uploads don't include the `data/` directory - you need a separate tool for that. ### LittleFS Upload Tool The [arduino-littlefs-upload](https://github.com/earlephilhower/arduino-littlefs-upload) plugin uploads `data/` to the ESP32's LittleFS. **Install:** 1. Download the VSIX from the [releases page](https://github.com/earlephilhower/arduino-littlefs-upload/releases). 2. Copy it to the Arduino IDE plugins folder: - **macOS / Linux:** `~/.arduinoIDE/plugins/` - **Windows:** `C:\Users\\.arduinoIDE\plugins\` 3. Restart Arduino IDE. **Use:** - **Windows / Linux:** `Ctrl+Shift+P` → Command Palette. - **macOS:** `⌘+Shift+P` → Command Palette. - Type and select **"Upload LittleFS to Pico/ESP8266/ESP32"**. Files in `data/` upload to LittleFS. :::info - ESP32 must be connected and the right port selected before uploading. - Upload **erases existing files** in LittleFS and replaces them with `data/`. - LittleFS upload is separate from sketch upload - do both. - After upload, restart the ESP32 or wait for the FS to be ready. ::: ## Testing the webserver ### Step 1: Upload the sketch 1. Open `basic-webserver.ino` in the Arduino IDE. 2. Make sure `secrets.h` exists (copy `secrets.h.example` and fill in WiFi). 3. Select your ESP32 board and port from **Tools**. 4. Click **Upload** (`Ctrl+U` / `⌘+U`). 5. Wait for upload, then open the Serial Monitor (115200 baud) to see the WiFi connection status. ### Step 2: Upload files to LittleFS 1. Make sure the ESP32 is still connected. 2. Command Palette (`Ctrl+Shift+P` / `⌘+Shift+P`). 3. Select **"Upload LittleFS to Pico/ESP8266/ESP32"**. 4. Wait for upload. ### Step 3: Find the IP address After WiFi connects, the Serial Monitor shows: ``` Web server started on http://192.168.1.100 ``` Note the IP - you'll need it. ### Step 4: Visit the site 1. From any device on the same WiFi, open a browser. 2. Go to `http://[ESP32_IP_ADDRESS]`. 3. You should see the "Hello World" page from `index.html`. :::info Troubleshooting - **Can't connect to WiFi:** check `secrets.h`. - **Can't reach the site:** confirm both devices are on the same WiFi. - **404 / blank page:** make sure you uploaded LittleFS after the sketch. - **No IP shown:** check the Serial Monitor for errors; confirm WiFi connected. ::: ## Further Resources - [arduino-littlefs-upload](https://github.com/earlephilhower/arduino-littlefs-upload) - upload tool for Arduino IDE 2.2.1+. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/05-qr-code-payments/getting-started) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05).* --- ## CIP-13 Integration A short detour into Cardano Improvement Proposals - specifically CIP-13, the URI scheme for payments and other wallet actions. ## What are CIPs? Cardano Improvement Proposals (CIPs) are formalised design documents that propose new features, standards, and processes for the Cardano ecosystem. Like Bitcoin's BIPs or Ethereum's EIPs, they ensure interoperability, document design decisions, and enable decentralised governance. The process is open and community-driven. Anyone can propose a CIP; it then goes through discussion, review, and approval stages before implementation. The full repository and process is at the [official CIP repository](https://cips.cardano.org/). :::info Not every wallet supports every CIP. Wallet teams pick which CIPs to implement based on their priorities. Always check wallet docs to verify support before depending on a specific CIP. ::: ## CIP-13: Cardano URI scheme [CIP-13](https://cips.cardano.org/cip/CIP-0013) defines a standard URI scheme for Cardano, with specific protocols for ADA transfers and other on-chain interactions. It's inspired by Bitcoin's BIP-21 - applications create URIs (links or QR codes) that initiate wallet actions when clicked or scanned. For payments, this means clickable / scannable links that open a compatible wallet pre-filled with the recipient address and amount - much better UX for donations, payments, and dApp interactions. The URI scheme uses the `web+cardano:` prefix (the prefix depends on context and browser requirements). For payment URIs, the address comes directly after the colon - that's the default protocol when no authority is specified. CIP-13 also supports other authorities beyond payments. The standard defines a **stake pool authority** (`//stake`) for delegation URIs (single pools or weighted lists). New authorities can be added in separate CIPs without modifying the core spec. **Upcoming enhancements.** Additional authorities are in development: - A **browse authority** that opens websites directly in the wallet's in-app browser (helpful for mobile dApp connections). - An **enhanced payment authority** with native asset and metadata support. These are tracked in the [CIP-0013 spec](https://cips.cardano.org/cip/CIP-0013). ## Cardano payment URIs A payment URI lets you create a link that pre-populates a wallet with a recipient and optional amount. Format: ``` web+cardano:{address}?amount={amount} ``` Where: - **`web+cardano:`** - the protocol prefix. - **`{address}`** - a valid Cardano address (Bech32). - **`amount`** - optional, decimal ADA (period as decimal separator, no commas). Example: ``` web+cardano:addr1qy...xyz?amount=10.5 ``` When a user clicks or scans, a compatible wallet opens with the recipient and amount pre-filled. The user still has to confirm and sign - security is preserved, no accidental payments. :::note Amount is in **ADA**, not lovelace. The wallet handles the lovelace conversion (1 ADA = 1,000,000 lovelace) and adds the transaction fee. ::: ## CIP-13 support This workshop uses Yoroi Mobile, which supports CIP-13 payment URIs. Install Yoroi Mobile on your phone with the same mnemonic from the previous workshop. :::info Testing payment URIs Wallet support varies. Test with the wallets your users actually use. Check the wallet's docs for the latest CIP support status. ::: ## Next steps Now that you can construct payment URIs, the next lesson renders one as a QR code on the TFT - scannable by a mobile wallet to send tADA. ## Further Resources - [CIP Repository](https://cips.cardano.org/) - all CIPs and the process. - [CIP-13 Specification](https://cips.cardano.org/cip/CIP-0013) - the full spec including grammar and security notes. - [CIP-13 microsite](https://cip13.cardanothings.io/) - examples, `web+cardano:` usage, wallet integration tracker from CardanoThings. - [CIP-1: CIP Process](https://cips.cardano.org/cips/cip1/) - how CIPs are proposed and structured. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/05-qr-code-payments/cip13-integration) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05).* --- ## QR-Code Creation Now turn the CIP-13 payment URI from the previous lesson into a QR code on the TFT - scannable by a mobile wallet. ## Introduction You already know how easy CIP-13 makes payment links. The next step is rendering the link as a QR code on the TFT display so a mobile wallet can scan it. The work splits into two parts: install the QR-code libraries, then write a small sketch that turns a string into a sprite and pushes it to the screen. With those pieces in place, we can drop in any payment URI in the next lessons. ## Installing required libraries Two libraries needed: **QRcodeDisplay** (the core QR generator) and **QRcode_eSPI** (the TFT_eSPI adapter). Install both - `QRcode_eSPI` depends on `QRcodeDisplay`. You should already have TFT_eSPI installed and configured from [Workshop 02](/docs/developers/curriculum/dapps/iot/read-and-output/02-display-data). ### Step 1: Install QRcodeDisplay 1. Open Arduino IDE. 2. **Sketch → Include Library → Manage Libraries** (or `Ctrl+Shift+I` / `⌘+Shift+I`). 3. Search for **"QRcodeDisplay"**. 4. Find **"QRcodeDisplay" by yoprogramo**. 5. Click **Install**. ### Step 2: Install QRcode_eSPI 1. In the same Library Manager, search **"QRcode_eSPI"**. 2. Find **"QRcode_eSPI" by yoprogramo**. 3. Click **Install**. ## QR code basics example The example generates a QR code containing a URL and renders it on your TFT. Most of the code is familiar from earlier lessons; the only new bit is QR generation. The QR is rendered to a sprite (off-screen buffer) first so you can position it freely before drawing it. ```cpp /* * This sketch renders a QR code to a sprite and displays it on a TFT screen. * The QR code is drawn to a sprite first, allowing for flexible positioning * and manipulation before displaying it on the screen. */ #include #include #include // Display object - handles communication with the TFT screen TFT_eSPI display = TFT_eSPI(); // Sprite object - an off-screen buffer for drawing the QR code // Sprites allow you to draw to memory first, then push to the display TFT_eSprite sprite = TFT_eSprite(&display); // QR code generator - configured to draw to the sprite instead of directly to // display QRcode_eSPI qrcode(&sprite); void setup() { // Initialize the display display.begin(); // Invert display colors (useful for certain display types) display.invertDisplay(true); // Set display rotation (0 = portrait, 1-3 = other orientations) display.setRotation(0); // Fill the entire screen with black background display.fillScreen(TFT_BLACK); // ===== QR Code Sprite Setup ===== // Set the desired QR code size in pixels // The sprite will be this size, and the QR code will be scaled to fit int qrSize = 200; // Create a sprite (off-screen buffer) with the specified dimensions // This allocates memory for a 200x200 pixel image sprite.createSprite(qrSize, qrSize); // Fill the sprite with white background (QR codes need white background) sprite.fillSprite(TFT_WHITE); // ===== QR Code Generation ===== // Initialize the QR code generator with the sprite dimensions // This automatically calculates the scaling factor (multiply) based on: // multiply = spriteSize / WD (where WD is the QR code module width) // The QR code will be automatically scaled and centered within the sprite qrcode.init(); // Generate and render the QR code to the sprite // The string will be encoded as a QR code and drawn to the sprite buffer qrcode.create("https://cardanothings.io"); // ===== Display Positioning ===== // Calculate position to center the sprite on the display // You can modify these values to position the QR code anywhere on screen int spriteX = (display.width() - qrSize) / 2; // X position (centered horizontally) int spriteY = (display.height() - qrSize) / 2; // Y position (centered vertically) // Push the sprite to the display at the calculated position // This copies the sprite buffer to the display at coordinates (spriteX, // spriteY) sprite.pushSprite(spriteX, spriteY); } void loop() { // Nothing to do in the loop - QR code is static // The delay prevents the loop from running too fast delay(1000); } ``` > Source: [`Workshop-05/examples/qr-code-basics/qr-code-basics.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/qr-code-basics/qr-code-basics.ino) :::info Sprites and memory Sprites are off-screen RAM buffers - they prevent flicker and let you position before display. They're not free though: a 200×200 sprite eats ~80 KB (200 × 200 × 2 bytes per pixel). The ESP32 typically has 200-300 KB free, so be mindful. If you hit out-of-memory errors, reduce `qrSize`. ::: ## Next steps Try a CIP-13 payment URI right now - generate one for the CardanoThings PingPong wallet (below) and send some tADA around with your mobile wallet. :::tip CardanoThings PingPong wallet Generate a `web+cardano:` URI pointing at this address, render it via the QR sketch above, scan with Yoroi Mobile, and pay. The PingPong wallet auto-refunds (minus fees) within ~60 seconds, so you'll see the round-trip immediately. Address: `addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w` Preprod-only. ::: The next two lessons build the full payment terminal: a frontend that creates payment requests, a backend that displays the QR on the TFT and listens for confirmations on-chain. ## Further Resources - [QRcodeDisplay](https://github.com/yoprogramo/QRcodeDisplay) - the core library. - [QRcode_eSPI](https://github.com/yoprogramo/QRcode_eSPI) - TFT_eSPI adapter. - [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) - the display library. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/05-qr-code-payments/qr-code-creation) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05).* --- ## Building the Frontend Build the frontend for the payment terminal: an HTML/CSS page served from LittleFS that lets the operator create payment requests and view a transaction list. ## Building the interface Now that the webserver runs, build the UI. The flow: - The page lists payment requests, their status, and a button to create a new request. - Clicking the button posts to the webserver, which creates the request, saves it in a JSON file, and renders a QR code on the TFT. - The user scans the QR with Yoroi (or another mobile wallet), signs, and submits. - The backend polls Koios for the transaction; on confirmation it updates the request's status and shows a confirmation on the TFT. :::info Re-uploading data The `data/` directory must be re-uploaded to LittleFS every time you change the frontend. See [Getting Started](/docs/developers/curriculum/dapps/iot/qr-code-payments/01-getting-started#uploading-files-to-littlefs) for the procedure. ::: ## Project structure The frontend lives in `data/`: ``` data/ ├── index.html ├── styles.css ├── requestPayment.js ├── transactionList.js ├── transactions.json ├── favicon.ico └── README.md ``` ## HTML & CSS The entry point - what the operator sees in the browser. `index.html`: ```html Cardano POS
Transactions
Create Payment Request
``` `styles.css`: ```css /* Basic styling */ body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } h1 { color: #333; text-align: center; } /* Button styles */ .btn-primary { background-color: #007bff; color: white; border: none; padding: 12px 24px; font-size: 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } .btn-primary:hover { background-color: #0056b3; } .btn-primary:disabled { background-color: #6c757d; cursor: not-allowed; } .btn-secondary { background-color: #6c757d; color: white; border: none; padding: 12px 24px; font-size: 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } .btn-secondary:hover { background-color: #5a6268; } /* Modal styles using :modal pseudo-class */ :modal { border: 1px solid #888; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); padding: 0; } /* Backdrop styling */ ::backdrop { background-color: rgba(0, 0, 0, 0.5); } .modal-content { background-color: #fefefe; padding: 0; } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #ddd; } .modal-header h2 { margin: 0; color: #333; } .modal-header form { margin: 0; } .close { background: none; border: none; color: #aaa; font-size: 28px; font-weight: bold; cursor: pointer; line-height: 20px; padding: 0; margin: 0; } .close:hover, .close:focus { color: #000; } .modal-body { padding: 20px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; color: #333; font-weight: bold; } .form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; box-sizing: border-box; } .form-group input:focus { outline: none; border-color: #007bff; } .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } /* Transactions section */ .transactions-section { margin-top: 40px; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .transactions-section h2 { margin-top: 0; color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; } .transactions-table { width: 100%; border-collapse: collapse; margin-top: 20px; } .transactions-table thead { background-color: #f8f9fa; } .transactions-table th { padding: 12px; text-align: left; font-weight: bold; color: #333; border-bottom: 2px solid #dee2e6; } .transactions-table td { padding: 12px; border-bottom: 1px solid #dee2e6; } .transactions-table tbody tr:hover { background-color: #f8f9fa; } .transactions-table .empty-hash { color: #999; font-style: italic; } ``` > Source: [`data/index.html`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/data/index.html), [`data/styles.css`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/data/styles.css) ## Creating the payment request A simple form: enter the amount of ADA, submit it to the backend, which creates a new payment request and renders the QR. `requestPayment.js`: ```javascript /** * Payment Request Handler * * This module handles the creation of new payment requests through a modal dialog. * It converts ADA amounts to lovelace, sends POST requests to the API, and * triggers the transaction list refresh. */ // Get references to DOM elements const modal = document.getElementById('paymentModal'); const openBtn = document.getElementById('openPaymentModal'); const cancelBtn = document.getElementById('cancelBtn'); const form = document.getElementById('paymentForm'); const amountInput = document.getElementById('adaAmount'); const submitBtn = form.querySelector('button[type="submit"]'); // Open modal when "New Payment Request" button is clicked openBtn.addEventListener('click', () => { modal.showModal(); // Show the native element amountInput.focus(); // Automatically focus the amount input field }); // Close modal when cancel button is clicked cancelBtn.addEventListener('click', () => modal.close()); // Reset form when dialog closes (handles both cancel and successful submission) modal.addEventListener('close', () => form.reset()); // Handle form submission - creates a new payment request form.addEventListener('submit', async (e) => { e.preventDefault(); // Prevent default form submission behavior // Validate and parse the ADA amount input const adaAmount = parseFloat(amountInput.value); if (isNaN(adaAmount) || adaAmount <= 0) { console.log('Please enter a valid ADA amount greater than 0'); return; // Exit early if validation fails } // Convert ADA to lovelace (1 ADA = 1,000,000 lovelace) // Math.round() ensures we get an integer value const lovelaceAmount = Math.round(adaAmount * 1000000); // Get current timestamp in milliseconds (Unix timestamp) const timestamp = Date.now(); // Disable submit button and show processing state submitBtn.disabled = true; submitBtn.textContent = 'Processing...'; try { // Prepare request data with amount in lovelace and timestamp const requestData = { amount: lovelaceAmount, timestamp: timestamp }; console.log('Sending request:', requestData); // Send POST request to create new transaction const response = await fetch('/api/transactions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); // Check if request was successful if (!response.ok) { // Parse error response and throw error const error = await response.json(); throw new Error(error.error || 'Failed to create payment request'); } // Parse successful response const transaction = await response.json(); // Close modal after successful creation modal.close(); // Convert lovelace back to ADA for display in console log // Note: transaction.amount includes the transaction ID, so this is for display only const adaDisplay = (transaction.amount / 1000000).toFixed(6); console.log(`Payment request created! ID: ${transaction.id}, Amount: ${adaDisplay} ADA (${transaction.amount} lovelace)`); // Refresh transaction list to show the new transaction // window.refreshTransactions is defined in transactionList.js if (window.refreshTransactions) { window.refreshTransactions(); } } catch (error) { // Log error to console (no alert shown to user) console.error('Error creating payment request:', error.message); } finally { // Always re-enable submit button and reset text, even if request failed submitBtn.disabled = false; submitBtn.textContent = 'Create Payment Request'; } }); ``` > Source: [`data/requestPayment.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/data/requestPayment.js) ## Displaying the transaction list A simple list of recent payment requests, fetched periodically from the backend so it picks up new transaction hashes as they confirm. `transactionList.js`: ```javascript /** * Transaction List Handler * * This module handles fetching and displaying transactions from the API. * It creates a table view of all transactions, automatically refreshes every * 30 seconds, and provides a manual refresh function for other modules. */ // Get reference to the container element where transactions will be displayed const transactionsContainer = document.getElementById('transactionsContainer'); /** * Format timestamp to readable date string * Converts Unix timestamp (milliseconds) to localized date/time string * * @param {number} timestamp - Unix timestamp in milliseconds * @returns {string} Formatted date/time string (e.g., "12/25/2023, 3:45:30 PM") */ function formatTimestamp(timestamp) { const date = new Date(timestamp); return date.toLocaleString(); } /** * Load transactions from API and display them * Fetches all transactions from the GET /api/transactions endpoint * and calls displayTransactions() to render them */ async function loadTransactions() { try { // Fetch transactions from API const response = await fetch('/api/transactions'); // Check if request was successful if (!response.ok) { throw new Error('Failed to fetch transactions'); } // Parse JSON response const transactions = await response.json(); // Display transactions in the table displayTransactions(transactions); } catch (error) { // Log error and show error message in container console.error('Error loading transactions:', error); transactionsContainer.innerHTML = 'Error loading transactions'; } } /** * Display transactions in a table format * Creates a table with columns: ID, Amount (ADA), Timestamp, Transaction Hash * * @param {Array} transactions - Array of transaction objects from API */ function displayTransactions(transactions) { // Handle empty transaction list if (transactions.length === 0) { transactionsContainer.innerHTML = 'No transactions yet'; return; } // Create table element const table = document.createElement('table'); table.className = 'transactions-table'; // Create table header const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); // Create header cells for each column ['ID', 'Amount (ADA)', 'Timestamp', 'Transaction Hash'].forEach(header => { const th = document.createElement('th'); th.textContent = header; headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); // Create table body const tbody = document.createElement('tbody'); // Create a row for each transaction transactions.forEach(transaction => { const row = document.createElement('tr'); // Transaction ID cell const idCell = document.createElement('td'); idCell.textContent = transaction.id; row.appendChild(idCell); // Amount cell - convert lovelace to ADA for display const amountCell = document.createElement('td'); // Note: transaction.amount includes the transaction ID, so this displays // the full amount including ID. For display purposes, we show it as ADA. // 1 ADA = 1,000,000 lovelace const adaAmount = (transaction.amount / 1000000).toFixed(2); amountCell.textContent = adaAmount; row.appendChild(amountCell); // Timestamp cell - format to readable date const timestampCell = document.createElement('td'); timestampCell.textContent = formatTimestamp(transaction.timestamp); row.appendChild(timestampCell); // Transaction hash cell const hashCell = document.createElement('td'); // Show "-" if hash is empty (payment not yet confirmed) hashCell.textContent = transaction.txHash || '-'; // Add CSS class for styling empty hashes (grayed out) hashCell.className = transaction.txHash ? '' : 'empty-hash'; row.appendChild(hashCell); // Add row to table body tbody.appendChild(row); }); table.appendChild(tbody); // Clear container and add the new table transactionsContainer.innerHTML = ''; transactionsContainer.appendChild(table); } // Polling interval ID (stored so it can be cleared if needed) let pollingInterval = null; /** * Start automatic polling for transactions * Fetches transactions from API every 30 seconds to keep the list up-to-date * This ensures new transactions and updated hashes are displayed automatically */ function startTransactionPolling() { // Clear any existing interval to prevent multiple polling instances if (pollingInterval) { clearInterval(pollingInterval); } // Set up interval to fetch transactions every 30 seconds pollingInterval = setInterval(() => { console.log('Polling for transactions...'); loadTransactions(); }, 30000); // 30 seconds = 30000 milliseconds } /** * Stop automatic polling * Useful for cleanup or if polling needs to be disabled */ function stopTransactionPolling() { if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; } } // Load transactions immediately when page loads loadTransactions(); // Start automatic polling to keep transaction list updated startTransactionPolling(); /** * Expose refresh function globally for other modules * This allows requestPayment.js to manually refresh the transaction list * after creating a new payment request, providing immediate feedback */ window.refreshTransactions = loadTransactions; ``` > Source: [`data/transactionList.js`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/data/transactionList.js) ## Next steps Frontend's done - you can upload `data/` to the microcontroller, but it can't actually create payment requests yet because the backend doesn't exist. That comes next. Things you could add later: a confirmation screen, different styling, direct links to [CardanoScan](https://cardanoscan.io/) for transaction hashes. The next lesson builds the backend - endpoints to create payment requests, render the CIP-13 QR code on the TFT, and confirm payments via Koios. ## Further Resources - [W3Schools](https://www.w3schools.com/) - free HTML/CSS/JS reference. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/05-qr-code-payments/building-the-frontend) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05).* --- ## Building the Backend The capstone for the IoT-on-Cardano course: the backend that ties the frontend, the QR code on the TFT, and on-chain confirmation together. ## Before we start Create a second Cardano wallet so you can pay from one and listen on the other. You already know how to create a wallet from [Workshop 01](/docs/developers/curriculum/dapps/iot/the-basics/01-cardano-setup). :::warning CIP-13 mobile wallet support As of writing, no mobile wallet correctly attaches the exact lovelace amount on a CIP-13 payment URI. To exercise the on-chain confirmation flow, send the exact lovelace amount manually with a desktop wallet. Track progress in the [CIP-0013 spec](https://cips.cardano.org/cip/CIP-0013). ::: ## Core concepts ### Identifying the payment request To match a confirmed transaction back to a payment request, encode the request ID in the lovelace amount. Instead of requesting 10 ADA, request 10.000001 ADA - the trailing lovelace is the request ID. When you find a UTxO with the exact lovelace amount (10000001), you've matched the request. :::info Limitations This is the simplest possible approach. It relies on individual lovelace amounts being added on top of the requested amount, and breaks if the user pays a different amount or if multiple UTxOs share the expected amount. You can harden this by also filtering on transaction timestamp. ::: ### Listening for the transaction There's no way to know the transaction hash up-front, or even whether the user scanned the QR. So we poll for a UTxO with the expected lovelace amount appearing on the recipient address. We use the Koios [`/address_utxos`](https://preprod.koios.rest/#get-/address_utxos) endpoint - a POST with the address in the body: ```json { "_addresses": [ "addr_test1qq3y2eqxprkk0dz2tyeuhav4hj3fem4duersn7w6eees9ru55stym27wkwyqw3z6uwr57plm22pyse00u9atdyzecg8skz0jec" ] } ``` Response (truncated): ```json [ { "tx_hash": "7ca6e052da9fa365ae156b40e5d6c208b808c3faa9985a8b1562ef9136fbdbd5", "tx_index": 0, "address": "addr_test1qq3y2eqxprkk0dz2tyeuhav4hj3fem4duersn7w6eees9ru55stym27wkwyqw3z6uwr57plm22pyse00u9atdyzecg8skz0jec", "value": "15140930", "stake_address": "stake_test1uz22g9jd408t8zq8g3dw8p60qla49qjgvhh7z74kjpvuyrctlwf4m", "payment_cred": "2245640608ed67b44a5933cbf595bca29ceeade64709f9dace73028f", "epoch_no": 258, "block_height": 4223127, "block_time": 1765568200, "datum_hash": null, "inline_datum": null, "reference_script": null, "asset_list": null, "is_spent": false } ] ``` We can filter directly for the expected lovelace amount via Koios's [horizontal filtering](https://preprod.koios.rest/#overview--horizontal-filtering): ``` https://preprod.koios.rest/api/v1/address_utxos?value=eq.13000004 ``` Returns only UTxOs of value 13000004 lovelace - 13 ADA requested + 4 lovelace request ID. When the API returns a UTxO for that query, the payment is confirmed: render confirmation on the TFT, update the JSON store with the transaction hash, stop listening for that request. :::info Future improvements A follow-up proposal ([CIP-0157](https://github.com/cardano-foundation/CIPs/pull/843)) adds individual metadata to CIP-13 payment URIs (such as a unique payment ID), which would replace this lovelace-encoding hack with proper IDs; check its status before designing around it. ::: ## Putting it all together Same setup as the basic webserver from the first lesson, with extra logic added in the main sketch and `secrets.h`. The WiFi manager is unchanged. `cardano-pos.ino`: ```cpp // Include necessary libraries #include #include // Include our custom header files #include "secrets.h" // WiFi credentials (not in git) #include "transaction_qr.h" // Transaction QR code display #include "web_server.h" // HTTP web server for serving files #include "wifi_manager.h" // WiFi connection management // Display object - handles communication with the TFT screen TFT_eSPI display = TFT_eSPI(); void setup() { // Initialize serial communication for debugging // Serial communication lets us send messages to the computer via USB // 115200 is the baud rate (speed of communication) // You can view these messages in the Arduino IDE Serial Monitor Serial.begin(115200); delay(1000); // Give serial monitor time to connect // Set up WiFi connection // WIFI_SSID is your WiFi network name // WIFI_PASSWORD is your WiFi password // These are defined in secrets.h (which you should create from // secrets.h.example) wifiManagerSetup(WIFI_SSID, WIFI_PASSWORD); // Wait for WiFi connection (with timeout) // We need WiFi to serve web pages, so we wait here Serial.println("Waiting for WiFi connection..."); const unsigned long wifiTimeout = 30000; // 30 seconds timeout (in milliseconds) const unsigned long wifiStart = millis(); // Record when we started waiting // Keep checking if WiFi is connected, but don't wait forever // millis() returns the number of milliseconds since the device started while (!wifiManagerIsConnected() && (millis() - wifiStart) < wifiTimeout) { wifiManagerLoop(); // Check WiFi status and try to connect delay(100); // Wait 100ms before checking again (don't waste CPU) } // Start web server if WiFi is connected if (wifiManagerIsConnected()) { webServerSetup(); } else { Serial.println("WiFi connection failed - web server not started"); } // Initialize the display display.begin(); // Invert display colors (useful for certain display types) display.invertDisplay(true); // Set display rotation (0 = portrait, 1-3 = other orientations) display.setRotation(0); // Fill the entire screen with black background display.fillScreen(TFT_BLACK); // Welcome Message display.setTextColor(TFT_WHITE); // Center "Cardano POS" text using MC_DATUM (Middle Center datum) display.setTextSize(2); display.setTextDatum(MC_DATUM); display.drawString("Cardano POS", display.width() / 2, display.height() / 2 - 10); // Center "www.cardanothings.io" text below display.setTextSize(1); display.drawString("www.cardanothings.io", display.width() / 2, display.height() / 2 + 20); delay(5000); display.fillScreen(TFT_BLACK); // Initialize transaction QR display transactionQRInit(display); // Register callback to display QR code when new transaction is created setTransactionCreatedCallback(displayNewTransactionQR, &display); } void loop() { // Keep WiFi connection alive and check for reconnection if needed // This needs to be called regularly to maintain the connection wifiManagerLoop(); // Handle web server requests (runs asynchronously, but we call loop for // consistency) if (wifiManagerIsConnected() && !webServerIsRunning()) { // If WiFi just reconnected, start the server webServerSetup(); } webServerLoop(); // Update transaction QR display and check on-chain status transactionQRUpdate(display); } ``` `secrets.h.example`: ```cpp /** * secrets.h.example - Template file for sensitive configuration values * * This is a template file showing what secrets need to be configured. * To use this file: * 1. Copy this file to secrets.h (secrets.h is in .gitignore and won't be committed) * 2. Fill in your actual WiFi credentials and API keys * 3. Never commit secrets.h to version control! * * IMPORTANT: Keep your secrets.h file private and never share it publicly. */ #ifndef SECRETS_H #define SECRETS_H // Your WiFi network name (SSID) // The name of the WiFi network your device should connect to #define WIFI_SSID "" // Your WiFi network password // The password required to connect to your WiFi network #define WIFI_PASSWORD "" // Your Cardano payment address // The address where payments should be sent // Format: addr1... or addr_test1... #define PAYMENT_ADDRESS "" // Koios API URL for checking transactions // Preprod: https://preprod.koios.rest/api/v1/address_utxos // Mainnet: https://api.koios.rest/api/v1/address_utxos #define KOIOS_API_URL "https://preprod.koios.rest/api/v1/address_utxos" #endif ``` > Source: [`cardano-pos.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/cardano-pos.ino) Add WiFi credentials, your payment address, and the Koios endpoint to `secrets.h`. ## Building the webserver Now build the webserver that serves the frontend, creates new payment requests, and stores them in a JSON file. `web_server.h`: ```cpp #ifndef WEB_SERVER_H #define WEB_SERVER_H #include // Forward declaration class TFT_eSPI; // Callback function type for new transaction notifications // transactionId: ID of the transaction // lovelaceAmount: Amount in lovelace (with ID already added) typedef void (*TransactionCallback)(TFT_eSPI* display, int transactionId, uint64_t lovelaceAmount); // Set callback for when a new transaction is created void setTransactionCreatedCallback(TransactionCallback callback, TFT_eSPI* display); // Initialize the web server (call after WiFi is connected) void webServerSetup(); // Handle server requests (call in loop()) void webServerLoop(); // Check if server is running bool webServerIsRunning(); #endif ``` `web_server.cpp`: ```cpp #include "web_server.h" #include #include #include #include namespace { WebServer server(80); // Web server on port 80 bool serverStarted = false; // Flag to check if server is started const char *TRANSACTIONS_FILE = "/transactions.json"; // Callback for new transaction notifications TransactionCallback transactionCallback = nullptr; TFT_eSPI* displayPtr = nullptr; // Get MIME type based on file extension String getContentType(String filename) { if (filename.endsWith(".html") || filename.endsWith("/")) { return "text/html"; } else if (filename.endsWith(".css")) { return "text/css"; } else if (filename.endsWith(".js")) { return "application/javascript"; } else if (filename.endsWith(".json")) { return "application/json"; } else { return "text/plain"; } } // Handle GET /api/transactions - serve transactions JSON file void handleGetTransactions() { Serial.println("GET /api/transactions"); // Check if transactions file exists if (!LittleFS.exists(TRANSACTIONS_FILE)) { // Return empty array if file doesn't exist server.send(200, "application/json", "[]"); Serial.println("Transactions file not found, returning empty array"); return; } // Read and serve the file File file = LittleFS.open(TRANSACTIONS_FILE, "r"); if (file) { server.streamFile(file, "application/json"); file.close(); Serial.println("Served transactions.json"); } else { server.send(500, "application/json", "{\"error\":\"Error opening transactions file\"}"); Serial.println("Error opening transactions file"); } } // Handle POST /api/transactions - add a new transaction void handlePostTransactions() { Serial.println("POST /api/transactions"); // Check if request has body if (!server.hasArg("plain")) { server.send(400, "application/json", "{\"error\":\"Missing request body\"}"); Serial.println("POST request missing body"); return; } String body = server.arg("plain"); Serial.print("Request body: "); Serial.println(body); // Parse the request body to get amount DynamicJsonDocument requestDoc(1024); DeserializationError error = deserializeJson(requestDoc, body); if (error) { server.send(400, "application/json", "{\"error\":\"Invalid JSON in request body\"}"); Serial.print("JSON parse error: "); Serial.println(error.c_str()); return; } if (!requestDoc.containsKey("amount")) { server.send(400, "application/json", "{\"error\":\"Missing 'amount' field\"}"); Serial.println("Missing 'amount' field"); return; } if (!requestDoc.containsKey("timestamp")) { server.send(400, "application/json", "{\"error\":\"Missing 'timestamp' field\"}"); Serial.println("Missing 'timestamp' field"); return; } // Use uint64_t to handle large lovelace amounts (e.g., 15000 ADA = 15 billion // lovelace) uint64_t amount = requestDoc["amount"].as(); // Use uint64_t to handle large JavaScript timestamps (milliseconds since // epoch) uint64_t timestamp = requestDoc["timestamp"].as(); // Read existing transactions DynamicJsonDocument transactionsDoc(4096); JsonArray transactions = transactionsDoc.to(); if (LittleFS.exists(TRANSACTIONS_FILE)) { File file = LittleFS.open(TRANSACTIONS_FILE, "r"); if (file) { DeserializationError error = deserializeJson(transactionsDoc, file); file.close(); if (error) { Serial.print("Error parsing existing transactions: "); Serial.println(error.c_str()); // Continue with empty array if parse fails transactions = transactionsDoc.to(); } else { transactions = transactionsDoc.as(); } } } // Find the highest ID to auto-increment int maxId = 0; for (JsonObject transaction : transactions) { if (transaction.containsKey("id")) { int id = transaction["id"]; if (id > maxId) { maxId = id; } } } // Calculate new transaction ID int newId = maxId + 1; // Create new transaction JsonObject newTransaction = transactions.createNestedObject(); newTransaction["id"] = newId; newTransaction["timestamp"] = timestamp; // Add the transaction ID to the amount (amount + id) newTransaction["amount"] = amount + newId; newTransaction["txHash"] = ""; // Empty transaction hash field // Write back to file File file = LittleFS.open(TRANSACTIONS_FILE, "w"); if (file) { serializeJson(transactionsDoc, file); file.close(); // Return the new transaction String response; serializeJson(newTransaction, response); server.send(201, "application/json", response); Serial.print("Added transaction with ID: "); Serial.print(newId); Serial.print(", Amount (with ID): "); Serial.println(amount + newId); // Notify callback about new transaction (if set) if (transactionCallback != nullptr && displayPtr != nullptr) { transactionCallback(displayPtr, newId, amount + newId); } } else { server.send(500, "application/json", "{\"error\":\"Error writing transactions file\"}"); Serial.println("Error writing transactions file"); } } // Handle file requests void handleFileRequest() { String path = server.uri(); // Default to index.html for root path if (path == "/" || path == "") { path = "/index.html"; } // Ensure path starts with / if (!path.startsWith("/")) { path = "/" + path; } // Check if file exists in LittleFS if (LittleFS.exists(path)) { String contentType = getContentType(path); File file = LittleFS.open(path, "r"); if (file) { server.streamFile(file, contentType); file.close(); Serial.print("Served file: "); Serial.println(path); } else { server.send(500, "text/plain", "Error opening file"); Serial.print("Error opening file: "); Serial.println(path); } } else { // File not found - try index.html as fallback if (path != "/index.html" && LittleFS.exists("/index.html")) { File file = LittleFS.open("/index.html", "r"); if (file) { server.streamFile(file, "text/html"); file.close(); Serial.print("File not found, serving index.html: "); Serial.println(path); } else { server.send(404, "text/plain", "File not found"); } } else { // 404 Not Found server.send(404, "text/plain", "File not found"); Serial.print("404 - File not found: "); Serial.println(path); } } } } // namespace void setTransactionCreatedCallback(TransactionCallback callback, TFT_eSPI* display) { transactionCallback = callback; displayPtr = display; } void webServerSetup() { // Initialize LittleFS file system if (!LittleFS.begin(true)) { Serial.println("ERROR: LittleFS Mount Failed"); return; } Serial.println("LittleFS mounted successfully"); // List all files in LittleFS (for debugging) File root = LittleFS.open("/"); File file = root.openNextFile(); Serial.println("Files in LittleFS:"); while (file) { Serial.print(" "); Serial.print(file.name()); Serial.print(" ("); Serial.print(file.size()); Serial.println(" bytes)"); file = root.openNextFile(); } // Register API endpoints server.on("/api/transactions", HTTP_GET, handleGetTransactions); server.on("/api/transactions", HTTP_POST, handlePostTransactions); // Serve files from root and all subdirectories (must be last) server.onNotFound(handleFileRequest); // Start the server server.begin(); serverStarted = true; // Print the server's IP address Serial.print("Web server started on http://"); Serial.println(WiFi.localIP()); } // Function to handle incoming client requests void webServerLoop() { // If the server is started, handle incoming client requests if (serverStarted) { server.handleClient(); } } // Function to check if the server is running bool webServerIsRunning() { return serverStarted; } ``` > Source: [`web_server.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/web_server.cpp) :::info Watch out when re-uploading data If you include `transactions.json` in `data/`, re-uploading the data directory will overwrite it. Great for testing, but you'll lose the transaction history. ::: ## QR code & transaction listener The QR-code display + transaction listener: render the CIP-13 QR with the right amount and address, listen for confirmation, render a confirmation message when the payment lands. `transaction_qr.h`: ```cpp #ifndef TRANSACTION_QR_H #define TRANSACTION_QR_H #include // Forward declaration class TFT_eSPI; // Initialize the transaction QR display system void transactionQRInit(TFT_eSPI &display); // Display QR code for newly created transaction (called immediately after // creation) // transactionId: ID of the transaction // lovelaceAmount: Amount in lovelace (with ID already added) void displayNewTransactionQR(TFT_eSPI *display, int transactionId, uint64_t lovelaceAmount); // Update function to be called in loop() - checks on-chain status and manages // display states void transactionQRUpdate(TFT_eSPI &display); #endif ``` `transaction_qr.cpp`: ```cpp #include "transaction_qr.h" #include "secrets.h" #include #include #include #include #include #include namespace { TFT_eSprite *qrSprite = nullptr; QRcode_eSPI *qrcode = nullptr; const char *TRANSACTIONS_FILE = "/transactions.json"; unsigned long lastCheckTime = 0; const unsigned long CHECK_INTERVAL = 10000; // Check every 10 seconds unsigned long waitingStartTime = 0; bool isWaitingForPayment = false; int waitingTransactionId = -1; uint64_t waitingLovelaceAmount = 0; unsigned long successStartTime = 0; bool isShowingSuccess = false; const unsigned long SUCCESS_DISPLAY_TIME = 10000; // 10 seconds } // namespace void transactionQRInit(TFT_eSPI &display) { int qrSize = min(display.width(), display.height()) - 20; qrSprite = new TFT_eSprite(&display); qrSprite->createSprite(qrSize, qrSize); qrSprite->fillSprite(TFT_WHITE); qrcode = new QRcode_eSPI(qrSprite); qrcode->init(); lastCheckTime = 0; waitingStartTime = 0; isWaitingForPayment = false; waitingTransactionId = -1; waitingLovelaceAmount = 0; successStartTime = 0; isShowingSuccess = false; } // Helper: Update transaction hash in LittleFS bool updateTransactionHash(int transactionId, const String &txHash) { if (!LittleFS.exists(TRANSACTIONS_FILE)) { return false; } File file = LittleFS.open(TRANSACTIONS_FILE, "r"); if (!file) { return false; } DynamicJsonDocument doc(4096); DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { return false; } JsonArray transactions = doc.as(); bool updated = false; for (JsonObject tx : transactions) { if (tx.containsKey("id") && tx["id"] == transactionId) { tx["txHash"] = txHash; updated = true; break; } } if (updated) { file = LittleFS.open(TRANSACTIONS_FILE, "w"); if (file) { serializeJson(doc, file); file.close(); return true; } } return false; } // Helper function: Format lovelace amount to ADA string with precise formatting // Avoids floating point precision issues by using integer arithmetic String formatLovelaceToADA(uint64_t lovelaceAmount) { uint64_t wholeADA = lovelaceAmount / 1000000; uint64_t fractionalLovelace = lovelaceAmount % 1000000; String result = String(wholeADA); result += "."; // Format fractional part with leading zeros (always 6 digits) if (fractionalLovelace == 0) { result += "000000"; } else { // Add leading zeros if needed uint64_t temp = fractionalLovelace; int digits = 0; while (temp > 0) { temp /= 10; digits++; } for (int i = 0; i < 6 - digits; i++) { result += "0"; } result += String(fractionalLovelace); } return result; } // Check for transaction using Koios API // transactionId: ID of the transaction // lovelaceAmount: Amount in lovelace (with ID already added) // Returns transaction hash if payment received, empty string otherwise String checkForTransaction(int transactionId, uint64_t lovelaceAmount) { if (!WiFi.isConnected()) { Serial.println("[Transaction Check] WiFi not connected, skipping check"); return ""; } Serial.print("[Transaction Check] Checking for payment - TX ID: "); Serial.print(transactionId); Serial.print(lovelaceAmount); Serial.println(" lovelace)"); // Build API URL with amount filter String url = String(KOIOS_API_URL); url += "?value=eq."; url += String(lovelaceAmount); // Make POST request HTTPClient http; http.begin(url); http.addHeader("Content-Type", "application/json"); // Build JSON request body DynamicJsonDocument requestDoc(512); JsonArray addresses = requestDoc.createNestedArray("_addresses"); addresses.add(PAYMENT_ADDRESS); String requestBody; serializeJson(requestDoc, requestBody); Serial.print("[Transaction Check] Sending request to: "); Serial.println(url); Serial.print("[Transaction Check] Request body: "); Serial.println(requestBody); int httpCode = http.POST(requestBody); Serial.print("[Transaction Check] HTTP response code: "); Serial.println(httpCode); if (httpCode == 200) { String payload = http.getString(); DynamicJsonDocument responseDoc(2048); DeserializationError error = deserializeJson(responseDoc, payload); http.end(); if (!error && responseDoc.is()) { JsonArray utxos = responseDoc.as(); Serial.print("[Transaction Check] Found "); Serial.print(utxos.size()); Serial.println(" UTXO(s)"); if (utxos.size() > 0 && utxos[0].containsKey("tx_hash")) { String txHash = utxos[0]["tx_hash"].as(); Serial.print("[Transaction Check] Payment found! Transaction hash: "); Serial.println(txHash); return txHash; } else { Serial.println( "[Transaction Check] No transaction hash in UTXO response"); } } else { Serial.print("[Transaction Check] JSON parsing error: "); Serial.println(error.c_str()); } } else { Serial.print("[Transaction Check] HTTP error, response: "); if (httpCode > 0) { String payload = http.getString(); Serial.println(payload); } else { Serial.println("Connection failed"); } } http.end(); Serial.println("[Transaction Check] No payment found yet"); return ""; } // Display success message and update transaction JSON with hash void displaySuccessAndUpdateHash(TFT_eSPI &display, int transactionId, const String &txHash) { // Update transaction with hash updateTransactionHash(transactionId, txHash); // Display success message display.fillScreen(TFT_BLACK); display.setTextColor(TFT_WHITE); display.setTextSize(2); display.setTextDatum(MC_DATUM); display.drawString("Payment Received!", display.width() / 2, display.height() / 2); // Set success state and start timer isShowingSuccess = true; successStartTime = millis(); Serial.print("Payment received! Transaction hash: "); Serial.println(txHash); Serial.println("Success message will be shown for 10 seconds"); } // Display QR code with call to action void displayWaitingMessage(TFT_eSPI &display, int transactionId, uint64_t lovelaceAmount, bool initialDraw) { // Calculate original amount (subtract ID) for display uint64_t originalAmount = lovelaceAmount - transactionId; float adaAmountForDisplay = (float)originalAmount / 1000000.0; // For QR code, use full amount including ID (lovelaceAmount already has ID // added) // Format using integer arithmetic to avoid floating point precision issues String adaAmountForQR = formatLovelaceToADA(lovelaceAmount); // Only draw static elements on initial draw if (initialDraw) { // White background display.fillScreen(TFT_WHITE); // Build QR code URL (use amount with ID included) String qrContent = "web+cardano:"; qrContent += PAYMENT_ADDRESS; qrContent += "?amount="; qrContent += adaAmountForQR; Serial.print("[Transaction Check] QR content: "); Serial.println(qrContent); // Generate QR code on sprite (white background, black QR code) qrSprite->fillSprite(TFT_WHITE); qrcode->create(qrContent); // Center QR code horizontally and position vertically int spriteX = (display.width() - qrSprite->width()) / 2; // Center QR code vertically, accounting for text above and info below int spriteY = (display.height() - qrSprite->height()) / 2; qrSprite->pushSprite(spriteX, spriteY); // Display "PLEASE PAY NOW!" text 20px above QR code display.setTextColor(TFT_BLACK); display.setTextSize(2); display.setTextDatum(TC_DATUM); display.drawString("PLEASE PAY NOW!", display.width() / 2, spriteY - 20); // Display transaction ID and ADA amount 20px below QR code int infoY = spriteY + qrSprite->height(); display.setTextSize(1); display.setTextColor(TFT_BLACK); // TX ID left-aligned with 25px padding from left edge of QR code display.setTextDatum(TL_DATUM); // Top Left datum String txInfo = "TX ID: " + String(transactionId); display.drawString(txInfo, spriteX + 25, infoY); // ADA amount right-aligned with 25px padding from right edge of QR code display.setTextDatum(TR_DATUM); // Top Right datum String adaInfo = String(adaAmountForDisplay, 2) + " ADA"; display.drawString(adaInfo, spriteX + qrSprite->width() - 25, infoY); } } void displayNewTransactionQR(TFT_eSPI *display, int transactionId, uint64_t lovelaceAmount) { if (display == nullptr) { return; } waitingStartTime = millis(); isWaitingForPayment = true; waitingTransactionId = transactionId; waitingLovelaceAmount = lovelaceAmount; lastCheckTime = millis(); uint64_t originalAmount = lovelaceAmount - transactionId; float adaAmount = (float)originalAmount / 1000000.0; Serial.println("========================================"); Serial.println("[Transaction Listener] Starting to listen for payment"); Serial.print(" Transaction ID: "); Serial.println(transactionId); Serial.print(" Amount: "); Serial.print(adaAmount, 6); Serial.print(" ADA ("); Serial.print(originalAmount); Serial.println(" lovelace)"); Serial.print(" Payment Address: "); Serial.println(PAYMENT_ADDRESS); Serial.print(" Check interval: "); Serial.print(CHECK_INTERVAL / 1000); Serial.println(" seconds"); Serial.println("========================================"); // Draw initial waiting screen displayWaitingMessage(*display, transactionId, lovelaceAmount, true); } void transactionQRUpdate(TFT_eSPI &display) { unsigned long currentTime = millis(); // Check if success message should be cleared (after 10 seconds) if (isShowingSuccess) { if (currentTime - successStartTime >= SUCCESS_DISPLAY_TIME) { // Clear screen to blank display.fillScreen(TFT_BLACK); isShowingSuccess = false; Serial.println("Success message cleared, returning to blank screen"); } return; // Don't check for payments while showing success } // If waiting for payment, check for payment if (isWaitingForPayment && waitingTransactionId != -1) { // Check for payment every CHECK_INTERVAL if (currentTime - lastCheckTime >= CHECK_INTERVAL) { lastCheckTime = currentTime; unsigned long waitTimeSeconds = (currentTime - waitingStartTime) / 1000; Serial.print("[Transaction Listener] Checking payment (waiting for "); Serial.print(waitTimeSeconds); Serial.println(" seconds)..."); String txHash = checkForTransaction(waitingTransactionId, waitingLovelaceAmount); if (txHash.length() > 0) { Serial.println( "[Transaction Listener] Payment confirmed! Stopping listener."); displaySuccessAndUpdateHash(display, waitingTransactionId, txHash); isWaitingForPayment = false; waitingTransactionId = -1; waitingLovelaceAmount = 0; } else { Serial.println("[Transaction Listener] Payment not found, will check " "again in 10 seconds"); } } } } ``` > Source: [`transaction_qr.cpp`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-05/examples/cardano-pos/transaction_qr.cpp) Backend's done. Upload `data/` and the [complete code](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05/examples/cardano-pos) to your microcontroller, navigate to the frontend, and test the payment flow. :::info You'll need to send the exact lovelace amount manually from a desktop wallet - mobile wallets don't yet honour CIP-13 exact amounts. ::: ## Next steps This is the end of the course. Some directions: - A screensaver when no payment is pending. - A more sophisticated frontend - touch input on the CYD if your variant supports it. - A physical vending machine that takes ADA payments. - A smart locker that opens when an exact amount lands. ![QR-Code Display on the CYD - 1](../img/CardanoPOS1.jpg) ![QR-Code Display on the CYD - 2](../img/CardanoPOS2.jpg) :::warning Production readiness These workshops are educational. For production: substantial error handling, authentication on the backend, and a host of other features are missing. ::: ## Further Resources - [LVGL](https://lvgl.io/) - for more sophisticated MCU UIs. - [CIP-0013 spec](https://cips.cardano.org/cip/CIP-0013) - current state of integration and next steps. - [CIP-0157](https://github.com/cardano-foundation/CIPs/pull/843) - proposal for metadata on CIP-13 URIs. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/05-qr-code-payments/building-the-backend) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05).* --- ## Workshop 05: QR-Code Payments This final workshop combines everything from the previous ones into a working payment terminal. The microcontroller hosts a small webserver (LittleFS-served HTML/CSS/JS), the operator creates a payment request from the browser, the TFT displays a [CIP-13](https://cips.cardano.org/cip/CIP-0013) `web+cardano:` QR code, and the device polls Koios `/address_utxos` until a UTxO with the expected exact lovelace amount appears. > Source code: [github.com/CardanoThings/Workshops/tree/main/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05) ## Steps 1. **[Getting Started](./01-getting-started.md)** - Project structure, LittleFS, and the basic webserver that serves an `index.html` from the device. 2. **[CIP-13 Integration](./02-cip13-integration.md)** - What CIPs are, the `web+cardano:` URI scheme, and how amounts and addresses encode into a payment URI. 3. **[QR-Code Creation](./03-qr-code-creation.md)** - Render a QR code on the TFT using the QRcodeDisplay + QRcode_eSPI libraries. 4. **[Building the Frontend](./04-building-the-frontend.md)** - HTML/CSS/JS for a simple payment-request UI served from LittleFS. 5. **[Building the Backend](./05-building-the-backend.md)** - Webserver routes, a JSON store of payment requests, and the on-chain transaction listener that matches by exact lovelace amount. ## What you'll need - The board with a screen from the [Hardware reference](/docs/developers/curriculum/dapps/iot/hardware/), as used in earlier workshops. - Two Cardano wallets (one to pay from, one to receive) on Preprod. - LittleFS upload tool for the Arduino IDE. - All libraries from previous workshops, plus QRcodeDisplay and QRcode_eSPI. > **Note on CIP-13 wallet support.** As of the workshop's writing, no major mobile wallet correctly attaches the exact-lovelace amount from a `web+cardano:` URI. To exercise the on-chain confirmation flow you'll need to send the exact amount manually from a desktop wallet. Track the [CIP-0013 spec](https://cips.cardano.org/cip/CIP-0013) for progress. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/05-qr-code-payments) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-05](https://github.com/CardanoThings/Workshops/tree/main/Workshop-05).* --- ## Fetch your Wallet Balance Poll your wallet balance from your microcontroller and react when it changes - the foundation for every "do something physical when something happens on-chain" project in this section. ## Setting up the API We use the Koios [`/account_info`](https://preprod.koios.rest/#post-/account_info) endpoint. It's a POST that takes your stake address in the request body. Get your stake address from Yoroi: 1. Open the Yoroi extension. 2. Confirm Preprod (orange banner at top). 3. Go to **Wallet → Receive**. 4. Copy your stake address from the rewards section - it starts with `stake_test1...`. You can test the endpoint from [Insomnia](https://insomnia.rest/) or [Postman](https://www.postman.com/) with this body: ```json { "_stake_addresses": [ "stake_test1urq4rcynzj4uxqc74c852zky7wa6epgmn9r6k3j3gv7502q8jks0l" ] } ``` Response (truncated): ```json [ { "stake_address": "stake_test1uz22g9jd408t8zq8g3dw8p60qla49qjgvhh7z74kjpvuyrctlwf4m", "status": "registered", "delegated_pool": null, "delegated_drep": "drep1ytesfw7n2pq5ys2rk0m7fxxd2dyagf820wy24d82rdd9yxqfm4qjg", "total_balance": "10497440929", "utxo": "10497440929", "rewards": "0", "withdrawals": "0", "rewards_available": "0", "deposit": "2000000", "reserves": "0", "treasury": "0", "proposal_refund": "0" } ] ``` Key fields: - **stake_address** - your stake address. - **status** - registration status. - **total_balance** - total balance in lovelace (1 tADA = 1,000,000 lovelace). - **utxo** - total UTxO value in lovelace. - **rewards_available** - rewards you can withdraw. - **delegated_pool** - pool ID if delegated. - **delegated_drep** - DRep ID for governance. ## Fetching the balance from the ESP32 The sketch will: 1. Connect to WiFi. 2. POST to Koios with your stake address. 3. Parse the response and extract `total_balance`. 4. Display the balance via the serial monitor. ```cpp // Include necessary libraries for WiFi, HTTP requests, and JSON parsing #include #include #include // WiFi credentials - replace with your network details const char* ssid = "Your SSID"; const char* password = "Your Password"; // Koios API endpoint for fetching account information const char* apiUrl = "https://preprod.koios.rest/api/v1/account_info"; // Your Cardano stake address (Preprod Testnet) String stakeAddress = "stake_test1..."; // Variables for timing balance checks unsigned long lastCheck = 0; // Timestamp of last balance check const unsigned long checkInterval = 30000; // Check every 30 seconds (30000 milliseconds) // Store previous balance to detect changes float previousBalance = 0; void setup() { // Initialize serial communication for debugging (115200 baud rate) Serial.begin(115200); // Start WiFi connection WiFi.begin(ssid, password); // Wait until WiFi is connected while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } // Print connection confirmation and IP address Serial.println("Connected to WiFi"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Perform initial balance check on startup fetchStakeBalance(); } void loop() { // Check if WiFi connection is still active if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection lost. Reconnecting..."); WiFi.reconnect(); // Wait for reconnection while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.print("."); } Serial.println("Reconnected!"); } // Get current time in milliseconds unsigned long currentMillis = millis(); // Check if enough time has passed since last check if (currentMillis - lastCheck >= checkInterval) { fetchStakeBalance(); lastCheck = currentMillis; // Update last check timestamp } } void fetchStakeBalance() { // Only proceed if WiFi is connected if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Initialize HTTP client with API URL http.begin(apiUrl); // Set content type header for JSON request http.addHeader("Content-Type", "application/json"); // Create JSON payload with stake address // Koios API expects stake addresses in an array under "_stake_addresses" key String jsonPayload = "{\\""; jsonPayload += "_stake_addresses"; jsonPayload += "\\":[\\""; jsonPayload += stakeAddress; jsonPayload += "\\"]}"; // Send POST request and get response code int httpResponseCode = http.POST(jsonPayload); // Check if request was successful (response code > 0) if (httpResponseCode > 0) { // Get response body as string String response = http.getString(); Serial.println("HTTP Response Code: " + String(httpResponseCode)); // Create JSON document to parse response (2048 bytes buffer) DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, response); // Check if JSON parsing was successful if (!error) { // Verify response is an array with at least one element if (doc.is() && doc.size() > 0) { // Get first account info object from array JsonObject accountInfo = doc[0]; // Extract total balance as string (Koios returns balance as string) // total_balance includes delegated amount + rewards const char* balanceStr = accountInfo["total_balance"]; // Convert string to long long (for large Lovelace values) long long balanceLovelace = 0; if (balanceStr != nullptr) { balanceLovelace = atoll(balanceStr); } // Convert from Lovelace (smallest unit) to tADA (test ADA) // 1 tADA = 1,000,000 Lovelace float balance = balanceLovelace / 1000000.0; // Print account information Serial.println("Stake Address: " + String(accountInfo["stake_address"].as())); Serial.println("Total Balance: " + String(balance, 6) + " tADA"); // Check if balance has changed since last check if (balance != previousBalance) { if (balance > previousBalance) { Serial.println("✓ Balance increased!"); } else { Serial.println("✓ Balance decreased!"); } // Update previous balance for next comparison previousBalance = balance; } } } else { // Print error if JSON parsing failed Serial.print("JSON parsing failed: "); Serial.println(error.c_str()); } } else { // Print error if HTTP request failed Serial.println("Error in HTTP request"); Serial.println("HTTP Response Code: " + String(httpResponseCode)); } // Close HTTP connection http.end(); } else { Serial.println("WiFi not connected"); } } ``` > Source: [`Workshop-02/examples/wallet-balance/wallet-balance.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/wallet-balance/wallet-balance.ino) Update WiFi credentials and your stake address before uploading. ## Listening for changes The sketch already detects balance changes by storing the previous balance and comparing on each poll. From there you can hang any side-effect off the change: - Light up an LED when funds arrive. - Buzz a buzzer. - Update a display (next lesson). - Trigger a relay (lesson 3 of this workshop). :::tip CardanoThings PingPong wallet Need an easy way to test the change-detection loop? Send tADA to the CardanoThings **PingPong** wallet - it auto-refunds your transaction (minus fees) within ~60 seconds, so you can trigger the change handler repeatedly. Address: `addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w` Preprod-only. ::: ## Alternative APIs Koios is free and open-source, but there are alternatives. ### Blockfrost [Blockfrost](https://blockfrost.io/) is a popular Cardano API with free and paid tiers. It uses a simpler GET request and requires an API key. Sign up at [blockfrost.io](https://blockfrost.io/), create a Preprod project, and grab the key. The Blockfrost response shape is slightly different: ```json { "stake_address": "stake_test1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7", "active": true, "active_epoch": 412, "controlled_amount": "619154618165", "rewards_sum": "319154618165", "withdrawals_sum": "12125369253", "reserves_sum": "319154618165", "treasury_sum": "12000000", "withdrawable_amount": "319154618165", "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", "drep_id": "drep15cfxz9exyn5rx0807zvxfrvslrjqfchrd4d47kv9e0f46uedqtc" } ``` ```cpp // Include necessary libraries for WiFi, HTTP requests, and JSON parsing #include #include #include // WiFi credentials - replace with your network details const char* ssid = "Your SSID"; const char* password = "Your Password"; // Blockfrost API endpoint (Preprod Testnet) // Note: Blockfrost uses GET requests with stake address in URL path const char* apiUrl = "https://cardano-preprod.blockfrost.io/api/v0/accounts/"; // Your Blockfrost API key (get free key from blockfrost.io) const char* apiKey = "your-blockfrost-api-key"; // Your Cardano stake address (Preprod Testnet) String stakeAddress = "stake_test1..."; // Variables for timing balance checks unsigned long lastCheck = 0; // Timestamp of last balance check const unsigned long checkInterval = 30000; // Check every 30 seconds void setup() { // Initialize serial communication for debugging Serial.begin(115200); // Start WiFi connection WiFi.begin(ssid, password); // Wait until WiFi is connected while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println("Connected to WiFi"); } void loop() { // Check if WiFi connection is still active if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); while (WiFi.status() != WL_CONNECTED) { delay(1000); } } // Get current time in milliseconds unsigned long currentMillis = millis(); // Check if enough time has passed since last check if (currentMillis - lastCheck >= checkInterval) { fetchStakeBalance(); lastCheck = currentMillis; // Update last check timestamp } } void fetchStakeBalance() { // Only proceed if WiFi is connected if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Build full URL by appending stake address to base URL String fullUrl = apiUrl + stakeAddress; // Initialize HTTP client with full URL http.begin(fullUrl); // Blockfrost requires API key in "project_id" header http.addHeader("project_id", apiKey); // Send GET request (Blockfrost uses GET, not POST like Koios) int httpResponseCode = http.GET(); // Check if request was successful if (httpResponseCode > 0) { // Get response body as string String response = http.getString(); // Create JSON document to parse response (1024 bytes buffer) DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, response); // Check if JSON parsing was successful if (!error) { // Extract controlled_amount as string (Blockfrost returns balance as string) // controlled_amount is the total balance including delegated amount and rewards const char* balanceStr = doc["controlled_amount"]; // Convert string to long long (for large Lovelace values) long long balanceLovelace = 0; if (balanceStr != nullptr) { balanceLovelace = atoll(balanceStr); } // Convert from Lovelace to tADA (test ADA) - 1 tADA = 1,000,000 Lovelace float balance = balanceLovelace / 1000000.0; // Print account information Serial.println("Stake Address: " + String(doc["stake_address"].as())); Serial.println("Total Balance: " + String(balance, 6) + " tADA"); } } // Close HTTP connection http.end(); } } ``` > Source: [`Workshop-02/examples/wallet-balance-blockfrost/wallet-balance-blockfrost.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/wallet-balance-blockfrost/wallet-balance-blockfrost.ino) :::tip PlatformIO + Blockfrost SDK If you use PlatformIO, Blockfrost ships an official Arduino SDK that handles auth and serialization: [github.com/blockfrost/blockfrost-arduino](https://github.com/blockfrost/blockfrost-arduino). ::: ### Maestro [Maestro](https://www.gomaestro.org/) is another Cardano API provider with a free tier and similar coverage to Blockfrost and Koios. ### Dolos You can also self-host with [TxPipe Dolos](https://docs.txpipe.io/dolos), which exposes a [Mini Blockfrost API](https://docs.txpipe.io/dolos/apis/minibf) over your own node. ## Further Resources - [Koios Documentation](https://preprod.koios.rest/) - full endpoint reference. - [Blockfrost](https://blockfrost.io/) - alternate API with free tier. - [Blockfrost API docs](https://docs.blockfrost.io/) - endpoint reference. - [Maestro](https://www.gomaestro.org/) - another provider. - [TxPipe Dolos](https://docs.txpipe.io/dolos) - self-host a Mini Blockfrost API. - [ArduinoJSON Library](https://docs.arduino.cc/libraries/arduinojson/) - JSON parsing in Arduino. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/02-read-and-output/fetch-wallet-balance) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-02](https://github.com/CardanoThings/Workshops/tree/main/Workshop-02).* --- ## Display Data on your Microcontroller Take the wallet-balance polling from the previous lesson and put it on a screen. :::info Hardware This lesson assumes the **Cheap Yellow Display (CYD)** with its built-in TFT. If you have a small I2C OLED instead (typical SSD1306 0.96", 128×64), the wiring and library are different - see the dropdown below. :::
**Reference: small I2C OLED display (SSD1306)** If your hardware is an ESP32-C3 with a small SSD1306 OLED instead of the CYD, here's the minimum-viable display sketch using `Adafruit_SSD1306` over I2C. **Install libraries** in the Arduino IDE Library Manager: - **Adafruit SSD1306** (by Adafruit) - **Adafruit GFX Library** (dependency) **Wiring (ESP32-C3):** | OLED pin | ESP32-C3 pin | |---|---| | VCC | 3.3V | | GND | GND | | SDA | GPIO 8 | | SCL | GPIO 9 | GPIO 8 / 9 are the default I2C pins on the ESP32-C3. If your board differs, adjust the `I2C_SDA` / `I2C_SCL` defines. ```cpp /* * ESP32-C3 I2C OLED Display Example (SSD1306) * * Display text and shapes on a 0.96" 128x64 OLED. * * Connections: * OLED VCC -> ESP32 3.3V * OLED GND -> ESP32 GND * OLED SDA -> ESP32 GPIO 8 * OLED SCL -> ESP32 GPIO 9 */ #include #include #include #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C // or 0x3D, use the I2C scanner if unsure #define I2C_SDA 8 #define I2C_SCL 9 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); void setup() { Serial.begin(115200); delay(1000); Wire.begin(I2C_SDA, I2C_SCL); if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println("SSD1306 allocation failed"); for (;;); } Serial.println("OLED Display initialized!"); } void loop() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 0); display.println("Small Text"); display.setTextSize(2); display.setCursor(0, 15); display.println("Medium"); display.setTextSize(3); display.setCursor(0, 40); display.println("Big!"); display.drawRect(95, 5, 30, 25, SSD1306_WHITE); display.display(); delay(2000); } ``` To adapt the rest of this lesson for the OLED: replace each `tft.*` call with the corresponding `display.*` call (`display.fillRect`, `display.setCursor`, `display.println`, then `display.display()` to push the buffer).
## Introduction to TFT_eSPI [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) is a powerful library for driving TFT displays from ESP32 / ESP8266. It supports many display chips and gives you fast text, shapes, image and sprite rendering with rotation support. ## Installing and configuring the library ### Step 1: Install TFT_eSPI 1. Open Arduino IDE. 2. **Sketch → Include Library → Manage Libraries**. 3. Search for **TFT_eSPI**. 4. Install the library by Bodmer. ### Step 2: Configure for the CYD TFT_eSPI needs to be configured for your specific display. Easiest path is to drop in a CYD-specific `User_Setup.h`: 1. Download the [CYD `User_Setup.h`](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/User_Setup.h) from the ESP32-Cheap-Yellow-Display repo. 2. Find your TFT_eSPI library folder: - **Windows:** `Documents\Arduino\libraries\TFT_eSPI\` - **macOS:** `~/Documents/Arduino/libraries/TFT_eSPI/` - **Linux:** `~/Arduino/libraries/TFT_eSPI/` 3. Replace the existing `User_Setup.h` with the downloaded one. 4. Restart Arduino IDE so the new config takes effect. :::warning Without this step, the display won't work The CYD-specific `User_Setup.h` configures: ILI9341_2_DRIVER, 240×320 resolution, the right GPIO pins, 55 MHz SPI, and GPIO 21 for backlight. ::: ## Testing the display Before fetching wallet data, verify the display works with a Hello World - centred white text on a blue background. ```cpp // Include TFT display library #include #include // Create TFT display object TFT_eSPI tft = TFT_eSPI(); // Define custom gray color (RGB565 format: 5 bits red, 6 bits green, 5 bits blue) #define TFT_GRAY 0x7BEF void setup() { // Initialize serial communication for debugging Serial.begin(115200); // Initialize TFT display tft.init(); // Set display rotation (1 = landscape) tft.setRotation(1); // Invert display colors (required for some CYD displays) tft.invertDisplay(true); // Fill entire screen with blue background tft.fillScreen(TFT_BLUE); // Set text properties tft.setTextColor(TFT_WHITE, TFT_BLUE); // White text on blue background tft.setTextSize(3); // Large text size // Calculate text position to center it on screen String text = "Hello World!"; int textWidth = text.length() * 6 * 3; // Approximate width (6 pixels per char * text size) int textHeight = 8 * 3; // Approximate height (8 pixels * text size) int textX = (320 - textWidth) / 2; // Center horizontally (320 is screen width) int textY = (240 - textHeight) / 2; // Center vertically (240 is screen height) // Display the centered text tft.setCursor(textX, textY); tft.println("Hello World!"); Serial.println("Display test complete!"); } void loop() { // Nothing to do in the loop for this test } ``` > Source: [`Workshop-02/examples/display-hello-world/display-hello-world.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/display-hello-world/display-hello-world.ino) If you see "Hello World!" on a blue screen, you're good. If you see corrupted graphics or nothing at all, double-check that you replaced `User_Setup.h`. ### Understanding the code - **`tft.init()`** - initialises the display. - **`tft.setRotation(1)`** - landscape orientation. Values 0-3 rotate by 90° each. - **`tft.invertDisplay(true)`** - some CYDs have inverted colours; without this, blue may show as yellow. - **`tft.fillScreen(TFT_BLUE)`** - fills the screen with a colour constant. - **`tft.setTextColor(fg, bg)`** - text colour and background. - **`tft.setTextSize(size)`** - size multiplier for the default font. - **`tft.setCursor(x, y)`** - origin (0,0) is top-left. :::info Screen dimensions The CYD is 320×240 in landscape (rotation 1). Use those dimensions when centring text. ::: ## Displaying the wallet balance Now combine the previous lesson's Koios fetch with the display: white text on blue, balance in large type, a live timestamp updating every second, balance refreshed every 60 seconds. ```cpp // Include necessary libraries #include // WiFi connectivity #include // HTTP client for API calls #include // JSON parsing #include // TFT display library #include // SPI communication for display // Create TFT display object TFT_eSPI tft = TFT_eSPI(); // WiFi credentials - replace with your network details const char* ssid = "Your SSID"; const char* password = "Your Password"; // Koios API endpoint for fetching account information const char* apiUrl = "https://preprod.koios.rest/api/v1/account_info"; // Your Cardano stake address (Preprod Testnet) String stakeAddress = "stake_test1..."; // Variables for timing balance checks unsigned long lastCheck = 0; // Timestamp of last balance check const unsigned long checkInterval = 60000; // Check every 60 seconds unsigned long lastFetchTime = 0; // Timestamp of last successful fetch unsigned long lastDisplayUpdate = 0; // Timestamp of last display update const unsigned long displayUpdateInterval = 1000; // Update display every 1 second // Store current balance to detect changes float currentBalance = 0.0; void setup() { // Initialize serial communication for debugging Serial.begin(115200); // Initialize TFT display tft.init(); tft.setRotation(1); // Set to landscape orientation tft.invertDisplay(true); // Invert colors for correct display tft.fillScreen(TFT_BLUE); // Fill screen with blue background tft.setTextColor(TFT_WHITE, TFT_BLUE); // White text on blue background tft.setTextSize(2); // Set text size // Display startup message on screen tft.setCursor(10, 10); tft.println("Connecting..."); // Start WiFi connection WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println("Connected to WiFi"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Clear screen and show connection success tft.fillScreen(TFT_BLUE); tft.setCursor(10, 10); tft.println("Connected!"); delay(1000); // Perform initial balance fetch and display fetchStakeBalance(); } void loop() { // Check if WiFi connection is still active if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection lost. Reconnecting..."); WiFi.reconnect(); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.print("."); } Serial.println("Reconnected!"); } // Get current time in milliseconds unsigned long currentMillis = millis(); // Check if enough time has passed since last check if (currentMillis - lastCheck >= checkInterval) { fetchStakeBalance(); lastCheck = currentMillis; // Update last check timestamp } // Update the timestamp display every second if (currentMillis - lastDisplayUpdate >= displayUpdateInterval) { updateTimestamp(); lastDisplayUpdate = currentMillis; } } void fetchStakeBalance() { // Only proceed if WiFi is connected if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Initialize HTTP client with API URL http.begin(apiUrl); // Set content type header for JSON request http.addHeader("Content-Type", "application/json"); // Create JSON payload with stake address // Koios API expects stake addresses in an array under "_stake_addresses" key String jsonPayload = "{\\""; jsonPayload += "_stake_addresses"; jsonPayload += "\\":[\\""; jsonPayload += stakeAddress; jsonPayload += "\\"]}"; // Send POST request and get response code int httpResponseCode = http.POST(jsonPayload); // Check if request was successful (response code > 0) if (httpResponseCode > 0) { // Get response body as string String response = http.getString(); Serial.println("HTTP Response Code: " + String(httpResponseCode)); // Create JSON document to parse response (2048 bytes buffer) DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, response); // Check if JSON parsing was successful if (!error) { // Verify response is an array with at least one element if (doc.is() && doc.size() > 0) { // Get first account info object from array JsonObject accountInfo = doc[0]; // Extract total balance as string (Koios returns balance as string) // total_balance includes delegated amount + rewards const char* balanceStr = accountInfo["total_balance"]; // Convert string to long long (for large Lovelace values) long long balanceLovelace = 0; if (balanceStr != nullptr) { balanceLovelace = atoll(balanceStr); } // Convert from Lovelace to tADA (test ADA) // 1 tADA = 1,000,000 Lovelace float balance = balanceLovelace / 1000000.0; // Print account information Serial.println("Stake Address: " + String(accountInfo["stake_address"].as())); Serial.println("Total Balance: " + String(balance, 6) + " tADA"); // Check if balance has changed since last check if (balance != currentBalance) { if (balance > currentBalance) { Serial.println("✓ Balance increased!"); } else if (balance < currentBalance) { Serial.println("✓ Balance decreased!"); } // Update current balance currentBalance = balance; } // Update last fetch time and refresh full display lastFetchTime = millis(); updateDisplay(); } } else { // Print error if JSON parsing failed Serial.print("JSON parsing failed: "); Serial.println(error.c_str()); } } else { // Print error if HTTP request failed Serial.println("Error in HTTP request"); Serial.println("HTTP Response Code: " + String(httpResponseCode)); } // Close HTTP connection http.end(); } else { Serial.println("WiFi not connected"); } } void updateDisplay() { // Fill entire screen with blue background tft.fillScreen(TFT_BLUE); // Display title "Wallet Balance" in white tft.setTextSize(2); tft.setTextColor(TFT_WHITE, TFT_BLUE); tft.setCursor(10, 10); tft.println("Wallet Balance"); // Display balance amount in large white text tft.setTextSize(4); tft.setTextColor(TFT_WHITE, TFT_BLUE); tft.setCursor(10, 50); tft.print(String(currentBalance, 2)); // Format to 2 decimal places // Display "ADA" unit tft.setTextSize(2); tft.println(" ADA"); // Display initial timestamp updateTimestamp(); } void updateTimestamp() { // Calculate seconds since last fetch unsigned long secondsAgo = (millis() - lastFetchTime) / 1000; // Clear the timestamp area (blue rectangle over the old text) tft.fillRect(10, 220, 200, 10, TFT_BLUE); // Display last update timestamp in lower left corner tft.setTextSize(1); tft.setTextColor(TFT_WHITE, TFT_BLUE); tft.setCursor(10, 220); tft.print("Updated "); tft.print(secondsAgo); tft.println("s ago"); } ``` > Source: [`Workshop-02/examples/display-wallet-balance/display-wallet-balance.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/display-wallet-balance/display-wallet-balance.ino) Replace WiFi credentials and your stake address before uploading. You should see your tADA balance update every minute and the timestamp tick every second. ## Next steps You can now poll on-chain data and render it on a screen. From here, try other Koios endpoints, or build alternative visualisations on the same hardware. The next lessons in this workshop add hardware actuation: a relay (drive a real light bulb) and an LED ring (an Epoch Clock). ## Further Resources - [TFT_eSPI on GitHub](https://github.com/Bodmer/TFT_eSPI) - the library. - [Adafruit GFX Graphics Library](https://learn.adafruit.com/adafruit-gfx-graphics-library) - TFT_eSPI builds on this. - [Arduino TFT_eSPI Reference](https://docs.arduino.cc/libraries/tft_espi/) - Arduino's library docs. - [ESP32-Cheap-Yellow-Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display) - community resources for the CYD. - [LVGL](https://github.com/lvgl/lvgl) - much more featureful UI library if you outgrow TFT_eSPI. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/02-read-and-output/display-data) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-02](https://github.com/CardanoThings/Workshops/tree/main/Workshop-02).* --- ## Light up the Tree Connect a relay to the ESP32 and switch a real light (or any AC appliance) based on what happens on-chain. Receiving funds turns the relay on; sending them turns it off. :::danger Safety warning - high voltage This lesson involves working with **110V/220V mains relays**. High voltage can cause serious injury, death, or destroyed equipment if mishandled. Only proceed if you: - Understand basic electronics and electrical safety. - Know how to safely work with mains voltage. - Understand the risks of relays and high-voltage circuits. - Have appropriate PPE and a safe work area. If unsure, use the LED examples first; consult someone experienced before connecting a relay; never work with live mains without proper training. We're not responsible for any injury or damage. ::: ## Hardware requirements - ESP32-C3 microcontroller. - Hardware relay module (with status LED - easier to debug). - Breadboard and jumper wires. - Optional: soldering iron for permanent installs. - **Recommended for testing:** an LED + resistor before going to the relay. ## Introduction to external hardware Microcontrollers run at 3.3V or 5V; they can't drive 110V/220V appliances directly. They control them through interface components - most commonly relays. A **relay** is an electrically operated switch: a low-power signal from the microcontroller controls a high-power circuit. Internally an electromagnet physically moves a switch contact, providing electrical isolation between the control side (low voltage) and the load side (high voltage). :::info Choosing a relay module Pick one with a **status LED** - it lights up when the relay activates, so you can see your code is working without actually wiring up a high-voltage device. ::: ### Why use a relay? - **Safety** - electrical isolation between control and load. - **Power handling** - switch loads way beyond what the microcontroller can supply. - **Versatility** - switches AC or DC. - **Reliability** - physical switching, robust at high currents. ## Wiring A typical relay module has: - **Input pins (VCC, GND, IN)** - to the microcontroller. - **Output terminals (NO, COM, NC)** - for the high-voltage device. - **Optocoupler** - isolates control and load. - **Status LED** - indicates when the relay is active. ### To the microcontroller 1. **VCC →** 5V (or 3.3V - check your module's spec). 2. **GND →** GND. 3. **IN (Signal) →** any GPIO (e.g., GPIO 2). :::info ESP32-C3 pinout reference Need a pinout reference for wiring? See the interactive ESP32-C3 pinout at [cardanothings.io](https://cardanothings.io), the official [ESP32-C3 datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf), or your board's specific schematic. Common pin protocols on the C3: **SPI** uses MOSI / MISO / SCK / SS-CS; **I2C** uses SDA / SCL (typically GPIO 8 / 9); **UART** uses TX / RX. ::: ### To the device The output side has three terminals: - **COM** - common. - **NO (Normally Open)** - disconnected from COM when off; connected when on. - **NC (Normally Closed)** - connected to COM when off; disconnected when on. For "turn the light on when activated," wire COM and NO: 1. Hot/live wire from the power source → COM. 2. One wire from your device → NO. 3. Neutral from the device back to the power source's neutral. :::info Active LOW vs Active HIGH Most relay modules are **active LOW** - set the GPIO LOW to turn the relay on. Some are active HIGH. Check your module. You'll know it's working when you hear the click and the status LED lights. ::: ## Blink test Before going on-chain, verify the relay works with a blink: ON for 2s, OFF for 2s. ```cpp // Simple Relay Blink Example // This example demonstrates basic relay control without any network connectivity // Perfect for testing your relay wiring before adding blockchain integration // // Wiring: // VCC -> 3.3V or 5V (check your relay module specifications) // GND -> GND // IN -> GPIO 4 (or any available GPIO pin) // // Note: Most relay modules are active LOW (LOW = ON, HIGH = OFF) // If your relay doesn't work, try reversing HIGH and LOW // Define the GPIO pin connected to the relay IN pin const int relayPin = 4; // Change this to match your wiring void setup() { // Initialize serial communication for debugging Serial.begin(115200); // Wait for serial port to initialize delay(1000); // Configure relay pin as output pinMode(relayPin, OUTPUT); // Set relay to OFF state initially // For active LOW relays: HIGH = OFF, LOW = ON // For active HIGH relays: LOW = OFF, HIGH = ON // Try both if unsure - you'll hear a click when the relay activates digitalWrite(relayPin, HIGH); // Start with relay OFF Serial.println("Relay Blink Example"); Serial.println("Relay will turn ON for 2 seconds, then OFF for 2 seconds"); Serial.println("You should hear a click when the relay activates"); } void loop() { // Turn relay ON // For active LOW: set pin to LOW // For active HIGH: set pin to HIGH Serial.println("Relay ON"); digitalWrite(relayPin, LOW); // LOW activates active LOW relays delay(2000); // Keep relay ON for 2 seconds // Turn relay OFF // For active LOW: set pin to HIGH // For active HIGH: set pin to LOW Serial.println("Relay OFF"); digitalWrite(relayPin, HIGH); // HIGH deactivates active LOW relays delay(2000); // Keep relay OFF for 2 seconds // This creates a continuous 2-second ON/OFF cycle // You should hear the relay clicking every 2 seconds } ``` > Source: [`Workshop-02/examples/relay-blink/relay-blink.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/relay-blink/relay-blink.ino) If it doesn't click, try inverting HIGH/LOW (some relays are active HIGH). ## Putting it on-chain Now wire the relay to wallet events. Receiving funds → relay on. Sending funds → relay off. ```cpp // Include necessary libraries for WiFi, HTTP requests, and JSON parsing #include #include #include // WiFi credentials - replace with your network details const char* ssid = "Your SSID"; const char* password = "Your Password"; // Koios API endpoint for fetching address information const char* apiUrl = "https://preprod.koios.rest/api/v1/address_info"; // Your Cardano wallet address (Preprod Testnet) String walletAddress = "addr_test1..."; // GPIO pin connected to relay module control input const int relayPin = 4; // Variables for timing balance checks unsigned long lastCheck = 0; // Timestamp of last balance check const unsigned long checkInterval = 30000; // Check every 30 seconds // Store previous balance to detect changes float previousBalance = 0; // Track current light state bool lightState = false; void setup() { // Initialize serial communication for debugging Serial.begin(115200); // Configure relay pin as output pinMode(relayPin, OUTPUT); // Start with light off (LOW = relay off for most modules) digitalWrite(relayPin, LOW); // Start WiFi connection WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.println("Connected to WiFi"); // Perform initial balance check on startup fetchWalletBalance(); } void loop() { // Check if WiFi connection is still active if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); while (WiFi.status() != WL_CONNECTED) { delay(1000); } } // Get current time in milliseconds unsigned long currentMillis = millis(); // Check if enough time has passed since last check if (currentMillis - lastCheck >= checkInterval) { fetchWalletBalance(); lastCheck = currentMillis; // Update last check timestamp } } void fetchWalletBalance() { // Only proceed if WiFi is connected if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Initialize HTTP client with API URL http.begin(apiUrl); // Set content type header for JSON request http.addHeader("Content-Type", "application/json"); // Create JSON payload with wallet address String jsonPayload = "{\"_addresses\":[\"" + walletAddress + "\"]}"; // Send POST request int httpResponseCode = http.POST(jsonPayload); // Check if request was successful if (httpResponseCode > 0) { // Get response body as string String response = http.getString(); // Create JSON document to parse response DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, response); // Check if JSON parsing was successful and response has data if (!error && doc.is() && doc.size() > 0) { // Get first address info object from array JsonObject addressInfo = doc[0]; // Extract balance and convert from Lovelace to ADA float balance = addressInfo["balance"] | 0.0; balance = balance / 1000000; // 1 ADA = 1,000,000 Lovelace // Check if balance increased (new transaction received) if (balance > previousBalance) { Serial.println("New transaction detected! Turning on light..."); turnOnLight(); // Activate relay to turn on light } else if (balance < previousBalance) { // Balance decreased (funds sent out) Serial.println("Balance decreased. Turning off light..."); turnOffLight(); // Deactivate relay to turn off light } // Update previous balance for next comparison previousBalance = balance; } } // Close HTTP connection http.end(); } } void turnOnLight() { // Set relay pin HIGH to activate relay (turn on light) digitalWrite(relayPin, HIGH); lightState = true; Serial.println("Light is ON"); } void turnOffLight() { // Set relay pin LOW to deactivate relay (turn off light) digitalWrite(relayPin, LOW); lightState = false; Serial.println("Light is OFF"); } ``` > Source: [`Workshop-02/examples/relay-events/relay-events.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/relay-events/relay-events.ino) Replace WiFi credentials and your wallet address (use a Preprod address starting with `addr_test1...`) before uploading. Send a test transaction to your wallet - the relay should fire. :::tip CardanoThings PingPong wallet For testing the relay loop without burning real test transactions, send tADA to the CardanoThings **PingPong** wallet - it auto-refunds your transaction (minus fees) within ~60 seconds, which round-trips through your wallet and triggers the relay twice. Address: `addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w` Preprod-only. ::: ## Next steps You now have the building blocks for blockchain-driven actuation. A few directions: - **Automated fountains.** Trigger when a specific transaction arrives. - **Vending machine.** Dispense product when payment confirms. - **Smart-home integration.** Lights, fans, appliances driven by token holdings or specific events. - **Event-driven displays.** Update a sign when conditions are met on-chain. ## Further Resources - [Arduino `digitalWrite()` reference](https://www.arduino.cc/reference/en/language/functions/digital-io/digitalwrite/) - controlling digital pins. - [Intro to ESP32-C3](https://www.youtube.com/watch?v=V9I9koQ0AeA) - video. - [Relays explained](https://www.youtube.com/watch?v=jXcdH1PgmMI) - how they work. - [SPI tutorial](https://www.youtube.com/watch?v=ZGaCXHvgcE4) and [I2C tutorial](https://www.youtube.com/watch?v=pxhg2Rwm_h8) - for when you wire more peripherals. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/02-read-and-output/light-up-the-tree) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-02](https://github.com/CardanoThings/Workshops/tree/main/Workshop-02).* --- ## Epoch Clock The capstone for Workshop 02: a physical Epoch Clock that visualises Cardano epoch progress on a circular WS2812 LED ring. Each of the 12 LEDs represents 1/12th of the epoch. ## Hardware requirements - ESP32-C3 microcontroller. - 12-LED WS2812 LED ring (NeoPixel). - Breadboard, jumper wires (M-M and M-F). - Optional: soldering iron for permanent installs. ## Epochs and slots A Cardano **epoch** is a ~5-day period during which the chain operates under specific parameters. Each epoch contains many slots; tracking the slot-within-epoch tells you how far through the epoch the chain has progressed. This project pulls in everything from earlier lessons: - WiFi connectivity from Workshop 01. - API calls from this workshop's lesson 1. - Display logic from this workshop's lesson 2. - Hardware integration from lesson 3. ## Setting up the LED ring We use a WS2812 (NeoPixel) ring - addressable RGB LEDs in a circle, each individually controllable. 12 LEDs is ideal because it maps cleanly to a clock-face metaphor. :::danger Current draw warning **WS2812 LEDs can draw significant current and damage your ESP32-C3 if mishandled.** Current facts: - Each LED can draw up to 60 mA at full white. - A 12-LED ring at full white can draw **720 mA**. - USB ports usually deliver 500 mA - 1 A. Insufficient for full brightness. - Exceeding ratings can damage your ESP32, USB port, or supply. Safety: - **Always set brightness low in code** (the sketch uses 5/255 ≈ 2%) when on USB power. - For brighter setups use an external 5V supply rated for ≥ 1 A. - With external power, tie grounds together (common ground). - Never run the ring at full brightness off the ESP32's 5V pin. - Test low first, then ramp up if you have proper external power. ::: ### Wiring WS2812 ring pins: - **V+** - 5V power. - **V-** - ground. - **IN** - data input. - **OUT** - data output (for daisy-chaining). Connect: 1. **V+** to 5V (low brightness on USB; external 5V for brighter). 2. **V-** to GND (common ground if external power). 3. **IN** to a GPIO pin for data (e.g., GPIO 4). WS2812 uses a single-wire timing protocol - any GPIO works. :::info OUT pin Only needed for daisy-chaining multiple rings. With a single ring, leave it. ::: :::info ESP32-C3 pinout reference Need a pinout reference for wiring? See the interactive ESP32-C3 pinout at [cardanothings.io](https://cardanothings.io), the official [ESP32-C3 datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf), or your board's specific schematic. Common pin protocols on the C3: **SPI** uses MOSI / MISO / SCK / SS-CS; **I2C** uses SDA / SCL (typically GPIO 8 / 9); **UART** uses TX / RX. ::: ### Install the library We use the Adafruit NeoPixel library: 1. Open Arduino IDE. 2. **Sketch → Include Library → Manage Libraries**. 3. Search for **Adafruit NeoPixel**. 4. Install the library by Adafruit. ## Basic LED ring test Before wiring anything to the chain, verify the ring lights up. This sketch lights each LED in sequence at very low brightness. ```cpp // Include the Adafruit NeoPixel library #include // Pin connected to the WS2812 data input #define LED_PIN 4 // Number of LEDs in the ring (12 LEDs) #define NUM_LEDS 12 // Create NeoPixel object // Parameter 1 = number of pixels // Parameter 2 = pin number // Parameter 3 = pixel type flags (NEO_GRB + NEO_KHZ800 for WS2812) Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); void setup() { // Initialize serial communication for debugging Serial.begin(115200); // Initialize the NeoPixel ring strip.begin(); // Set brightness to a very low value (5 out of 255) to protect ESP32-C3 // This is approximately 2% brightness - safe for USB power strip.setBrightness(5); // Clear all LEDs (turn them all off) strip.clear(); // Update the strip to apply changes strip.show(); Serial.println("LED Ring initialized. Starting blink sequence..."); } void loop() { // Loop through all 12 LEDs one at a time for (int i = 0; i < NUM_LEDS; i++) { // Clear all LEDs first strip.clear(); // Set the current LED to white (R=255, G=255, B=255) // The brightness is already limited by setBrightness(5) in setup() strip.setPixelColor(i, strip.Color(255, 255, 255)); // Update the strip to show the change strip.show(); // Print which LED is lit Serial.print("LED "); Serial.print(i); Serial.println(" ON"); // Wait 200 milliseconds before moving to next LED delay(200); } // After all LEDs have been lit, clear the display strip.clear(); strip.show(); } ``` > Source: [`Workshop-02/examples/led-ring-blink/led-ring-blink.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/led-ring-blink/led-ring-blink.ino) Update `LED_PIN` to match your wiring. If no LEDs light, double-check power, ground, and the data pin. ## Fetching epoch and block data The Koios `/tip` endpoint (from this workshop's first lesson) returns: - **epoch_no** - current epoch. - **epoch_slot** - slot within the current epoch (used for progress). - **abs_slot** - absolute slot. - **block_no** - current block height. We use `epoch_slot` to compute progress: each epoch has ~432,000 slots. Map percent-complete onto the 12-LED ring. :::info At slot 216,000 in an epoch, you're at 50% - six of 12 LEDs lit. ::: ## The Epoch Clock Combine WiFi, the API call, and ring control. The 12 LEDs light progressively in blue as the epoch progresses. ```cpp // Include necessary libraries #include #include #include #include #include // Pin connected to the WS2812 data input #define LED_PIN 4 // Number of LEDs in the ring (12 LEDs) #define NUM_LEDS 12 // Total slots in an epoch (approximately 432,000 on Mainnet) #define SLOTS_PER_EPOCH 432000 // Create NeoPixel object Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); // WiFi credentials const char* ssid = "Your SSID"; const char* password = "Your Password"; // Koios API endpoint const char* apiUrl = "https://preprod.koios.rest/api/v1/tip"; // Variables for timing API calls unsigned long lastCheck = 0; const unsigned long checkInterval = 60000; // Check every minute // Variables for walking LED - creates a clock-like second hand effect // The white LED moves around the ring every 5 seconds // 12 LEDs × 5 seconds = 60 seconds (1 minute) for a full rotation unsigned long lastWalkUpdate = 0; const unsigned long walkInterval = 5000; // Move to next LED every 5 seconds int walkPosition = 0; // Current position of walking LED (0-11) // Store current epoch data int currentEpoch = 0; int currentEpochSlot = 0; int lastEpoch = -1; void setup() { Serial.begin(115200); // Initialize LED ring strip.begin(); strip.setBrightness(5); // Low brightness for safety strip.clear(); strip.show(); // Connect to WiFi WiFi.begin(ssid, password); WiFi.setTxPower(WIFI_POWER_8_5dBm); // Workaround for ESP32-C3 Super Mini while (WiFi.status() != WL_CONNECTED) { delay(1000); } // Initial fetch fetchEpochData(); displayProgress(); } void loop() { // Check WiFi connection if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); while (WiFi.status() != WL_CONNECTED) { delay(1000); } } // Check if enough time has passed for API call unsigned long currentMillis = millis(); if (currentMillis - lastCheck >= checkInterval) { fetchEpochData(); displayProgress(); lastCheck = currentMillis; } // Update walking LED every 5 seconds (creates second-hand effect) if (currentMillis - lastWalkUpdate >= walkInterval) { updateWalkingLED(); lastWalkUpdate = currentMillis; } } void fetchEpochData() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; WiFiClientSecure client; client.setInsecure(); http.begin(client, apiUrl); int httpResponseCode = http.GET(); if (httpResponseCode > 0) { String response = http.getString(); JsonDocument doc; DeserializationError error = deserializeJson(doc, response); if (!error && doc.is() && doc.size() > 0) { JsonObject tip = doc[0]; currentEpoch = tip["epoch_no"] | 0; currentEpochSlot = tip["epoch_slot"] | 0; // Reset display if epoch changed if (currentEpoch != lastEpoch) { lastEpoch = currentEpoch; strip.clear(); strip.show(); delay(500); } } } http.end(); } } void displayProgress() { // Calculate epoch progress percentage int progressPercent = (currentEpochSlot * 100) / SLOTS_PER_EPOCH; if (progressPercent > 100) progressPercent = 100; // Calculate how many LEDs should be lit int ledsToLight = (progressPercent * NUM_LEDS) / 100; // Clear all LEDs strip.clear(); // Light up LEDs based on progress in blue for (int i = 0; i < ledsToLight; i++) { strip.setPixelColor(i, strip.Color(0, 0, 255)); // Blue } strip.show(); } void updateWalkingLED() { // Display epoch progress first (blue LEDs showing epoch completion) displayProgress(); // Add white walking LED at current position (creates clock second-hand effect) // This LED blinks white for 5 seconds at each position before moving strip.setPixelColor(walkPosition, strip.Color(255, 255, 255)); // White strip.show(); // Move to next position (wrap around after LED 11 to complete 60-second cycle) walkPosition = (walkPosition + 1) % NUM_LEDS; } ``` > Source: [`Workshop-02/examples/epoch-clock/epoch-clock.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-02/examples/epoch-clock/epoch-clock.ino) Update `LED_PIN` and WiFi credentials. Upload, and you should see LEDs progressively light around the ring as the chain moves through the epoch. ## Next steps You've finished Workshop 02 - you can fetch chain data, render it, and drive physical hardware off it. Some extensions: - **Visual variations** - clockwise / anti-clockwise / alternating; animations on new blocks. - **Colour-coded progress** - green early, yellow mid, red late in the epoch. - **Multi-network rings** - one ring per network (mainnet / preprod / preview). - **Epoch transition effects** - a chase animation when the new epoch starts. - **Custom enclosures** - 3D print, laser-cut acrylic, or wood housing. - **Battery / solar** - a portable epoch indicator. ## Further Resources - [Adafruit NeoPixel Library](https://github.com/adafruit/Adafruit_NeoPixel) - controlling WS2812s. - [Cardano Testnets](https://docs.cardano.org/cardano-testnets/environments) - Preview, Preprod, Mainnet. - [Koios `/tip` endpoint](https://preprod.koios.rest/#get-/tip) - the API doc. - [WS2812 LED guide](https://learn.adafruit.com/adafruit-neopixel-uberguide) - Adafruit's NeoPixel deep dive. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/02-read-and-output/epoch-clock) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-02](https://github.com/CardanoThings/Workshops/tree/main/Workshop-02).* --- ## Workshop 02: Read and Output This workshop covers reading data from the blockchain in intervals and using that data to trigger actions on your microcontroller. You will work with two more Cardano APIs (Koios and Blockfrost) and learn how to wire external hardware - a TFT display, a relay, and a WS2812 LED ring - to the ESP32. > Source code: [github.com/CardanoThings/Workshops/tree/main/Workshop-02](https://github.com/CardanoThings/Workshops/tree/main/Workshop-02) ## Steps 1. **[Fetch your Wallet Balance](./01-fetch-wallet-balance.md)** - Poll your stake address balance via Koios (and Blockfrost as an alternative), parse the JSON, and detect changes. 2. **[Display Data on your Microcontroller](./02-display-data.md)** - Configure the TFT_eSPI library for the Cheap Yellow Display and render the wallet balance on a 320×240 TFT. 3. **[Light up the Tree](./03-light-up-the-tree.md)** - Drive a 110V/220V relay from on-chain events. Includes safety guidance. 4. **[Epoch Clock](./04-epoch-clock.md)** - Build a physical Epoch Clock on a 12-LED WS2812 ring that lights up progressively as the epoch advances. ## What you'll need - Everything from Workshop 01. - The screen, relay module, and LED ring from the [Hardware reference](/docs/developers/curriculum/dapps/iot/hardware/), plus breadboard and jumper wires. - An external 5V supply if you want to run the LED ring brighter than ~2%. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/02-read-and-output) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-02](https://github.com/CardanoThings/Workshops/tree/main/Workshop-02).* --- ## Cardano Setup Set up a Cardano wallet on the Preprod testnet so the workshops have somewhere to send and receive funds. The workshops use [Yoroi](https://yoroi-wallet.com/), but any Cardano wallet that supports Preprod will work. ## Install Yoroi Yoroi is a browser-extension and mobile wallet for Cardano. Head to the [Yoroi website](https://yoroi-wallet.com/) and install the extension for your browser. ## Create a wallet After installing the extension, click **Create Wallet** in the extension. You will set a password and be shown a recovery phrase. Back up the recovery phrase somewhere safe - anyone who has it controls the wallet. - Create a fresh wallet for these workshops; don't reuse a mainnet wallet. - Write down the recovery phrase and never share it. ## Request tADA tADA is the testnet version of ADA, used to pay fees on Preprod. 1. **Switch to Preprod.** Open Yoroi → Settings → Switch Network → **Preprod Testnet**. An orange banner at the top confirms you're on Preprod. 2. **Copy your receive address.** Wallet → Receive tab → copy the address (starts with `addr_test1...`). 3. **Hit the faucet.** Go to [docs.cardano.org/cardano-testnets/tools/faucet](https://docs.cardano.org/cardano-testnets/tools/faucet), select **Preprod Testnet** as environment, **Receive test ADA** as action, paste your address, and click **Request funds**. Within a few seconds, the faucet sends a generous amount of tADA to your wallet, more than enough for every workshop in this section. ## Sending and receiving tADA Now that you have tADA in your wallet, practise sending and receiving - you'll need this flow constantly when testing IoT projects that interact with the chain. :::tip CardanoThings PingPong wallet For testing transaction flows on Preprod, send tADA to the CardanoThings **PingPong** wallet - it auto-refunds your transaction (minus the network fee) within ~60 seconds. Perfect for exercising flows without finding a friend with a Preprod wallet. Address: `addr_test1qpvla0l6zgkl4ufzur0wal0uny5lyqsg4rw7g6gxj08lzacth0hnd66lz6uqqz7kwkmx07xyppsk2cddvxnqvfd05reqf7p26w` Preprod-only. ::: To send tADA: open Yoroi → **Send** tab, paste the recipient address (use the PingPong address above to test), enter an amount, confirm. The transaction takes a few seconds to land. ## Checking transactions Once you've sent or received tADA, verify it on a block explorer using the transaction hash. ### What is a transaction hash (txhash)? A transaction hash (or txhash, transaction ID) is a unique identifier for a single transaction on the chain - a fingerprint. On Cardano they're 64-character hexadecimal strings (no `0x` prefix), e.g.: ``` d4d57c0339eb955c4c5f80d87779bbf6b820aa0387b2349adf2f7c7ce074c909 ``` Every Cardano transaction has a unique txhash you can use to look up its details. ### Finding your transaction hash In Yoroi: 1. Go to the **Transactions** tab. 2. Click any transaction in the list. 3. The transaction details panel includes the txhash (64-char hex string). 4. Copy the txhash. ### Looking up transactions on CardanoScan Once you have a txhash, look it up on [Preprod CardanoScan](https://preprod.cardanoscan.io/): 1. Go to [preprod.cardanoscan.io](https://preprod.cardanoscan.io/). 2. Paste the txhash into the search bar. 3. The transaction view shows: - Sender and receiver addresses - Amount transferred - Transaction fee - Block number and timestamp - Status (confirmed / pending) This is especially useful when testing IoT projects - you can verify that transactions were sent and received as expected. CardanoScan is the explorer these workshops use, but several others work too: [Adastat](https://adastat.net/), [Cexplorer](https://cexplorer.io/), [pool.pm](https://pool.pm/). They differ in features and UI but all let you look up transactions, addresses, and other on-chain data. ## Further Resources - [Yoroi Wallet](https://yoroi-wallet.com/) - install page. - [Cardano Testnets faucet](https://docs.cardano.org/cardano-testnets/tools/faucet) - get tADA on Preprod. - [Cardano.org](https://cardano.org/) - official Cardano site. - [Preprod CardanoScan](https://preprod.cardanoscan.io/) - block explorer for the Preprod testnet. - [Adastat](https://adastat.net/) - explorer for transactions, addresses, blockchain data. - [Cexplorer](https://cexplorer.io/) - detailed blockchain explorer (transactions, addresses, epochs). - [pool.pm](https://pool.pm/) - explorer + NFT viewer. - [Lido Nation](https://lidonation.com/) - articles and resources on Cardano and blockchain in general. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/01-basics/cardano-setup) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-01](https://github.com/CardanoThings/Workshops/tree/main/Workshop-01).* --- ## Arduino Setup Install the toolchain you'll use for every workshop in this section, then upload two starter sketches: a blink and a WiFi connect. ## Install Arduino IDE We use the [Arduino IDE](https://www.arduino.cc/en/software/#ide) as the development environment for every workshop. It's free, easy to install, and gets you up and running fast. Pick your operating system and follow the installer. :::tip Pro tip If you're an experienced developer and want a more advanced editor, look at [PlatformIO](https://platformio.org/) for VS Code, or the [Espressif Arduino SDK](https://docs.espressif.com/projects/arduino-esp32/en/latest/getting_started.html). For beginners, stick with the Arduino IDE. ::: ## Set up your microcontroller Once the IDE is installed, add support for ESP32 boards. ### Step 1: Install ESP32 board support The ESP32 package is available directly in the Arduino IDE Boards Manager. 1. Open Arduino IDE. 2. Go to **Tools → Board → Boards Manager**. 3. Search for **esp32**. 4. Find **"esp32 by Espressif Systems"** in the list. 5. Click **Install** (this may take a few minutes). 6. Wait for the installation to finish. ### Install CH340 driver (CYD users) If you're using a Cheap Yellow Display, you'll need the CH340 driver: 1. Download from the [SparkFun CH340 driver guide](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all). 2. Install it following the instructions for your OS. ### Step 2: Select your board After installation, select the board: :::danger USB cable matters Use a USB cable that supports **data transfer**, not just charging. Many cheap USB cables and the cables that ship with power banks are charging-only - the data lines aren't connected. If your computer doesn't recognise your ESP32, swap cables. ::: 1. Connect your ESP32 to your computer via USB. 2. Go to **Tools → Board → esp32**. 3. Select **ESP32C3 Dev Module** (or the variant that matches your board). 4. Go to **Tools → Port** and pick the port your ESP32 is on: - **Windows:** usually `COM3`, `COM4`, etc. - **macOS:** usually `/dev/cu.usbserial-*` or `/dev/cu.SLAB_USBtoUART`. - **Linux:** usually `/dev/ttyUSB0` or `/dev/ttyACM0`. ## Your first sketch Upload a blink sketch to confirm the toolchain works. :::info LED pin This sketch uses pin 8 on the ESP32-C3. On the Cheap Yellow Display (CYD), the LED is on pin 4. ::: ```cpp // Define the pin number for the LED #define LED_PIN 8 // Setup function is called once when the microcontroller starts up. void setup() { pinMode(LED_PIN, OUTPUT); // Set the LED pin as an output. } // Loop function is called repeatedly. void loop() { digitalWrite(LED_PIN, HIGH); // Turn the LED on (HIGH is the voltage level). delay(1000); // Wait for 1 second. digitalWrite(LED_PIN, LOW); // Turn the LED off by making the voltage LOW. delay(1000); // Wait for 1 second. } ``` Copy the code into the Arduino IDE and upload it. The on-board LED should blink. > Source: [`Workshop-01/examples/blink/blink.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-01/examples/blink/blink.ino) ## Connect to WiFi Now connect your microcontroller to WiFi. ```cpp #include // Include the WiFi library for ESP32 const char* ssid = "Your SSID"; // Your WiFi SSID const char* password = "Your Password"; // Your WiFi Password // Setup function is called once when the microcontroller starts up. void setup() { Serial.begin(115200); // Initialize the serial communication at 115200 baud rate WiFi.mode(WIFI_STA); // Set WiFi mode to Station (client mode) WiFi.setTxPower(WIFI_POWER_8_5dBm); // Workaround for ESP32-C3 Super Mini WiFi.begin(ssid, password); // Connect to WiFi using the SSID and Password // Wait for the connection to be established while (WiFi.status() != WL_CONNECTED) { delay(1000); // Wait for 1 second Serial.println("Connecting to WiFi..."); // Print "Connecting to WiFi..." to the serial monitor } Serial.println("Connected to WiFi"); // Print "Connected to WiFi" to the serial monitor Serial.println("IP address: "); // Print "IP address: " to the serial monitor Serial.println(WiFi.localIP()); // Print the IP address to the serial monitor } // Loop function is called repeatedly. void loop() { // Check if the WiFi connection is lost if (WiFi.status() != WL_CONNECTED) { // Print "WiFi connection lost. Reconnecting..." to the serial monitor Serial.println("WiFi connection lost. Reconnecting..."); WiFi.reconnect(); // Reconnect to WiFi // Wait for the connection to be established while (WiFi.status() != WL_CONNECTED) { delay(1000); // Wait for 1 second Serial.print("."); // Print "." to the serial monitor Serial.print("."); } Serial.println("Reconnected!"); // Print "Reconnected!" to the serial monitor } } ``` Update SSID/password, upload, and watch the serial monitor - you should see the IP address printed. > Source: [`Workshop-01/examples/wifi/wifi.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-01/examples/wifi/wifi.ino) :::info ESP32-C3 Super Mini WiFi workaround If you're using an ESP32-C3 Super Mini and hit WiFi connection issues, the sketch includes `WiFi.setTxPower(WIFI_POWER_8_5dBm);` as a workaround. This sets the transmit power to 8.5 dBm and resolves connectivity problems specific to that board variant. ::: :::tip Serial Monitor Open it via **Tools → Serial Monitor** or `Ctrl+Shift+M` (`Cmd+Shift+M` on Mac). Set the baud rate to **115200** to match the sketch. ::: ## Further Resources - [Arduino Workshop Video Tutorial](https://www.youtube.com/watch?v=EdXQUEMOfgU&list=PLPK2l9Knytg5s2dk8V09thBmNl2g5pRSr&index=2) - covers Arduino setup and basics. - [Arduino Documentation](https://docs.arduino.cc/) - official. - [Arduino IDE Download](https://www.arduino.cc/en/software/) - installer. - [SparkFun CH340 Driver Guide](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all) - Windows/macOS/Linux. - [Adafruit CH9102 Driver Guide](https://learn.adafruit.com/how-to-install-drivers-for-wch-usb-to-serial-chips-ch9102f-ch9102/overview) - alternate WCH USB-serial chips. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/01-basics/arduino-setup) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-01](https://github.com/CardanoThings/Workshops/tree/main/Workshop-01).* --- ## API Setup & First Call Make your first API call from the ESP32 to a Cardano API. We'll use [Koios](https://preprod.koios.rest/), a free and open-source REST API for Cardano data. ## What is Koios? Koios is a free, open-source REST API for the Cardano blockchain (mainnet and testnets). All responses are JSON. The full endpoint reference is at [preprod.koios.rest](https://preprod.koios.rest/). For this lesson we'll use the [chain tip](https://preprod.koios.rest/#get-/tip) endpoint to get the current epoch. ## The API endpoint Open the [chain tip endpoint](https://preprod.koios.rest/api/v1/tip) in your browser to see the JSON response. :::tip For readable JSON in the browser, install the [Awesome JSON Viewer](https://github.com/rbrahul/Awesome-JSON-Viewer) extension. ::: The response includes the current epoch number, the absolute slot, the epoch slot, the block height, the block number, the block time, and the block hash. We'll fetch it from the ESP32 and log the epoch to the serial monitor. ## Fetching data in Arduino We use the [HTTP Client](https://github.com/espressif/arduino-esp32/tree/master/libraries/HTTPClient) library to make the request and the [ArduinoJSON](https://www.arduino.cc/reference/en/libraries/arduinojson/) library to parse the response. ### Install ArduinoJSON 1. Open Arduino IDE. 2. **Tools → Manage Libraries** (or `Ctrl+Shift+I` / `Cmd+Shift+I`). 3. Search for **ArduinoJson**. 4. Install **"ArduinoJson" by Benoit Blanchon** (the one with millions of downloads). The HTTPClient library ships with the ESP32 board package, so no extra install needed. ## The sketch The sketch will: 1. Connect to WiFi. 2. Make an HTTP GET to the Koios `/tip` endpoint. 3. Parse the JSON response. 4. Log the epoch number to the serial monitor. ```cpp #include // Include the WiFi library for ESP32 #include // Include the HTTPClient library for ESP32 #include // Include the ArduinoJSON library for ESP32 const char* ssid = "Your SSID"; // Your WiFi SSID const char* password = "Your Password"; // Your WiFi Password const char* apiUrl = "https://preprod.koios.rest/api/v1/tip"; // The API URL for the Koios API int epochNumber = 0; // Variable to store the epoch number void setup() { Serial.begin(115200); // Initialize the serial communication at 115200 baud rate WiFi.begin(ssid, password); // Connect to WiFi using the SSID and Password // Wait for the connection to be established while (WiFi.status() != WL_CONNECTED) { delay(1000); // Wait for 1 second Serial.println("Connecting to WiFi..."); // Print "Connecting to WiFi..." to the serial monitor } Serial.println("Connected to WiFi"); // Print "Connected to WiFi" to the serial monitor Serial.println("IP address: "); // Print "IP address: " to the serial monitor Serial.println(WiFi.localIP()); // Print the IP address to the serial monitor } void parseJsonResponse(String response) { JsonDocument doc; // Create a JSON document DeserializationError error = deserializeJson(doc, response); // Deserialize the JSON response if (error) { Serial.print("Failed to parse JSON: "); Serial.println(error.c_str()); return; } // Extract the epoch number from the first element in the array epochNumber = doc[0]["epoch_no"]; } void loop() { // Check if the WiFi connection is lost if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection lost. Reconnecting..."); // Print "WiFi connection lost. Reconnecting..." to the serial monitor WiFi.reconnect(); // Reconnect to WiFi while (WiFi.status() != WL_CONNECTED) { delay(1000); // Wait for 1 second Serial.print("."); // Print "." to the serial monitor } Serial.println("Reconnected!"); // Print "Reconnected!" to the serial monitor } makeHttpRequest(); // Make the HTTP request delay(60000); // Wait 60 seconds before making the next request } void makeHttpRequest() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; http.begin(apiUrl); int httpResponseCode = http.GET(); if (httpResponseCode > 0) { String response = http.getString(); Serial.println("HTTP Response Code: " + String(httpResponseCode)); Serial.println("Response:"); Serial.println(response); parseJsonResponse(response); Serial.println("Epoch number: " + String(epochNumber)); } else { Serial.println("Error in HTTP request"); Serial.println("HTTP Response Code: " + String(httpResponseCode)); } http.end(); } else { Serial.println("WiFi not connected"); } } ``` > Source: [`Workshop-01/examples/koios-api/koios-api.ino`](https://github.com/CardanoThings/Workshops/blob/main/Workshop-01/examples/koios-api/koios-api.ino) ## What you'll see in the Serial Monitor After uploading and opening the Serial Monitor at 115200 baud, expect output like: ``` Connecting to WiFi... Connecting to WiFi... Connected to WiFi IP address: 192.168.1.XXX HTTP Response Code: 200 Response: [{ "hash": "14c6413b8df915c58d9da162cf22ad58dc52834c8ce7105fe91d08e804cb5a36", "epoch_no": 252, "abs_slot": 107460097, "epoch_slot": 237697, "block_height": 4122947, "block_no": 4122947, "block_time": 1763143297 }] Epoch number: 252 ``` The output repeats every 60 seconds with fresh blockchain data. - **Connecting to WiFi...** - joining your network. - **HTTP Response Code: 200** - successful API call. - **Response** - the full JSON from Koios. - **Epoch number** - the value parsed out of the JSON. :::warning Troubleshooting If you see `HTTP Response Code: -1` or connection errors: - Confirm WiFi credentials. - Confirm the ESP32 is in WiFi range. - Check that your firewall isn't blocking HTTPS. - Confirm the API is up at [preprod.koios.rest](https://preprod.koios.rest/). ::: ## Further Resources - [Koios documentation](https://preprod.koios.rest/) - full endpoint reference. - [REST API Tutorial](https://www.restapitutorial.com/) - REST primer. - [Awesome JSON Viewer](https://github.com/rbrahul/Awesome-JSON-Viewer) - browser extension for readable JSON. - [Insomnia](https://insomnia.rest/) - free open-source API client. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/01-basics/api-setup) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-01](https://github.com/CardanoThings/Workshops/tree/main/Workshop-01).* --- ## Workshop 01: The Basics This first workshop sets you up for everything that follows: install and fund a Cardano wallet on the Preprod testnet, install the Arduino IDE, set up your microcontroller, and make your first API call to a Cardano endpoint. > Source code: [github.com/CardanoThings/Workshops/tree/main/Workshop-01](https://github.com/CardanoThings/Workshops/tree/main/Workshop-01) ## Steps 1. **[Cardano Setup](./01-cardano-setup.md)** - Install [Yoroi](https://yoroi-wallet.com/), switch to Preprod, create a wallet, and request tADA from the faucet. 2. **[Arduino Setup](./02-arduino-setup.md)** - Install the Arduino IDE, add ESP32 board support, install the CH340 driver if needed, and upload your first blink sketch. 3. **[API Setup & First Call](./03-api-setup.md)** - Use the Koios `/tip` endpoint to fetch the current epoch from your microcontroller and log it to the serial monitor. ## What you'll need - The board and USB data cable from the [Hardware reference](/docs/developers/curriculum/dapps/iot/hardware/). - A computer with the Arduino IDE installed. - WiFi with internet access. - Yoroi wallet on Preprod testnet. --- *Adapted from the [CardanoThings](https://cardanothings.io/workshops/01-basics) workshop series, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source code: [github.com/CardanoThings/Workshops/Workshop-01](https://github.com/CardanoThings/Workshops/tree/main/Workshop-01).* --- ## Troubleshooting Quick fixes for the issues most readers run into while working through this section. ## My ESP32-C3 won't connect to WiFi Check WiFi credentials and confirm the ESP32-C3 is on the same network as your computer. Two things specific to the C3: - **The ESP32-C3 will not connect to 5 GHz WiFi networks.** Use a 2.4 GHz network. - **Use the WiFi power workaround** in your sketch: ```cpp WiFi.setTxPower(WIFI_POWER_8_5dBm); ``` This sets the WiFi transmit power to 8.5 dBm and resolves connectivity issues specific to the C3. See also: the WiFi sketch in [Workshop 01: Arduino Setup](/docs/developers/curriculum/dapps/iot/the-basics/02-arduino-setup) which already includes this workaround. ## My code won't upload to my ESP32 CYD Check, in order: 1. **Right board and port selected** in Arduino IDE (`Tools → Board → ESP32 → ESP32 Dev Module`, then `Tools → Port`). 2. **CH340 driver installed.** The CYD uses a CH340 USB-serial chip; the driver isn't installed by default on macOS or some Windows setups. See [SparkFun's CH340 install guide](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all). 3. **Different USB cable.** Many cheap USB cables are charge-only - they don't have data lines. If your computer doesn't see the board, swap the cable. 4. **Lower the upload speed.** Drop to **115200 baud** in `Tools → Upload Speed`. Slower uploads are sometimes the only way to flash on flaky USB connections. ## I'm having issues with I2C communication Most I2C problems boil down to one of: - **Wrong SDA / SCL pins** in your code (check your board's actual I2C pins - for the ESP32-C3 the defaults are GPIO 8 / GPIO 9). - **Wrong I2C address** for the device. Different sensors use different addresses (AHT10 = `0x38`, SH1106 OLED = `0x3C` or `0x3D`). - **Missing pull-up resistors** on SDA / SCL. Most breakout boards include them; if yours doesn't, add 4.7 kΩ - 10 kΩ between each line and VCC. If you don't know the I2C address of your device, run the **I2C scanner sketch** included in [Workshop 03: Connect and Read Sensor Data](/docs/developers/curriculum/dapps/iot/input-and-write/01-connect-and-read-sensor-data) - it enumerates every device responding on the bus. ## My board is not detected by the Arduino IDE Check, in order: 1. **Right board selected** (`Tools → Board → ...`). 2. **ESP32 board package installed** (`Tools → Board → Boards Manager` → search "esp32" → install). See [Arduino Setup](/docs/developers/curriculum/dapps/iot/the-basics/02-arduino-setup) for the full setup. 3. **CH340 driver installed** if using the CYD. See [SparkFun's CH340 install guide](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all) or the [Adafruit CH9102 guide](https://learn.adafruit.com/how-to-install-drivers-for-wch-usb-to-serial-chips-ch9102f-ch9102/overview) for similar chips. 4. **Different USB cable** (data, not charge-only). ## My serial monitor is not working or shows garbled text Two likely causes: - **Baud rate mismatch.** The serial monitor's baud rate (bottom-right of the IDE's Serial Monitor) must match `Serial.begin(...)` in your sketch. The workshops all use **115200**. - **Bad USB cable.** Same as above - try a different one. ## Next steps - [Ship to Production](/docs/developers/curriculum/production/overview): the curriculum's final module. Choose your chain access, harden the app, and scale. --- *Adapted from the [CardanoThings](https://cardanothings.io/troubleshooting) project, originally produced under [Project Catalyst Fund 11](https://projectcatalyst.io/funds/11). Source: [github.com/CardanoThings](https://github.com/CardanoThings).* --- ## Listening for ada payments Detecting incoming payments is a core need for shops, payment gateways, donations, subscriptions, ticketing, and vending or IoT machines: you need to know reliably when ada arrives at an address. ## How it works Every method follows the same loop: 1. **Generate a payment address** for the order (often shown as a [CIP-13 QR code](#as-a-uri-or-qr-code-cip-13)). 2. **Display it** to the customer. 3. **Poll the address** for incoming transactions. 4. **Compare the received amount** against what you expect. 5. **Fulfill** once the payment confirms. ![Payment flow](./img/ada-online-shop.png) The only thing that differs between methods is *how you read the chain*: a hosted API, your own node via cardano-cli, or a cardano-wallet service. Start with Blockfrost unless you already run your own infrastructure. Cardano's read APIs don't push events, so every method here **polls** on an interval. In production you can replace the loop two ways: with a provider **webhook** (for example [Blockfrost webhooks](https://blockfrost.dev/docs/start-building/webhooks/)) that calls your backend when a matching transaction lands, or with an [event-stream indexer](/docs/developers/curriculum/production/indexing-and-analytics) you run yourself, which tails the chain and forwards matching transactions with no third party in the path. ## Detecting a payment Generate a fresh payment address per order, then poll it: read the address's UTXOs, sum the lovelace, and compare to what you expect. The same loop works through either SDK's provider, or with cardano-cli against your own node. ```typescript const client = Client.make(preprod).withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY!, }) const expectedLovelace = 1_000_000n async function receivedLovelace(address: string) { const utxos = await client.getUtxos(Address.fromBech32(address)) return utxos.reduce((sum, utxo) => sum + (utxo.assets.lovelace ?? 0n), 0n) } // poll every few seconds until paid const timer = setInterval(async () => { if ((await receivedLovelace(address)) >= expectedLovelace) { clearInterval(timer) // payment confirmed: fulfill the order } }, 3000) ``` ```typescript const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY!) const expectedLovelace = 1_000_000n async function receivedLovelace(address: string) { const utxos = await provider.fetchAddressUTxOs(address) return utxos.reduce((sum, u) => { const lovelace = u.output.amount.find((a) => a.unit === "lovelace")?.quantity ?? "0" return sum + BigInt(lovelace) }, 0n) } const timer = setInterval(async () => { if ((await receivedLovelace(address)) >= expectedLovelace) { clearInterval(timer) // payment confirmed: fulfill the order } }, 3000) ``` If you run your own [node](/docs/operators/node/installing-cardano-node), query the address UTXOs directly and sum their lovelace, no third-party API involved: ```bash cardano-cli query utxo --address "$(cat payment.addr)" --testnet-magic 1 --output-json ``` ```js const expectedLovelace = 1_000_000n function receivedLovelace(addr) { const out = execSync(`cardano-cli query utxo --address ${addr} --testnet-magic 1 --output-json`) const utxos = JSON.parse(out.toString()) return Object.values(utxos).reduce((sum, u) => sum + BigInt(u.value.lovelace), 0n) } // poll receivedLovelace(addr) on an interval and compare to expectedLovelace ``` For a complete point-of-sale app with a React UI, QR codes, and live USD/ADA conversion, fork the [Cardano POS starter](https://github.com/fill-the-fill/cardano-pos-starting-point). :::tip Wait for confirmations A transaction in a recent block can still be [rolled back](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros#how-does-finality-work). Cardano produces a block roughly every 20 seconds, so for anything valuable, wait 10-20 blocks (a few minutes) before treating a payment as final; the larger the amount, the deeper you should wait. Track deposits by transaction id and credit each one exactly once, only after your chosen depth, so a rollback that replays the same transaction cannot double-credit. ::: ## Requesting a payment Detection is the receiver's half. The sender's half is **requesting the payment**: from your dApp, the user's connected wallet builds a transfer to your address, signs it, and submits it. Together they close the loop, the user pays and you detect it. The simplest case is a plain transfer, where the user pays out of their own wallet with no app key or logic involved. Once they have [connected a wallet](/docs/developers/curriculum/dapps/connect-a-wallet), a "pay" button builds a single `payToAddress`, the wallet prompts for a signature, and you submit: ```typescript // walletApi = await window.cardano..enable() from connecting the wallet declare const walletApi: any // A Signing Client: a provider for params + submission, the connected wallet for signing const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }) .withCip30(walletApi) const tx = await client .newTx() .payToAddress({ address: "addr_test1...", assets: Assets.fromLovelace(10_000_000n) }) // pay 10 ADA .build() const txHash = await (await tx.sign()).submit() // wallet prompts the user, then submit ``` ```tsx function PayButton({ recipient, lovelace }) { const { wallet, connected } = useWallet() async function pay() { const unsignedTx = await new MeshTxBuilder() .txOut(recipient, [{ unit: "lovelace", quantity: lovelace }]) .changeAddress(await wallet.getChangeAddress()) // browser wallet: bech32 string .selectUtxosFrom(await wallet.getUtxos()) // browser wallet: UTxO list .complete() const signedTx = await wallet.signTx(unsignedTx) // wallet prompts the user await wallet.submitTx(signedTx) // submits through the wallet, no API key } return } ``` Show the amount and recipient before prompting, and handle the wallet's rejection and loading states. Two things decide where the build belongs: - **Provider keys.** The Evolution flow submits through a provider, so its key lives wherever the client runs; in the browser that key is exposed. Mesh's browser wallet submits through the wallet itself, so a plain transfer needs no key client-side. For anything beyond a trivial transfer, prefer building server-side. - **App-controlled transactions.** The moment your app contributes its own inputs, a minting policy, or a co-signature, the build moves to the backend and the user only partial-signs. That is the [sponsored and multi-party](/docs/developers/curriculum/dapps/sponsored-transactions) pattern, and the reason [connecting a wallet](/docs/developers/curriculum/dapps/connect-a-wallet) recommends the frontend only sign. ### As a URI or QR code (CIP-13) The connected-wallet path assumes the payer is already in your dApp with a browser wallet. The other way to request a payment needs no connection at all: encode the request as a [CIP-13](https://cips.cardano.org/cip/CIP-0013) `web+cardano:` URI, a link or QR code the payer opens or scans with a compatible mobile wallet, which opens pre-filled with your address and the amount for them to confirm and sign. This is the phone-first case with no dApp connector involved: a point-of-sale terminal, a code shown on a screen or printed on an invoice, a donation link. The format is `web+cardano:{address}?amount={ada}`, with the amount in decimal ada. Treat it as send-side convenience only. Wallet support for the amount varies, so it may or may not arrive pre-filled, and nothing tells you the payer scanned the code or what they will actually send. The URI improves the payer's experience but is never the source of truth: you still confirm exactly as [above](#detecting-a-payment), by polling the address for the lovelace you expect. To match a specific incoming payment to a specific request when you can't attach a memo, make the requested amount unique per request; a follow-up proposal ([CIP-157](https://github.com/cardano-foundation/CIPs/pull/843)) adds a proper payment identifier to the URI, so check its status and wallet support before relying on it. For a full device-hosted build of this, a QR payment terminal running on hardware, see [IoT Workshop 05: CIP-13 Integration](/docs/developers/curriculum/dapps/iot/qr-code-payments/02-cip13-integration). ## Use cases E-commerce checkout, payment gateways, donation platforms, subscription billing, event ticketing, in-app purchases, and vending or IoT machines: anywhere you fulfill something only after ada arrives. ## Next steps - [Sponsored transactions](/docs/developers/curriculum/dapps/sponsored-transactions): multi-party transactions where someone else covers the fee - [Ship to Production](/docs/developers/curriculum/production/overview): take the app from testnet to mainnet --- ## Oracles on Cardano ## What are oracles? Oracles connect blockchains with external data sources, bridging the gap between on-chain smart contracts and off-chain information. They fetch, verify, and deliver real-world data to smart contracts in a format they can use. The name comes from ancient oracles who delivered messages from the gods to mortals. Modern blockchain oracles bring real-world information from APIs, websites, and datasets onto the blockchain where smart contracts can access it. ```mermaid graph TD A[Real World Data Sources] --> B[APIs] A --> C[Websites] A --> D[Datasets] B & C & D --> E[Oracle Network] E --> F[Fetch Data] F --> G[Validate & Verify] G --> H[Reach Consensus] H --> I[Publish to Cardano] I --> J[Smart Contract Readsvia Reference Input] style E fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style I fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` Blockchains are deterministic systems that can only see data within their own ledger. Oracles solve this limitation by bringing external data on-chain. ## Why oracles matter Smart contracts execute conditional logic: when event X happens, trigger action Y. Their code runs on a decentralized network, producing the same result every time. This "trustless" quality comes from cryptographic proofs and distributed consensus and no need for a trusted third party. But smart contracts need real-world data as inputs. A DeFi protocol needs current prices. An insurance contract needs weather data. A prediction market needs event outcomes. This data must be trustworthy because smart contract execution has real economic consequences, and blockchain transactions can't be reversed. Common oracle use cases: - **Price feeds**: exchange-rate changes trigger trades, liquidations, or limit orders in DeFi protocols. - **Real-world events**: weather data triggers crop-insurance payouts; flight delays trigger travel-insurance claims. - **Sports and betting**: game scores trigger payouts in prediction markets. - **Cross-chain data**: bridge contracts need information from other blockchains. - **Supply chain and IoT**: tracking requires sensor data, GPS coordinates, shipment verification. - **Randomness**: Raffles, lotteries, and games need a verifiable random draw, which a validator cannot generate itself. See [On-chain randomness](/docs/developers/curriculum/dapps/oracles/randomness) ## The oracle problem The "oracle problem" refers to a fundamental challenge: how can smart contracts trust external data to be authentic and accurate? DeFi alone is critically dependent on oracle-provided data. But there are many opportunities for false data to slip into the collection, validation, and publication pipeline. This creates a lucrative attack vector, bad actors can trigger large payouts from smart contracts by feeding them false information. ### Key challenges - **Single point of failure**: an oracle that pulls from just one data source is a critical vulnerability. If that source is hacked or malfunctions, every smart contract using the oracle is affected. - **Man-in-the-middle attacks**: data can be intercepted and modified between the source and the blockchain, and preventing this is hard. - **Lack of transparency**: some oracles don't show how they collect and validate data. You see a price appear on-chain with no way to verify where it came from. - **Consensus vs. authenticity**: a decentralized oracle pool can agree on a value without that value being authentic. Agreement on bad data is still bad data. ## Oracles on Cardano's eUTXO model Cardano's Extended UTXO (eUTXO) model offers unique advantages for oracle implementations. A functional oracle system on Cardano requires three components: diverse data sources, a computation platform to validate accuracy, and network participants to transfer data on-chain. ### Reference inputs The Vasil hard fork introduced reference inputs, UTXOs that transactions can read without consuming them. This eliminates a major bottleneck for oracles: ```mermaid graph TD A[Oracle UTXOADA/USD: $0.50] A -.->|Reference| B[Transaction 1] A -.->|Reference| C[Transaction 2] A -.->|Reference| D[Transaction 3] A -.->|Reference| E[Transaction 4] B & C & D & E --> F[All Execute in Parallel] style A fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` ### Multi-oracle validation Smart contracts can reference UTXOs from multiple oracle providers simultaneously, performing on-chain reconciliation. The script reads values from different oracles and verifies they fall within an acceptable deviation threshold: ```mermaid graph LR A[Smart Contract] -->|Ref| B[Oracle A$0.505] A -->|Ref| C[Oracle B$0.502] A -->|Ref| D[Oracle C$0.498] B & C & D --> E[Validation Script] E --> F{Within ±2%?} F -->|Yes| G[Execute] F -->|No| H[Reject] style B fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style C fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style D fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` This on-chain check provides a trustless, programmatic guarantee against single oracle network failure or attack. Even if one oracle is compromised, the deviation check catches the problem. ## Publication models Oracles use different publication models depending on the use case: ### Push model Data is published continuously at regular intervals. Smart contracts read whatever's most recent. Updates happen: - At fixed intervals (e.g., every 5 minutes, hourly) - When data deviates beyond a threshold from the last publication - Or both, regular updates plus deviation triggers ```mermaid graph LR A[Oracle Service] --> B[Update Trigger] B --> C[Fetch & Validate] C --> D[Publish to Chain] D --> E[Oracle UTXO] F[DApp] -.->|Read| E style A fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style E fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` ### Pull model Data is fetched only when requested. A smart contract or user asks for data, and the oracle responds (just-in-time delivery). ```mermaid graph LR A[DApp] --> B[Request Data] B --> C[Oracle Fetches] C --> D[Validates] D --> E[Publish to Chain] A -.->|Read| E style C fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style E fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` ### Who publishes, and what that guarantees The push/pull split is also a **trust choice**, not just a question of timing. In a **push** design, the oracle network writes the price on-chain itself, into a UTXO your contract reads as a reference input. The value is produced and published independently of the protocol that consumes it, so a protocol cannot quietly substitute a different number: it never touches the publication step. In a **pull** design, the oracle signs the price off-chain and whoever builds the transaction submits it on-chain, where a validator checks that signature before accepting the value. This is the model Cardano's recommended oracle, [Pyth](/docs/developers/curriculum/dapps/oracles/pyth), uses, and it is a standard, sound pattern: the signature is what makes it trustless. Verifying several oracle signatures instead of one raises the bar further, but the shape is the same. A common on-chain shape for pull feeds: the signed price rides in the redeemer of a [withdrawal validator](/docs/developers/curriculum/smart-contracts/write-a-validator#withdrawal-validator) that runs once for the whole transaction, while an oracle NFT read as a reference input only *authenticates* the feed's identity, so every consumer in the transaction shares one verification and the value stays per-transaction fresh without ever being written into a UTXO first. Be precise, though, about what that signature does and does not cover: - **Integrity is guaranteed.** A forged or altered price will not verify. No one can feed your contract a number the oracle did not sign. - **Liveness and timing are not.** Whoever assembles the transaction decides whether and when to include an update, so a pull feed can be withheld, delayed, or posted only when it suits the submitter, and the signature check alone will not catch that. Enforce a freshness window yourself (Pyth exposes `timestamp_us` for exactly this), and where reuse matters, post the verified price into a public oracle UTXO that any contract can reference, so the feed stays composable instead of staying locked inside one protocol's transaction. This is also why the [multi-oracle reconciliation](#multi-oracle-validation) above is worth the effort: reading and cross-checking more than one feed on-chain protects you when any single feed is stale, withheld, or wrong. ## Designing with a price feed Once your contract can read a verified price, the design question becomes: *when* does it read, and *which parts* of the update does it use? Most oracle-consuming designs reduce to one of two shapes. **Settlement at a deadline.** The contract stores a question at creation (which feed, what threshold, by when) and reads the oracle exactly once, at resolution. Prediction markets, options expiry, and parametric insurance all work this way. The timing belongs in the transaction validity interval, not just in application code: interactions that must happen before the deadline require the validity upper bound at or below it, and the resolving transaction requires the lower bound at or above it. That turns the freshness rule from the previous section into something the ledger enforces structurally rather than something your off-chain code promises. **Live parameter.** Every interaction reads the current update and feeds it into the contract's logic as an input: lending protocols checking collateral ratios, liquidation triggers, dynamic pricing, in-game economies. Here the freshness window matters on every transaction, because each one acts on the value it carries. The update itself carries more signal than the headline number, and each field maps to a mechanic: - **Price and exponent** are the headline value. - **Confidence and the bid-ask spread** measure how certain the market is. A contract can widen its safety margins, scale position limits, or refuse to act at all when the spread blows out. That is volatility protection with no extra infrastructure. - **The EMA against the spot price** is a momentum signal: spot above the moving average means the asset is trending up. This gives trend-aware logic without storing any price history on-chain. - **Two feeds combined** yield a cross-asset ratio, so anything can be priced in anything: an ADA-denominated contract can settle a EUR obligation by dividing two USD feeds. - **The update timestamp** can be an input, not only an accept/reject check: behavior can degrade gracefully as data ages instead of failing outright. Two architecture patterns are worth knowing. A market or game lifecycle can live in a single UTXO identified by a [state-thread token](https://aiken-lang.org/fundamentals/common-design-patterns#state-thread-tokens-aka-stt), with position tokens minted against user actions and burned to claim; the oracle read then happens exactly once, at the state transition that settles the outcome. And keeping oracle verification in a swappable provider validator, separate from a pure logic validator that consumes normalized price data, lets you test the logic against a mock provider on a local devnet and swap in the real oracle for production. The [Pyth guide](/docs/developers/curriculum/dapps/oracles/pyth#validator-patterns) shows the settlement shape in working Aiken, and its [complete example](/docs/developers/curriculum/dapps/oracles/prediction-market) composes both patterns into a full prediction market. ## Security considerations Oracle security matters because smart contracts depend on accurate external data. Oracles defend against bad data in several ways: ```mermaid graph TD A[Multiple Data Sources] A --> B[Source 1: $0.505] A --> C[Source 2: $0.502] A --> D[Source 3: $100.00] A --> E[Source 4: $0.498] B & C & D & E --> F[Oracle Nodes] F --> G[Outlier Detection] G --> H[Consensus Algorithm] H --> I[Final Value: $0.502] I --> J[Publish to Cardano] style F fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style J fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` ### Data source diversity Multiple independent data sources verify accuracy and reduce vulnerability. If one source is compromised or fails, others catch the problem. Aggregating diverse sources helps identify outliers and produces more reliable values. ### Decentralized validation Multiple independent validator nodes collect and verify data before publication. This reduces single points of failure and makes it harder for attackers to manipulate feeds they'd need to compromise multiple nodes. ### Cryptographic verification Oracles use cryptographic signatures, tokens, or NFTs to prove data authenticity. Smart contracts verify these proofs before accepting oracle data as valid input. ### Transparency and auditability Audit trails document how data was collected, validated, and published. This transparency lets you verify oracle operations and hold providers accountable. ### Outlier detection Statistical methods identify and exclude anomalous data that deviates significantly from expected ranges, preventing manipulation or errors from affecting outputs. ## Choosing an oracle The factors that actually differentiate oracles are concrete, and the sections above explain what each one buys you: the data sources a feed draws from and how diverse they are, the publication model (push or pull) and the trust choice it implies, how distributed the operator set is, whether collection and validation are auditable, the integration effort, and the fee for consuming updates. Weigh them against your use case, in particular against whether you read the feed once at settlement or on every interaction. ### Recommended: Pyth For most Cardano applications, **[Pyth](/docs/developers/curriculum/dapps/oracles/pyth)** is the recommended oracle: sub-second, high-frequency price feeds through a pull-based model, with on-chain verification handled by an Aiken library, so your contract reads verified updates directly from the transaction it validates. Contracts can also read **multiple** oracle feeds and reconcile them on-chain, as shown above, so a single feed failing or being manipulated does not compromise the result. See the [Pyth integration guide](/docs/developers/curriculum/dapps/oracles/pyth) to wire it into your validator and off-chain code. ## Next steps - [Pyth](/docs/developers/curriculum/dapps/oracles/pyth): wire the recommended oracle into your validator and off-chain code - [Randomness](/docs/developers/curriculum/dapps/oracles/randomness): verifiable random values, the other hard data problem --- ## A Price-Settled Prediction Market The [Pyth integration guide](/docs/developers/curriculum/dapps/oracles/pyth) covers the three-step setup and the individual [validator patterns](/docs/developers/curriculum/dapps/oracles/pyth#validator-patterns). This page assembles them into a full dApp: a binary prediction market, "will BTC be above this price at this time?", where the Pyth feed is the judge. The contract is adapted from the [winning entry](https://github.com/SAIB-Inc/cardano-pyth-prediction-market) of the recent Cardano x Pyth hackathon. Read it end to end to see how an oracle-consuming contract fits together, or jump to the path you need. It assumes the client and wallet set up in [Step 3](/docs/developers/curriculum/dapps/oracles/pyth#step-3-include-the-update-in-a-cardano-transaction) of the integration guide. Everything so far has been pieces. This section assembles them into a full dApp: a binary prediction market, "will BTC be above this price at this time?", where the Pyth feed is the judge. The contract is adapted from the [winning entry](https://github.com/SAIB-Inc/cardano-pyth-prediction-market) of the recent Cardano x Pyth hackathon. The market's lifecycle: ```mermaid graph LR A[Createone-shot mint] --> B[Open] B -->|Bet: mint YES/NObefore deadline| B B -->|Resolve: Pyth readafter deadline| C[Resolved] C -->|Claim: burn winnersfor pro-rata payout| C C -->|last claim burnsthe state thread| D[Closed] ``` ### The market lives in one UTxO The whole market, its question, its pot, its accounting, is a single UTxO carrying an inline datum and a **[state-thread token](https://aiken-lang.org/fundamentals/common-design-patterns#state-thread-tokens-aka-stt)** that proves it is the genuine market and not a look-alike someone paid into the script address: ```aiken use cardano/assets.{PolicyId} use cardano/transaction.{OutputReference} pub type MarketDatum { creator: ByteArray, pyth_id: PolicyId, feed_id: Int, target_price: Int, resolution_time: Int, token_policy: PolicyId, yes_reserve: Int, no_reserve: Int, k: Int, total_yes_minted: Int, total_no_minted: Int, total_ada: Int, resolved: Bool, winning_side: Option, } pub type BetDirection { Yes No } pub type MarketAction { Bet { direction: BetDirection, amount: Int } Resolve Claim { burn_amount: Int } } pub type MintAction { MintTokens BurnTokens } /// Parameter for the market validator (makes each market's policy ID unique) pub type MarketParams { one_shot: OutputReference, } ``` Four groups of fields: - **The question**: `pyth_id`, `feed_id`, `target_price`, `resolution_time`. Which Pyth deployment, which feed, what threshold, by when. `target_price` is stored in raw feed units, exactly as the [settlement pattern](/docs/developers/curriculum/dapps/oracles/pyth#settle-an-outcome-at-a-deadline) above prescribes. - **Position pricing**: `yes_reserve`, `no_reserve`, `k`. A constant-product curve (the same `x * y = k` idea AMMs use) prices YES and NO positions dynamically, so betting on the side the market already favors buys you fewer tokens. - **Accounting**: `total_yes_minted`, `total_no_minted`, `total_ada`. What claims will be paid from and divided by. - **Lifecycle**: `resolved`, `winning_side`. Flipped exactly once, by the oracle. The `one_shot` parameter is an output reference the creating transaction must consume, a [one-shot minting policy](https://aiken-lang.org/fundamentals/common-design-patterns#one-shot-minting-policies). Since no UTxO can be spent twice, each market instantiates a unique script, and therefore a unique policy ID and address. ### One script, three identities The validator is a single Aiken `validator` block with both a spend and a mint handler. That means the same script hash is simultaneously the **spending validator** guarding the market UTxO, the **minting policy** of the YES/NO position tokens, and the **identity** of the state-thread token. This identity trick is the backbone of the design: any check against `market_policy_id` is a check against all three at once. ```aiken use cardano/assets.{PolicyId} use cardano/transaction.{OutputReference, Transaction} use prediction_market/market_validation.{ validate_bet, validate_burn_tokens, validate_claim, validate_mint_tokens, validate_resolve, } use prediction_market/types.{ Bet, BurnTokens, Claim, MarketAction, MarketDatum, MarketParams, MintAction, MintTokens, Resolve, } validator market(params: MarketParams) { spend( datum: Option, redeemer: MarketAction, spend_out_ref: OutputReference, self: Transaction, ) { expect Some(market_datum) = datum when redeemer is { Bet { direction, amount } -> validate_bet(market_datum, direction, amount, spend_out_ref, self) Resolve -> validate_resolve(market_datum, spend_out_ref, self) Claim { burn_amount } -> validate_claim(market_datum, burn_amount, spend_out_ref, self) } } mint(redeemer: MintAction, policy_id: PolicyId, self: Transaction) { when redeemer is { MintTokens -> validate_mint_tokens(params.one_shot, policy_id, self) BurnTokens -> validate_burn_tokens(policy_id, self) } } else(_) { fail } } ``` The logic lives in a library module (`prediction_market/market_validation.ak`, with the types above in `prediction_market/types.ak`). Three token names are fixed: `"YES"`, `"NO"`, and `""` (the empty name) for the state thread: ```aiken use aiken/collection/dict use aiken/collection/list use aiken/interval.{Finite} use cardano/address.{Script} use cardano/assets use cardano/transaction.{InlineDatum, OutputReference, Transaction, find_input} use prediction_market/types.{MarketDatum, No, Yes} use pyth use types/u32 pub const yes_token: ByteArray = "YES" pub const no_token: ByteArray = "NO" pub const state_thread_token: ByteArray = "" fn has_state_thread(value: assets.Value, policy_id: assets.PolicyId) -> Bool { assets.quantity_of(value, policy_id, state_thread_token) == 1 } ```
Mint-policing helpers used below These enforce that a transaction's mint field contains exactly what the action allows, so nothing extra can ride along under the market's policy: ```aiken fn has_no_market_policy_mint( policy_id: assets.PolicyId, tx: Transaction, ) -> Bool { let mint_dict = tx.mint |> assets.tokens(policy_id) dict.foldl(mint_dict, True, fn(_asset_name, _qty, _acc) { False }) } fn only_mints_market_token( policy_id: assets.PolicyId, token_name: ByteArray, amount: Int, tx: Transaction, ) -> Bool { let mint_dict = tx.mint |> assets.tokens(policy_id) assets.quantity_of(tx.mint, policy_id, token_name) == amount && dict.foldl( mint_dict, True, fn(asset_name, qty, acc) { acc && asset_name == token_name && qty == amount }, ) } fn only_burns_market_token( policy_id: assets.PolicyId, token_name: ByteArray, amount: Int, burn_state_thread: Bool, tx: Transaction, ) -> Bool { let mint_dict = tx.mint |> assets.tokens(policy_id) assets.quantity_of(tx.mint, policy_id, token_name) == amount && if burn_state_thread { assets.quantity_of(tx.mint, policy_id, state_thread_token) == -1 } else { assets.quantity_of(tx.mint, policy_id, state_thread_token) == 0 } && dict.foldl( mint_dict, True, fn(asset_name, qty, acc) { acc && if asset_name == token_name { qty == amount } else { burn_state_thread && asset_name == state_thread_token && qty == -1 } }, ) } ```
### Bet A bet spends the market UTxO and recreates it with the pot grown, the reserves shifted, and the bettor's position tokens minted. Note the [mirror rule](/docs/developers/curriculum/dapps/oracles/pyth#settle-an-outcome-at-a-deadline) in action: the validity **upper** bound must sit at or below the deadline. ```aiken pub fn validate_bet( datum: MarketDatum, direction: types.BetDirection, amount: Int, spend_out_ref: OutputReference, tx: Transaction, ) -> Bool { expect !datum.resolved expect amount > 0 // Must be before resolution time expect Finite(upper) = tx.validity_range.upper_bound.bound_type expect upper <= datum.resolution_time // Tokens out via the constant product formula let (tokens_out, new_datum) = when direction is { Yes -> { let tokens = datum.yes_reserve - datum.k / ( datum.no_reserve + amount ) expect tokens > 0 let new = MarketDatum { ..datum, yes_reserve: datum.yes_reserve - tokens, no_reserve: datum.no_reserve + amount, total_yes_minted: datum.total_yes_minted + tokens, total_ada: datum.total_ada + amount, } (tokens, new) } No -> { let tokens = datum.no_reserve - datum.k / ( datum.yes_reserve + amount ) expect tokens > 0 let new = MarketDatum { ..datum, yes_reserve: datum.yes_reserve + amount, no_reserve: datum.no_reserve - tokens, total_no_minted: datum.total_no_minted + tokens, total_ada: datum.total_ada + amount, } (tokens, new) } } // The market must continue: same address, state thread intact, new datum expect Some(spend_input) = find_input(tx.inputs, spend_out_ref) expect Script(market_policy_id) = spend_input.output.address.payment_credential let script_address = spend_input.output.address expect has_state_thread(spend_input.output.value, market_policy_id) expect Some(continuing_output) = tx.outputs |> list.find(fn(output) { output.address == script_address }) expect InlineDatum(out_datum) = continuing_output.datum expect output_market_datum: MarketDatum = out_datum expect output_market_datum == new_datum expect has_state_thread(continuing_output.value, market_policy_id) // Exactly the earned position tokens are minted, nothing else let expected_token_name = when direction is { Yes -> yes_token No -> no_token } expect only_mints_market_token( market_policy_id, expected_token_name, tokens_out, tx, ) // The pot must actually grow by the bet let input_lovelace = assets.lovelace_of(spend_input.output.value) let output_lovelace = assets.lovelace_of(continuing_output.value) expect output_lovelace >= input_lovelace + amount True } ``` The validator recomputes the CPMM math itself and demands the continuing datum match exactly. The off-chain code proposes the new state; the validator verifies it. ### Resolve Resolve is where the oracle comes in, and it is exactly the [settle-at-a-deadline pattern](/docs/developers/curriculum/dapps/oracles/pyth#settle-an-outcome-at-a-deadline): validity lower bound at or past the deadline, one `pyth.get_updates` read, one integer comparison. ```aiken pub fn validate_resolve( datum: MarketDatum, spend_out_ref: OutputReference, tx: Transaction, ) -> Bool { expect !datum.resolved // Must be after resolution time expect Finite(lower) = tx.validity_range.lower_bound.bound_type expect lower >= datum.resolution_time // The verified Pyth price decides the winner expect [update] = pyth.get_updates(datum.pyth_id, tx) expect Some(feed) = list.find(update.feeds, fn(f) { u32.as_int(f.feed_id) == datum.feed_id }) expect Some(Some(oracle_price)) = feed.price let winning_side = if oracle_price > datum.target_price { Yes } else { No } // Creator must sign (hackathon simplification, see below) expect tx.extra_signatories |> list.has(datum.creator) // Continuing output: same value, datum flipped to resolved expect Some(spend_input) = find_input(tx.inputs, spend_out_ref) expect Script(market_policy_id) = spend_input.output.address.payment_credential let script_address = spend_input.output.address expect has_state_thread(spend_input.output.value, market_policy_id) let expected_datum = MarketDatum { ..datum, resolved: True, winning_side: Some(winning_side) } expect Some(continuing_output) = tx.outputs |> list.find(fn(output) { output.address == script_address }) expect InlineDatum(out_datum) = continuing_output.datum expect output_market_datum: MarketDatum = out_datum expect output_market_datum == expected_datum expect has_state_thread(continuing_output.value, market_policy_id) expect has_no_market_policy_mint(market_policy_id, tx) // No ADA may leave during resolve let input_lovelace = assets.lovelace_of(spend_input.output.value) let output_lovelace = assets.lovelace_of(continuing_output.value) expect output_lovelace >= input_lovelace True } ``` :::caution Hackathon simplifications to harden for production Two corners were cut here, both flagged by the original authors. First, resolution requires the **creator's signature**; as [Let anyone settle](/docs/developers/curriculum/dapps/oracles/pyth#settle-an-outcome-at-a-deadline) explains, that reintroduces a liveness dependency. Drop the `extra_signatories` check and let the deadline plus the verified price decide. Second, there is **no freshness check** on the update; add the [`is_fresh`](/docs/developers/curriculum/dapps/oracles/pyth#enforce-a-freshness-window) guard so a resolver cannot settle with an old price that happened to suit them. ::: ### Claim After resolution, winners burn their tokens for a pro-rata share of the pot. The arithmetic is one line: `payout = burn_amount * total_ada / total_winning_minted`. Note the ending: when the last winning token is burned, the state thread burns with it and the market UTxO disappears. The contract cleans up after itself. ```aiken pub fn validate_claim( datum: MarketDatum, burn_amount: Int, spend_out_ref: OutputReference, tx: Transaction, ) -> Bool { expect datum.resolved expect Some(winning_side) = datum.winning_side expect burn_amount > 0 let (winning_token, total_winning_minted) = when winning_side is { Yes -> (yes_token, datum.total_yes_minted) No -> (no_token, datum.total_no_minted) } let payout = burn_amount * datum.total_ada / total_winning_minted expect Some(spend_input) = find_input(tx.inputs, spend_out_ref) expect Script(market_policy_id) = spend_input.output.address.payment_credential let script_address = spend_input.output.address expect has_state_thread(spend_input.output.value, market_policy_id) let expected_datum = when winning_side is { Yes -> MarketDatum { ..datum, total_ada: datum.total_ada - payout, total_yes_minted: datum.total_yes_minted - burn_amount, } No -> MarketDatum { ..datum, total_ada: datum.total_ada - payout, total_no_minted: datum.total_no_minted - burn_amount, } } if total_winning_minted == burn_amount { // Last claimer takes what remains, burns the state thread, market closes expect only_burns_market_token( market_policy_id, winning_token, -burn_amount, True, tx, ) True } else { expect Some(continuing_output) = tx.outputs |> list.find(fn(output) { output.address == script_address }) expect InlineDatum(out_datum) = continuing_output.datum expect output_market_datum: MarketDatum = out_datum expect output_market_datum == expected_datum expect has_state_thread(continuing_output.value, market_policy_id) expect only_burns_market_token( market_policy_id, winning_token, -burn_amount, False, tx, ) // ADA may decrease by at most the payout let input_lovelace = assets.lovelace_of(spend_input.output.value) let output_lovelace = assets.lovelace_of(continuing_output.value) expect output_lovelace >= input_lovelace - payout True } } ``` ### The mint policy The mint handler enforces the lifecycle from the token side. The state thread can only ever mint in the transaction that consumes the one-shot input, which happens once in the market's existence. After that, position tokens mint only alongside a legitimate market spend (whose `validate_bet` polices the amounts), and burns are always allowed since burning your own tokens harms no one: ```aiken pub fn validate_mint_tokens( one_shot: OutputReference, policy_id: assets.PolicyId, tx: Transaction, ) -> Bool { let one_shot_consumed = tx.inputs |> list.any(fn(input) { input.output_reference == one_shot }) let has_market_spend = tx.inputs |> list.any( fn(input) { input.output.address.payment_credential == Script(policy_id) && has_state_thread( input.output.value, policy_id, ) }, ) let minted_state_thread_qty = assets.quantity_of(tx.mint, policy_id, state_thread_token) let mint_dict = tx.mint |> assets.tokens(policy_id) let all_market_mints_are_positive = dict.foldl(mint_dict, True, fn(_asset_name, qty, acc) { acc && qty > 0 }) expect all_market_mints_are_positive if one_shot_consumed { expect minted_state_thread_qty == 1 True } else { expect minted_state_thread_qty == 0 has_market_spend } } pub fn validate_burn_tokens(policy_id: assets.PolicyId, tx: Transaction) -> Bool { let mint_dict = tx.mint |> assets.tokens(policy_id) dict.foldl(mint_dict, True, fn(_key, qty, acc) { acc && qty < 0 }) } ``` ### Off-chain: placing a bet The market UTxO is found by its state-thread token (empty asset name, so its unit is just the policy ID). The off-chain code computes the same CPMM math the validator will recompute, and proposes the continuing state: ```typescript // client and wallet from Step 3 const marketAddress = Address.fromBech32(MARKET_ADDRESS); const [marketUtxo] = await client.getUtxosWithUnit(marketAddress, MARKET_POLICY_ID); const betAmount = 50_000_000n; // 50 ADA on YES // Same formula the validator checks: yes_reserve - k / (no_reserve + amount) const tokensOut = yesReserve - k / (noReserve + betAmount); // The continuing datum: reserves shifted, totals grown, all other fields unchanged const newDatum = Data.constr(0n, [ /* ...same fields, with yes_reserve - tokensOut, no_reserve + betAmount, total_yes_minted + tokensOut, total_ada + betAmount */ ]); let position = Assets.fromLovelace(0n); position = Assets.addByHex(position, MARKET_POLICY_ID, "594553", tokensOut); // "YES" let marketValue = Assets.fromLovelace(potLovelace + betAmount); marketValue = Assets.addByHex(marketValue, MARKET_POLICY_ID, "", 1n); // state thread const tx = await wallet .newTx() .collectFrom({ inputs: [marketUtxo], redeemer: Data.constr(0n, [Data.constr(0n, []), betAmount]), // Bet { Yes, amount } }) .payToAddress({ address: marketAddress, assets: marketValue, datum: new InlineDatum.InlineDatum({ data: newDatum }), }) .mintAssets({ assets: position, redeemer: Data.constr(0n, []) }) // MintTokens .attachScript({ script: marketScript }) .setValidity({ to: resolutionTime }) // upper bound at or below the deadline .build(); await (await tx.sign()).submit(); ``` ### Off-chain: resolving the market This transaction ties the whole page together. It spends the market UTxO with the `Resolve` redeemer and performs the Pyth zero-withdrawal from Step 3 in the same transaction, so when `validate_resolve` calls `pyth.get_updates`, the verified update is right there in the withdrawal redeemer of the transaction being validated: ```typescript getPythScriptHash, getPythState, } from "@pythnetwork/pyth-lazer-cardano-js"; // `update` is the signed payload fetched exactly as in Step 2 const pythState = await getPythState(PYTH_POLICY_ID, client); const pythScript = getPythScriptHash(pythState); // The datum the validator will demand: resolved, winner recorded const resolvedDatum = Data.constr(0n, [ /* ...same fields, with resolved = True and winning_side = Some(Yes | No), computed from the fetched price vs target_price */ ]); const tx = await wallet .newTx() .collectFrom({ inputs: [marketUtxo], redeemer: Data.constr(1n, []), // Resolve }) .readFrom({ referenceInputs: [pythState] }) .withdraw({ amount: 0n, redeemer: [update], stakeCredential: ScriptHash.fromHex(pythScript), }) .payToAddress({ address: marketAddress, assets: marketValue, // unchanged pot + state thread datum: new InlineDatum.InlineDatum({ data: resolvedDatum }), }) .attachScript({ script: marketScript }) .addSigner({ keyHash: creatorKeyHash }) // creator gate, see the caution above .setValidity({ from: resolutionTime, to: resolutionTime + 300_000n }) .build(); await (await tx.sign()).submit(); ``` Two validators run here: the market script (checking the state transition and reading the price through `pyth.get_updates`) and the Pyth withdraw script (verifying the update's signature). The reference input supplies the Pyth state, the withdrawal carries the update bytes, and the validity window proves the deadline has passed. That is the whole integration. :::caution This is a teaching adaptation of a hackathon entry: unaudited, with the simplifications flagged above. Read it to understand how an oracle-consuming dApp fits together, but do not deploy it with real funds as-is. ::: ## Next steps - [Integrate a price feed](/docs/developers/curriculum/dapps/oracles/pyth): the three-step setup and the reusable validator patterns this market composes - [Oracles on Cardano](/docs/developers/curriculum/dapps/oracles/overview): publication models, trust, and how to choose a feed --- ## Integrate a Price Feed: Pyth Feeding a live market price into a validator is a workflow every price-dependent contract shares: fetch a signed price update off-chain, include it in the transaction, and verify it on-chain. This page walks that workflow with Pyth, the [recommended](/docs/developers/curriculum/dapps/oracles/overview#recommended-pyth) production price oracle for Cardano contracts. If oracles are new to you, [Oracles](/docs/developers/curriculum/dapps/oracles/overview) covers the general problem of getting off-chain data on-chain and the pull-based model Cardano contracts use to read it. ## What is Pyth? [Pyth](https://pyth.network) is a high-frequency oracle network that delivers real-time price data across multiple blockchains. [Pyth Pro (Lazer)](https://docs.pyth.network/price-feeds/pro) provides sub-second price feeds using a pull-based model: consumers fetch signed updates off-chain and verify them on-chain. On Cardano, price updates are verified through a **zero-withdrawal** from the Pyth withdraw script. Your validator calls `pyth.get_updates` to read verified updates directly from the transaction being validated. The Pyth script handles signature verification so your contract doesn't have to. ## What Pyth provides **Pyth Pro (Lazer)**: Sub-second, high-frequency price feeds via a pull-based model. You subscribe to a websocket or fetch the latest price and include the signed update in your transaction. **On-chain Aiken Library**: The [`pyth-lazer-cardano`](https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/contracts/cardano) library handles signature verification and exposes parsed price data including price, confidence, EMA price, bid/ask, and exponent. **Off-chain TypeScript SDK**: The [`@pythnetwork/pyth-lazer-sdk`](https://www.npmjs.com/package/@pythnetwork/pyth-lazer-sdk) provides websocket streaming and one-shot fetching of signed price updates. ## Integration guide Integrating Pyth Pro into a Cardano smart contract is a three-step process: ### Step 1: Use the Aiken library on-chain Add the Pyth Lazer Cardano library to your `aiken.toml`: ```toml [[dependencies]] name = "pyth-network/pyth-lazer-cardano" version = "main" source = "github" ``` Your contract reads verified updates from the transaction via `pyth.get_updates`. This function reads the Pyth state from `reference_inputs` and the verified update bytes from the Pyth withdraw script's redeemer. The following example reads the `ADA/USD` feed (Pyth Pro feed ID `16`) and converts the result into Aiken's `Rational` type: ```aiken use aiken/collection/list use aiken/math/rational.{Rational} use cardano/assets.{PolicyId} use cardano/transaction.{Transaction} use pyth use types/u32 fn read_ada_usd_price(pyth_id: PolicyId, self: Transaction) -> Rational { expect [update] = pyth.get_updates(pyth_id, self) expect Some(feed) = list.find(update.feeds, fn(feed) { u32.as_int(feed.feed_id) == 16 }) expect Some(Some(price)) = feed.price expect Some(exponent) = feed.exponent expect Some(multiplier) = rational.from_int(10) |> rational.pow(exponent) rational.from_int(price) |> rational.mul(multiplier) } ``` Each `PriceUpdate` includes `timestamp_us`, `channel_id`, and a list of `feeds`. Each `Feed` includes fields such as `feed_id`, `price`, `best_bid_price`, `best_ask_price`, `exponent`, `confidence`, `ema_price`, and `feed_update_timestamp`. :::warning The Pyth withdraw script verifies signature validity but does **not** enforce freshness. A valid signature proves the price is genuinely Pyth's (integrity); it does not prove the update is recent, or that one was posted at all (liveness). If your contract requires a validity window, enforce it directly by checking the `timestamp_us` field. See [who publishes, and what that guarantees](/docs/developers/curriculum/dapps/oracles/overview#who-publishes-and-what-that-guarantees) for the trust model behind this. ::: :::warning `pyth.get_updates` requires the Pyth state UTxO to be present as a reference input. If you omit it, your validator will fail when it tries to locate the Pyth State NFT and withdraw-script hash. ::: ### Step 2: Fetch signed price updates off-chain Use the TypeScript SDK to fetch a signed update. You need to request the `solana` format, which is the little-endian Ed25519-signed binary format used for both Cardano and Solana integrations. :::tip Getting an access token Fetching updates requires a Pyth Pro access token. Intersect has arranged access for projects building on Cardano; see the [Intersect announcement](https://intersectmbo.org/news/pyth-pro-on-cardano-subscription-offer) for how to request an API key. ::: ```typescript const lazer = await PythLazerClient.create({ token: LAZER_TOKEN }); const latestPrice = await lazer.getLatestPrice({ channel: "fixed_rate@200ms", formats: ["solana"], jsonBinaryEncoding: "hex", priceFeedIds: [16], properties: ["price", "exponent"], }); if (!latestPrice.solana?.data) { throw new Error("Missing update payload"); } const update = Buffer.from(latestPrice.solana.data, "hex"); ``` If you need streaming integration instead of a one-shot fetch, see the [Pyth Pro subscription guide](https://docs.pyth.network/price-feeds/pro/subscribe-to-prices). ### Step 3: Include the update in a Cardano transaction Build a transaction that performs a zero-withdrawal from the Pyth withdraw script, passing the signed update as the redeemer. The `pyth_id` is the Pyth deployment policy ID for your network. ```typescript getPythScriptHash, getPythState, } from "@pythnetwork/pyth-lazer-cardano-js"; const client = Client.make(preprod).withKoios({ baseUrl: "https://preprod.koios.rest/api/v1", }); const pythState = await getPythState(POLICY_ID, client); const pythScript = getPythScriptHash(pythState); const wallet = client.withSeed({ mnemonic: CARDANO_MNEMONIC }); const now = BigInt(Date.now()); const tx = wallet .newTx() .setValidity({ from: now - 60_000n, to: now + 60_000n }) .readFrom({ referenceInputs: [pythState] }) .withdraw({ amount: 0n, redeemer: [update], stakeCredential: ScriptHash.fromHex(pythScript), }); // Add your own scripts and transaction data, then sign and submit: const builtTx = await tx.build(); const digest = await builtTx.signAndSubmit(); ``` :::warning The zero-withdrawal and your consuming validator must be in the **same transaction**. `pyth.get_updates` reads the withdrawal redeemer directly from the transaction being validated. ::: ### Verify the integration Before wiring Pyth into a real contract, deploy a minimal validator that does nothing but read the feed. If this works, the whole pipeline works: the fetch, the zero-withdrawal, the reference input, and `pyth.get_updates`. Any failure after this point is in your own logic, not the integration. ```aiken use aiken/collection/list use cardano/address.{Credential} use cardano/assets.{PolicyId} use cardano/certificate.{Certificate, RegisterCredential} use cardano/transaction.{Transaction} use pyth use types/u32 /// Minimal test validator to verify the Pyth integration works. /// Parameterized with the Pyth deployment policy ID. validator pyth_test(pyth_id: PolicyId) { withdraw(_redeemer: Data, _account: Credential, self: Transaction) { expect [update] = pyth.get_updates(pyth_id, self) // Find BTC/USD (feed ID 1) and assert a price exists expect Some(btc_feed) = list.find(update.feeds, fn(f) { u32.as_int(f.feed_id) == 1 }) expect Some(Some(_price)) = btc_feed.price True } publish(_redeemer: Data, certificate: Certificate, _self: Transaction) { when certificate is { RegisterCredential { .. } -> True _ -> fail } } else(_) { fail } } ``` This test consumer is itself a [withdrawal validator](/docs/developers/curriculum/smart-contracts/write-a-validator#withdrawal-validator), and a withdrawal validator's stake credential must be **registered on-chain before its first use**. That is what the `publish` handler is for: it allows the registration certificate and nothing else. Register the credential once, then run a transaction with two zero-withdrawals, the Pyth one carrying the signed update and this one running the check. ## Validator patterns The steps above get a verified price into your validator. What follows are the recurring shapes for actually using it, written against the current library types. Two unit conventions matter throughout: transaction validity bounds are POSIX **milliseconds**, while `timestamp_us` is **microseconds**. Feed fields are also double-optional: the outer `Option` tells you whether you requested that property in the off-chain fetch (Step 2's `properties` array), the inner whether Pyth has a value for it right now. ### Enforce a freshness window The signature check proves integrity, not recency, so bound the age yourself. Anchor the check to the validity **upper** bound: then no matter when inside its validity window the transaction lands on-chain, the update is at most `max_age_ms` old. ```aiken use aiken/interval.{Finite} use cardano/transaction.{Transaction} use pyth.{PriceUpdate} use types/u64 const max_age_ms: Int = 60_000 fn is_fresh(update: PriceUpdate, self: Transaction) -> Bool { expect Finite(upper) = self.validity_range.upper_bound.bound_type let age_us = upper * 1_000 - u64.as_int(update.timestamp_us) age_us >= 0 && age_us <= max_age_ms * 1_000 } ``` The two-sided check rejects both stale updates and updates timestamped after the validity window, since either one means the transaction was assembled inconsistently. It also forces the transaction to have a finite validity interval: an unbounded transaction fails the `expect`. ### Settle an outcome at a deadline In the settlement shape, the datum stores the question (which feed, what threshold, by when) and the oracle answers it exactly once, after the deadline. The deadline is enforced through the validity interval, so the ledger itself refuses a transaction that tries to settle early. ```aiken use aiken/collection/list use aiken/interval.{Finite} use cardano/assets.{PolicyId} use cardano/transaction.{Transaction} use pyth use types/u32 pub type Terms { pyth_id: PolicyId, feed_id: Int, target_price: Int, deadline: Int, } fn settles_above_target(terms: Terms, self: Transaction) -> Bool { // The transaction cannot be valid before the deadline expect Finite(lower) = self.validity_range.lower_bound.bound_type expect lower >= terms.deadline expect [update] = pyth.get_updates(terms.pyth_id, self) expect Some(feed) = list.find(update.feeds, fn(f) { u32.as_int(f.feed_id) == terms.feed_id }) expect Some(Some(price)) = feed.price price > terms.target_price } ``` Two details make this cheap and safe: - **Store the target in raw feed units.** ADA/USD publishes with exponent `-8`, so a target of $0.45 is stored as `45_000_000`. Comparing two integers avoids rational arithmetic on-chain entirely; the conversion example in Step 1 is only needed when you must combine feeds with different exponents. - **Mirror the bound for the other side of the deadline.** Any action that must happen *before* it, such as placing a bet or adjusting a position, requires the validity **upper** bound at or below `terms.deadline`. Between the two rules, the state machine cannot accept positions after expiry or settle before it, and none of that depends on off-chain code behaving. - **Let anyone settle.** The oracle signature already fixes the outcome, so the resolving transaction needs no privileged signer. Requiring one (say, the market creator) reintroduces exactly the liveness dependency the signature model warns about: settlement then happens only when that party chooses to act. Keep resolution permissionless and let the deadline plus the verified price decide. This is the core of a prediction market, an option expiry, or a parametric insurance payout. The surrounding contract only adds how positions are entered and how the pot is paid out. ### Refuse to act in a dislocated market The bid-ask spread is a live uncertainty measure. A settlement or liquidation that fires during a momentary dislocation is technically correct and practically wrong, so let the contract demand an orderly market: ```aiken fn spread_within(feed: Feed, max_spread: Int) -> Bool { expect Some(Some(bid)) = feed.best_bid_price expect Some(Some(ask)) = feed.best_ask_price ask - bid <= max_spread } ``` The same idea extends to the other payload fields: `ema_price` against `price` gives a momentum signal with no on-chain history, and two feeds fetched in one update give a cross-asset ratio. When you compare a ratio against bounds, cross-multiply instead of dividing (`min_num * price_b <= price_a * min_den`) so everything stays in integers. The design-space view of these options is in [Designing with a price feed](/docs/developers/curriculum/dapps/oracles/overview#designing-with-a-price-feed). The three patterns above compose into a full contract in [A price-settled prediction market](/docs/developers/curriculum/dapps/oracles/prediction-market), a complete dApp walked end to end. ## Network support Pyth deployments are per-network: each network has its own `pyth_id` policy ID, which your validator and off-chain code use to locate the Pyth state and withdraw script. The examples above target preprod. For the deployment on your target network, see the [Pyth documentation](https://docs.pyth.network/price-feeds/pro/integrate-as-consumer/cardano). ## Additional resources - [Pyth Pro Price Feed IDs](https://docs.pyth.network/price-feeds/pro/price-feed-ids): complete list of supported feeds - [Contract sources](https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/contracts/cardano): Aiken contracts and off-chain SDK - [fetch-and-verify.ts](https://github.com/pyth-network/pyth-crosschain/blob/main/lazer/contracts/cardano/sdk/js/src/examples/fetch-and-verify.ts): full off-chain example ## Next steps - [A price-settled prediction market](/docs/developers/curriculum/dapps/oracles/prediction-market): these patterns assembled into a working oracle-consuming dApp - [On-chain randomness](/docs/developers/curriculum/dapps/oracles/randomness): the other hard data problem, where a feed cannot help you --- ## On-chain Randomness Raffles, lotteries, games, and reward draws all need a random number, but there is no `random()` a Cardano validator can call. A [validator sees only the transaction and its context](/docs/developers/curriculum/smart-contracts/overview#smart-contracts-are-validators-not-actors), never the block it lands in, a clock, or a source of entropy. That is a direct consequence of [determinism](/docs/developers/curriculum/smart-contracts/overview#deterministic-validation): every node has to reach the same verdict, so nothing unpredictable is allowed inside validation. So randomness on Cardano is not something you read, it is something you **construct and make verifiable**. This page covers the patterns that actually work, and is honest about what each one protects against. Cardano has no native, trustless, high-quality randomness primitive, so the right choice depends on your trust model and how adversarial your setting is. ## What good randomness has to be Before picking a pattern, know the bar. A random value used to move money has to be: - **Unpredictable before it is fixed.** No participant, and no one watching, can know the outcome while they can still act on it. - **Verifiable after.** Anyone can recompute the value from public data and confirm it was not fabricated. - **Grind-resistant.** Nobody who can influence the inputs (a transaction builder, a block producer, the last person to act) can retry or withhold to nudge the result their way. - **High-entropy enough.** A handful of guessable bits is not a seed. No single Cardano mechanism gives you all four for free. The patterns below trade among them. ## Commit-reveal Commit-reveal is the one pattern a validator can enforce end to end, because everything it needs is in the transaction. It runs in two phases: ```mermaid graph LR COMMIT["Commit phaseeach party posts hash(value + salt)"] --> REVEAL["Reveal phaseeach party posts its value"] REVEAL --> SEED["seed = hash(values sorted + concatenated)"] SEED --> OUT["random output"] style SEED fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style COMMIT fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style REVEAL fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style OUT fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` Each participant first commits a hash of a secret value (plus a salt), so the value is locked in but hidden. Once everyone has committed, they reveal, and the validator checks each revealed value against its commitment and combines them into one seed. Combine by sorting and hashing the revealed values, not by XOR: a naive XOR lets a participant who commits after seeing others' commitments cancel out the result. The weakness is the **last revealer**. Whoever reveals last has already seen every other value, so they can compute the outcome and, if they dislike it, simply not reveal. A group of colluding participants controls one bit of the result per member they are willing to sacrifice. Mitigate it, don't ignore it: - **Slashable deposits.** Each participant locks a deposit that is forfeited if they fail to reveal by the deadline, so withholding costs more than the draw is worth. - **Reveal deadlines.** Use the transaction [validity interval](/docs/developers/curriculum/fundamentals/core-concepts/transactions#validity-intervals-and-time) to bound the reveal window and let the protocol resolve (or refund) if someone drops out. Even then, a determined last revealer can grief the round (force a restart) if not the outcome. Commit-reveal is strongest with a fixed, accountable set of participants who each contribute entropy. ## Randomness from a block's VRF (via an oracle) Every Cardano block already carries a [verifiable random value](/docs/developers/curriculum/fundamentals/cryptographic-primitives#what-are-verifiable-random-functions-vrfs): the VRF output the winning stake pool produced to claim its slot in [Ouroboros leader election](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros). It is public, and anyone can verify it against the pool's key once the block exists. That looks like a ready-made randomness beacon, with one catch that shapes everything: **a validator cannot see it.** The block header, the slot leader, and the VRF output are not in the script context, so the value has to be brought on-chain by an [oracle](/docs/developers/curriculum/dapps/oracles/overview), using the same reference-input and signed-value machinery as a price feed. The trick that makes it unpredictable is timing: pick the VRF of a block that does not exist yet when a user commits, for example the block *after* the commit transaction. At commit time the value is unknown, and afterward anyone can recompute it from public data. Useful, but be blunt about the trust involved: - **You trust the oracle unless the contract re-verifies.** Publishing the VRF value on-chain is not the same as proving it. If the contract only checks the publisher's signature, a faulty or dishonest operator can post whatever value it likes. The result is *verifiable off-chain* (a consumer can recompute it), not *enforced on-chain*. This is the same integrity-versus-liveness distinction the [oracles page](/docs/developers/curriculum/dapps/oracles/overview#who-publishes-and-what-that-guarantees) draws for price feeds. - **Block producers can grind it.** The pool that wins the target slot computes its own VRF value before it publishes the block, so it can choose to withhold the block and try again. Block randomness is grindable; hardening it is a protocol-level concern (the [grinding defense](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros#common-attacks-and-defenses) in Ouroboros, with further work proposed in CIP-0161). If a draw's beneficiary could collude with a block producer, this matters. - **Extraction can bias the result.** Mapping a VRF output to your range carelessly (say, keeping only decimal digits of its string form) skews the distribution. Reduce modulo your range from the full output, and mind modulo bias. Oracle-published VRF is a good fit for public, auditable draws where the priority is that outsiders can check the result, and a poor fit where a well-resourced insider could collude with a block producer. ## A VRF the validator itself verifies The oracle pattern above trusts the publisher unless the contract re-verifies, and re-verifying a *block's* VRF on-chain is not practical. But nothing stops you from running your own VRF: the BLS12-381 builtins let a validator [verify an ECVRF proof directly](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives#verifiable-random-functions), so the proof itself travels in the transaction and the contract enforces its validity, not an operator's signature. The shape: an operator publishes a VRF public key in advance. Each round's input is public and fixed before the draw, a round number or the hash of a commit transaction. The operator computes the output and its proof off-chain and submits both; the validator checks the proof against the registered key and input. Because a VRF has exactly one valid output per key and input, the operator cannot grind alternatives; their only remaining power is **withholding**, refusing to publish a round they dislike. That shifts the trust from integrity (enforced on-chain) to liveness (mitigate with deposits and deadlines, as with commit-reveal). ## On-chain entropy to be wary of Some values sitting in the ledger look random and are not safe to treat as such: - **Block or transaction hashes and validity-range timestamps.** These are chosen or influenced by whoever builds the transaction or produces the block, so they are grindable. Treating a timestamp as randomness is a known footgun, see [time handling](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/time-handling). - **The treasury amount.** Plutus V3 (Conway) can expose the current treasury balance to a validator, but only when the transaction chooses to include that optional field, and only as the current value with no built-in delta. It changes each epoch, yet the change is mostly predictable, only the low digits are hard to guess. It updates once every ~5 days and is public for the whole epoch, so it is low-entropy and already known to anyone acting late in the epoch. At best a weak supplementary seed, never a standalone source. ## Choosing an approach | Your situation | Reasonable approach | |---|---| | A fixed, accountable set of participants who each contribute | Commit-reveal with slashable deposits and a reveal deadline | | A public draw where auditability matters more than stopping a determined insider | Oracle-published block VRF, contract-verified where you can | | An accountable operator is acceptable, but their honesty about the *value* should not be assumed | Operator-run VRF with the proof verified in the validator; deposits and deadlines against withholding | | You only need a weak, non-adversarial nudge | On-chain entropy, with eyes open about its limits | | High value, open participation, and a strong adversary | No fully trustless native primitive exists; combine commit-reveal with deposits or a verifiable oracle, and design explicitly against grinding and withholding | The honest bottom line: match the pattern to your threat model, and state the trust assumption out loud. If your "random" draw quietly depends on an operator being honest or a block producer not colluding, your users deserve to know that is the assumption. ## Key takeaways - **Validators cannot generate randomness.** Determinism forbids it, so verifiable randomness is constructed, not read. - **Commit-reveal is the only fully on-chain-enforceable pattern**, and its Achilles heel is the last revealer; deposits and deadlines are not optional. - **Block VRF is verifiable but not visible to a validator**, so it arrives through an oracle you must either trust or re-verify, and it is grindable by block producers. - **An operator-run VRF closes the integrity gap**: the validator verifies the proof itself via the BLS12-381 builtins, leaving only withholding to defend against. - **No native primitive is unpredictable, verifiable, grind-resistant, and high-entropy all at once.** Choose against your adversary, not against the happy path. ## Next steps - [AI agents on Cardano](/docs/developers/curriculum/dapps/ai-agents/overview): the next track, agents that hold wallets and act on-chain - [BLS signatures, VRFs & credentials](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives#verifiable-random-functions): the mechanics of an ECVRF a validator can verify - [Verifiable Random Functions](/docs/developers/curriculum/fundamentals/cryptographic-primitives#what-are-verifiable-random-functions-vrfs): what a VRF is and why its output is verifiable - [Time handling](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/time-handling): why a timestamp is not a source of randomness --- ## Build a dApp You arrive from [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview) able to write and test validators. This module is about meeting users where they are: connecting Cardano to web apps, services, and protocols. It runs as two arcs. The first puts an application in front of users; the second builds the protocols underneath. Two side-tracks branch off, and you can skip both without losing the thread. ## Connect users The path most applications follow, in order: - **[Your first dApp](/docs/developers/curriculum/dapps/your-first-dapp)**: assemble a working app end to end from a runnable template, connect a wallet, read a balance, send ADA. - **[Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet)**: the CIP-30 connector in depth, including the frontend-signs, backend-submits split every production dApp needs. - **[Wallet authentication](/docs/developers/curriculum/dapps/wallet-authentication)**: passwordless sign-in by proving wallet ownership with a signed message. - **[Listen for payments](/docs/developers/curriculum/dapps/listen-for-payments)**: the receiving side, detecting and confirming ADA arriving at an address. - **[Sponsored transactions](/docs/developers/curriculum/dapps/sponsored-transactions)**: multi-party transactions where someone other than the user covers the fee. For the transactions behind these flows, see [your first transaction](/docs/developers/curriculum/start-building/your-first-transaction) and [lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend). ## Build protocols What runs underneath an application, where the eUTXO model shapes the design: - **[DeFi on Cardano](/docs/developers/curriculum/dapps/defi)**: DEXes, AMMs, liquidity pools, lending, and the eUTXO-specific answers to concurrency (order batching, pool sharding, transaction chaining). - **[Oracles on Cardano](/docs/developers/curriculum/dapps/oracles/overview)**: how off-chain data gets on-chain, the push and pull models, and what each trust choice buys you. - **[Integrate a price feed](/docs/developers/curriculum/dapps/oracles/pyth)**: the practice, a working Pyth integration in three steps plus the validator patterns that use it. - **[A price-settled prediction market](/docs/developers/curriculum/dapps/oracles/prediction-market)**: those patterns assembled into one complete oracle-consuming dApp, walked end to end. - **[On-chain randomness](/docs/developers/curriculum/dapps/oracles/randomness)**: why a validator cannot generate a random number, and the constructions that work anyway. ## Side-track: AI agents An agent that holds a wallet and acts without a human is the same building blocks driven by different logic, plus the infrastructure an agent economy needs. - **[AI agents on Cardano](/docs/developers/curriculum/dapps/ai-agents/overview)**: what an autonomous on-chain agent requires. - **[Agent economy (Masumi)](/docs/developers/curriculum/dapps/ai-agents/masumi)**: identity, escrowed payments, and discovery as a protocol. - **[MCP access](/docs/developers/curriculum/dapps/ai-agents/mcp)**: giving an AI assistant Cardano tools, and where the signing boundary stays. ## Side-track: Internet of Things - **[IoT on Cardano](/docs/developers/curriculum/dapps/iot/)**: hands-on workshops that read and write the chain from microcontrollers, from fetching a wallet balance onto a display to minting sensor data on-chain, plus hardware references and troubleshooting. ## Exchanges and custodial services Integrating at a lower level than a dApp (accounting, address management, transaction handling) is its own guide: [Exchange integrations](/docs/developers/exchange-integrations). The components such an integration builds on are listed in [Cardano components](/docs/developers/curriculum/fundamentals/cardano-components). ## Next steps - New to dApps? Start with [Your first dApp](/docs/developers/curriculum/dapps/your-first-dapp), a working app assembled end to end. - Building a protocol? Read [DeFi on Cardano](/docs/developers/curriculum/dapps/defi), then [Oracles](/docs/developers/curriculum/dapps/oracles/overview). - Ready to launch? [Ship to Production](/docs/developers/curriculum/production/overview) takes the app from testnet to mainnet, and scales it. --- ## Sponsored and multi-party transactions Most transactions are built, signed, and paid for by one wallet. But those are three separable roles, and pulling them apart unlocks two patterns that matter for real applications: letting your **server build and co-sign** a transaction the user only adds their signature to, and **sponsoring fees** so a user can transact before they hold any ADA. The mechanism underneath both is the same. A Cardano transaction is built once, then carries a set of **witnesses** (signatures); the ledger accepts it once every required witness is present. [CIP-30](https://cips.cardano.org/cip/CIP-0030) **partial signing** is what makes this practical in a browser: a wallet can add just its own signature to a transaction it did not build, without finalizing it. So one party can construct a transaction and another can authorize it, in any combination. ## Co-signed (multi-party) transactions The canonical case is a service where the **user pays but your application is in control of something**. An NFT minting service is the clearest example: the user provides the inputs that cover the cost, but the minting policy belongs to your app, so the transaction needs both signatures. You never hand the user your policy key, and the user never hands you the right to move their funds; you each sign the same transaction. The flow is always the same four steps: ```mermaid graph LR A[User walletprovides UTxOs] --> B[Server buildsthe transaction] B --> C[User partiallysigns in browser] C --> D[Server adds itssignature, submits] style B fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style D fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` The server builds the transaction with the user's UTXOs as inputs (so the user pays) and the app's own action (here, a mint under the app's policy). The unsigned transaction goes to the browser, the user's wallet **partial-signs** it, and the result comes back to the server, which adds its own signature and submits. ```typescript // appClient: backend wallet that owns the policy. userClient: the browser CIP-30 wallet. declare const userUtxos: UTxO.UTxO[] // selected by the browser, sent to the server declare const appPolicy: any // app's native signature policy (requires the app's signature) declare const mintedNft: any declare const userAddress: any // 1. SERVER builds: the user's inputs pay, the app's policy mints const tx = await appClient .newTx() .collectFrom({ inputs: userUtxos }) .mintAssets({ assets: mintedNft }) .attachScript({ script: appPolicy }) .payToAddress({ address: userAddress, assets: mintedNft }) .build() const unsignedCbor = Transaction.toCBORHex(await tx.toTransaction()) // 2. USER partial-signs in the browser (CIP-30): returns a witness, not a final tx const userWitness = await userClient.signTx(unsignedCbor) // 3. SERVER adds the app wallet's witness, assembles both, and submits const appWitness = await tx.partialSign() const submit = await tx.assemble([appWitness, userWitness]) const txHash = await submit.submit() ``` ```typescript // appWallet: backend wallet that owns the policy. wallet: the browser CIP-30 wallet. // userUtxo, policyId, tokenNameHex, forgingScript, userAddress come from the request / app config. // 1. SERVER builds: the user's input pays, the app's policy mints const unsignedTx = await new MeshTxBuilder({ fetcher: provider }) .txIn(userUtxo.input.txHash, userUtxo.input.outputIndex, userUtxo.output.amount, userUtxo.output.address) .mint("1", policyId, tokenNameHex) .mintingScript(forgingScript) .txOut(userAddress, [{ unit: policyId + tokenNameHex, quantity: "1" }]) .changeAddress(userAddress) .complete() // 2. USER partial-signs in the browser (the `true` means partial) const userSignedTx = await wallet.signTx(unsignedTx, true) // 3. SERVER partial-signs with the app wallet and submits const fullySignedTx = await appWallet.signTx(userSignedTx, true) const txHash = await appWallet.submitTx(fullySignedTx) ``` The same shape covers any shared-control transaction: a 2-of-3 treasury where each signer adds a witness in turn, an escrow that needs both buyer and arbiter, or a backend that countersigns to enforce a business rule. For the on-chain side of native-script multisig, see [Write a validator](/docs/developers/curriculum/smart-contracts/write-a-validator#native-scripts-multisig-and-time-locks-without-plutus). ## Fee sponsorship Sponsorship is the same mechanism with one change: the **inputs that cover the fee come from a sponsor, not the user**. Because a transaction must [balance exactly](/docs/developers/curriculum/fundamentals/core-concepts/transactions#the-balancing-equation), inputs equal outputs plus fee, whoever adds the extra input and takes the change back is the one who pays. A new user who holds zero ADA can still act: your sponsor wallet supplies the fee (and, for a script transaction, the [collateral](/docs/developers/curriculum/fundamentals/core-concepts/fees#collateral)), while the user only signs for the part that genuinely needs their key, authorizing a required-signer check, say, or spending a token they already hold. Both parties partial-sign; the sponsor's wallet provides the fee inputs and the change address. This removes the hardest step in onboarding, "go buy ADA before you can do anything," and is why sponsorship usually pairs with a wallet the user did not have to install. Hosted wallet services take it further: they create a non-custodial wallet through social login and sponsor the user's first transactions, so there is no extension and no seed phrase to start. See the note on hosted sign-in and sponsorship in [Wallet authentication](/docs/developers/curriculum/dapps/wallet-authentication#hosted-sign-in-as-a-service). :::note "Gasless" means someone else pays, not a different fee token Developers coming from other chains call this a **gasless** transaction or a **meta-transaction**. Cardano has no gas: fees are always paid in ADA and are [fixed by the transaction's size and script cost](/docs/developers/curriculum/fundamentals/core-concepts/fees#the-fee-formula), so "gasless" here means only that a third party supplies that ADA. It works with the partial signing shown above, no special protocol feature required. Paying a fee in a **native token** instead of ADA is a separate idea (**Babel fees**) that depends on ledger changes still in development, so do not design around it yet. ::: ## Security Co-signing means the user authorizes a transaction your server built, so the trust rules are strict: - **Never modify a transaction after the user signs it.** Any change invalidates their witness, and a flow that silently rebuilds is indistinguishable from an attempt to get the user to sign something they didn't see. Build the complete transaction, then collect signatures. - **The user must be able to inspect what they sign.** Show the amounts, recipients, and assets before prompting. A wallet displays the transaction, but your UI sets the expectation. - **Protect the server key.** The application wallet's mnemonic stays server-side and never reaches the client; treat a leak as a full compromise of whatever that key controls. - **Validate the user's inputs** before building, and assume their UTXOs may be spent by the time you submit. Show a clear retry rather than a cryptic failure. - **Gate and rate-limit a sponsorship endpoint.** A sponsor service spends your own ADA on every request. Without a whitelist, a token-holding requirement, authentication, or rate limiting, anyone can drain it by spamming requests, so decide who qualifies and cap how often. - **Confirm you only pay the fee.** Before the sponsor signs, check that its net contribution equals the transaction fee exactly and that any of its own tokens return to it, so a crafted transaction cannot make the sponsor overpay or leak its assets. ## Next steps - [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet): get the user's address and UTXOs in the browser - [Wallet authentication](/docs/developers/curriculum/dapps/wallet-authentication): prove ownership without a transaction, and hosted sign-in with sponsored fees - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): the contract interactions a co-signed transaction often wraps --- ## Authenticating users with their Cardano wallet Wallet-based authentication lets users prove they own a Cardano wallet by cryptographically signing a message. This is passwordless authentication backed by blockchain identity, more secure than password-based systems and with nothing for you to store but a public address. It is distinct from [connecting a wallet](/docs/developers/curriculum/dapps/connect-a-wallet): connecting reads the user's addresses and UTXOs and lets them sign transactions, whereas authentication only has them sign a nonce to prove ownership, with no transaction and nothing transferred. ## How it works The authentication process uses message signing as described in [CIP-8](https://cips.cardano.org/cip/CIP-0008) with [CIP-30](https://cips.cardano.org/cip/CIP-0030)-compatible wallets: 1. **User connects wallet** - the application requests access to the user's wallet 2. **Backend generates nonce** - a unique random string is created for this authentication attempt 3. **User signs nonce** - the wallet prompts the user to sign the nonce with their private key 4. **Backend verifies signature** - the signature is cryptographically verified to prove wallet ownership ```mermaid graph LR A[User] --> B[Connect Wallet] B --> C[Request Nonce] C --> D[BackendGenerates Nonce] D --> E[Sign Noncewith Private Key] E --> F[Submit Signature+ Stake Address] F --> G[Backend VerifiesSignature] G --> H[AuthenticationComplete] style D fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff style G fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#fff ``` The **nonce** ("number used once") is a unique random string the backend generates for each attempt. It prevents replay attacks: because the user signs that specific nonce with their private key, an old signature cannot be reused. Only the private key holder can produce a valid signature, and any tampering with the message invalidates it. CIP-8 wraps that signature in the COSE (CBOR Object Signing and Encryption) format, which CIP-30 wallets and the SDKs all produce and verify. Use the **staking address** (reward address) as the user's identifier. Unlike payment addresses, which change frequently, the staking address stays constant for a wallet, so you can track users across sessions reliably. It can be derived from any payment address in the wallet. :::warning Never accept the same nonce twice. After each verification attempt, rotate the nonce to maintain security. ::: ## Implement it yourself In the browser, the user's CIP-30 wallet signs the backend-issued nonce with `signData` after you [connect the wallet](/docs/developers/curriculum/dapps/connect-a-wallet); your backend then verifies it. Both SDKs implement CIP-8 message signing, so pick whichever your stack already uses. ```typescript declare const privateKey: PrivateKey.PrivateKey declare const myAddress: Address.Address // Sign a payload (e.g. the nonce) when you hold the key, such as a backend-held // wallet or in tests; in a dApp the user's CIP-30 wallet does this step. const payload = COSE.Utils.fromText("login-nonce-abc123") const signedMessage = COSE.SignData.signData(Address.toHex(myAddress), payload, privateKey) ``` ```typescript declare const expectedAddress: Address.Address declare const expectedKeyHash: KeyHash.KeyHash declare const signedMessage: COSE.SignData.SignedMessage // Backend: verify the signature against the nonce and the expected signer const payload = COSE.Utils.fromText("login-nonce-abc123") const isValid = COSE.SignData.verifyData( Address.toHex(expectedAddress), KeyHash.toHex(expectedKeyHash), payload, signedMessage ) ``` Verification confirms the payload matches, the signer address and key hash are as expected, and the Ed25519 signature is valid. `COSE.Utils` converts payloads to and from text and hex (`fromText`/`toText`/`fromHex`/`toHex`), and the SDK exposes the low-level `COSE.Sign1` / `COSE.Key` / `COSE.Header` structures for advanced use. Mesh wraps the same flow in high-level helpers. Install it: ```bash npm install @meshsdk/core @meshsdk/react ``` On the client, get the staking address, request a nonce, sign it, and send the signature back: ```tsx const { wallet } = useWallet(); const userAddress = (await wallet.getUsedAddresses())[0]; const nonce = await backendGetNonce(userAddress); // your REST call const signature = await wallet.signData(nonce, userAddress); await backendVerifySignature(userAddress, signature); // your REST call ``` On the backend, issue a nonce with `generateNonce`, then verify with `checkSignature`: ```ts function backendGetNonce(userAddress) { const nonce = generateNonce("Sign in to our app: "); // store the nonce against userAddress, then return it return nonce; } function backendVerifySignature(userAddress, signature) { // load the stored nonce for userAddress const ok = checkSignature(nonce, signature, userAddress); // rotate the nonce, then issue a session or JWT if ok } ``` Mesh's `` React component gives you a ready-made connect-and-sign button. ## Hosted sign-in as a service Not every user has a browser wallet installed. [UTXOS](https://utxos.dev) offers sign-in as a hosted service: users create a non-custodial wallet through social login, so onboarding needs no extension or seed phrase. Keys are split with Shamir's Secret Sharing and reconstructed only on the user's device at signing time, so neither UTXOS nor your app can access them. The same platform can also sponsor transaction fees, letting users transact before they hold any ADA. See the [UTXOS documentation](https://docs.utxos.dev) to integrate it. ## Zero-knowledge login :::info In active development [zkLogin for Cardano](https://github.com/eryxcoop/zklogin-aiken) lets users authenticate with an existing account (such as Google) and control funds through zero-knowledge proofs, without exposing their identity on-chain. A Circom circuit verifies the provider's signed identity token and binds it to an **ephemeral session key**, so the Aiken validator checks one proof and the session key signs transactions from there; a user-chosen salt keeps the web identity unlinkable to the on-chain address. It runs on the preprod testnet as an unaudited proof of concept, with known limitations (proof generation runs on a backend, and there is no oracle yet for rotating the identity provider's public keys). Track progress at the [zklogin-aiken repository](https://github.com/eryxcoop/zklogin-aiken), and see [Zero-knowledge proofs](/docs/developers/curriculum/smart-contracts/advanced/zero-knowledge) for how on-chain verification works. ::: ## Use cases Wallet-based authentication fits many scenarios: passwordless login where wallet ownership is the identity, whitelist verification (confirming a user controls a specific wallet or stake address), token-gated content (access for holders of a given native token or NFT), authenticating reward claims, and verifying approval for off-chain actions like in-game trading. ## Next steps - [Listen for payments](/docs/developers/curriculum/dapps/listen-for-payments): the receiving side, detecting ADA arriving at an address - [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet): the CIP-30 connector this sign-in flow builds on --- ## Build your first dApp You have met the pieces separately: [wallets](/docs/developers/curriculum/dapps/connect-a-wallet), [transactions](/docs/developers/curriculum/start-building/your-first-transaction), and [providers](/docs/developers/curriculum/production/connecting-to-the-chain). A dApp is those pieces assembled into one running application. This page builds the smallest complete one, connect a wallet, show its balance, send ADA, and points you at a runnable template for each SDK so you start from working code, not a blank directory. ## What a dApp is made of Whatever framework or SDK you use, a browser dApp is the same handful of building blocks: ```mermaid flowchart LR UI["Frontend(your UI framework)"] --> Conn["Wallet connector(CIP-30)"] Conn --> Build["Transaction builder(build, sign, submit)"] Build --> Prov["Provider(reads + submission)"] Prov --> Chain["Cardano"] style UI fill:#0033AD,stroke:#0033AD,color:#fff style Chain fill:#0033AD,stroke:#0033AD,color:#fff ``` - **A frontend**, your UI framework (React here), which renders the app and holds state. - **A wallet connector** ([CIP-30](https://cips.cardano.org/cip/CIP-0030)): how the user links their wallet and authorizes actions. This is the one piece every dApp needs and the standard every wallet implements. - **A provider**: reads chain data (balances, UTXOs, parameters) and submits transactions. - **A transaction builder**: assembles a transaction; the wallet signs it; the provider or wallet submits it. On-chain logic (a validator) is an optional fifth block you layer on later. The minimal dApp below uses only the first four. ## Start from a template Setting up a dApp means wiring together an off-chain library, a frontend, a provider, and often an on-chain language and a local devnet. A template does that wiring for you, so you start from a runnable project instead of a blank directory. Each SDK has a runnable starter, browsable in the [templates gallery](/templates): [Evolution + Vite + React](/templates/evolution-vite-react) and [Mesh + Next.js](/templates/mesh-nextjs). Both do the same thing, connect a wallet, show the balance, and send ADA. Scaffold one with [giget](https://github.com/unjs/giget) (it copies a single template folder into a new project), then install and run. Both need a free [Blockfrost](https://blockfrost.io) key in the env file. ```bash npx giget@latest gh:cardano-foundation/developer-portal/examples/templates/evolution-vite-react my-app cd my-app npm install cp .env.example .env # set VITE_BLOCKFROST_PROJECT_ID and VITE_NETWORK npm run dev # http://localhost:5173 ``` [Browse the template on GitHub](https://github.com/cardano-foundation/developer-portal/tree/staging/examples/templates/evolution-vite-react). It is Vite + React. Evolution ships no wallet UI, so the template pairs it with the framework-agnostic [`@cardano-foundation/cardano-connect-with-wallet`](https://github.com/cardano-foundation/cardano-connect-with-wallet) for the connect button. ```bash npx giget@latest gh:cardano-foundation/developer-portal/examples/templates/mesh-nextjs my-app cd my-app npm install cp .env.example .env # set NEXT_PUBLIC_BLOCKFROST_API_KEY npm run dev # http://localhost:3000 ``` [Browse the template on GitHub](https://github.com/cardano-foundation/developer-portal/tree/staging/examples/templates/mesh-nextjs). It is Next.js. Mesh ships React components and hooks, so the connect button and wallet state come built in. If you want a contract from the start, the [Mesh Aiken template](https://github.com/MeshJS/mesh-aiken-template) is a full-stack starter pairing the Mesh SDK off-chain with Aiken on-chain. :::info cardano-init is on the way [cardano-init](https://github.com/input-output-hk/cardano-init) aims to unify scaffolding into one tool: you pick the tools for each role (on-chain validators, off-chain transaction building, local devnet, infrastructure, or formal methods) and it generates a runnable monorepo with everything pre-wired, plus an end-to-end example that builds and passes tests. It is an early prototype and not yet ready for use, with its templates, CLI flags, and output still changing. Track progress at the [cardano-init repository](https://github.com/input-output-hk/cardano-init). ::: The rest of this page walks the building blocks the template wires together. ## Connect a wallet The connector finds the CIP-30 wallets installed in the browser, the user picks one and grants access, and you get a wallet handle to read state and request signatures. The two SDKs differ here in the obvious way: Mesh ships a UI component and hooks; Evolution leaves the UI to a connector library. Both speak CIP-30 underneath, so the connector is pluggable. ```tsx const { isConnected, enabledWallet, connect, disconnect, installedExtensions } = useCardano() // render a button per installedExtensions entry; connect(name) opens the wallet ``` ```tsx const { connected, wallet } = useWallet() // renders the connect button and wallet picker for you ``` The connection mechanics (discovery, enabling, the frontend-signs rule) are covered in [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet). ## Read the balance Once connected, read the wallet's balance from its state. ```tsx const { accountBalance } = useCardano() // ADA balance of the connected wallet ``` ```tsx const lovelace = useLovelace() // string of lovelace; divide by 1_000_000 for ADA ``` ## Send a payment The climax: build a transfer to a recipient, have the wallet sign it, and submit. This is the [requesting a payment](/docs/developers/curriculum/dapps/listen-for-payments#requesting-a-payment) flow, assembled into the app. ```tsx const api = await window.cardano[enabledWallet].enable() const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: import.meta.env.VITE_BLOCKFROST_PROJECT_ID }) .withCip30(api) const tx = await client .newTx() .payToAddress({ address: Address.fromBech32(recipient), assets: Assets.fromLovelace(amount) }) .build() const txHash = await (await tx.sign()).submit() ``` ```tsx const provider = new BlockfrostProvider(process.env.NEXT_PUBLIC_BLOCKFROST_API_KEY!) const unsignedTx = await new MeshTxBuilder({ fetcher: provider, submitter: provider }) .txOut(recipient, [{ unit: "lovelace", quantity: lovelaceAmount }]) .changeAddress(await wallet.getChangeAddress()) .selectUtxosFrom(await wallet.getUtxos()) .complete() const txHash = await wallet.submitTx(await wallet.signTx(unsignedTx)) ``` Each template wraps this in a form with input handling and error states; see `src/components/TransactionBuilder.tsx` (Evolution) or `src/pages/index.tsx` (Mesh). ## Run it Start the dev server, open the app, connect a wallet on a testnet, and send some test ADA from the [faucet](/docs/developers/curriculum/start-building/networks-and-test-ada#get-test-ada). You have a working dApp: it reads the chain, builds a transaction, and submits one through a real wallet. The templates ship the bundler configuration each SDK needs. If you build a Mesh app from scratch instead, see [Building for the browser](/docs/developers/curriculum/dapps/connect-a-wallet#building-for-the-browser) for the polyfill and `libsodium` setup that a production build requires. ## Next steps - **On-chain logic.** Lock funds at a validator and spend them: [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend). - **Detect payments.** The receiver side of the loop: [Listen for payments](/docs/developers/curriculum/dapps/listen-for-payments). - **Toward autonomous dApps.** These same building blocks, wallet, provider, transaction builder, are what an [autonomous agent](/docs/developers/curriculum/dapps/ai-agents/overview) drives when it holds a wallet and acts without a human in the loop. The agent economy adds identity, discovery, and payments on top ([Masumi](/docs/developers/curriculum/dapps/ai-agents/masumi); machine-payment rails like x402 are emerging). A clean, composable dApp is the foundation an agent builds on. ## Key takeaways - A dApp is four building blocks: a frontend, a CIP-30 wallet connector, a provider, and a transaction builder. On-chain logic is an optional fifth. - The blocks are SDK-agnostic; what differs is ergonomics. Mesh ships React components and hooks; Evolution is framework-agnostic and pairs with a connector library. - Start from a runnable template, then replace its single payment with your own logic. --- ## Cardano Architecture Cardano is a layered, formally specified blockchain. Its architecture is **four layers**, each with one responsibility and a boundary defined in a mathematical specification before any code is written: the **ledger** (the rules), **consensus** (agreeing which block comes next), **networking** (moving blocks and transactions between nodes), and **scripting** (on-chain computation). Those layers are the architecture, defined independently of how anyone builds them, so every Cardano node shares the same four. The reference implementation almost everyone runs is `cardano-node`, and this page uses it to make each layer concrete. ## The four layers ```mermaid flowchart TD APP["Applicationsyour dApp, wallets, explorers"] APP -->|"CLIs, SDKs, and APIs"| LEDGER subgraph NODE["A Cardano node (cardano-node)"] direction TB LEDGER["Ledger · the rulesUTXOs, scripts, protocol parameters, governance"] CONSENSUS["Consensus · Ouroboroswhich block comes next"] NETWORK["Networkingpropagating blocks and transactions"] SCRIPT["Scriptingon-chain computation (Plutus Core)"] end ``` [`cardano-node`](https://github.com/IntersectMBO/cardano-node) (Haskell) bundles the four layers into one process: it keeps a copy of the chain, validates blocks and transactions, takes part in consensus, and talks to other nodes. Relays, block producers, and full-node wallets all run it. You don't drive these layers directly; you reach the chain through CLIs, SDKs, and APIs (see [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain) for the developer stack). Each layer is specified and implemented as its own package: ### Ledger layer The ledger is the rules of the blockchain: what a valid transaction looks like, how UTXOs are created and consumed, how protocol parameters change, how governance actions are ratified. It is derived directly from formal specifications written in a mathematical notation and machine-checked for correctness (reference implementation: [`cardano-ledger`](https://github.com/IntersectMBO/cardano-ledger)). The ledger does not know about the network or consensus, it is purely a set of state transition rules. Given a current ledger state and a block, it either accepts the block and produces a new state, or rejects it with a specific rule violation. ### Consensus layer The consensus layer runs the Ouroboros family of proof-of-stake protocols. It decides which chain a node considers valid when competing chains exist, handles chain selection under forks, and manages the Hard Fork Combinator, the mechanism that lets Cardano transition between protocol eras without a disruptive network split (reference implementation: [`ouroboros-consensus`](https://github.com/IntersectMBO/ouroboros-consensus)). The consensus layer sits between the network and the ledger: it receives block candidates from peers, asks the ledger to validate them, and uses the Ouroboros rules to decide which chain to follow. ### Networking layer The networking layer is a typed, multiplexed peer-to-peer stack purpose-built for proof-of-stake blockchains (reference implementation: [`ouroboros-network`](https://github.com/IntersectMBO/ouroboros-network)). It handles: - **Peer discovery and selection**, finding and maintaining connections to peers via P2P topology negotiation - **Mini-protocols**, typed request/response protocols for chain sync, block fetch, transaction submission, and local queries - **Pipelining**, requesting multiple blocks ahead of confirmation to maximize throughput - **Adversarial resistance**, protections against peers that are slow, malicious, or eclipse-attacking The networking layer handles peer topology and connection management. Both relays and block producers run the same networking code; what distinguishes them is configuration, a relay accepts external connections from any peer, while a block producer's topology is configured to connect only to its own relays (see [Network topology](#network-topology) below). The wire protocol itself is not reserved for nodes: any client can speak it, see [the network protocol beneath the APIs](/docs/developers/curriculum/production/network-protocol). ### Scripting layer The scripting layer is the smart-contract execution engine embedded in the ledger. At its core it is a lambda calculus, a minimal formally-verified computation model. Compilers work through a typed form, Typed Plutus Core, but the language the ledger executes is untyped: smart contracts compiled from Aiken, Plinth, Plutarch, or any other high-level language ultimately become [Untyped Plutus Core (UPLC)](/docs/developers/curriculum/smart-contracts/advanced/uplc) for on-chain execution (reference implementation: [Plutus Core](https://github.com/IntersectMBO/plutus)). Execution happens within the ledger layer during transaction validation. Every script execution is bounded by an execution unit budget (CPU steps and memory units) that must be declared in the transaction. The declared budget is consumed during validation; both per-transaction and per-block execution unit limits are enforced by the protocol parameters, preventing unbounded computation. ## Tooling around the node [`cardano-cli`](https://github.com/IntersectMBO/cardano-cli) is the command-line interface to a running node. It connects over a local socket to build, sign, and submit transactions, query chain state (UTXOs, protocol parameters, governance state), and manage keys and certificates. It is not a daemon; it runs a command against the node and exits. A few other components sit *around* the node rather than inside it: **cardano-tracer** collects the node's logs and Prometheus metrics, **[Mithril](/docs/operators/operator-tools/mithril)** lets a fresh node bootstrap to the chain tip in minutes from a stake-certified snapshot, and **cardano-db-sync** indexes the whole chain into PostgreSQL for rich SQL queries, one of several [indexer shapes](/docs/developers/curriculum/production/indexing-and-analytics). These are operational and indexing concerns, documented where you would actually reach for them. ## Network topology The network is made of two kinds of node. **Relays** are public-facing: they accept connections from any peer and propagate blocks and transactions. **Block producers** forge new blocks and stay isolated behind their own relays, never exposed directly to the network. P2P topology is negotiated automatically, so nodes discover and maintain peers without hand-maintained lists. Running this topology, hardening a block producer, and managing its keys are operator concerns. See [Network topology](/docs/operators/node/topology) in the operator curriculum for the configuration detail. ## Ouroboros consensus The consensus layer runs **Ouroboros Praos**, Cardano's proof-of-stake protocol: time is divided into slots and epochs, stake-weighted slot leaders are chosen privately by a VRF, and nodes follow the longest valid chain. [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros) covers the protocol in full, including slot-leader election, chain selection, finality, and the forward-secure KES keys that block producers sign with. ## Cardano eras Cardano has evolved through multiple ledger eras, each introducing new capabilities via a hard fork: | Era | Key addition | |-----|-------------| | Byron | Federated launch (Ouroboros BFT) | | Shelley | Decentralized block production, staking | | Allegra | Token locking | | Mary | Native tokens and NFTs | | Alonzo | Plutus smart contracts | | Babbage | Reference inputs, inline datums, reference scripts | | Conway | On-chain governance (CIP-1694), DReps, Constitutional Committee | For the full history of these era transitions, see [Historical Cardano Hardforks](https://cardano.org/hardforks/). Since the Conway Era each era transition is triggered by a hard fork initiation governance action, a process that requires SPO, DRep, and Constitutional Committee votes to ratify. The Hard Fork Combinator in the consensus layer handles the transition transparently, without requiring a separate node binary per era. ## Formal specifications What distinguishes Cardano's engineering approach is that each layer is specified formally before implementation. The ledger rules are defined in a mathematical notation (Agda and LaTeX), and the consensus protocol has a formal proof of security. This means: - Rule changes are proposed as spec changes first, then implemented - The implementation can be checked against the spec for conformance - Security properties are proved, not just tested The formal specs are public: - [Cardano Ledger Specifications](https://github.com/IntersectMBO/cardano-ledger#cardano-ledger) - [Ouroboros papers](https://cardano.org/research/), the academic papers underpinning the consensus protocol - [Cardano Blueprint](https://cardano-scaling.github.io/cardano-blueprint/), implementation-independent descriptions of each layer, written so alternative nodes can be built from them; the readable companion to the formal specs ## Further reading - [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros): how the consensus layer chooses blocks, in depth - [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo): how the ledger layer tracks ownership - [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain): the developer stack you reach the chain through - [Self-hosting](/docs/developers/curriculum/production/self-hosting): install and run cardano-node yourself --- ## Consensus & Ouroboros A consensus mechanism is the protocol-level rule set that lets thousands of independent nodes agree on a single canonical chain without any central coordinator. A blockchain is a distributed ledger, and [cryptographic primitives](/docs/developers/curriculum/fundamentals/cryptographic-primitives) secure its individual transactions and blocks. This page answers the remaining question: when multiple nodes each propose a different block at the same time, how does the network decide which one becomes part of the chain? If you have used Raft or Paxos, the shape is familiar: a leader is elected to sequence writes, which maps to slot-leader selection, log entries to blocks, the term to an epoch, and heartbeats to block propagation. The critical difference is the threat model: Raft assumes honest nodes and only tolerates crashes, while Ouroboros assumes some nodes are malicious (Byzantine fault tolerance), which is why it needs VRFs, stake-weighted election, and a formal security proof. ## Why is consensus hard in distributed systems? Consensus is hard because distributed nodes have different views of pending transactions, face network latency, may go offline, and some may act maliciously, yet they must all agree on a single truth without a central coordinator. ``` Node A (Tokyo) sees [T1, T2, T3] Node B (New York) sees [T2, T4, T5] Node C (Berlin) sees [T1, T4, T6] ``` Three honest nodes, three different views, and no two of them agree on what is pending. Now suppose Node B is lying and T5 never existed. The network still has to settle on **who** produces the next block, **what** goes in it, and **when** it is final, despite latency, node failures, malicious actors, and no central coordinator. ## How does Proof of Work achieve consensus? Proof of Work requires block producers to solve a computationally expensive puzzle before adding a block; the first to find a valid solution wins, and the cost makes attacks economically irrational. ``` Find nonce such that hash(block_header + nonce) < target ``` This is **mining**: enormous effort to find, instant to verify. Strengths and weaknesses: - **Security**: attacking needs more compute than the rest of the network (a "51% attack"), which is prohibitively expensive for established chains. - **Energy**: PoW is intentionally wasteful; the security budget is the electricity consumed. - **Hardware centralization**: ASIC rewards concentrate mining near cheap electricity. - **Finality**: probabilistic; a transaction becomes exponentially unlikely to reverse as blocks pile on (Bitcoin convention: 6 confirmations). ## How does Proof of Stake differ? Proof of Stake replaces computational work with economic commitment: the right to produce a block is proportional to how much of the native currency you stake. ```mermaid graph TB subgraph PoW["Proof of Work"] HW["Hardware"] --> ELEC["Electricity"] --> SOLVE["Solve puzzle"] --> BPOW["Produce block"] end subgraph PoS["Proof of Stake"] STAKE["Stake ADA"] --> VRF["VRF lottery"] --> ELECT["Slot leader"] --> BPOS["Produce block"] end ``` If you hold 1% of staked tokens, you produce about 1% of blocks. The security model shifts from "attacking costs electricity" to "attacking costs money": acquiring a majority of stake drives the price up, and attacking collapses the value of what you hold. Attacking PoS is economically self-destructive. | Property | Proof of Work | Proof of Stake (Cardano) | |---|---|---| | Block producer selection | First to solve the puzzle | Protocol probabilistically selects by stake | | Energy efficiency | Low | High | | Hardware | Specialized ASICs | Standard servers | | Attack cost | 51% of hash power | 51% of staked ADA | ## What is Cardano's Ouroboros protocol? Ouroboros is Cardano's consensus protocol and the first Proof of Stake protocol with a rigorous, peer-reviewed security proof (Kiayias, Russell, David, Oliynykov, CRYPTO 2017). It divides time into epochs and slots, uses VRFs for private slot-leader election, and is provably secure as long as honest participants control the majority of staked ADA. ### How do epochs and slots structure time? **Slots** are 1 second each; **epochs** are 432,000 slots (exactly 5 days). A slot may or may not contain a block (target: roughly one block every 20 seconds). Epochs are the administrative boundary for stake snapshots, reward distribution, protocol-parameter changes, and pool registrations. ### How does slot leader election work? For each slot, each pool evaluates a VRF locally; the result is private until it publishes a block with the proof. ``` For slot S in epoch E: (vrf_output, vrf_proof) = VRF_eval(pool_vrf_key, epoch_nonce + slot_number) threshold = calculate_threshold(pool_stake / total_stake) if vrf_output < threshold: this pool IS the slot leader for slot S ``` The election is **private** (prevents targeted attacks on upcoming leaders), **proportional** (1% of stake wins ~1% of slots), **verifiable** (the VRF proof lets anyone confirm legitimacy), and allows **zero or multiple leaders** per slot (handled by chain selection). ### How does the stake snapshot work? The stake used for election is a **snapshot from two epochs ago**. This delay stops an attacker from rapidly acquiring stake and immediately using it. For delegators it means your delegation becomes active for rewards after a ~15-20 day ramp. ### How does chain selection handle forks? When multiple valid chains exist, nodes follow the **longest chain rule**, and Praos breaks equal-length ties by the block's leader VRF value. (The recent-chain-density rule is a feature of Ouroboros Genesis, which lets newly joining nodes bootstrap safely.) Blocks on abandoned forks are discarded and their transactions return to the mempool, which is why transactions need a few confirmations before they are settled. Short forks happen for two mundane reasons, and naming them removes the mystery. A **slot battle**: VRF elections are independent, so two pools can both win the same slot and both produce a block. A **height battle**: a leader elected a few slots later has not yet received the previous block and builds on the older tip. Both create momentary one-block forks that the selection rule resolves. ### Block diffusion and the security parameter k When a leader produces a block it must reach other nodes fast (Cardano targets diffusion within ~5 seconds) or risk being orphaned. The parameter **k** (currently 2160) defines settlement: a block is considered settled once k blocks follow it, roughly 12 hours at ~20s/block. And k is not only a probability statement: nodes never adopt a chain that forks more than k blocks below their tip, so everything deeper than k is immutable by construction and only the last k blocks are ever up for revision. In practice forks are typically a block or two deep. Most applications treat 10-20 confirmations (a few minutes) as very safe for ordinary value; high-value receivers wait deeper, exchanges commonly 20-30 blocks or more; k is the absolute bound. The [Cardano Blueprint's chain selection page](https://cardano-scaling.github.io/cardano-blueprint/consensus/chainsel.html) covers the rule and its tie-breakers in detail. ### How do rewards and incentives drive decentralization? Each epoch the protocol distributes rewards (from fees and monetary expansion) to operators (a fixed cost plus margin) and delegators (the remainder, proportional to stake). The reward formula caps oversized pools: ``` desirable pool size ~ 1 / k0 (k0 = target number of pools, currently 500) ``` Past that size a pool's rewards are capped. The excess stake earns nothing, so delegators have a reason to move to a smaller pool, and the operator has no reason to want them to stay. Decentralization is not enforced by a rule; it emerges from economic incentives (a Nash equilibrium toward ~500 evenly-sized pools). Operators can also **pledge** their own ADA, which slightly raises rewards and resists Sybil attacks (many tiny pools are less profitable than one well-pledged pool). ## How does finality work? Cardano provides **probabilistic finality**: the chance of reversal decreases exponentially with each block added, and beyond k = 2160 blocks (~12 hours) chain selection refuses to roll back at all, making k a hard bound on rollback depth rather than a probability. Practical finality is 10-20 confirmations, a few minutes. | Network | Typical finality | Mechanism | |---|---|---| | Bitcoin (PoW) | ~60 min (6 blocks) | Probabilistic | | Ethereum (PoS) | ~15 min | Deterministic after finalization | | Cardano (Praos) | ~3-7 min practical, ~12h bound | Probabilistic, stake-based | ## What happens during a complete epoch? Three epochs are in flight at any moment, because the inputs to block production are fixed two epochs ahead. - **Epoch N-2**: a stake snapshot is taken. This is the active stake that will decide leadership in epoch N. - **Epoch N-1**: VRF outputs from this epoch feed the nonce that seeds epoch N's leader election. - **Epoch N**: in every slot, each pool checks its VRF against its threshold. A pool that wins selects transactions, builds a block, signs it with its KES key, and publishes it with the VRF proof. Every other node verifies that proof, the KES signature, and each transaction in the block. - **At the boundary**: rewards are calculated and distributed, a new snapshot is taken, queued protocol-parameter changes take effect, and pool registrations and retirements are processed. ### What are KES keys? **Key-Evolving Signature (KES)** keys are a forward-security mechanism: the key evolves at regular intervals and old key material is deleted. If a pool's KES key is compromised, an attacker can only forge blocks from that point forward, not retroactively, and the operator can rotate to a new key from their cold keys. Analogous to short-lived, auto-rotating TLS certificates, applied to block production. (For how VRF, KES, and cold keys are generated and stored, see the [stake pool key reference](/docs/operators/basics/cardano-key-pairs).) ## Common attacks and defenses - **51% attack**: acquire majority stake. Defense: enormous cost, and success destroys the attacker's holdings. - **Nothing-at-stake**: produce blocks on many forks for free. Defense: Ouroboros's VRF election and formal proof make it unprofitable. - **Long-range attack**: build an alternative chain from far in the past. Defense: the 2-epoch snapshot delay limits it; Ouroboros Genesis solves it fully. - **Grinding**: manipulate the election randomness. Defense: the epoch nonce derives from many VRF outputs. ## Key takeaways - **Consensus** is how distributed nodes agree on one chain without a central authority, resilient to delays, failures, and malice. - **Proof of Work** secures via computational cost (energy-intensive, centralizing); **Proof of Stake** secures via economic stake. - **Ouroboros Praos** is the first PoS protocol with a formal security proof, selecting slot leaders via VRFs proportional to stake. - Time is **epochs (5 days) and slots (1 second)**; snapshots, nonces, and rewards happen at epoch boundaries. - Cardano's incentive design makes **decentralization an emergent economic equilibrium**, not an enforced rule. ## Next steps That settles how blocks are produced and agreed on. The next question is what is inside them: Cardano's Extended UTXO model. See [the eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo). --- ## Addresses An address is where value lives on Cardano: a public identifier others use to send you funds, much like an email address. Unlike one, it is self-sovereign (tied to keys you control, not a service provider) and bakes in its own spending rules and stake settings rather than being just a destination. Before you can follow how transactions move value or how the [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo) works, you need to know what an address actually encodes: who can spend funds held there, and who controls their stake. ## Address structure A Cardano (Shelley-era) address has two or three parts, laid out by [CIP-19](https://cips.cardano.org/cip/CIP-19): ``` +--------+-------------------+-----------------------+ | Header | Payment credential| Delegation credential | | 1 byte | 28 bytes | 28 bytes (optional) | +--------+-------------------+-----------------------+ ``` - **Header** describes the address type and network (mainnet or testnet). The network discriminant prevents sending mainnet funds to a testnet address. - **Payment credential** defines the spending condition: who can spend funds at this address. - **Delegation credential** (optional) controls stake delegation and reward withdrawal. The credentials are not keys but 28-byte Blake2b-224 hashes, of the public keys your wallet derived (see [Keys & Wallets](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys)) or of scripts: ```mermaid flowchart LR P[Payment keypublic] -->|"Blake2b-224"| PC[Payment credential28-byte hash] S[Staking keypublic] -->|"Blake2b-224"| DC[Delegation credential28-byte hash] H[Headertype + network] --> A[Base addressaddr1...] PC --> A DC --> A style P fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style S fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style H fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style PC fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style DC fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style A fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF ``` Addresses are **Bech32**-encoded with human-readable prefixes: `addr` (mainnet), `addr_test` (testnet), `stake` (reward addresses). ``` addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w stake1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5egfu2p0u ``` ## Payment credentials A payment credential comes in two forms: - **Verification key hash**: the Blake2b-224 hash of an Ed25519 public key. Regular wallets use this. To spend, you provide the public key and a signature. - **Script hash**: the Blake2b-224 hash of a Plutus or native script. Smart contracts, DEX pools, and escrows use this. To spend, you provide the script and satisfy its validation logic. :::tip Addresses hold hashes, not keys An address contains the **hash** of a public key, not the key itself. You cannot recover a public key from an address; the key is only revealed when funds are spent. This adds a layer of protection (and is why quantum concerns are reduced for unspent, unreused addresses). ::: When the payment credential is a script hash, the address is a **script address**: UTXOs there can only be spent by a transaction that satisfies the script. This is how contracts are "deployed", the script's hash *is* its address, and anyone who compiles the same script gets the same address. See [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview). ## Delegation credentials The delegation credential controls two things: publishing a delegation certificate (delegating stake to a pool) and withdrawing staking rewards. Like payment credentials, it can be a verification key hash or a script hash. **Key insight:** delegating does not move your funds. They stay at your payment address under your control; the delegation credential only decides which pool receives your stake and who can withdraw rewards. ## Address types | Type | Credentials | Use | |---|---|---| | **Base** | Payment + delegation | The most common type. Standard wallets; can hold funds and delegate for rewards. | | **Enterprise** | Payment only | No staking. Exchanges and organizations that explicitly opt out of stake rights. Shorter than base. | | **Reward (stake)** | Delegation only | Receives staking rewards; cannot receive regular payments. One per stake key. Prefix `stake`. | | **Pointer** | Payment + pointer to a stake registration | Space-efficient alternative to base; functionally equivalent, but rarely used. | | **Script** | Script-hash payment credential | A base or enterprise address whose payment credential is a script hash (smart contracts). | ## Privacy: stake-key linking Multiple payment addresses that share the same delegation credential are publicly linked, because the same stake key hash appears in all of them: ``` addr1q[payment_hash_1][stake_hash_shared]... addr1q[payment_hash_2][stake_hash_shared]... ``` Anyone can see these belong together. Options: - **Accept it** (standard wallet behavior, all addresses under one stake key). - **Forgo staking** with enterprise addresses (unlinked, but no rewards). - **Multiple stake keys** (complex and impractical for most). For most applications the linking is acceptable; only privacy-critical apps need alternatives. ## Working with addresses in code Whatever tool you reach for, the same handful of operations come up once an address enters your application: - **Parse** an address from its encodings (Bech32, hex, or raw bytes) into a structured value. - **Check the network** before using it. An address's header marks it mainnet or testnet; rejecting a mismatch up front prevents a costly class of mistakes (see the tip below). - **Inspect the credentials**: whether the payment credential is a key hash or a script hash, and whether a delegation credential is present (a base address) or absent (enterprise). - **Convert** between Bech32, hex, and bytes for storage, display, or transaction building. - **Build** an address from raw credentials. This is the rare case: in a dApp you usually *get* the user's address from the [wallet connector](/docs/developers/curriculum/dapps/connect-a-wallet) rather than constructing one. ```typescript // Parse, from Bech32, hex, or bytes const address = Address.fromBech32("addr1...") // also Address.fromHex(...) / Address.fromBytes(...) // Validate user input AND check the network (0 = testnet, 1 = mainnet) function parseChecked(input: string, expect: 0 | 1) { try { const a = Address.fromBech32(input.trim()) return a.networkId === expect ? a : null // wrong network → reject } catch { return null // malformed → reject } } // Inspect const details = Address.getAddressDetails("addr1...") // { type: "Base", networkId, address: { bech32, hex } } const hasStake = Address.hasStakingCredential(address) // base vs enterprise const isEnterprise = Address.isEnterprise(address) // Convert const hex = Address.toHex(address) const bytes = Address.toBytes(address) // 57 bytes for a base address, 29 for enterprise const bech32 = Address.toBech32(address) ``` ```typescript // Parse and inspect: pull the credentials out of a Bech32 address const { pubKeyHash, scriptHash, stakeCredentialHash } = deserializeAddress("addr1...") // Payment credential: a key hash (regular wallet) or a script hash (contract) const isScript = scriptHash !== undefined // Base vs enterprise: a base address carries a stake credential, enterprise doesn't const hasStake = stakeCredentialHash !== undefined // Shorthand when you only need the payment key hash const keyHash = resolvePaymentKeyHash("addr1...") ``` Building one from raw credentials (the rare case above) looks like: ```typescript declare const paymentKeyHash: Uint8Array // 28 bytes declare const stakeKeyHash: Uint8Array // 28 bytes const address = new Address.Address({ networkId: 1, paymentCredential: new KeyHash.KeyHash({ hash: paymentKeyHash }), stakingCredential: new KeyHash.KeyHash({ hash: stakeKeyHash }), // omit for an enterprise address }) ``` ```typescript declare const paymentKeyHash: string // 28-byte hash, hex declare const stakeKeyHash: string // 28-byte hash, hex // Build the address object from raw credentials (omit the stake hash for an enterprise address) const addressObj = pubKeyAddress(paymentKeyHash, stakeKeyHash) // Serialize to bech32 (networkId: 0 = testnet, 1 = mainnet) const address = serializeAddressObj(addressObj, 1) ``` :::tip Always validate the network Checking the network discriminant before using an address in a transaction is the cheapest guard against sending mainnet funds to a testnet address (and vice versa). Legacy Byron and pointer formats are still parsed automatically when reading existing UTXOs, but shouldn't be used for new addresses. ::: ## Key takeaways - An address encodes a header, a payment credential (who can spend), and an optional delegation credential (stake control). - Payment and delegation credentials are each either a key hash or a script hash. - Base addresses are the norm; enterprise opt out of staking; script addresses are where contracts hold funds. - Addresses store key *hashes*, not public keys, and reused stake keys link addresses publicly. ## Next steps - [Keys & Wallets](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys): where the keys behind these credentials come from - [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions): how value moves between addresses --- ## The Extended UTXO Model The Extended UTXO (eUTXO) model is how Cardano tracks who owns what. It records value as discrete, immutable "coins" (unspent transaction outputs) rather than mutable account balances, and it extends that idea with datums, redeemers, and script context so smart contracts can run while transactions stay deterministic and parallelizable. If you have used event sourcing or CQRS, eUTXO will feel natural: state is derived from an immutable log, you never modify past events, and new state is created by appending. Here, state is the set of unspent outputs, you never modify a UTXO, and new state comes from consuming and producing outputs. (It is no coincidence that Cardano's contract languages are functional: transactions behave like pure functions, same inputs, same outputs.) The one piece without a clean web2 parallel is change: you spend a UTXO whole and receive the remainder as a new output, the way you hand over a 50 and get 20 back, rather than decrementing a balance. :::note Quick summary Cardano tracks value as discrete UTXOs, not account balances. Each transaction consumes existing UTXOs and creates new ones. Smart contracts validate whether a UTXO can be spent; they never act on their own. The payoff is determinism: you know exactly what a transaction will do before you submit it. ::: Every blockchain needs a way to track ownership, and there are two fundamentally different approaches: the **account model** (Ethereum) and the **UTXO model** (Bitcoin, and in extended form, Cardano). This is not a minor implementation detail. It shapes how you think about transactions, how you design smart contracts, and what guarantees the protocol can give you. ## How does the account model work? The account model works like a bank account: each address has a mutable balance, and transactions update balances in place by debiting the sender and crediting the receiver. If you have worked with databases or Ethereum, you already understand it. State is a row per address, exactly like a database table: | Address | Balance | |---|---| | `addr_alice` | 5,000 ADA | | `addr_bob` | 3,000 ADA | Alice sending 1,000 ADA to Bob is an update in place: ```sql UPDATE accounts SET balance = balance - 1000 WHERE address = 'addr_alice'; UPDATE accounts SET balance = balance + 1000 WHERE address = 'addr_bob'; ``` This is familiar, but the simplicity comes with costs in a decentralized setting. Transactions are **stateful**: they depend on and modify shared global state, so a transaction's validity can change based on what executed before it, creating unpredictability between the moment you build a transaction and the moment it runs. ![eUTXO vs account model](./img/eutxo-vs-account-model.jpg) *Cardano represents assets as a directed graph of unspent outputs; account-based chains keep a database of balances that update with each state transition.* ## How does the UTXO model track ownership? The UTXO model tracks ownership through discrete, immutable "coins" (unspent outputs from previous transactions) instead of mutable balances. When you spend, you consume whole UTXOs as inputs and create new UTXOs as outputs, receiving change back to yourself, exactly like paying with physical cash. When you have 50 dollars, you do not hold an abstract "balance of 50." You hold specific bills (a 20, a 20, a 10). Buy something for 25 and you hand over 30 and get 5 back. ``` Alice's UTXOs (her "wallet"): UTXO_1: 3,000 ADA (from tx_abc, output #0) UTXO_2: 2,000 ADA (from tx_def, output #1) Alice sends 4,500 ADA to Bob: INPUTS: OUTPUTS: UTXO_1 3,000 ADA To Bob 4,500 ADA (new UTXO) UTXO_2 2,000 ADA To Alice 300 ADA (change, new UTXO) Total in: 5,000 ADA Fee 200 ADA Total out: 5,000 ADA After: UTXO_1, UTXO_2: SPENT (destroyed) New UTXO: 4,500 ADA to Bob New UTXO: 300 ADA to Alice (change) ``` ![UTXO transaction flow](./img/utxo-transaction-flow.png) *A transaction consumes existing UTXOs and creates new ones. Consumed UTXOs leave the UTXO set; new outputs become available for future transactions.* Four properties fall out of this: 1. **UTXOs are consumed entirely.** You cannot partially spend one. Spending 1,000 from a 3,000 UTXO means consuming all 3,000 and creating a 1,000 output plus 2,000 of change. 2. **Inputs equal outputs plus fees.** Every transaction balances exactly. The protocol enforces it. 3. **UTXOs are immutable.** Once created, a UTXO never changes. There is no UPDATE, only CREATE (as an output) and CONSUME (as an input). 4. **Each UTXO is spent once.** This is how double-spending is prevented. Once consumed by a confirmed transaction, a UTXO can never be used again. ### What is the UTXO set? The **UTXO set** is the complete collection of all unspent outputs at a point in time. It is the current state of the chain. | TxId:Index | Address | Value | |---|---|---| | `tx_01:#0` | `addr_alice` | 300 ADA | | `tx_02:#0` | `addr_bob` | 4,500 ADA | | `tx_03:#1` | `addr_dave` | 750 ADA | Each UTXO is uniquely identified by the transaction ID that created it plus its output index. That `(TxId, Index)` pair is a **transaction output reference** (TxOutRef). On mainnet the set holds millions of entries, and every node keeps it for fast validation. ## What does "Extended" add? The Extended UTXO model adds three things to Bitcoin's original UTXO concept: **datums** (data attached to a UTXO), **redeemers** (arguments provided when spending), and **script context** (a view of the whole transaction). Together they enable smart contracts while preserving UTXO determinism and parallelism. ```mermaid graph LR U["UTXO (Bitcoin)Value: 5,000 ADAAddress: key_hash"] E["Extended UTXO (Cardano)Value: 5,000 ADAAddress: script_hashDatum: { owner, deadline }Redeemer: given when spendingScript context: the transaction"] U -->|"Cardano adds"| E ``` **Datum** is data attached to an output: state that lives inside a specific UTXO. In the account model, contract state sits in mutable storage; in eUTXO, state lives in UTXOs, and you update it by consuming a UTXO and creating a new one with new data. Cardano supports two modes: a **datum hash** (only the hash is on-chain, the full datum is supplied at spend time) and an **inline datum** (the full datum is stored in the UTXO so others can read it without off-chain coordination). **Redeemer** is the argument you provide when spending a script-locked UTXO. The validator uses it to decide whether spending is allowed. **Script context** is the comprehensive view the validator receives: all inputs, all outputs, the fee, the validity range, signatories, and more. This is what makes conditions like these possible: - "This UTXO can only be spent if the transaction also sends 100 ADA to address X." - "This UTXO can only be spent after slot 50,000,000." - "This UTXO can only be spent if the transaction recreates this same script address with an updated datum." (This last pattern is the foundation of stateful contracts in eUTXO: the script enforces that its own state propagates correctly.) The full mechanics of writing validators against datum, redeemer, and context are covered in [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview); this page only needs the model. ### What can a validator actually see? Bitcoin vs Ethereum vs Cardano The scope of information available to a script is the key difference between the three models: - **Bitcoin (UTXO):** scripts see only the redeemer (the unlocking data). Simple and secure, but it limits contracts to "dumb" logic. - **Ethereum (account):** scripts can read and modify the entire global state. Powerful, but it introduces unpredictability and a large security surface. - **Cardano (eUTXO):** scripts see all inputs and outputs of the specific transaction plus its context, but not arbitrary global state. This middle ground was designed to provide expressive power comparable to the account model while keeping stronger security guarantees. Smart contract validators (written in Plutus or Aiken) are **pure functions**: given the same datum, redeemer, and context, they always return the same result. That purity buys you: - **Tractable security analysis.** You can reason about a script from the transaction alone, not the entire unpredictable chain state. - **Fail-fast validation.** You can run the exact validation locally before submitting. If an input is already spent, it fails off-chain and costs you nothing. - **No partial failures.** Either all conditions are met and the transaction succeeds, or it fails atomically. There is no "ran out of gas halfway through" state. ## What does a complete eUTXO transaction look like? A script interaction consumes script-locked UTXOs with a redeemer, the validator checks the datum, redeemer, and context, and if validation passes the transaction produces new UTXOs with updated state. Take a vesting contract. Alice locked 1,000 ADA at a script address, carrying the datum `{ beneficiary: addr_alice, release_slot: 50000000 }`, and she holds a separate 10 ADA UTXO to cover the fee. Once the chain is past slot 50,000,000, she builds a transaction that spends both: ``` Inputs Outputs [1] script UTXO 1,000 ADA [1] addr_alice 1,000 ADA (the vested funds) redeemer: { action: "withdraw" } [2] addr_alice 8 ADA (change) [2] Alice's UTXO 10 ADA fee 2 ADA Validity interval: from slot 50,000,000 onwards ``` The validator runs once, against this transaction and nothing else. It asks three questions: 1. **Is the validity interval entirely after `release_slot`?** The lower bound is exactly 50,000,000, so yes. The script never asks what time it is; the ledger has already refused to include the transaction any earlier. 2. **Does an output pay `beneficiary`?** Output 1 does. 3. **Is the transaction signed by `beneficiary`?** Alice's signature is on it. All three hold, so the validator returns true and the transaction is valid. Break any one of them and it returns false, which you can see by running the same validation locally before you submit anything. ## Why is deterministic validation such a big deal? Deterministic validation means you can predict exactly what a transaction will do before you submit it, because eUTXO transactions reference specific UTXOs by ID instead of reading mutable global state. In the account model, outcomes depend on state that can change between construction and execution. Alice builds a transaction against a DEX quoting 100 TOKEN per ETH. Bob's transaction lands first and moves the price to 200. Alice's then executes at the worse price, or fails and costs her gas anyway. At the moment she built it, neither outcome was knowable. In eUTXO, the transaction names its exact inputs. Alice builds a transaction consuming `UTXO_A` and `UTXO_B`. If either has been spent by the time it reaches a validator, the transaction fails and nothing happens. Otherwise it executes against exactly the state Alice saw when she built it. The result is the one she expected, or none at all. ## How does concurrency work in eUTXO? Concurrency in eUTXO is explicit, because two transactions cannot consume the same UTXO at once; only one wins and the other fails. A DEX that keeps its whole state in one script UTXO shows the problem plainly. Alice builds a transaction consuming it to buy 100 tokens, Bob builds one consuming it to buy 50. Whichever reaches a block first succeeds; the other now points at a UTXO that no longer exists, and fails. Nothing was corrupted, but one user has to rebuild. There are a few patterns to handle concurrency: 1. **UTXO fan-out.** Split state across many UTXOs instead of one, so many users transact in parallel against different UTXOs. 2. **Batching (order-book pattern).** Users submit orders as their own UTXOs; a batcher consumes many orders plus the protocol's state UTXO in a single transaction. 3. **Reference inputs.** A transaction can read a UTXO without consuming it, so many transactions can read the same oracle or config UTXO simultaneously with no contention. 4. **Reference scripts.** Script code can live in a UTXO and be referenced instead of included in every transaction, cutting size and cost. Reference inputs and reference scripts are transaction-level features; for the full mechanics see [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions#reference-inputs-and-reference-scripts). ## How do native tokens fit in? On Cardano, custom tokens are **native**: they live inside UTXOs alongside ADA at the protocol level, not inside smart contracts, so they inherit ADA's security without script execution for basic transfers. A single output carries a bundle, not a number: ``` Address: addr_alice Value: 5 ADA (5,000,000 lovelace) PolicyID_abc.TokenA: 1,000 PolicyID_def.MyNFT: 1 ``` ADA itself is denominated in **lovelace** (1 ADA = 1,000,000 lovelace), named after Ada Lovelace, just as Ethereum has wei. For how tokens are identified, minted, and why every token-bearing UTXO carries a minimum amount of ADA, see [What are native tokens](/docs/developers/curriculum/native-tokens/overview). ## How do the two models compare? Neither model is objectively better; they make different trade-offs. | Aspect | Account model (Ethereum) | eUTXO model (Cardano) | |---|---|---| | State representation | Mutable balances | Immutable UTXOs, consumed and created | | Smart contract state | Mutable storage slots | Datum attached to UTXOs | | Parallelism | Limited by shared state | Natural (different UTXOs) | | Determinism | State may change before execution | Inputs are specific UTXOs | | Wallet complexity | Simple (read balance) | Manage a UTXO set | | Fee predictability | Approximate (gas estimation) | Exact | | Native tokens | ERC-20 contracts | Protocol-level, no contract needed | ## How should you think in eUTXO? A framework for developers coming from account-based or web2 backgrounds: 1. **State lives in UTXOs, not variables.** Instead of a mutable `balance`, you have discrete value containers. 2. **State transitions consume and create UTXOs.** Instead of `balance -= 100`, you consume a UTXO and create new ones. Every change is a create-destroy cycle. 3. **Transactions are atomic functions.** Inputs in, outputs out, no side effects. If any part fails, none of it applies. 4. **Concurrency is UTXO selection, not locking.** If someone already spent the UTXO you wanted, you retry with different inputs. 5. **Scripts validate, they do not execute.** A validator checks that a proposed transition is legal; the builder constructs the transition. :::info Going deeper Read the [eUTXO handbook (PDF)](https://ucarecdn.com/3da33f2f-73ac-4c9b-844b-f215dcce0628/EUTXOhandbook_for_EC.pdf) for the formal treatment. ::: ### Is high TPS the right way to compare chains? ## Key takeaways - **The UTXO model tracks discrete coins**, consumed entirely and recreated as change, like physical cash, rather than mutable balances. - **eUTXO extends it** with datums (state), redeemers (action arguments), and script context (transaction awareness), enabling smart contracts without losing UTXO benefits. - **Determinism is the superpower.** Outcomes are predictable before submission, which shuts out fee-auction front-running and enables exact fees and off-chain validation. - **Concurrency is explicit.** Fan-out, batching, and reference inputs are the standard ways deployed protocols handle contention. - **Native tokens live in UTXOs alongside ADA**, inheriting protocol-level security without contracts for basic transfers. ## Next steps - [Addresses](/docs/developers/curriculum/fundamentals/core-concepts/addresses): where value lives and the credentials that guard it - [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions): how inputs and outputs are assembled, signed, and confirmed - [Transaction Fees](/docs/developers/curriculum/fundamentals/core-concepts/fees): the deterministic fee formula and collateral - Ready to build? [Smart Contracts Overview](/docs/developers/curriculum/smart-contracts/overview) --- ## Transaction Fees Transaction fees on Cardano are deterministic and predictable. They are calculated from a simple linear formula based on transaction size (plus script execution and reference script costs), so you can compute the exact fee before submitting, with no auctions and no gas-price spikes. If you have used a metered cloud API, fees will feel familiar: just as an API charges per request and throttles abuse, Cardano charges per transaction by size and complexity, pricing both bandwidth (size) and compute (ExUnits). Collateral works like a pre-authorized hold or security deposit: if your script crashes and consumes node resources, the deposit covers it; if everything succeeds, you keep it. ## Why fees exist 1. **Prevent spam.** Without a cost, an attacker could flood the network with meaningless transactions. 2. **Compensate stake pool operators.** Fees are part of the reward that incentivizes block production. 3. **Keep the network sustainable.** Fees cover both processing and long-term storage of the data each transaction adds. ## The fee formula ``` fee = a * size(tx) + b ``` - **`a`**: cost per byte of transaction data (currently 44 lovelace/byte). - **`b`**: fixed base fee on every transaction (currently 155,381 lovelace). - **`size(tx)`**: serialized transaction size in bytes. A typical simple transfer costs roughly **0.17-0.20 ADA**. Transactions with native tokens, metadata, or many outputs are larger and cost more; smart-contract transactions add execution fees on top. Both parameters serve a purpose: `a` covers the resource cost of processing and storing larger transactions, while `b` is a base security layer, a minimum cost regardless of size that makes flooding the network with tiny transactions prohibitively expensive. The formula gives the minimum the ledger will accept. The fee a transaction declares only has to be at least that, and tooling often pads slightly for safety, which is why fees you see on-chain sit a little above what the formula computes. :::note Parameters change through governance Query current values with `cardano-cli query protocol-parameters` or via your API provider. They are set on-chain and can change through governance. ::: ## Fee distribution Unlike chains where fees go straight to the block producer, Cardano pools them: fees collected in an epoch are distributed across all stake pools that produced blocks that epoch, regardless of which pool processed a given transaction. This promotes stability and fair rewards. ## Script execution fees When a transaction runs Plutus scripts (spending from a script address, minting with a smart-contract policy, validating certificates), an additional fee applies based on computational resources. ```mermaid flowchart TD A["Transaction size (bytes)"] --> B["Size fee: a * size + b"] C["Script execution (CPU + memory)"] --> D["Script fee: cpu*price_cpu + mem*price_mem"] F["Referenced script bytes"] --> G["Ref-script fee: tiered per byte"] B --> E["Total fee"] D --> E G --> E ``` Script costs are measured in **execution units (ExUnits)**: memory units (peak memory) and CPU steps (CPU budget). The script fee is `mem_price * memory_units + step_price * cpu_steps`, added to the size fee. Transaction-building libraries simulate execution to compute ExUnits automatically before submission, see the Evolution SDK's [script evaluation](https://github.com/IntersectMBO/evolution-sdk) for how this works under the hood. ## Reference script fees Transactions that use [reference scripts](/docs/developers/curriculum/fundamentals/core-concepts/transactions#reference-inputs-and-reference-scripts) pay a third component: every byte of every reference script carried by the transaction's inputs, spent or referenced, used or not, is charged, starting at 15 lovelace per byte (the `minFeeRefScriptCostPerByte` parameter, set through governance). The price is tiered: for each successive 25,600-byte increment of total referenced script size, the per-byte price multiplies by 1.2, so referencing a few kilobytes costs a fraction of an ADA while very large reference scripts get progressively expensive, up to a hard cap of 200 KiB of referenced script per transaction. Only the script bytes themselves count, not the CBOR tag and Plutus version number the ledger wraps around them. The fee exists because a referenced script is cheap for the transaction that names it but real work for every node, which must fetch and deserialize it. Reference scripts were free when Babbage introduced them, and that asymmetry was attacked on mainnet in June 2024; Conway priced them, with an escalating rather than flat rate, so ordinary scripts stay cheap while abuse prices itself out. For a fully worked mainnet example, from raw CBOR to the final lovelace, see the [Cardano Blueprint's transaction fee page](https://cardano-scaling.github.io/cardano-blueprint/ledger/transaction-fee.html). ## Collateral Transactions that execute scripts must provide **collateral**: ADA-only UTXOs that are forfeited only if a script fails during phase-2 validation. **Why it exists:** nodes spend real compute evaluating scripts. If a script fails after that work, collateral compensates for it and discourages submitting transactions that will fail. Rules: - Must contain **only ADA** (no native tokens). - Must be at least 150% of the transaction fee (the `collateralPercentage` protocol parameter, currently 150). - **Returned untouched** if the transaction succeeds. - **Consumed only** if phase-2 validation fails. - **Collateral return (CIP-40):** since Vasil, a transaction can specify a collateral return address so only the required amount is taken, not the entire UTXO. Losing collateral is avoidable in practice. Phase-2 validation is deterministic: it depends only on the transaction and the outputs it spends or references, so a script that passed when you evaluated it locally cannot fail on-chain against those same inputs. If the chain changes underneath the transaction, say an input gets spent first, it fails phase 1 instead, which costs nothing. A submitter who validates before submitting should never forfeit collateral. The CIP-40 return address exists for the case where you cannot pre-validate because a third party evaluates scripts on your behalf. This is the canonical reference for collateral; the [transaction lifecycle](/docs/developers/curriculum/fundamentals/core-concepts/transactions#deterministic-outcomes) and [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview) link here. ## UTXO fragmentation and fees Because fees scale with transaction size, **how your wallet's value is spread across UTXOs affects what you pay**. A wallet holding one large UTXO spends cheaply (one input); the same balance split across many tiny UTXOs needs many inputs to cover the same amount: a larger transaction, and a larger fee. This is **fragmentation**. It's a real tuning axis, not just theory: - **Consolidation**: periodically combining many small UTXOs into one (a self-payment) lowers the cost of future transactions. The tradeoff is spending flexibility and parallelism: separate UTXOs let you build independent transactions at the same time without contention. - **Native tokens amplify it**: token-bearing UTXOs are larger and each carries [min-ADA](/docs/developers/curriculum/native-tokens/overview#the-minimum-ada-requirement), so a fragmented token wallet is both bigger and ties up more ADA. - **Coin selection** decides which UTXOs to spend; SDKs default to largest-first to minimize input count. See [transaction building](/docs/developers/curriculum/start-building/transaction-building#coin-selection). ## Key takeaways - Fees are deterministic: `fee = a * size + b`, knowable exactly before submission. - Script transactions add an ExUnits-based execution fee on top of the size fee; builders compute it automatically. - Reference scripts add a third, per-byte fee that escalates in tiers for very large scripts. - Collateral (ADA-only) is forfeited only on phase-2 script failure; CIP-40 returns the excess. - Fees are pooled and distributed across block-producing stake pools each epoch. ## Next steps - [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions): how fees fit into building and submitting - [What are native tokens](/docs/developers/curriculum/native-tokens/overview): why token outputs cost more (min-ADA) --- ## Core Concepts These pages cover how value is represented, owned, moved, and priced on Cardano. They are the foundation everything else builds on. You don't need to read all of it before you start building, most build guides link back here when a concept becomes relevant, but if you are new to Cardano, reading it in order pays off. ## Recommended reading order 1. **[The eUTXO Model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo)**: how Cardano represents and spends value 2. **[Addresses](/docs/developers/curriculum/fundamentals/core-concepts/addresses)**: where value lives and the credentials that guard it 3. **[Keys & Wallets](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys)**: who controls value, and how wallets manage keys 4. **[Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions)**: how value moves 5. **[Transaction Fees](/docs/developers/curriculum/fundamentals/core-concepts/fees)**: what transactions cost and why ## Why these concepts matter Cardano uses the Extended UTXO (eUTXO) model rather than account balances. That single choice changes how transactions, state, and smart contracts work compared to account-based chains like Ethereum: - **Transactions are deterministic.** You know exactly what will happen before you submit. - **Smart contracts validate, they don't act.** Scripts approve or reject a proposed transaction. - **Tokens are native.** No smart contract is needed for basic token operations. ## The big picture Cardano was designed with input from a global team of experts in programming languages, network design, and cryptography. If you haven't seen it, the 2017 whiteboard video is still a worthwhile primer on what Cardano is and where it came from (some details have since evolved). ## Next steps - [Start Building](/docs/developers/curriculum/start-building/overview): Module 2, where these concepts become transactions --- ## Transactions A transaction is a signed data structure that consumes existing UTXOs as inputs and produces new UTXOs as outputs. Every transfer of value, every mint, and every smart-contract interaction begins and ends with one. Cardano transactions are explicit and deterministic: they list exactly what goes in and what comes out, so you know the outcome before you submit. If you have made HTTP requests, the lifecycle is familiar: you construct a transaction, authenticate it by signing, submit it, and await confirmation, all asynchronously. The difference that matters is atomicity: a transaction's inputs and outputs apply with database-style ACID guarantees, so the whole thing succeeds or none of it does, with no partial state left behind. ## Anatomy of a transaction Every transaction consists of inputs, outputs, a fee, and witnesses; it can optionally include metadata, validity intervals, minting, and certificates. ```mermaid flowchart LR subgraph Inputs A["UTXO_A\n100 ADA (Alice)"] end subgraph Body["Transaction body"] F["Fee: 0.2 ADA"] end subgraph Outputs B["30 ADA -> Bob"] C["69.8 ADA -> Alice (change)"] end A --> Body --> B Body --> C ``` - **Inputs** are pointers to existing UTXOs, each identified by `tx_hash#index`. Including one declares "I want to consume this specific UTXO." The protocol checks it exists, is unspent, and is properly authorized (a signature or a passing script). - **Outputs** each create a new UTXO with an address, a value (ADA and/or native tokens), and an optional datum. - **Witnesses** are the signatures (and scripts) that authorize the inputs. ### The balancing equation ``` Sum(Inputs) = Sum(Outputs) + Fee ``` This must hold *exactly*, not approximately. ADA cannot be created or destroyed in a normal transaction (minting is a separate, policy-controlled mechanism). This explicitness is the opposite of account-based systems where you just say "send X from A to B" and the protocol does the arithmetic. ## Deterministic outcomes Because a transaction names its exact inputs and fixes all script arguments, its result is predictable before submission. Scripts are pure and always terminate; signatures prevent tampering; the [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo) guarantees deterministic ledger updates. Either the expected result happens, or the transaction fails with no effect. (Phase-1 structural failures cost nothing; only phase-2 script failures consume [collateral](/docs/developers/curriculum/fundamentals/core-concepts/fees#collateral).) ## Validity intervals and time Smart-contract execution is fully deterministic, which raises a question: how do you handle time without breaking determinism? Cardano uses **validity intervals**, a slot range during which a transaction may be included in a block. ``` validity_interval = { invalid_before: slot_500, invalid_hereafter: slot_1000 } ``` - **Lower bound** (`invalid_before`): valid only after this slot. - **Upper bound** (`invalid_hereafter`, also called the TTL, for time to live): expires after this slot. These are checked in phase-1, before scripts run, so a validator can safely assume the transaction is within the window, enabling deterministic time logic (deadlines, time-locks). Most simple transfers omit the lower bound and set a generous upper bound so a stuck transaction expires instead of lingering. Each slot maps to wall-clock time (one second on mainnet), so contracts reason about time without an oracle. ### Setting validity in code In the SDKs you give a wall-clock window and the builder converts it to the on-chain slot range. That conversion is per-network: each one has its own slot length (one second on mainnet, preprod, and preview, configurable on a [local devnet](/docs/developers/curriculum/start-building/local-testing#local-devnets)) and its own genesis `zeroTime`/`zeroSlot`, so the builder has to know which chain you are on. ```typescript const now = BigInt(Date.now()) const tx = await client .newTx() .payToAddress({ address: recipient, assets: Assets.fromLovelace(2_000_000n) }) .setValidity({ from: now, to: now + 300_000n }) // valid for the next 5 minutes; both bounds optional .build() // Convert between wall-clock time and slots when a contract works in slots: const slot = Time.unixTimeToSlot(now, SlotConfig.SLOT_CONFIG_NETWORK.Preprod) const time = Time.slotToUnixTime(50_000_000n, SlotConfig.SLOT_CONFIG_NETWORK.Mainnet) ``` ```typescript // Convert a wall-clock window to on-chain slots for the network const lowerSlot = resolveSlotNo("preprod", Date.now()) // valid from now const upperSlot = resolveSlotNo("preprod", Date.now() + 300_000) // expires in 5 minutes const unsignedTx = await txBuilder .txOut(recipient, [{ unit: "lovelace", quantity: "2000000" }]) .invalidBefore(Number(lowerSlot)) // lower bound; optional .invalidHereafter(Number(upperSlot)) // upper bound (TTL); optional .changeAddress(changeAddress) .selectUtxosFrom(utxos) .complete() ``` ## Reference inputs and reference scripts Two Vasil-era features let transactions share data without contention: - **Reference inputs (CIP-31):** read a UTXO without consuming it. Many transactions can reference the same oracle or config UTXO in the same block. This is the canonical fix for read-only contention. - **Reference scripts (CIP-33):** store a script in a UTXO and reference it, instead of embedding the full script in every transaction. The referenced bytes are not free, they carry a [per-byte fee of their own](/docs/developers/curriculum/fundamentals/core-concepts/fees#reference-script-fees) that escalates for very large scripts, but it is normally far below the cost of inlining the script. ``` Inputs (consumed): UTXO_A (Alice's payment) Reference inputs (read): UTXO_Oracle (price feed), UTXO_Script (reference script) Outputs: result computed from the oracle data ``` ## The transaction lifecycle ```mermaid flowchart LR A["1. Construct"] --> B["2. Balance\n(coin selection + fee)"] B --> C["3. Sign"] C --> D["4. Submit\n(node validates -> mempool)"] D --> E["5. Block inclusion"] E --> F["6. Confirmation"] ``` 1. **Construct** the body: select inputs, define outputs, set validity, attach metadata. 2. **Balance**: pick UTXOs to cover outputs + fee, add a change output, iterate until it balances (**coin selection**). 3. **Sign**: hash the body and sign with the relevant private keys; the signatures become witnesses. 4. **Submit** to a node (directly or via a provider like Blockfrost/Koios). It validates: inputs unspent? witnesses match? fee sufficient? validity satisfied? outputs meet min-UTXO? scripts pass? If so, it enters the **mempool**. 5. **Block inclusion**: a slot leader includes it in a block and propagates it. 6. **Confirmation**: confidence grows with each subsequent block. :::note No reverted transactions Once a transaction passes submission validation and enters the mempool, it stays on track for inclusion for as long as it remains valid against the node's view of the chain. There is no Ethereum-style "reverted but you still paid" for phase-1 failures: if it was valid when submitted, it is valid when included, and if the chain changes underneath it (an input spent first, the validity window passing) it is dropped without charging you anything. ::: ### Latency vs finality - **Latency**: time to appear in a block (~20s average block time). - **Finality**: time to become practically irreversible. Depends on your risk tolerance; most applications treat [10-20 confirmations](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros#how-does-finality-work) (a few minutes) as strong finality, high-value transfers wait longer. ## Serialization (CBOR) At the lowest level, transactions are binary data encoded with **CBOR** (Concise Binary Object Representation, like a binary JSON). Cardano defines the exact structure with **CDDL** per era; any deviation is rejected. CBOR is compact (smaller transactions, lower fees) and has canonical encoding rules so the same logical transaction always hashes the same way. The transaction hash (its unique ID) is the Blake2b-256 hash of the serialized **body** (not the witnesses), so you can compute the ID before signing. You rarely touch raw CBOR, libraries (Evolution, Mesh, cardano-cli) handle it, but understanding it helps when debugging, since explorers show processed data rather than the bytes nodes validate.
Advanced: transaction body field numbers ``` transaction = [ transaction_body, transaction_witness_set, is_valid, auxiliary_data ] transaction_body: 0 inputs 1 outputs 2 fee 3 ttl (invalid_hereafter) 11 script_data_hash 13 collateral 14 required_signers ... transaction_witness_set: 0 vkey signatures 3 Plutus scripts 4 Plutus data (datums) 5 redeemers ``` Inputs are sorted lexicographically by `(tx_id, index)`, not the order you specify, which affects redeemer indexing. Any change to redeemers/datums/parameters requires recomputing the script data hash; libraries do this automatically. For deep CBOR debugging see [Debugging CBOR](/docs/developers/curriculum/smart-contracts/advanced/debug-cbor).
## What else can a transaction carry? - **Fees** scale with size (and script execution). See [Transaction Fees](/docs/developers/curriculum/fundamentals/core-concepts/fees). - **Metadata**: arbitrary off-chain-readable data (e.g. CIP-20 messages under label 674, CIP-25 NFT metadata under 721). Not visible to scripts. See [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry). - **Minting**: create or burn native tokens via the `mint` field. See [What are native tokens](/docs/developers/curriculum/native-tokens/overview). - **Min-UTXO**: every output must carry a minimum amount of ADA. See [the min-ADA requirement](/docs/developers/curriculum/native-tokens/overview#the-minimum-ada-requirement). - **Datums** for smart-contract outputs. See [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview). ## Key takeaways - A transaction consumes UTXOs and creates UTXOs; inputs must equal outputs plus the fee, exactly. - Outcomes are deterministic: you can validate locally and know the result before submitting. - Validity intervals give time control without breaking determinism; reference inputs/scripts share data without contention. - The lifecycle is construct, balance, sign, submit, include, confirm; there is no "reverted" transaction once it is valid and in the mempool. - CBOR is the underlying encoding; the transaction ID is the Blake2b-256 hash of the body. ## Next steps - [Transaction Fees](/docs/developers/curriculum/fundamentals/core-concepts/fees): the deterministic fee formula and collateral - [What are native tokens](/docs/developers/curriculum/native-tokens/overview): minting and multi-asset values - Ready to build one? [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview) --- ## Keys & Wallets Keys and wallets are the identity and access layer of Cardano. A seed phrase generates a tree of key pairs, public keys are hashed into the [address](/docs/developers/curriculum/fundamentals/core-concepts/addresses) credentials that lock UTXOs, and wallet software manages it all so users can send, receive, and stake. Whoever holds the private key controls the funds, there is no password reset. If you have used SSH, the model will feel familiar: you already generate an `ed25519` key pair, keep the private key local, share the public key, and prove possession by signing. Cardano keys work the same way, except you authenticate to the whole network and can move value, so losing the key costs more than losing server access. A CIP-30 wallet connector, meanwhile, is like "Sign in with Google" (OAuth): the dApp receives the signatures it asks for but never sees your raw private key. ## What is a key pair? A key pair is a private key (32 bytes of entropy) and its public key (derived via Ed25519, the same algorithm as SSH `ed25519` keys). ``` private_key = random_256_bits() public_key = ed25519_derive(private_key) private -> public: easy public -> private: infeasible sign(msg, private) -> 64-byte signature verify(msg, sig, public) -> true/false ``` - **The private key is your identity.** Whoever holds it can spend the funds. - **The public key is your verifiable identity.** Share it freely; others verify your signatures and derive your address from it. ## Why not use raw key pairs? One key per address creates real problems: transactions become trivially linkable, managing hundreds of unrelated keys is error-prone, backups are impractical, and a compromised key cannot be rotated. The fix is **Hierarchical Deterministic (HD) wallets**. The next two sections walk the pipeline an HD wallet runs when it is created: random entropy is encoded as a mnemonic you back up, stretched into a root key, grown into a tree of derived keys, and hashed into addresses. ```mermaid flowchart LR E[Entropy128 / 256 bits] -->|"BIP-39"| M[Mnemonic15 / 24 words] E -->|"CIP-3 IcarusPBKDF2"| R[Root key] R -->|"CIP-1852m/1852'/1815'/0'"| D[Derived keyspayment + staking] D -->|"CIP-19Blake2b-224 + Bech32"| A[Addressaddr1...] style E fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style M fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style R fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style D fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style A fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF ``` Note that the mnemonic is a branch, not a step: both the words and the root key are derived from the same entropy, which is why the phrase alone can always rebuild the whole tree. ## Seed phrases (BIP-39) A mnemonic seed phrase is a human-readable encoding of random entropy as words from a standard 2048-word list (Cardano wallets use 15 or 24 words). A checksum is folded into the words, so a mistyped or misplaced word is caught at recovery instead of silently restoring the wrong wallet. This single phrase deterministically regenerates your entire key tree, so it is the only backup you need. ``` 24 words = 256 bits of entropy = 2^256 possible phrases (~10^77) ``` Brute-forcing that is not merely impractical, it is physically impossible. Cardano wallets follow the Icarus standard ([CIP-3](https://cips.cardano.org/cip/CIP-3)): the phrase's underlying entropy runs through PBKDF2-HMAC-SHA512 (4,096 rounds, deliberately slow to make brute-forcing expensive) to produce a 96-byte Ed25519 extended root key. An optional passphrase (a "25th word") produces a completely different wallet from the same words. ## One seed, many keys A wallet does not keep a pile of unrelated keys. It derives them on demand from the root key, walking down a tree, and every branch is reproducible from the seed phrase. That is why the phrase alone restores a wallet, and why your wallet can hand you a fresh receive address forever without going back to the seed: it takes the next number along one branch. Two consequences matter when you write code. **An account has many payment keys and exactly one staking key.** Payment keys control spending, one per address, so you can hand out a new address whenever you like. The staking key controls delegation and reward withdrawal, and there is a single one for the whole account. Every address in that account shares it. This is why delegating once covers all your addresses, and why you can delegate stake to a pool without giving anyone the ability to spend your funds. **The path names which key you mean.** Cardano's layout is [CIP-1852](https://cips.cardano.org/cip/CIP-1852), which uses the same five-level shape as the rest of the industry (BIP-44) with numbers of its own: ``` m / 1852' / 1815' / account' / role / index | | | | | | | | | +- which address, counting up from 0 | | | +- 0 receive, 1 change, 2 staking | | +- which account, almost always 0 | +- ADA, 1815, the year Ada Lovelace was born +- Shelley-era layout, 1852, the year she died ``` You almost never type this. The SDK derives it for you, and the only level you normally set is the account, which shows up as `accountIndex` in the code further down this page. The full path becomes relevant when you run several accounts from one seed, when you are reconciling an address your wallet displays against one you derived yourself, or when a hardware wallet asks you to confirm a path on its screen. The last two levels are not hardened, which has a practical payoff: you can give a service the account's *public* key and it can derive every address to watch balances, while remaining unable to sign anything. ## What is a wallet, really? A wallet is software that stores your keys, scans the chain for UTXOs at your addresses, computes your balance, and builds and signs transactions. Your funds live on-chain as UTXOs; they are not "inside" the app. | Wallet type | Examples | Trade-off | |---|---|---| | **Full-node** | Daedalus | Maximum trustlessness; downloads the whole chain | | **Light** | Browser and mobile wallets | Fast; relies on a backend for chain data (signing stays local) | | **Hardware** | Ledger, Trezor | Keys never leave a secure device; strongest theft protection | | **Browser extension** | (implements CIP-30) | The standard way dApps connect to users | ## How dApps connect: CIP-30 CIP-30 is the dApp connector standard. The wallet exposes an API; it signs only what the user approves and never exposes private keys, much like "Sign in with Google" hands an app a token, not your password. ```typescript // No library: the standard as the wallet exposes it, on window.cardano const wallet = await window.cardano.eternl.enable() const utxos = await wallet.getUtxos() const signed = await wallet.signTx(unsignedTx) // user approves in the wallet ``` ```typescript const walletApi = await window.cardano.eternl.enable() const client = Client.make(mainnet).withCip30(walletApi) ``` ```typescript const wallet = await MeshCardanoBrowserWallet.enable("eternl") const utxos = await wallet.getUtxosMesh() ``` CIP-30 is a standard, not a library. Every SDK and connect-button package is a wrapper over the same browser API, and the wallet holds the keys whichever one you pick. [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet) covers discovery, the sign-then-submit split, and the framework options. Wallets can also sign arbitrary messages (CIP-8 / COSE) to prove address ownership without submitting a transaction, the basis for wallet login. For implementations, see [Wallet authentication](/docs/developers/curriculum/dapps/wallet-authentication). ## Working with wallets in code For a **browser dApp**, you don't manage keys at all: you connect the user's CIP-30 wallet (above), covered in [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet). For **backend services, scripts, and tests**, you create a wallet from a mnemonic, a private key, or just an address (read-only). The SDK handles the BIP-32/CIP-1852 derivation described above. In Evolution, a wallet is one capability of a client. Add it with `.withSeed()`, `.withPrivateKey()`, or `.withAddress()` (a wallet on its own can sign and derive addresses; add a provider to also query and submit): ```typescript // From a 24-word mnemonic (dev, testing, multi-account via accountIndex) const seedClient = Client.make(preprod) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) // From an extended private key (backend automation; load from a vault) const keyClient = Client.make(preprod) .withPrivateKey({ paymentKey: process.env.PAYMENT_SIGNING_KEY! }) // Read-only, observe an address, no signing (backend tx-building, monitoring) const watchClient = Client.make(preprod) .withAddress({ address: "addr1..." }) const address = await seedClient.address() ``` Generate a fresh mnemonic with `PrivateKey.generateMnemonic()`. Switch networks by changing the network parameter (`preprod` → `mainnet`); use a different mnemonic per environment. Mesh's `MeshCardanoHeadlessWallet` loads from a mnemonic, a root key, or explicit credential sources: ```typescript // Generate a fresh mnemonic const mnemonic = MeshCardanoHeadlessWallet.brew() const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = testnet, 1 = mainnet walletAddressType: AddressType.Base, fetcher: blockchainProvider, submitter: blockchainProvider, mnemonic: process.env.WALLET_MNEMONIC!.split(" "), }) // other factories: fromBip32Root (bech32 xprv), fromBip32RootHex, fromCredentialSources (read-only / advanced) ``` These four map to four security models. Pick the one with the least capability that still does the job: | Type | Holds keys | Can sign | Where the key belongs | Use for | |---|---|---|---|---| | **Seed phrase** | Yes, in your process | Yes | A local `.env`, never committed and never in production | Development and tests | | **Private key** | Yes, in your process | Yes | A secret manager, read at startup, never on disk | Backend automation | | **CIP-30 (browser)** | No, the wallet does | The user does | The user's wallet or hardware device | Frontend dApps | | **Read-only** | No key at all | No | Nothing to store, only an address | Backend transaction building, monitoring | :::warning Backend key handling Never bundle a mnemonic or private key into frontend code, and never commit one. On a backend, load keys from a secret manager (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault), use a **read-only** wallet wherever you only need to build transactions, and keep separate keys per environment. Production key handling is covered in [going to production](/docs/developers/curriculum/production/going-to-production). ::: ## Security: it all reduces to key management - **Seed phrase**: never store digitally (no photos, cloud, or text files); write on durable material; consider the optional passphrase. - **Key isolation**: hardware wallets keep keys in a secure element; extensions encrypt with a spending password. - **Address hygiene**: let the wallet generate fresh addresses; reuse links your history. - **Verify on-device**: confirm transaction details on the hardware wallet screen, not just the app. - **Cold-key custody**: for keys that sign high-value or governance transactions, keep them off internet-connected machines. See the operator guides on [air-gapped signing](/docs/operators/security/air-gap) and the [secure transaction workflow](/docs/operators/security/secure-workflow). ## Key takeaways - A 24-word seed phrase is the root of your whole identity; the CIP-1852 derivation tree grows every key the wallet will ever use from it. - An account has many payment keys and one staking key, so you can delegate without exposing spending control. - A wallet is software that manages keys and builds transactions; funds live on-chain as UTXOs. - CIP-30 lets dApps request signatures without ever seeing private keys; security reduces to protecting the seed phrase and isolating keys. ## Next steps - [Addresses](/docs/developers/curriculum/fundamentals/core-concepts/addresses): how these keys become the credentials that lock funds - [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions): how wallets build, sign, and submit --- ## Cryptographic Primitives Cryptographic primitives are mathematical functions with special properties that make it computationally infeasible to cheat, and they form the security foundation of every blockchain transaction. [What Is a Blockchain?](/docs/developers/curriculum/fundamentals/what-is-a-blockchain) described properties like immutability and tamper-evidence; this page covers the concrete tools that enforce them, hash functions, Merkle trees, and digital signatures, and why Cardano chose the specific algorithms it did (Blake2b, Ed25519). If you have worked with JWTs, the model will feel familiar: a JWT signs a payload with a private key, anyone with the public key verifies it, and modifying the payload invalidates the signature. A Cardano transaction works the same way, except verification happens on thousands of independent nodes rather than one server. Two related ideas carry over: a transaction ID is a content hash, so the content determines its identity the way a Git commit SHA does; and a VRF is a random value anyone can verify, which a server-side `Math.random()` can never be. ## What is a cryptographic hash function? A cryptographic hash function takes an input of any size and produces a fixed-size output (the "hash" or "digest") such that the same input always produces the same output, but even a tiny change in input produces a completely different hash. It is the most fundamental building block in blockchain security. ```mermaid graph LR A["Input (any size)"] --> H["Hash Function (Blake2b-256)"] H --> O["Fixed-Size Output (32 bytes)"] A2["Input + 1 bit changed"] --> H2["Hash Function"] H2 --> O2["Completely Different Output"] ``` A cryptographic hash function must satisfy: 1. **Deterministic**: the same input always produces the same output. 2. **Fixed output size**: regardless of input size, output length is constant. 3. **Pre-image resistance**: given a hash, it is infeasible to find the original input (you cannot "reverse" a hash; ~2^256 guesses). 4. **Second pre-image resistance**: given an input and its hash, it is infeasible to find a different input with the same hash. 5. **Collision resistance**: it is infeasible to find any two different inputs with the same hash (~2^128 operations, the birthday bound). 6. **Avalanche effect**: a small input change produces a drastically different output. ### Why does Cardano use Blake2b instead of SHA-256? SHA-256 is what Bitcoin uses. **Blake2b** (by Aumasson and colleagues, based on a SHA-3 finalist) is what Cardano uses extensively: Blake2b-256 for most hashing and Blake2b-224 for address generation. | Property | SHA-256 | Blake2b-256 | |---|---|---| | Speed | Slower in software | 2-3x faster in software | | Security margin | Well-established | Comparable, based on ChaCha | | Parallelism | Sequential | Designed for parallelism | | Flexibility | Fixed | Configurable output, keyed hashing | | Hardware | Efficient in ASICs | Efficient in general-purpose CPUs | Speed matters because hashing happens constantly (validating blocks, verifying transactions, computing addresses). Faster hashing means higher throughput. ### Where does Cardano use hashing? 1. **Block header hashing**: each header is hashed and included in the next block, forming the chain. 2. **Transaction IDs**: every transaction is identified by a Blake2b-256 hash of its serialized content. 3. **Address generation**: addresses derive from public keys hashed with Blake2b-224. 4. **Script hashing**: a Plutus validator's compiled code is hashed; that hash is its script address. 5. **Datum hashing**: data attached to UTXOs can be stored as hashes to save space. 6. **Policy IDs**: native-token minting policies are identified by the hash of the policy script. ## How do Merkle trees enable efficient verification? A Merkle tree organizes transaction hashes into a binary tree where each leaf is a transaction hash, each internal node is the hash of its two children, and the root (stored in the block header) is a single 32-byte fingerprint of all transactions. This enables logarithmic-time membership proofs. ```mermaid graph TB MR["Merkle Rootblake2b(Hash_AB + Hash_CD)"] --- HAB["Hash(AB)"] MR --- HCD["Hash(CD)"] HAB --- HA["Hash(A) = blake2b(Tx1)"] HAB --- HB["Hash(B) = blake2b(Tx2)"] HCD --- HC["Hash(C) = blake2b(Tx3)"] HCD --- HD["Hash(D) = blake2b(Tx4)"] ``` To prove Tx3 is in a block you do not download all transactions; you need only the sibling hashes along the path (a **Merkle proof**). For a block of N transactions, membership needs only about log2(N) hashes. If any transaction is modified, its hash changes and propagates up to the root, which no longer matches the header, immediately revealing tampering. This also enables **light clients** that verify inclusion without storing the full chain. Cardano uses Merkle structures for transaction verification, stake-distribution snapshots at epoch boundaries, and committing to large datasets while storing only the root on-chain. ## How do digital signatures prove identity? A digital signature scheme lets someone sign a message with a private key so anyone can verify it with the corresponding public key. It provides authentication (the signer held the private key), integrity (any modification invalidates the signature), and non-repudiation (the signer cannot deny it). ``` Key Generation: (private_key, public_key) = generate_keypair() Signing: signature = sign(message, private_key) Verification: is_valid = verify(message, signature, public_key) ``` Only the private key can produce a valid signature; the public key verifies it without revealing the private key; and the signature is bound to the specific message. ### Why does Cardano use Ed25519? Cardano uses **Ed25519** (EdDSA over Curve25519, by Bernstein and colleagues). | Property | Ed25519 | ECDSA (Bitcoin/Ethereum) | |---|---|---| | Signature size | 64 bytes | ~72 bytes (DER) | | Speed | Very fast | Slower | | Deterministic | Yes | Needs a random nonce (a frequent source of bugs) | | Side-channel resistance | Designed in | Vulnerable if implemented carelessly | | Batch verification | Efficient | Not native | Ed25519's deterministic signing is a major security advantage: ECDSA requires a random nonce, and a flawed RNG can leak the private key (this has caused real-world compromises). Ed25519 derives the nonce deterministically, eliminating that class of bug. ### How do signatures work in Cardano transactions? What gets signed is the **hash of the transaction body**, not the raw transaction: ``` 1. Build the body (inputs, outputs, fee) 2. tx_body_hash = blake2b_256(serialize(body)) 3. signature = ed25519_sign(tx_body_hash, private_key) 4. Assemble: body + witnesses [(public_key, signature), ...] 5. Each node verifies ed25519_verify(tx_body_hash, signature, public_key) AND that the public key matches the address controlling the input ``` Signing a 32-byte hash is fast, and the hash's collision resistance ensures the signature covers all transaction data. Cardano supports **multi-signature** at the protocol level (multiple witness entries), no smart contract required for basic multisig. ## How do these primitives combine for security? ``` 1. A user signs a transaction: sig = ed25519_sign(blake2b_256(tx_body), key) -> authenticity + integrity of the transaction 2. The block producer builds a block: tx hashes form a Merkle tree; root goes in the header -> efficient verification + tamper detection 3. The block is chained: next block references blake2b_256(header) -> immutability across the chain 4. The chain grows: altering history would require re-signing, recomputing every Merkle root and every subsequent block hash, faster than the whole network -> practical immutability ``` No single primitive does the job alone: hashes provide integrity and binding, Merkle trees provide efficient structure, signatures provide authorization. Together the cost of cheating exceeds any benefit. ## What are Verifiable Random Functions (VRFs)? A VRF combines a signature with a random number generator: given a private key and an input, it produces a random output that is unpredictable without the private key, plus a proof anyone can verify with the public key. In Cardano's Ouroboros protocol, the slot number is the input and a pool's VRF key decides whether it "wins" the right to produce a block, making block-producer selection both random and verifiable. This is covered in depth in [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros). VRFs are not consensus-only machinery either: a smart contract can verify VRF proofs itself, covered in [BLS signatures, VRFs & credentials](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives#verifiable-random-functions) once you reach the smart contract module. ## What makes a curve pairing-friendly? Ed25519 signatures rely on one operation family: multiplying a curve point by a secret number, easy to compute and infeasible to reverse. That is all signing needs, and ordinary curves like Curve25519 or Bitcoin's secp256k1 offer nothing more. A pairing-friendly curve supports a second operation. A **pairing** takes one point from each of two groups on the curve and combines them into a value in a third group, and it is **bilinear**: scaling either input by a secret factor carries through to the output predictably. That single property lets a verifier check that hidden values stand in a claimed relationship, that a hundred signatures collapse into one valid aggregate, or that a random output really came from a specific key, without ever seeing the secrets themselves. **BLS12-381** is the pairing-friendly curve Cardano exposes to smart contracts (since the Chang upgrade). Aggregated signatures, the script-verifiable VRF proofs above, anonymous credentials, and the on-chain proof verification below all come down to a pairing check on this curve; the protocols built from it are collected in [BLS signatures, VRFs & credentials](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives). ## What are zero-knowledge proofs? Every primitive above proves something by *showing* something: a signature shows the public key, a Merkle proof shows the sibling hashes, a hash pre-image shows the secret itself. A **zero-knowledge proof** breaks that pattern: it convinces a verifier that a statement is true while revealing nothing else. The classic example is proving a predicate rather than the data, "this person is over 18" without the birthdate, "this account is solvent" without the balance, "I know the password" without the password. A zero-knowledge proof system guarantees **completeness** (an honest prover can always convince the verifier), **soundness** (a dishonest prover practically never can), and **zero-knowledge** (the verifier learns nothing beyond the statement itself). Since the Chang upgrade, Plutus has built-in operations for the BLS12-381 curve, which let a smart contract verify such proofs on-chain in a single script execution. How that works, and what people are building with it, is covered in [Zero-knowledge proofs](/docs/developers/curriculum/smart-contracts/advanced/zero-knowledge) once you reach the smart contract module. ## How does hashing secure off-chain data? A common pattern stores the hash of large data on-chain while keeping the full data off-chain, giving verifiability without bloating the chain: ``` Off-chain: large file on IPFS On-chain: { document_hash: "7f2d...", ipfs_cid: "Qm..." } Anyone verifies: blake2b_256(downloaded_file) == on_chain_hash ``` Used for NFT metadata (CIP-25 / CIP-68), governance proposals, and audit trails. ## Key takeaways - **Hash functions** (Blake2b) produce unique fixed-size fingerprints; they underpin integrity, transaction IDs, addresses, and the chain itself. - **Merkle trees** enable logarithmic-time verification of a transaction within a block and make light clients possible. - **Digital signatures** (Ed25519) prove a transaction was authorized by the private-key holder; deterministic signing removes a whole class of bugs. - **Together** they create layered security: signatures authorize, Merkle trees organize, hash chains make history immutable. - **Pairing-friendly curves** (BLS12-381) add an operation that checks relationships between secrets without revealing them, the basis for signature aggregation and on-chain proof verification. - **Zero-knowledge proofs** go one step further: they prove a statement is true without revealing the data behind it, and Cardano can verify them on-chain. ## Next steps These primitives secure individual blocks and transactions. But who decides which block comes next, and how do thousands of nodes agree? See [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros). --- ## Cardano Fundamentals This is the conceptual bedrock for building on Cardano. It explains what a blockchain actually is, the cryptography that secures it, how Cardano reaches agreement through Ouroboros, and how the platform differs from account-based chains like Ethereum. You don't need all of it before you start building, but if you are new to Cardano (or new to blockchains), it pays for itself quickly. ## Recommended reading order 1. **[What Is a Blockchain?](/docs/developers/curriculum/fundamentals/what-is-a-blockchain)**: distributed ledgers, blocks, immutability, and the trust problem 2. **[Cardano Architecture](/docs/developers/curriculum/fundamentals/cardano-components)**: the node, the layers, and the components you will interact with 3. **[Cryptographic Primitives](/docs/developers/curriculum/fundamentals/cryptographic-primitives)**: hashing, Merkle trees, and digital signatures 4. **[Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros)**: how the network agrees on one chain, via Proof of Stake 5. **[Core Concepts](/docs/developers/curriculum/fundamentals/core-concepts/overview)**: how Cardano represents and moves value, the eUTXO model, addresses, keys and wallets, transactions, and fees ## Where this leads Everything you build sits on top of these fundamentals. Once they click, [Start Building](/docs/developers/curriculum/start-building/overview) gets you a working environment and your first transaction on a test network. --- ## What Is a Blockchain? A blockchain is a distributed, append-only data structure that lets multiple participants agree on shared state without a central authority. It chains together cryptographically linked blocks of transactions, producing an immutable ledger that no single party controls. This page builds a precise mental model of what a blockchain actually is, why it was invented, and how Cardano's design choices matter to you as a developer. If you have used Git, the shape is familiar: every commit references its parent, history is append-only (no force-push, no rebase), every developer keeps a full clone, and merging happens by protocol (consensus) rather than human decision. The difference that matters is that no participant can rewrite history at all, because thousands of independent nodes enforce the rules instead of one trusted server. ## What problem do blockchains solve? Blockchains solve the trust problem: they let participants who do not trust each other agree on a shared ledger of truth without any single party in control. Traditional systems centralize trust in one entity (a bank, a platform, a server operator), and that centralization introduces vulnerabilities. In traditional web development, trust is centralized. When a user sends money through a banking app, both parties trust the bank to update balances honestly. When you store data in PostgreSQL, your application trusts that the database has not been tampered with. When two services talk over REST, they trust the auth layer (OAuth, JWT) to verify identities. This works well until it does not. Centralized trust introduces: - **Single point of failure**: if the server goes down, nothing happens; if the database is corrupted, data is lost. - **Single point of control**: the managing entity can unilaterally change the rules, freeze an account, or delete data. - **Single point of trust**: users must believe the central authority is honest and competent, indefinitely. The fundamental question is: **can a group of participants who do not trust each other agree on a shared ledger of truth, without any single party having control?** This is not new. Distributed systems researchers studied it for decades as the "Byzantine Generals Problem." What Bitcoin (and later Cardano) achieved was the first practical, large-scale solution. ## What is a ledger? A ledger is an ordered list of records tracking events in sequence, so all parties see the same consistent, durable information. Your bank statement is a ledger. The key properties: 1. **Ordered**: events are recorded in sequence. A happened before B. 2. **Consistent**: everyone sees the same information. 3. **Durable**: once recorded, entries are not lost. In web2, a ledger is a database table maintained by one server (or a primary-replica cluster). The organization running it is the source of truth. A **distributed ledger** is maintained by many independent participants, none with unilateral control; every participant holds a complete copy and follows a protocol to agree on what gets added. ## How do blocks batch transactions? Blocks batch transactions into discrete, cryptographically sealed units so the network can process them efficiently. Each block has a header (metadata plus a hash linking it to the previous block) and a body (the transactions). ``` Block { header: { block_number: 9821453 previous_block_hash: "a4f2c8..." merkle_root: "7b3d1e..." // fingerprint of all transactions block_producer: "pool1abc..." // the stake pool that created this block } body: { transactions: [tx1, tx2, ... tx_n] } } ``` Each block references the **hash of the previous block**. This is the "chain" in blockchain. Alter a transaction in block 100 and its hash changes, so the reference in block 101 no longer matches, which invalidates 101, and so on. Tampering with any historical block breaks the entire chain from that point forward. That is the source of **immutability**: not that altering data is physically impossible, but that it is immediately detectable and would require re-creating every subsequent block, which is economically infeasible. ```mermaid graph LR G["Genesis Blockhash: 0x00"] -->|"hash(Genesis)"| B1["Block 1prev: hash(Genesis)"] B1 -->|"hash(B1)"| B2["Block 2prev: hash(B1)"] B2 -->|"hash(B2)"| B3["Block 3prev: hash(B2)"] B3 -->|"..."| BN["Block Nprev: hash(B(N-1))"] ``` The **genesis block** is the very first block; it has no predecessor. You can open Cardano's on an explorer: [block `5f20df93...`](https://cexplorer.io/block/5f20df933584822601f9e3f8c024eb5eb252fe8cefb24d1317dc3d432e940ebb), dated 23 September 2017 in the Byron era, with an empty parent field and 14,505 transactions handing out the initial 31.1 billion ADA. Every block since links back to it through an unbroken chain of hash references. On mainnet a new block is produced roughly every 20 seconds. ## Who runs a blockchain network? A decentralized network of independent node operators runs a blockchain. On Cardano, anyone can run a node with the `cardano-node` software; there is no registration and no central authority deciding who participates. Nodes serve two roles: 1. **Relay nodes** propagate blocks and transactions across the network. 2. **Block-producing nodes (stake pools)** create new blocks according to the consensus protocol ([Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros)). Thousands of active stake pools, run by independent operators worldwide, produce Cardano's blocks. No single entity, not even IOG (which built Cardano), controls the network. Decentralization is a spectrum, not a binary, and Cardano's reward design specifically incentivizes spreading stake across many pools. It helps to separate two properties that are easy to conflate. A system is **distributed** when many independent machines each hold and replicate the data, and **decentralized** when no single party controls it. These are independent axes: a large web service can be highly distributed across data centers yet remain centrally controlled by one company, while a system can be decentralized in authority without being widely distributed. A blockchain deliberately combines both, which is what makes its ledger hard to destroy and hard to capture. ### Why does decentralization matter for developers? - **Censorship resistance**: no single entity can block a valid transaction. It confirms as long as at least one block producer will include it; only the unanimous refusal of every producer could keep it out, and that agreement would itself be a form of consensus. - **Permissionless deployment**: you can deploy a contract without anyone's approval. - **Guaranteed execution**: once deployed, a contract runs exactly as written; no one can alter it. - **Transparent state**: every participant can verify the entire history. ## What makes blockchain data immutable? Three mechanisms work together: cryptographic hashing (each block contains the previous block's hash), distributed replication (every node holds a complete copy), and the consensus protocol (which makes rewriting history economically irrational). The cryptographic machinery behind this is covered in [Cryptographic Primitives](/docs/developers/curriculum/fundamentals/cryptographic-primitives). For developers this is a mental shift: in web2 you routinely UPDATE and DELETE; in blockchain you only INSERT. Corrections are made by adding new transactions that supersede old ones, never by modifying history. ## What is Byzantine Fault Tolerance? Byzantine Fault Tolerance (BFT) is the ability of a distributed system to function correctly even when some participants are actively malicious, not just offline but deliberately lying. The concept comes from the Byzantine Generals Problem (Lamport, Shostak, Pease, 1982). In blockchain terms: generals are nodes, the battle plan is the next block, and traitors are malicious nodes. Cardano's Ouroboros protocol provides BFT as long as the majority of stake (in ADA) is controlled by honest participants. This is a stronger guarantee than most web2 systems: traditional distributed databases (Raft, Paxos) tolerate crash failures but assume all nodes are honest; blockchains assume some nodes are adversarial. ## What makes Cardano distinct? A few design choices set Cardano apart and directly affect how you build: - **Extended UTXO model**: Cardano tracks individual "coins" (unspent transaction outputs) rather than account balances. This shapes how you design applications. See [the eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo). - **Native tokens**: custom tokens live at the ledger level alongside ADA, not inside smart contracts, so they inherit ADA's security without contract execution for basic transfers. - **On-chain governance**: ADA holders vote on protocol changes (the Voltaire era), a level of decentralized decision-making with no web2 parallel. ## How does data flow through the network? When you submit a transaction, it travels from your wallet through relay nodes, enters a mempool, gets selected by a stake pool for a block, and then propagates to all nodes for validation and permanent storage. The whole process typically takes 20 to 60 seconds. ```mermaid graph LR W["WalletConstruct & Sign Tx"] --> R1["Relay NodeValidate & Propagate"] R1 --> MP["MempoolWaiting Area"] MP --> SP["Stake PoolSlot Leader"] SP --> BK["New Block"] BK --> CH["ChainPermanent Ledger"] CH --> ND["All NodesValidate & Store"] ``` After a few more blocks are added on top, the transaction is considered final with extremely high confidence. (How a pool gets selected to produce that block is the subject of [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros).) ## Common misconceptions **"Blockchain is just a database."** It is a specific data structure with properties (decentralization, immutability, permissionless access) that databases deliberately avoid because they add overhead. Use a database when you trust the operator; use a blockchain when you need trustless coordination. **"Everything should be on the blockchain."** On-chain storage is expensive and slow. Store only what needs to be verifiable and immutable on-chain; use off-chain storage (IPFS, databases) for the rest, with on-chain hashes as anchors. **"Blockchains are anonymous."** Cardano is **pseudonymous**, not anonymous. Transactions and addresses are public; usage patterns can be analyzed. Never assume transactions are private. ## Key takeaways - A blockchain is a distributed, append-only ledger maintained by independent nodes following a shared protocol. - The "chain" comes from cryptographic hash-linking: each block contains the previous block's hash, making tampering detectable. - Decentralization removes single points of failure and control, enabling censorship-resistant, permissionless applications. - Immutability is a feature: auditability and trust without a trusted intermediary. You INSERT, you never UPDATE. - Cardano's eUTXO model, native tokens, and on-chain governance are the design choices that most affect how you build. ## Next steps The guarantees on this page all rest on three cryptographic building blocks: hash functions, Merkle trees, and digital signatures. [Cryptographic Primitives](/docs/developers/curriculum/fundamentals/cryptographic-primitives) covers each one. --- ## Authenticated Products Pairing a physical product with an on-chain NFT lets anyone verify the product is genuine. An [NFT](/docs/developers/curriculum/native-tokens/overview#fungible-non-fungible-and-semi-fungible) is a natural fit: it is unique, permanent, and public, so a physical item linked to one carries a certificate of authenticity nobody can forge without the minting key. Cardano's [POC Hoodie](https://store.cardano.org/products/hoodie), sold through the [Cardano Store](https://store.cardano.org/), is a worked example. An NFC chip knitted into the hoodie links it to an NFT on Cardano, and tapping the chip verifies authenticity. It builds on an earlier [Lacrosse World Cup Jersey](https://cardanofoundation.org/en/news/technical-collaboration-with-epoch-sports-merchandise/) showcase with a stronger security model. The design is a proof of concept, not a finished product. This page walks through how it works, how a holder verifies it, and where it is headed. ## How a holder verifies it For the owner, verification is one tap. The NFC tag opens a website that shows the verification status, with no tools or blockchain knowledge required. That convenience carries a trust assumption: the website is centrally hosted, so a holder who takes its answer at face value is trusting whoever runs it. Anyone who wants to check for themselves can. The URL from the NFC tag carries the NFT's asset name; look that asset up in any [explorer](https://cardano.org/apps/?tags=explorer) and confirm its policy ID is `e886a328333c28bf3e8fc527206b02dc9ff65fb04cf569ec71983330`, the hash of the hoodie collection's minting policy. Every hoodie NFT sits under that one policy ([pool.pm](https://pool.pm/policy/e886a328333c28bf3e8fc527206b02dc9ff65fb04cf569ec71983330)). ## What happens under the hood Tapping the tag hands the phone a URL containing encrypted data that identifies the NFT. Following it lands on the verification website, which forwards the encrypted payload to a validation service. The service decrypts it, reads the asset ID inside, and looks up the matching NFT on Cardano. If it finds one, it compares the [NFT's metadata](https://adastat.net/tokens/e886a328333c28bf3e8fc527206b02dc9ff65fb04cf569ec71983330484f4f44494532) against the data read from the chip. The check rests on [digital signatures](/docs/developers/curriculum/fundamentals/cryptographic-primitives#how-do-digital-signatures-prove-identity): a signature produced when the NFT was minted is verified against the key material carried in the encrypted payload. A match proves the chip and the on-chain asset belong together. ![Verification flow for the POC Hoodie](./img/nft-merch-store-poc.png) ## The chip: shared secret and replay protection The data on the chip is protected with symmetric encryption, so a secret is shared between the chip (written while it is prepared) and the validation service. The chip also holds a counter that increments on every tap. Because the backend sees the counter, it can reject a URL that has already been used, closing off replay attacks where someone copies a valid tap URL and reuses it. :::tip Build it yourself The chip model, the flashing process, and the tooling are in the [Cardano Store POC Hoodies repository](https://github.com/cardano-foundation/cardano-store-poc-hoodies). You need an NFC reader/writer to program the chips. ::: ## Limitations and where it is headed Two things keep this a proof of concept rather than a trust-minimized product. **The validation service is centralized.** Authenticity cannot be confirmed without it, so the trust the blockchain removes at the data layer reappears at the service layer. **The NFTs use [CIP-25](/docs/developers/curriculum/native-tokens/metadata-registry#cip-25-nft-metadata-in-the-minting-transaction).** CIP-25 records metadata in the minting transaction, where a smart contract can neither read nor update it. A [CIP-68](/docs/developers/curriculum/native-tokens/metadata-registry#cip-68-datum-metadata-updatable-on-chain)-style design would let a contract manage ownership instead: the holder keeps a token that points at the contract, and ownership counts only when an inline datum listing the current owners points back to the asset. That turns the physical-to-digital link into a transferable, trustless one. The larger step is the chip itself. [Signing NFC chips](https://www.azuki.com/blog/pbt) can sign a challenge with their own private key, so no secret has to be shared with the backend at all. Removing that last shared secret opens the door to multi-signature ownership transfer, where handing over the physical good and its NFT becomes a signed, on-chain event. ## Next steps - [Programmable tokens](/docs/developers/curriculum/native-tokens/programmable-tokens): where token rules move from the mint to every transfer --- ## Token Metadata & Registry Metadata is what makes a token usable: a name, image, ticker, decimals, or royalty terms that wallets, explorers, and marketplaces read to display it. Cardano organizes this through community standards (CIPs), and which one you use depends on whether the data is static or needs to be updatable and on-chain. ## Metadata labels at a glance | Label | CIP | Purpose | |---|---|---| | `674` | CIP-20 | Transaction messages / comments | | `721` | CIP-25 | NFT metadata (name, image, attributes) | | `777` | CIP-27 | Royalty information | CIP-20 transaction messages are covered under [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions); this page focuses on token metadata. ## CIP-25: NFT metadata in the minting transaction CIP-25 stores metadata in the minting transaction under label `721`. It is the simplest of the standards: the metadata is recorded permanently in the transaction, but it is **not** readable by smart contracts. ```json { "721": { "": { "": { "name": "My NFT", "image": "ipfs://Qm...", "mediaType": "image/png", "description": "A unique digital artwork", "attributes": { "rarity": "legendary" } } } } } ``` Required fields are `name` and `image`; `mediaType`, `description`, and `files` are optional. ## CIP-68: datum metadata (updatable, on-chain) CIP-68 stores metadata in an inline datum on a **reference NFT**, which means it can be updated (by consuming and recreating the reference UTXO) and read on-chain by smart contracts via reference inputs. It splits into two tokens: a reference token (at a script address, holding the metadata) and a user token (in the holder's wallet). ## CIP-25 or CIP-68? | Choose CIP-25 when | Choose CIP-68 when | |---|---| | Metadata is static | Metadata must be updatable | | Simplicity matters | A contract must read the metadata on-chain | | Standard collectibles or art | Dynamic NFTs, evolving game assets | ## CIP-26: the off-chain metadata registry CIP-26 is an **off-chain registry** where projects publish human-readable info for a token, name, ticker, **decimals**, and logo, that wallets and explorers read to display it. The metadata lives in a public GitHub repo, [cardano-foundation/cardano-token-registry](https://github.com/cardano-foundation/cardano-token-registry), and is served over a REST API at `https://tokens.cardano.org`. Registration is **optional** and independent of on-chain activity; your tokens work with or without an entry. The field that matters most is **`decimals`**: on-chain quantities are always integers, so without a registered decimals value a wallet can't know that `1000000` of your token should display as `1.0`. ### CIP-26 (off-chain) or CIP-68 (on-chain)? Both publish token metadata; the difference is where it lives and how it updates: | | CIP-26 (registry) | CIP-68 (on-chain datum) | | --- | --- | --- | | Where metadata lives | Off-chain GitHub registry | On-chain, in a reference NFT datum | | Cost | Free, no on-chain footprint | Extra UTXO and transaction cost | | Updating | New pull request, human-reviewed | An on-chain transaction you control | | Readable by contracts | No | Yes, via reference inputs | | Live after a change | Hours to days (review + re-sync) | Immediately, once on-chain | Reach for **CIP-26** for static metadata on a fungible token (a stablecoin's ticker and decimals): free, simple, widely supported. Reach for **CIP-68** when metadata must change or a contract must read it on-chain. They aren't exclusive, the [Token Metadata Server](/docs/developers/curriculum/native-tokens/token-registry/metadata-server) serves both and falls back per field. To publish an entry, see [Register an entry](/docs/developers/curriculum/native-tokens/token-registry/register-an-entry); to query it, see the [Token Metadata Server](/docs/developers/curriculum/native-tokens/token-registry/metadata-server). ## CIP-27: royalties CIP-27 (label `777`) records a royalty rate and recipient address for an NFT policy, so marketplaces can honor creator royalties. ## Attaching metadata in code ```typescript const tx = await client .newTx() .payToAddress({ address, assets }) .attachMetadata({ label: 721n, metadata: nftMetadata }) // bigint label .build() ``` ```javascript txBuilder.metadataValue(721, metadata) // CIP-25 ``` ```bash cardano-cli latest transaction build ... --metadata-json-file metadata.json ``` The Evolution metadata label is a `bigint` (`721n`), not `721`. ## Next steps - [Mint an NFT](/docs/developers/curriculum/native-tokens/mint-nft): attach CIP-25 metadata while minting - [What are native tokens](/docs/developers/curriculum/native-tokens/overview): fungibility, policy IDs, min-ADA --- ## Mint a Fungible Token A fungible token is a native token minted with a quantity greater than one, where every unit is interchangeable. You define a [minting policy](/docs/developers/curriculum/native-tokens/minting-policies), mint the supply, and the tokens then move through ordinary transactions. Pick your tool below. ## What you'll build - A signature-based minting policy - A supply of one fungible token minted to your own address - (Optional) a burn transaction that destroys some of them ## Prerequisites - Test ADA on Preview or Pre-Production ([faucet](/docs/developers/curriculum/start-building/networks-and-test-ada)) - A provider key (Blockfrost) for the SDK tabs, or a running node for cardano-cli - Min-ADA travels with tokens, keep a little ADA in the output ([why](/docs/developers/curriculum/native-tokens/overview#the-minimum-ada-requirement)) ## Mint it ```typescript const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) declare const mintingPolicy: any // native script or smart contract, see Minting policies const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0" const assetName = "4d79546f6b656e" // "MyToken" in hex let assets = Assets.fromLovelace(0n) assets = Assets.addByHex(assets, policyId, assetName, 1000n) // quantity > 1 const tx = await client .newTx() .mintAssets({ assets, redeemer: Data.constr(0n, []), label: "mint-my-token" }) .attachScript({ script: mintingPolicy }) .build() const signed = await tx.sign() await signed.submit() ``` The builder tracks the policy, indexes redeemers, evaluates execution units, and calculates fees for you. ```javascript const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY!); const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = preprod/preview testnet walletAddressType: AddressType.Base, fetcher: provider, submitter: provider, mnemonic: process.env.WALLET_MNEMONIC!.split(" "), }); const changeAddress = await wallet.getChangeAddressBech32(); const forgingScript = ForgeScript.withOneSignature(changeAddress); const policyId = resolveScriptHash(forgingScript); const tokenName = "MeshToken"; const txBuilder = new MeshTxBuilder({ fetcher: provider }); const unsignedTx = await txBuilder .mint("1000000", policyId, stringToHex(tokenName)) // quantity > 1 .mintingScript(forgingScript) .changeAddress(changeAddress) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete(); const signedTx = await wallet.signTx(unsignedTx); const txHash = await wallet.submitTx(signedTx); ``` `ForgeScript.withOneSignature` derives a signature policy from your address. Full key, address, and node setup is in [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction). The token-specific steps: Signature policy (`policy/policy.script`): ```json { "keyHash": "", "type": "sig" } ``` Get the policy ID, then build, sign, and submit (token name hex-encoded): ```bash cardano-cli latest transaction policyid --script-file policy/policy.script > policy/policyID cardano-cli latest transaction build-raw \ --fee $fee \ --tx-in $txhash#$txix \ --tx-out "$address+$output+$amount $policyid.$tokenname" \ --mint "$amount $policyid.$tokenname" \ --minting-script-file policy/policy.script \ --out-file matx.raw # calculate-min-fee, rebuild with the fee, then: cardano-cli latest transaction sign \ --signing-key-file payment.skey --signing-key-file policy/policy.skey \ --tx-body-file matx.raw --out-file matx.signed cardano-cli latest transaction submit --tx-file matx.signed ``` ## Burn tokens Burning is minting with a negative quantity, authorized by the same policy. ```typescript let burn = Assets.fromLovelace(0n) burn = Assets.addByHex(burn, policyId, assetName, -500n) const tx = await client .newTx() .mintAssets({ assets: burn, redeemer: Data.constr(1n, []), label: "burn-tokens" }) .attachScript({ script: mintingPolicy }) .build() await (await tx.sign()).submit() ``` ```javascript // same imports, provider, wallet, forgingScript, policyId, and tokenName as "Mint it" above const txBuilder = new MeshTxBuilder({ fetcher: provider }); const unsignedTx = await txBuilder .mint("-500", policyId, stringToHex(tokenName)) // negative quantity burns .mintingScript(forgingScript) // same policy that minted .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete(); const signedTx = await wallet.signTx(unsignedTx); const txHash = await wallet.submitTx(signedTx); ``` ```bash cardano-cli latest transaction build-raw \ --tx-in $txhash#$txix \ --tx-out "$address+$output+$remaining $policyid.$tokenname" \ --mint "-500 $policyid.$tokenname" \ --minting-script-file policy/policy.script \ --out-file burn.raw # sign with payment.skey + policy.skey, then submit ``` ## Next steps - [Mint an NFT](/docs/developers/curriculum/native-tokens/mint-nft): quantity 1, plus CIP-25 metadata and a time-lock - [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry): give your token a name, ticker, and decimals - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): lock tokens at a script address to build escrows, swaps, or token sales --- ## Mint an NFT An NFT is just a native token with a quantity of 1, made permanently unique by a minting policy that can only ever run once. The name, image, and description are attached to the minting transaction as CIP-25 metadata (label `721`). This page mints one and sends it to a wallet, pick your tool below. New to policies and what makes a token "non-fungible"? Read [Minting policies](/docs/developers/curriculum/native-tokens/minting-policies) and [What are native tokens](/docs/developers/curriculum/native-tokens/overview) first. This page is the hands-on version. ## What you'll build - A minting policy only you can mint from (time-locked, so the supply is provably fixed) - One NFT (quantity 1) carrying CIP-25 metadata - A transaction that mints it, attaches the metadata, and pays it to a recipient ## Prerequisites - Test ADA on Preview or Pre-Production ([faucet](/docs/developers/curriculum/start-building/networks-and-test-ada)) - A provider key (Blockfrost) for the SDK tabs, or a running node for cardano-cli - An image pinned to IPFS (the `ipfs://...` URI goes in the metadata) :::tip CIP-25 or CIP-68? **CIP-25** stores metadata in the minting transaction (label 721). Simplest, and what this page uses. **CIP-68** stores metadata in an on-chain datum that a smart contract can read and update later. Choose CIP-68 only if your NFT's metadata needs to change or be read on-chain. See [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry). ::: ## Mint it ```typescript Address, Assets, NativeScripts, Bytes, TransactionMetadatum, preprod, Client } from "@evolution-sdk/evolution" const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY!, }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) const myKeyHash = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de") const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash) const nativeScript = new NativeScripts.NativeScript({ script: mintingPolicy }) const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de" const assetName = "4d794e4654303031" // "MyNFT001" in hex let mintAssets = Assets.fromLovelace(0n) mintAssets = Assets.addByHex(mintAssets, policyId, assetName, 1n) let sendAssets = Assets.fromLovelace(2_000_000n) // min ADA travels with the NFT sendAssets = Assets.addByHex(sendAssets, policyId, assetName, 1n) const nftMetadata = new Map([ [policyId, new Map([ [assetName, new Map([ ["name", "My First NFT"], ["image", "ipfs://QmYourImageHashHere"], ["mediaType", "image/png"], ["description", "Minted with Evolution SDK"], ])] ])] ]) const tx = await client .newTx() .mintAssets({ assets: mintAssets }) .attachScript({ script: nativeScript }) .attachMetadata({ label: 721n, metadata: nftMetadata }) // 721n, bigint .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: sendAssets }) .build() const signed = await tx.sign() const txHash = await signed.submit() ``` The builder handles fees, coin selection, and change. `mintAssets` with quantity `1n` is what makes it non-fungible; `attachMetadata` under `721n` is the CIP-25 standard. ```javascript const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY!); const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = preprod/preview testnet walletAddressType: AddressType.Base, fetcher: provider, submitter: provider, mnemonic: process.env.WALLET_MNEMONIC!.split(" "), }); const changeAddress = await wallet.getChangeAddressBech32(); const forgingScript = ForgeScript.withOneSignature(changeAddress); const demoAssetMetadata = { name: "Mesh Token", image: "ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua", mediaType: "image/jpg", description: "This NFT was minted by Mesh (https://meshjs.dev/).", }; const policyId = resolveScriptHash(forgingScript); const tokenName = "MeshToken"; const metadata = { [policyId]: { [tokenName]: { ...demoAssetMetadata } } }; const txBuilder = new MeshTxBuilder({ fetcher: provider }); const unsignedTx = await txBuilder .mint("1", policyId, stringToHex(tokenName)) .mintingScript(forgingScript) .metadataValue(721, metadata) // CIP-25 .changeAddress(changeAddress) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete(); const signedTx = await wallet.signTx(unsignedTx); const txHash = await wallet.submitTx(signedTx); ``` `ForgeScript.withOneSignature` derives the policy from your address; `.mint("1", ...)` sets quantity 1. The cardano-cli path is the most manual. Full key/address setup is in [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction); the NFT-specific parts are the time-locked policy, the metadata file, and the build flags. Time-locked policy (`policy/policy.script`): ```json { "type": "all", "scripts": [ { "type": "before", "slot": 90000000 }, { "type": "sig", "keyHash": "" } ] } ``` Set the `before` slot to a real future slot: the current slot plus a buffer (for example `+ 10000`). A past slot like `0` would make the policy immediately unmintable. CIP-25 metadata (`metadata.json`): ```json { "721": { "": { "NFT1": { "name": "Cardano NFT guide token", "description": "My first NFT", "image": "ipfs://" } } } } ``` Build, sign, and submit (set `--testnet-magic 1|2` or `--mainnet`): ```bash cardano-cli latest transaction build \ --tx-in $txhash#$txix \ --tx-out "$address+1500000+1 $policyid.$tokenname" \ --change-address $address \ --mint "1 $policyid.$tokenname" \ --minting-script-file policy/policy.script \ --metadata-json-file metadata.json \ --invalid-hereafter $slot \ --out-file matx.raw cardano-cli latest transaction sign \ --signing-key-file payment.skey --signing-key-file policy/policy.skey \ --tx-body-file matx.raw --out-file matx.signed cardano-cli latest transaction submit --tx-file matx.signed ``` ## Make it a true one-of-one An NFT derives value from guaranteed scarcity. A **time-locked policy** (the `before` slot above, or a time-lock on the native script in the SDK tabs) means no more tokens can ever be minted under that policy once the deadline passes, enforced at the protocol level. Buyers can verify it by inspecting the policy. See [Validity intervals](/docs/developers/curriculum/fundamentals/core-concepts/transactions#validity-intervals-and-time). ## Updatable metadata: CIP-68 CIP-25 writes the metadata into the minting transaction, where it is permanent and readable only off-chain. **[CIP-68](https://cips.cardano.org/cip/CIP-68)** instead stores it in an **inline datum on a reference token**, so it can be updated later and read on-chain by smart contracts through reference inputs. Each asset becomes a pair: a **reference token** (asset-name label `100`) held at a script address carrying the metadata datum, and a **user token** (label `222`) that lives in the holder's wallet. For when to choose it over CIP-25, see [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry#cip-68-datum-metadata-updatable-on-chain). Minting both tokens in one transaction needs a Plutus minting policy and an always-succeed reference-token holder (see [Smart contracts](/docs/developers/curriculum/smart-contracts/overview)). Both SDKs ship CIP-68 helpers: ```typescript const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) // Metadata lives on the reference token as a typed CIP-68 datum const metadata = Data.map([ [Text.toBytes("name"), Text.toBytes("CIP-68 Token")], [Text.toBytes("image"), Text.toBytes("ipfs://QmYourImageHashHere")], ]) const referenceDatum: CIP68Metadata.CIP68Datum = { metadata, version: 1n, extra: [] } // Asset names carry the CIP-67 label prefix: (100) reference, (222) user const name = Text.toBytes("MyCIP68Token") const refNameHex = Bytes.toHex(new Uint8Array([0x00, 0x0f, 0x42, 0x00, ...name])) const userNameHex = Bytes.toHex(new Uint8Array([0x00, 0x0f, 0x42, 0x02, ...name])) // Your compiled minting policy and the always-succeed script address holding the reference token declare const mintingScript: any declare const policyId: string const scriptAddress = Address.fromBech32("addr_test1...") let mintAssets = Assets.fromLovelace(0n) mintAssets = Assets.addByHex(mintAssets, policyId, refNameHex, 1n) mintAssets = Assets.addByHex(mintAssets, policyId, userNameHex, 1n) let refOutput = Assets.fromLovelace(2_000_000n) refOutput = Assets.addByHex(refOutput, policyId, refNameHex, 1n) const tx = await client .newTx() .mintAssets({ assets: mintAssets, redeemer: Data.constr(0n, []) }) .attachScript({ script: mintingScript }) // reference token (100) -> script address, metadata as its inline datum (the user token goes to change) .payToAddress({ address: scriptAddress, assets: refOutput, datum: new InlineDatum.InlineDatum({ data: CIP68Metadata.Codec.toData(referenceDatum) }), }) .build() const signed = await tx.sign() const txHash = await signed.submit() ``` ```typescript MeshTxBuilder, BlockfrostProvider, resolveScriptHash, stringToHex, mConStr0, mTxOutRef, applyParamsToScript, serializePlutusScript, metadataToCip68, CIP68_100, CIP68_222, } from "@meshsdk/core"; const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY!); const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, walletAddressType: AddressType.Base, fetcher: provider, submitter: provider, mnemonic: process.env.WALLET_MNEMONIC!.split(" "), }); const txBuilder = new MeshTxBuilder({ fetcher: provider }); const utxos = await wallet.getUtxosMesh(); const collateral = (await wallet.getCollateralMesh())[0]; const changeAddress = await wallet.getChangeAddressBech32(); // Your compiled Plutus scripts (see Smart contracts): an always-succeed holder // for the reference token, and a one-time minting policy. const alwaysSucceedCbor = "..."; // PlutusScript V1 CBOR const oneTimeMintingPolicyCbor = "..."; // parameterized minting policy CBOR const userTokenMetadata = { name: "CIP-68 Token", image: "ipfs://QmYourImageHashHere", mediaType: "image/png", description: "A CIP-68 token with updatable, on-chain metadata", }; const { address: scriptAddress } = serializePlutusScript({ code: alwaysSucceedCbor, version: "V1" }); // Parameterize the policy by the UTXO it consumes, so it can only ever run once const scriptCode = applyParamsToScript(oneTimeMintingPolicyCbor, [ mTxOutRef(utxos[0].input.txHash, utxos[0].input.outputIndex), ]); const policyId = resolveScriptHash(scriptCode, "V2"); const tokenNameHex = stringToHex("MyCIP68Token"); const unsignedTx = await txBuilder .txIn(utxos[0].input.txHash, utxos[0].input.outputIndex, utxos[0].output.amount, utxos[0].output.address) // reference token (label 100) -> script address, metadata stored as its datum .mintPlutusScriptV2().mint("1", policyId, CIP68_100(tokenNameHex)).mintingScript(scriptCode).mintRedeemerValue(mConStr0([])) // user token (label 222) -> the holder's wallet .mintPlutusScriptV2().mint("1", policyId, CIP68_222(tokenNameHex)).mintingScript(scriptCode).mintRedeemerValue(mConStr0([])) .txOut(scriptAddress, [{ unit: policyId + CIP68_100(tokenNameHex), quantity: "1" }]) .txOutInlineDatumValue(metadataToCip68(userTokenMetadata)) .changeAddress(changeAddress) .selectUtxosFrom(utxos) .txInCollateral(collateral.input.txHash, collateral.input.outputIndex, collateral.output.amount, collateral.output.address) .complete(); const signedTx = await wallet.signTx(unsignedTx, true); const txHash = await wallet.submitTx(signedTx); ``` Mesh's `metadataToCip68` / `CIP68_100` / `CIP68_222` helpers and Evolution's typed `CIP68Metadata` schema reach the same result by different routes (helper functions versus a typed codec): encode the metadata as the reference token's datum and apply the CIP-67 label prefixes. To **update** the metadata later, spend the reference UTXO and recreate it with a new datum. ## Royalties: CIP-27 A royalty is recorded as a **single token** (empty asset name) under metadata label **`777`**, carrying a rate and a recipient address, minted once under the **same policy** as the NFTs it covers. Marketplaces that honor [CIP-27](https://cips.cardano.org/cip/CIP-27) read label 777 to route a cut of secondary sales to the creator. Evolution has no royalty-specific helper, so you attach the CIP-27 structure as plain metadata under label `777n`: ```typescript // reuse the client and your single-signature native policy from above const royaltyMetadata = new Map([ ["rate", "0.05"], // 5% ["addr", "addr_test1qz..."], // royalty recipient ]) let royaltyToken = Assets.fromLovelace(0n) royaltyToken = Assets.addByHex(royaltyToken, policyId, "", 1n) // empty asset name const tx = await client .newTx() .mintAssets({ assets: royaltyToken }) .attachScript({ script: nativeScript }) .attachMetadata({ label: 777n, metadata: royaltyMetadata }) .build() const signed = await tx.sign() const txHash = await signed.submit() ``` Mesh ships a typed `RoyaltiesStandard` helper for the 777 structure: ```typescript const txBuilder = new MeshTxBuilder({ fetcher: provider }); // same provider + wallet as above const address = (await wallet.getUsedAddressesBech32())[0]; const forgingScript = ForgeScript.withOneSignature(address); const policyId = resolveScriptHash(forgingScript); const royaltyMetadata: RoyaltiesStandard = { rate: "0.05", // 5% address: "addr_test1qz...", // royalty recipient }; const unsignedTx = await txBuilder .mint("1", policyId, "") // empty asset name = the policy's royalty token .mintingScript(forgingScript) .metadataValue(777, royaltyMetadata) .changeAddress(address) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete(); const signedTx = await wallet.signTx(unsignedTx); const txHash = await wallet.submitTx(signedTx); ``` ## Common pitfalls | Problem | Cause | Fix | |---|---|---| | NFT not showing in wallet | metadata structure mismatch | policy ID and asset name in metadata must exactly match the minted token | | "Minting not allowed" | wrong key signed | the signing key's hash must match the policy | | Type error on label (Evolution) | `721` instead of `721n` | use the bigint `721n` | | Min UTxO too low | not enough ADA with the NFT | include 2 ADA in the NFT output, comfortably above the floor | ## Next steps - [Mint a fungible token](/docs/developers/curriculum/native-tokens/mint-fungible): the same flow with quantity greater than 1 - [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry): CIP-25 vs CIP-68, royalties (CIP-27) - Advanced: the smart contract [one-shot NFT policy](/docs/developers/curriculum/smart-contracts/write-a-validator#one-shot-policies) for protocol-guaranteed uniqueness - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): lock your NFT at a script address for sales, swaps, or escrow --- ## Minting Policies A minting policy is the rule set that controls who can mint or burn a token, when, and how many. The policy is a script; its hash becomes the token's [Policy ID](/docs/developers/curriculum/native-tokens/overview#how-tokens-are-identified), so the rules are permanently bound to the token's identity. ## How a policy runs Whenever a transaction's `mint` field includes tokens under a policy, that policy script runs and must return true. Unlike a spending validator, a minting policy receives two arguments and no datum: ``` minting_policy(redeemer, scriptContext) -> Bool ``` Positive quantities mint, negative quantities burn. The policy can allow one and forbid the other, or apply different rules to each. ## Native script policies (no smart contract needed) The simplest policies use Cardano's native script language, just signatures and time: - **Signature-based**: only the holder of key X can mint. ```json { "type": "sig", "keyHash": "" } ``` - **Time-locked**: minting only allowed before (or after) a slot. ```json { "type": "all", "scripts": [ { "type": "before", "slot": 1000000 }, { "type": "sig", "keyHash": "" } ] } ``` Time-locks matter because once the window closes, **no one** can ever mint more under that policy, a provably fixed supply. This is the standard way to lock an NFT or a capped collection. ## Smart contract policies When you need logic beyond signatures and time, the policy is a smart contract (a validator written in Plutus or Aiken): - **One-shot**: requires a specific UTXO as input. Since a UTXO can be spent only once, the policy can succeed only once in history, the canonical way to guarantee true NFT uniqueness. - **Parameterized**: compile-time parameters bake into the script, producing a distinct policy ID per configuration. - **Multi-action**: the redeemer selects an action (mint / burn / init) and the script validates each differently. Writing smart contract minting policies, including the [one-shot pattern](/docs/developers/curriculum/smart-contracts/write-a-validator#one-shot-policies) for provable uniqueness, is covered in [Write a validator](/docs/developers/curriculum/smart-contracts/write-a-validator). ## Native script or smart contract? | Use a native script when | Use a smart contract when | |---|---| | Fixed issuer and/or fixed supply by deadline | Uniqueness must be guaranteed by protocol (one-shot) | | Simple multisig issuance | Minting depends on on-chain state (oracles, other UTXOs) | | You want zero script-execution cost | You need multiple actions or parameterized families | ## Key takeaways - The policy script's hash is the policy ID; rules are bound to the token forever. - Native scripts cover signatures and time-locks, including provably fixed supply. - Smart contract policies add one-shot uniqueness, parameterization, and multi-action logic. ## Next steps - [Mint a fungible token](/docs/developers/curriculum/native-tokens/mint-fungible): a native signature policy in practice - [Mint an NFT](/docs/developers/curriculum/native-tokens/mint-nft): a time-locked policy plus CIP-25 metadata --- ## What Are Native Tokens You can build and submit transactions; this module puts assets of your own inside them. A Cardano native token is a custom asset the ledger tracks directly, alongside ADA, without a smart contract for basic transfers. Where Ethereum needs an ERC-20 or ERC-721 contract per token, Cardano treats your token the same way it treats ADA: it lives in UTXOs and moves through ordinary transactions. :::note Quick summary Tokens are "native" because the protocol tracks them with the same UTXO machinery that tracks ADA. That means lower fees, no contract-execution risk on transfers, atomic multi-asset transactions, and the same ledger-level guarantees ADA has. ::: ## What makes a token "native"? On Ethereum, creating a token means deploying a contract that holds its own ledger of who owns what; every transfer is a contract call that costs gas and can fail in contract logic. On Cardano, the ledger itself tracks your token. Sending it works exactly like sending ADA, through inputs and outputs, with no contract execution for a basic transfer. This builds directly on the [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo): each UTXO can hold ADA plus any number of native tokens bundled together. ## How tokens are identified Every native token is identified by two parts: - **Policy ID**: the 28-byte (56 hex character) hash of the minting policy script that authorized the token. It groups all tokens minted under that policy, and because it is a script hash, the minting rules are permanently bound to the token's identity. - **Asset Name**: an optional label (up to 32 bytes) distinguishing tokens within a policy. One name for a fungible token; a unique name per item in an NFT collection. It can be empty. Together they form a globally unique **Asset ID**: `PolicyID.AssetName`. Two tokens are fungible only if they share the complete Asset ID; the same asset name under different policies is not the same token. ### Why ADA is special ADA is the only token with no policy ID. It is the protocol's **base currency**, used for fees, rewards, deposits, and minimum-UTXO values. In the value structure it sits under an empty policy ID and empty asset name. ADA is denominated in **lovelace**: 1 ADA = 1,000,000 lovelace (named after Ada Lovelace, like Ethereum's wei). ## Token bundles A UTXO's value is not a single number, it is a nested map, `Map>`, so one UTXO can carry ADA plus many tokens at once: ``` Output value: 2.5 ADA + 1,000 HOSKY + 50 SUNDAE + 1 SpaceBudz #4567 (NFT) ``` This is the **token bundle** (multi-asset value). It makes transfers atomic (send many token types in one output) and storage-efficient, at the cost of more complex coin selection (the wallet must balance every asset, not just ADA). ## Fungible, non-fungible, and semi-fungible The only on-chain difference is quantity: - **Fungible token**: quantity greater than 1, all units interchangeable (currencies, utility tokens, stablecoins). - **NFT**: quantity exactly 1, made unique by a one-time minting policy (art, collectibles, certificates). - **Semi-fungible**: a set of identical units that are distinct as a category (for example, 100 event tickets). For the full mechanics see [Minting policies](/docs/developers/curriculum/native-tokens/minting-policies). ## The minimum ADA requirement Every token-bearing UTXO must carry a minimum amount of ADA, scaling with the output's byte size. This prevents dust UTXOs from bloating the ledger. Rough estimates: ``` ADA-only output: ~1.0 ADA 1 token (1 policy, 1 name): ~1.2 ADA 1 NFT with inline datum: ~1.5-2.0 ADA many tokens in one output: ~3-5+ ADA ``` These are floors, not the amounts you should send. The exact figure comes from `coinsPerUtxoByte` (a governance-controlled protocol parameter) multiplied by the serialized output size, so it shifts with the output's contents and can change by governance action. Production code usually sends a round 2 ADA with a token rather than computing the floor: it clears every case above, the surplus stays spendable by the recipient, and it removes a class of failed transactions. The examples on this page do exactly that. Practical consequences: - **You cannot send "just a token."** Some ADA always travels with it. - **Airdrop cost**: budget what you will actually send, not the floor. 10,000 recipients at 2 ADA each is ~20,000 ADA locked in min-UTXO (held by recipients); at the ~1.2 floor it would be ~12,000. - **Consolidation**: combining many small token UTXOs into one frees the excess ADA. :::tip This page is the canonical reference for min-ADA, token bundles, and fungibility. Other pages link here instead of re-explaining. ::: ## Working with assets in code In the SDKs, a token amount is an **asset bundle**: lovelace plus zero or more native tokens keyed by `policyId + assetNameHex`. The `Assets` module builds and combines bundles: ```typescript // ADA only (1 ADA = 1,000,000 lovelace) const ada = Assets.fromLovelace(5_000_000n) // ADA + a native token: addByHex(bundle, policyId, assetNameHex, quantity) let bundle = Assets.fromLovelace(2_000_000n) // min-ADA travels with the token bundle = Assets.addByHex(bundle, policyId, assetNameHex, 100n) // Combine bundles const total = Assets.merge(ada, bundle) ``` `policyId` is 56 hex chars (28 bytes); `assetNameHex` is the hex-encoded asset name (empty `""` is valid). The builder enforces [min-ADA](#the-minimum-ada-requirement) on every token-bearing output automatically. Mesh keys a bundle by `unit` (`policyId + assetNameHex`, or `"lovelace"`) with string quantities. `MeshValue` builds and combines them: ```typescript // ADA only (string lovelace; 1 ADA = 1,000,000 lovelace) const ada = MeshValue.fromAssets([{ unit: "lovelace", quantity: "5000000" }]) // ADA + a native token: unit is policyId + assetNameHex const bundle = MeshValue.fromAssets([{ unit: "lovelace", quantity: "2000000" }]) // min-ADA travels with the token bundle.addAsset({ unit: policyId + assetNameHex, quantity: "100" }) // Combine bundles const total = ada.merge(bundle) // Back to the `{ unit, quantity }[]` the builder consumes const assets = total.toAssets() ``` `assetNameHex` is the hex-encoded asset name (empty `""` is valid). The builder enforces [min-ADA](#the-minimum-ada-requirement) on every token-bearing output automatically. For display, **CIP-14 asset fingerprints** give a short checksummed `asset1...` identifier derived from the policy ID + asset name. Use it in UIs, but the canonical on-chain identifier stays `policyId + assetName`. ## Native tokens vs Ethereum ERC-20/721 On Ethereum, a token is a smart contract, so every transfer is a contract call that costs gas and can revert in the contract's logic. On Cardano, a token is part of the ledger. Minting and burning are governed by a policy script, but once minted, tokens move in ordinary transactions with no script execution, so a plain transfer cannot fail in contract logic the way an ERC-20 transfer can. One transaction can carry many token types at once, fungible and non-fungible tokens use the same mechanism, and you pay the normal transaction fee rather than variable gas. The flip side is that there is no built-in transfer logic. Behavior like blacklists or transfer fees requires locking the tokens at a script address, and at that point spending them is subject to validation just like an ERC-20 transfer. There are also no built-in decimals: on-chain quantities are integers, so a 6-decimal token is minted in micro-units and displayed with decimals via a CIP-26 registration. ## Token lifecycle Native tokens move through: policy design, then policy creation (the script hash becomes the policy ID), then minting (positive quantity), then circulation (ordinary transfers, no scripts), then optional smart-contract interaction, and finally burning (negative quantity). ![Native token lifecycle](./img/multiasset-lifecycle.png) ## Key takeaways - Tokens are native: same ledger as ADA, no contract for basic transfers, so they are cheaper and safer. - Every token is `PolicyID.AssetName`; the policy ID is the minting script's hash, binding rules to identity. - A single UTXO holds ADA plus any number of tokens (the token bundle). - Every token-bearing UTXO carries min-ADA; it scales with size and shapes airdrop and consolidation design. ## In this module - [Minting policies](/docs/developers/curriculum/native-tokens/minting-policies): the rules that control creation and burning - [Mint a fungible token](/docs/developers/curriculum/native-tokens/mint-fungible): your first token, minted end to end - [Mint an NFT](/docs/developers/curriculum/native-tokens/mint-nft): one-shot policies and NFT metadata - [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry): how metadata works across CIP-25, CIP-68, and CIP-26 - [Register an entry](/docs/developers/curriculum/native-tokens/token-registry/register-an-entry): submit your token to the Cardano Token Registry - [Token metadata server](/docs/developers/curriculum/native-tokens/token-registry/metadata-server): query registry metadata from your application - [Authenticated products](/docs/developers/curriculum/native-tokens/authenticated-products): a physical-goods case study with NFC chips - [Programmable tokens](/docs/developers/curriculum/native-tokens/programmable-tokens): where token rules move from the mint to every transfer ## Next steps - Start with [Minting policies](/docs/developers/curriculum/native-tokens/minting-policies); the mint pages build directly on it - The module ends by handing over to [Staking & Governance](/docs/developers/curriculum/staking-governance/overview) --- ## Programmable Tokens A plain [native token](/docs/developers/curriculum/native-tokens/overview) has no built-in transfer logic. Once minted, it moves through ordinary transactions with no script execution, which is exactly what makes it cheap and safe, but it also means you cannot enforce rules on who holds it or how it moves. **Programmable tokens** ([CIP-113](https://github.com/cardano-foundation/CIPs/pull/444)) add that missing layer: validation logic that runs on every transfer, mint, and burn. ## The idea A [minting policy](/docs/developers/curriculum/native-tokens/minting-policies) gives you control at mint and burn, but nothing in between. Programmable tokens extend that control to circulation: every movement passes through a validator, so an issuer can encode rules the ledger then enforces on their behalf. The model works without changing the protocol: - **Shared script custody.** All programmable tokens of a given standard are locked at a shared smart-contract address rather than sitting free in user wallets. Every transfer is a spend from that script, so the validation logic always runs. - **Ownership by stake credential.** Even though the tokens live at a script address, ownership is tracked by stake credential, so standard wallets still control them and the normal send and receive experience is preserved. - **An on-chain registry.** A registry records which tokens are programmable and which rules apply, so wallets, indexers, and dApps can discover and respect the logic. - **Substandards.** Specific behaviors, such as freeze and seize for a regulated asset, are defined as substandards on top of the shared base framework, so issuers pick the policy they need instead of re-implementing the plumbing. ## Why it matters Transfer-time validation is what regulated assets need and what plain native tokens cannot offer on their own: - **Stablecoins and tokenized securities** that must support freeze, seize, or allowlist controls. - **Real-world assets (RWAs)** that carry compliance obligations like sanctions screening or KYC/AML gating. - **Any asset** whose issuer must be able to enforce rules after issuance, not just at mint. Because it builds on Cardano's existing native-asset and scripting machinery, none of this requires a hard fork. ## The trade-off Programmability has a cost, and it is the exact thing plain native tokens avoid. Once transfers run through a validator, you are back to script execution on every movement, with the fees and validation surface that come with it. That is the point of the design, not a flaw, but it means programmable tokens are for assets that genuinely need enforced rules, not a default for every token. For a token that only needs control at mint and burn, a plain [minting policy](/docs/developers/curriculum/native-tokens/minting-policies) is still the right tool. :::info In active development CIP-113 is still a draft ([PR #444](https://github.com/cardano-foundation/CIPs/pull/444)) and the specification may change. The Cardano Foundation's reference implementation has **not been professionally audited** and has only been briefly tested on the Preview testnet, so it is **not production-ready**. Track progress, read the architecture and integration guides, and follow the contracts at the [cip113-programmable-tokens repository](https://github.com/cardano-foundation/cip113-programmable-tokens). This page will be expanded as the standard matures. ::: ## Next steps - [Staking & Governance](/docs/developers/curriculum/staking-governance/overview): the next module, how delegation, rewards, and on-chain governance work, and how to build against them - [Minting policies](/docs/developers/curriculum/native-tokens/minting-policies): the control layer plain native tokens do have, at mint and burn --- ## Token Metadata Server The **Token Metadata Server** is the read API for token metadata. Wallets, explorers, and dApps query it to turn an on-chain asset ID into a human-readable name, ticker, decimals, and logo. API v2 serves both **CIP-26** (the off-chain [registry](/docs/developers/curriculum/native-tokens/metadata-registry)) and **CIP-68** (on-chain datum) metadata through one interface, so a caller does not need to know which standard a token uses. The API is at `https://tokens.cardano.org` and is subject to the [API Terms of Use](https://github.com/cardano-foundation/cardano-token-registry/blob/master/API_Terms_of_Use.md). Full OpenAPI specifications are at [tokens.cardano.org/apidocs](https://tokens.cardano.org/apidocs). ## Network environments The Cardano Foundation runs the server for both mainnet and the preprod testnet, so you can resolve metadata while building and testing before going live. | Network | Base URL | OpenAPI docs | | --- | --- | --- | | Mainnet | `https://tokens.cardano.org` | [`/apidocs`](https://tokens.cardano.org/apidocs) | | Preprod | `https://preprod.tokens.cardano.org` | [`/apidocs`](https://preprod.tokens.cardano.org/apidocs) | Both expose the same v2 surface and combine the two standards the same way: - **CIP-68 (on-chain)** comes directly from the chain of that network. The preprod instance reads CIP-68 reference NFTs minted on preprod. - **CIP-26 (off-chain)** comes from the [cardano-token-registry](https://github.com/cardano-foundation/cardano-token-registry) on mainnet, and from the [IOHK metadata-registry-testnet](https://github.com/input-output-hk/metadata-registry-testnet) on preprod. To query preprod, use the requests below with the preprod base URL. ## How priority and fallback work When a token has metadata under both standards, the server returns one value per field and tells you its `source`. By default it prefers **CIP-68** (on-chain) for its decentralization, and falls back to CIP-26 field by field when CIP-68 is missing a value. Override this with the `query_priority` parameter. ## Fetch one subject `GET /api/v2/subjects/{subject}` `subject` is the asset identifier: the hex `policyId` concatenated with the hex asset name, for example `577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454`. Query parameters: - `property` (optional, repeatable): limit the response to specific fields, such as `name`, `ticker`, `decimals`. Omit to return all. - `query_priority` (optional, `CIP_26` or `CIP_68`): which standard to prefer. Defaults to CIP-68 with fallback to CIP-26. - `show_cips_details` (optional, default `false`): include the raw per-standard payloads. ```console curl https://tokens.cardano.org/api/v2/subjects/577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454 | jq . ``` ```json { "subject": { "subject": "577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454", "metadata": { "name": { "value": "FLDT", "source": "CIP_68" }, "description": { "value": "The official token of FluidTokens.", "source": "CIP_68" }, "ticker": { "value": "FLDT", "source": "CIP_68" }, "decimals": { "value": 6, "source": "CIP_68" }, "logo": { "value": "https://fluidtokens.com/fldt.png", "source": "CIP_68" }, "url": { "value": "https://fluidtokens.com", "source": "CIP_26" }, "version": { "value": 1, "source": "CIP_68" } } }, "queryPriority": ["CIP_68", "CIP_26"] } ``` Here CIP-68 has priority, but because it carries no `url`, that one field is served from CIP-26. ### Fetch specific properties ```console curl "https://tokens.cardano.org/api/v2/subjects/577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454?property=name&property=ticker&property=decimals" | jq . ``` ### Force a single standard ```console curl "https://tokens.cardano.org/api/v2/subjects/577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454?query_priority=CIP_68" | jq . ``` ### Inspect the raw per-standard payloads `show_cips_details=true` returns the original CIP-26 and CIP-68 records side by side, including signatures. Useful when debugging a registration or comparing the two standards' raw formats. ```console curl "https://tokens.cardano.org/api/v2/subjects/577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454?show_cips_details=true" | jq . ``` ## Fetch many subjects at once `POST /api/v2/subjects/query` returns metadata for multiple subjects in one call. It takes the same `query_priority` and `show_cips_details` parameters, plus a `subjects` array and an optional `properties` array in the body. ```console curl -H 'Content-Type: application/json' \ -d '{"subjects": ["577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454", "a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235484f534b59"], "properties": ["name", "ticker"]}' \ https://tokens.cardano.org/api/v2/subjects/query | jq . ``` ```json { "subjects": [ { "subject": "577f0b1342f8f8f4aed3388b80a8535812950c7a892495c0ecdf0f1e0014df10464c4454", "metadata": { "name": { "value": "FLDT", "source": "CIP_68" }, "ticker": { "value": "FLDT", "source": "CIP_68" } } }, { "subject": "a0028f350aaabe0545fdcb56b039bfb08e4bb4d8c4d7c3c7d481c235484f534b59", "metadata": { "name": { "value": "HOSKY Token", "source": "CIP_26" }, "ticker": { "value": "HOSKY", "source": "CIP_26" } } } ], "queryPriority": ["CIP_68", "CIP_26"] } ``` The second token has no CIP-68 record, so its fields fall back to CIP-26. ## Next steps - [Register an entry](/docs/developers/curriculum/native-tokens/token-registry/register-an-entry): publish the CIP-26 metadata this server returns - [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry): how CIP-25, CIP-26, CIP-27, and CIP-68 fit together --- ## Register an entry This page covers the full lifecycle of a **CIP-26** registry entry: preparing it with `token-metadata-creator`, submitting it as a pull request, and later updating or removing it. For what the registry is and when to use it instead of CIP-68, see [Token metadata & registry](/docs/developers/curriculum/native-tokens/metadata-registry). ## Before you start You should already have minted a native asset (see [Mint a fungible token](/docs/developers/curriculum/native-tokens/mint-fungible)) and have: - The **policy ID** and **asset name** of your token. - The **monetary policy script** (native script) or the **Plutus script** that hashes to that policy ID. - The **signing key(s)** that control the policy. - The `token-metadata-creator` tool, from [offchain-metadata-tools](https://github.com/input-output-hk/offchain-metadata-tools). ## Entry fields Each entry is a single JSON file describing one asset: | Field | Required | Description | | --- | --- | --- | | `subject` | Required | The base16 `policyId` concatenated with the base16 asset name, all lowercase. If the asset name is empty, the subject is just the policy ID. | | `name` | Required | Human-readable name shown in interfaces. | | `description` | Required | Human-readable description. | | `policy` | Optional | The CBOR of the monetary policy script, used to verify ownership. Optional for Plutus scripts, where verification uses your trusted keys instead. | | `ticker` | Optional | Short ticker symbol. | | `url` | Optional | An HTTPS URL related to the token. | | `logo` | Optional | A PNG logo encoded as a byte string. | | `decimals` | Optional | How many decimal places wallets should display. Omitted means zero. | Only `subject` is unique, because it is derived from the on-chain asset ID. Names and tickers are **not** validated for collisions, so do not rely on them being one of a kind. For the full field reference and signing options, see [offchain-metadata-tools](https://github.com/input-output-hk/offchain-metadata-tools). ## Prepare your entry The example below uses `policyId = baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f` and `assetName = "myassetname"`. ### 1. Generate the subject Base16-encode the asset name and concatenate it onto the policy ID: ```console $ echo -n "myassetname" | xxd -ps 6d7961737365746e616d65 ``` `baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f6d7961737365746e616d65` The subject string must be all lowercase. For a **Plutus** policy, get the policy ID with `cardano-cli transaction policyid --script-file `. ### 2. Initialize a draft ```console token-metadata-creator entry --init baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f6d7961737365746e616d65 ``` This writes a draft JSON file named after your subject. ### 3. Add the required fields ```console token-metadata-creator entry baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f6d7961737365746e616d65 \ --name "My Gaming Token" \ --description "A currency for the Metaverse." \ --policy policy.json ``` `policy.json` is the monetary policy script that hashes to your policy ID. Omit `--policy` for a Plutus-script policy. ### 4. Add optional fields ```console token-metadata-creator entry baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f6d7961737365746e616d65 \ --ticker "TKN" \ --url "https://example.com" \ --logo "icon.png" \ --decimals 4 ``` ### 5. Sign Sign with the key(s) that control your policy: the policy signing key for a native script, or the trusted key(s) you manage for a Plutus script. A single key signs all fields at once here. See [offchain-metadata-tools](https://github.com/input-output-hk/offchain-metadata-tools) for multi-key options. ```console token-metadata-creator entry baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f6d7961737365746e616d65 -a policy.skey ``` ### 6. Finalize ```console token-metadata-creator entry baa836fef09cb35e180fce4b55ded152907af1e2c840ed5218776f2f6d7961737365746e616d65 --finalize ``` This runs validations and produces the finalized file, ready to submit. ## Submit your entry Submissions are pull requests against [cardano-foundation/cardano-token-registry](https://github.com/cardano-foundation/cardano-token-registry), and must follow these rules: 1. A single commit off the **master** branch of the registry. 2. Add or modify exactly one file in the [`mappings/`](https://github.com/cardano-foundation/cardano-token-registry/tree/master/mappings) folder. Split multiple mappings across multiple pull requests. 3. The file name must equal the entry's `subject`, all lowercase. 4. A single entry may be at most 370 KB. Then fork the repository, add your finalized file, and open a pull request: ```console $ git clone git@github.com:/cardano-token-registry $ cd cardano-token-registry $ cp /path-to/baa83...d65.json mappings/ $ git add mappings/baa83...d65.json $ git commit -m "My Gaming Token" $ git push origin HEAD ``` [Open a pull request from your fork.](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) Once the automated checks pass and a maintainer reviews it, the entry is merged subject to the [Registry Terms of Use](https://github.com/cardano-foundation/cardano-token-registry/blob/master/Registry_Terms_of_Use.md). It can take a few hours after merge before the API serves the entry. ## Update an entry Updating reuses the same flow with one critical addition: **increment the `sequenceNumber`** of every field you change, before signing. The signature covers the sequence number, so it must be bumped first or the update is rejected. 1. Start from your existing draft, or recreate it with `token-metadata-creator`. 2. Edit the values you want to change. 3. Increment `sequenceNumber` on each changed field. 4. Re-sign and finalize. 5. Open a new pull request. A field that has been updated once looks like this in the finalized file (note `sequenceNumber` is now `1`): ```json "description": { "signatures": [ { "publicKey": "04a72e...", "signature": "50e867..." } ], "sequenceNumber": 1, "value": "My new description." } ``` ## Remove an entry There is no delete command. To retract an entry you update it, setting `name` and `description` to `VOID`, and title the pull request to make the deletion request explicit. The verification is the same as any update (you sign with the policy key), which is what stops anyone from deleting someone else's entry. After the maintainers verify it, they remove the file. ```console token-metadata-creator entry --name "VOID" --description "VOID" token-metadata-creator entry -a policy.skey token-metadata-creator entry --finalize ``` Increment the `sequenceNumber` on `name` and `description` as with any update. ## Troubleshooting **Are names and tickers unique?** No. Only `subject` is unique, because it is the on-chain asset ID. Names and tickers are not validated for collisions. **My asset is on a testnet.** The Cardano Token Registry is for **mainnet** assets only. For preview or preprod assets, register with the [IOHK metadata-registry-testnet](https://github.com/input-output-hk/metadata-registry-testnet), and query the preprod metadata server at `https://preprod.tokens.cardano.org` (see [Token Metadata Server](/docs/developers/curriculum/native-tokens/token-registry/metadata-server)). **My pull request was closed.** Pull requests that fail the automated checks are closed after a while. Open the **Checks** tab and read the failing test for the reason. A pull request can also be rejected even when checks pass if it breaks the [Registry Terms of Use](https://github.com/cardano-foundation/cardano-token-registry/blob/master/Registry_Terms_of_Use.md). For questions about a submission, contact tokenregistry@cardanofoundation.org. **My pull request has not merged yet.** Review is done by humans, oldest first, and well-formed pull requests that pass every check are processed first. Make sure your checks are green (a red mark links to the failing detail) and be patient. ## Next steps - [Token metadata server](/docs/developers/curriculum/native-tokens/token-registry/metadata-server): query the entry you just registered from your application --- ## Connecting to the Chain Every application does exactly two things against the chain: **read state** (UTXOs, balances, datums, protocol parameters) and **submit transactions**. Every piece of infrastructure on this page exists to serve those two workflows. The tools differ in *where the work runs* and *whom you trust to run it*, not in what they ultimately do. You have used both workflows since Module 2, always through a **provider** ([Query the chain](/docs/developers/curriculum/start-building/query-the-chain), [Transaction building](/docs/developers/curriculum/start-building/transaction-building)). This page is the map behind that word: what actually serves your SDK, what the categories of infrastructure are, and which of them are genuine alternatives to each other. Two practice pages then set them up: [Use a provider](/docs/developers/curriculum/production/use-a-provider) for the hosted path, [Self-hosting](/docs/developers/curriculum/production/self-hosting) for running your own. If you have run web infrastructure, the categories map onto things you know: - A **query API** (Blockfrost, Koios, Maestro) is a managed database service, the Cardano equivalent of RDS or Supabase: trade some control for zero operations. - **cardano-node** is running your own database server: full control, full operational commitment (storage, updates, monitoring). - **Ogmios** is a database driver: it translates the node's wire protocol into a developer-friendly interface, like `pg` for PostgreSQL. - **Indexers** are materialized views and read replicas: db-sync is a full materialized view of the chain; Kupo is a targeted one for the UTXOs you care about. - A **data node** (Dolos) is a single-binary read replica: one process that syncs and serves. - A **managed platform** (Demeter) is someone else hosting your components: the stack you would self-host, run as a service. ## A provider is an interface, not just a service ```mermaid graph LR App[Your app] --> SDK[SDK] SDK --> |API call| Provider[Provider] Provider --> Node[cardano-node] Provider --> Indexer[Indexer] Provider --> API[API layer] Node --> |syncs| Blockchain[Cardano chain] Node --> |chain-sync| Indexer API --> |serves data| SDK style App fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style SDK fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style Provider fill:#FFFFFF,stroke:#0033AD,stroke-width:3px,color:#000000 style Node fill:#FFFFFF,stroke:#0033AD,stroke-width:1px,color:#000000 style Indexer fill:#FFFFFF,stroke:#0033AD,stroke-width:1px,color:#000000 style API fill:#FFFFFF,stroke:#0033AD,stroke-width:1px,color:#000000 style Blockchain fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF ``` On the service side, a provider runs a node (which syncs the chain), an indexer (which makes the node's data queryable), and an API layer that serves it to your SDK over HTTP or WebSocket. Everything below is a different answer to who runs those three boxes. Inside your SDK, a provider is also an **interface**: a small contract the SDK calls, with two halves. - A **read** side: fetch a UTXO set, protocol parameters, and account or asset info (Mesh names this `IFetcher`; Evolution exposes the same reads through its provider). - A **write** side: submit a signed transaction (Mesh's `ISubmitter`). Because the SDK only depends on that contract, anything that implements it is a valid provider: a hosted service, your own stack, a private indexer, even an in-memory fixture. Two pages already use this: the [failover and cache wrapper](/docs/developers/curriculum/production/going-to-production#harden-your-provider) that composes several providers behind one interface, and the [in-memory emulator](/docs/developers/curriculum/start-building/local-testing#the-in-memory-emulator) that serves a simulated ledger to a test suite with no network at all. ## The categories The landscape looks crowded until you sort it by what each thing conceptually is. Six categories cover all of it, and only some of them are alternatives to each other. ### Query APIs **Blockfrost, Koios, Maestro.** A complete data service behind one HTTP API: someone else runs the node, the indexer, and the API layer, and you get a base URL and usually a key. They are operated commercially (Blockfrost, Maestro) or by a community cluster with no single operator (Koios, which you can also self-host). This is the category most applications start with, and many never need anything else. [Use a provider](/docs/developers/curriculum/production/use-a-provider) sets up all three with one skeleton, and [Builder Tools](/tools/?tags=api) has the rest, including narrower services built for one kind of data. ### Node interfaces **Ogmios, cardano-submit-api.** Translators for a node you already run, not data services. A cardano-node speaks its own binary [wire protocol](/docs/developers/curriculum/production/network-protocol), so two small bridges translate it: **Ogmios** exposes the node's mini-protocols as WebSocket JSON (stream blocks, submit transactions, query state, watch the mempool), and **cardano-submit-api** accepts a serialized transaction over plain HTTP and hands it to the node. On their own they serve nothing: without a node there is no data to translate. An SDK typically pairs Ogmios with the indexer Kupo to get both halves of the provider contract, a combination known as the **Kupmios** stack. ```text Transaction submission options: cardano-cli local, CLI: cardano-cli transaction submit --tx-file signed.tx cardano-submit-api local, HTTP: POST /api/submit/tx (application/cbor) Ogmios local, WebSocket: { "method": "submitTransaction", ... } Blockfrost/Koios/... remote, HTTP: POST /tx/submit (application/cbor) ``` ### Indexers **db-sync, Kupo, Oura, Adder, Yaci Store.** The node holds the whole chain, but not in a form an application can query: there is no "UTXOs at this address" lookup inside cardano-node. An **indexer** follows the chain and stores what it sees in a queryable shape. The shapes differ, and picking by shape is the whole game: a full SQL copy (db-sync), a filtered UTXO set (Kupo), an event stream (Oura, Adder), or modular per-table stores (Yaci Store). What they share is the hard part, which is that the chain can take blocks back, so an index has to be able to unwind. [Custom indexing & analytics](/docs/developers/curriculum/production/indexing-and-analytics) covers the shapes, that problem, and builds one out. ### The full node **cardano-node.** The foundation everything above sits on: it validates every block and transaction itself, maintains the UTXO set, and is the only category that trusts nobody. The cost is operational: a machine that stays synced and running, with real storage and memory requirements. Run one when you cannot afford to trust a third party, when you operate an indexer that needs a local node, or when you [operate a stake pool](/docs/operators/); most application developers never need to. ### Data nodes **Dolos.** The classic self-hosted read stack runs three components and stores the ledger twice (once in the node, once in the database). A **data node** collapses node, indexer, and API layer into one lightweight process with embedded storage, built for exactly one job: serving chain data to applications. ```mermaid flowchart LR subgraph classic["Full-node stack"] NET1[Cardano network] -->|"trustlessvalidation"| NODE[cardano-node] NODE -->|unix socket| SYNC[db-sync] SYNC --> PG[(PostgreSQL)] PG -->|SQL| APP1[Your app] end subgraph datanode["Data node"] NET2[Cardano network] -->|"trustedrelay"| DOLOS[Dolosembedded storage] DOLOS -->|"HTTP / gRPC"| APP2[Your app] end style NET1 fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style NET2 fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style NODE fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style SYNC fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style PG fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style DOLOS fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style APP1 fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style APP2 fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` The trade is explicit: a data node syncs from relay nodes you configure and trusts them, rather than validating trustlessly, and it cannot produce blocks. In exchange, a few GB of RAM and one copy of the ledger serve your SDK a Blockfrost-compatible API from your own machine. ### Managed platforms **Demeter.** A managed platform hosts the *self-hosted* categories for you: node access, indexers (db-sync, Kupo), and node interfaces (Ogmios, submit API) provisioned as cloud services. You get the architecture of the self-hosted stack without operating it, at the price of reintroducing an operator you trust. ## What is not comparable to what The most common confusion in this landscape is comparing across categories. Three cases come up constantly: - **Ogmios is not an alternative to Blockfrost or Koios.** A query API is a complete data service; Ogmios is a protocol translator that is inert without a node behind it. The real comparison is between complete shapes: *hosted query API* versus *your node + Ogmios + Kupo*. - **Dolos is not an indexer.** It does not sit next to Kupo or db-sync in a stack; it replaces the node + indexer + API combination for read-and-submit workloads. Compare a data node to that whole stack, not to any single component. - **Demeter is not another query API.** It does not offer one unified chain API of its own; it hosts the components of the self-hosted shape. Compare it to running db-sync, Kupo, and Ogmios yourself, not to Blockfrost. Koios is the one product that legitimately appears twice: used through the community cluster it is a query API; deployed from the [guild-operators stack](https://cardano-community.github.io/guild-operators/) it is a self-hosted stack serving the same API. ## Choosing: the axes Only complete shapes are alternatives, and four axes separate them: **what you operate**, **whom you trust**, **what it can do**, and **what it costs**. | Shape | You operate | Trust model | Ops burden | Cost shape | | --- | --- | --- | --- | --- | | Hosted query API | Nothing | The operator's data, uptime, and rate limits | None | Free tier, then subscription | | Managed platform | Configuration only | The platform operator | Minimal | Usage-based | | Data node | One process | The relays it syncs from | Low: one binary, a few GB of RAM | Your server | | Node + interface + indexer | Several processes and a database | Nobody: trustless validation | High: sync, storage, monitoring | Your servers | Two axes deserve a sentence each. **Trust** is about data integrity and privacy both: a hosted API sees every address you query and every transaction you submit, along with your IP. **Capability** mostly follows the indexer you (or your operator) run: full history needs a full-copy indexer wherever it lives; a filtered UTXO set is enough for a dApp backend. ## Common stacks | Use case | Stack | Why | |---|---|---| | Hobby / learning | Hosted query API free tier + Preview | Zero infrastructure to manage, fast iteration | | Production dApp backend | Kupo + Ogmios + cardano-node, or a hosted query API + Preprod staging | Fast UTXO queries at your script addresses; full control or managed reliability | | Self-hosted backend, minimal ops | Data node | One process serving a local Blockfrost-compatible API | | Block explorer / analytics | db-sync + PostgreSQL + node | Full historical chain data in SQL | | Event-driven app | Oura + Kafka/Redis + a query API | React to on-chain events in near real time | | Enterprise / high-throughput | Dedicated managed infrastructure, or multiple nodes + custom indexing | SLAs and throughput beyond shared free tiers | Most teams combine approaches: a hosted query API while developing, self-hosted or dedicated infrastructure in production, with the [provider interface](#a-provider-is-an-interface-not-just-a-service) making the switch a configuration change. ## Next steps - [Use a provider](/docs/developers/curriculum/production/use-a-provider): set up Blockfrost, Koios, or Maestro, one skeleton for all three - [Self-hosting](/docs/developers/curriculum/production/self-hosting): run a data node, a Kupmios stack, or a full node, with Demeter as the managed variant - [Custom indexing & analytics](/docs/developers/curriculum/production/indexing-and-analytics): the indexer shapes, and building an index of your own --- ## Going to Production Working on a testnet is not the same as being production-ready. Mainnet has real value, real users, and **irreversible** transactions. This page is a checklist for the jump: each item links to the canonical guide for that concern, so treat it as a map rather than a tutorial. ## 1. Test thoroughly - **On-chain validators**: your validators are pure functions, so test them exhaustively with mock transactions. See [Testing](/docs/developers/curriculum/smart-contracts/testing), and use the fuzzer for property-based coverage ([Optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization)). - **Off-chain code**: test transaction building and submission too, with an in-memory emulator or devnet integration tests ([Local testing](/docs/developers/curriculum/start-building/local-testing)). - **Rehearse on Preprod**: Preprod mirrors mainnet (same protocol parameters and epoch length). Do a full dry run of your user flow there before mainnet. See [Choose a network](/docs/developers/curriculum/start-building/networks-and-test-ada). Mainnet transactions cannot be reversed, so the burn-in happens here. ### Testnets are your staging environments Mirror the staging progression you already use in web2, with the faucet as your Stripe test mode: valueless ADA to exercise real flows. ```mermaid graph LR Dev[Local devnet\nseconds to iterate] --> Preview[Preview testnet\nnew features first, 1-day epochs] Preview --> Preprod[Preprod testnet\nproduction mirror, 5-day epochs] Preprod --> Mainnet[Mainnet\nreal ADA, real users] style Dev fill:#9E9E9E,color:#fff style Preview fill:#FF9800,color:#fff style Preprod fill:#2196F3,color:#fff style Mainnet fill:#4CAF50,color:#fff ``` For the fastest loop, run a [local devnet](/docs/developers/curriculum/start-building/local-testing#local-devnets). **Preview** receives protocol upgrades first (1-day epochs), best for testing new features. **Preprod** is the final rehearsal above. Get test ADA and explorer links from [Choose a network](/docs/developers/curriculum/start-building/networks-and-test-ada). ## 2. Secure it - **Guard the vulnerability classes**: datum hijacking, double satisfaction, token forgery, resource exhaustion. See [Smart contract security](/docs/developers/curriculum/smart-contracts/security), and sharpen your eye on the [CTF](/docs/developers/curriculum/smart-contracts/security/ctf). - **Get an audit**: for any contract holding meaningful value, a professional audit is standard practice before mainnet. Testing finds the bugs you thought of; audits find the ones you didn't. See [Audits](/docs/developers/curriculum/smart-contracts/security#audits) for the process and how to prepare. - **Keep keys and secrets safe**: the frontend should only sign; build and submit on a backend ([frontend signs, backend submits](/docs/developers/curriculum/dapps/connect-a-wallet#frontend-signs-backend-builds-and-submits)). Never ship provider API keys in client-side code. Review [key & wallet security](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys#working-with-wallets-in-code). For backends that custody keys controlling real value, the operator cold-key playbook applies too: see the [secure transaction workflow](/docs/operators/security/secure-workflow) (build online, sign offline, submit online) and an [air-gapped environment](/docs/operators/security/air-gap). ## 3. Make transactions reliable The most common production failure mode is a transaction rejected because an input was already spent or an indexer lagged. - **Retry safely**: structure build → sign → submit so retries re-read chain state instead of replaying a stale UTxO. See [resilient submission](/docs/developers/curriculum/start-building/transaction-building#resilient-submission-retry-safe). - **Chain multi-step flows**: build dependent transactions up front without waiting for confirmation between steps. See [transaction chaining](/docs/developers/curriculum/production/transaction-chaining) for the concept and [chaining transactions](/docs/developers/curriculum/start-building/transaction-building#chaining-transactions) for the code. - **Handle errors structurally**: distinguish recoverable (stale input, provider hiccup) from terminal (insufficient funds) failures. See [Error handling](https://github.com/IntersectMBO/evolution-sdk). ### Harden your provider A single managed API is a single point of failure, and chatty code can hit its rate limits. Two patterns fix this, and both rest on the same idea: a **provider is a pluggable data source behind a common interface**, so you can stack or swap providers without touching transaction-building code. - **Failover**: try the next provider when one errors, so a single outage doesn't take you down. - **Caching**: memoize slow-changing reads (protocol parameters, asset metadata) for a short window to cut redundant calls. How you get there differs by SDK: one ships failover as configuration, the other gives you a small interface to assemble it yourself. Evolution has failover **built in**. Wrap your providers in a `MultiProvider` with a **priority** strategy (try them in order) or **round-robin** (spread load), and it switches automatically on a provider error, accumulating the failures for debugging: ```typescript // priority: try provider 1, fall through to 2 on error const strategy = { type: "priority", providers: [ { provider: blockfrost, priority: 1 }, { provider: koios, priority: 2 }, ], } // or spread requests evenly: { type: "round-robin", providers: [...] } ``` See the [Evolution provider docs](https://intersectmbo.github.io/evolution-sdk/docs/providers/) for wiring `MultiProvider` into a client. Pointing at your own indexer or node beyond the four built-in providers is an advanced, internal path. Mesh has no built-in multi-provider, but its providers are a public `IFetcher` / `ISubmitter` interface, so failover and a cache are a few lines you write once and reuse. The same interface lets you point at a private indexer, node, or GraphQL source: ```typescript // Failover: an IFetcher that falls through to the next provider on error class ResilientProvider implements IFetcher { constructor(private providers: IFetcher[]) {} async fetchAddressUTxOs(address: string, asset?: string) { for (const p of this.providers) { try { return await p.fetchAddressUTxOs(address, asset); } catch { /* next */ } } throw new Error("All providers failed"); } // wrap the remaining IFetcher methods (and add a TTL cache) the same way } const txBuilder = new MeshTxBuilder({ fetcher: new ResilientProvider([blockfrost, koios]) }); ``` For the full walkthrough, see Mesh's [custom provider](https://meshjs.dev/guides/custom-provider) and [production deployment](https://meshjs.dev/guides/production-deployment) guides. ## 4. Optimize - **On-chain cost (ExUnits)**: smaller, faster validators mean lower fees and more headroom under the per-transaction and per-block limits. See [Optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization) and the [execution-cost model](/docs/developers/curriculum/smart-contracts/choose-a-language#what-you-pay-for-execution-costs). - **Off-chain efficiency**: coin selection and change management affect transaction size and UTxO fragmentation. See [Performance](https://github.com/IntersectMBO/evolution-sdk). ## 5. Choose your infrastructure Decide how your dApp will read and submit to the chain: a hosted query API (fastest to ship) or infrastructure you run (most control). [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain) maps the options and the axes to decide by; [use a provider](/docs/developers/curriculum/production/use-a-provider) and [self-hosting](/docs/developers/curriculum/production/self-hosting) set up whichever you choose. ## 6. Smooth the on-ramp Production also means users who may not have a wallet or any ADA. Lower the barrier: - **Wallet-as-a-Service**: let users create a non-custodial wallet with social login ([connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet#no-browser-extension-wallet-as-a-service)). - **Transaction sponsorship**: pay fees on behalf of users so they can transact before holding ADA ([sponsorship](https://docs.utxos.dev/sponsor)). ## Checklist - [ ] Validators and off-chain code covered by tests; full flow rehearsed on Preprod - [ ] Security reviewed; audit done for value-bearing contracts - [ ] Frontend signs only; provider keys server-side - [ ] Transactions are retry-safe; errors handled by category - [ ] On-chain and off-chain paths optimized within limits - [ ] Infrastructure chosen (managed vs self-hosted) and load-appropriate - [ ] Onboarding path decided (browser wallet, WaaS, sponsorship) ## Next steps - [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain): pick your stack with the full map in view - [Ship to Production overview](/docs/developers/curriculum/production/overview): the Scale arc, if production load needs Hydra or batching --- ## Hydra: Layer 2 Scaling Hydra is a Layer 2 scaling solution for Cardano that enables near-instant, low-cost transactions between participants. It operates as a **state channel**: a temporary off-chain ledger where a known set of parties transact as fast as their network connection allows while keeping the security guarantees of the Cardano main chain (Layer 1). Inside a Hydra Head, transactions use the same format as Cardano Layer 1. **Fees are configurable down to zero**, confirmation is instant (limited only by network latency between participants), and all parties must agree on every state transition. If you have put a Redis cache in front of a Postgres database, the model is familiar: Layer 1 is the durable source of record, and the Head is the fast, temporary layer shared by a known set of participants. Depositing funds loads state into that fast layer, the participants transact there with no per-operation cost, and fanout flushes the agreed final state back to Layer 1, with the contestation period acting as a grace window to catch a disagreement before it finalizes. The trade-off is the same as a cache cluster: you pay a Layer 1 cost to open and close the Head, but everything inside is fast and free. ## The security model A Head runs on **unanimous consensus**. Every confirmed Layer 2 transaction produces a new **snapshot** of the Head's state, and a snapshot only counts once every participant has signed it. That is a much stronger requirement than the majority or two-thirds thresholds of Layer 1 consensus protocols, and it buys a correspondingly strong guarantee: your funds cannot move without your signature, and the [Hydra Head paper](https://eprint.iacr.org/2020/299.pdf) proves the protocol secure as long as even one participant is honest. The price of unanimity is **liveness**: the Head only makes progress while all participants are online and cooperating. If someone disappears or refuses to sign, the Head stalls, and the remedy is Layer 1. Anyone can **close** the Head with the latest signed snapshot; a **contestation period** follows (configurable, 12 hours by default) during which any participant can contest with a newer snapshot, each contest extending the deadline. When it expires, the final state fans out to Layer 1. So the worst case in an uncooperative Head is not lost funds, it is waiting out the contestation window to get them back on Layer 1. Two key pairs per participant make this work: **Cardano keys** sign the Layer 1 boundary transactions (open, deposit, close, fanout), and separate **Hydra keys** sign snapshots inside the Head. ## How a Hydra Head works A Hydra Head is a state channel with a defined lifecycle: 1. **Initialize**: a participant posts an init transaction on Layer 1, and the Head opens directly, empty if you like. 2. **Deposit and decommit**: funds move *into* the running Head through Layer 1 deposit transactions, and back *out* through decommits, at any time while it stays open. A deposit that is not picked up by the Head is recoverable, so funds cannot strand between layers. 3. **Transact**: process transactions instantly off-chain, each confirmed one becoming a snapshot signed by all parties. 4. **Close**: any participant submits the latest snapshot to Layer 1, starting the contestation period. 5. **Fanout**: after contestation passes, distribute funds on Layer 1 according to the final state; large UTXO sets fan out in multiple steps. ```mermaid graph LR A[Idle] -->|Init| B[Open] B -->|L2 transactions| B B -->|Deposit / decommit| B B -->|Close| C[Closed] C -->|Contestation period| D[ReadyToFanout] D -->|"Fanout (one or more steps)"| E[Finalized] style B fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style A fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style C fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style D fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style E fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` Because funds flow in and out of a running Head, there is no reason to treat Heads as short-lived: a Head can stay open indefinitely, and in practice you close one only when the group is done or a breaking node upgrade requires it. :::info What changed in Hydra 2.0 Earlier versions had a commit phase: after init, the Head waited in an "initializing" state until every participant had committed funds, and only then opened. Hydra 2.0 (2026) removed that phase, an initialized Head opens immediately, and deposits became the only way funds enter. You will still meet the older flow in existing tutorials and SDKs, including the walkthrough below. Fanout in steps arrived in Hydra 2.2, removing the earlier limit on how many UTXOs a Head could hold at close. ::: ## When to use Hydra Hydra is ideal for: - **High-frequency transactions**: gaming, micropayments, real-time applications. - **Cost-sensitive applications**: batch many transactions off-chain; only pay L1 fees to open and close. - **Private transactions**: keep details off-chain until settlement. - **Interactive multi-party protocols**: rapid state updates among a known group. It is not a fit for open, anonymous, low-frequency interactions: a Head is among a **fixed, known set of participants**, and every participant must sign every snapshot. Membership is static, whoever should be in the Head must be decided before it opens. ## Choose a topology Who runs the hydra-nodes is a design decision, and it determines the trust model: - **Direct**: the transacting parties run their own nodes and hold both key pairs themselves. This gives the full protocol guarantees with no intermediaries, and fits small known groups doing heavy flows between themselves: settlement between trading desks, recurring business-to-business payments, machine-to-machine and [AI agent payments](/docs/developers/curriculum/dapps/ai-agents/masumi). - **Delegated**: a set of operators runs the nodes, and users interact with the Head through the application. The useful design insight: most applications already have points of trust, a prediction market has resolvers who settle outcomes, an exchange has an operator matching orders. If the parties you already trust are the ones running the Head, the Head adds speed and zero fees without adding any *new* trust, and one honest operator still keeps the whole set in check. - **Managed**: a service provisions and operates Head infrastructure for you, so you integrate an API instead of running nodes, with users keeping their own keys. One honest note on privacy: what happens inside a Head stays off-chain, only the final state settles to Layer 1, and that is genuinely useful when a month of business flows should not be public. But it is operational privacy, not cryptographic privacy, in a delegated setup the operators see everything, and any participant can publish Head data. When auditability is the requirement rather than privacy, that is a feature: operators can publish snapshots deliberately, or auditors can simply be given a seat in the Head. ## Beyond one head One Head does not have to carry everything. Heads are cheap to run in parallel, so applications shard load across many of them, and total throughput grows with the number of Heads rather than being capped by one. Heads can also interoperate: a participant who sits in two Heads can move funds between them with hash time-locked contracts, without either Head closing, the same pattern payment channel networks use for multi-hop payments. A public walkthrough lives at [eutxo-l2-interop](https://github.com/cardano-scaling/eutxo-l2-interop). ## Hydra in production The pattern at scale, from deployments you can inspect: - **Glacier Drop**: the largest Hydra deployment to date, [Midnight's token claims](https://midnight.network/blog/hydra-heads-overview) for roughly 34 million eligible addresses were validated inside Hydra Heads run by independent operators, with only consolidated outcomes settling to Layer 1. - **[Hydra Doom](https://github.com/cardano-scaling/hydra-doom)**: 1993's Doom with every game frame as a Head transaction; the December 2024 tournament peaked around a million transactions per second in aggregate across many parallel Heads. - **[DeltaDeFi](https://www.deltadefi.io/)**: a spot exchange executing orders inside a Head for exchange-grade speed, with self-custodial settlement on Layer 1. - **[Masumi](/docs/developers/curriculum/dapps/ai-agents/masumi)**: agent-to-agent payments over Hydra, AI agents transacting at machine frequency for fractions of a cent. - **[Intersect voting](https://hydra-voting.intersectmbo.org)**: DRep polling on a Hydra-based voting system, a reminder that Heads also fit information processing, not just value transfer. ## End-to-end flow with MeshJS The off-chain flow uses `@meshsdk/hydra`. The condensed happy path is below; for setting up the hydra-node pair it talks to, see the [Hydra documentation](https://hydra.family/head-protocol/docs/getting-started). :::info Version note This walkthrough follows the commit-phase flow of hydra-node 1.x. On Hydra 2.x the Head opens directly after `init()` and funds enter through deposits; the Mesh APIs are largely the same, but the head-status events differ. ::: ### Prerequisites - A synced `cardano-node` with `cardano-cli` (preprod), and the `hydra-node` binary ([install](https://hydra.family/head-protocol/docs/installation)). - Test ADA per participant ([faucet](/docs/developers/curriculum/start-building/networks-and-test-ada#get-test-ada)), for L1 node fees and funds to commit. - Each participant generates **Cardano keys** (L1 identity/fees) and **Hydra keys** (snapshot signing), then starts a `hydra-node` peered with the others. Inside the Head, protocol parameters set all fee fields to zero. ### Connect, initialize, commit ```ts const blockfrost = new BlockfrostProvider("YOUR_BLOCKFROST_KEY"); const hydraProvider = new HydraProvider({ httpUrl: "http://localhost:4001" }); const instance = new HydraInstance({ provider: hydraProvider, fetcher: blockfrost, submitter: blockfrost }); await hydraProvider.connect(); await hydraProvider.init(); // any participant opens the Head -> "HeadIsInitializing" // during Initializing, each participant commits a UTxO (or commitEmpty()) const commitTx = await instance.commitFunds(utxo.input.txHash, utxo.input.outputIndex); const signedCommit = await wallet.signTx(commitTx, true, false); // partial sign await wallet.submitTx(signedCommit); // -> "HeadIsOpen" once all commit ``` ### Transact on Layer 2 Once the Head is open, build with `MeshTxBuilder` using `isHydra: true` and the Head's (zero-fee) protocol parameters. `submitTx` goes to the Head, not Layer 1: ```ts const pp = await hydraProvider.fetchProtocolParameters(); const l2Utxos = await hydraProvider.fetchAddressUTxOs(aliceAddress); const txBuilder = new MeshTxBuilder({ fetcher: hydraProvider, submitter: hydraProvider, isHydra: true, params: pp }); const unsignedTx = await txBuilder .txOut(bobAddress, [{ unit: "lovelace", quantity: "5000000" }]) .changeAddress(aliceAddress) .selectUtxosFrom(l2Utxos) .setNetwork("preprod") .complete(); const signedTx = await wallet.signTx(unsignedTx, false); await hydraProvider.submitTx(signedTx); // instant, zero-fee; emits "TxValid" / "SnapshotConfirmed" ``` Submit as many transactions as you need; each confirmed one updates the shared state via a new signed snapshot. ### Close and fanout ```ts await hydraProvider.close(); // posts the latest snapshot to L1, starts the contestation period // on "ReadyToFanout": await hydraProvider.fanout(); // distributes final balances back to L1 -> "HeadIsFinalized" ``` `close()` posts the final state on-chain and opens a contestation window (any participant can dispute with a newer snapshot). After it passes, `fanout()` returns funds to their Layer 1 addresses. ## Next steps - [Hydra protocol docs](https://hydra.family/head-protocol/) and [MeshJS Hydra](https://meshjs.dev/hydra): the full protocol and SDK reference Hydra closes the curriculum. From here the paths lead outward: - [Operate a Stake Pool](/docs/operators/): running Cardano infrastructure as a discipline of its own - [Templates](/templates): start your next project from a runnable starter - [Developer community](/docs/community/cardano-developer-community): where Cardano developers ask, answer, and ship --- ## Custom Indexing and Chain Analytics Most applications never need their own indexer. A [hosted provider](/docs/developers/curriculum/production/use-a-provider) runs one for you, and [self-hosting](/docs/developers/curriculum/production/self-hosting) gets you a private copy of the same thing. You end up here when neither answers your question: - You want one narrow slice of the chain in a database you control, say every transaction carrying a given metadata label, without storing the other 99% of it. - You want to **react** to something on-chain rather than poll for it. - You want to ask questions across the chain's *full history* without keeping infrastructure alive to answer them. An [indexer](/docs/developers/curriculum/production/connecting-to-the-chain#indexers) is what makes any of those possible. It follows the chain and stores what it sees in a form your application can query, because the node itself has no "UTXOs at this address" lookup. The landscape sorts by the **shape** of what gets stored, and picking by shape is the whole game: - **Full copy**: [cardano-db-sync](https://github.com/IntersectMBO/cardano-db-sync) writes the whole chain into PostgreSQL for full historical SQL. Heavy, hundreds of GB and growing, with an initial sync measured in days, and the backbone of explorers and analytics platforms. - **Filtered UTXO set**: [Kupo](https://cardanosolutions.github.io/kupo/) tracks only UTXOs matching patterns you configure (by address, policy ID, and more), with fast sync and low resource use. Ideal for dApp backends, and the indexer half of the [Kupmios stack](/docs/developers/curriculum/production/self-hosting). - **Event stream**: a pipeline that tails the chain and forwards each block and transaction somewhere else, a queue, a webhook, a file, without keeping a queryable copy itself. [Oura](https://docs.txpipe.io/oura/v2) and [Adder](https://docs.blinklabs.io/guides/adder/001-adder/) are the two to look at. - **Modular stores**: per-table stores you enable selectively, with filtering hooked into the pipeline; this is Yaci Store, the rest of this page. Browse the full set in [Builder Tools](/tools/?tags=indexer), which shelves indexers, data nodes and pipelines together because they answer one question: what do I self-host to get queryable data. This page draws a finer line and treats a [data node](/docs/developers/curriculum/production/connecting-to-the-chain#data-nodes) as its own category. ## What the indexer handles, and what stays yours Those four shapes split on one question: **does it keep state?** A full copy, a filtered UTXO set and modular stores all persist something you query later, so the problems below are theirs and your reads stay consistent. An **event stream** persists nothing. It emits and forgets, which is what makes it cheap and immediate, and it hands you a rollback signal and nothing else. Choose a stream and they all become yours. So does anything you derive on top of a stateful indexer, your own running totals included. **The chain takes blocks back.** When your peer switches to a longer fork, blocks you already applied are no longer on the chain, and anything you derived from them is wrong. [Settlement is probabilistic](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros#how-does-finality-work), so near the tip this is ordinary behavior rather than an exception, and the [rollback arrives as a point](/docs/developers/curriculum/production/network-protocol#addressing-a-block-chain-points) rather than a list of things to undo. Three rules turn that into a single delete: tag every row with the slot that produced it, never update a row in place, and undo by deleting everything above the rollback point. Running totals are the trap, because a balance held as one mutable number no longer remembers which blocks produced it. Aggregate at read time and rollbacks correct themselves for free. **You have to know where to resume.** A follower starts from genesis, from the current tip, or from an explicit **chain point**, and only the third resumes anything. That needs a durable **cursor**, which is easy to get wrong. Advance it from the consumer once the write lands, never when the block arrives, or a restart silently skips blocks. Keep a short trail of recent points rather than a single one, since the point you saved may itself have been orphaned while you were down. And a stored cursor overrides configuration: once one exists, changing the configured start point does nothing until you delete it. **The same block can arrive twice.** Reconnects and replays redeliver blocks, and a cursor flushed on a timer guarantees some overlap after a crash. Nothing upstream promises exactly-once delivery, so key writes on something the chain provides, a transaction hash or an output reference, never an insertion counter. [Confirming a payment](/docs/developers/curriculum/dapps/listen-for-payments#detecting-a-payment) applies the same rule one level up. ## A modular indexer [Yaci Store](https://store.yaci.xyz/) is the worked example for the rest of this page: an open-source (MIT) modular indexer in Java from the [BloxBean project](https://github.com/bloxbean/yaci-store). :::info The Yaci family Three related projects share the name: [yaci](https://github.com/bloxbean/yaci) is the underlying Java implementation of the [Ouroboros mini-protocols](https://ouroboros-network.cardano.intersectmbo.org/pdfs/network-spec/network-spec.pdf); **Yaci Store** is the indexer built on it, covered here; [Yaci DevKit](https://devkit.yaci.xyz/introduction) is the local devnet tool that bundles a Store instance, covered in [Local testing](/docs/developers/curriculum/start-building/local-testing#yaci-devkit). ::: Most indexers make the sizing decision for you: db-sync stores everything, Kupo stores only UTXOs. Yaci Store is assembled from **stores** you enable per use case: blocks, transactions, UTXOs, metadata, assets, scripts, staking, and governance each ship as separate modules, plus aggregation modules that derive account balances, rewards, and ledger state independently, without a db-sync instance behind them. It syncs directly from any Cardano node over the node-to-node protocol, so it can follow a remote relay without you operating a node, and writes to PostgreSQL, MySQL, or H2. A local node is optional and does a different job: node-to-client is used for live protocol parameters, governance state, and transaction submission, never for indexing. It also ships **Blockfrost-compatible REST APIs**, behind a profile you switch on, so an SDK configured for Blockfrost can point at your own index unchanged. That is the same property [Yaci DevKit](/docs/developers/curriculum/start-building/local-testing#yaci-devkit) uses to serve a local devnet. The next section switches it on. Rollback is its problem rather than yours: tables are slot-tagged and append-only, so an unwind is one delete. ## Run one The [Docker distribution](https://store.yaci.xyz/docs/v2/getting-started/installation/docker) is the shortest path, because it brings PostgreSQL with it. Everything up to starting it goes in `config/application.properties`. Point it at a public relay, and no local node is involved: ```properties store.cardano.host=preprod-node.play.dev.cardano.org store.cardano.port=3001 store.cardano.protocol-magic=1 ``` Any synced relay works. If that one stops answering, current public relays for each network ship with the [environment configs](https://book.play.dev.cardano.org/environments/preprod/topology.json). Every store is on by default, so a focused index is mostly a list of what you switch off: ```properties # keep the UTXO store, drop the rest store.utxo.enabled=true store.assets.enabled=false store.blocks.enabled=false store.epoch.enabled=false store.metadata.enabled=false store.mir.enabled=false store.script.enabled=false store.staking.enabled=false store.transaction.enabled=false store.governance.enabled=false ``` Disabling a store means its processors never register, so the work is never done rather than done and thrown away. That is where the sync time and the disk savings come from. The aggregation modules that derive balances, rewards, and ledger state are off by default and switch on with the `ledger-state` profile. Profiles are how every optional module here is enabled, including the two the rest of this page uses. Then decide where to begin, the [cursor question](#what-the-indexer-handles-and-what-stays-yours) from above. Syncing an application-shaped index from genesis is usually waste, so take a `(slot, hash)` pair from any explorer for a point before the data you care about: ```properties store.cardano.sync-start-slot= store.cardano.sync-start-blockhash= ``` **Start it.** Switch on the Blockfrost-compatible API at the same time: it is off by default, and enabling the extension without also setting its URL prefix stops the application from starting, so use the profile that sets both. In `config/env`: ```properties SPRING_PROFILES_ACTIVE=blockfrost ``` ```bash ./yaci-store.sh start ``` **Check it.** Your own index now answers Blockfrost-shaped requests on port 8080, with no API key involved: ```sh curl -s localhost:8080/api/v1/blockfrost/blocks/latest | jq ``` **Point your SDK at it.** `http://localhost:8080/api/v1/blockfrost` is the base URL you hand an SDK already configured for Blockfrost, exactly as in [Query the chain](/docs/developers/curriculum/start-building/query-the-chain#choosing-a-provider). The provider changes, your query code does not. ## Index exactly what you need: plugins A common request, near verbatim from builders: *index every transaction carrying a given metadata label, cheaply and reliably*. A full indexer means storing the whole chain to use a sliver of it. A provider means polling, inside their API shapes and rate limits. Yaci Store's **plugin system** solves it with filter-before-persist: a predicate evaluated on each item before it is written, so cost scales with what matches rather than with the size of the chain. Filters attach to an extension point per store operation, named `..save`, and are written in **MVEL** or **SpEL** directly in configuration, with no Java and no fork of the indexer. JavaScript and Python are supported too, currently as preview. Keeping one NFT collection's mints is a single predicate, in `config/application-plugins.yml`: ```yaml store: plugins: enabled: true filters: asset.save: - name: "Keep one policy's mints" lang: mvel expression: 'policy == "" && mintType.name() == "MINT"' ``` That moves the predicate from query time to write time. Nothing else was ever stored, so the queries that follow carry no `WHERE policy` clause and the database stays small enough to be uninteresting. When one expression is not enough, the same extension point takes a script that receives the whole batch and returns the list to keep. Yaci Store's tutorials work the pattern end to end for [addresses](https://store.yaci.xyz/docs/v2/tutorials/tracking-address-utxos), [NFT mints](https://store.yaci.xyz/docs/v2/tutorials/tracking-nft-mints), and [governance](https://store.yaci.xyz/docs/v2/tutorials/governance-watch). The governance tutorial is worth reading for one decision in particular: it filters proposals but computes vote tallies from the stored rows rather than keeping a running count in the plugin. That is the [read-time aggregation rule](#what-the-indexer-handles-and-what-stays-yours) above: a tally computed on demand reflects rollbacks for free. The [IntersectMBO administration-data indexer](https://github.com/IntersectMBO/administration-data/tree/main/indexer) does exactly this, filtering on metadata label `1694` for treasury administration data, a few lines of configuration standing in for a bespoke indexer. Filtering is one of five plugin kinds. Plugins can also reshape a record before it is saved, act on one after it is saved, run on a schedule, or handle chain events directly, and they are given an HTTP client. That is what answers the **react** case from the top of this page without standing up a second pipeline: filter down to what you care about, then call your own service when it lands. [Write your first plugin](https://store.yaci.xyz/docs/v2/plugins/write-first-plugin) covers the kinds and the expression languages. If you don't want to run anything, the hosted alternative remains: Blockfrost serves [transactions by metadata label](/docs/developers/curriculum/start-building/transaction-building#transaction-metadata) over REST. The plugin route earns its keep when you need your own database, your own filtering logic, or independence from a third party. ## Analytics without running infrastructure Answering *historical* questions has traditionally required the full stack: node + indexer + database, days of sync, and a server bill that outlives the question. Yaci Store's **Analytics Store** module changes the economics: it continuously exports every table to **[Parquet](https://parquet.apache.org/) files**, the columnar format the wider data industry standardizes on. Once the files exist, the infrastructure has done its job; the dataset is a folder you can copy, share, archive, and query on a laptop. ```mermaid graph LR N[Cardano node] -->|mini-protocols| YS[Yaci Store] YS --> DB[(PostgreSQL)] DB -->|analytics exporters| PQ[Parquet files] PQ --> T[DuckDB, Spark, pandas, ...] style YS fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style N fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style DB fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style PQ fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style T fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` There is one exporter per table, and partitioning follows the data: continuous tables (transactions, UTXOs, blocks, address activity) by day (`date=2026-06-01/`), tables tied to Cardano's ~5-day epochs (stake snapshots, rewards, the ada pots) by epoch (`epoch=450/`). Query engines then skip straight to the slices they need. Two properties matter for research and reporting: - **Finalized data only.** Exports deliberately lag the chain tip (two days by default, `yaci.store.analytics.finalization-lag-days`), so the files never change retroactively; a rollback can't rewrite your dataset. Two people querying the same files get the same answers. - **Open format, no lock-in.** By default the export runs in [DuckLake](https://ducklake.select/) mode, a catalog layer adding ACID transactions and named tables (`analytics.block`); set `yaci.store.analytics.storage.type=parquet` for plain partitioned files. Either way the output is standard Parquet that DuckDB, Spark, Polars, pandas, ClickHouse, Athena, and BigQuery all read natively. ### Enabling the export The Analytics Store is a Spring profile on a running Yaci Store; add `ledger-state` if you want rewards and stake snapshots in the export: ```bash # Docker: in config/env SPRING_PROFILES_ACTIVE=ledger-state,analytics # Zip distribution: pass to the start script ./bin/start.sh ledger-state,analytics ``` Files land in `./data/analytics` by default (`yaci.store.analytics.export-path`), and on mainnet the export starts automatically once the initial sync reaches the tip. ### Querying with DuckDB [DuckDB](https://duckdb.org/) is the natural first tool: a free analytics engine that runs in-process on your machine and reads Parquet directly. Transaction statistics per epoch, over the full exported history: ```sql SELECT epoch, COUNT(*) AS tx_count, SUM(fee) AS total_fees, AVG(fee) AS avg_fee FROM read_parquet('data/analytics/main/transaction/**/*.parquet', hive_partitioning = true) GROUP BY epoch ORDER BY epoch; ``` (`main` in the path is the source schema name; adjust to your export location.) Because partition-aware queries read only the files they need, questions like this return in seconds even against full-history exports. The built-in exporters mirror tables one to one, but real questions often span tables. For those, **custom exporters** are defined entirely in YAML: give the module SQL with joins, a name, and a partition strategy, and it produces a fresh Parquet dataset on the same schedule, filling in `{source}`, `{start_slot}`, `{end_slot}` and `{epoch}` per partition. Enable them with the `custom-exporters` profile alongside `analytics`; the [Analytics Store docs](https://store.yaci.xyz/docs/v2/analytics/overview) carry the full configuration surface. :::info Beta, and you produce the files yourself The Analytics Store ships in the 3.0.0 beta line rather than the current stable releases, and there is no public dataset mirror yet, so you run Yaci Store once to produce the export. The output is the stable part: standard Parquet files that remain useful regardless of what produced them. ::: ## Choosing your approach db-sync remains the answer when you need the entire chain in SQL continuously. Reach for Yaci Store when you want an index shaped like your application: modular stores, plugin filters, and a Blockfrost-compatible API against your own database. Reach for the Analytics Store when the goal is a portable, reproducible dataset rather than a running service. Reach for an event stream when the trigger matters more than the history, when you want to act on something within a block or two of it happening and have somewhere else to put the result. Just remember which side of the [state question](#what-the-indexer-handles-and-what-stays-yours) that puts you on. --- ## The Network Protocol Beneath the APIs Most applications reach Cardano through an abstraction: a [query API](/docs/developers/curriculum/production/connecting-to-the-chain#query-apis), an [indexer](/docs/developers/curriculum/production/indexing-and-analytics), a [node interface](/docs/developers/curriculum/production/connecting-to-the-chain#node-interfaces) like Ogmios, or a [node of your own](/docs/developers/curriculum/production/self-hosting) queried over its socket. All of them bottom out in the same place: the **Ouroboros network protocol**, the wire format Cardano nodes use to talk to each other. A relay does not speak REST. It speaks a set of typed **mini-protocols** multiplexed over a single TCP connection. Nothing about that layer is reserved for nodes: any client that implements the protocol can dial a public relay and take part. This page explains that layer, then demonstrates it by fetching a block straight off a mainnet relay in about twenty lines of Rust, with no node, no indexer, and no API key involved. You will rarely build on this layer directly, but understanding it demystifies everything above it: what a provider actually abstracts, what an indexer actually consumes, and why every tool in the stack keeps talking about chain-sync and rollbacks. ## Two interfaces to a node A Cardano node exposes two distinct interfaces, built from the same protocol machinery but designed for different trust settings: - **Node-to-client (N2C)** runs over a local Unix socket, the `CARDANO_NODE_SOCKET_PATH` you set when [querying your own node](/docs/developers/curriculum/production/self-hosting). It is a trusted interface for local processes: `cardano-cli` uses it, and Ogmios translates it into WebSocket JSON. Beyond following the chain and submitting transactions, it can query live ledger state (UTXOs, protocol parameters), which is why it stays local: those queries are not designed to be served to strangers. - **Node-to-node (N2N)** runs over TCP between peers that do not trust each other. It is how relays exchange blocks and transactions across the open internet, and it is deliberately narrow: sync headers, fetch blocks, diffuse transactions. This is the interface the rest of this page uses. The distinction explains a pattern you have already met in this module: "run your own node" tooling always talks about a local socket (N2C), while the network itself, and anything that taps it directly, speaks N2N. ## Mini-protocols over one connection Each interface is a bundle of **mini-protocols**: small, typed state machines, each doing one job. The node-to-node bundle: - **Handshake** negotiates the protocol version and the [network magic](/docs/developers/curriculum/start-building/networks-and-test-ada), the identifier proving both sides are on the same chain, before anything else happens. - **Chain-sync** streams block headers as the chain grows, including *rollback* instructions when the peer switches to a better fork. This is the protocol every indexer is built on. - **Block-fetch** downloads block bodies for the headers you decide you want. - **Tx-submission** diffuses transactions toward block producers. It runs opposite to the block protocols: blocks fan out from the producing pool to everyone, while transactions converge from everywhere toward the producers. - **Keep-alive** and **peer-sharing** maintain the connection and support peer discovery. All of them share one TCP connection through a **multiplexer**: every message segment carries an 8-byte header, a timestamp, a 16-bit protocol identifier (one bit of which marks the direction of the conversation), and the payload length, so the demultiplexer on the other side can hand each segment to the right mini-protocol. ```mermaid graph LR HS[Handshake] --> MUX[Multiplexer] CS[Chain-sync] --> MUX BF[Block-fetch] --> MUX TS[Tx-submission] --> MUX MUX <-->|one TCP connection| R[Relay] style MUX fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style HS fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style CS fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style BF fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style TS fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style R fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` The [Cardano Blueprint's mini-protocols section](https://cardano-scaling.github.io/cardano-blueprint/network/mini-protocols.html) is the readable orientation: a state machine diagram, agency table, and message CDDL for each protocol. The full state machines and wire encodings are specified in the [Ouroboros network specification](https://ouroboros-network.cardano.intersectmbo.org/pdfs/network-spec/network-spec.pdf); [`ouroboros-network`](https://github.com/IntersectMBO/ouroboros-network) is the reference implementation inside `cardano-node`. ## Addressing a block: chain points Mini-protocols refer to positions on the chain with a **point**: a `(slot, header hash)` pair. The slot says *when*, the hash says exactly *which* block, since a slot alone could refer to a block that was rolled back. A special `Origin` point means the very start of the chain. Points are the shared handle across the whole bundle: chain-sync finds where your view and the peer's chain *intersect* by exchanging points, rollbacks are announced as "go back to this point", and block-fetch requests bodies by point. Every explorer already shows you both halves; any block page gives you a slot and a block hash, which is the header hash. ## Fetch a block from mainnet, no node required [Pallas](https://github.com/txpipe/pallas) is a Rust library by TxPipe that re-implements this stack natively: "Rust-native building blocks for the Cardano blockchain ecosystem", including the multiplexer and every mini-protocol above. One dependency gets you on the wire (plus [Tokio](https://tokio.rs) for the async runtime): ```bash cargo add pallas tokio --features tokio/full ``` The whole program is one connect, one point, one fetch, mirroring the [`block-download` example](https://github.com/txpipe/pallas/tree/main/examples/block-download) in the Pallas repository: ```rust use pallas::network::{ facades::PeerClient, miniprotocols::{Point, MAINNET_MAGIC}, }; #[tokio::main] async fn main() { // TCP dial + handshake: version negotiation carrying the network magic let mut peer = PeerClient::connect( "backbone.mainnet.cardanofoundation.org:3001", MAINNET_MAGIC, ) .await .unwrap(); // a real mainnet block: (slot, header hash), as shown by any explorer let point = Point::Specific( 191447882, hex::decode("d557600b99c14678e58ad7da7152fd2dbf188f290d4cef296ab6df774d2e0f3d") .unwrap(), ); // drive the block-fetch mini-protocol, get the raw block back let block = peer.blockfetch().fetch_single(point).await.unwrap(); println!("downloaded block of {} bytes", block.len()); println!("{}", hex::encode(&block)); } ``` `connect` dials the address, performs the handshake, and starts the multiplexer; `backbone.mainnet.cardanofoundation.org` is one of the public bootstrap relays run by the founding entities (any synced relay that accepts your connection works). `fetch_single` runs the block-fetch state machine end to end and returns the block exactly as the peer has it on disk: raw bytes, no translation. The same `Point` type is what you would hand to chain-sync to start following the chain from that position; the [`n2n-miniprotocols` example](https://github.com/txpipe/pallas/tree/main/examples/n2n-miniprotocols) shows that flow. ## From bytes to data What comes back is [CBOR](/docs/developers/curriculum/fundamentals/core-concepts/transactions#serialization-cbor), the same binary format transactions are serialized in; a block is CBOR all the way down. Since the encoding differs by era, Pallas provides a multi-era wrapper that detects the era and decodes into typed structs, as in its [`block-decode` example](https://github.com/txpipe/pallas/tree/main/examples/block-decode): ```rust use pallas::ledger::traverse::MultiEraBlock; let block = MultiEraBlock::decode(&cbor).expect("invalid cbor"); println!("slot {} hash {}", block.slot(), block.hash()); println!("{} transactions", block.txs().len()); ``` From here you can walk transactions, outputs, datums, and native assets across any era with one API. If you ever need to inspect the raw bytes themselves, the [CBOR debugging guide](/docs/developers/curriculum/smart-contracts/advanced/debug-cbor) covers the tooling. ## Where this sits in your stack Everything in [connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain) is built on these two interfaces. Ogmios bridges the node-to-client protocols of a local node into WebSocket JSON. Indexers and pipelines like Oura and Dolos speak node-to-node to follow the chain, and Dolos then serves node-to-client APIs to your app. Managed providers run all of that for you behind REST. The protocol itself has implementations beyond the Haskell reference: [Pallas](https://github.com/txpipe/pallas) in Rust (the foundation of Dolos, Oura, and the Amaru node project), [gOuroboros](https://github.com/blinklabs-io/gouroboros) in Go (the foundation of the Dingo node and of Adder), and [yaci](https://github.com/bloxbean/yaci) in Java (the foundation of Yaci Store). Reach for this layer when you are building the tools other developers use: a custom indexer or event pipeline, chain monitoring that must not depend on third parties, or lightweight tooling that needs one thing from the chain without running a node. For a typical application backend, a [hosted provider](/docs/developers/curriculum/production/use-a-provider) or [your own stack](/docs/developers/curriculum/production/self-hosting) remains the right entry point; now you know exactly what they are abstracting. --- ## Ship to Production You arrive here with a working application from [Build a dApp](/docs/developers/curriculum/dapps/overview): a wallet connects, transactions build and submit, contracts validate on a testnet. This module covers what stands between that and a service real users rely on, in two arcs: **Ship**, then **Scale**. ## Ship Shipping is readiness plus infrastructure. The readiness half is a checklist; the infrastructure half is a decision you make once, with the concepts to make it well: - **[Going to production](/docs/developers/curriculum/production/going-to-production)**: the pre-mainnet checklist: testing, security, reliable transactions, optimization, key safety, and the staging path through the testnets. - **[Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain)**: the concept map of chain access: what query APIs, node interfaces, indexers, data nodes, full nodes, and managed platforms each are, which of them are genuine alternatives to each other, and the axes to choose by. - **[Use a provider](/docs/developers/curriculum/production/use-a-provider)**: the hosted path in practice: Blockfrost, Koios, and Maestro, set up with one identical skeleton. - **[Self-hosting](/docs/developers/curriculum/production/self-hosting)**: the self-run path in practice: a Dolos data node, a node with Ogmios and Kupo, or a full node, with Demeter as the managed variant. - **[Custom indexing & analytics](/docs/developers/curriculum/production/indexing-and-analytics)**: when your application needs its own slice of the chain, or answers over its full history. - **[The network protocol beneath the APIs](/docs/developers/curriculum/production/network-protocol)**: an appendix on the wire protocol everything above abstracts, and how to speak it directly. ## Scale Scaling isn't one thing. Cardano scales at several layers, and the right approach depends on your workload. ### Layer 1: the base chain The base chain has bounded capacity per block, so on Layer 1 you scale by **using blocks efficiently** rather than by sending more independent transactions at a shared piece of state. Because the [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo) makes a UTXO spendable only once per block, high-contention designs (like a single shared pool) need the concurrency patterns covered in [DeFi on Cardano](/docs/developers/curriculum/dapps/defi#the-eutxo-design-challenge): **order batching** (many user intents executed in one transaction) and **pool sharding** (state split across many UTXOs so transactions run in parallel). You can also drop the confirmation wait between dependent transactions with [transaction chaining](/docs/developers/curriculum/production/transaction-chaining), spending each transaction's outputs before it settles. At the protocol level, proposed upgrades to Ouroboros (Leios, input endorsers) aim at substantially higher base-layer throughput. ### Layer 2: Hydra When you need **near-instant, near-free, high-frequency** transactions, gaming, micropayments, real-time interactions, you move them off the base chain into a [Hydra](/docs/developers/curriculum/production/hydra) Head: a state channel where a known set of participants transact thousands of times per second, settling back to Layer 1 only to open and close. You pay L1 cost once to open and once to close; everything inside is fast and free. | Need | Reach for | |---|---| | More throughput against shared state on L1 | [Order batching / pool sharding](/docs/developers/curriculum/dapps/defi#the-eutxo-design-challenge) | | Submit many dependent transactions without waiting for confirmation | [Transaction chaining](/docs/developers/curriculum/production/transaction-chaining) | | Instant, free, high-frequency transactions among known parties | [Hydra (Layer 2)](/docs/developers/curriculum/production/hydra) | | Higher base-layer throughput (future) | Ouroboros Leios (proposed protocol upgrade) | ## Where the curriculum ends This is the last module. Past it, the paths lead outward: running Cardano infrastructure as a discipline of its own ([Operate a Stake Pool](/docs/operators/)), starting the next project from a runnable [template](/templates), and the [developer community](/docs/community/cardano-developer-community) where the ecosystem builds. ## Next steps - [Going to production](/docs/developers/curriculum/production/going-to-production): start the Ship arc with the checklist - [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain): understand the infrastructure before you pick it --- ## Self-Hosting Self-hosting serves the same [two workflows](/docs/developers/curriculum/production/connecting-to-the-chain) as a hosted query API, read state and submit transactions, from infrastructure you run. You take it on for privacy (nobody sees your queries), independence (no third-party outage or rate limit in your path), or trust (validating the chain yourself instead of believing an operator). It comes in three shapes, and they are not steps on one ladder but different trades of trust against operations. Each tab below is a complete path: start it, check it, point your SDK at it. | Shape | Processes | Trust model | Resources | | --- | --- | --- | --- | | **Data node** (Dolos) | One | Trusts the relays it syncs from | A few GB of RAM, one ledger copy | | **Node + Ogmios + Kupo** | Three | Trustless: your node validates everything | A full node plus two light services | | **Full node** (+ your choice of serving layer) | One, then more | Trustless | Real storage, memory, and uptime commitment | ## Run it **What it is.** [Dolos](https://docs.txpipe.io/dolos) is a [data node](/docs/developers/curriculum/production/connecting-to-the-chain#data-nodes): a single lightweight Rust process that syncs the ledger directly from Cardano relays and serves it over several APIs, replacing the node + indexer + API stack for read-and-submit workloads. It trusts the relays you configure and cannot produce blocks; in exchange it is the smallest self-hosted footprint there is. **Start it.** Install Dolos and sync it against your network; the [TxPipe quickstart](https://docs.txpipe.io/dolos) covers installation and initial configuration. Then enable the API your SDK already speaks, **Mini-Blockfrost**, in `dolos.toml` and start Dolos: ```toml [serve.minibf] listen_address = "[::]:3000" permissive_cors = true ``` Dolos exposes each API on its own port, enabled selectively: Mini-Blockfrost (REST, a [subset of the Blockfrost API](https://docs.txpipe.io/dolos/apis/minibf)), Mini-Kupo (Kupo-style UTXO queries), UTxO RPC (gRPC), and the Ouroboros node-to-client socket for tools that expect a node (such as cardano-cli). **Check it.** Port 3000 now serves Blockfrost-shaped responses from your local copy of the ledger, no API key required: ```sh curl -s localhost:3000/blocks/latest | jq ``` **Point your SDK at it.** Every major transaction builder has a Blockfrost provider; point its base URL at your Dolos instance (the SDK's network must match the network Dolos is syncing): ```typescript const client = Client.make(preprod) .withBlockfrost({ baseUrl: "http://localhost:3000", projectId: "dolos" // Mini-Blockfrost doesn't check the key; any value works }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) ``` ```typescript const provider = new BlockfrostProvider("http://localhost:3000"); const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider, }); ``` **Size it.** Not every workload needs the full chain, so Dolos stores only what you configure: **ledger-only** (current state, enough to build and submit), **sliding window** (recent history with configurable retention), or **full archive** (everything, for explorers and historical queries). Check the [Mini-Blockfrost endpoint list](https://docs.txpipe.io/dolos/apis/minibf) covers your queries before swapping it in for a hosted API. **What it is.** The trustless read-and-submit stack: your own cardano-node validates the chain, [Ogmios](https://ogmios.dev) translates its [node interface](/docs/developers/curriculum/production/connecting-to-the-chain#node-interfaces) into WebSocket JSON, and [Kupo](https://github.com/CardanoSolutions/kupo) indexes the UTXOs matching patterns you configure (by address, policy ID, and more) with fast sync and low resource use. Together they are the **Kupmios** backend both SDKs support directly. **Start it.** The Ogmios repository ships a Docker Compose file that orchestrates the node and Ogmios together: ```sh git clone --depth 1 https://github.com/CardanoSolutions/ogmios.git cd ogmios docker compose up ``` For source builds or non-Docker installation, see [ogmios.dev/getting-started](https://ogmios.dev/getting-started); Kupo's [manual](https://cardanosolutions.github.io/kupo/) covers adding the indexer with your match patterns. **Check it.** Ogmios serves a dashboard at [localhost:1337](http://localhost:1337) and a health endpoint: ```sh curl -H 'Accept: application/json' http://localhost:1337/health ``` The fields that matter are `networkSynchronization` (1.0 means fully synced) and `lastKnownTip`; the rest are connection and runtime metrics for the dashboard. **Point your SDK at it.** ```typescript const client = Client.make(preprod).withKupmios({ ogmiosUrl: "ws://localhost:1337", kupoUrl: "http://localhost:1442" }) ``` ```typescript // Mesh has no single Kupmios provider: OgmiosProvider submits and evaluates, // pair it with Kupo queries for indexed reads const ogmios = new OgmiosProvider("ws://localhost:1337") ``` **Go deeper.** Ogmios exposes the node's mini-protocols themselves (chain-sync, mempool monitoring, state queries) at [ogmios.dev/mini-protocols](https://ogmios.dev/mini-protocols); [the network protocol beneath the APIs](/docs/developers/curriculum/production/network-protocol) explains what those are. **What it is.** A passive (non-block-producing) [cardano-node](/docs/developers/curriculum/production/connecting-to-the-chain#the-full-node) of your own: the most trustless access there is, and the foundation the other shapes replace or build on. For development or a read-and-submit backend it is enough on its own, queried over its socket. ```text cardano-node (mainnet), rough requirements: Storage: ~180 GB (growing ~15 GB/year) RAM: 16+ GB (24 GB recommended) CPU: 4+ cores Sync: hours to days from genesis (minutes with a Mithril snapshot) Uptime: must stay running to serve queries ``` **Start it.** 1. **Install** the node from the release binaries: [Installing cardano-node](/docs/operators/node/installing-cardano-node). 2. **Run** it against your network and let it sync. A certified [Mithril](/docs/operators/operator-tools/mithril) snapshot makes the initial sync minutes rather than hours: [Running cardano-node](/docs/operators/node/running-cardano). **Check it.** Query it with cardano-cli over the node socket: [querying the node](/docs/operators/node/running-cardano#querying-the-node). **Serve your app from it.** A bare node has no application API, so pair it with a serving layer: Ogmios and Kupo (the neighboring tab, pointed at this node), a [custom indexer](/docs/developers/curriculum/production/indexing-and-analytics), or a full self-hosted query API ([blockfrost-backend-ryo or a Koios gRest instance](/docs/developers/curriculum/production/use-a-provider#the-same-apis-self-hosted)). **Running it as real infrastructure** (peer topology, monitoring, hardening, high availability, and, if you run a stake pool, registration and block production) is an operations discipline of its own; the [Operate a Stake Pool](/docs/operators/) curriculum covers it end to end. A developer standing up a node for queries does not need most of it, but it is the place to go when you do. ## The managed variant: Demeter [Demeter](https://demeter.run) hosts this page's architecture as a [managed platform](/docs/developers/curriculum/production/connecting-to-the-chain#managed-platforms): node access, indexers (db-sync, Kupo, Mumak), and node interfaces (Ogmios, submit API, UTxO RPC, Blockfrost RYO) provisioned as cloud services across mainnet, preprod, and preview. You get the self-hosted shapes without the operations, at the price of reintroducing a platform operator into your trust model. It fits when you want stack-level control (your own Kupo patterns, your own Ogmios session) but not the servers. Create an account at [demeter.run](https://demeter.run) and see the [Demeter documentation](https://docs.demeter.run) for service setup; wiring an SDK to a hosted Kupmios with the platform's API keys is shown in [Query the chain](/docs/developers/curriculum/start-building/query-the-chain#choosing-a-provider). ## Next steps - [Custom indexing & analytics](/docs/developers/curriculum/production/indexing-and-analytics): shape your own index instead of adopting a fixed one - [The network protocol beneath the APIs](/docs/developers/curriculum/production/network-protocol): what the node interfaces translate, one layer further down - [Going to production](/docs/developers/curriculum/production/going-to-production): the checklist that decides between these shapes and a hosted API --- ## Transaction Chaining The normal build loop is **build, sign, submit, wait for confirmation**, then build the next one. That wait, ten to thirty seconds per step, is the bottleneck for anything that sends many dependent transactions. **Transaction chaining** removes it: you build and submit a transaction that spends an output of an earlier transaction that has not been confirmed yet. The [SDK how-to](/docs/developers/curriculum/start-building/transaction-building#chaining-transactions) shows the code in Evolution, Mesh, and cardano-cli. This page is the concept underneath it: why an unconfirmed output is safe to spend, what chaining buys you, and where it sits among Cardano's scaling options in the [scaling overview](/docs/developers/curriculum/production/overview). ## Why the next transaction's inputs already exist A transaction's id is the hash of its **body**, the inputs, outputs, and everything else you sign over, not of the signatures that get added afterward. So the id is fixed the moment the transaction is built, before it is signed or submitted. Every output it will create is therefore addressable in advance as `txid#index`, along with its exact value and datum. This is a direct consequence of [deterministic validation](/docs/developers/curriculum/smart-contracts/overview#deterministic-validation). A transaction that validates locally produces exactly the outputs you built, or it does not apply at all, with nothing in between. You know the result before submission, so you can build on it before submission. An account-based chain can queue dependent transactions too, ordered by nonce, but each one references a mutable account whose balance and state are only resolved when it is included in a block. On [eUTXO](/docs/developers/curriculum/fundamentals/core-concepts/eutxo#why-is-deterministic-validation-such-a-big-deal) the next transaction references a concrete, immutable output that is fully determined at build time. That is the whole trick: build transaction 1, read the id it will have, and build transaction 2 spending transaction 1's outputs, all before transaction 1 reaches a block. ## The mempool accepts the chain When you submit a transaction, the node validates it against the current ledger state **plus the transactions already waiting in its mempool**. So a transaction whose input is an output of another transaction still in the mempool is valid: the node sees the producing transaction ahead of it and applies them in order. You can submit the whole chain back to back without waiting for a single confirmation, each transaction layering onto the state the previous ones established. ```mermaid graph LR T1["Tx 1"] -->|"produces UTxO"| T2["Tx 2 spends it"] T2 -->|"produces UTxO"| T3["Tx 3 spends it"] T1 --> MP["Mempool"] T2 --> MP T3 --> MP MP --> B["Block"] style MP fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style T1 fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style T2 fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style T3 fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style B fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` Under the hood the mempool is an ordered sequence, validated against a recent ledger state (its *anchor*). In the reference node, a change of tip revalidates the sequence in order, and a transaction that drops out forces everything after it to be rechecked, because later transactions may have been building on it. The ledger only requires that a producing transaction come before its spender; a dependency-aware mempool could recheck less. The [Cardano Blueprint's mempool page](https://cardano-scaling.github.io/cardano-blueprint/mempool/index.html) documents this machinery in full. Once a transaction is accepted into the mempool it keeps its place [toward inclusion](/docs/developers/curriculum/fundamentals/core-concepts/transactions#the-transaction-lifecycle) for as long as it stays valid against the node's evolving view of the chain. The chain as a whole, though, is only as durable as its first link. :::warning Submit in order A chained transaction is only valid once its predecessor is in the mempool or a block. If it arrives first, the node sees inputs that do not exist yet and rejects it. Submit sequentially, and never hand a not-yet-submitted output to a provider query: your provider only knows about outputs that are already on-chain. ::: ## What chaining unlocks **Throughput without the wait.** Sending N dependent transactions the naive way costs N confirmation waits. Chained, they are built and submitted in one pass and settle together. This is what makes high-volume flows practical: large [airdrops](/docs/developers/curriculum/start-building/transaction-building#batching-and-airdrops), minting many tokens in sequence, or any multi-step interaction where each step consumes the output of the last. **A decentralized way to order contended state.** [eUTXO concurrency](/docs/developers/curriculum/dapps/defi#the-eutxo-design-challenge) means a shared UTxO, a liquidity pool or a registry, can be spent only once per block, so many parties competing for it need their turns ordered. The common answer is off-chain [order batching](/docs/developers/curriculum/dapps/defi#order-batching), where an operator collects intents and settles them, at the cost of latency and of trusting that operator to include and sequence fairly. Chaining offers another shape: each interaction builds directly on the previous one's unconfirmed output, so the order is fixed by the on-chain input dependencies rather than chosen by an off-chain operator. The two compose, a protocol can use chaining to keep its own batching pipeline moving without waiting on confirmations. Pushed all the way it drops the operator outright: in a [batcher-free pool](/docs/developers/curriculum/dapps/defi#batcher-free-pools) the contenders are many independent, mutually distrusting users each extending the same shared UTxO, so no one picks the order and it settles by whoever wins the mempool tip, a client that loses the race rebuilding on fresh state and resubmitting. ## The trade-offs Chaining trades away the wait, not the work. The costs are real and worth designing around: - **The chain is only as strong as its first link.** If an early transaction is dropped, evicted, or its input is spent by someone else, every transaction downstream of it fails, because the outputs they depend on never come to exist. - **Every transaction pays its own fee.** Chaining removes the waiting, not the per-transaction [minimum fee](/docs/developers/curriculum/fundamentals/core-concepts/fees#the-fee-formula). A chain of N transactions is N fees. - **You track the unconfirmed UTxOs yourself.** Thread each transaction's outputs into the next build in your own code; the SDKs that automate this are covered in the [how-to](/docs/developers/curriculum/start-building/transaction-building#chaining-transactions). - **The mempool is not permanent.** Transactions past their [validity interval](/docs/developers/curriculum/fundamentals/core-concepts/transactions#validity-intervals-and-time) are dropped, and a change of chain tip revalidates the mempool, so a long chain that lingers can be invalidated partway. Keep chains bounded and submit promptly. - **Script transactions each need collateral.** Every script-bearing transaction in the chain sets aside its own [collateral](/docs/developers/curriculum/smart-contracts/lock-and-spend#collateral); determinism lets you confirm locally that it will not be taken. - **Shared reference inputs must stay put.** If transactions in the chain read a [reference input or reference script](/docs/developers/curriculum/fundamentals/core-concepts/transactions#reference-inputs-and-reference-scripts), that UTxO has to stay unspent for the life of the chain; anything that spends it breaks the dependents relying on it. ## Building a chain The code lives with the other build-side how-tos. [Chaining transactions](/docs/developers/curriculum/start-building/transaction-building#chaining-transactions) shows it three ways: Evolution tracks the produced outputs for you, Mesh has you thread them in by hand, and at the lowest level `cardano-cli transaction txid` computes a transaction's id from its body so you can reference `txid#index` in the next one before anything is submitted. When a long chain has to carry a large, verifiable piece of state, a registry or set updated at every step, an on-chain [Merkle Patricia Forestry](/docs/developers/curriculum/smart-contracts/advanced/optimization#use-merkle-patricia-forestry-for-larger-registries) keeps the whole structure behind a single root hash. ## Where chaining fits Chaining is one of several ways Cardano scales, and each solves a different problem: - **Versus off-chain batching.** Both raise throughput against contended state. Batching aggregates many intents into one transaction through an operator; chaining keeps each interaction as its own transaction, ordered by on-chain input dependencies, and can also be used to scale a batching pipeline itself. - **Versus [Hydra](/docs/developers/curriculum/production/hydra) (Layer 2).** Hydra moves transactions off the main chain entirely, among a fixed, known set of participants, for near-instant and near-free throughput. Chaining stays on Layer 1 and is open to anyone: it removes the confirmation wait, but every transaction is still a real Layer 1 transaction with a Layer 1 fee. - **Versus input endorsers.** Higher base-layer throughput is also coming at the protocol level through Leios and its input endorsers, still in research and not yet live. See the [Ouroboros roadmap](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros). Reach for chaining when you have many dependent transactions to submit from one place and do not want a confirmation wait between each, or when you want interactions with a contended UTxO ordered by their on-chain dependencies rather than by an off-chain operator. ## Next steps - [Chaining transactions](/docs/developers/curriculum/start-building/transaction-building#chaining-transactions): the SDK code, in Evolution, Mesh, and cardano-cli - [Scaling & production](/docs/developers/curriculum/production/overview): how chaining sits alongside batching, sharding, and Hydra - [DeFi on Cardano](/docs/developers/curriculum/dapps/defi#the-eutxo-design-challenge): the concurrency problem chaining helps with --- ## Use a Provider A [query API](/docs/developers/curriculum/production/connecting-to-the-chain#query-apis) is the fastest way to serve your application the chain: someone else runs the node and the indexer, and your SDK points at a URL. The three documented here are directly comparable, they answer the same REST-shaped reads and accept the same transaction submissions, so this page sets all three up with one identical skeleton: create access, find your network's endpoint, make a first raw request, point your SDK at it, and know the limits. Pick one tab and the rest of the curriculum works the same; switching later is a configuration change. The differences that matter are operational, not functional: who operates it, how access is granted, and how usage is limited. Where they genuinely differ, the tabs say so. :::warning Keep keys server-side An API key identifies and bills *you*. Store it in an environment variable on your backend, never commit it, and never ship it in client-side code, where anyone can read it. The [frontend signs, backend submits](/docs/developers/curriculum/dapps/connect-a-wallet#frontend-signs-backend-builds-and-submits) pattern exists partly for this reason. ::: ## Create access Create a free account at [blockfrost.io](https://blockfrost.io/auth/signin). After signing in, create a project: click **+ ADD PROJECT**, name it, and select the network. Each project is scoped to **one network** and gets its own `project_id`, which is your API key (it starts with the network name, e.g. `preprod...`). Nothing to create for basic use: Koios is a community-run cluster and its **public tier needs no key**. For higher limits, generate a free auth token from your [Koios profile](https://koios.rest/) and send it as a `Authorization: Bearer ` header. The same token works across networks. Create a free account at [dashboard.gomaestro.org](https://dashboard.gomaestro.org/). Create a project, selecting **Cardano** and your network, then copy the project's API key from the dashboard. Each project is scoped to one network. ## Network endpoints Each network has its own base URL, and your access is scoped to the network you set it up for (Koios excepted: same token everywhere, different URL per network). | Network | Base URL | | --- | --- | | Mainnet | `https://cardano-mainnet.blockfrost.io/api/v0` | | Preprod | `https://cardano-preprod.blockfrost.io/api/v0` | | Preview | `https://cardano-preview.blockfrost.io/api/v0` | | Network | Base URL | | --- | --- | | Mainnet | `https://api.koios.rest/api/v1` | | Preprod | `https://preprod.koios.rest/api/v1` | | Preview | `https://preview.koios.rest/api/v1` | | Network | Base URL | | --- | --- | | Mainnet | `https://mainnet.gomaestro-api.org/v1` | | Preprod | `https://preprod.gomaestro-api.org/v1` | | Preview | `https://preview.gomaestro-api.org/v1` | ## Your first request The same request three ways: ask for the chain tip (or latest block) with your key in the provider's auth header. A JSON answer means access, endpoint, and network line up. Authentication is the `project_id` header: ```bash curl -H "project_id: $BLOCKFROST_PROJECT_ID" \ https://cardano-preprod.blockfrost.io/api/v0/blocks/latest ``` ```json { "time": 1641338934, "height": 15243593, "hash": "4ea1ba291e8eef538635a53e59fddba7810d1679631cc3aed7c8e6c4091a516a", "slot": 412162133, "epoch": 425, "tx_count": 1, "fees": "592661" } ``` No header needed on the public tier: ```bash curl https://preprod.koios.rest/api/v1/tip ``` ```json [ { "hash": "3448481b954a5e90adafc8c16784e787e73acb0aaead2ca13902b2172f3047a5", "epoch_no": 304, "abs_slot": 129752219, "block_no": 4997788, "block_time": 1785435419 } ] ``` Koios is built on [PostgREST](https://postgrest.org/), so every endpoint supports column selection, filtering, ordering, and paging through query parameters; the [API usage guide](https://api.koios.rest/#overview--api-usage) covers the syntax. Authentication is the `api-key` header: ```bash curl -H "api-key: $MAESTRO_API_KEY" \ https://preprod.gomaestro-api.org/v1/chain-tip ``` The response is the current chain tip: block hash, height, and slot. Responses are cursor-paginated where lists are involved: pass the returned `next_cursor` back as the `cursor` query parameter to fetch the next page. ## Point your SDK at it You will rarely call the REST API directly: the SDK's provider does it behind the [interface](/docs/developers/curriculum/production/connecting-to-the-chain#a-provider-is-an-interface-not-just-a-service) you already use. Configure the one you set up (base URL and network must match your key): ```typescript // Blockfrost const bf = Client.make(preprod).withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! }) // Koios const koios = Client.make(preprod).withKoios({ baseUrl: "https://preprod.koios.rest/api/v1" }) // Maestro const maestro = Client.make(preprod).withMaestro({ baseUrl: "https://preprod.gomaestro-api.org/v1", apiKey: process.env.MAESTRO_API_KEY! }) ``` ```typescript // Blockfrost, network auto-detected from the key prefix const bf = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID!) // Koios, pass the network const koios = new KoiosProvider("preprod") // Maestro const maestro = new MaestroProvider({ network: "Preprod", apiKey: process.env.MAESTRO_API_KEY! }) ``` Every query and submission from [Query the chain](/docs/developers/curriculum/start-building/query-the-chain) runs unchanged on any of the three. ## Limits The free tier is rate-limited per project, with paid tiers above it; current numbers are on [blockfrost.io](https://blockfrost.io/#pricing). Beyond the chain API, the same account gives you an [IPFS gateway](https://blockfrost.dev/docs/start-building/ipfs/) for pinning off-chain content and [webhooks](https://blockfrost.dev/docs/start-building/webhooks/) that push on-chain events to you instead of you polling. The public tier carries shared rate limits that protect the community cluster; an auth token raises them. Review the [limits](https://api.koios.rest/#overview--limits) before you integrate. Response times can vary by instance, since the cluster is served by independent community operators. Requests per second depend on your subscription plan, and each call also consumes **compute credits** from a tier-dependent allowance (heavier queries cost more). The `X-RateLimit-*` and `X-Maestro-Credits-*` response headers report where you stand. Current tiers are on [gomaestro.org/developer](https://gomaestro.org/developer). ## The full references Each provider's own documentation is the authority on its endpoints: - **Blockfrost**: [blockfrost.dev](https://blockfrost.dev/), with official SDKs for 15+ languages beyond the Cardano SDK providers above - **Koios**: [api.koios.rest](https://api.koios.rest/), every endpoint with a runnable sample query - **Maestro**: [docs.gomaestro.org](https://docs.gomaestro.org/cardano), covering the indexer plus transaction-management extras ## The same APIs, self-hosted Two of the three are open source end to end, so outgrowing a hosted tier does not mean leaving the API behind: - **Blockfrost**: the backend ([blockfrost-backend-ryo](https://github.com/blockfrost/blockfrost-backend-ryo)), SDKs, and [OpenAPI spec](https://github.com/blockfrost/openapi) are open source; a Docker image runs your own instance against your own node and db-sync. - **Koios**: the [guild-operators suite](https://cardano-community.github.io/guild-operators/Build/grest/) deploys a full gRest instance (node, db-sync, PostgREST, HAProxy) with the same API, removing shared rate limits; you can also [contribute the instance back](https://github.com/cardano-community/koios-artifacts/tree/main/topology) to the community cluster. Running your own serving layer, in these shapes and lighter ones, is the [self-hosting](/docs/developers/curriculum/production/self-hosting) page. ## Harden it for production One hosted API is a single point of failure, and chatty code hits rate limits. Because every provider implements the same interface, failover and caching are composition, not migration: see [harden your provider](/docs/developers/curriculum/production/going-to-production#harden-your-provider) for the pattern in both SDKs. ## Next steps - [Self-hosting](/docs/developers/curriculum/production/self-hosting): the same two workflows served from infrastructure you run - [Going to production](/docs/developers/curriculum/production/going-to-production): the pre-mainnet checklist, including provider hardening --- ## BLS Signatures, VRFs, and Anonymous Credentials ## Introduction The [zero-knowledge proofs page](/docs/developers/curriculum/smart-contracts/advanced/zero-knowledge) covers the headline use of the BLS12-381 builtins: verifying a SNARK inside a validator. But proof verification is only one member of a family. Because BLS12-381 is a **pairing-friendly** curve, the same handful of operations, scalar multiplication, point addition, hashing-to-curve, and the pairing check, compose into several other protocols that run entirely on-chain: - **BLS signatures**: thousands of signatures or public keys collapse into one small value, verified with a constant number of pairings. - **Key derivation functions**: turn seeds or shared secrets into valid curve keys, natively in a validator. - **Verifiable random functions**: pseudorandom outputs that anyone can verify came from a specific key. - **BBS+ anonymous credentials**: prove selected attributes from a signed credential without revealing the rest. For what the builtins are and when they shipped (CIP-0381 in Chang, cheaper via CIP-0133 and CIP-0109 in van Rossem), see [the primitives section](/docs/developers/curriculum/smart-contracts/advanced/zero-knowledge#the-primitives-what-shipped-when) of the ZK page; this page assumes them and builds up. The worked examples come from the [Cardano Foundation's BLS repository](https://github.com/cardano-foundation/bls), and the protocols were introduced in depth in [this article on the Aiken primitives](https://cardanofoundation.org/blog/aiken-primitives-explained). ## The primitives in Aiken Aiken exposes the raw Plutus builtins through `aiken/builtin` and wraps them in a type-safe interface under [`aiken/crypto/bls12_381`](https://aiken-lang.github.io/stdlib/aiken/crypto.html), with one module per group: `g1` (48-byte compressed points) and `g2` (96-byte compressed points). Deriving a public key from a secret is one scalar multiplication: ```aiken use aiken/builtin use aiken/crypto/bls12_381/g1 fn sk_to_pk(sk: ByteArray) -> ByteArray { let s = builtin.bytearray_to_integer(True, sk) expect s != 0 builtin.bls12_381_g1_compress(builtin.bls12_381_g1_scalar_mul(s, g1.generator)) } ``` Uncompressed points are twice the size, so anything stored in a datum or redeemer is compressed first. That size difference also drives a convention: with **public keys in G1 and signatures in G2** (the *minimal public key size* variant of BLS), the long-lived data (keys, held in datums and UTXOs) stays at 48 bytes, while the larger 96-byte signatures are usually transient. The second primitive everything below leans on is **hash-to-curve**: hashing a message directly to a point on the curve rather than to an integer. Signing is then one more scalar multiplication: ```aiken use aiken/builtin fn hash_and_sign(sk: ByteArray, message: ByteArray, dst: ByteArray) -> ByteArray { let s = builtin.bytearray_to_integer(True, sk) expect s != 0 let h = builtin.bls12_381_g2_hash_to_group(message, dst) builtin.bls12_381_g2_compress(builtin.bls12_381_g2_scalar_mul(s, h)) } ``` The `dst` argument is a **domain separation tag**, a public string baked into the hash so that a hash computed for a signature can never collide with one computed for a VRF or any other protocol, even on identical input. Finally the pairing itself, always a two-step dance: a **Miller loop** per point pair produces intermediate results, and one `bls12_381_final_verify` compares them: ```aiken use aiken/builtin.{ bls12_381_final_verify, bls12_381_g1_scalar_mul, bls12_381_g2_scalar_mul, bls12_381_miller_loop, } use aiken/crypto/bls12_381/g1 use aiken/crypto/bls12_381/g2 /// Bilinearity: e(2*G1, 3*G2) == e(6*G1, G2) test bilinearity_demo() { bls12_381_final_verify( bls12_381_miller_loop( bls12_381_g1_scalar_mul(2, g1.generator), bls12_381_g2_scalar_mul(3, g2.generator), ), bls12_381_miller_loop( bls12_381_g1_scalar_mul(6, g1.generator), g2.generator, ), ) } ``` The split matters for cost: intermediate Miller-loop results can be multiplied together with `bls12_381_mul_miller_loop_result`, so verifying *many* relationships still ends in a *single* final verify. That one property is what makes signature aggregation affordable on-chain. ## BLS signatures and aggregation BLS (Boneh-Lynn-Shacham) signatures do everything Ed25519 does, plus one thing no ordinary scheme can: signatures are curve points, so they **add together**. A hundred signers produce a hundred signatures that aggregate into one 96-byte value. With ECDSA or Ed25519, verification cost and witness size grow linearly with the signer set; with BLS they barely grow at all. That is why Ethereum's consensus layer runs on BLS attestations, and it is what makes large on-chain committees, vote tallies, and k-of-n schemes with big n practical inside one script budget. (For small, fixed signer sets, [native-script multisig](/docs/developers/curriculum/smart-contracts/write-a-validator#native-scripts-multisig-and-time-locks-without-plutus) remains the simpler tool, no Plutus required.) Two aggregation patterns, with different costs: - **Signature aggregation, distinct messages.** Each party signs its own message; the signatures sum into one value. The verifier runs one Miller loop per `(public key, message)` pair, multiplies the intermediate results, and finishes with a single final verify. Cost is roughly one pairing per message, but the witness is one signature instead of n. - **Public-key aggregation, same message.** When everyone signs the *same* message, the public keys themselves aggregate too (point addition in G1, 48 bytes total). Verification is then **two pairings, no matter how many signers**: aggregated key against the hashed message, aggregated signature against the generator. The [`ilap/bls`](https://github.com/ilap/bls) library (Apache-2.0) implements the [IETF BLS signature draft](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature) on top of the builtins, and the [`signature-aggregation-case`](https://github.com/cardano-foundation/bls/tree/main/aiken/signature-aggregation-case) and [`publickey-aggregation-case`](https://github.com/cardano-foundation/bls/tree/main/aiken/publickey-aggregation-case) examples exercise both patterns end to end. The library's API is small: `sk_to_pk`, `sign`, `verify`, `aggregate`, `aggregate_verify`. ```aiken use bls/g1/basic as bls // Off-chain: anyone can aggregate the collected signatures let sig_aggr = bls.aggregate([sig1, sig2, sig3]) // On-chain: one check covers all three signers bls.aggregate_verify([pk1, pk2, pk3], [msg1, msg2, msg3], sig_aggr) ``` ### The rogue-key attack, and the three modes that defend against it Aggregation introduces an attack that plain signatures do not have. Public keys are just points, so an attacker who sees honest keys `pk_1` and `pk_2` can *claim* the key `pk_rogue = pk_att - (pk_1 + pk_2)` for a secret `sk_att` they control. They never knew the secrets behind the honest keys, but the aggregate `pk_1 + pk_2 + pk_rogue` collapses to `pk_att`, so a signature they produce alone verifies as if all three signed. The IETF draft defines three signing modes, `ilap/bls` implements all of them, and the difference is exactly how each one kills this attack: | Mode | How it works | Defense | When to use | |---|---|---|---| | **Basic** (`bls/g1/basic`) | Sign the raw message | `aggregate_verify` rejects duplicate messages, so cancellation cannot pay off | Trusted or pre-validated keys, messages known to be distinct | | **Augmented** (`bls/g1/aug`) | Sign `pk ‖ message` | Each hash input is unique per signer, so a forged aggregate never matches | Untrusted keys that change often, duplicate messages allowed | | **Proof-of-possession** (`bls/g1/pop`) | Register each key once with a signature over itself | Registration proves the key's owner holds its secret, rogue keys cannot register | Fixed committees or pools that register once and sign many times | One composition rule to respect: **public-key aggregation only works in Basic mode with an identical message.** Augmented and PoP signatures bind each signature to the individual signer's key, so the pairing equation over an aggregated key no longer balances; the `publickey-aggregation-case` example demonstrates the failure explicitly. ## Deriving keys on-chain: KDFs The examples above assume a curve-ready 32-byte secret. Real inputs are messier, a seed, a Diffie-Hellman shared secret, a password, and interpreting raw bytes as a scalar can fall outside the curve's prime field. A **key derivation function** bridges the gap, and the [`kdf`](https://github.com/cardano-foundation/bls/tree/main/aiken/kdf) example implements two RFC-compliant ones purely from Plutus builtins, plus helpers (`gen_keys_hkdf`, `gen_keys_pbkdf2`) that reduce the output modulo the field order and hand back a valid `(sk, pk)` pair: - **HKDF ([RFC 5869](https://datatracker.ietf.org/doc/html/rfc5869))** for input that is already high-entropy. Cheap: a 32-byte derivation costs around 15M CPU units, background noise in a script budget. The `info` parameter gives domain separation, so one master secret can yield many unlinkable keys. - **PBKDF2 ([RFC 8018](https://datatracker.ietf.org/doc/html/rfc8018#page-11))** for low-entropy input, made deliberately slow through iteration. On-chain the iteration count must stay modest: a handful of iterations costs a few million CPU units, while the classic off-chain recommendation of 4,096 iterations measures around 5.7 billion, more than half of an entire transaction's execution budget. (This is the same PBKDF2 your wallet uses off-chain, at full strength, to [stretch mnemonics into master keys](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys).) :::warning Everything in a transaction is public, forever A password or salt passed to a validator through a datum or redeemer is published on-chain permanently; anyone can extract it and rerun the KDF off-chain at leisure. On-chain KDFs are therefore **not** a password-hashing mechanism. Their legitimate inputs are values that are already public or already committed, and their legitimate uses are narrow: deriving session or child keys from strong secrets, and testing or teaching. Hash human passwords off-chain, in the wallet or application layer, and put only the resulting key or hash on-chain. ::: The same project also documents a boundary worth knowing: **memory-hard KDFs (Argon2, Balloon) are fundamentally incompatible with on-chain execution.** Argon2's minimum recommended settings want 64 MiB to 4 GiB of RAM against a per-transaction memory budget orders of magnitude smaller, it needs a 64-byte BLAKE2b variant Plutus does not expose, and its data-dependent memory access is exactly what execution-unit pricing punishes. If you need memory-hard hashing, it happens off-chain, and the chain verifies the result. ## Verifiable random functions A **VRF** is a keyed hash with a public verification story: only the secret key holder can compute the output for a given input, the output is deterministic and looks random to everyone else, and it comes with a proof that anyone can check against the public key. Three properties do the work: **verifiability** (the proof binds output to key and input), **uniqueness** (exactly one valid output per key and input, nothing to grind), and **non-interactivity** (one published tuple, no challenge rounds). You have met VRFs before: [Ouroboros uses one for leader election](/docs/developers/curriculum/fundamentals/cryptographic-primitives#what-are-verifiable-random-functions-vrfs). The [`vrf`](https://github.com/cardano-foundation/bls/tree/main/aiken/vrf) example brings the same tool into application space, an ECVRF over BLS12-381 G2 written entirely in Aiken. The API is four functions, and the proof is 144 bytes (a compressed G2 point, a 16-byte challenge, a 32-byte response): ```aiken use vrf/core as vrf let (sk, pk) = vrf.keys_from_secret(operator_secret) // Operator, per round: input is public (say, a block hash or round number) let pi = vrf.prove(sk, round_input, "ECVRF_") let Some(beta) = vrf.proof_to_hash(pi) // Anyone, including a validator: recompute and confirm the same beta vrf.verify(pk, round_input, pi, "ECVRF_", False) == Some(beta) ``` What it buys you on-chain: - **A randomness beacon a validator can enforce.** An operator publishes a key in advance; each round's input is public and fixed, so they get exactly one possible output and one shot, no grinding. Because verification is just curve arithmetic on the same builtins (an ECVRF never actually computes a pairing), the *validator itself* can check the proof, rather than trusting an oracle's signature. How this compares with commit-reveal and oracle-published block VRFs, including the liveness trust you still carry, is covered in [on-chain randomness](/docs/developers/curriculum/dapps/oracles/randomness). - **Enumeration-resistant data structures.** Store records in a public Merkle tree keyed by `sha2_256(record_name)` and anyone can guess common names and probe for them. Key the tree by the owner's VRF output instead and outsiders see only unlinkable pseudorandom addresses, while the owner can still prove any record's membership later by revealing the name and its proof. - **Proofs of knowledge without revealing it**, such as proving you held a secret at some point in time (use the secret as VRF input) or passwordless authentication (prove the password-derived key, never send the password). ## Anonymous credentials with BBS+ **BBS+ signatures** turn a signed list of attributes into a privacy-preserving credential. An issuer signs n attributes (name, birthdate, citizenship, tier, ...) into one constant-size signature. Later the holder proves, to a verifier or a validator, that they hold a valid signature over all n attributes **while revealing only a chosen subset**. The signature itself never leaves the holder's hands: each showing randomizes it and wraps it in a zero-knowledge proof, so verifiers cannot link two showings to each other or to issuance. "Over 18 and an EU citizen", without the birthdate, the name, or a trackable identifier. This runs on the same builtins: attributes are hashed to curve points, the issuer's key lives in G2, and verification reduces to a pairing equation plus checks that the disclosed values match the proof. The [`lambdasistemi/cardano-bbs`](https://github.com/lambdasistemi/cardano-bbs) project implements the scheme with Aiken on-chain verification (check its repository for licensing before depending on it), and the CF BLS repository has a related [`selective-disclosure`](https://github.com/cardano-foundation/bls/tree/main/aiken/selective-disclosure) example. For the full issuer, holder, and verifier lifecycle in one place, the [ZeroJ reusable-KYC example](https://github.com/bloxbean/zeroj-usecases/tree/main/reusable-kyc) runs the flow end to end from Java, with a Plutus V3 validator performing the BBS proof verification on-chain at roughly a quarter of a script's CPU budget; it shares its parent project's experimental, not-for-production status. On-chain, the shape is the familiar one from the ZK page: ```aiken use bbs/types.{BBSProof, RegulatorRegistry} use bbs/verify validator bbs_credential { spend(datum: Option, redeemer: BBSProof, own_ref, _self) { when datum is { // Registry datum: issuer's G2 public key + the credential's attribute schema Some(registry) -> verify.verify(registry, redeemer, nonce_from_output_reference(own_ref)) None -> False } } } ``` The trust anchor (issuer key and attribute schema) sits in the datum; the proof travels in the redeemer with its disclosed indices and values; and the challenge nonce is derived from the `OutputReference` being spent, so a proof lifted from one transaction is useless in any other. That last move is the same replay defense the ZK page insists on for [proofs in redeemers](/docs/developers/curriculum/smart-contracts/advanced/zero-knowledge#what-to-watch-for): bind every proof to its context. Proof size is constant regardless of how many attributes the credential carries, and the dominant cost is a few pairings, which is what makes membership, compliance, and identity checks with selective disclosure practical inside a Plutus budget. ## Key takeaways - The BLS12-381 builtins are a **protocol construction kit**, not just a SNARK verifier: signatures, KDFs, VRFs, and credentials all compose from the same five operations. - **BLS aggregation changes the economics of many-party signing**: one 96-byte signature for any number of signers, and constant two-pairing verification when everyone signs the same message. Pick the IETF mode by your key-trust model; the rogue-key attack is what the modes exist for. - **On-chain KDFs are for already-public or high-entropy inputs.** Datums and redeemers are public forever, so passwords are hashed off-chain, always. - **A VRF gives applications what Ouroboros already has**: deterministic, unpredictable, publicly verifiable randomness, now checkable inside a validator. - **BBS+ selective disclosure** is the first credential primitive on this list with no other home in the Cardano stack: prove attributes, keep the credential. :::info Research-grade, and moving fast As with the ZK verifiers, the builtins themselves sit on an audited library, but every protocol library on this page (`ilap/bls`, the CF examples, `cardano-bbs`) is unaudited, experimental code. Prototype on testnets, read the code you depend on, and treat cost figures as measured snapshots, not guarantees. ::: ## Next steps - [Build a dApp](/docs/developers/curriculum/dapps/overview): the next module. Put your contracts in front of users: wallets, payments, oracles, and agents. --- ## Debugging CBOR ## Introduction :::info This article assumes that you are familiar with the EUTxO model and with Cardano transactions at an abstract level (e.g. you can understand the information shown in a blockchain explorer). ::: :::note This article is a modernized version of the joinplank [blog post](https://www.joinplank.com/articles/debugging-plutus-an-introduction-to-low-level-cardano-transactions-in-the-alonzo-era) ([conway era CDDL](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl) specifications instead of Alonzo) and shares a [section](https://aiken-lang.org/language-tour/troubleshooting#cbor-diagnostic) from Aiken documentation ::: The first thing to know about the specification is that transactions are defined and serialized using the CBOR format, defined first in [RFC 7049](https://www.rfc-editor.org/rfc/rfc7049) and then updated in [RFC 8949](https://www.rfc-editor.org/rfc/rfc8949). CBOR stands for Concise Binary Object Representation, a data format that can be seen as a "binary JSON". This binary representation allows for more compact messages at the cost of human readability. Fortunately, CBOR messages can be easily encoded and decoded using any existing [implementation](https://cbor.io/impls.html) of the CBOR protocol in a variety of languages, but also online by using the [CBOR playground](https://cbor.me/). CBOR is all around Cardano, as transactions themselves are encoded using this format, also do the Plutus data inside them, and even complete blocks of transactions are. ## Cardano specification/s As Cardano has passed through different eras, the specification is split into several documents, one for each era, describing incrementally the modifications and additions that each era introduced. For each era there is also as part of the specification a text file that precisely defines the CBOR schema used for blocks and transactions. For this, the CDDL (Concise Data Definition Language) format is used, a notational convention defined in [RFC 8610](https://www.rfc-editor.org/rfc/rfc8610) that is used to describe CBOR data structures. This page covers the Conway era specification, the current ledger era. Smart contract support through Plutus was introduced earlier, in the Alonzo era; Conway added Cardano's on-chain governance. ## A simple smart contract To learn about Conway transactions we will take an example transaction related to a very simple smart contract designed for this purpose. The smart contract is just a single script UTxO that holds an integer in its datum. It admits two operations: increment, to add 1 to the integer, and decrement, to subtract 1 from it. The UTxO also holds in its value an NFT we only use to identify it. The example transaction we provide is an increment operation. It increments the integer from 47 to 48 in the datum, while preserving the NFT in the value. It can be illustrated the following way: ![Tx-Diagram](../img/cbor-1.png) Here, besides the script input and output, there are also wallet input and output, that are used to pay for the fees and get back the change. ### The raw transaction The transaction was made in the preprod testnet, and it can be found in the Cardano explorer with hash [8ae88d7ee59eda5a7a95dd66e9cf123a89758f2ec31e73a5c65b4d9cf312f71c](https://explorer.cardano.org/transaction?id=8ae88d7ee59eda5a7a95dd66e9cf123a89758f2ec31e73a5c65b4d9cf312f71c&network=preprod). As the transaction is in CBOR format, we can decode it using the CBOR playground to find something that looks like this: ```text [ { 0: [[h'A1D13B016FD106784482D2B2E1C85330090B3C27464D2163F752AD42730A2867', 0], [h'A51F7E6EC66F1DB366F2A7AD63B8041B51B269CEBD5D52A140F6C5B7E069DFB7', 1]], 1: [[h'706EB7C29D9362DD710902977BB645EB97BB76C6246768851E76489F1A', [2000000, {h'725BA16E744ABF2074C951C320FCC92EA0158ED7BB325B092A58245D': {h'': 1}}], h'B034C17CF9EEF7E2D38FFF1EC8956C3A3C9FECE616E1CE03DF5860FEE81ADB1E'], [h'002D106290A8EAEB46BDC5A5AD92401306263E77FAA00D3E7E60055C0784064433AEAE5D686016D7CA93DB82FD72D83AA67E55511B0AE9C3F8', 1247328024]], 2: 473726, 11: h'E1D6B63A31FD6FBE2BF9A7403160BEF41A6E03B31776BB6B140BF0C1B19494D4', 13: [[h'834E3714ED521A76CCAD0DFECCAB998C25EA068BFE120C2DDCF32D4555F79A6B', 7]] }, { 0: [[h'41BF65F9DBEACE48BDC6ABA5CC9149C16077EE2338A97292414FD0C1BB7D797E', h'F2F0A179D98C04B51BC1C7A891708ADFEF9EB28818054E28FCA6CF769518E4DD148ED024208CA33F5ED996BD1A0A6AF46E1A941BDD54061EC47CA10A1D8D2C00']], 3: [h'5910FE010000332323232323232323232323232323322323232322232232322323253353330093333573466E1CD55CEA803A4000464646666AE68CDC39AAB9D5001480008DD69ABA135573CA004464C6A66AE7007C0780740704DD50009ABA135573CA010464C6A66AE7007006C068064CCCD5CD19B875004480188488880108CCCD5CD19B875005480108C848888C004014CCD54069D73AE357426AAE79401C8CCCD5CD19B8750064800884888800C8CCCD5CD19B875007480008488880088C98D4CD5CE00F80F00E80E00D80D00C9999AB9A3370E6AAE754009200023322123300100300232323232323232323232323333573466E1CD55CEA8052400046666666666444444444424666666666600201601401201000E00C00A00800600466A02E464646666AE68CDC39AAB9D5002480008CC8848CC00400C008C088D5D0A801180E1ABA135744A004464C6A66AE700B00AC0A80A44D55CF280089BAA00135742A01466A02E0306AE854024CCD54069D7280C9ABA150083335501A75CA0326AE85401CCD405C088D5D0A80319A80B99AA812811BAD35742A00A6464646666AE68CDC39AAB9D5002480008CC8848CC00400C008C8C8C8CCCD5CD19B8735573AA004900011991091980080180119A8143AD35742A00460526AE84D5D1280111931A99AB9C03002F02E02D135573CA00226EA8004D5D0A8011919191999AB9A3370E6AAE754009200023322123300100300233502875A6AE854008C0A4D5D09ABA2500223263533573806005E05C05A26AAE7940044DD50009ABA135744A004464C6A66AE700B00AC0A80A44D55CF280089BAA00135742A00866A02EEB8D5D0A80199A80B99AA812BAE200135742A004603E6AE84D5D1280111931A99AB9C028027026025135744A00226AE8940044D5D1280089ABA25001135744A00226AE8940044D5D1280089ABA25001135573CA00226EA8004D5D0A8011919191999AB9A3370EA0029003119091111802002980D1ABA135573CA00646666AE68CDC3A8012400846424444600400A60386AE84D55CF280211999AB9A3370EA0069001119091111800802980C1ABA135573CA00A46666AE68CDC3A8022400046424444600600A6EB8D5D09AAB9E500623263533573804604404204003E03C03A26AAE7540044DD50009ABA135744A004464C6A66AE7007006C06806440684C98D4CD5CE2481035054350001A019135573CA00226EA80044D55CEA80089BAA001137540022464460046EB0004C8004D5405088CCCD55CF80092804919A80418021ABA100230033574400402646464646666AE68CDC39AAB9D5003480008CCC88848CCC00401000C008C8C8C8CCCD5CD19B8735573AA0049000119910919800801801180A9ABA1500233500E014357426AE8940088C98D4CD5CE00C80C00B80B09AAB9E5001137540026AE85400CCCD5401DD728031ABA1500233500A75C6AE84D5D1280111931A99AB9C015014013012135744A00226AAE7940044DD5000899AA800BAE75A224464460046EAC004C8004D5404888C8CCCD55CF80112804119A80399AA80A98031AAB9D5002300535573CA00460086AE8800C0484D5D080088910010910911980080200189119191999AB9A3370EA0029000119091180100198029ABA135573CA00646666AE68CDC3A801240044244002464C6A66AE7004404003C0380344D55CEA80089BAA001232323333573466E1CD55CEA80124000466442466002006004600A6AE854008DD69ABA135744A004464C6A66AE7003803403002C4D55CF280089BAA0012323333573466E1CD55CEA800A400046EB8D5D09AAB9E500223263533573801801601401226EA8004488C8C8CCCD5CD19B87500148010848880048CCCD5CD19B875002480088C84888C00C010C018D5D09AAB9E500423333573466E1D400D20002122200223263533573801E01C01A01801601426AAE7540044DD50009191999AB9A3370EA0029001109100111999AB9A3370EA0049000109100091931A99AB9C00B00A009008007135573A6EA80048C8C8C8C8C8CCCD5CD19B8750014803084888888800C8CCCD5CD19B875002480288488888880108CCCD5CD19B875003480208CC8848888888CC004024020DD71ABA15005375A6AE84D5D1280291999AB9A3370EA00890031199109111111198010048041BAE35742A00E6EB8D5D09ABA2500723333573466E1D40152004233221222222233006009008300C35742A0126EB8D5D09ABA2500923333573466E1D40192002232122222223007008300D357426AAE79402C8CCCD5CD19B875007480008C848888888C014020C038D5D09AAB9E500C23263533573802602402202001E01C01A01801601426AAE7540104D55CF280189AAB9E5002135573CA00226EA80048C8C8C8C8CCCD5CD19B875001480088CCC888488CCC00401401000CDD69ABA15004375A6AE85400CDD69ABA135744A00646666AE68CDC3A80124000464244600400660106AE84D55CF280311931A99AB9C00C00B00A009008135573AA00626AE8940044D55CF280089BAA001232323333573466E1D400520022321223001003375C6AE84D55CF280191999AB9A3370EA004900011909118010019BAE357426AAE7940108C98D4CD5CE00480400380300289AAB9D5001137540022244464646666AE68CDC39AAB9D5002480008CD54028C018D5D0A80118029ABA135744A004464C6A66AE7002402001C0184D55CF280089BAA0014984800524103505431001122123300100300211232300100122330033002002001332323322332232323232332232323232323232323232323232323232332232323232323232323232222232323232323253333500815335333573466E1CCDC3004A400890010170168817099AB9C49011C5374617465206E6F742076616C696420666F7220636C6F73696E672E0002D15335300D0072135001223500122253353330170120023550082220021500715335335738920115496E76616C6964206F75747075742076616C75652E0003315007103313562615335300D00721350012235001222533533301701200235500A222002150091533533573892115496E76616C6964206F75747075742076616C75652E00033150091033135626232323232153353232325335333573466E20044CDC0000A400806A06C2A0042A66A666AE68CDC480899B81001480100D80D45400840D4D4044880044CCD5CD19B883322333355002323350272233350250030010023502200133502622230033002001200122337000029001000A4000603C2400266062A06C002900301A019A8058A8008A99A99AB9C491225374617465206E6F742076616C696420666F72206D696E74696E67207072697A652E000321500110321533533301501032323355301D1200123500122335503A002335530201200123500122335503D00233350012330374800000488CC0E00080048CC0DC0052000001330180020015004500A355009222002150011533533573892115496E76616C6964206F75747075742076616C75652E0003115001103115335333573466E1C030D540208894CD400484D4038894CD4CC06000C008854CD4C0A400484004540CC540C8540BC0C40C05400454CD4CD5CE248115496E76616C6964206F75747075742073746174652E000301500110301533533301300E500135009223500222222222220071030133573892011E546865207072697A65206973206E6F74206265696E67206D696E7465642E0002F13500122335032335503400233503233550340014800940CD40CC54CD4CCD5CD19B8753333500710081337020109001099B800084800884024D540048894CD400484D4028894CD4CC05000C008854CD4C09400484004540A4540A0540940B40B040B44CD5CE248115496E76616C6964206F75747075742073746174652E0002C153353009005130204988854CD40044008884C0912615335333573466E1D4CCCD401440184CDC080324004266E00019200221007355001222533500121350082253353301200300221533530230012100115029150281502502B02A102B133573892115496E76616C6964206F75747075742073746174652E0002A153353007003130204988854CD40044008884C09126153353006002130214988854CD40044008884C09526153353007001213500122350012220021356262350012235002222222222253353301000A00B2135001223500122233355301C12001223500222235008223500522325335335005233500425335333573466E3C0080041081045400C410481048CD4010810494CD4CCD5CD19B8F002001042041150031041133504100A0091009153350032153350022133500223350022335002233500223303200200120442335002204423303200200122204422233500420442225335333573466E1C01800C11C11854CD4CCD5CD19B8700500204704613302500400110461046103F153350012103F103F503800F132635335738921024C660002302222333573466E1C00800409008C8D400488D40088888888888CC03802802C88CCCD40049407094070940708CCD54C0344800540148D4004894CD54CD4CCD5CD19B8F350022200235004220020260251333573466E1CD400888004D40108800409809440944D408000C5407C00C88D400488888888894CD4CCD54C0544800540348D4004894CD4CCD5CD19B8F00200F02E02D135028003150270022135026350012200115024133500E225335002210031001501722233355300A120013500F500E2350012233355300D1200135012501123500122333500123300A4800000488CC02C0080048CC028005200000133004002001223355300712001235001223355024002333500123355300B1200123500122335502800235500D0010012233355500801000200123355300B1200123500122335502800235500C00100133355500300B00200111122233355300412001501F335530071200123500122335502400235500900133355300412001223500222533533355300C1200132335013223335003220020020013500122001123300122533500210251001022235001223300A002005006100313350230040035020001335530071200123500122323355025003300100532001355025225335001135500A003221350022253353300C002008112223300200A004130060030023200135501E221122253350011002221330050023335530071200100500400111212223003004112122230010043200135501B22112253350011501D22133501E300400233553006120010040013200135501A22112225335001135006003221333500900530040023335530071200100500400112350012200112350012200222333573466E3C008004054050448CC004894CD40084004405004C48CD400888CCD400C88008008004D40048800448848CC00400C0088C8C8CCCCCCD5D200191999AB9A3370E6AAE75400D2000233335573EA0064A01C46666AAE7CD5D128021299A9919191999999ABA400323333573466E1CD55CEA801A400046666AAE7D400C940548CCCD55CF9ABA250042533532333333357480024A0304A0304A03046A0326EB400894060044D5D0A802909A80C0008A80B1280B0078071280A0061280992809928099280980609AAB9E5001137540026AE85401484D40440045403C9403C02001C94034014940309403094030940300144D55CF280089BAA001498480048D58984D58988D58984D58988D589848488C00800C44880044D589888CDC0001000990009AA803911299A80088011109A8011119803999804001003000801990009AA8031111299A80088011109A80111299A999AB9A3370E00290000050048999804003803001899980400399A80589199800804001801003001891001091000889100109109119800802001889109198008018010891918008009119801980100100099A89119A8911980119AA80224411C725BA16E744ABF2074C951C320FCC92EA0158ED7BB325B092A58245D00488100481508848CC00400C0088004448848CC00400C0084480041'], 4: [121([121([47])]), 121([121([48])])], 5: [[0, 0, 121([]), [1302238, 360901332]]] }, true, null ] ``` Each line has a meaning. This page walks through them, pointing to where each is specified. ## Transactions in the Conway era The main reference to understand Cardano transactions are the CDDL specification, where we can find the [transaction definition](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L17): ```text transaction = [ transaction_body , transaction_witness_set , bool , auxiliary_data / null ] ``` Note that transactions are comprised of four parts. The two main parts of a transaction are its body and its witness set. This page focuses on these parts and ignores the other two, just saying that the third one has to do with transaction validity, and the fourth one is where the metadata goes, among other things. ### The transaction body We can find the schema for the transaction body at [line 130](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L130) of the CDDL specification: ```text transaction_body = { 0 : set , 1 : [* transaction_output] , 2 : coin , ? 3 : slot_no , ? 4 : certificates , ? 5 : withdrawals , ? 7 : auxiliary_data_hash , ? 8 : slot_no , ? 9 : mint , ? 11 : script_data_hash , ? 13 : nonempty_set //the inputs that can be collected if phase-2 script validation fails , ? 14 : required_signers , ? 15 : network_id , ? 16 : transaction_output , ? 17 : coin , ? 18 : nonempty_set , ? 19 : voting_procedures , ? 20 : proposal_procedures , ? 21 : coin , ? 22 : positive_coin } ``` So, the transaction body is a map from integer keys to values of different types. Some of the entries, marked as '?', are optional. The mandatory parts are the inputs (field 0), the outputs (field 1) and the fee (field 2). In the smart contract example above, also the fields 11 (`script_data_hash`) and 13 (`nonempty_set` meaning -> collateral) are present, which are always required if the transaction involves script execution. ### Transaction inputs Transaction inputs are listed in field 0 of the transaction body. A transaction input, you probably know, is a reference to a UTxO (Unspent Transaction Output). In other words, it is the output of a previous transaction that was not spent yet (i.e. used as input) by any other transaction. According to the [CDDL](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L156), a reference to a UTxO is defined by the following pair: ```text transaction_input = [ transaction_id : $hash32 , index : uint ] ``` where transaction_id is the hash of the transaction that generated the UTxO, and index is the index in the list of its outputs (starting from 0, obviously). In our example, two inputs are present: ```text 0: [[h'A1D13B016FD106784482D2B2E1C85330090B3C27464D2163F752AD42730A2867', 0], [h'A51F7E6EC66F1DB366F2A7AD63B8041B51B269CEBD5D52A140F6C5B7E069DFB7', 1]], ``` So, the first input is the UTxO corresponding to the first output of transaction [A1D13B...](https://explorer.cardano.org/transaction?id=a1d13b016fd106784482d2b2e1c85330090b3c27464d2163f752ad42730a2867&network=preprod), and the second one corresponds to the second output of transaction [A51F7E...](https://explorer.cardano.org/transaction?id=a51f7e6ec66f1db366f2a7ad63b8041b51b269cebd5d52a140f6c5b7e069dfb7&network=preprod ). As we will see next, one of them corresponds to the smart contract, and the other one is used to pay for the transaction fees. #### Inputs information To be able to understand and debug our transaction, it is important to know the information about the inputs, not present in the transaction itself. The information is comprised of these three components: - The address encoding the owner of the input, that for a wallet UTxO it is a pubkey hash and for a script UTxO it is the hash of the validation script. - The value it holds, a number of ADA and maybe some other assets. - An optional datum hash, in the case the UTxO encodes some data. In our example, we can find this information by navigating the Cardano explorer: **Input ([A1D13B...](https://explorer.cardano.org/transaction?id=A1D13B016FD106784482D2B2E1C85330090B3C27464D2163F752AD42730A2867&network=preprod), 0)**: - Address: [addr_test1wpht0...](https://preprod.cexplorer.io/address/addr_test1wpht0s5ajd3d6ugfq2thhdj9awtmkakxy3nk3pg7weyf7xs6nm2gz) (a script UTxO) - Value: 2 ADA and an [unnamed NFT](https://preprod.cexplorer.io/asset/asset17s6927lhfd39y874f3psjfkz7ds444lw6g5gxt) - Datum hash: 8cf95d... (encodes the integer 47, see section "The Plutus data" below) **Input ([A51F7E...](https://explorer.cardano.org/transaction?id=a51f7e6ec66f1db366f2a7ad63b8041b51b269cebd5d52a140f6c5b7e069dfb7&network=preprod), 1)**: - Address: addr_test1qqk3qc... (a wallet UTxO) - Value: 1,247.80 ADA - Datum hash: Not present. From this information, it is clear that the first input is used to pay for the transaction, and the second one is the "smart contract". Usually, when the off-chain code of a dapp builds a transaction, the inputs used to pay for it are introduced in a last stage called "balancing". As a wallet may have several UTxOs, selecting which one/s will be used to pay is a complex subject called "coin selection", something that is extensively discussed in [CIP 2](https://cips.cardano.org/cips/cip2/). #### The ordering of the inputs In the CDDL specification you can see that the inputs are in a set, not a list. Why a set? Well, Cardano doesn't allow us to choose how to order the inputs. The ordering we use in the serialized raw transaction is completely ignored. Instead, the specifications assumes that the inputs are ordered lexicographically in the pair (transaction_id, index). This is important, because in the redeemers we will use indexes to refer to positions in the list of inputs following this ordering criteria. We will talk about this later. ### Transaction outputs Transaction outputs (field 1) are a bit more complex than inputs, as they are new UTxOs that are being created by the transaction. Their [CDDL](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L165) specification is: ```text transaction_output = [ address , amount : value , ? datum_hash : $hash32 ] ``` The components are: the raw value of the address where the UTxO is paid to, the value it will contain and an optional datum hash it can also carry. The datum hash can be used to encode data in the UTxO, and was introduced in Alonzo to store "state" information for script UTxOs (Babbage later added inline datums, CIP-32). While not forbidden, datum hashes are rarely used in wallet UTxOs. In the example we have two outputs: **Output 0**: - Raw address: [706EB7...](https://preprod.cexplorer.io/address/addr_test1wpht0s5ajd3d6ugfq2thhdj9awtmkakxy3nk3pg7weyf7xs6nm2gz) - Value: `[2000000, {h'725BA1...': {h'': 1}}]` - Datum hash: B034C1... (encodes the integer 48, see section "The Plutus data" below) **Output 1**: - Raw address: [002D10...](https://preprod.cexplorer.io/address/addr_test1qqk3qc5s4r4wk34ackj6myjqzvrzv0nhl2sq60n7vqz4cpuyqezr8t4wt45xq9khe2fahqhawtvr4fn724g3kzhfc0uqyagtkd) - Value: 1247328024 The first one corresponds to the script address where the smart contract lives. The value is a list because it does not contain only ADA but also another asset: An NFT with currency symbol [725BA1...](https://preprod.cexplorer.io/asset/asset17s6927lhfd39y874f3psjfkz7ds444lw6g5gxt) (aka policy) and an empty token name. You can see how values are specified in [209](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L209) of the CDDL. The datum hash is encoding the new integer value: 48. The second output is the "change", the remaining ADA value that goes back to the wallet that paid for the transaction. This output is usually introduced in the balancing stage of the transaction building process. ### The script data hash Field 11 of the transaction body is the script data hash, also called `ScriptIntegrityHash` in the specification document. This hash encodes information that determines the results of scripts execution otherwise not present in the body. The encoding includes the redeemers and the data, both from the witnesses (see below), but also the protocol parameters that determine the costs and limits for script execution. Computing this field is a bit complex, and all transaction libraries that support Plutus do it for you. Note that any modification to a transaction that alters the script data hash requires recomputing and updating this field. ### Collaterals Transaction validation is divided into two phases, where phase 1 involves all basic checks for transaction correctness, and phase 2 is comprised of the execution of all the involved Plutus scripts. If the validation in phase 2 fails, the transaction is rejected but a penalty must be applied to cover the execution costs (and discourage failing transactions). This is the collateral, a set of inputs that is spent in this case. Collaterals (field 13) must be wallet UTxOs, can only contain ADA and the included signatures must allow their spending. An interesting observation is that the same inputs can be used as regular inputs and as collateral, because only one of the two sets will be spent. In our example, the collateral is a single input with 5 ADA, a standard amount for collaterals. ### Required signers The required signers (field 14) is a set of hashed keys that can be used to require additional signatures besides those required to spend wallet UTxOs. If a key is present but the corresponding signature is not in the witness set, the transaction will fail in phase 1. The required signers set is also made available to the Plutus script executions through the "script context". This way, validation scripts can do indirect checks on the presence of signatures, as the script context doesn't explicitly include them. Our example doesn't have required signers, but it is an important component because checking for signatures is a frequent requirement in smart contracts. ### Other relevant body fields So far we only covered fields 0, 1, 11, 13 and 14 of the transaction body. Of course, there are other important fields. We briefly describe here the ones we find interesting: - Field 2: The fee paid by this transaction (in Lovelace). It must be enough to cover for the costs related to transaction size and script execution units. - Fields 3 and 8: The "time to live" (TTL) and the "validity interval start". Together, they form the ValidityInterval defined in the specification document (Fig. 2). It is the slot range where we expect the transaction to be executed, and phase 1 will fail if it is not the case. If phase 1 succeeds, the interval information is then converted to a POSIX time range and passed to the scripts, allowing for phase 2 checks. - Field 9: The minted value, all assets that are being minted or burned in the transaction. Minting can be done using pre-Alonzo simple scripts (native scripts) (defined in field 1 of the witness set) or using minting policies (included in field 3 for the witness set). For the latter, redeemers must be specified, and for this a lexicographical ordering in the policy IDs is assumed (see below in section about redeemers). #### Witness set Here is the CDDL specification for the [witness set](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L652): ```text transaction_witness_set = { ? 0 : nonempty_set , ? 1 : nonempty_set , ? 2 : nonempty_set , ? 3 : nonempty_set , ? 4 : nonempty_set , ? 5 : redeemers , ? 6 : nonempty_set , ? 7 : nonempty_set } ``` You can see that all fields are optional. However, field 0 will be present as it contains the transaction signatures and at least one signature is always required. For Plutus, the most relevant fields are the last three, so we address them in the following subsections. ## Plutus scripts Field 3 is the list of Plutus scripts, this is, the binaries of the Plutus Core code for all the Plutus scripts that must be executed to validate the transaction, both for consuming script UTxOs and for minting Plutus assets. Plutus scripts are without doubt the biggest part of Conway transactions and an important source of headache for any engineer trying to develop a meaningful dapp without hitting the transaction size limit (`maxTxSize`, currently ~16 kB). For instance, in our example the Plutus script takes up to 4353 bytes, more than 45% of the total transaction size (9666 bytes). The Babbage era introduced "reference scripts" [CIP 33](https://cips.cardano.org/cip/CIP-33), a feature that provides a way to use scripts without the need for explicitly including them in transactions; see [reference scripts](/docs/developers/curriculum/smart-contracts/lock-and-spend#reference-scripts) for how to use them. ### Plutus data Field 4 is the Plutus data, a list that has the unhashed datums of all datum hashes present in the transaction inputs and outputs. These datums are made available to the Plutus validators through the "script context", so checks can be made on them. In the example, the Plutus data is: ```text 4: [121([121([47])]), 121([121([48])])], ``` The first one corresponds to the datum hash of the script input, and the second one to the datum hash of the script output. In these datums, the integers are wrapped into some other data constructors, but this is just the way we chose to encode them, and has to do with the Haskell data structures we defined for the contract state. ### Redeemers Field 5 is the list of redeemers. Each redeemer refers to the execution of a Plutus script. The CDDL specifies that a [redeemer](https://github.com/IntersectMBO/cardano-ledger/blob/c2b7ea777317dd1dfeba576d044be2cbe742d9a8/eras/conway/impl/cddl-files/conway.cddl#L677) is as follows: ```text redeemers = [ + [ tag : redeemer_tag , index : uint .size 4 , data : plutus_data , ex_units : ex_units ] ``` So, a redeemer is a 4-uple with the following components: - tag: It specifies the type of redeemer. In the Conway era there are six values: 0 spend (a script UTxO), 1 mint (minting/burning with a Plutus policy), 2 cert (certificates), 3 reward (stake reward withdrawals), 4 vote (governance votes), and 5 propose (governance proposals). This example uses tags 0 and 1. - index: It is an integer with a different meaning depending on the tag. For the spending tag (value 0), the index refers to the position in the inputs list after ordering it lexicographically according to the TxId and TxIdx. For the minting tag (value 1), the index refers to the position in the lexicographically ordered list of policy IDs present in the minting field. - data: This is arbitrary data that is passed as a parameter to the script. Most times this data is what is actually called the "redeemer", instead of the complete 4-uple. - ex_units: The budget for the script execution in memory and CPU units. These numbers are used to compute the fee and must be higher or equal to the actual units used by the script execution. Execution units are computed according to the cost model, part of the protocol parameters of the Cardano blockchain. There is also a limit of the total memory and CPU units that all redeemers of a transaction can use. Also defined in the protocol parameters, Alonzo started with limits of 10,000,000,000 steps for CPU and 10,000,000 units for memory. The per-transaction memory limit has since been raised in stages to the current mainnet value of 16,500,000 (steps remain 10,000,000,000). Execution units and their limits are a big issue in Cardano, as they impose important restrictions to smart contracts and developers must pay special attention to on-chain code optimization. In our example, we have only one redeemer for spending the script UTxO that is the first input according to the lexicographic order: ```text 5: [[0, 0, 121([]), [1302238, 360901332]]] ``` We use `121([])` as the redeemer data to indicate to the script that we are trying to perform an increment operation. If it was `122([])`, it would be a decrement operation. The script will validate, among other things, that the datum is updated according to the operation. The execution budget is 1302238 memory units and 360901332 CPU units, and was obtained in the balancing stage by doing an off-chain run of the validation. ## Trivia To finish, try answering these questions. They are a good way to test your understanding of the specification. - Is it possible to successfully submit a transaction with no wallet inputs? - Why it is not possible to successfully submit a transaction without signatures? - Is it possible to successfully submit a transaction with an empty list of inputs? - Is it possible to successfully submit a transaction with an empty list of outputs? Answer True or False to the following assertions: - Every transaction needs a collateral. - Every transaction with script inputs needs a collateral. - No transaction with no script inputs needs a collateral. ## Runtime CBOR debugging with Aiken While the previous sections focused on analyzing transactions at the blockchain and understanding what is CBOR and how to interpret it, developers need to debug CBOR data during smart contract development. This section covers techniques for inspecting individual values and data structures as you build and test your contracts. :::info You can read more in the [Aiken CBOR diagnostic](https://aiken-lang.org/language-tour/troubleshooting#cbor-diagnostic) section. ::: ### CBOR diagnostics in Aiken When developing smart contracts with Aiken, compiled programs lose type information and variable names, making runtime inspection challenging. However, Aiken provides the `cbor.diagnostic()` function to inspect values at runtime using CBOR diagnostic notation - a human-readable representation of binary CBOR data. ```aiken use aiken/cbor pub fn diagnostic(data: Data) -> String ``` CBOR diagnostics use a JSON-like syntax that can represent binary data. For example, the serialized bytes `83010203` appear as `[1, 2, 3]` in diagnostic notation. ### CBOR diagnostic syntax reference | Type | Examples | |------|----------| | Int | `1`, `-14`, `42` | | ByteArray | `h'FF00'`, `h'666f6f'` | | List | `[]`, `[1, 2, 3]`, `[_ 1, 2, 3]` | | Map | `{}`, `{ 1: h'FF', 2: 14 }`, `{_ 1: "AA" }` | | Tag | `42(1)`, `10(h'ABCD')`, `1280([1, 2])` | **Tags** are particularly important for custom types on-chain. Aiken uses tag 121 for the first constructor of a data type, 122 for the second, and so forth. The tagged content represents the constructor's fields as a list. ### Practical examples Here are examples showing how Aiken values translate to CBOR diagnostics: ```aiken use aiken/cbor // Basic types cbor.diagnostic(42) == @"42" cbor.diagnostic("foo") == @"h'666F6F'" cbor.diagnostic([1, 2, 3]) == @"[_ 1, 2, 3]" cbor.diagnostic((1, 2)) == @"[_ 1, 2]" // Maps from list of tuples cbor.diagnostic([(1, #"ff")]) == @"{ 1: h'FF' }" // Option types using tags cbor.diagnostic(Some(42)) == @"121([_ 42])" // First constructor cbor.diagnostic(None) == @"122([])" // Second constructor ``` ### Testing datum and redeemer representations You can use CBOR diagnostics to verify the exact binary representation of your data structures: ```aiken type MyDatum { foo: Int, bar: ByteArray } test my_datum_representation() { let datum = MyDatum { foo: 42, bar: "Hello, World!" } cbor.diagnostic(datum) == @"121([42, h'48656c6c6f2c20576f726c6421'])" } ``` This diagnostic output can then be converted to raw CBOR using tools like [cbor.me](https://cbor.me) for use in transaction building. ### Integration with transaction analysis The diagnostic output from development tools directly corresponds to what you'll see in transaction CBOR: 1. **During development**: Use `cbor.diagnostic()` to inspect your datum/redeemer values 2. **In transactions**: These same values appear in the Plutus data field (field 4 of witness set) 3. **For debugging**: Convert between diagnostic notation and raw CBOR using online tools This creates a complete debugging workflow from contract development to transaction analysis. --- ## Linked List ## Introduction Storing lists in datums is generally impractical, as their growth can lead to unspendable UTxOs due to limited resources available on-chain. A linked list is a construct for storing an infinitely large array of elements on-chain, such that each element is represented with a UTxO that points to its immediate successor. Linked list structures leverage the EUTXO model to enhance scalability and throughput significantly. By linking multiple UTxOs together through a series of minting policies and validators, they improve the user experience when interacting with smart contracts concurrently. ### Structure Each element in the list is stored as a separate UTxO containing: - **NFT**: A unique authentication token identifying the element - **Datum**: An `Element` containing the element's data and a link (pointer) to the next element ![linked-list](img/linked-list-1.png) The list distinguishes between two kinds of elements: - **Root**: The first element of the linked list, holding `root_data` - **Node**: Any other element, holding `node_data` Each element's NFT asset name encodes its identity, the root uses a configurable `RootKey`, while nodes use a `NodeKeyPrefix` concatenated with their unique `NodeKey`. ### Insertion ![insert entry](img/linked-list-2.png) Inserting involves spending an existing anchor element and producing three UTxOs: - The anchor element, updated to point to the new node - The new node, pointing to what the anchor previously linked to - For ordered lists, key ordering is validated automatically ### Removal ![remove entry](img/linked-list-3.png) Removing a node requires spending both the node and its predecessor (anchor): - The anchor element is updated to point to what the removed node was pointing to - The removed node's NFT is burnt ### NFTs as Pointers NFTs serve as robust and unique pointers within the list. Their uniqueness is ensured by minting policies tied to the list's authentication policy. Each node's NFT asset name is derived from a prefix concatenated with its unique key. ### Key Considerations - **Efficiency**: On-chain lookups are inefficient; off-chain structures are recommended for indexing - **Security**: List integrity is maintained through minting policies, datum validation, and NFT authentication - **No reference scripts**: Elements are required to have no scripts attached to them, keeping the API manageable - **Address flexibility**: While continued anchor nodes are validated to go to the same address, new/removed nodes only validate that payment credentials match, allowing customization of staking parts ### Membership and non-membership proofs Sorting the list by key turns it into a proof structure. Because every node points to the next and keys only increase, a single node answers a membership question without traversing anything. To prove a key K **is** present, exhibit the node whose key equals K. To prove K is **absent**, exhibit the one node whose key is smaller than K while its link is larger: the two adjacent keys straddle K, so no node for K can sit between them. Either way the proof is one UTxO, read as a reference input, not a walk over the whole list. This is the reason to reach for the ordered `insert_ascending`/`insert_descending` over `append_unordered`: only a sorted list supports these proofs, and `get_element_info` is the primitive that reads a node's key and link so another script can check the straddle. That absence proof is what makes the list a uniqueness-enforcing set or a live registry. Ordered insertion can never place a duplicate key, so a policy that appends a node for every asset it mints guarantees no asset is minted twice. And because a proof is a single node read as a reference input, an unrelated contract can attest that some party is, or is not, already in a directory of registered participants without spending or scanning the list. ## Aiken Implementation The API handles all linked list structural validations internally, pointer updates, key ordering, NFT minting/burning, and element authentication. Your contract only needs to provide application-specific validations through callback functions (`additional_validations`). This is why the API does not expose granular helper functions, and only provides functions that perform primary linked list operations (e.g. `init`, `insert_ascending`, etc.). A good rule of thumb: if a validation is related to the linked list structure itself, the library has already taken care of it. The implementation uses a Reader monad-style pattern where operations return `Eval` or `RootEval` functions that must be finalized with environment constants (policy ID, root key, node key prefix). ### Key Types ```aiken /// The datum type for linked list UTxOs. Define your datum as: /// pub type Datum = linked_list.Element pub type Element { data: ElementData, link: Link, } pub type ElementData { Root { data: root_data } Node { data: node_data } } /// Asset name of the root element's NFT (32 bytes max). /// Keep in mind that some UTF-8 characters occupy more than 1 byte /// (e.g. the tree emoji 🌳 takes up 4 bytes). pub type RootKey = AssetName /// Key for linked list nodes (at least 1 byte, at most /// (32 - NodeKeyPrefixLength) bytes). Keys should be unique, the /// recommended approach is to use the hash of an input's output /// reference. If you need duplicate keys, proceed with extreme /// caution as linked list integrity can break without other means /// of preserving it. pub type NodeKey = ByteArray /// Bytes prefixing all node NFT asset names. Note that the total /// bytes allowed for node NFTs is 32 at most, so if your prefix /// occupies 4 bytes, keys can have 28 bytes at most. pub type NodeKeyPrefix = ByteArray /// Length of the NodeKeyPrefix in bytes. Use `const` to leverage /// Aiken compiler optimizations and avoid computing the length /// on-chain: /// const node_key_prefix_length = bytearray.length(node_key_prefix) pub type NodeKeyPrefixLength = Int /// Pointer to the next element pub type Link = Option /// Reader monad for operations that validate node key prefixes pub type Eval = fn(PolicyId, RootKey, NodeKeyPrefix, NodeKeyPrefixLength) -> Bool /// Reader monad for init/deinit (no node key prefix needed) pub type RootEval = fn(PolicyId, RootKey) -> Bool /// Reader monad with polymorphic return type pub type ElementEval = fn(PolicyId, RootKey, NodeKeyPrefix, NodeKeyPrefixLength) -> a ``` ### Initialization and De-initialization #### `init` Initialize a linked list by producing a root element UTxO with its authentication NFT. ```aiken pub fn init( nonce_validated: Bool, produced_element_output: Output, tx_mint: Value, root_validator: fn(Lovelace, Data) -> Bool, ) -> RootEval ``` - `nonce_validated`: guardrail confirming nonce validation has been performed - `produced_element_output`: the output UTxO that will hold the root element - `tx_mint`: transaction's mint field - `root_validator`: callback to validate Lovelace count and root data #### `deinit` Destroy an empty linked list by spending and burning the root element. ```aiken pub fn deinit( root_input: Input, tx_mint: Value, root_validator: fn(Lovelace, Data) -> Bool, ) -> RootEval ``` - `root_input`: the spent root element input - `tx_mint`: transaction's mint field - `root_validator`: callback to validate Lovelace count and root data ### Element Addition All addition functions validate NFT minting, pointer updates, address matching, and datum structure. The `additional_validations` callback receives context about the anchor element and the new node for application-specific checks. #### `insert_ascending` Insert a node maintaining ascending key order relative to the anchor element. ```aiken pub fn insert_ascending( anchor_element_input: Input, continued_anchor_element_output: Output, new_element_output: Output, tx_mint: Value, additional_validations: fn( LovelaceChange, Option, Data, Lovelace, NodeKey, NodeData, Link, ) -> Bool, ) -> Eval ``` The `additional_validations` callback receives: 1. Change in Lovelace count from anchor input to continued anchor output 2. Anchor element's key (`None` if anchor is root) 3. Underlying data of the anchor element 4. Lovelace count of the new node UTxO 5. Key of the new node 6. Underlying data of the new node 7. Link of the new node #### `insert_descending` Identical to `insert_ascending`, but the inserted key is expected to be less than the anchor's and greater than the anchor's link. ```aiken pub fn insert_descending( anchor_element_input: Input, continued_anchor_element_output: Output, new_element_output: Output, tx_mint: Value, additional_validations: fn( LovelaceChange, Option, Data, Lovelace, NodeKey, NodeData, Link, ) -> Bool, ) -> Eval ``` #### `append_unordered` Append a new element at the end of an unordered list. No key ordering is validated. ```aiken pub fn append_unordered( anchor_element_input: Input, continued_anchor_element_output: Output, new_element_output: Output, tx_mint: Value, additional_validations: fn( LovelaceChange, Option, Data, Lovelace, NodeKey, NodeData, ) -> Bool, ) -> Eval ``` #### `prepend_unordered` Prepend a new element at the start of an unordered list. The anchor must be the root element. ```aiken pub fn prepend_unordered( root_element_input: Input, continued_root_element_output: Output, new_element_output: Output, tx_mint: Value, additional_validations: fn( LovelaceChange, RootData, Lovelace, NodeKey, NodeData, Link, ) -> Bool, ) -> Eval ``` ### Element Removal #### `remove` Remove a node from the list. Expects two spent UTxOs: the node being removed and its anchor (predecessor) element. ```aiken pub fn remove( anchor_element_input: Input, removing_node_input: Input, continued_anchor_element_output: Output, tx_mint: Value, additional_validations: fn( LovelaceChange, Option, Data, Lovelace, NodeKey, NodeData, Link, ) -> Bool, ) -> Eval ``` The `additional_validations` callback receives the same values as `insert_ascending`, but the `NodeKey` and `NodeData` refer to the node being removed. ### Fold Operations #### `fold_from_root` Spend a root element and its linked node, reproduce the root while burning the node's NFT. Useful for accumulating data from nodes back into the root. ```aiken pub fn fold_from_root( anchor_root_input: Input, folding_node_input: Input, continued_anchor_root_output: Output, tx_mint: Value, additional_validations: fn( LovelaceChange, RootData, Lovelace, NodeKey, NodeData, Link, RootData, ) -> Bool, ) -> Eval ``` The `additional_validations` callback receives: 1. Change in Lovelace count from input root to output 2. Underlying data of the input root 3. Lovelace count of the folding node 4. Key of the folding node 5. Underlying data of the folding node 6. Link of the folding node 7. Underlying data of the reproduced root ### Data Updates #### `spend_for_updating_elements_data` Update an element's data without affecting the linked list structure. Uses the [UTxO Indexers](../utxo-indexers) pattern for one-to-one input/output mapping. ```aiken pub fn spend_for_updating_elements_data( element_input_index: Int, continued_element_output_index: Int, element_input_outref: OutputReference, inputs: List, outputs: List, tx_mint: Value, additional_validations: fn(LovelaceChange, Option, Data, Data) -> Bool, ) -> Eval ``` The `additional_validations` callback receives: 1. Change in Lovelace count from input to output 2. Possible node key (`None` if element is root) 3. Input underlying data 4. Updated data in the reproduced element ### Helpers #### `spend_for_adding_or_removing_an_element` Checks if any tokens are being minted or burnt under the linked list's policy. Used in spending validators to gate structural operations. ```aiken pub fn spend_for_adding_or_removing_an_element( list_nft_policy_id: PolicyId, tx_mint: Value, ) -> Bool ``` #### `get_element_info` Extract validated element information from a UTxO. Useful when another script needs to validate expenditure from the linked list. ```aiken pub fn get_element_info( element_utxo: Output, info_validations: fn(Lovelace, Option, Data, Link) -> a, ) -> ElementEval ``` The continuation receives: 1. Lovelace count 2. Possible node key (`None` if root) 3. Underlying data 4. Element's link ### Finalization Operations return `Eval` or `RootEval` values that must be finalized with environment constants. Define a helper like this: ```aiken use aiken_design_patterns/linked_list const node_key_prefix = "NODE" const node_key_prefix_length = bytearray.length(node_key_prefix) pub fn finalize_linked_list( eval: linked_list.Eval, list_nft_policy_id: PolicyId, root_key: RootKey, ) -> Bool { linked_list.run_eval_with( eval, list_nft_policy_id, root_key, node_key_prefix, node_key_prefix_length, ) } ``` Then use it in your minting contract: ```aiken let linked_list_eval = insert_ascending(...) expect linked_list_eval |> finalize_linked_list(own_policy_id, root_key) ``` The three finalization functions: ```aiken /// Finalize an Eval with all environment constants pub fn run_eval_with( reader: Eval, list_nft_policy_id: PolicyId, root_key: RootKey, node_key_prefix: NodeKeyPrefix, node_key_prefix_length: NodeKeyPrefixLength, ) -> Bool /// Finalize a RootEval (for init/deinit) pub fn run_root_with( reader: RootEval, list_nft_policy_id: PolicyId, root_key: RootKey, ) -> Bool /// Finalize an ElementEval with polymorphic return type pub fn run_element_with( reader: ElementEval, list_nft_policy_id: PolicyId, root_key: RootKey, node_key_prefix: NodeKeyPrefix, node_key_prefix_length: NodeKeyPrefixLength, ) -> a ``` ## Example Code Library implementation: [linked_list module](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/linked-list.ak) Test suite: [linked_list tests](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/tests/linked-list.ak) ## Acknowledgments This documentation and the linked list implementation draw inspiration from original ideas presented in the Plutonomicon. For further details on the foundational concepts, see the [Plutonomicon's Associative Data Structures Overview](https://github.com/Plutonomicon/plutonomicon/blob/main/assoc.md#overview). --- ## Merkelized Validator ## Introduction There are very tight execution budget constraints imposed on Plutus script evaluation; this, in combination with the fact that a higher execution budget equates to higher transaction fees for end-users makes it such that ex-unit optimization is an extremely important component of smart contract development on Cardano. Often the most impactful optimization techniques involve trade-offs between ex-units and script size. This results in a tight balancing act where you want to minimize the ex-units while keeping the script below the current ~16 kB limit (script size that you can store as a reference script is limited by transaction size limit). Powerful ExUnit optimizations such as unrolling recursion, inlining functions and preferring constants over variables all can drastically reduce ExUnit consumption at the cost of increasing script size. We can take advantage of reference scripts and the withdraw-zero trick to separate the logic (and code) of our validator across a number of stake scripts (which we provide as reference inputs). Then our main validator simply checks for the presence of the associated staking script in the redeemers (and verifies that the redeemer to the scripts are as expected) where necessary to execute the branch of logic. This is useful because with reference scripts this essentially gives us the ability to create scripts with near infinite size which means optimization strategies that involve increasing script size to reduce mem / CPU (ie loop unrolling) now are available to us. The referenced bytes still pay the [tiered reference script fee](/docs/developers/curriculum/fundamentals/core-concepts/fees#reference-script-fees), but that is far below carrying the script inline. Consider a batching architecture, with a very large `processOrders` function. Normally it would not be feasible to perform recursion unrolling / inlining optimizations with such a function since it would quickly exceed the max script size limit; however, with this design pattern we simply move `processOrders` into its own validator script which we can fill with 16kb of loop unrolling and other powerful optimizations which increase script size in order to reduce ExUnits. We provide this new script as a reference script when executing our main validator. Then in our main validator we verify that the `processOrders` validator was executed with the expected redeemer (`input_arg` must match the arguments we want to pass to `processOrders`) after which we have access to the result of the optimized `processOrders` function applied to our inputs. ## Aiken Implementation Since transaction size is limited in Cardano, some scripts benefit from a solution which allows them to delegate parts of their validations. This becomes more prominent in cases where such validations can greatly benefit from optimization solutions that trade computation resources for script sizes (e.g. table lookups can take up more space so that costly computations can be averted). This design pattern offers an interface for off-loading such validations into an external observer/withdrawal script, so that the sizes of the scripts themselves can stay within the limits of Cardano. :::note Be aware that total size of reference scripts is currently limited to 200KiB (204800 bytes), and they impose [per-byte fees that escalate in tiers](/docs/developers/curriculum/fundamentals/core-concepts/fees#reference-script-fees). ::: ### Key Types Datatype for the redeemer of the "computation staking validator" to represent input argument(s) and output value(s). As a simple example, a summation logic where it adds all its inputs together can work with a redeemer of type `ComputationRedeemer, Int>`, and a valid redeemer data would be: ```aiken let valid_summation_io = ComputationRedeemer { input_arg: [1, 2, 3, 4, 5], result: 15, } ``` The library defines two redeemer types for the staking scripts: ```aiken /// Datatype for redeemer of the "computation staking validator" to represent /// input argument(s) and output value(s). pub type ComputationRedeemer { input_arg: a, result: b, } /// Datatype for a delegated validation. Compared to `ComputationRedeemer`, this /// datatype only carries input argument(s), and simply validates whether the /// computation passes. pub type ValidationRedeemer { input_arg: a, } ``` ### Delegating Computation: `delegated_compute` Given an arbitrary `Data` as input, this function expects to find a `Withdraw` script purpose in `redeemers` for `staking_validator`, with a redeemer of type `ComputationRedeemer`, which will be coerced into your custom datatypes using your provided `Data` validators (`input_data_coercer` and `output_data_coercer`). The given input argument must be identical to the one provided to the withdrawal validator. It returns the coerced result. ```aiken pub fn delegated_compute( function_input: a, staking_validator: ScriptHash, redeemers: Pairs, redeemer_index: Int, input_data_coercer: fn(Data) -> a, output_data_coercer: fn(Data) -> b, ) -> b ``` ### Delegating Validation: `delegated_validation` Similar to `delegated_compute`, with the difference that no values are expected to be returned by the staking script: ```aiken pub fn delegated_validation( function_input: a, staking_validator: ScriptHash, redeemers: Pairs, redeemer_index: Int, input_data_coercer: fn(Data) -> a, ) -> Bool ``` ### Withdrawal Script Wrappers For defining the staking scripts that carry out the computation or validation: **`computation_withdrawal_wrapper`** - Helper function for defining your "computation stake validator." The resulting stake validator will carry out the provided `function`'s logic, and `redeemer` must contain the input(s) and expected output(s): ```aiken pub fn computation_withdrawal_wrapper( redeemer: ComputationRedeemer, function: fn(a) -> b, ) -> Bool ``` **`validation_withdrawal_wrapper`** - Helper function for defining your delegated validation. The resulting stake validator will carry out the provided `validation`'s logic with given input(s) through its redeemer: ```aiken pub fn validation_withdrawal_wrapper( redeemer: ValidationRedeemer, validation: fn(a) -> Bool, ) -> Bool ``` ### Full Example Here is a complete example showing both the spending validator (which delegates) and the staking scripts (which perform the actual logic): ```aiken use aiken/builtin use aiken/crypto.{ScriptHash} use aiken_design_patterns/merkelized_validator.{ ComputationRedeemer, ValidationRedeemer, } use aiken_design_patterns/utils.{sum_of_squares} use cardano/address.{Credential} use cardano/transaction.{OutputReference, Transaction} pub type ExampleSpendRedeemer { withdraw_redeemer_index: Int, second_integer: Int, } /// Definition of a custom validator for spending transactions, utilizing both /// `delegated_compute` and `delegated_validation`. validator spending_example( summation_stake_validator: ScriptHash, forty_two_stake_validator: ScriptHash, ) { spend( m_x: Option, r: ExampleSpendRedeemer, _own_ref: OutputReference, tx: Transaction, ) { expect Some(x) = m_x let sum = [x, r.second_integer] |> merkelized_validator.delegated_compute( staking_validator: summation_stake_validator, redeemers: tx.redeemers, redeemer_index: r.withdraw_redeemer_index, input_data_coercer: fn(d: Data) -> List { expect ints: List = d ints }, output_data_coercer: builtin.un_i_data, ) merkelized_validator.delegated_validation( function_input: sum, staking_validator: forty_two_stake_validator, redeemers: tx.redeemers, redeemer_index: r.withdraw_redeemer_index, input_data_coercer: builtin.un_i_data, ) } else(_) { fail } } /// Definition of a custom validator for withdrawal transactions. We are using /// `ComputationRedeemer, Int>` to showcase how multiple inputs and/or /// outputs can be incorporated. /// /// Result of compiling this validator and acquiring its hash, should be used as /// the `summation_stake_validator` parameter of the spending script above. validator summation_staking_script { withdraw( redeemer: ComputationRedeemer, Int>, _own_credential: Credential, _tx: Transaction, ) { let ints <- merkelized_validator.computation_withdrawal_wrapper(redeemer) sum_of_squares(ints) } else(_) { fail } } /// Result of compiling this validator and acquiring its hash, should be used as /// the `forty_two_stake_validator` parameter of the spending script above. validator forty_two_staking_script { withdraw( redeemer: ValidationRedeemer, _own_credential: Credential, _tx: Transaction, ) { let num <- merkelized_validator.validation_withdrawal_wrapper(redeemer) num == 42 } else(_) { fail } } ``` ## Example Code Full working example: [merkelized-validator.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/merkelized-validator.ak) Library implementation: [merkelized_validator module](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/merkelized-validator.ak) Additional sample: [aiken-delegation-sample](https://github.com/keyan-m/aiken-delegation-sample/blob/main/validators/main-contract.ak) --- ## Merkle Tree A **Merkle tree** summarizes a large set of items in a single **root hash**, and lets anyone prove that one item belongs to the set with a proof whose size grows only with the logarithm of the set. On Cardano that is the useful property: a contract stores just the 32-byte root in its datum and verifies membership on-chain from a small proof, while the tree itself is built off-chain. The [aiken-merkle-tree](https://github.com/Anastasia-Labs/aiken-merkle-tree) library packages this for Aiken. For the hashing fundamentals (why the structure is tamper-evident and why proofs are log-sized), see [Cryptographic primitives](/docs/developers/curriculum/fundamentals/cryptographic-primitives#how-do-merkle-trees-enable-efficient-verification). The recap below is enough to use it. ## How it works Each leaf holds the hash of one item, each parent holds the hash of its two children, and the single root at the top is a fingerprint of everything below it. ```text Merkle Root | +-----------+-----------+ | | Hash(A+B) Hash(C+D) | | +---+---+ +---+---+ | | | | Hash(A) Hash(B) Hash(C) Hash(D) ``` 1. Hash each item: `Hash(A)`, `Hash(B)`, `Hash(C)`, `Hash(D)`. 2. Concatenate and hash siblings: `Hash(Hash(A) + Hash(B))`, and likewise for C and D. 3. Hash those two together to get the root. Two properties make it useful on-chain: - **Tamper-evident.** Changing any leaf cascades new hashes all the way to the root, so the root no longer matches. Any alteration is detectable. - **Cheap membership proofs.** To prove an item is in the set, you supply only the sibling hashes along its path to the root, about `log2(n)` of them, not the whole tree. ## Verifying membership on-chain Because building the tree is expensive, you construct it and its proofs **off-chain** and only **verify** on-chain. A spending validator stores the root in its datum and checks a proof supplied in the redeemer: ```aiken use aiken_merkle_tree/mt.{Proof, Root, is_member} type MyDatum { merkle_root: Root, } type MyRedeemer { my_proof: Proof, user_data: ByteArray, } validator { fn spend_validator(datum: MyDatum, redeemer: MyRedeemer, _ctx: ScriptContext) { let MyDatum { merkle_root } = datum let MyRedeemer { my_proof, user_data } = redeemer is_member(merkle_root, user_data, my_proof, identity) } } ``` The library exposes the operations you build on: - `from_list` / `to_list`: build a tree from a list of serialized items, and back. - `root`: the tree's root hash; `size` and `is_empty`: its element count and emptiness. - `get_proof`: build a membership proof for an element (off-chain). - `is_member`: verify an element against a root and proof (the on-chain check). - `combine`: hash two child roots into their parent. The full implementation and the off-chain builder are in the [aiken-merkle-tree repository](https://github.com/Anastasia-Labs/aiken-merkle-tree); it ports the [aiken-lang/trees](https://github.com/aiken-lang/trees) `mt.ak` module, which derives from Hydra's Plutus Merkle tree and uses SHA-256 throughout. (The validator above shows the pattern; check the repository for the current Aiken API.) ## Case study For a real-world application on Cardano, sidechain-to-main-chain token transfers, see [Cardano Sidechain Toolkit: main-chain Plutus scripts](https://docs.cardano.org/cardano-sidechains/sidechain-toolkit/mainchain-plutus-scripts/), which walks the workflow end to end. ## Related - [Cryptographic primitives](/docs/developers/curriculum/fundamentals/cryptographic-primitives#how-do-merkle-trees-enable-efficient-verification): the hashing fundamentals behind the tree. - [Linked list](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/linked-list) and [Trie](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/trie): other on-chain data structures. --- ## Design Patterns This section covers common design patterns and data structures for building efficient and secure Cardano smart contracts, all with Aiken implementations and code examples. These are reference material, not a sequential read: reach for a pattern when you hit the problem it solves. They are about **efficiency and architecture**, which is a different concern from the [security vulnerabilities](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview) (what can go wrong) and [optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization) (making a single validator cheaper). The most broadly useful idea here is the principle below. ## Avoid redundant validation When several scripts run in one transaction (multiple spends, a mint, a withdrawal) and each independently validates the same conditions, you pay for those checks once *per script*: more execution units, higher fees, and duplicated logic that can drift out of sync between validators. The fix is **validation delegation**. Put the shared checks in a single validator and have every other script merely confirm that this central validator ran in the same transaction. The common logic then executes once, no matter how many scripts are involved. The cleanest validator to centralize into is a **withdrawal (stake) validator**, triggered by the **withdraw-zero trick**: include a withdrawal of 0 lovelace from the script's reward account. That fires the withdrawal script without touching stake rewards, consuming a UTXO, or minting a token, so each spending or minting script only has to check `tx.withdrawals` for the central script's hash. The full implementation (central validator, delegating spend/mint scripts, and the off-chain withdraw-0) is the [Stake Validator](../stake-validator) pattern. Reach for it when multiple scripts in a protocol share validation and cost matters; skip it when the scripts are genuinely independent, or when one self-contained extra script is simpler than the indirection. ## Design Patterns Library The patterns below come from the [Anastasia Labs aiken-design-patterns](https://github.com/Anastasia-Labs/aiken-design-patterns) library (v1.5.0). This is a ready-to-use Aiken library that provides production-grade implementations of common on-chain patterns, so developers can import and use them directly without the overhead of reimplementing the base logic themselves. | Pattern | Description | |---------|-------------| | [Stake Validator](../stake-validator) | Delegate computations to staking scripts using the "withdraw zero trick" for optimized validation | | [UTxO Indexers](../utxo-indexers) | Efficient one-to-one and one-to-many mappings between inputs and outputs with O(1) lookups | | [Transaction Level Minting Policy](../tx-level-minter) | Couple spend and mint endpoints for single-execution validation logic | | [Validity Range Normalization](../validity-range-normalization) | Standardize validity range handling to eliminate redundancies | | [Merkelized Validator](../merkelized-validator) | Delegate logic to external withdrawal scripts to stay within size limits | | [Parameter Validation](../parameter-validation) | Verify script instances are derived from specific parameterized scripts | | [Linked List](../linked-list) | On-chain linked list for storing arbitrarily large collections across UTxOs | ## Data Structures The data structures below are standalone Aiken implementations from separate repositories. They are not part of the `aiken-design-patterns` library but serve as reference implementations that demonstrate how to use these structures on-chain. | Data Structure | Description | |----------------|-------------| | [Merkle Tree](../merkle-tree) | Merkle tree for efficient data verification and proof of membership | | [Trie](../trie) | Distributed trie for scalable on-chain key-value storage across UTxOs | --- ## On-Chain Parameter Validation ## Introduction When writing onchain code you might encounter a situation where you want to be able to check that a script hash is an instantiation of an unparameterised script. It is common for smart contracts to accept parameters (e.g. fees, references to other scripts, magical numbers). Perhaps the most well known example of such a script is the one-shot minting policy script, that enforces that the minting policy can only ever succeed once (ie. for NFTs, or fixed-supply fungible tokens). Given a plutus script that accepts parameters (such as a `TxOutRef`), with this design pattern you can verify onchain that a given script hash is an instance of that script with a specific parameter applied. It works by reconstructing the script hash from the serialised CBOR bytes before and after the parameter, and comparing it to the hash to check. ## Aiken Implementation In some cases, validators need to be aware of instances of a parameterized script in order to have a more robust control over the flow of assets. As a simple example, consider a minting script that needs to ensure the destination of its tokens can only be instances of a specific spending script, e.g. parameterized by users' wallets. Since each different wallet leads to a different script address, without verifying instances, instances can only be seen as arbitrary scripts from the minting script's point of view. This can be resolved by validating an instance is the result of applying specific parameters to a given parameterized script. ### Requirements To allow this validation on-chain, some restrictions are needed: 1. Parameters of the script must have constant lengths, which can be achieved by having them hashed 2. Consequently, for each transaction, the resolved value of those parameters must be provided through the redeemer 3. The dependent script must be provided with CBOR bytes of instances before and after the parameter(s) 4. Wrapping of instances' logics in an outer function so that there'll be single occurrences of each parameter ### Library Functions This pattern provides two sets of functions. One for applying parameter(s) in the dependent script (i.e. the minting script in the example above), and one for wrapping your parameterized scripts with. After defining your parameterized scripts, you'll need to generate instances of them with dummy data in order to obtain the required `prefix` value for your target script to utilize. Note that your prefix should be from a single CBOR encoded result. #### 1. Parameter Application Functions (for the dependent script) Use these inside your contracts that depend on parameterized scripts. The parameter must be serialised before getting passed. It'll be hashed with `blake2b_224` before placement after `prefix`. Note that your prefix should be from a single CBOR encoded result. And also, the `version` should either be 1, 2, or 3 depending on your script. - **`apply_param(version, prefix, param)`** - Use this inside your contracts that depend on scripts with single parameters. The parameter must be serialised before getting passed here. It'll be hashed with `blake2b_224` before placement after `prefix`. - **`apply_param_2(version, prefix, param_0, param_1)`** - Similar to `apply_param`, but for scripts with 2 parameters. - **`apply_param_3(version, prefix, param_0, param_1, param_2)`** - Similar to `apply_param`, but for scripts with 3 parameters. - **`apply_prehashed_param(version, prefix, param)`** - Similar to `apply_param`, but for scripts that their parameters don't need to be resolved (e.g. have a script hash as their parameter). Can be used for any hashing algorithms, i.e. the length of the provided hash does not matter (`prefix` covers it). - **`apply_prehashed_param_2(version, prefix, param_0, param_1)`** - Similar to `apply_prehashed_param`, but for scripts with 2 parameters. Note that while the first parameter (`param_0`) can still be of any length, `blake2b_224` is the presumed hashing algorithm for the second parameter, i.e. the parameter is expected to be 28 bytes long. - **`apply_prehashed_param_3(version, prefix, param_0, param_1, param_2)`** - Similar to `apply_prehashed_param`, but for scripts with 3 parameters. Here again the first parameter can be of arbitrary length, while the other two must be 28 bytes long. All functions return a `ScriptHash`. #### 2. Wrapper Functions (for the parameterized scripts) Helper functions for parameterized scripts, which take care of validating resolved parameter hashes, and provide you with both the parameter and your custom redeemer. **Key types:** ```aiken /// Datatype for redeemer of your single parameterized scripts. pub type ParameterizedRedeemer { param: p, redeemer: r, } /// Datatype for parameterized scripts that don't need a redeemer. pub type Parameter { param: p, } ``` (Also available: `ParameterizedRedeemer2`, `ParameterizedRedeemer3`, `Parameter2`, `Parameter3`) **Wrapper functions with redeemer:** - **`wrapper`** - For scripts with one parameter - **`wrapper_2`** - For scripts with two parameters - **`wrapper_3`** - For scripts with three parameters **Wrapper functions without redeemer:** - **`wrapper_no_redeemer`** - Wrapper function for scripts with one parameter that don't need a redeemer - **`wrapper_no_redeemer_2`** - For two parameters - **`wrapper_no_redeemer_3`** - For three parameters ### Examples The following examples are from the upstream library and show both the dependent script using `apply_param`, and parameterized scripts using the wrapper functions: **Dependent minting script** - uses `apply_param` to verify the destination address is an instance of a parameterized spending script: ```aiken use aiken/cbor use aiken/collection/list use aiken/crypto.{Blake2b_224, Hash, blake2b_224} use aiken/primitive/bytearray use aiken_design_patterns/parameter_validation.{ Parameter, ParameterizedRedeemer, apply_param, } use cardano/address.{Address, Script} use cardano/assets.{PolicyId} use cardano/transaction.{Output, OutputReference, Transaction} // Sample prefix and postfix values obtained from `parameterized_spend` const destination_script_prefix: ByteArray = #"59012f0101003229800aba2aba1aab9faab9eaab9dab9a9bae002488888896600264653001300800198041804800cc0200092225980099b8748008c020dd500144ca60026018003300c300d0019b874800122259800980098061baa00789919192cc004c04c00a264b300130053010375400313232332259800980c001c4c8c96600266e3cde41bb30010148acc004c02cc058dd500644cdc79bc83371466e28dd98009bb30013766603260340046eb8c064c05cdd5006459015459015180c000980a9baa00f8b202c375a602a0026eb8c054008c054004c044dd5000c5900f1809001c590111bad30110013011001300d375400f16402c3009375400516401c300800130043754011149a26cac80109811e581c" validator dependent_mint { mint(redeemer: OutputReference, _own_policy: PolicyId, tx: Transaction) { let Transaction { mint, outputs, .. } = tx let target_script_hash = apply_param( version: 3, prefix: destination_script_prefix, param: cbor.serialise(redeemer), ) expect [ Output { address: Address { payment_credential: Script(destination_script_hash), .. }, value: produced_value, .. }, ] = outputs and { assets.without_lovelace(produced_value) == mint, destination_script_hash == target_script_hash, list.length(assets.flatten(mint)) == 1, } } else(_) { fail } } ``` **Parameterized spending script** - uses `wrapper` to validate the resolved parameter: ```aiken validator parameterized_spend( hashed_parameter: Hash, ) { spend( m_secret: Option>, outer_redeemer: ParameterizedRedeemer, _own_out_ref: OutputReference, _tx: Transaction, ) { let parameter, redeemer, <- parameter_validation.wrapper( hashed_parameter, fn(p: OutputReference) { cbor.serialise(p) }, outer_redeemer, ) expect Some(hashed_secret) = m_secret let nonce = parameter let answer = redeemer let raw_secret = cbor.serialise(nonce) |> bytearray.concat(cbor.serialise(nonce)) |> bytearray.concat(cbor.serialise(answer)) blake2b_224(raw_secret) == hashed_secret } else(_) { fail } } ``` **Parameterized minting script** - uses `wrapper_no_redeemer` for a script that doesn't need a custom redeemer: ```aiken validator parameterized_mint( hashed_parameter: Hash, ) { mint( outer_redeemer: Parameter, own_policy: PolicyId, tx: Transaction, ) { let param <- parameter_validation.wrapper_no_redeemer( hashed_parameter: hashed_parameter, parameter_serialiser: fn(p: OutputReference) { cbor.serialise(p) }, outer_redeemer: outer_redeemer, ) let nonce = param let token_name = cbor.serialise(nonce) |> blake2b_224 assets.flatten(tx.mint) == [(own_policy, token_name, 1)] } else(_) { fail } } ``` ## Examples and Implementation Full working example: [parameter-validation.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/parameter-validation.ak) Library implementation: [parameter_validation module](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/parameter-validation.ak) ## Considerations This design pattern only works under the assumption that the parameter is constant size (this holds true for `TxOutRef`). If a script accepts parameters with dynamic size (ie. arbitrary size integer / bytestring) then to use it with this design pattern you should modify the parameter to be the hash of the original parameter, and then allow the pre-image to be provided in the tx and verify that it matches the hash. The proposed [builtinApplyParams CIP](https://github.com/cardano-foundation/CIPs/pull/934) would add a builtin that makes this pattern much more accessible and robust. --- ## Stake Validator Pattern ## Overview The Stake Validator pattern allows you to delegate validation logic to a staking script, significantly reducing script execution costs when processing multiple UTxOs. This is achieved through the "withdraw zero trick" - where spending validators simply check for the presence of a staking credential withdrawal, and the staking validator performs the actual business logic validation once per transaction. ## The Problem When multiple UTxOs from the same script are spent in a transaction, the validator logic runs for each UTxO. This quickly becomes expensive: ```mermaid graph LR TX[Transaction] S1((UTxO 1)) -->|validates logic| TX S2((UTxO 2)) -->|validates logic| TX S3((UTxO 3)) -->|validates logic| TX TX --> O1((Output 1)) TX --> O2((Output 2)) TX --> O3((Output 3)) ``` ## The Solution Move heavy validation logic to a staking validator that runs once per transaction. Spending validators only check that the staking credential is present: ```mermaid graph LR TX[Transaction] S1((UTxO 1)) -->|check credential| TX S2((UTxO 2)) -->|check credential| TX S3((UTxO 3)) -->|check credential| TX ST{{Staking Script}} -.-o |validates logic once| TX TX --> O1((Output 1)) TX --> O2((Output 2)) TX --> O3((Output 3)) ``` ## Aiken Implementation This pattern allows for delegating some computations to a given staking script. The primary application for this is the so-called "withdraw zero trick," which is most effective for validators that need to go over multiple inputs. With a minimal spending logic (which is executed for each UTxO), and an arbitrary withdrawal logic (which is executed only once), a much more optimized script can be implemented. The module offers three functions, primarily meant to be implemented under spending endpoints: - `validate_withdraw` - `validate_withdraw_with_amount` - `validate_withdraw_minimal` Use `validate_withdraw_minimal` if you don't need to perform any validations on either the staking script's redeemer or withdrawal Lovelace quantity. All three functions go over the `withdrawals` list in the transaction. However, `validate_withdraw` and `validate_withdraw_with_amount` also traverse the `redeemers` field in order to let you validate against the redeemer (and the withdrawal quantity in case of the latter). ### Example The following example shows a spending validator that uses `validate_withdraw_with_amount` to delegate to a staking script, and a minimal accompanying withdrawal script: ```aiken use aiken/crypto.{ScriptHash} use aiken_design_patterns/stake_validator use cardano/address.{Credential} use cardano/transaction.{OutputReference, Transaction} pub type ExampleSpendRedeemer { withdraw_redeemer_index: Int, withdrawal_index: Int, } /// Example for a validator that requires a withdrawal from a given staking /// script hash. validator spend(withdraw_script_hash: ScriptHash) { spend( _datum, redeemer: ExampleSpendRedeemer, own_out_ref: OutputReference, tx: Transaction, ) { // Extract needed values from `tx` let Transaction { withdrawals, redeemers, .. } = tx // Validate the required staking script is present in transaction, and grab // its redeemer data and withdraw quantity in Lovelace. let redeemer_data, withdraw_amount, <- stake_validator.validate_withdraw_with_amount( withdraw_script_hash: withdraw_script_hash, redeemers: redeemers, withdraw_redeemer_index: redeemer.withdraw_redeemer_index, withdrawals: withdrawals, withdrawal_index: redeemer.withdrawal_index, ) // Example validation, ensuring the staking script has been invoked with // access to the output reference of the UTxO being spent. expect out_ref_passed_to_staking_script: OutputReference = redeemer_data expect out_ref_passed_to_staking_script == own_out_ref // Another example validation, only allowing withdrawals from the staking // script as long as no rewards had been accumulated for said script. withdraw_amount == 0 } else(_) { fail } } /// A very minimal example just to show how an accompanying staking script can /// be defined. validator withdraw { withdraw( redeemer: OutputReference, _own_credential: Credential, _tx: Transaction, ) { let OutputReference { output_index, .. } = redeemer // A contrived check. Only UTxOs that have an output index of 0 pass this // script's validation. output_index == 0 } else(_) { fail } } ``` ## Key Functions The library provides three functions, all meant to be used in spending endpoints: ### `validate_withdraw` Helper function for implementing validation for spending UTxOs, essentially delegating their requirements to the given withdrawal validator. In simpler terms, it says: As long as there is a redeemer with a withdrawal purpose, for the given script in transaction, this UTxO can be spent. Allows you to validate based on the withdrawal's redeemer, which is mostly useful for ensuring specific endpoints are invoked. ```aiken pub fn validate_withdraw( withdraw_script_hash: ScriptHash, redeemers: Pairs, withdraw_redeemer_index: Int, withdraw_redeemer_validator: fn(Redeemer) -> Bool, ) -> Bool ``` ### `validate_withdraw_with_amount` Similar to `validate_withdraw`, but with the additional steps for extracting the withdrawal amount from the `withdrawals` field: ```aiken pub fn validate_withdraw_with_amount( withdraw_script_hash: ScriptHash, redeemers: Pairs, withdraw_redeemer_index: Int, withdrawals: Pairs, withdrawal_index: Int, withdraw_redeemer_validator: fn(Redeemer, Lovelace) -> Bool, ) -> Bool ``` ### `validate_withdraw_minimal` A more minimal version of `validate_withdraw`, where only the presence of a given staking script is checked, regardless of withdraw amount or redeemer: ```aiken pub fn validate_withdraw_minimal( withdraw_script_hash: ScriptHash, withdrawals: Pairs, withdrawal_index: Int, ) -> Bool ``` ## Why "Withdraw Zero"? The pattern is called "withdraw zero trick" because you can withdraw 0 lovelace from the staking credential to trigger the staking validator - the withdrawal amount is irrelevant to the validation logic. ## Submitting the trigger off-chain The off-chain half of the pattern is an ordinary transaction carrying a zero-amount withdrawal for the staking script, with a redeemer and the script attached. Withdrawal validators must be registered on-chain first; the on-chain logic that decides which registrations, delegations, and reward withdrawals to allow is the [certificate and withdrawal handlers](/docs/developers/curriculum/smart-contracts/write-a-validator#certificate-validator) in Write a validator. ```typescript const tx = await client .newTx() .withdraw({ stakeCredential: scriptStakeCredential, amount: 0n, redeemer: Data.constr(0n, []), label: "coordinator-trigger" }) .attachScript({ script: stakeScript }) .build() ``` ```typescript declare const scriptRewardAddress: string // reward address derived from the stake script hash declare const stakeScriptCbor: string const collateral = await wallet.getCollateralMesh() const unsignedTx = await new MeshTxBuilder({ fetcher: provider }) .withdrawalPlutusScriptV3() .withdrawal(scriptRewardAddress, "0") // zero-amount withdrawal triggers the validator .withdrawalScript(stakeScriptCbor) .withdrawalRedeemerValue(mConStr0([])) .txInCollateral(collateral[0].input.txHash, collateral[0].input.outputIndex) .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` Script control is not limited to the withdrawal trigger. In Evolution every staking operation accepts a `redeemer` and an attached script, so a script-held credential can also delegate: ```typescript declare const scriptStakeCredential: Credential.Credential declare const stakeScript: any const tx = await client .newTx() .delegateToPool({ stakeCredential: scriptStakeCredential, poolKeyHash, redeemer: Data.constr(0n, []), label: "delegate-script-stake" }) .attachScript({ script: stakeScript }) .build() ``` Mesh's builder takes a redeemer on a script **withdrawal** (the trigger above) but not on a stake **delegation** certificate, so script-controlled delegation is Evolution or cardano-cli only. The script does not have to hold the stake credential itself, either. When locked funds should keep earning for their depositor, the script address carries the *depositor's* stake credential, and staking needs no script logic at all: production lending pools stake idle liquidity this way, with the validator simply preserving the full address on every continuing output. ## Double Satisfaction Protection When using this pattern with multiple inputs/outputs, protect against [double satisfaction attacks](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction) by: 1. **Tagging outputs** - Include input OutRef in output datums 2. **Unique indexing** - Use redeemer indices to pair inputs with outputs 3. **Filtering inputs** - Validate only inputs from your script address See [UTxO Indexers](../utxo-indexers) for robust input/output pairing patterns. ## Example Code Full working example: [stake-validator.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/stake-validator.ak) Library implementation: [stake_validator module](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/stake-validator.ak) ## When to Use **Use stake validators when:** - Processing multiple UTxOs in single transactions - Validation logic is expensive (CPU/memory) - You need transaction-level validation rather than per-UTxO validation ## Registration state as global state The withdraw zero trick uses a stake credential for its logic, but the credential carries a second, often overlooked property: its registration status. Whether a credential is currently registered is tracked on the account-based side of the ledger, outside the UTxO set. That makes it one bit of global state that any transaction can set, unset, or test: - Registering the credential sets the bit. Registration fails if the credential is already registered, so a successful registration also proves the bit was unset. - Deregistering the credential unsets the bit, and proves it was set. - Registering and then deregistering in the same transaction tests that the bit is unset without leaving it set. The interesting consequence is proving that an event has *not* happened. The usual proof-of-occurrence technique mints a token when an event occurs; anyone can later prove the event happened by referencing the UTxO holding that token. It cannot prove the opposite, because no validator can force another transaction to include that UTxO as a reference input. If the event instead requires registering a stake credential, absence becomes checkable: a transaction that registers the credential only succeeds while the event has not occurred. Because this state lives on the account side of the ledger, reading or flipping it spends no UTxO. There is no contention: many transactions can interact with the same credential within one block without competing for an input. Two caveats. The certificate operations that make this work are exactly the ones covered in [Unconstrained Certificate Operations](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/staking-and-certificates#unconstrained-certificate-operations): an unguarded certificate path lets anyone flip the bit and claim the registration deposit. And while several credentials give several bits, treating them as an integer reintroduces the concurrency problems the technique avoids; the [global state write-up](https://github.com/Anastasia-Labs/design-patterns/blob/main/stake-validator/GLOBAL-STATE.md) in the design patterns repository demonstrates multi-bit counters but labels them a proof of concept. --- ## Trie A **trie** (prefix tree) stores a set of keys by their shared prefixes. Each node is one step along a key, and a key's value sits at the node where its path ends, so keys that begin the same way share the same branch. That makes a trie space-efficient for large sets of similar keys, with lookups, inserts, and deletes proportional to the length of the key rather than the size of the set. On Cardano, the [aiken-trie](https://github.com/Anastasia-Labs/aiken-trie) library distributes a trie across many UTXOs, so different parts of the structure can be updated in parallel while staying verifiable on-chain. ## Structure - **Root**: the (usually empty) node every key descends from. - **Intermediate nodes**: the shared prefixes of the keys, narrowing the search at each step. - **Leaf nodes**: where a key's path ends and its value is held. A key is encoded as a path: each byte is a step down from the root toward a leaf. Inserting a key walks that path, creating nodes where none exist. Because keys share prefixes, a common beginning is stored once. Example with the keys `car`, `cat`, and `dog`: ```mermaid graph TD ROOT((Root)) -->|c| C(C) C -->|a| A(A) A -->|r| R(R) A -->|t| T(T) ROOT -->|d| D(D) D -->|o| O(O) O -->|g| G(G) ROOT -->|other| OTH((Others)) style ROOT fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style C fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style A fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style R fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style T fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style D fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style O fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style G fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style OTH fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` `car` and `cat` share the `ca` branch and split at the final character; `dog` descends its own path. Storing that shared prefix once is where a trie saves space over keeping each key independently, and the saving grows with the number of keys that share prefixes. ## On-chain design The library keeps each part of the trie in its own UTXO and validates changes through a staking script rather than per-node spend logic: a spend defers to a withdrawal validator that runs **once for the whole transaction** and checks the trie operation. This is the [withdraw-zero coordinator](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) shape applied to a data structure, and it keeps updates cheap as the trie grows. For the validator code and an off-chain transaction-builder guide, see the [aiken-trie repository](https://github.com/Anastasia-Labs/aiken-trie). ## Related - [Linked list](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/linked-list): the other on-chain associative structure, ordered by key, with membership and non-membership proofs. - [Merkle tree](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/merkle-tree): commit to a large set behind a single root hash. --- ## Transaction-Level Minting ## Introduction When crafting transactions to process a single script (smart contract) UTxO, enforcing spending requirements seems straightforward. However, in high-throughput applications, a more efficient approach is desired, allowing the processing (spending) of these script UTxOs in a "batch." Unfortunately, invoking the validator script for each input UTxO in a transaction repeats pre-processing steps, making it less optimal. To overcome this, the technique of "transaction level validation" is employed. This design pattern couples the spend and minting endpoints of a validator. Very similar to the [Stake Validator](../stake-validator) pattern, this approach delegates validation logic to execute once per transaction rather than per UTxO. For transaction level validation using staking validators, refer to the [Stake Validator Design pattern](../stake-validator). This document outlines implementing the same pattern via minting policies. ## The Problem A batch transaction involves multiple input UTxOs at a specific script address, and spending can only occur when specific conditions apply (i.e., the validator function does not reject the transaction). In this scenario, the validator script is executed for each UTxO, and the transaction fails if any of the scripts reject it. ```mermaid graph LR TX[Transaction] subgraph Spending Script S1((UTxO 1)) S2((UTxO 2)) S3((UTxO 3)) end S1 -->|validates conditions| TX S2 -->|validates conditions| TX S3 -->|validates conditions| TX TX --> A1((Output 1)) TX --> A2((Output 2)) TX --> A3((Output 3)) classDef emphasized fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF classDef regular fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 class TX emphasized class S1,S2,S3,A1,A2,A3 regular ``` ## The Solution The role of the spending input is to ensure the minting endpoint executes. It does so by looking at the mint field and making sure a non-zero amount of its asset (i.e. with a policy identical to the provided script hash) are getting minted/burnt. The arbitrary logic is passed to the minting policy so that it can be executed a single time for a given transaction. ```mermaid graph LR TX[Transaction] subgraph Spending Script S1((UTxO 1)) S2((UTxO 2)) S3((UTxO 3)) end S1 -->|mints validation token| TX S2 -->|mints validation token| TX S3 -->|mints validation token| TX ST{{Minting policy}} -.-o |validates Business Logic| TX TX --> A1((Output 1)) TX --> A2((Output 2)) TX --> A3((Output 3)) classDef emphasized fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF classDef regular fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 class TX,ST emphasized class S1,S2,S3,A1,A2,A3 regular ``` ## Aiken Implementation This design pattern couples the spend and minting endpoints of a validator, in order to have minimal spend costs, in exchange for a single execution of the minting endpoint. In other words, spend logic only ensures the minting endpoint executes. It does so by looking at the mint field and making sure a non-zero amount of its asset (i.e. with a policy identical to the provided script hash) are getting minted/burnt. The arbitrary logic is passed to the minting policy so that it can be executed a single time for a given transaction. ### Key Functions #### `validate_mint` Function primarily meant to be used in your spending validator. It looks at both the redeemers and minted tokens to allow you to validate against the policy's redeemer, and its tokens getting minted/burnt. `mint_redeemer_index` is the positional index of the policy's redeemer in the `redeemers` field of the transaction. ```aiken pub fn validate_mint( mint_script_hash: PolicyId, mint: Value, redeemers: Pairs, mint_redeemer_index: Int, mint_validator: fn(Redeemer, Dict) -> Bool, ) -> Bool ``` #### `validate_mint_minimal` A minimal version of `validate_mint`, where the only validation is the presence of at least one minting/burning action with the given policy ID: ```aiken pub fn validate_mint_minimal( mint_script_hash: PolicyId, mint: Value, ) -> Bool ``` ### Example The following example shows a complete validator with both spend and mint endpoints. The spending logic uses `validate_mint` to delegate to the minting policy, which performs the actual business logic: ```aiken use aiken/collection/dict use aiken/collection/list use aiken_design_patterns/tx_level_minter use cardano/address.{Address, Script} use cardano/transaction.{Input, Output, OutputReference, Transaction} pub type SampleSpendRedeemer { own_index: Int, mint_redeemer_index: Int, burn: Bool, } pub type SampleMintRedeemer { max_utxos_to_spend: Int, } validator example { // Sample spend logic on how to use the provided interface. Here we are // passing script's own hash as the expected minting policy. spend( _datum, redeemer: SampleSpendRedeemer, own_out_ref: OutputReference, tx: Transaction, ) { // Grabbing spending UTxO based on the provided index. expect Some(Input { output: Output { address: own_addr, .. }, output_reference, }) = list.at(tx.inputs, redeemer.own_index) // Validating that the found UTxO is in fact the spending UTxO. expect own_out_ref == output_reference // Getting the validator's script hash. expect Script(own_hash) = own_addr.payment_credential // Getting access to the mint script's redeemer, and the tokens being // minted/burnt using the design pattern. The logic that follows expects a // single "BEACON" token to be either burnt or minted. let redeemer_data, tn_qty_dict, <- tx_level_minter.validate_mint( mint_script_hash: own_hash, mint: tx.mint, redeemers: tx.redeemers, mint_redeemer_index: redeemer.mint_redeemer_index, ) expect SampleMintRedeemer { max_utxos_to_spend } = redeemer_data expect max_utxos_to_spend > 0 expect [Pair("BEACON", mint_quantity)] = dict.to_pairs(tn_qty_dict) if redeemer.burn { mint_quantity == -1 } else { mint_quantity == 1 } } // Sample mint logic to complete the example. This policy expects a specific // number of inputs to be spent in each transaction. mint(redeemer: SampleMintRedeemer, own_policy, tx: Transaction) { let script_inputs_count = tx.inputs |> list.foldr( 0, fn(i, acc) { when i.output.address.payment_credential is { Script(input_script_hash) -> if input_script_hash == own_policy { acc + 1 } else { acc } _ -> acc } }, ) script_inputs_count == redeemer.max_utxos_to_spend } else(_) { fail } } ``` ## Minting vs Burning The drawback of this approach is that the minted validation tokens must be included in one of the outputs, potentially consuming unnecessary block space. A more desirable solution involves burning the validation tokens instead of minting them in the batch transaction. However, this requires minting and storing the validation tokens in the input UTxOs beforehand, allowing for pre-validation steps attached to their creation. ### Initial Minting ```mermaid graph LR TX[Transaction] TX --> A1((UTxO \n containing \n validation token)) MP{{Minting policy}} -.-o |validates token minting| TX classDef emphasized fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF classDef regular fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 class TX,MP emphasized class A1 regular ``` ### Batch Burning ```mermaid graph LR TX[Transaction] subgraph Spending Script S1((UTxO 1)) S2((UTxO 2)) S3((UTxO 3)) end S1 -->|burns \n validation token| TX S2 -->|burns \n validation token| TX S3 -->|burns \n validation token| TX MP{{Minting policy}} -.-o |validates Business Logic| TX TX --> A1((Output 1)) TX --> A2((Output 2)) TX --> A3((Output 3)) classDef emphasized fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF classDef regular fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 class TX,MP emphasized class S1,S2,S3,A1,A2,A3 regular ``` ## Stake Validator vs Minting Policy Transaction level validation can be implemented using minting policies. However, if minting validation tokens is impractical, the recommended approach is to implement transaction level validation using a staking validator, which in practice costs fewer ExUnits than minting policy checks. ## Example Code Full working example: [tx-level-minter.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/tx-level-minter.ak) Library implementation: [tx_level_minter module](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/tx-level-minter.ak) Additional sample: [aiken-delegation-sample](https://github.com/keyan-m/aiken-delegation-sample/blob/main/validators/spend-logic.ak) ## Related Patterns - [Stake Validator](../stake-validator) - Alternative approach with lower ExUnits cost - [UTxO Indexers](../utxo-indexers) - Combine with indexing for batch processing --- ## UTxO Indexers ## Overview UTxO Indexers provide optimized and composable solutions for mapping inputs to outputs in transactions. Instead of expensive linear searches on-chain, indices are computed off-chain and passed via redeemers, enabling O(1) lookups. ## The Problem: Linear Search is Expensive Without indexing, validators must search through all inputs/outputs to find specific ones: ```aiken // Expensive O(n) search find_input_with_token(inputs, my_token) // Searches entire list ``` This becomes costly as transaction size grows due to limited execution budgets. There is a second, sneakier reason indices must come from the redeemer: **the ledger does not preserve input order**. A transaction's inputs are a set, re-sorted lexicographically by (transaction hash, output index) before the validator ever sees them, so on-chain code can never rely on the order in which the builder added inputs, and any correspondence between "the third input" and "the third thing to process" has to be re-established explicitly. Outputs are the opposite: they are a list that keeps exactly the order the builder created, which is why patterns on this page can walk outputs head-first while inputs need index hints. ## The Solution: Redeemer Indexing Leverage Cardano's deterministic script evaluation - all inputs to validators are known at transaction construction time: 1. **Off-chain**: Build transaction, find indices of relevant inputs/outputs 2. **Off-chain**: Include indices in redeemer 3. **On-chain**: Use `list.at(index)` for O(1) access, verify element meets criteria ```aiken // Efficient O(1) access expect Some(input) = inputs |> list.at(input_index) expect input meets criteria ``` ## Pattern Variants There are a total of 4 variations available: - Single, one-to-one indexer - Single, one-to-many indexer - Multiple, one-to-one indexer, with ignored redeemers - Multiple, one-to-one indexer, with provided redeemers :::note Neither of the singular UTxO indexer patterns provide protection against the [double satisfaction](https://github.com/Plutonomicon/plutonomicon/blob/b6906173c3f98fb5d7b40fd206f9d6fe14d0b03b/vulnerabilities.md#double-satisfaction) vulnerability, as this can be done in multiple ways depending on the contract. However, they require a dedicated argument as a reminder for the potential requirement of implementing a protection against this vulnerability. ::: Depending on the variation, the functions you can provide are: - One-to-one validator for an input and its corresponding output - this is always the validation that executes the most times (i.e. for each output) - One-to-many validator for an input and all of its corresponding outputs - this executes only once ### 1. Singular UTxO Indexer #### One-to-one Helper function to be defined in the spending endpoint of your contract, for appointing an input at `input_index` against an output at `output_index`. By including this in your spending endpoint, you'll get an efficient access to your input, and its corresponding output. Within the function you pass as `validation_logic`, you have access to the picked input and output. Apart from `validation_logic`, the only other validation this function performs for you is the equality of the picked input's output reference with the one extracted from `ScriptInfo`. `double_satisfaction_prevented` is a required `Bool`, which is just a reminder that this function does NOT cover the [double satisfaction](https://github.com/Plutonomicon/plutonomicon/blob/b6906173c3f98fb5d7b40fd206f9d6fe14d0b03b/vulnerabilities.md#double-satisfaction) vulnerability out-of-the-box. ```aiken use aiken_design_patterns/singular_utxo_indexer use aiken_design_patterns/utils.{authentic_input_is_reproduced_unchanged} use cardano/assets use cardano/transaction.{OutputReference, Transaction} validator one_to_one(state_token_symbol: assets.PolicyId) { spend( _datum, redeemer: Pair, own_out_ref: OutputReference, tx: Transaction, ) { let Transaction { inputs, outputs, .. } = tx let input, output, <- singular_utxo_indexer.one_to_one( input_index: redeemer.1st, output_index: redeemer.2nd, own_ref: own_out_ref, inputs: inputs, outputs: outputs, double_satisfaction_prevented: True, ) // double_satisfaction_prevented authentic_input_is_reproduced_unchanged( state_token_symbol, None, input.output, output, ) } else(_) { fail } } ``` #### One-to-many Helper function for appointing an input against a set of outputs in a transaction. Similar to `one_to_one`, this function also validates the spent UTxO's output reference matches the one found using the input index. Here we also have the `double_satisfaction_prevented` argument as a mere reminder that this function does not cover double satisfaction on its own. ```aiken validator one_to_many( _state_token_symbol: assets.PolicyId, _state_token_name: assets.AssetName, ) { spend( _datum, redeemer: Pair>, own_ref: OutputReference, tx: Transaction, ) { let Transaction { inputs, outputs, .. } = tx singular_utxo_indexer.one_to_many( input_output_validator: fn(_input, _output_index, _output) { True }, input_collective_outputs_validator: fn(_input, _outputs) { True }, input_index: redeemer.1st, output_indices: redeemer.2nd, own_ref: own_ref, inputs: inputs, outputs: outputs, double_satisfaction_prevented: True, ) } else(_) { fail } } ``` Required validation functions are provided with: 1. `Input` itself, output index, and `Output` itself (this validation is executed for each output) 2. `Input` itself, and the list of all `Output`s (this validation is executed only once) ### 2. Multi UTxO Indexer #### `one_to_one_no_redeemer` Helper function for performing spending validation on multiple inputs from a given script, each with a corresponding output. It expects both the input and output indices be in ascending order. The validation function you should provide has access to the index of the `Input` being validated, the `Input` itself, the index of the `Output` being validated, and the `Output` itself. #### `one_to_one_with_redeemer` Another variant with a staking script (i.e. withdraw-0 script) as a coupling element for spending multiple UTxOs from a given spending script. Here the `redeemers` is also traversed to provide the validation logic with the redeemer used for spending each of the UTxOs. The assumption here is that redeemers carry the staking credential of the withdraw-0 validator, which is the purpose of the additional argument, i.e. coercing the redeemer `Data` into an expected structure, and extracting the staking credential. **Example with stake validator:** ```aiken use aiken_design_patterns/multi_utxo_indexer use aiken_design_patterns/stake_validator use aiken_design_patterns/utils.{authentic_input_is_reproduced_unchanged} use cardano/address.{Address, Credential, Script} use cardano/assets use cardano/transaction.{Output, OutputReference, Transaction} pub type ExampleSpendRedeemer { withdraw_redeemer_index: Int, withdrawal_index: Int, } validator example( state_token_symbol: assets.PolicyId, state_token_name: assets.AssetName, ) { spend( _datum, redeemer: ExampleSpendRedeemer, own_ref: OutputReference, tx: Transaction, ) { let Transaction { inputs, redeemers, withdrawals, .. } = tx expect Output { address: Address { payment_credential: Script(own_hash), .. }, .. } = utils.resolve_output_reference(inputs, own_ref) let r, qty, <- stake_validator.validate_withdraw_with_amount( withdraw_script_hash: own_hash, redeemers: redeemers, withdraw_redeemer_index: redeemer.withdraw_redeemer_index, withdrawals: withdrawals, withdrawal_index: redeemer.withdrawal_index, ) expect coerced: Pairs = r when coerced is { [] -> False _ -> qty > 0 } } withdraw(redeemer: Pairs, stake_cred: Credential, tx: Transaction) { expect Script(own_script_hash) = stake_cred let Transaction { inputs, outputs, .. } = tx let _input_index, input, _output_index, output, <- multi_utxo_indexer.one_to_one_no_redeemer( indices: redeemer, spending_script_hash: own_script_hash, inputs: inputs, outputs: outputs, ) authentic_input_is_reproduced_unchanged( state_token_symbol, Some(state_token_name), input.output, output, ) } else(_) { fail } } ``` ## Key Benefits 1. **O(1) Lookups** - Direct array access instead of linear search 2. **Reduced Execution Costs** - Minimal on-chain computation 3. **Batch Processing** - Handle multiple UTxOs efficiently 4. **Composability** - Works with stake validator pattern ## Important: Double Satisfaction Protection Singular indexers require manual double satisfaction protection: ```aiken // Vulnerable - same output can satisfy multiple inputs one_to_one(validate, 0, 0, ...) // Input 0 -> Output 0 one_to_one(validate, 1, 0, ...) // Input 1 -> Output 0 (same!) ``` **Protection strategies:** 1. **Unique output tagging** - Include input OutRef in output datum 2. **Index uniqueness checks** - Verify no duplicate indices in redeemer 3. **Use multi-indexer** - Built-in protection via stake validator ## Example Code **Singular indexer:** - [singular-utxo-indexer.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/singular-utxo-indexer.ak) - [Library implementation](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/singular-utxo-indexer.ak) **Multi indexer:** - [multi-utxo-indexer.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/multi-utxo-indexer.ak) - [Library implementation](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/multi-utxo-indexer.ak) ## Related Patterns - [Stake Validator](../stake-validator) - Multi-indexers work best with stake validators - [Double Satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction) - Vulnerability to protect against --- ## Validity Range Normalization ## Introduction Cardano validators cannot read the current time directly. To keep execution deterministic, a validator sees only the transaction's validity range, the slot window in which the transaction may be included. The ledger admits the transaction only inside that window, so a validator can enforce time-based rules while staying pure, with no side-effects and no dependence on a live clock. ## The Problem Plutus can represent the same validity range in more than one way. Each bound can be finite or infinite (`-∞`, `+∞`), and a flag marks whether the end is open or closed. So `(a, b)` (open on both ends) equals `[a+1, b-1]` (closed on both ends) when `a` and `b` are finite, and infinite ranges are sometimes written closed on the infinite side (the always-range is denoted `[-∞, +∞]` even though real times never include the infinities). A validator that does not handle every representation can behave incorrectly on the ones it did not expect. And because the encoding can change at a hard fork, a long-lived contract that assumes one form risks locking funds forever when the form shifts. ## The Solution Normalize the range to a single canonical form before checking it. The design-patterns library does this, reducing every equivalent range to one representation: - `[a, b]`: a closed range when both bounds are finite. - `(-∞, x]` and `[x, +∞)`: half-open on the infinite side, with `x` finite. - `(-∞, +∞)`: open on both sides, used for the always-range, matching the mathematical convention. ## Aiken Implementation Cardano's validity-range type allows values that are either meaningless or redundant: because the bounds are integers, the inclusive/exclusive flag is unnecessary once you fix a convention (treat every bound as inclusive). This module maps the range onto a smaller datatype that drops the redundant flag and rules out the meaningless cases. `normalize_time_range` takes a `ValidityRange` and returns it: ```aiken pub type NormalizedTimeRange { ClosedRange { lower: Int, upper: Int } FromNegInf { upper: Int } ToPosInf { lower: Int } Always InvalidRange } ``` ### Example Usage ```aiken use aiken_design_patterns/validity_range_normalization.{ NormalizedTimeRange, normalize_time_range, } validator my_validator { spend( _datum: Option, _redeemer: Redeemer, _own_ref: OutputReference, tx: Transaction, ) { let Transaction { validity_range, .. } = tx when normalize_time_range(validity_range) is { ClosedRange { lower, upper } -> { // Handle finite range [lower, upper] validate_closed_range(lower, upper) } FromNegInf { upper } -> { // Handle range (-∞, upper] validate_until(upper) } ToPosInf { lower } -> { // Handle range [lower, +∞) validate_from(lower) } Always -> { // Handle unbounded range (-∞, +∞) True } InvalidRange -> { // Handle invalid range (e.g. lower >= upper) False } } } } ``` ## Example Code Full working example: [validity-range-normalization.ak](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/validity-range-normalization.ak) --- ## Smart Contract Optimization > Sourced from the [Aiken team's optimization guide](https://aiken-lang.org/optimizing-programs). ## Before optimizing Optimizing code can be counter-intuitive, especially in the context of smart contracts. The virtual machine and its associated cost models can be sometimes confusing and move in ways that one fails to anticipate. Hence, before doing any optimisation work it is essential to set up some baseline benchmarks. Those benchmarks shall cover simple and complex scenarios alike, to easily identify the impact of changes. Sometimes, a change may introduce a one-time cost that slightly increases a simple case while making a more complex scenario significantly better. ### Writing baseline benchmarks A good strategy for writing baseline benchmarks is to write a simple _test_ executing one or more validator from a pre-constructed context and redeemer. This allows to get an overview that is as close as possible to a "real scenario" with a "real transaction". For example: ```aiken use my_validator test baseline() { my_validator.main.withdraw( baseline_redeemer, baseline_credential, baseline_transaction, ) } ``` ### Use `const` Aiken constants are fully evaluated **during compilation** and **inlined** where used. It is, therefore, highly recommended to define preparatory code for benchmarks as constants. This not only allows separating fixture code from benchmark executions, but also allows measuring a cost closer to the real execution. ```aiken const baseline_transaction: Transaction = Transaction { ..transaction.placeholder, inputs: some_inputs(3), } test baseline() { my_validator.main.withdraw( baseline_redeemer, baseline_credential, baseline_transaction, ) } ``` ### Using `Fuzzer` Fuzzers constitute a very practical way to write fixtures. Transactions in particular can be easily created using the primitives from [`fuzz/cardano`](https://aiken-lang.github.io/fuzz/cardano/fuzz.html). For example: ```aiken use aiken/fuzz use cardano/fuzz as cardano pub fn some_basic_input() -> Fuzzer { let output_reference <- fuzz.and_then(cardano.output_reference()) let address <- fuzz.and_then(cardano.address()) fuzz.constant(Input { output_reference, output: Output { address, value: min_ada_value, datum: NoDatum, reference_script: None, } }) } ``` A good rule of thumb is to start off `transaction.placeholder` from the standard library, and progressively add elements to a transaction in order to make it valid. Fuzzers are handy for elements in the transaction that can be arbitrary, while you may stick to well-known values for others. ```aiken const transaction = Transaction { ..transaction.placeholder, inputs: [ some_input_with_programmable_tokens( [(policy_programmable_token, asset_name_programmable_token, 42)], "programmable tokens input", ), some_input_fuel("fuel input"), ], outputs: [ some_output_with_programmable_tokens( [(policy_programmable_token, asset_name_programmable_token, 28)], ), some_output_with_programmable_tokens( [(policy_programmable_token, asset_name_programmable_token, 14)], ), some_output_change("change output"), ], } ``` ### The standard library: good or bad? Let's cover one last point before we dive in: the standard library. Should you use it? Most certainly yes. Will it harm the performance of your program? To some extent, yes. The standard library is **reasonably well optimised**, yet it is tuned for **correctness** and **ease of use**. Its main goal is to get you started and to be convenient. Yet, it is easy to replace surgically where needed. Most functions in the standard library are standalone, easily inlinable and can be specialised. Thus it is recommended to always start with the standard library in order to write the most _obviously correct_ code and only then, think about where it could be optimised. Many optimisations are actually domain-specific and require intrinsic knowledge to be really effective. While still designing smart contracts, optimisations about how the code is written shouldn't be the priority (but rather, be only an architectural concern). Once your on-chain code is mostly fleshed out, it's good to take a step back and reflect on your usage of the standard lib in critical parts of your program: maybe you don't need all the genericity offered by this particular function, or maybe you can use a simpler, more direct recursive implementation of that other function. There are few functions from the standard library that you particularly want to look for and avoid in validators. Those functions are usually only good for testing, but not so much for critical paths. These red flags are: - `assets.{flatten, flatten_with, restricted_to}` - `dict.{from_pairs, keys, map, values}` - `list.{count, flat_map, map, reverse, sort, zip}` You almost certainly never want to use any of those in validators. ## Decide early and cheaply The cheapest work is work that never runs. These three reorder a validator so the common case exits as soon as it can, and the expensive case is the only one that pays. ### Fail fast On-chain code isn't about error handling. If something is wrong: fail. `Option` is _rarely_ something you want to use. **mem=1.80K** · **cpu=501.69K** ```aiken // Be nice to transaction builders in case they provide a negative value let value = if datum.value <= 0 { -datum.value } else { datum.value } ``` **mem=1.40K** · **cpu=336.48K** ```aiken /// invariant violation: value must be non-negative expect datum.value >= 0 ``` ### Put cheap and likely checks first When chaining conditions with `and` or `or`, order matters. Aiken short-circuits boolean operators, which means that the first satisfied branch of an `or` avoids evaluating the others, and the first failing branch of an `and` stops the rest. So, when possible, place first the checks that are both: 1. cheaper to evaluate, and 2. more likely to determine the result (i.e. more frequently `True`) ```aiken or { input.output.value |> assets.has_nft_strict(my_nft), input.output.address.payment_credential != my_script_credential, } ``` ```aiken or { input.output.address.payment_credential != my_script_credential, input.output.value |> assets.has_nft_strict(my_nft), } ``` In this example, comparing credentials is a direct and predictable check. Inspecting the value to determine whether a specific NFT is present is more involved. Since the first condition may already be sufficient to decide the whole expression, putting it first gives the runtime more opportunities to stop early. ### Defer distinctions until they matter Another common source of unnecessary work comes from splitting terminal cases too early. When several branches eventually collapse into a smaller number of "real" outcomes, it is often better to test the broader condition first and refine only when necessary. **mem=85.04K** · **cpu=29.49M** ```aiken fn insert_in_order(self: List, elem: Int) -> List { when self is { [] -> [elem] [head, ..tail] -> if head == elem { self } else if elem < head { [elem, ..self] } else { [head, ..insert_in_order(tail, elem)] } } } ``` **mem=72.91K** · **cpu=26.11M** ```aiken fn insert_in_order(self: List, elem: Int) -> List { when self is { [] -> [elem] [head, ..tail] -> if elem <= head { if head == elem { self } else { [elem, ..self] } } else { [head, ..insert_in_order_alt(tail, elem)] } } } ``` ```aiken test baseline() { and { insert_in_order([], 1) == [1], insert_in_order([1, 2, 3, 4, 5, 6, 7], 8) == [1, 2, 3, 4, 5, 6, 7, 8], insert_in_order([1, 2, 3, 5, 6], 4) == [1, 2, 3, 4, 5, 6], insert_in_order([1, 2, 3, 4, 5], 3) == [1, 2, 3, 4, 5], } } ``` This is a small transformation, but it matters in tight recursive loops and in code that executes frequently over large structures. ## Choose cheaper representations Every value a validator builds or compares costs execution units proportional to its shape. Picking a leaner representation is often a larger saving than any change to the logic around it. ### Use simple(r) structures Unless it's coming from the _outside world_ (i.e. datum or redeemer), avoid constructing large records. Aiken is a language which operates directly on encoded objects (a.k.a. `Data`). This is handy when objects have been pre-constructed ahead of the script execution as it the case for datum, redeemers or the transaction itself. Yet, constructing large records to carry context across multiple transaction elements will often come at a significant cost. So, prefer using functions with explicit arguments when you do not actually need to materialize an intermediate structure. **mem=5.81M** · **cpu=19.53K** ```aiken type MultisigContext { owner: VerificationKeyHash, signatories: List, withdrawals: Pairs, } // NOTE: The implementation is irrelevant. fn verify_multisig(ctx: MultisigContext) { or { list.has(ctx.signatories, ctx.owner), list.any(ctx.withdrawals, fn(Pair(vk, _)) { vk == ctx.owner }), } } ``` **mem=3.71M** · **cpu=14.12K** ```aiken // NOTE: The implementation is irrelevant. fn verify_multisig(owner, signatories, withdrawals) { or { list.has(signatories, owner), list.any(withdrawals, fn(Pair(vk, _)) { vk == owner }), } } ``` ```aiken test baseline_dont() { let ctx = MultisigContext { owner: baseline_owner, signatories: baseline_signatories, withdrawals: baseline_withdrawals, } verify_multisig(ctx) } test baseline_do() { verify_multisig( baseline_owner, baseline_signatories, baseline_withdrawals, ) } ``` In particular, if you can avoid it, do not construct `Value` and prefer `Dict` or `Pairs` over `Value` whenever possible. `Value` preserves two important invariants: it does not contain assets with null quantities or policies with empty assets. If you do not rely on these invariants, you can safely go down to `Dict`. `Dict` preserves two important invariants: their keys are in ascending orders and contain no duplicate. If you do not rely on these invariants, you can safely go down to `Pairs` ### Prefer `Data` equality over manual structural comparisons Aiken programs operate over encoded `Data`, and comparing `Data` values directly is often surprisingly efficient. If two values are expected to match structurally, a raw equality check is almost always cheaper than reconstructing that logic manually. This can be particularly helpful when working with datums that represent values or state snapshots. The standard library already exposes useful helpers for this. For instance, [`assets.match`](https://aiken-lang.github.io/stdlib/cardano/assets.html#match) can compare a runtime `Value` against a `Data` representation while letting you parameterize how lovelace should be checked: ```aiken pub fn match( left: Value, right: Data, assert_lovelace: fn(Lovelace, Lovelace) -> Bool, ) -> Bool ``` The more general lesson is that if most of a structure should remain unchanged, it is often better to compare the unchanged parts directly and isolate only the parts that are expected to vary. For instance, a powerful optimisation pattern is to quickly split a structure into: * the part before the variable region, * the variable region itself, * the part after the variable region. You can then compare the stable regions directly through data equality and only inspect the changing part in detail. ```aiken let input_tokens_before, input_tokens_at, input_tokens_after <- split_at(input.value, policy_id) let output_tokens_before, output_tokens_at, output_tokens_after <- split_at(output.value, policy_id) ``` Then: ```aiken expect and { (input_tokens_before == output_tokens_before)?, (input_tokens_after == output_tokens_after)?, (input_tokens_at != output_tokens_at)?, } ``` This is often much cheaper than re-computing full semantic comparisons over complete `Value` structures. ### Use backpassing when returning more than one value Returning large tuples or records is convenient, but it also means constructing intermediary values only to immediately destructure them again. In hot paths, that overhead can become noticeable. Backpassing lets you thread the "continuation" directly through the function instead. **mem=63.84K** · **cpu=19.49M** ```aiken // Construct and de-construct a 2-tuple on each pass pub fn count_and_sum(self: List) -> (Int, Int) { when self is { [] -> (0, 0) [head, ..tail] -> { let (count, sum) = count_and_sum(tail) (count + 1, sum + head) } } } ``` **mem=47.26K** · **cpu=12.69M** ```aiken // Leverage back-passing to avoid needless tuple constructions pub fn count_and_sum_ret(self: List, return: fn(Int, Int) -> result) -> result { when self is { [] -> return(0, 0) [head, ..tail] -> { let count, sum <- count_and_sum_ret(tail) return(count + 1, sum + head) } } } ``` ```aiken test baseline_tuple() { expect (0, 0) = count_and_sum([]) expect (3, 3) = count_and_sum([1, 1, 1]) expect (5, 15) = count_and_sum([1, 2, 3, 4, 5]) Void } test baseline_backpassing() { expect 0, 0 <- count_and_sum([]) expect 3, 3 <- count_and_sum([1, 1, 1]) expect 5, 15 <- count_and_sum([1, 2, 3, 4, 5]) Void } ``` This style becomes even more useful in recursive code and stateful folds. It is also the reason helpers such as `list.foldl2` and `list.foldr2` are so valuable: they allow you to accumulate multiple pieces of state without repeatedly packaging and unpackaging them. ### If backpassing is not an option, prefer `Pair` over 2-tuples When you need to return exactly two values and backpassing would make the code less readable, `Pair` is often slightly preferable to `(a, b)`. Both are ergonomic to access: * `pair.1st` / `tuple.1st` * `pair.2nd` / `tuple.2nd` But `Pair` integrates more naturally with dictionaries and pairs-based APIs, so it tends to compose better with the rest of the standard library. This is not usually a game-changing optimisation, but it is a good default when dealing with key-value shaped data. ### Lean more on ByteArrays Byte arrays are extremely cheap compared to richer structured data. So, when cost is absolutely critical, one option is to give up some of the convenience of structured encodings and operate directly on bytes. ```aiken pub type MyRedeemer { key: ByteArray, signature: ByteArray, } let MyRedeemer { key, signature } = redeemer ``` ```aiken pub type MyRedeemer = ByteArray let key = bytearray.slice(redeemer, 0, 32) let signature = bytearray.slice(redeemer, 32, 64) ``` This comes with obvious trade-offs: * less self-documenting code, * more manual slicing and offset management, * fewer type-level guarantees. So it should only be used when the savings are worth the loss in readability and maintainability. ## Search and recurse efficiently Most validator cost is a traversal of transaction inputs, outputs, or a datum collection. How you write the recursion, and whether you exploit any ordering the data already has, sets what that traversal costs. ### Use fast recursion for infallible searches This is a more specific version of the fail fast strategy that applies to _'infallible searches'_. This happens when looking for specific elements within a collection without any possible error recovery: if not present, then it's an error and the entire validator must fail. Such a scenario is actually quite common in validators, especially when dealing with elements that are part of a protocol. **mem=12.15K** · **cpu=3.13M** ```aiken // Repeatedly check for empty list pub fn has(haystack: List, needle: a) -> Bool { when haystack is { [] -> False [head, ..tail] -> head == needle || has(tail, needle) } } ``` **mem=9.56K** · **cpu=2.33M** ```aiken // Fails anyway on empty list, value must be present. pub fn has(haystack: List, needle: a) -> Bool { head_list(haystack) == needle || has(tail_list(haystack), needle) } ``` ```aiken test baseline() { expect has(["alice", "bob", "carol"], "carol") } ``` ### Favor binary searches over linear searches It is quite common to have chains of multiple conditions which, when written in the most naive way can result in unnecessary evaluations. When conditions are somewhat equiprobable (i.e. there's no clear unbalance that one may be satisfied way more often than others), it may be useful to restructure and nest certain if/then/else to perform a binary search. **mem=46.98K** · **cpu=12.68M** ```aiken // Linear search, not ideal. fn mod32(i) { if i < 32 { 0 } else if i < 64 { 1 } else if i < 96 { 2 } else if i < 128 { 3 } else if i < 160 { 4 } else if i < 192 { 5 } else if i < 224 { 6 } else { 7 } } ``` **mem=37.06K** · **cpu=9.76M** ```aiken // Binary search, more efficient and predictable. fn mod32(i) { if i < 128 { if i < 64 { if i < 32 { 0 } else { 1 } } else { if i < 96 { 2 } else { 3 } } } else { if i < 192 { if i < 160 { 4 } else { 5 } } else { if i < 224 { 6 } else { 7 } } } } ``` ```aiken test baseline() { and { mod32(15) == 0, mod32(47) == 1, mod32(89) == 2, mod32(114) == 3, mod32(147) == 4, mod32(171) == 5, mod32(200) == 6, mod32(225) == 7, } } ``` In this example, we branch based on the value of some integer chosen between 0 and 255. The first form evaluates each condition one after the other, resulting in a **linear search** that will average at `(n + 1) / 2` evaluations. So for `n=7`, that's an average of `4` evaluations. The _do_ example, however, arranges the conditions to reduce the amount of evaluations done at each pass. It performs a **binary search** which results in `log2(n)` evaluations. So for `n=7`, that's an average of `3` evaluations. Moreover, the binary search has the benefit of being more **predictable**. In the previous example, it does not only average to 3 condition evaluations, it always evaluates 3 conditions per pass. Unlike the _don't_ example, which sometimes evaluates one condition, sometimes three, sometimes seven, etc... ### Unroll recursions When a recursive function advances one step at a time, its convergence can sometimes be improved by manually unrolling the first few steps. This reduces the number of recursive calls needed in the common case. **mem=47.53K** · **cpu=12.74M** ```aiken fn elem_at(elems: List, at: Int) -> a { if at <= 0 { list.head(elems) } else { elem_at(list.tail(elems), at - 1) } } ``` **mem=35.01K** · **cpu=9.70M** ```aiken fn elem_at(elems: List, at: Int) -> a { if at >= 2 { elem_at(list.tail(list.tail(elems)), at - 2) } else { list.head(if at == 1 { list.tail(elems) } else { elems }) } } ``` ```aiken test baseline() { and { elem_at([1], 0) == 1, elem_at([1, 2, 3, 4, 5], 0) == 1, elem_at([1, 2, 3, 4, 5], 4) == 5, elem_at([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 9) == 10, } } ``` This sort of transformation is most useful for small, performance-critical helpers that get called repeatedly. ### Write tail-recursive functions The Plutus VM usually behaves better with tail-recursive functions, especially when working with bytes and accumulators. So when you can express a function as a loop with an explicit accumulator, prefer that form. **mem=80.36K** · **cpu=21.83M** ```aiken fn fib(n: Int) -> Int { if n <= 1 { 1 } else { fib(n - 1) + fib(n - 2) } } ``` **mem=49.98K** · **cpu=12.60M** ```aiken fn fib(n: Int) -> Int { do_fib(1, 1, n) } fn do_fib(last: Int, current: Int, n: Int) -> Int { if n <= 1 { current } else { do_fib(current, current + last, n - 1) } } ``` ```aiken test baseline() { and { fib(0) == 1, fib(1) == 1, fib(2) == 2, fib(3) == 3, fib(4) == 5, fib(5) == 8, } } ``` The tail-recursive version makes the control flow more explicit and typically avoids building up deferred work across calls. This pattern is especially relevant for: * folds, * list traversals, * byte processing, * numeric loops. ## Traverse once Each of these replaces several passes over a collection with a single pass, either by combining the work or by keeping what the first pass already computed. ### Avoid re-traversals Traversing the same collection multiple times is one of the easiest ways to accumulate avoidable costs. In validators, collections are often not that large, but repeated passes still add up quickly. If you need several derived values from the same list, try to compute them in one traversal. **mem=143.78K** · **cpu=39.27M** ```aiken fn count_all_and_filter( self: List, return: fn(Int, List) -> result, ) -> result { let count = list.count(self, fn(_) { True }) let positive = list.filter(self, fn(n) { n >= 0 }) return(count, positive) } ``` **mem=103.30K** · **cpu=27.79M** ```aiken fn count_all_and_filter( self: List, return: fn(Int, List) -> result, ) -> result { list.foldr2(self, 0, [], fn(n, count, positive, next) { next( count + 1, if n >= 0 { [n, ..positive] } else { positive }, ) }, return ) } ``` ```aiken test baseline() { expect 0, [] <- count_all_and_filter([]) expect 1, [] <- count_all_and_filter([-1]) expect 3, [1, 2, 3] <- count_all_and_filter([1, 2, 3]) expect 8, [1, 3, 5, 7] <- count_all_and_filter([1, -2, 3, -4, 5, -6, 7, -8]) Void } ``` The exact shape of the fold depends on the situation, but the principle remains the same: if you already have the element in hand, do as much useful work with it as possible before moving on. ### Validate while iterating The same principle applies to validation. If your goal is merely to ensure that all matching elements satisfy some property, it is often unnecessary to first extract them into a separate list. **mem=71.42K** · **cpu=20.37M** ```aiken fn validate_outputs(self: Pairs) -> Void { let my_outputs = list.filter(self, fn(output) { output.1st == "my_address" }) expect list.all(my_outputs, fn(output) { output.2nd >= 42 }) } ``` **mem=59.7K** · **cpu=17.44M** ```aiken fn validate_outputs(self: Pairs) -> Void { expect list.all( self, fn(output) { output.1st != "my_address" || output.2nd >= 42 }, ) } ``` ```aiken test baseline() { validate_outputs([Pair("me", 42), Pair("you", 14), Pair("me", 1337)]) validate_outputs([Pair("a", 1), Pair("b", 2), Pair("c", 3)]) validate_outputs([Pair("me", 100), Pair("me", 100), Pair("me", 100)]) } ``` This form avoids building an intermediate list and often short-circuits earlier. ### Build local caches Aiken functions are first-class and cheap enough to make small local caches a practical optimisation technique. When you repeatedly test membership against the same collection, you can pre-build a closure that captures the known elements. **mem=40.79K** · **cpu=11.82M** ```aiken // Repeatedly traverse the withdrawals for each ask fn has_withdrawal( script: ScriptHash, withdrawals: Pairs, ) -> Bool { list.any(withdrawals, fn(Pair(k, _)) { k == script }) } ``` **mem=31.39K** · **cpu=8.15M** ```aiken // Build a local cache that's faster to repeatedly call fn new_cache(has: fn(k) -> Bool, elems: Pairs) -> fn(k) -> Bool { when elems is { [] -> has [Pair(head, _), ..tail] -> new_cache(fn(k) { head == k || has(k) }, tail) } } ``` ```aiken const baseline_withdrawals = [Pair("a", 1), Pair("b", 2), Pair("c", 3), Pair("d", 4)] test baseline_dont() { and { has_withdrawal("a", baseline_withdrawals), has_withdrawal("b", baseline_withdrawals), has_withdrawal("c", baseline_withdrawals), has_withdrawal("d", baseline_withdrawals), } } test baseline_do() { let has_withdrawal = new_cache(fn(_) { False }, baseline_withdrawals) and { has_withdrawal("a"), has_withdrawal("b"), has_withdrawal("c"), has_withdrawal("d"), } } ``` Conceptually, this transforms the collection into a small decision chain that can then be reused multiple times without unpacking the original structure again and again. This is particularly nice when the source collection is static for the whole validator execution, but queried many times. ## Replace computation with lookup or proof The biggest savings come from not computing at all: look the answer up, verify an answer the transaction supplies, or lean on something the ledger has already guaranteed. ### Replace expensive computations with lookups A recurring optimisation theme is that computation is often more expensive than lookup. If a function operates on a bounded domain, it may be possible to precompute all results and store them in a compact lookup structure. For very small domains, a byte array can already act as a lookup table. The exact implementation may vary, but the technique is useful whenever: * the input space is bounded, * the output can be encoded compactly, * and the computation is expensive enough to justify precomputation. A nice example from the standard library is `math.pow2` which combines bytearray lookups and unrolling recursions for maximum efficiency: **mem=91.50K** · **cpu=25.44M** ```aiken // Naively iterating one operand at a time pub fn pow2(e: Int) -> Int { if e <= 0 { if e == 0 { 1 } else { 0 } } else { 2 * pow2(e - 1) } } ``` **mem=25.86K** · **cpu=6.58M** ```aiken // Unrolling the last 8 levels of recursions thanks to a bytearray lookup pub fn pow2(e: Int) -> Int { if e < 8 { if e < 0 { 0 } else { bytearray.at(#[1, 2, 4, 8, 16, 32, 64, 128], e) } } else { 256 * pow2(e - 8) } } ``` ```aiken test baseline() { and { pow2(0) == 1, pow2(1) == 2, pow2(7) == 128, pow2(10) == 1024, pow2(11) == 2048, } } ``` Notice how the small cases are handled via direct byte array lookup instead of repeated multiplication. That same idea can often be applied for small operations on integers. Even multiple array lookups (for values larger than 255 for example) may sometimes be worth considering! ### Use Merkle Patricia Forestry for larger registries For larger lookup spaces, plain byte arrays are no longer enough. This is where [Merkle Patricia Forestry](https://github.com/aiken-lang/merkle-patricia-forestry) can become useful: it lets you represent arbitrarily large key-value registries with very cheap membership checks on-chain. So instead of: ```aiken let y = expensive_function(x) ``` you can sometimes do: ```aiken expect mpf.member(y, root) ``` where `root` is the precomputed authenticated structure committed off-chain. The general pattern is: 1. compute a large registry off-chain, 2. commit its root on-chain, 3. pass proofs or queried values through the redeemer, 4. verify membership instead of recomputing. This is especially attractive for deterministic but expensive (>1% of the total execution budget) functions over bounded domains. ### Don't compute, verify This brings us to a broader principle: validators do not always need to perform a computation from scratch. Very often, it is enough to verify that a value provided by the transaction is correct. **mem=55.79K** · **cpu=18.16M** ```aiken math.sqrt(123456789) == Some(11111) ``` **mem=1.00K** · **cpu=0.22M** ```aiken math.is_sqrt(123456789, 11111) ``` Redeemers are perfect for this. They can carry precomputed candidate values, witnesses, proofs, or decompositions, and the validator only needs to check that they are valid. This is **one of the most important optimisation patterns** in on-chain programming: move work off-chain whenever correctness can still be verified cheaply on-chain. ### Leverage ledger invariants A final optimisation technique is not really about code shape, but about knowing the ledger well enough to rely on its guarantees. Many structures that validators inspect already satisfy strong invariants. For example: * inputs are alphabetically ordered, * values are ordered by policy and asset name, * redeemers and datums are indexed by hashes, * output values never contain negative quantities, * output values always include ADA. * minted values never include ADA. * etc... These are not merely convenient facts; they are often the key to writing much cheaper algorithms. If you know a structure is already sorted, you can merge or compare it linearly instead of re-sorting it. If you know quantities cannot be negative, you can avoid defensive normalization logic. If you know a map has no duplicate keys, you can use more direct traversal strategies. The most effective optimisations are often not "clever code tricks", but code that aligns tightly with invariants already guaranteed by the ledger. ## Summary Most optimisation work in Aiken follows a few recurring themes: * fail early instead of recovering, * avoid reconstructing rich structures unnecessarily, * traverse collections as few times as possible, * replace computation by lookup when practical, * replace computation by verification whenever possible, * and lean on the invariants the ledger already gives you. A good workflow is usually: 1. write the simplest obviously correct validator, 2. benchmark it end-to-end, 3. identify the actual hot paths, 4. optimise only those parts, 5. re-benchmark after every significant change. --- ## Advanced Smart Contracts You can write, test, and secure a validator. Everything here is what you reach for after that, when a contract has to be cheaper, larger, more composable, or has to prove something rather than check it. This is a reference shelf, not a sequential read. Nothing below is a prerequisite for anything else, so come back to a page when you hit the problem it solves. ## What is here - **[Design patterns](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview)**: the reusable architectures the ecosystem has converged on, including the withdraw-zero trick for running shared validation once per transaction and on-chain structures (linked lists, tries, Merkle trees) for state too large to sit in one UTXO. - **[UPLC](/docs/developers/curriculum/smart-contracts/advanced/uplc)**: the untyped lambda calculus every Cardano language compiles down to. Read this when you want to know what your code actually becomes. - **[Debug CBOR](/docs/developers/curriculum/smart-contracts/advanced/debug-cbor)**: decoding the binary encoding that datums, redeemers, and scripts travel in, which is how you diagnose a mismatch the compiler cannot see. - **[Contract optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization)**: benchmarking first, then the techniques that cut execution units, grouped by the kind of saving they make. - **[Zero-knowledge proofs](/docs/developers/curriculum/smart-contracts/advanced/zero-knowledge)**: verifying a computation on-chain without re-running it or revealing its inputs. - **[BLS signatures, VRFs and credentials](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives)**: the BLS12-381 builtins as a construction kit for aggregated signatures, verifiable randomness, and selective-disclosure credentials. Security has its own section rather than living here: the [vulnerability reference](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview) and the [CTF](/docs/developers/curriculum/smart-contracts/security/ctf) are things every contract author needs, not advanced material. ## Next steps - [Design patterns](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview) is the most broadly useful page here, and the one to read even if you have no specific problem yet - [Build a dApp](/docs/developers/curriculum/dapps/overview): the next module, where these contracts meet users --- ## Untyped Plutus Core ## Untyped Plutus Core: The Execution Layer At the lowest level, all Cardano smart contracts execute as **Untyped Plutus Core** (UPLC) programs. Understanding UPLC shows you how your high-level smart contract code actually runs on-chain. ### What is UPLC? UPLC is the "assembly language" of Cardano smart contracts. Every smart contract language (Aiken, Plutus Haskell, OpShin, etc.) compiles down to UPLC before execution. Think of it as the intermediate representation that the Cardano virtual machine actually executes. (Why several languages target it, and how to pick one, is [Choose a language](/docs/developers/curriculum/smart-contracts/choose-a-language).) **Compilation Pipeline:** ``` High-level Code → Typed Plutus Core → UPLC → Binary Encoding → On-chain Execution ``` ### Properties - While UPLC has no explicit types, it preserves the implicit type structure from the original typed program. Type mismatches still cause runtime errors, but the execution model is simpler. - UPLC uses lambda calculus with functions, variables, constants, and application. Everything is a function or can be applied to a function. Variables use DeBruijn indices instead of names, referring to bound variables by their position in ancestor lambdas. - UPLC can express any computation that can be performed by a computer, making it powerful enough to handle complex smart contract logic. - Since there are no operators, all operations (even basic arithmetic) use built-in functions like `addInteger`, `appendByteString`, or `verifyEd25519Signature`. ### Basic UPLC Components #### Primitive Types UPLC supports seven primitive types: - **unit**: `(con unit ())` - **bool**: `(con bool True)` - **integer**: `(con integer 42)` - **bytestring**: `(con bytestring #41696b656e)` - **string**: `(con string "Hello")` - **pair**: `(con pair [True, 42])` - **list**: `(con list [1, 2, 3])` #### Functions and Application Functions use lambda syntax: ``` (lam x x) // Identity function [ (lam x x) (con integer 42) ] // Apply identity to 42 ``` #### Built-in Functions Essential operations use built-ins: ``` [ [ (builtin addInteger) (con integer 16) ] (con integer 26) ] // 16 + 26 [ [ (builtin equalsByteString) #hello ] #world ] // Compare bytes ``` #### Data Type The generic `Data` type is crucial for smart contracts, representing arbitrary structured data used in datums and redeemers. It supports five constructors: ``` data Data = Constr Integer [Data] -- Tagged constructors with data | Map [(Data, Data)] -- Key-value mappings | List [Data] -- Homogeneous lists | I Integer -- Integer values | B ByteString -- Binary data ``` **Working with Data**: Built-in functions help construct and access Data values: - `constrData`, `unConstrData` - Work with tagged constructors - `listData`, `unListData` - Build and extract lists - `mapData`, `unMapData` - Handle key-value pairs - `iData`, `unIData` - Convert integers to/from Data - `bData`, `unBData` - Convert bytestrings to/from Data This type system allows high-level languages to serialize complex data structures into a format that UPLC can process uniformly. ### Binary Encoding and Execution On-chain, UPLC programs are stored as compact binary data using the "flat" encoding format. This binary representation is what validators actually receive and execute. The flat blob is then wrapped in a CBOR byte string, and the script hash that addresses the script on-chain is computed over a one-byte language tag (PlutusV1, V2, or V3) followed by that wrapper. That is why identical bytes hash differently under different Plutus versions, and why script bytes sometimes appear double-CBOR-encoded in tooling. The [reference script fee](/docs/developers/curriculum/fundamentals/core-concepts/fees#reference-script-fees) is metered on those same wrapped bytes, the language tag aside. **Size Implications**: UPLC programs can be large, which is why the transaction size limit (`maxTxSize`, currently ~16 KB) becomes important for complex smart contracts. Recent improvements like reference scripts help mitigate this. **Execution Costs**: Every UPLC operation has precise memory and CPU costs defined by the protocol's cost model. These costs enable predictable fee calculation and execution budgets. The model has two kinds of charge: every step the evaluator takes (looking up a variable, processing a lambda) costs a small fixed amount, and every built-in call is priced by a costing function whose parameters were fitted statistically from benchmarks of the real evaluator. Integer multiplication, for example, is costed by the machine-word sizes of its two arguments, which is why a built-in's charge grows with the size of its inputs, not just the number of calls. The charged costs are deliberately conservative: each built-in is costed for its worst case, and much of the charge covers the interpreter machinery around the operation rather than the operation itself. Even so, a built-in call is far cheaper than expressing the same logic as plain UPLC terms, which is why compilers push as much work as possible into built-ins. ### Why This Matters for Developers - When smart contracts fail, understanding UPLC helps interpret low-level error messages and execution traces for debugging. Stepping debuggers such as [Gastronomy](https://github.com/SundaeSwap-finance/gastronomy) replay a script's execution state by state, forward and backward. - Knowing how high-level constructs compile to UPLC helps write more efficient smart contracts. - UPLC is the common target for all smart contract languages, enabling cross-language compatibility. - Understanding the compilation pipeline helps minimize the size of on-chain scripts and optimize your contracts to their last bits. While you won't write UPLC directly, understanding it as the execution foundation helps you write better smart contracts in any high-level language. For complete technical details including formal syntax and semantics, see the [Formal Specification of the Plutus Core Language](https://plutus.cardano.intersectmbo.org/resources/plutus-core-spec.pdf). For a readable walk-through of the machine that executes UPLC, its states and every transition rule, see the [Cardano Blueprint's CEK machine page](https://cardano-scaling.github.io/cardano-blueprint/plutus/cek.html). --- ## Zero-Knowledge Proofs ## Introduction A zero-knowledge proof lets one party convince another that a statement is true without revealing why it is true. On a public ledger that unlocks two things at once: **privacy**, prove you are eligible, solvent, or authorized without exposing the underlying data, and **succinctness**, verify one short proof on-chain instead of re-running an expensive computation, which is the foundation most zk-rollup scaling designs build on. You already know the primitive this improves on. A hash lock ([pre-image resistance](/docs/developers/curriculum/fundamentals/cryptographic-primitives#what-is-a-cryptographic-hash-function)) also proves you know a secret, but revealing the pre-image burns it: once it is on-chain, anyone can copy it. A zero-knowledge proof makes the same claim, "I know a value that hashes to this", without ever publishing the value, so the secret survives being used. A proof system gives you three guarantees: **completeness** (an honest prover with a valid secret can always convince the verifier), **soundness** (a prover without a valid secret cannot, except with negligible probability), and **zero-knowledge** (the verifier learns nothing beyond the fact that the statement is true). ## What a proof actually claims Most proof systems used on Cardano are **zk-SNARKs**: succinct non-interactive arguments of knowledge. The statement you want to prove is encoded as an **arithmetic circuit**, a set of equations over a finite field. The prover holds a private **witness** (the secret), the circuit takes some **public inputs** (values both sides can see), and the proof asserts: *I know a witness that satisfies this circuit for these public inputs.* The important consequence: **the proof only proves what the circuit constrains**. A circuit that checks `a * b * c == n` proves you know three factors of `n`, not three *prime* factors; if primality matters, the circuit must enforce it. When you design or audit a ZK application, read the circuit, not the marketing: every property the application claims must appear as a constraint. ## How proofs fit the eUTxO model Proving and verifying have wildly asymmetric costs, and that asymmetry maps cleanly onto Cardano's architecture. Generating a proof is heavy, seconds to minutes of computation, and happens **off-chain**, in a browser or on a backend. Verifying is cheap and deterministic, so it can happen **on-chain**, inside a validator, within one script execution: ```mermaid graph LR C["Circuit(Circom, gnark, Halo2)"] --> P["Prover, off-chain(witness + public inputs)"] P --> PR["Proof(a few hundred bytes,compressed points)"] PR --> R["Redeemer"] R --> V["Validator(Aiken / Plinth verifier)"] K["Verification key(datum or constant)"] --> V V --> B["BLS12-381 builtins(pairing check)"] style V fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style C fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style P fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style PR fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style R fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style K fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style B fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` The recurring on-chain shape, whichever toolchain you use: - The **verification key** (a handful of curve points that commit to the circuit) lives in a datum, often on a reference input, or is compiled into the validator. - The **proof** travels in the [redeemer](/docs/developers/curriculum/smart-contracts/datum-redeemer-context) of the spending transaction. - The **public inputs** come from the datum, the redeemer, or the transaction context. - The validator decompresses the points, runs the proof system's pairing equation with the BLS12-381 builtins, and approves or rejects the spend. Because Plutus evaluation is deterministic, verification cost is known before the transaction is submitted; a proof that verifies locally will verify on-chain. ## The primitives: what shipped when On-chain verification is built on two protocol upgrades: - **Chang (September 2024, Plutus V3)** shipped [CIP-0381](https://cips.cardano.org/cip/CIP-0381): 17 builtins for the pairing-friendly **BLS12-381** curve, group operations (`bls12_381_G1_add`, `bls12_381_G1_scalarMul`, and their G2 counterparts), the pairing (`bls12_381_millerLoop`, `bls12_381_mulMlResult`, `bls12_381_finalVerify`), plus compression, hashing-to-group, and equality. They are backed by the independently audited `blst` library, so the *primitives* are production-grade even where the verifiers built on them are not. This made pairing-based SNARK verification possible on Cardano. - **van Rossem (July 2026, protocol version 11)** made it cheaper: [CIP-0133](https://cips.cardano.org/cip/CIP-0133) added multi-scalar multiplication builtins (`bls12_381_G1_multiScalarMul`, `bls12_381_G2_multiScalarMul`), the operation that dominates PLONK and Halo2 verification, and [CIP-0109](https://cips.cardano.org/cip/CIP-0109) added `expModInteger` for modular field arithmetic. Two practical constraints follow from the builtin design: - **Points cross the boundary compressed.** Curve points can only be stored in datums and redeemers in compressed form, 48 bytes for G1, 96 bytes for G2, so every pipeline includes a compression step off-chain and `uncompress` calls in the validator. - **Verification is affordable but not free.** A Groth16 verification measures around a fifth to a quarter of one script's CPU budget; a full PLONK verification has been measured around a third. Circuit size has no effect on that cost, the proof is the same three points whether the circuit has five constraints or five million; what grows it is the **public-input count**, at roughly 50M CPU units each, with about a hundred public inputs as the practical ceiling. Keep public inputs few, and commit to bulky data with a single hash instead. Proof verification is also not all these builtins do: the same operations compose into aggregated [BLS signatures, on-chain key derivation, VRFs, and BBS+ anonymous credentials](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives). ## Proof systems in use on Cardano | Proof system | Proof size | Setup | How it reaches Cardano | |---|---|---|---| | **Groth16** | Smallest (~200 bytes: two G1 points and one G2 point) | Trusted setup **per circuit** | Circuits in Circom or gnark; Groth16 verifiers in Aiken | | **PLONK** | ~0.5 KB | **Universal** trusted setup, reusable across circuits | Circom circuits via a snarkjs adaptation; verifiers in Plutus and Aiken | | **Halo2 (KZG)** | Circuit-dependent | Universal setup | Verifier code generated from a Rust circuit into Plinth or Aiken | | **Sigma protocols** (e.g. Schnorr) | A few group elements | **None** | Implemented directly on the BLS12-381 builtins | Two contrasts worth internalizing. Groth16 gives the smallest proofs and cheapest verification but requires a new trusted setup for every circuit; PLONK and Halo2 pay slightly more per proof for a setup you do once. And not everything needs a circuit: a **sigma protocol** proves knowledge of a discrete logarithm ("I know the secret behind this public point") with no circuit, no ceremony, and a much simpler verifier, if that is all your application needs, it is the lighter tool. Halo2 is also the proof system used by [Midnight](https://midnight.network/), which is why generated Halo2 verifiers matter for bridging proofs from it into Cardano validators. ## The pipeline in practice Three circuit frontends are in active use, all converging on the on-chain shape above: - **Circom + snarkjs**: write the circuit in Circom and compile with `--prime bls12381` (the default field targets a different curve); a [snarkjs adaptation for Cardano](https://github.com/perturbing/snarkjs-cardano) runs the setup and generates proofs that Plutus verifiers accept, in Node or in the browser. - **gnark (Go)**: the [gnark-cardano toolkit](https://github.com/logical-mechanism/Peace-Protocol/tree/dev/public/gnark-cardano) wraps the gnark prover in an end-to-end pipeline: it runs the setup, generates the proof, converts everything to Cardano datum format, and generates Aiken tests for your circuit against a generic Groth16 verifier. It also ships a `proof.pattern` mini-language for describing circuits declaratively, and commands for running a multi-party setup ceremony. - **Halo2 (Rust)**: the [Halo2 Plutus verifier generator](https://github.com/input-output-hk/plutus-halo2-verifier-gen) extracts the verification logic from a Rust Halo2 circuit and emits a ready-made verifier in Plinth or Aiken. ### What to watch for - **Trusted setup is a real ceremony.** Groth16 and PLONK setups produce toxic waste: whoever holds the setup randomness can forge proofs. Multi-party ceremonies fix this, the result is secure if *any one* participant was honest, and phase 1 ("powers of tau") is circuit-independent and reusable, while Groth16's phase 2 must be redone per circuit. A single-party setup on your laptop is fine for a demo and disqualifying for production. For phase 1 you no longer have to start from scratch: a community-run [perpetual powers of tau ceremony](https://github.com/p0tion-tools/cardano-ppot) for BLS12-381 has published `.ptau` files from 41 verified contributions, covering circuits up to 2^23 constraints, so a Cardano project only needs to run its own phase 2. - **Use circuit-friendly hashes, on the right curve.** SHA-256 or Blake2b inside a circuit costs tens of thousands of constraints; **Poseidon** and **MiMC** are hashes designed for circuits. One sharp edge: standard Poseidon parameters are generated for other curves, if you hash over BLS12-381 in-circuit, generate matching parameters, or the hash your circuit computes will never equal the one your off-chain code computed. Signatures are worse still: verifying a real Ed25519 signature in-circuit runs to millions of constraints, so circuits substitute curve-native schemes such as EdDSA over JubJub, and a proof about such a key proves nothing about your actual Cardano signing key. - **A proof on-chain is public.** Anyone can read a valid proof out of a redeemer and replay it. Bind every proof to its context by putting a challenge nonce, the spending transaction, or a session key into the public inputs, so a copied proof is useless anywhere else. - **Whoever runs the prover sees the witness.** Browser proving works (proving keys run to hundreds of megabytes and proofs take tens of seconds, both cacheable), and it keeps the secret on the user's device. If you outsource proving to a server, the user has server-side secrecy, not zero-knowledge privacy, name that trust assumption if you make it. ## Verifiers and toolkits Everything below is open source and none of it is audited; treat these as research-grade building blocks. - [gnark-cardano](https://github.com/logical-mechanism/Peace-Protocol/tree/dev/public/gnark-cardano): the gnark-to-Aiken Groth16 pipeline described above, the most automated path from circuit to tested validator. - [Halo2 Plutus verifier generator](https://github.com/input-output-hk/plutus-halo2-verifier-gen): generates Halo2/KZG verifiers in Plinth or Aiken; includes an aggregate multisignature (ATMS) example. Explicitly a research proof of concept. - [snarkjs-cardano](https://github.com/perturbing/snarkjs-cardano): the snarkjs toolchain (Groth16, PLONK) adapted to BLS12-381 output for Plutus verifiers. - [plutus-plonk-example](https://github.com/perturbing/plutus-plonk-example): an end-to-end PLONK verifier in Plutus with published cost benchmarks. - [Cardano Foundation BLS repository](https://github.com/cardano-foundation/bls): a Groth16 pipeline built to be inspected as much as used, a generic Aiken verifier that takes any circuit's verification key, a Rust prover that consumes Circom artifacts, and a multi-party phase-2 ceremony CLI, with every proving step cross-checked bit for bit against an independent SageMath implementation. Apache-2.0; the same repository holds the BLS signature, VRF, and KDF examples on the [BLS primitives page](/docs/developers/curriculum/smart-contracts/advanced/bls-primitives). - [ak-381](https://github.com/Modulo-P/ak-381): a community Groth16 verifier library for Aiken with Circom/snarkjs conversion scripts; several of the applications below build on it. Early-stage: point compression handling is still listed as future work, and the repository currently ships no license. - [Aiken ZKP library](https://github.com/adaocommunity/zk): a community effort collecting Groth16 (building on ak-381), PLONK, and early Bulletproofs verifiers in Aiken, aiming at a standards-compliant library; Apache-2.0 licensed, under development. - [ZeroJ](https://github.com/bloxbean/zeroj): a pure-Java toolchain spanning the whole pipeline, a circuit DSL, Groth16 and PLONK provers and verifiers over BLS12-381, Circom interoperability, and generated Plutus V3 verifier validators (demonstrated on a local devnet). A companion [use-cases repository](https://github.com/bloxbean/zeroj-usecases) holds eight runnable end-to-end examples, from proof of reserves and private voting to reusable KYC. The project states plainly that the codebase is AI-generated with human-assisted design and testing, and not for production use. ## What people have built Working applications, all explicitly experimental, that show the range of what the primitives support: - **[Sudoku bounty](https://github.com/perturbing/sudoku-bounty)** (mainnet): bounties locked at a script address that only someone who *proves they solved the puzzle* can claim, without revealing the solution. The browser generates a PLONK proof from a ~36,000-constraint Circom circuit; the Aiken validator verifies it as the spending condition. The purest demonstration that a proof can *be* the redeemer logic, and that publishing the answer is no longer the price of claiming you know it. - **[zkLogin](https://github.com/eryxcoop/zklogin-aiken)** (preprod): authenticate with an existing OpenID account such as Google and control funds without a seed phrase. The circuit verifies the provider's RSA-signed token and binds it to an ephemeral session key; the validator checks the proof once, then the session key signs transactions, and a user salt keeps the web identity unlinkable to the address. An unaudited proof of concept, proving currently runs on a backend. See [wallet authentication](/docs/developers/curriculum/dapps/wallet-authentication#zero-knowledge-login) for the dApp-side context. - **[Seedelf](https://github.com/logical-mechanism/Seedelf-Wallet)** (stealth wallet): hides who is paying whom by re-randomizing address material, and authorizes spending with a Schnorr sigma protocol verified on-chain, no circuits, no trusted setup. Its README candidly documents known attacks on its privacy; read it as an honest research wallet. - **[Cardano Semaphore](https://github.com/Modulo-P/Cardano-Semaphore)** (alpha): a port of Ethereum's Semaphore protocol for anonymous signaling. Members register an identity commitment into a Merkle-tree group, then publish messages (a vote, an endorsement, a withdrawal request) with a proof that *some* member sent it, without revealing which one. Each message carries a **nullifier**, a value derived from the identity that exposes double-signaling without deanonymizing anyone; the port keeps the spent-nullifier set in a Merkle Patricia Forestry. Unaudited, and its trusted-setup ceremony is still listed as to-do. - **[Proof of innocence](https://github.com/eryxcoop/cardano-zk-proof-of-innocence)** (proof of concept): prove that your funds do not originate from a blacklisted set of transactions, without revealing which transactions are yours. Privacy tools create anonymity sets; this is the compliance complement, letting an honest user clear themselves while keeping the anonymity intact. Circom circuits with an Aiken verifier. - **[PEACE protocol / Veiled](https://github.com/logical-mechanism/Peace-Protocol)** (preprod): a marketplace for encrypted data where a Groth16 proof shows the seller derived the re-encryption key *correctly*, so decryption rights transfer trustlessly. Notable as a proof of correct *computation* rather than mere knowledge, and as the project the gnark-cardano toolkit was extracted from. - **[Janus Wallet](https://github.com/leobel/janus-wallet)** (preview): a smart-contract wallet where spending authority is knowledge of a password, proved with Groth16 against an on-chain challenge nonce that prevents replay, an end-to-end reference tying together most of the patterns on this page. ## Where to go deeper - **[ZK from zero on Cardano](https://github.com/elRaulito/ZK-from-zero-on-Cardano)**: an open-source ebook that builds from first principles to a complete password-locked UTxO with a Groth16 verifier in Aiken, the most complete written walkthrough of the Circom-to-Aiken pipeline. Chapters are still being added. - **[Unlocking zero-knowledge proofs for Cardano](https://iohk.io/en/blog/posts/2025/08/26/unlocking-zero-knowledge-proofs-for-cardano-the-halo2-plutus-verifier/)**: the IOG post introducing the Halo2 verifier generator. - The CIPs behind it all: [CIP-0381](https://cips.cardano.org/cip/CIP-0381) (pairing builtins), [CIP-0133](https://cips.cardano.org/cip/CIP-0133) (multi-scalar multiplication), [CIP-0109](https://cips.cardano.org/cip/CIP-0109) (modular exponentiation). - For applications that want privacy as the default rather than a feature, [Midnight](https://midnight.network/) is a Cardano partner chain built around zero-knowledge, and its Halo2 proofs can be verified inside Cardano validators via the generator above. :::info Research-grade, and moving fast The cryptographic primitives are in place and hardened, the CIP-0381 builtins sit on an audited library, and van Rossem made verification markedly cheaper. The verifiers and toolchains built on top are another matter: none are audited, several are explicit proofs of concept, and APIs are churning. Build and experiment, on testnets first, and read every circuit you depend on. ::: --- ## Choose a Smart Contract Language You write a validator in a high-level language, and it compiles down to **UPLC** (Untyped Plutus Core), the one bytecode every Cardano node executes. Because the on-chain target is the same regardless of source language, choosing a language is mostly about **ergonomics**: which one lets your team write correct, efficient validators fastest. This page helps you pick. ## Everything compiles to UPLC ```mermaid flowchart TD subgraph Sources["High-level languages"] A1["Aiken"] A2["Plinth / Plutus Tx (Haskell)"] A3["OpShin (Python)"] A4["Others"] end A1 --> C1["Aiken compiler (Rust)"] A2 --> C2["GHC + Plutus Tx plugin"] A3 --> C3["OpShin compiler"] A4 --> C4["Language-specific compiler"] C1 --> UPLC["UPLC bytecode"] C2 --> UPLC C3 --> UPLC C4 --> UPLC UPLC --> CBOR["CBOR serialization"] CBOR --> EXEC["On-chain execution\nby Cardano nodes"] EXEC --> RESULT["True / False"] ``` UPLC is a minimalist, lambda-calculus-based language: variables, functions, function application, constants, a fixed set of built-ins, and an `error` term. No loops, no mutable variables, no objects. That extreme simplicity is intentional: it makes on-chain execution deterministic and the node's evaluator small and auditable. The trade-off is that nobody writes UPLC by hand; you write something higher-level and let a compiler emit it. (If you want the low-level detail, see the [UPLC reference](/docs/developers/curriculum/smart-contracts/advanced/uplc).) The practical consequence: **your language choice does not change what's possible on-chain, only how pleasant it is to get there.** ## Aiken [Aiken](https://aiken-lang.org) is a language purpose-built for Cardano validators, with syntax borrowed from Rust, Elm, and Gleam. In the [developer ecosystem survey](https://cardano-foundation.github.io/state-of-the-developer-ecosystem/2025/#what-do-you-use-or-plan-to-use-for-writing-plutus-script-validators-smart-contracts) it is the most-used language for writing validators, and it is the language the examples in this module are written in. Why Aiken: - **Lower barrier to entry**: developers from Rust, TypeScript, or any ML-family language become productive quickly. - **Fast iteration**: the Rust-based compiler builds in seconds, not minutes. - **Smaller scripts**: optimized UPLC output means lower fees for your users. - **Built-in testing**: a test runner ships with the toolchain, so you write and run unit tests without extra tooling. (See [Testing](/docs/developers/curriculum/smart-contracts/testing).) - **Clean separation**: Aiken is on-chain only. Off-chain code stays in whatever language your app uses (TypeScript, Python, Rust), which reinforces the on-chain/off-chain split Cardano's architecture wants. - **Strong static typing**: full algebraic data types, pattern matching, generics, and inference, modern type safety with no runtime or garbage collector. ## Getting started with Aiken Install Aiken with `aikup`, its official version manager: ```bash npm install -g @aiken-lang/aikup # or: brew install aiken-lang/tap/aikup aikup # installs the latest Aiken ``` See [other install methods](https://aiken-lang.org/installation-instructions) for Homebrew and the standalone script. Aiken ships a single toolchain: Language Server support across the major editors, a built-in test runner that reports CPU and memory costs (see [Testing](/docs/developers/curriculum/smart-contracts/testing)), and a compiler that emits a CIP-57 Plutus blueprint on every build. To go deeper: - [aiken-lang.org](https://aiken-lang.org) for the language tour, guides, and standard library - [I Can Aiken](https://book.io/book/i-can-aiken/), an open book from the Cardano Foundation Academy - [Aiken video course](https://www.youtube.com/playlist?list=PLCuyQuWCJVQ1Zz9ySRMH_J6EymxhnZ0Hu), a multi-part walkthrough - [Awesome Aiken](https://github.com/aiken-lang/awesome-aiken) for community projects and reusable libraries ## When to choose something else Cardano's language diversity is a strength: because UPLC is a clean compilation target, many languages can target it (much as Rust, Go, and C++ all target WebAssembly). Pick by your team's existing expertise. | Language | Best for | Notes | |---|---|---| | **[Aiken](https://aiken-lang.org)** | Most new projects | Purpose-built, fast, small output, built-in tests. | | **[Plinth](https://plutus.cardano.intersectmbo.org/docs/)** (Plutus Tx) | Haskell teams | The original language; full Haskell type system, on- and off-chain code sharing. Steeper learning curve and larger scripts. | | **[Plutarch](https://github.com/Plutonomicon/plutarch-plutus)** | Performance-critical validators | Fine-grained control close to writing UPLC by hand; the most verbose path, aimed at minimizing execution costs. | | **[OpShin](https://opshin.dev)** | Python teams | Write validators in a subset of valid Python; pairs with PyCardano. | | **[Scalus](https://scalus.org)** | JVM / Scala teams | Scala 3 for both on-chain and off-chain; works with the JVM and JavaScript. | | **[Pebble](https://github.com/HarmonicLabs/pebble)** | TypeScript-familiar teams | Strongly-typed, TypeScript-like syntax that compiles to UPLC. | | **[Marlowe](https://marlowe-lang.org)** | Financial contracts | A domain-specific language, intentionally **not** Turing-complete, guaranteeing termination; has a visual playground. | That table is the shortlist. Browse it alongside the rest of the on-chain toolchain in [Builder Tools](/tools/?tags=smart-contracts). ### A note on Plutus Tx (Plinth) Plutus Tx was the original framework: you write Haskell, annotate it, and a GHC plugin translates it to Plutus Core and then UPLC. Its strengths are real: the full Haskell type system, shared types between on-chain and off-chain code, and a path toward formal verification. Its costs are equally real: a steep learning curve (Haskell + blockchain + Template Haskell), long build times, cryptic errors, and relatively large scripts. It remains important for projects deeply embedded in the Haskell ecosystem; for teams not already there, Aiken avoids those costs. ## What you pay for: execution costs On-chain execution is metered in **ExUnits (Execution Units)**, across two dimensions: - **CPU steps**: the number of computational steps the script performs. Each built-in has a defined cost; integer addition is cheap, cryptographic hashing is expensive. - **Memory units**: the peak memory the script uses during evaluation. Every script declares its budget up front, and there are per-transaction and per-block limits (protocol parameters that can change through governance). Two implications for your language choice: 1. **Feasibility**: a validator that exceeds the per-transaction limit simply can't be used; you must optimize or restructure. 2. **Cost**: higher ExUnits mean higher fees for your users, and a transaction that eats more of the per-block budget leaves room for fewer others. This is the concrete reason "smaller, faster scripts" matters, and why Aiken's efficient output is a practical advantage. For tuning your own validator, see [Optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization); to compare how different compilers' UPLC output actually performs on shared benchmarks, see [UPLC-CAPE](https://github.com/IntersectMBO/UPLC-CAPE), an IntersectMBO framework that measures CPU units, memory units, and script size across compilers and publishes [live reports](https://intersectmbo.github.io/UPLC-CAPE/). ## Blueprints: the contract's interface Whatever language you choose, the compiled output is described by a **[CIP-57](https://cips.cardano.org/cip/CIP-57) Plutus Blueprint**: a machine-readable JSON document listing the validators, their datum/redeemer schemas, the type definitions, and the compiled code. Think of it as the ABI for a Cardano contract. Blueprints are what let your off-chain code interact with a contract without reading its source: tools can generate TypeScript, Python, or Rust types directly from the blueprint, and different off-chain frameworks can all consume the same format. Aiken generates a blueprint automatically as part of its build. To turn one into type-safe off-chain code, see [Write a validator › from validator to blueprint](/docs/developers/curriculum/smart-contracts/write-a-validator#from-validator-to-blueprint); to read one field by field yourself, see [Reading a blueprint by hand](/docs/developers/curriculum/smart-contracts/write-a-validator#reading-a-blueprint-by-hand). ## Next steps - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): write the off-chain transactions that interact with your validator - [Testing](/docs/developers/curriculum/smart-contracts/testing): test Aiken validators with mock transactions - [Contract library](/templates/contracts): real validators to read and learn from --- ## Datum, Redeemer, and ScriptContext Every Cardano validator receives exactly three arguments: the **datum** (state locked at a script address), the **redeemer** (action submitted by the spender), and the **ScriptContext** (a complete snapshot of the transaction being validated). Together these three give a validator everything it needs to decide whether a UTXO can be spent. If the [overview](/docs/developers/curriculum/smart-contracts/overview) gave you the mental model (validators validate, they don't act), this page is the data model that makes it work: each argument in turn, how datums are stored on-chain, what reference scripts buy you, and the design patterns that fall out of this three-argument architecture. If you build web back-ends, the moving parts map cleanly onto things you already know: - **Datum is a database row.** It holds structured state for a specific record (the UTXO). Updating state is like DELETE-then-INSERT (consume the old UTXO, create a new one). The validator is the constraint or trigger that checks the update is valid. - **Redeemer is an API request body.** Like the JSON body of a POST/PUT, it says what action the client wants and carries the data to do it. `{ "action": "bid", "amount": 500 }` is exactly a `Bid { amount: 500 }` redeemer. - **ScriptContext is the request context / middleware.** Like the full HTTP request available to Express or Django middleware: headers (signatures), body (redeemer, inputs), the response being built (outputs), auth (signatories), timing (validity interval). A validator can inspect any aspect of the transaction to decide. - **Inline datums are embedded documents (MongoDB).** Moving from datum hashes to inline datums is like moving from a foreign key to embedding the full document. The data is right there, self-contained. - **Reference scripts are a shared library on a CDN.** Instead of every transaction bundling the validator, they all reference the same on-chain copy: smaller payloads, single-source updates. - **State machines are workflow engines.** Each state (datum) has valid transitions (redeemers), and the engine (validator) enforces the rules, like AWS Step Functions or a Redux reducer. ## What are the three arguments? When a transaction tries to spend a UTXO sitting at a script address, the node invokes the validator with three arguments and reads back a single boolean: ```text validator(datum, redeemer, scriptContext) -> Bool ``` ```mermaid flowchart LR D["Datum\n(State)"] --> V["Validator\nScript"] R["Redeemer\n(Action)"] --> V SC["ScriptContext\n(Environment)"] --> V V -->|"True"| ALLOW["Spend allowed"] V -->|"False"| DENY["Transaction rejected"] ``` 1. **Datum**: data associated with the UTXO being spent. It is the "state" locked at the script address. 2. **Redeemer**: data supplied by whoever is trying to spend the UTXO. It is the "action" they want to take. 3. **ScriptContext**: a comprehensive snapshot of the entire transaction: all inputs, outputs, signatures, minting, and more. It is the "environment" the validation happens in. Let's take each in turn. ## How does the datum represent state? The datum is structured data attached to a UTXO when it is created, encoding whatever the validator needs to know about that specific UTXO. Because Cardano has **no persistent contract storage**, state lives in datums attached to UTXOs; "updating" state means consuming the old UTXO and creating a new one with an updated datum. ### What can a datum contain? A datum can be any structured data that serializes to Cardano's on-chain format (`PlutusData`). Common examples: - **Ownership information**: a public key hash identifying who may claim the UTXO. - **Deadlines**: a POSIX timestamp or slot after which certain actions are allowed or prohibited. - **State values**: counters, balances, configuration parameters, or any application-specific state. - **Hashes or identifiers**: references to off-chain data, other UTXOs, or policy IDs. ```text -- Example: Escrow datum EscrowDatum { beneficiary: PubKeyHash, -- who can claim deadline: POSIXTime, -- when the deadline expires refund_address: PubKeyHash -- who gets a refund after deadline } -- Example: Auction datum AuctionDatum { seller: PubKeyHash, highest_bid: Integer, highest_bidder: PubKeyHash, lot_asset: AssetClass, min_bid_increment: Integer, auction_end: POSIXTime } ``` ### The continuing-output pattern In account-based systems like Ethereum, contract state lives in persistent storage variables. On Cardano there is no persistent storage. Instead, **state is encoded in datums attached to UTXOs**. When a validator wants to "update" its state, the transaction consumes the old UTXO (with the old datum) and creates a new UTXO at the same script address (with an updated datum). The validator checks that the state transition is legal. ```mermaid flowchart LR OLD["UTXO at script address\nDatum: counter = 5"] -->|"Transaction with\nredeemer: Increment"| TX["Transaction"] TX --> NEW["New UTXO at script address\nDatum: counter = 6"] TX --> VAL{{"Validator checks:\nnew_counter == old_counter + 1"}} ``` This pattern, consume a UTXO and recreate it with updated state, is the fundamental mechanism for state management on Cardano. It is called the **continuing-output pattern**, because the script address continues to hold a UTXO, just with new data. ### Datum hash vs inline datum Cardano historically stored datums in two ways, and the evolution matters: **Datum hash (pre-Vasil)**: the UTXO only contained a *hash* of the datum. The actual datum data had to be supplied separately in the transaction that created or spent the UTXO. This meant: - To spend a UTXO, you needed to know the full datum, not just its hash. - The datum had to be included in the spending transaction, increasing its size and fees. - If you lost track of the datum, the UTXO became effectively unspendable, funds locked forever. **Inline datum (post-Vasil, [CIP-32](https://cips.cardano.org/cip/CIP-32))**: datums can be stored directly ("inline") in the UTXO. This means: - Anyone can read the datum by inspecting the UTXO on-chain. - The spending transaction does not need to include the datum separately. - There is no risk of losing the datum data. - Other transactions can read this datum via reference inputs ([CIP-31](https://cips.cardano.org/cip/CIP-31)). ```text Pre-Vasil UTXO: Post-Vasil UTXO: +---------------------+ +---------------------+ | Address | | Address | | Value | | Value | | Datum Hash: 0xabc.. | | Inline Datum: | +---------------------+ | { counter: 5, | | owner: 0x123 } | Full datum must be +---------------------+ stored and provided separately Datum is right there, readable by anyone ``` :::tip Best practice Use inline datums for virtually all new development. Datum hashes still work for backward compatibility, but inline datums are superior in almost every scenario. ::: ## How does the redeemer represent actions? The redeemer is data provided by the transaction attempting to spend a UTXO, telling the validator what action the spender wants to perform. Its structure is entirely defined by the validator (the protocol imposes no requirements) and it commonly takes the form of tagged action constructors so a single validator can support multiple distinct operations. ### What can a redeemer contain? Any `PlutusData` value. Common patterns: **Simple values**, a password, a secret, a number: ```text Redeemer = ByteString -- the secret that hashes to the datum ``` **Action tags**, an enumeration of which action the spender wants: ```text Redeemer = | Bid { amount: Integer } | Close | Cancel | Update { new_price: Integer } ``` **Proof data**, evidence that the spender is authorized: ```text Redeemer = MerkleProof { leaf_index: Integer, proof_hashes: List } ``` ### Multi-action validators The redeemer tells the validator *what kind of operation* is being attempted, which lets a single validator support many operations. The validator pattern-matches on the redeemer to decide which rules to apply: ```text validator multi_action(datum: State, redeemer: Action, ctx: ScriptContext) -> Bool { when redeemer is { Bid { amount } -> validate_bid(datum, amount, ctx) -- bid higher than current, signed by bidder Close -> validate_close(datum, ctx) -- auction ended; winner gets lot, seller gets payment Cancel -> validate_cancel(datum, ctx) -- only seller, only before any bids } } ``` This pattern is ubiquitous. Almost every non-trivial validator uses a redeemer with multiple constructors to represent different operations. :::note Keep redeemers small The redeemer is included in the transaction body, so its size affects the fee. If your redeemer carries a large Merkle proof or other bulky data, that increases cost. ::: ## How on-chain data is encoded (Plutus Data) Datums, redeemers, and script parameters are all the same thing under the hood: **Plutus Data**, Cardano's on-chain data format. Everything reduces to five types: | Type | Represents | Used for | |---|---|---| | **Integer** | `bigint` | amounts, indices, timestamps, deadlines | | **ByteArray** | bytes | hashes, addresses, policy IDs, asset names | | **Constructor** | a tag (`index`) + ordered fields | variants / tagged unions, the shape of most datums and redeemers | | **Map** | key → value pairs | metadata, key-value state | | **List** | ordered values | arrays | A **Constructor** is the workhorse: index `0` with fields models a record (a vesting datum is "constr 0 with `[beneficiary, deadline]`"); different indices model an enum (a redeemer that's `Claim` = constr 0, `Cancel` = constr 1): ```typescript // A vesting datum: constructor 0 with { beneficiary, deadline } const datum = Data.constr(0n, [ Bytes.fromHex("abc1...23de"), // beneficiary key hash (ByteArray) 1735689600000n, // deadline (Integer) ]) // A redeemer enum: Claim / Cancel const claim = Data.constr(0n, []) const cancel = Data.constr(1n, []) ``` ```typescript // A vesting datum: constructor 0 with { beneficiary, deadline } const datum = mConStr0([ "abc1...23de", // beneficiary key hash (ByteString, hex) 1735689600000n, // deadline (Integer) ]) // A redeemer enum: Claim / Cancel (index picks the constructor) const claim = mConStr0([]) const cancel = mConStr1([]) ``` Writing raw `Data.constr` is error-prone for real contracts. Evolution's **`TSchema`** defines the shape once and gives a type-safe codec, so the off-chain types match the on-chain definitions in your validator: ```typescript const VestingDatum = TSchema.Struct({ beneficiary: TSchema.ByteArray, deadline: TSchema.Integer }) const Codec = Data.withSchema(VestingDatum) const datum = Codec.toData({ beneficiary: Bytes.fromHex("abc1...23de"), deadline: 1735689600000n }) // Codec.toCBORHex(...) / Codec.fromData(...) round-trip too ``` Mesh has no equivalent typed codec: you build the same datum with the raw `mConStr` constructors shown above, keeping field order and types aligned with your validator by hand. ### Sum-type redeemers (Claim / Cancel / Update) The multi-action validator above pattern-matches on a redeemer with several constructors. Off-chain you build that sum type the same way: a `TSchema.Variant` in Evolution, or the constructor-index shorthands in Mesh. Each variant maps to a constructor index, and the index must match the order in your validator's type. ```typescript // Claim = constr 0, Cancel = constr 1, Update = constr 2 const Redeemer = TSchema.Variant({ Claim: {}, Cancel: {}, Update: { new_beneficiary: TSchema.ByteArray, new_deadline: TSchema.Integer }, }) const Codec = Data.withSchema(Redeemer) const claim = Codec.toData({ Claim: {} }) const update = Codec.toData({ Update: { new_beneficiary: Bytes.fromHex("def4...56ab"), new_deadline: 1735776000000n } }) ``` `TSchema` also gives you `Map`, `Array`, `Tuple`, and `NullOr`/`UndefinedOr` for the rest of a datum's shape. For the structures you reach for constantly, the `@evolution-sdk/evolution/plutus` barrel ships pre-built, validator-matching schemas: `Address`, `Credential`, `Value`, `OutputReference`, and `CIP68Metadata`. Import and compose them rather than hand-rolling the encoding. ```typescript // The constructor index picks the action (must match the validator's enum order) const claim = mConStr0([]) // Claim const cancel = mConStr1([]) // Cancel const update = mConStr2(["def4...56ab", 1735776000000n]) // Update(new_beneficiary, new_deadline) ``` ### Datums and redeemers that carry a Value When a datum or redeemer holds a multi-asset value (a DEX order, locked escrow funds), both SDKs give you value helpers so you don't assemble nested asset maps by hand. In Mesh, `MeshValue` does the arithmetic: ```typescript // Build, add, and merge values const offered = MeshValue.fromAssets([{ unit: "lovelace", quantity: "5000000" }]) offered.addAsset({ unit: "policyId...assetName", quantity: "100" }) const required = MeshValue.fromAssets([{ unit: "lovelace", quantity: "3000000" }]) offered.geq(required) // true: covers what the swap needs offered.merge(required) // combine two values const datumValue = offered.toData() // → Mesh Data (nested Maps) for the datum field ``` Evolution's equivalent is the `Value` schema from `@evolution-sdk/evolution/plutus`, a `Map` of policy → asset name → quantity that you drop straight into a `TSchema.Struct`. ### Serialization round-trip Every Plutus Data value serializes to **CBOR**, the binary format the ledger stores. When you need the raw hex (a `cardano-cli` datum file, a value read back from the chain), round-trip through these: ```typescript const hex = Data.toCBORHex(datum) // PlutusData → CBOR hex const back = Data.fromCBORHex(hex) // CBOR hex → PlutusData const bytes = Bytes.fromHex(hex) // hex string ↔ Uint8Array const asHex = Bytes.toHex(bytes) Address.fromBech32("addr1...") // bech32 ↔ Address (addr_test1.../addr1...) Address.toBech32(addr) ``` ```typescript // CBOR hex (read off-chain or from a UTXO) → { constructor, fields } const datum = deserializeDatum(cborHex) // On the encode side, the tx builder serializes a JSON/Mesh datum for you: // txBuilder.txOutInlineDatumValue(mConStr0([...]), "Mesh") ``` The [CIP-57 blueprint](/docs/developers/curriculum/smart-contracts/write-a-validator#from-validator-to-blueprint) your validator compiles to describes these schemas so tools can generate the codecs for you. ## What does the ScriptContext provide? The ScriptContext is the richest of the three arguments: a comprehensive data structure the node hands the validator, describing the whole transaction. It contains a `TxInfo` (all inputs, outputs, signatures, minting, fees, validity range, and more) plus a `ScriptPurpose` indicating *why* the validator is running. ### The transaction, as the validator sees it These are the properties a validator can inspect through the context. This is a representation of the transaction *as seen by on-chain scripts*, not a 1:1 copy of the ledger transaction. | Property | Description | | --- | --- | | **inputs** | The transaction inputs being spent. Every transaction produces outputs, which become inputs for future transactions. | | **reference_inputs** | Inputs used for reading only, not spent. | | **outputs** | The new UTXOs created by the transaction. | | **fee** | Transaction fee in lovelace. Predictable, and depends on transaction size. | | **mint** | The value of tokens being minted or burned. | | **certificates** | Certificates for delegation, pool operations, governance roles, etc. | | **withdrawals** | Stake reward withdrawals as credential-lovelace pairs. | | **validity_range** | The time range in which the transaction is valid. | | **signatories** | Hashes representing who signed the transaction. | | **redeemers** | Script-purpose and redeemer pairs for the scripts executed in the transaction. | | **datums** | Map from datum hashes to datum data. | | **id** | The transaction hash, unique per transaction. | | **votes** / **proposal_procedures** | Governance votes and proposals (Conway era). | :::note Transaction context representation The underlying ledger uses a different structure with numeric field keys, defined in the [Conway CDDL specification](https://github.com/IntersectMBO/cardano-ledger/blob/master/eras/conway/impl/cddl/data/conway.cddl). In particular, on-chain scripts can't see inputs locked by bootstrap addresses, outputs to bootstrap addresses, or transaction metadata. ::: ### The ScriptPurpose The `ScriptPurpose` tells the validator *why* it is being invoked: ```text ScriptPurpose = | Spending TxOutRef -- spending a UTXO at a script address | Minting PolicyId -- minting/burning tokens under this policy | Certifying DCert -- issuing a stake certificate | Rewarding StakeCred -- withdrawing staking rewards | Voting Voter -- governance voting (Plutus V3) | Proposing -- governance proposals (Plutus V3) ``` ### What validators typically check The ScriptContext is where most of the interesting logic happens. The most common checks: **Signature verification**: "Is the transaction signed by the expected key?" ```text list.has(ctx.transaction.signatories, datum.owner) ``` **Output inspection**: "Does the transaction create the correct outputs?" ```text expect Some(output) = find_output_to(ctx.transaction.outputs, beneficiary_address) output.value >= expected_amount ``` **Time-range checking**: "Is the transaction within the allowed window?" ```text valid_range_start(ctx.transaction.valid_range) > datum.deadline ``` **Minting inspection**: "Are the correct tokens being minted?" ```text quantity_of(ctx.transaction.mint, own_policy_id, token_name) == 1 ``` **Input counting**: "Are the right UTXOs being consumed or referenced?" ```text list.any(ctx.transaction.reference_inputs, fn(input) { input.output.address == oracle_address }) ``` ### Why the ScriptContext is so powerful The ScriptContext is what makes Cardano validators expressive despite being "just" boolean functions. A validator can enforce conditions about the *entire transaction*, not only the single UTXO it guards. That enables patterns impossible if a validator could see only its own input: - **Multi-validator coordination**: two validators in the same transaction can each check conditions the other enforces, cooperating without direct communication. - **Atomic swaps**: a validator can verify that a specific output exists in the transaction, enabling trustless exchange in a single transaction. - **Forwarder patterns**: a validator can delegate its decision to another by checking that another script input is present. ## Common design patterns Several patterns emerge from the datum-redeemer-context architecture. (The [Design Patterns](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview) reference covers production-grade implementations.) ### State machine Encode a finite set of states in the datum and a set of transitions in the redeemer. The validator checks each transition is valid given the current state. ```text Datum (State): Redeemer (Transition): | Collecting | Contribute { amount } | Funded | Dispute | Disputed | Resolve { ruling } | Completed | Complete Validator checks: Collecting + Contribute -> is amount sufficient? -> Collecting or Funded Funded + Dispute -> is disputer authorized? -> Disputed Disputed + Resolve -> is resolver the arbiter? -> Completed ``` The transaction consumes the UTXO with the old state and creates a new UTXO with the new state; the validator verifies the transition is legal. ### Multi-validator (validator linking) Complex applications use multiple validators that cooperate within a single transaction. A DEX might have a liquidity-pool validator, an LP-token minting policy, and an order validator. All three run in one transaction. They do not call each other; they independently verify conditions about the same transaction through ScriptContext. ```text Single transaction: Inputs: Pool UTXO (pool validator), Order UTXO (order validator) Mint: LP tokens (minting policy) Outputs: Updated pool UTXO, LP tokens to provider, swapped tokens to trader Each validator checks its own rules against ScriptContext: Pool validator: "Are reserves correctly updated? Are LP tokens minted?" Order validator: "Is the swap executed at the correct price?" Minting policy: "Is the pool UTXO consumed? Is the amount correct?" ``` ### One-shot pattern Use a specific UTXO as input to guarantee uniqueness. Since each UTXO can be spent only once, a validator or minting policy that requires a specific UTXO can only ever succeed once. Common uses: minting a unique NFT, or ensuring a contract's initial state can only be created once. ### Withdraw-zero trick A spending validator delegates its logic to a staking validator by requiring a zero-ADA withdrawal from a staking script. The staking validator runs **once** for the whole transaction, while a spending validator runs **once per input**, so this is more efficient when a transaction spends many UTXOs from the same script. See [Stake Validator](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) for the full pattern. ### Beacon / pointer token A beacon token is a unique native token held at a script address alongside a datum. It acts as a pointer that makes the UTXO easy to find: query the chain for the token, and you immediately locate the right UTXO among potentially thousands at the same address. ### On-chain configuration Compose a beacon token with a reference input and you get the pattern most production protocols reach for first: a single **config UTXO** whose datum holds the protocol's mutable settings, a swap fee, an admin key, a treasury address. A unique [state NFT](/docs/developers/curriculum/smart-contracts/write-a-validator#one-shot-policies) marks the canonical UTXO so a look-alike cannot be substituted (the failure mode is [missing UTXO authentication](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/missing-utxo-authentication)), the whole protocol reads it as a [reference input](/docs/developers/curriculum/fundamentals/core-concepts/transactions#reference-inputs-and-reference-scripts) so any number of transactions read the same settings in parallel without consuming them, and an admin credential in the datum gates updates: an admin key checked as a required signer, or any other credential by requiring it to authorize the transaction. Production settings UTXOs usually go one step further and split authority into roles, an operations admin, a treasury admin, each with its own field allowlist: the validator reconstructs the expected output datum from the input datum, pinning every field the acting role may not touch, and compares wholesale, so a stray change to a protected field fails the equality check without per-field logic. Updating is an ordinary [continuing-output](#the-continuing-output-pattern) spend, consume the config UTXO and recreate it with new values, and the mint and spend rules usually live in [one validator that shares a single hash](/docs/developers/curriculum/smart-contracts/write-a-validator#one-validator-many-purposes-one-hash). The one-time transaction that first mints the NFT into the initial config is often called bootstrapping. This is protocol-level configuration, distinct from the network's own protocol parameters. ### Authorization methods The config pattern gates updates on an admin credential; generalize that and you get a vocabulary worth a datum field of its own: store *how* a party authorizes, not just who they are. Four checks cover almost everything a validator will meet: a **signature** (a key hash present in `extra_signatories`), a **spend script** (an input from that script address is being spent), a **withdraw script** (a withdrawal by that script credential is present, the withdraw-zero shape above), or a **mint script** (the transaction mints under that policy). Encoded as a small enum in the datum, the same ownership field lets a position belong to a wallet today and a script tomorrow, which is what makes protocol positions transferable to multisigs and other contracts rather than only to people. ## How reference scripts (CIP-33) reduce costs Reference scripts let a compiled validator be stored once in a UTXO and referenced by all future transactions that need it, instead of including the full script bytes every time. This shrinks transactions (lowering fees), relaxes the practical limit on validator size, and turns deployment into a one-time cost shared by all users. ```text Step 1: Store the script in a UTXO Transaction creates: UTXO_Script at some_address Value: min ADA Reference Script: [compiled validator bytecode] Step 2: Use the script via reference Transaction spends from script_address: Reference Input: UTXO_Script (not consumed, just referenced) Input: UTXO at script_address (being spent) Redeemer: { action data } ``` The reference-script UTXO must remain unspent for as long as you want it available; if it is consumed, transactions referencing it will fail. For the SDK mechanics, see [Reference scripts](/docs/developers/curriculum/smart-contracts/lock-and-spend#reference-scripts). ## Putting it together: a vesting example Consider a simple vesting contract: Alice locks 1000 ADA for Bob, who can claim it after a specific date. **1. Lock funds.** Alice creates an output at the vesting script address: ```text Output: Address: vesting_script_address Value: 1000 ADA Inline Datum: { beneficiary: Bob's PubKeyHash, deadline: 1735689600 (January 1, 2025, as POSIX time) } ``` **2. Bob claims (after the deadline).** Bob builds a spending transaction: ```text Input: the UTXO Alice created Redeemer: Claim Output: 999.8 ADA to Bob's wallet Validity interval: invalid_before = slot for Jan 2, 2025 Signatures: Bob's signature ``` **3. The validator executes** with `datum = { beneficiary, deadline }`, `redeemer = Claim`, and a ScriptContext showing the signatories, validity range, and outputs. It checks: 1. Is the transaction signed by `datum.beneficiary` (Bob)? Check `signatories`. 2. Has the deadline passed? Check that `datum.deadline` is before the start of `valid_range`. Both hold, so the validator returns `True` and the transaction is included. If Eve tries to claim early with a validity interval starting before the deadline, the time check returns `False`, the transaction is rejected, and Eve pays nothing. It never made it on-chain. You can build exactly this flow with an SDK on the [Lock and Spend](/docs/developers/curriculum/smart-contracts/lock-and-spend) page. ## Key takeaways - **Datum is state**: the data locked at a script address. Use inline datums for all new development. - **Redeemer is action**: what the spender wants to do, usually tagged constructors for multiple operations in one validator. - **ScriptContext is environment**: a complete view of the transaction, letting validators enforce rules about inputs, outputs, signatures, time, and minting. - **State is managed by consuming and recreating UTXOs** (the continuing-output pattern), not by mutating storage. - **Reference scripts and inline datums** cut transaction sizes, lower fees, and simplify off-chain code. ## Next steps - [Choose a language](/docs/developers/curriculum/smart-contracts/choose-a-language) to write your validator - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): build the off-chain transactions with an SDK - [Design Patterns](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview): production-grade implementations of the patterns above --- ## Lock and Spend Every smart contract interaction has two halves: you **lock** funds at a script address (sending value with a datum attached), and later you **spend** them (consuming the UTXO by providing a redeemer the validator accepts). This is the off-chain work. Your validator just says yes or no; this page is how you build the transactions it judges. Pick your tool below. The SDK tabs use the same Evolution and Mesh setup as [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction). ## Before you start - A **compiled validator** and its [blueprint](/docs/developers/curriculum/smart-contracts/choose-a-language#blueprints-the-contracts-interface) (`plutus.json`). If you don't have one yet, [choose a language](/docs/developers/curriculum/smart-contracts/choose-a-language) and write one. - A **tool + provider key**: the tabs use Blockfrost on Preprod ([choose your tools](/docs/developers/curriculum/start-building/choose-your-tools)). - **Test ADA** in a wallet you control ([faucet](/docs/developers/curriculum/start-building/networks-and-test-ada#get-test-ada)). - Background, if you want it: [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context) explains what the datum and redeemer below actually are. ## Lock funds Locking means sending ADA (and optionally native tokens) to the script address with a **datum** attached. The datum is the state your validator will check when someone later tries to spend the UTXO. ```typescript const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) const scriptAddress = Address.fromBech32("addr_test1...") // your script's address const tx = await client .newTx() .payToAddress({ address: scriptAddress, assets: Assets.fromLovelace(10_000_000n), // 10 ADA datum: new InlineDatum.InlineDatum({ data: Data.constr(0n, []) }) }) .build() const signed = await tx.sign() const txHash = await signed.submit() console.log("Locked funds at:", txHash) ``` For real contracts, define the datum with `TSchema` for type safety instead of a raw `Data.constr`. See [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context). ```typescript const { address: scriptAddress } = serializePlutusScript(script); // script: PlutusScript const utxos = await wallet.getUtxosMesh(); const changeAddress = await wallet.getChangeAddressBech32(); const txBuilder = new MeshTxBuilder({ fetcher: provider }); const unsignedTx = await txBuilder .txOut(scriptAddress, [{ unit: "lovelace", quantity: "10000000" }]) // 10 ADA .txOutInlineDatumValue("meshsecretcode") // the datum .changeAddress(changeAddress) .selectUtxosFrom(utxos) .complete(); const signedTx = await wallet.signTx(unsignedTx); const txHash = await wallet.submitTx(signedTx); ``` Use `.txOutDatumHashValue(data)` instead of `.txOutInlineDatumValue(data)` if you need a datum hash rather than an inline datum. See the [Mesh smart contracts guide](https://meshjs.dev/apis/txbuilder/smart-contracts). With `cardano-cli` you build the datum as JSON, then send a normal transaction to the script address that attaches the datum (by hash or inline): ```bash # Build to the script address, attaching an inline datum cardano-cli latest transaction build \ --tx-in # \ --tx-out "$(< script.addr)+10000000" \ --tx-out-inline-datum-file datum.json \ --change-address "$(< payment.addr)" \ --out-file tx.raw cardano-cli latest transaction sign \ --tx-body-file tx.raw --signing-key-file payment.skey --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` Derive the script address from the compiled validator with `cardano-cli address build --payment-script-file validator.plutus --out-file script.addr`; the datum file is JSON like `{"constructor":0,"fields":[]}`. ## Spend funds Spending means consuming a UTXO locked at the script address by providing a **redeemer**: the data your validator checks to authorize the spend. Because a Plutus script runs, the transaction also needs **collateral** (see [Collateral](#collateral) below). The SDKs select it for you. ```typescript // reuse the client from the lock step declare const scriptUtxos: UTxO.UTxO[] // the UTxO(s) you locked, queried back declare const validatorScript: any // your compiled validator const tx = await client .newTx() .collectFrom({ inputs: scriptUtxos, redeemer: Data.constr(0n, []) // the action your validator expects }) .attachScript({ script: validatorScript }) .addSigner({ keyHash: myKeyHash }) // if the validator checks a signature .build() const signed = await tx.sign() const txHash = await signed.submit() ``` Evolution handles script evaluation, redeemer indexing, and collateral automatically. For time-locked validators, add `.setValidity({ from, to })` so the script can check the current time. See [redeemer indexing](/docs/developers/curriculum/start-building/transaction-building#redeemer-indexing) for the static, self, and batch redeemer modes. ```typescript const collateral = await wallet.getCollateralMesh(); const changeAddress = await wallet.getChangeAddressBech32(); const txBuilder = new MeshTxBuilder({ fetcher: provider }); const unsignedTx = await txBuilder .spendingPlutusScriptV3() // match your script's Plutus version .txIn(assetUtxo.input.txHash, assetUtxo.input.outputIndex) .txInInlineDatumPresent() // datum is inline on the UTxO .txInRedeemerValue(mConStr0([])) // the redeemer .txInScript(scriptCbor) // or .spendingTxInReference(...) for a reference script .txInCollateral( collateral[0].input.txHash, collateral[0].input.outputIndex, ) .changeAddress(changeAddress) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete(); const signedTx = await wallet.signTx(unsignedTx); const txHash = await wallet.submitTx(signedTx); ``` To spend, Mesh needs three things beyond `.txIn()`: the **script** (supplied with `.txInScript()` or referenced with `.spendingTxInReference()`), the **datum** (`.txInInlineDatumPresent()` or `.txInDatumValue()`), and the **redeemer** (`.txInRedeemerValue()`). See the [Mesh smart contracts guide](https://meshjs.dev/apis/txbuilder/smart-contracts). Spending a Plutus UTXO requires the script, the datum, the redeemer, and a collateral input: ```bash cardano-cli latest transaction build \ --tx-in # \ --tx-in-script-file validator.plutus \ --tx-in-inline-datum-present \ --tx-in-redeemer-file redeemer.json \ --tx-in-collateral # \ --change-address "$(< payment.addr)" \ --out-file tx.raw ``` Then `sign` and `submit` as usual. Pass the wrong redeemer and `build` fails up front with the script's own error message, a quick way to sanity-check validator logic before submitting. ### Collateral A transaction that runs a Plutus script is validated in two phases: phase 1 checks structure (inputs exist, signatures, balancing), and phase 2 runs the scripts. **Collateral** is a set of ADA-only UTXOs the node consumes only if a script fails phase 2. A transaction that succeeds never loses its collateral, so honest users are safe, while flooding the network with failing scripts becomes expensive. The SDKs pick collateral automatically from your wallet's ADA-only UTXOs; with cardano-cli you mark it explicitly with `--tx-in-collateral`. Keep a few pure-ADA UTXOs around for this. With [CIP-40](https://cips.cardano.org/cip/CIP-40) any excess is returned to a collateral-change address. ## Reference scripts Including a multi-kilobyte validator in every spend transaction is wasteful. A **reference script** (Plutus V2+) stores the script once in a UTXO; later transactions point at that UTXO with `readFrom` instead of attaching the script: much smaller transactions and lower fees. ```typescript // 1. Deploy: park the script in a UTXO (the `script` field makes it a reference script) const deploy = await client .newTx() .payToAddress({ address: await client.address(), assets: Assets.fromLovelace(10_000_000n), script: validatorScript }) .build() await (await deploy.sign()).submit() // 2. Spend by referencing it: no attachScript, the node reads the script from the referenced UTXO declare const scriptUtxos: UTxO.UTxO[] declare const referenceScriptUtxo: UTxO.UTxO const spend = await client .newTx() .collectFrom({ inputs: scriptUtxos, redeemer: Data.constr(0n, []) }) .readFrom({ referenceInputs: [referenceScriptUtxo] }) .build() ``` ```typescript // 1. Deploy: park the script in a UTXO with .txOutReferenceScript const deployTx = await new MeshTxBuilder({ fetcher: provider }) .txOut(await wallet.getChangeAddressBech32(), [{ unit: "lovelace", quantity: "10000000" }]) .txOutReferenceScript(scriptCbor, "V3") // makes it a reference script .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() const deployTxHash = await wallet.submitTx(await wallet.signTx(deployTx)) // 2. Spend by referencing it: .spendingTxInReference instead of .txInScript const collateral = await wallet.getCollateralMesh() const spendTx = await new MeshTxBuilder({ fetcher: provider }) .spendingPlutusScriptV3() .txIn(scriptUtxo.input.txHash, scriptUtxo.input.outputIndex) .txInInlineDatumPresent() .txInRedeemerValue(mConStr0([])) .spendingTxInReference(deployTxHash, 0) // node reads the script from the deployed UTXO .txInCollateral(collateral[0].input.txHash, collateral[0].input.outputIndex) .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() await wallet.submitTx(await wallet.signTx(spendTx)) ``` `readFrom` also reads a UTXO **without consuming it**, the same mechanism oracles use to expose price data and contracts use to read shared configuration (a reference input can carry a datum, not just a script). Reach for a reference script once a script is used across more than a few transactions; the one-time deploy cost pays for itself quickly. ## Parameterized scripts A parameterized validator leaves values like an owner key or a deadline as compile-time holes, so one validator serves many deployments: each set of parameters produces a distinct script (and address). Apply the parameters off-chain before use: ```typescript declare const compiledScript: string // the parameterized script from `aiken build` // Raw data params, applied in the order of the script's lambda bindings const applied = UPLC.applyParamsToScript(compiledScript, [ Data.bytearray("abc123def456abc123def456abc123def456abc123def456abc123de"), // owner Data.int(1735689600000n), // deadline ]) // Or type-safe via a schema const Params = Data.withSchema(TSchema.Struct({ owner: TSchema.ByteArray, deadline: TSchema.Integer })) const appliedTyped = UPLC.applyParamsToScriptWithSchema( compiledScript, [Params.toData({ owner: Bytes.fromHex("abc1...23de"), deadline: 1735689600000n })], (v) => v, ) ``` ```typescript declare const compiledScript: string // the parameterized script from `aiken build` // Apply params in the order of the script's lambda bindings (raw Mesh data values) const applied = applyParamsToScript(compiledScript, [ "abc123def456abc123def456abc123def456abc123def456abc123de", // owner (ByteString, hex) 1735689600000n, // deadline (Integer) ]) // Address of the parameterized script const { address: scriptAddress } = serializePlutusScript({ code: applied, version: "V3" }) ``` Mesh has no typed-schema equivalent to Evolution's `TSchema`: parameters are raw values applied in binding order, so you ensure their types and order match the script yourself. The applied script is what you attach (or deploy as a reference script). Use parameters for per-deployment config (owner, deadline, token policy, oracle address); use **datum fields** instead for state that changes per transaction. `applyParamsToScript` defaults to Aiken-compatible CBOR: pass `CBOR.CML_DATA_DEFAULT_OPTIONS` for CML-compiled scripts. ## A complete example: vesting The lock-then-spend shape above becomes a real contract when the datum carries meaningful state and the validator enforces a rule. A **vesting** contract is the canonical first example: lock funds with a `{ beneficiary, deadline }` datum, and the validator allows the spend only when the transaction is signed by the beneficiary and its validity interval starts after the deadline. The on-chain validator (logic and tests) is walked through in [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context#putting-it-together-a-vesting-example); here is the off-chain flow end to end. It is two transactions: **lock** the funds with the datum, then **claim** them after the deadline. The claim is the interesting half: a validator cannot read the wall clock, so you set the transaction's validity interval to start *after* the deadline, and the ledger's guarantee that the transaction really was in that window is what proves to the validator that the deadline has passed. ```typescript // reuse the client from the lock step; vestingScript is your compiled validator declare const vestingScript: any const VestingDatum = TSchema.Struct({ beneficiary: TSchema.ByteArray, deadline: TSchema.Integer }) const Codec = Data.withSchema(VestingDatum) const scriptAddress = Address.fromBech32("addr_test1w...") // the vesting script's address const beneficiary = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de") // key hash, 28 bytes const deadline = BigInt(new Date("2025-12-31T23:59:59Z").getTime()) // POSIX time, ms // 1. LOCK: send 50 ADA with the { beneficiary, deadline } datum const lock = await client .newTx() .payToAddress({ address: scriptAddress, assets: Assets.fromLovelace(50_000_000n), datum: new InlineDatum.InlineDatum({ data: Codec.toData({ beneficiary, deadline }) }), }) .build() await (await lock.sign()).submit() // 2. CLAIM (after the deadline): beneficiary signs, validity starts past the deadline declare const vestingUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress) const now = BigInt(Date.now()) // must be > deadline const claim = await client .newTx() .collectFrom({ inputs: vestingUtxos, redeemer: Data.constr(0n, []) }) // Claim .attachScript({ script: vestingScript }) .addSigner({ keyHash: new KeyHash.KeyHash({ hash: beneficiary }) }) .setValidity({ from: now, to: now + 300_000n }) // proves the deadline has passed .build() await (await claim.sign()).submit() ``` ```typescript // vestingScriptCbor is your compiled validator; beneficiaryHash is the 28-byte key hash (hex) const { address: scriptAddress } = serializePlutusScript({ code: vestingScriptCbor, version: "V3" }) const deadline = new Date("2025-12-31T23:59:59Z").getTime() // POSIX time, ms // 1. LOCK: send 50 ADA with the { beneficiary, deadline } datum const lock = await new MeshTxBuilder({ fetcher: provider }) .txOut(scriptAddress, [{ unit: "lovelace", quantity: "50000000" }]) .txOutInlineDatumValue(mConStr0([beneficiaryHash, deadline])) .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() await wallet.submitTx(await wallet.signTx(lock)) // 2. CLAIM (after the deadline): beneficiary signs, validity starts past the deadline const collateral = await wallet.getCollateralMesh() const deadlineSlot = resolveSlotNo("preprod", deadline) // POSIX ms -> slot const claim = await new MeshTxBuilder({ fetcher: provider }) .spendingPlutusScriptV3() .txIn(vestingUtxo.input.txHash, vestingUtxo.input.outputIndex) .txInInlineDatumPresent() .txInRedeemerValue(mConStr0([])) // Claim .txInScript(vestingScriptCbor) .requiredSignerHash(beneficiaryHash) // beneficiary must sign .invalidBefore(deadlineSlot) // validity starts past the deadline .txInCollateral(collateral[0].input.txHash, collateral[0].input.outputIndex) .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() await wallet.submitTx(await wallet.signTx(claim)) ``` Submit the claim before the deadline and the ledger rejects it up front, so the funds stay locked until the time genuinely passes. Once a vesting validator is used more than a few times, deploy it once as a [reference script](#reference-scripts) so each claim transaction stays small. ## Next steps - [Testing](/docs/developers/curriculum/smart-contracts/testing): test the validator before you deploy it - [Security](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview): the vulnerabilities to guard against when spending logic gets real - [Contract library](/templates/contracts): escrow, marketplace, swap, and more - Reference: the [Mesh smart contracts guide](https://meshjs.dev/apis/txbuilder/smart-contracts) --- ## Smart Contracts You arrive able to build transactions, mint under policies you wrote, and delegate stake and votes. Until now most rules you enforced were the ledger's own; this module generalizes the policy idea into validator scripts that guard any UTXO with logic you define. ## What are smart contracts? Smart contracts are agreements defined in code that enforce their terms automatically, without intermediaries. On Cardano they work differently from account-based chains, and the key to understanding them is the [eUTXO](/docs/developers/curriculum/fundamentals/core-concepts/eutxo) model: a smart contract is a **validator script** that guards UTXOs locked at its address. You lock a UTXO at the script's address, and from then on it can only be spent by a transaction the script approves. ## Smart contracts are validators, not actors :::tip Mental model shift The most important shift when coming from other blockchains: **a smart contract cannot take actions**. It can only approve or reject a proposed transaction. ::: A Cardano validator cannot send tokens, call another contract imperatively, initiate anything on its own, make network requests, read external data directly, generate random numbers, or loop forever (execution budgets enforce termination). Instead it **validates**: users propose a transaction, and the validator approves or rejects it against the logic you wrote. These limits are features. They make validators **deterministic** (the same inputs always produce the same result), which is the foundation of Cardano's predictability. ## On-chain and off-chain A contract has two halves: - **On-chain (the validator)**: the immutable logic that runs on every node and approves or rejects spends from the contract address. It runs once per script input in a transaction. - **Off-chain**: the application that finds the locked UTXOs and builds transactions the validator will approve. It can be written in any language and handles the UI, data fetching, and transaction building. :::tip The lawyer and the judge Think of off-chain code as the **lawyer drafting a contract** and on-chain code as the **judge reviewing it**. The lawyer does the creative work of figuring out what the agreement should look like; the judge only checks whether it complies with the rules. This is why on-chain execution stays cheap, off-chain code can be in any language, and you can test the two halves independently. ::: Sending a UTXO to the script address initialises a contract instance. Anyone can send a UTXO there (with any datum, or none); the validator decides what can leave. ## The validator's inputs A validator is a function of three arguments: ```text title="Validator function signature" f(datum, redeemer, context) = success | failure ``` ```mermaid graph TB subgraph LOCKED[" "] UTXO["UTXO at Script AddressValue: 100 ADA"] DATUM["Datum(state data)"] end TX["Transactionwants to spend this UTXO"] TX -.->|"trying to spend"| UTXO TX -->|"provides"| REDEEMER["Redeemer(spending argument)"] SCRIPT["Validator Script asks:'Is this transaction allowedto spend this UTXO?'"] LOCKED --> SCRIPT REDEEMER --> SCRIPT TX -.->|"transaction details visible to script"| SCRIPT SCRIPT -->|"Yes ✓"| APPROVED["Validation succeedsUTXO is spent"] SCRIPT -->|"No ✗"| REJECTED["Validation failsUTXO remains locked"] style UTXO fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style DATUM fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style TX fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style REDEEMER fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style SCRIPT fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style APPROVED fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style REJECTED fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` - **Datum**: state attached to the locked UTXO, set when it is created (the "e" in eUTXO). - **Redeemer**: the argument the spender supplies to unlock it, naming the action they want. - **Context**: the transaction the validator is judging, its inputs, outputs, signatures, mint, and validity range, so the script can assert facts about the whole transaction, not just the one UTXO it guards. :::tip Deep dive This is the quick tour. The full reference, the complete transaction context, the `ScriptPurpose`, inline-vs-hash datums, and the patterns they enable, is **[Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context)**. ::: ## Script addresses and purposes A **script address** is derived from the hash of the validator, so the rules are bound to the address. UTXOs sent there can only be spent when the script approves the spending transaction. The hash includes a language tag (`0x01` PlutusV1, `0x02` PlutusV2, `0x03` PlutusV3), so identical code under different versions yields different addresses. :::caution Address collision The same contract code always produces the same address within the same Plutus version. If you deploy code someone else already deployed, you get the same address, and there may already be history there. ::: Unlike key-controlled addresses, a script address is governed by code: anyone can send funds to it, but only a transaction that satisfies the validator can spend them. A script also has a **purpose**, the kind of action it guards: | Script purpose | Validates | | --- | --- | | **Spend** | Consuming a UTXO. The most common, and the only purpose that receives a datum. | | **Mint** | Token creation and destruction (minting policies). | | **Publish** | Certificates: stake delegation, pool and DRep registration, committee changes. | | **Withdraw** | Stake reward withdrawals. | | **Vote** | Governance votes (Conway). | | **Propose** | Governance proposals (Conway). | | **Native** | The pre-Plutus scripting language for simple multisig and time-locks (all-of, any-of, before/after). | ## How scripts execute A transaction that includes scripts is validated in two phases: - **Phase 1** checks the transaction structure: inputs exist, signatures are valid, the transaction balances. - **Phase 2** runs the scripts. Each gets a budget of execution units (ExUnits), priced into the fee. Because phase-2 work is real, a script transaction also carries **collateral**: ADA-only UTXOs the node consumes only if a script fails phase 2. Honest transactions that succeed never lose it, while flooding the network with failing scripts becomes expensive. The full rules live in [collateral](/docs/developers/curriculum/fundamentals/core-concepts/fees#collateral); setting it in practice is covered in [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend#collateral), and the SDKs select it for you. ### Deterministic validation Validation depends only on the transaction and its context, never on live network state. That determinism lets you compute a transaction's outcome and its exact cost before you submit it, unlike chains where gas and ordering shift under load. It also means a validator cannot generate randomness; getting a verifiable random number despite that is its own topic, covered in [on-chain randomness](/docs/developers/curriculum/dapps/oracles/randomness). ## The contract lifecycle In practice a stateful contract moves through three steps, shown here with a counter that only increments: 1. **Write the validator**: it approves a spend only when the new datum is a valid transition from the old one (here `count + 1`) and the right party signed. 2. **Lock**: send a UTXO to the script address with the initial datum (`count: 0`). 3. **Unlock and update**: spend that UTXO with a redeemer (`increment`); the validator checks the transition, and a new UTXO carries the updated datum (`count: 1`) while the old one is consumed. ```mermaid flowchart LR A[UTXO₁State: count=0] --> B{Validate:• count₁ = count₀ + 1• Signed by owner} B -->|✓ Valid| C[UTXO₂State: count=1] B -->|✗ Invalid| G[Transaction Fails] C --> D{Validate:• count₂ = count₁ + 1• Signed by owner} D -->|✓ Valid| E[UTXO₃State: count=2] D -->|✗ Invalid| H[Transaction Fails] E --> F[...] style A fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style C fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style E fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style G fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style H fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` The hands-on version, with Evolution, Mesh, and cardano-cli, is in [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend). ## What makes Cardano contracts different A few ledger features shape how you design contracts: - **Reference inputs ([CIP-31](https://cips.cardano.org/cip/CIP-31))**: read a UTXO's data without spending it, so many contracts can read one oracle feed at once. - **Inline datums ([CIP-32](https://cips.cardano.org/cip/CIP-32))**: store the datum in the output itself instead of a hash. See [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context#datum-hash-vs-inline-datum). - **Reference scripts ([CIP-33](https://cips.cardano.org/cip/CIP-33))**: deploy a script once and reference it from later transactions. The transaction shrinks, and the referenced bytes carry a [per-byte fee](/docs/developers/curriculum/fundamentals/core-concepts/fees#reference-script-fees) well below the cost of inlining. See [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend#reference-scripts). - **Collateral output ([CIP-40](https://cips.cardano.org/cip/CIP-40))**: return excess collateral to an address you choose. A validator's rules cannot be changed after deployment, and the compiled code cannot be turned back into source. That permanence runs one way only. On-chain code keeps working unchanged for as long as the chain exists, across hard forks and new ledger eras. Off-chain code is the opposite: it depends on details that shift between eras, such as fee parameters and how transactions are assembled, so it has to be kept current and re-tested as the network evolves. The validator you deployed years ago still judges by the same rules; the code that builds transactions against it may need maintenance to keep up. ## Choose a language Validators can be written in several languages that all compile to the same on-chain bytecode (UPLC). The examples in this module are written in **[Aiken](https://aiken-lang.org)**. See **[Choose a language](/docs/developers/curriculum/smart-contracts/choose-a-language)** for the full comparison (Aiken, Plinth, Plutarch, OpShin, Scalus, Pebble, Marlowe). ## Key takeaways - **Smart contracts are validators, not programs.** They check whether a transaction is allowed; they do not perform its logic. - **On-chain validates; off-chain constructs.** This keeps on-chain execution cheap and lets you write and test the two halves independently. - **Determinism is the superpower.** You know a transaction's outcome and cost before submitting it, which removes wasted fees and the fee-auction form of front-running. - **Script addresses lock UTXOs under code,** replacing key-based authorization with arbitrary rules. - **The eUTXO model extends UTXOs** with datums, redeemers, and context, enabling full contract logic while preserving determinism and parallelism. ## Next steps This module builds up from here, in order: 1. **[Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context)**: the three arguments every validator receives, in depth. 2. **[Choose a language](/docs/developers/curriculum/smart-contracts/choose-a-language)**: pick how you'll write validators. 3. **[Write a validator](/docs/developers/curriculum/smart-contracts/write-a-validator)**: the on-chain code itself, purpose by purpose. 4. **[Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend)**: build the off-chain transactions that interact with a contract. 5. **[Testing](/docs/developers/curriculum/smart-contracts/testing)**: verify validators with mock transactions before you deploy. 6. **[Contract library](/templates/contracts)**: audited, open-source contracts to read or start from. 7. **[Security](/docs/developers/curriculum/smart-contracts/security)**: what the eUTXO model protects you from, what it leaves to you, and how contracts are verified, with the [vulnerability reference](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview) and the [CTF](/docs/developers/curriculum/smart-contracts/security/ctf) beneath it. 8. **[Advanced](/docs/developers/curriculum/smart-contracts/advanced/overview)**: design patterns, UPLC, CBOR debugging, optimization, and the cryptographic primitives. Reference material, read when you need it. Then **[Build a dApp](/docs/developers/curriculum/dapps/overview)**, the next module, where your contracts meet users. --- ## Cardano Capture The Flag (CTF) The Cardano Capture The Flag (CTF) is an interactive security game where developers exploit purposely vulnerable smart contracts to learn about common security issues and prevention techniques. The game is completely open-source and designed for developers, auditors, and security researchers. ## What you'll learn - **Smart contract vulnerabilities**: hands-on experience with real Cardano security issues. - **Aiken development**: read and write smart contracts using Aiken. - **Off-chain integration**: build the exploit transactions in TypeScript with Lucid Evolution. - **Security mindset**: think like an attacker to build more secure contracts. ## How it works Each level presents a vulnerable smart contract with a sample interaction. Your goal is to: 1. Analyze the contract code for security flaws 2. Develop an exploit to drain funds or break the contract 3. Test locally, then execute on Cardano testnet 4. Learn the vulnerability and prevention techniques Challenges progress from basic to advanced, covering the most critical smart contract security issues on Cardano. ## Where to start: the Banking Series If you are new to Cardano security, begin with the **Banking Series**: 14 levels (0 to 13) that grow a deliberately simple bank, deposit and withdrawal, into a real protocol one vulnerability at a time. It is the gentler on-ramp, and each level's fix sets up the next level's flaw, which is exactly how real protocols evolve. The progression is worth seeing as a whole: - **Levels 0-1** are pure code smells: missing checks that would be bugs in any language. - **Levels 2-4** introduce UTxO thinking, where account-based assumptions stop translating. - **Levels 5-7** use tokens for authentication and teach that a token's security is its minting and movement, not its existence. - **Levels 8-9** bring [double satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction), including the case where you play both sides yourself. - **Level 10** is a refactor pitfall: a check that was obviously necessary early on gets lost when a feature is added. - **Levels 11-13** are off-chain signature "cheques", walking three distinct ways to get signature verification wrong (binding the signer, preventing replay, and signing every security-relevant field). Once the bank stops surprising you, the **original series** (multi-validator protocols, complex transaction construction, deep UTxO specifics) is the full-complexity challenge. Every vulnerability here maps to the [vulnerability reference](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview). ## Get started **Repository**: [cardano-ctf](https://github.com/Invariant-0/cardano-ctf) (open-source, GPL-3.0) 1. Clone the repository 2. Follow the setup instructions in the README (Node.js and Yarn, Aiken, and a Blockfrost key for the Preview testnet) 3. Start with the Banking Series level 0 and work your way up, editing the off-chain script to exploit each validator **Community**: Join the [Discord server](https://discord.com/invite/5XVW2MUdWu) to discuss solutions and get help. **Hints & Solutions**: Need a nudge in the right direction? Check out the [Cardano CTF Hints and Solutions blog](https://medium.com/@invariant0/cardano-ctf-hints-and-solutions-e3991ce6a944) with spoiler-free hints and detailed solution explanations for all challenges. --- ## Double Satisfaction > Adapted from [Invariant0's Cardano vulnerabilities series](https://medium.com/@invariant0/cardano-vulnerabilities-1-double-satisfaction-219f1bc9665e). Cardano validates a transaction against the rules of *every* input independently, and every validator sees the same transaction. Double satisfaction is the bug that follows: when two validators each look for "an output that pays me", one output can satisfy both, so an attacker meets two obligations while paying for one. ## A payment contract Take a `BuyNFT` contract that sells an NFT to whoever pays the seller. Its datum holds two fields: - **seller**: the address the buyer must pay. - **price**: how much the buyer must pay. The rule is one line: the spending transaction contains an output to `seller` worth at least `price`. ![Paying the seller unlocks the BuyNFT UTXO](../img/ds-2.png) The contract deliberately says nothing about the NFT itself. Alice only cares that she is paid, and what Eve does with the NFT afterward is her business. (If Alice locks no NFT in the contract, Eve simply sees an empty offer and does not spend it.) ## The attack Alice lists two NFTs as two separate `BuyNFT` UTXOs. Buying both should mean paying both sellers: ![Two offers, expected to require two payments](../img/ds-3.png) But each validator checks the transaction on its own, and both see the same outputs. So Eve consumes both offers and pays Alice once: ![One payment satisfies both validators](../img/ds-4.png) Both validators find an output paying Alice at least the price, both pass, and Eve buys two NFTs for the price of one. She satisfied two conditions where she should have satisfied one. ## Escalating defenses, and why they fall short **Forbid a second copy of the same script.** A first fix makes each `BuyNFT` validator reject any transaction that spends another UTXO under the same validator hash. That stops two `BuyNFT` offers colliding. But when the contract is later updated to `BuyNFTv2`, the new hash differs from the old, and Eve can pair an old offer with a new one: ![Two different script hashes sidestep the same-hash check](../img/ds-5.png) **Forbid every other script.** So each validator must reject *any* other script among the inputs, not just its own. Stronger, but still not enough, because minting policies and staking scripts are validators too: - A **minting policy** that lets anyone mint `AToken` by paying Alice 100 ADA is satisfied by the very same payment that satisfies a `BuyNFT` offer. ![Double satisfaction between a spending script and a minting policy](../img/ds-6.png) - A **staking script** that releases rewards to Alice's address is satisfied the same way: Eve withdraws the rewards and uses that payment to buy the NFT. **Double satisfaction within one script.** Even a single contract can double-satisfy itself. Suppose a 10% fee is added: 90% of the price to the seller, 10% to the operator, Bob. When Bob himself sells an NFT, he should receive both the fee and the price, but Eve can pay him just the price and let it count for both: ![Eve pays 90 ADA for an NFT that should cost 100](../img/ds-8.png) The fix for this last case is to compute how much each address is owed by summing the relevant datum fields, then check the actual total paid to each. Here that requires 100 ADA to Bob, and Eve's underpayment fails. ## Remediation A script that expects a payment can, in the strict form: - ban all other scripts from the transaction inputs, - ban all staking withdrawals, and - ban minting of any tokens. That is very restrictive, and many protocols genuinely need several scripts to interact. The looser options each pair an output to its obligation explicitly: - **Ordering.** Match inputs to outputs by position: the first input's validator checks the first output, the second the second, and so on. Each script owns one output. - **Tagging.** Tag each output with a unique marker (an output datum, or an input-to-output map passed in the redeemer) so a validator checks *its* output, not just *some* output. - **Transaction-level validation.** Offload the whole check to the minting policy of a single token, so one script pairs every input to its output. This is the [transaction-level minter](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/tx-level-minter) pattern. No approach fully protects a script against interacting with *other* scripts that do not know about it: script A might defend by ordering while script B defends by tagging, and an attacker can tag the first output and fool both at once: ![Two scripts defending differently can still both be satisfied](../img/ds-9.png) Until an approach is standardized, treat any interaction between mutually unaware scripts as either forbidden or potentially vulnerable. Users can help too: a script deployed at a unique address per usage cannot collide with another expecting payment to the same address, though that is not always possible, for example when chaining one script's output into another. ## Formal framework > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `multiple-satisfaction` **Property statement:** All scripts consider the totality of inputs to the transaction, as well as the totality of minted value and value withdrawn from staking validators, when allowing spending, minting, or withdrawing value. **Test:** A transaction consumes multiple UTXOs, spending the value of each individual UTXO while respecting each individual UTXO's conditions, but without respecting the intended *aggregate* condition under which the total value could be spent. More general variants draw the extra value from minted value or staking withdrawals rather than from inputs. **Impact:** - Leaking protocol tokens. - Unauthorised protocol actions. **A common vulnerable pattern:** ```haskell vulnValidator __ ctx = traceIfFalse "Must continue tokens" (txOutValue ownInput == txOutValue ownOutput) where ownInput = txInInfoResolved $ findOwnInput ctx [ownOutput] = getContinuingOutputs ctx ``` This checks that a consumed UTXO's value continues to an output locked back at the validator ("own input" to "own output"). The logic is correct for one UTXO in isolation, but breaks when two outputs at the validator hold the same value. Consider two of them: ```text Output A - TxOut ($FOO x 1 + $ADA x 2) Output B - TxOut ($FOO x 1 + $ADA x 2) ``` A transaction that spends both can steal the value of one. It pays `$FOO x 1 + $ADA x 2` back to the validator's address, which satisfies both validator runs (each finds a continuing output holding the expected value), and sends the other `$FOO x 1 + $ADA x 2` to an arbitrary address. The fix is to account for *all* inputs, not only that the desired input is present but that no undesired ones are, and, for full protection, to check that no minting policies or staking validators are also executing. ## Code examples - [Mesh: Double Satisfaction Example](https://github.com/MeshJS/mesh/tree/main/packages/mesh-contract/src/swap/double-satisfaction) --- ## Evaluation and Grinding Both entries here exploit the same property that makes Cardano validation predictable. Execution is deterministic and the transaction author picks the inputs, so an author can work out in advance which branches will run and what any hash will evaluate to, then choose inputs that suit them. A check placed on a branch that gets skipped never runs, and a decision derived from an attacker-influenced hash can simply be ground until it comes out favorably. ## Lazy Evaluation Traps > A language-level footgun in Plutus Core evaluation; see the [Aiken language tour](https://aiken-lang.org/language-tour/control-flow) on control flow. **Identifier:** `evaluation-order` **Property statement:** Every check that must run to keep the validator safe is actually forced, not placed on a branch that can be skipped. **Test:** A transaction passes validation because a required check sat on a short-circuited or untaken branch and never executed. **Impact:** - Required checks silently skipped - A validator that succeeds when it should have failed **Further explanation:** Plutus Core, which Aiken compiles to, is lazy in a few places that matter for validators. The boolean operators `&&` and `||` **short-circuit**: the second operand runs only if the first did not already decide the result. `if/else` and `when` evaluate only the branch that is taken. And an `error` or `fail` fires only when it is actually forced. Aiken is otherwise strict, but these control-flow constructs keep the lazy behavior. The trap is a required check placed where it can be skipped. A necessary assertion in the right operand of `||` never runs when the left operand is already `True`. A guard inside a branch that is not taken never fires. A `fail` you expected to stop a bad transaction sits on a side that is never forced. In each case the validator returns success while a check you thought was protecting it did nothing. Idiomatic Aiken avoids most of this: the `and { ... }` and `or { ... }` blocks list all conditions explicitly, and `expect` and `fail` are strict where the code path is taken. The exposure is real for hand-written Plutus, Plutarch, or UPLC, and for misjudged operand order in any language. Prevent it by never putting a required predicate or a security-relevant `fail` on a branch that can be short-circuited away, forcing every check that must run (a statement-level `expect`/`fail`, or listing conditions explicitly), and ordering operands so cheap, non-security guards go first and the checks that must always run are never the ones skipped. ## Hash Grinding on Ordering > An advanced, emerging class. Grounded in Cardano's deterministic validation and demonstrated by on-chain proof-of-work such as [Fortuna](https://github.com/aiken-lang/fortuna). **Identifier:** `hash-grinding` **Property statement:** No placement, ordering, or selection that matters for security is derived from a hash of data the transaction author can influence. **Test:** An attacker chooses transaction contents so that a hash-derived position or outcome lands where they want. **Impact:** - Biased placement in on-chain data structures (denial of service) - Rigged "random" selection **Further explanation:** A validator has no source of randomness. It sees only data the transaction author chose, and its execution is deterministic. So any value derived from a hash of attacker-influenceable input, the transaction hash, an output reference, a datum field, a token name, can be **ground**: the author re-tries transaction contents until the hash comes out favorable. This is not expensive. Cardano's own proof-of-work token, Fortuna, is a living demonstration that grinding on-chain hashes at scale is cheap; its entire mechanism is nonce grinding. The vulnerability appears when a hash decides **placement or order** in an on-chain structure: - a bucket in a sharded or distributed map, - a position in a sorted or linked association list, - a path in a Merkle Patricia trie, - the winner of a "random" selection, raffle, or sortition. An attacker grinds the input to force a chosen location: cluster many entries into one branch to bloat its proofs and push transactions into execution-unit or size limits (a denial of service against everyone who must traverse it), engineer adjacency to attack a specific neighbor, or win a draw that was supposed to be fair. Prevent it by never treating an author-influenced on-chain hash as randomness. Use a commit-reveal scheme or a verifiable random function (VRF) for randomness, make placement independent of attacker-controlled hashes, or bound the blast radius (cap per-bucket size, and require inclusion proofs the attacker cannot cheaply densify). --- ## Missing UTxO Authentication ## Overview > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `missing-utxo-authentication` **Property statement:** All spending and referencing of legit protocol outputs is authenticated. **Test:** A transaction can successfully spend or reference an illegitimate protocol output. **Impact:** Unauthorised protocol actions **Further explanation:** This vulnerability can easily be illustrated by using oracles as an example. Imagine a protocol that relies on information about the real world to allow or disallow certain actions. For instance, an insurance company could allow spending from a pool of funds if some natural disaster such as an earthquake or a hurricane had hit a certain region in the last 30 days. In order for the validator locking the funds (`insuranceVal`) to know whether such a natural disaster has occurred, it relies on the information given by an oracle. The way the oracle provides the information is by locking in the oracle validator (`oracleVal`) a UTxO carrying as datum the latest date when a natural disaster happened in a certain region. A naive implementation of `insuranceVal` could be to search for an input coming from `oracleVal`, read the information stored in the datum and decide whether to allow spending or not based on that information. However, by using this approach it would be very easy to fool `insuranceVal` to unlock the funds. This is due to the nature of validators on Cardano, which only validate the consumption of UTxOs locked by them, but do not control the locking of outputs. This allows anybody to send funds to a validator's address, effectively locking all kinds of UTxOs. In the context of our example, this means that anybody could lock a UTxO carrying as datum false information, for instance stating that a hurricane happened in the last week. This would fool `insuranceVal` to allow spending of the funds. In order to prevent this, the legit UTxO in `oracleVal` that holds the real information provided by the oracle should be authenticated. One way of achieving this would be to hold a specific non-fungible token (`oracleNFT`) as part of the value. Now, instead of searching for an input coming from `oracleVal`, `insuranceVal` could safely look for an input holding `oracleNFT`, which is unique. --- ## In-depth analysis: trust no UTxO The same vulnerability is commonly seen in multi-step contracts, where an attacker creates a script UTxO in an invalid state and uses it for attacks. It is often easily preventable for small contracts, but as contracts grow in complexity it becomes much more difficult to prevent. As an example, consider a simple voting contract that a DAO (decentralized autonomous organization) can use. ### Voting example The protocol is implemented using two contracts, the voting contract and the DAO contract. Let's break these contracts down. In the voting contract, everyone votes by signing a transaction where their vote is added to a list kept in the datum. Multiple participants vote on the same proposal across multiple transactions, adding their names one at a time. In the example transaction below, we can see Eve voting for a new proposal by signing the transaction and adding her name to the list. ![TNU-1](../img/tnu-1.png) Eve votes by appending her name to the list in the datum. Note that she must sign the transaction, so no one else can vote as her. Once enough votes accumulate, the contract can be spent by the DAO contract and the proposal is passed: ![TNU-2](../img/tnu-2.png) A proposal is passed because enough eligible voters voted for it. Each of them had to sign transaction appending their name into the list of votes. We leave out the parts where the passed proposals are utilized and where the details of the proposals are kept as they are not important for our purposes. Note that we also leave out some other details, such as holding minimum required Ada in each UTxO. ### The vulnerability Each validator runs only if a UTxO that the validator protects is spent. No validator protects rightful creations of such UTxOs. Anyone can freely create a UTxO with any datum and any value at any address, including script addresses. Therefore, evil Eve who wants her proposal to pass can simply create the following UTxO at the address of the voting contract: ![TNU-3](../img/tnu-3.png) Nothing checks the initial state of the contract, so Eve can just create a UTxO with a maliciously constructed datum without anyone else signing it. Note that neither Bob, Alice nor John need to be aware of this. On the other hand, the UTxO looks like they all signed the proposal. Eve can immediately spend the UTxO by the DAO contract, thus passing the proposal. Looking at the blockchain history, someone could easily spot this attack. The DAO contract has no access to the whole blockchain history and can not distinguish such a UTxO from a valid one, though. ### Remediation Assuming multi-step contracts, the validation logic itself cannot verify that all the previous state transitions were correct. That's because anyone can create a UTxO in a state that is equivalent to a later state of the contract without passing the validations. We need to be somehow able to verify that a given UTxO comes from a chain of valid transitions from a valid initial state. To generally remediate this vulnerability, we need to be able to verify the initial state of a given contract and that all state transitions were done correctly. Verifying a state transition is simple in the Cardano model by using validators which always check whether the next state can be reached from the previous state. To verify that the initial state was correct, we use validity tokens, also sometimes called state (thread) tokens. These tokens are minted by a minting policy and we can allow them to be minted only in transactions that create a new valid proposal at the voting address. We can see an example in the following transaction: ![TNU-4](../img/tnu-4.png) A validity token can be minted as we create a voting UTxO in a valid initial state. Although Eve can still create any voting contract UTxO, she can create one that contains a freshly minted valid minting token only if she sets the list of votes to an empty list. The minting policy of the validity token verifies that the newly-created voting UTxO is in the correct initial state. Next, the voting contract's validator must check that the validity token stays in the contract. Therefore, the validity token in a contract verifies that the contract was created from some valid initial state. In the end, the DAO contract can simply reject voting contracts not holding a correct validity token. It should also make sure that the validity token is burned once the proposal is passed. ### Dangers of validity tokens Once a client implements validity tokens in their protocol, we can try to find ways to steal them from the contract. As mentioned previously, the ownership of a validity token means that it is assumed that it was properly initialized and went only through valid state transitions. If we can find a way to get the validity token from a UTxO, we can fool other contracts again. In practice, it is often possible to somehow steal the token. It's especially tricky to prevent it entirely in complex multi-step contracts with multiple different validity tokens. In each step, we need to verify that the validity tokens goes into the intended script outputs. If there is at least one place in the code where this verification contains a mistake, an attacker can use that to steal one of the tokens. A complex example seen in practice was a double satisfaction between the validity token minting policy and a validator's validation branch. The idea can be demonstrated on a modified version of the DAO contract. Let's add the possibility to retract a vote. To do this, the voter must have already voted. Secondly, the voter must sign the retracting transaction. Note that if there was only one voter, retracting his vote would return the voting contract to the valid initial state, the same one it was in when the proposal was created, thus possibly allowing the mint of a fresh new validation token: ![TNU-5](../img/tnu-5.png) Eve abuses the fact that by retracting her vote she creates a contract in the valid initial state, allowing her to mint an additional validity token. A similar example has been seen in audits. Because both the voting validator and the validity token minting policy expect the validity token to be in the resulting voting UTxO, putting it there satisfies both conditions, this is also called a double satisfaction. The full attack chain Eve uses to get her proposal passed is as follows: 1) Create a valid voting contract and mint a validity token into it. 2) Vote for the proposal. 3) Retract the vote. Using the double satisfaction vulnerability, mint a second validity token and steal the original one by sending it to her address. 4) Create a new voting UTxO where the datum looks like all the voters have already voted for the proposal and put the stolen validity token into it. 5) Pass the proposal. The DAO contract validates as it sees that the UTxO contains a correct validity token and has enough votes to pass. As an exercise you can try to come up with an additional check to prevent this vulnerability. ### Conclusion This vulnerability stems from the fact that a UTxO can be created by anyone. Cardano developers must account for this when designing their smart contracts and use appropriate mitigation strategies. Validity tokens are used a lot in practice, and stealing them often results in critical severity findings. For an open source example illustrating the issue, see the [Agora report](https://github.com/vacuumlabs/audits/blob/master/reports/liqwid-agora-v1.pdf), issue AGO-001: Stake state token can be taken away. --- ## Vulnerability reference This is the reference half of [Smart Contract Security](/docs/developers/curriculum/smart-contracts/security), which teaches what the eUTXO model protects you from and what it leaves to you. Here every failure is written up in full, so you can look one up while building or work through them while auditing. Most entries carry an **identifier**, a **property statement** (what must hold for the protocol to be safe), a **test** that demonstrates the failure, and its **impact**. The identifiers are stable and are what audit reports cite. ## Four deep dives These come up most often, are the most subtle to get right, and each is a page of its own. | Vulnerability | Identifier | Description | |---|---|---| | [Double Satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction) | `double-satisfaction` | Multiple UTxOs in one transaction, each validator sees the same outputs, so one payment satisfies all of them | | [Missing UTxO Authentication](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/missing-utxo-authentication) | `missing-utxo-authentication` | Anyone can create UTxOs at script addresses, so without authentication you cannot tell legitimate from fake | | [Time Handling](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/time-handling) | `time-handling` | Validators only see time intervals, not exact timestamps, and incorrect bound handling enables time manipulation | | [Token Security](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/token-security) | `token-security` | Native tokens, validation tokens, dust attacks, and execution limit exploits | ## Four classes The rest group into four classes. Reading a class start to finish beats reading its entries separately: within a class the failures share a mechanism, and usually a defense. ### [Resource exhaustion](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion) Something unbounded grows past a ledger limit, or many actors compete for one UTXO. Value stops being spendable, or the protocol stops making progress. | Vulnerability | Identifier | Description | |---|---|---| | [Unbounded Value](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#unbounded-value) | `unbounded-value` | Unlimited tokens in a UTxO cause size and execution limit failures, and funds become unspendable | | [Unbounded Datum](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#unbounded-datum) | `unbounded-datum` | A datum growing without limits eventually exceeds resource constraints | | [Unbounded Inputs](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#unbounded-inputs) | `unbounded-inputs` | Too many UTxOs required simultaneously hits transaction size and resource limits | | [Cheap Spam](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#cheap-spam) | `cheap-spam` | Low-cost spam actions stall legitimate protocol operations | | [UTxO Contention](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#utxo-contention) | `utxo-contention` | Shared global state creates contention when multiple users need the same UTxO | ### [Unchecked inputs](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs) The validator trusted something the transaction author chose. Anything it does not explicitly check is an attacker's free choice. | Vulnerability | Identifier | Description | |---|---|---| | [Arbitrary Datum](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#arbitrary-datum) | `arbitrary-datum` | Not validating a datum when locking allows invalid data that causes spend failures | | [Other Redeemer](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#other-redeemer) | `other-redeemer` | Logic expecting a specific redeemer is bypassed by using a different redeemer on the same script | | [Other Token Name](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#other-token-name) | `other-token-name` | Minting policies not checking all token names allow unintended tokens under the same policy ID | | [Missed Input](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#missed-input-validation) | `missed-input` | A redeemer index not bound to the spent input lets an unvalidated input slip past a global validator | | [Signature Domain Separation](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#missing-signature-domain-separation) | `signature-domain-separation` | Off-chain signatures without a domain separator or nonce replay across protocols or repeatedly | ### [Staking and certificates](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/staking-and-certificates) An address has two credentials. A validator that governs only the payment half leaves the staking half open. | Vulnerability | Identifier | Description | |---|---|---| | [Insufficient Staking Control](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/staking-and-certificates#insufficient-staking-control) | `insufficient-staking-control` | Missing staking credential checks allow reward redirection, franken addresses, and stake-key spoofing | | [Certificate Deregistration](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/staking-and-certificates#unconstrained-certificate-operations) | `certificate-deregistration` | An unguarded staking-script certificate path lets anyone deregister the credential and halt a withdraw-zero protocol | ### [Evaluation and grinding](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/evaluation-and-grinding) Determinism makes validation predictable for you and for an attacker, who can work out in advance which checks will run and what a hash will come out to. | Vulnerability | Identifier | Description | |---|---|---| | [Evaluation Order](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/evaluation-and-grinding#lazy-evaluation-traps) | `evaluation-order` | Short-circuiting boolean operators can skip a required check or a deferred failure | | [Hash Grinding](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/evaluation-and-grinding#hash-grinding-on-ordering) | `hash-grinding` | Author-influenced on-chain hashes are grindable, biasing placement or selection | ## Practice Attack these yourself in the **[Cardano CTF](/docs/developers/curriculum/smart-contracts/security/ctf)**, an interactive security game where you exploit vulnerable contracts. ## Sources Reference material: - **[MLabs](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities)** - Formal vulnerability framework - **[Invariant0](https://medium.com/@invariant0)** - In-depth security analysis - **[Mesh](https://github.com/MeshJS/mesh)** - Code examples --- ## Resource Exhaustion Every UTXO and every transaction has hard limits: a maximum serialized size, a memory budget, and an execution-unit budget. This class is what happens when a protocol lets something it never bounded grow past one of those limits, or lets many actors compete for a single UTXO. The outcome is the same each time, value that can no longer be spent or a protocol that can no longer make progress, and so is the defense: bound the thing before an attacker does. The first three entries are the size limits, which share one mechanism. The last two are liveness attacks, where nothing exceeds a limit but the protocol still stops working. ## Unbounded Value > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `unbounded-value` **Property statement:** Values of all legit UTxOs locked by the protocol have an upper bound for their size, and the upper bound is low enough to not prevent consumption of the UTxO as an input in a future transaction. **Test:** A transaction can successfully lock in the protocol a legit UTxO with a value large enough to make its consumption fail due to an unexpected number of tokens or reaching the network resources constraints. **Impact:** - Unspendable outputs - Protocol halting **Further explanation:** Typically, a large value could make a transaction fail in three ways: **Scenario 1:** if a script expects an exact or bounded number of tokens in some of its inputs, the transaction will fail if more tokens are present in those inputs. For instance, in the case where a validator script contains code similar to `let [(cs,tn,amt)] = flattenValue (input.value)`, if a previous transaction had locked an output with any token other than ADA, a subsequent transaction consuming that output would fail. **Scenario 2:** if an input UTxO has N native tokens in the value, then just by passing on the input values to the output and adding some M additional tokens, the transaction might fail due to exceeding the transaction size limit. The most common pattern where this becomes a problem are script logics that require the ongoing addition of distinct tokens to the UTxOs locked by scripts. Note that values held by UTxOs only contribute to the size of the transaction when being part of the outputs of the transaction, but not when they are part of the inputs. **Scenario 3:** if the input UTxO contains a lot of different native tokens and the script logic is such that it must go through and process them, then the transaction might fail due to execution resources (XU limits) being breached. This is the hardest scenario to identify, as it becomes a problem in scripts where unexpected tokens are not taken into account, being easy to forget about them. For instance, if a script had to fold through the value of an input looking for a specific combination of asset class and amount, it would be problematic if that input contained a large amount of asset classes. A common case where this problem arises is when the logic of the scripts allow the presence and addition of foreign tokens (i.e. tokens not expected by the protocol). This problem can be prevented with tighter constraints on output values: it is not enough to check that the expected tokens are present in the locked outputs, you should also explicitly check that *only* the expected tokens are present, disallowing any extras. Also, resource consumption monitoring tests should be implemented, and transactions involving maximum expected flows of value should be covered by those tests. ## Unbounded Datum > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `unbounded-datum` **Property statement:** Datum for all legit UTxOs locked by the protocol have an upper bound for their size, and the upper bound is low enough to not prevent consumption of the UTxO as an input in a future transaction. **Test:** A transaction can successfully lock in the protocol a legit UTxO with a datum such that its consumption in a second transaction fails due to reaching the network resources constraints. **Impacts:** - Unspendable outputs - Protocol halting **Further explanation:** A common design pattern that introduces such vulnerability can be observed in the following excerpt: ```haskell data MyDatum = Foo { users :: [String], userToPkh :: Map String PubKeyHash } ``` If the protocol allows `MyDatum` to grow indefinitely, eventually memory and CPU usage limits and/or size limits imposed by the Plutus interpreter will be reached, rendering the output unspendable. Note that although inline datum for the inputs of a transaction do not contribute to its size (unlike a non-inline datum, as it must be attached), they still might contribute to increase the memory and CPU usage depending on the validator's logic. The recommended design patterns are either to limit the growth of such datum in validators or to split the datum across different outputs. ## Unbounded Inputs > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `unbounded-inputs` **Property statement:** All transactions within the scope of the protocol can be performed with a number of inputs low enough to not exceed the transaction size or resources usage (memory and CPU usage) limits. **Test:** The protocol reaches a state where too many UTxOs are supposed to be consumed simultaneously, making a legit transaction fail because of exceeding the size or resources usage limit. **Impact:** - Unspendable outputs - Protocol halting **Further explanation:** Consider the case of a faucet where users are allowed to claim 100 ADA in each transaction. A naive implementation could look like the following: ```haskell vulnValidator _ _ ctx = traceIfFalse "Must return change to script" $ contOutputsValue == (inputsOwnAddressValue - (singleton "" "" 100000000)) where ownInput = txInInfoResolved $ findOwnInput ctx ownAddress = txOutAddress ownInput inputsOwnAddress = filter (\i -> txOutAddress (txInInfoResolved i) == ownAddress) $ txInfoInputs (scriptContextTxInfo ctx) inputsOwnAddressValue = sum [txOutValue i | i <- inputsOwnAddress] contOutputs = getContinuingOutputs ctx contOutputsValue = sum [txOutValue o | o <- contOutputs] ``` The validator above ensures that only 100 ADA (100,000,000 lovelaces) is spent from the faucet and that the rest is locked backed in the same script. However, it does not enforce anything about the structure of these outputs. Therefore, all the value in the inputs coming from the script (minus the claimed 100 ADA) could be locked back in the script diluted in as many outputs as the size of the transaction allows. Depending on the original amount and distribution of ADA locked in the script, this could result in a situation in which in order to claim the next 100 ADA, too many inputs are needed (as no individual or small amount of inputs contain the needed 100 ADA) and the limits are reached. This would result in unspendable UTxOs locked by the script. In order to prevent the issue described above, it could be enforced that there is a single input coming from the script and a single output being locked back in the script. ## Cheap Spam > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `cheap-spam` **Property statement:** All intended actions can be performed in a timely manner under the assumption that nobody is willing to spend more resources than the potential gain by denying service of the protocol. **Test:** A denial of service status is achieved by introducing many actions that interfere with the intended use of the protocol, making it impossible to consume the target UTxO in a timely manner. **Impact:** - Protocol stalling - Protocol halting **Further explanation:** Stalling is problematic when the cost to stall is lower than the loss of opportunity cost it causes (i.e., by spending N Ada you cause the protocol to lose M Ada, where M > N). Usually this snowballs, especially in financially incentivised protocols because people lose trust and then it all amplifies. For instance, if the solvency of a lending protocol depends on liquidations of debt to be performed in a timely manner, it is important to make sure that there are no actions such as creating many small and undercollateralised debt positions that would delay liquidation of a big debt position. Note that the combination of this vulnerability with [UTxO contention](#utxo-contention) increases its severity, as it would be easier to deny service to a single UTxO. ## UTxO Contention > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `utxo-contention` **Property statement:** The protocol is designed in such a way that disincentivises the attempt to consume the same UTxO by multiple actors. **Test:** One out of two or more transactions trying to consume the same UTxO fails due to the UTxO not existing anymore. **Impact:** Protocol stalling **Further explanation:** This vulnerability is very common in the case where a UTxO carries some global datum or shared value (global state). For instance, a decentralised exchange (DEX) that holds in a single UTxO (global UTxO) the pool of assets available to be swapped would experience a high degree of contention, since every swap would require consuming the global UTxO and recreating it by locking back the pool of assets with the swap already performed. In practice, this would make the DEX unusable, since as soon as it becomes popular and volume of transactions is significant, the global UTxO would be unavailable for most of the users. Protocols that aim to minimise this vulnerability should aim for parallel transactions and distributed state management wherever possible. Concretely, this means not holding mutable shared state in one global UTxO. Instead, partition it across many independent UTxOs, one per user, position, or campaign, so that concurrent transactions land on different UTxOs and never compete for the same one. This per-instance (or "siloed") layout is the deliberate design response to contention: the [linked list](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/linked-list) and [trie](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/trie) patterns are concrete structures for distributing state this way while keeping it verifiable on-chain. For state that genuinely must be shared, keep it in a UTxO that transactions consume as a reference input rather than spend, so any number of them can read it in parallel without contending. --- ## Staking and Certificate Control An address has two credentials, and a validator that only governs the payment half leaves the other one open. The staking credential controls who collects rewards and who may register or deregister the credential itself, and both are authorized independently of spending. These two entries are what goes wrong when a protocol forgets that. ## Insufficient Staking Control > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `insufficient-staking-control` **Property statement:** All scripts explicitly account for staking credentials. **Test:** A transaction successfully changes or incorrectly sets the staking credential of a UTxO locked by a validator of the protocol. Alternatively, a transaction sets an arbitrary staking credential for an output being locked by an external credential and holding value consumed from the protocol. **Impact:** - Unpredictable addresses - Illegitimate staking rewards **Further explanation:** When writing the logic for a Plutus script, it is easy to focus too much on the set of rules that must be enforced by a validator and start thinking of these rules as solely defining the Cardano addresses. This is, treating validator hashes and addresses interchangeably. An example of such behaviour is illustrated by the following excerpt: ```haskell vulnValidator __ ctx = traceIfFalse "Must continue tokens" (txOutValue ownInput == contVal) where ownInput = txInInfoResolved $ findOwnInput ctx ownValidatorHash = ownHash ctx [(_, contVal)] = scriptOutputsAt ownValidatorHash (scriptContextTxInfo ctx) ``` The validator above tries to make sure that after consuming a UTxO locked by vulnValidator, an output holding the same value is locked back. However, it forgets about the staking credentials, so the output can actually be locked in a very large number of addresses. This is because addresses are composed of credentials that control the spending of UTxOs and staking credentials that control the claiming of ADA staking rewards. Therefore, validation would succeed as long as the output is locked in an address which has ownHash as credential. However, there are as many such addresses as possible public keys or script hashes for a staking credential. By exploiting this, anybody could send the funds to an address with a staking credential controlled by them. This would not grant them control over the funds, since they are still guarded by the validator's logic, but would grant them control over the staking rewards generated by all the ADA present in the locked output. Apart from losing control over staking rewards, ignoring the staking credentials could have further consequences and result in a catastrophic outcome. This is because since the UTxO holding the funds can live in a big spectrum of addresses, it becomes more difficult to reason about the rules that control their spending. For instance, to prevent a multiple satisfaction attack, a validator could have a rule ensuring that only one input coming from the address of the input being validated is present in the transaction. This works correctly assuming that all relevant funds are locked in the same address as the input being validated. However, as soon as part of the funds end up in an address with the same credential but different staking credential, the check could be by-passed and tokens could leak. Finally, this not only applies when locking UTxOs in scripts but also when sending funds from a script to a public key. If the script controlling it only checks that funds are correctly sent to the public key but neglects the staking credentials, the value could be sent to a mangled address, where the public key is correct but an arbitrary staking key has been used. Although the legit owner of the funds would still have access to them, the right to claim staking rewards generated by the ADA in these funds would temporarily belong to an actor different than the legit owner of the funds. This problem is prevented by explicitly checking the staking credentials, taking into account complete addresses instead of only the credentials controlling the spending of funds. An address assembled this way, the correct payment credential paired with an attacker's staking credential, is often called a **franken address** (or mangled address), and it has caused real value loss in deployed protocols. A related anti-pattern is using the **staking credential as an authorizer**: gating an action, a whitelist, or an airdrop on the stake credential of an address rather than the payment credential. Because anyone can build an address that reuses a victim's stake credential under a payment key they control, authorizing by the stake credential can be spoofed. Authorize by the payment credential (which actually controls the funds), and when paying out to an address, check or reconstruct the complete address rather than a single credential. ## Unconstrained Certificate Operations > Concerns the withdraw-zero pattern documented by [Anastasia Labs design patterns](https://github.com/Anastasia-Labs/design-patterns/blob/main/stake-validator/STAKE-VALIDATOR.md) and [CIP-112](https://cips.cardano.org/cip/CIP-112). **Identifier:** `certificate-deregistration` **Property statement:** A staking script explicitly handles its certificate (registration and deregistration) operations, denying by default any it does not intend to allow. **Test:** A transaction submitted by an unrelated party successfully deregisters the protocol's staking credential. **Impact:** - Protocol liveness halted until re-registration - Repeatable griefing plus theft of the refunded key deposit **Further explanation:** Many protocols centralize validation with the **withdraw-zero** pattern: instead of each input re-running the same expensive checks, the spend validators only require that a specific staking script executes in the transaction, and the real logic runs once in that staking script. A staking script executes when the transaction includes a withdrawal from its reward account, even a withdrawal of zero, which is why the pattern is cheap. It has a precondition, though: the stake credential must be **registered**, or the zero withdrawal fails phase-1 validation. A staking script runs for more than withdrawals. Registering or deregistering its credential also invokes it, under its certifying (`publish`) purpose. If the script does not constrain that purpose, for example a catch-all fallback that returns success for any operation it did not explicitly consider, then anyone can submit a deregistration certificate for the credential and the script will approve it. Deregistration does two things: it refunds the key deposit (2 ADA on mainnet) to whoever submitted the certificate, and it removes the credential. Every subsequent protocol transaction that relies on the withdraw-zero withdrawal now fails, because it withdraws from a reward account that no longer exists, until someone re-registers the credential and pays the 2 ADA deposit again. An attacker can repeat this, turning it into a cheap, repeatable denial of service with a small profit on each round. The attack has a mirror image. If the unguarded operation is registration and **delegation** rather than deregistration, an attacker can register the credential and delegate it to a stake pool. Nothing halts on-chain, which is what makes it subtle: real rewards begin accruing to the reward account, and off-chain code that hardcodes a withdrawal amount of zero starts building invalid transactions, because the reward account's balance is no longer zero. Production teams have called this out as one of the least obvious ways to break a withdraw-zero deployment. Aiken's default is protective here: a validator with no fallback handler rejects any purpose it does not explicitly handle, so an unhandled certificate operation is denied. The vulnerability appears when a developer adds a permissive `else` that succeeds, or writes a `publish` handler that does not guard which certificate is being posted. Prevent it by handling the certificate purpose explicitly and denying deregistration unless the protocol genuinely intends to allow it, and by not making liveness depend on a single credential that anyone can deregister. The same discipline covers the mirror attack: on a forwarding-only credential, deny registration and delegation as well, and have off-chain code read the actual reward balance rather than assume it is zero. --- ## Time Handling Time in Cardano transactions is a complex topic. To understand why, you must first understand Cardano's transaction validation philosophy. One of its main ideas is that the transaction execution is deterministic. The whole transaction is constructed locally on the computer of the transaction creator and then it is sent to the blockchain. A Cardano transaction includes the end result. This contrasts with Ethereum transactions, where the creator of the transaction just calls a function of a smart contract, not necessarily knowing the end result as it depends on the chain state at that moment which is not fully known in advance. Taken to the extreme, the function can potentially do something completely different than the user expected. Cardano smart contract validation consists of two phases. The first phase validates that the relevant part of the blockchain state the transaction sees is the same as the state when the user built the transaction. For example, if some transaction outputs that the transaction spends have already been spent, the transaction fails the first validation phase. The second validation phase executes Plutus scripts. Consider time handling. In Ethereum, smart contracts can use the block.timestamp to get the timestamp of the current block in which the transaction executes. You could, for example, create an Ethereum smart contract that checks whether the "current timestamp" is even or odd, and depending on that either sends the user money or not. Some smart contracts even go as far as to consider this timestamp a source of randomness in their contracts. It is not a good idea, as it can be manipulated by the block producers. As mentioned, a user building a Cardano transaction needs to know exactly what the transaction does. It is part of the transaction itself. As a result, it's impossible to write a similar script on Cardano. The timestamp at which the block is mined is unknown when the user builds the transaction, therefore it can not be part of the transaction data. However, without any kind of access to the current time, Cardano scripts would be greatly limited. Many dApp use-cases such as vesting, lending, and much more depend on it. The Cardano time model needs to satisfy both these points: 1) Give validators some information about the current time if they need it. 2) Not require a user to know the exact time when the transaction will be processed as it is impossible to know. ## Time intervals Even though a user can't exactly know when a transaction will be included in a block, he can set a time interval when the transaction is valid and can be included in a block. By doing so, he provides the validator with some information about the current time. This interval is included in the txInfoValidRange field that is part of the transaction properties accessible from Plutus: ![TH-1](../img/th-1.png) Transaction information in Plutus The valid range is an interval of timestamps, which must contain the actual block timestamp. It has a lower bound and an upper bound, both of which can be either integer values or can refer to a negative/positive infinity. Phase 1 transaction validation, which has access to the current timestamp, validates that the current timestamp lies somewhere in this interval. The Plutus script only sees the interval specified by the transaction creator. In summary: 1) The transaction creator does not know the exact timestamp when his transaction will be validated but can specify a sufficiently long interval covering it. 2) The Phase 1 transaction validation sees both the current timestamp and the specified transaction validity interval. It verifies that the current timestamp lies in the interval, and if so, continues with the Phase 2 validation. 3) The Phase 2 validation that includes Plutus script execution only sees the interval. The user building the transaction can now provide the validator with useful time information without having to know the exact timestamp at which the transaction will be validated. What's more, the transaction creator can still deterministically check whether the transaction will pass or fail the Phase 2 validation. Note that he can not influence when the transaction will be included in a block and so he can not know whether it will pass the Phase 1 validation. However, he can be sure that if it does, meaning the time falls in the validity interval, the scripts will pass. This solution elegantly solves the problem while maintaining determinism but is sometimes difficult for new programmers to fully understand and leads to potential smart contract security issues. ## Example vulnerabilities Consider potential time-related vulnerabilities on a simple peer-to-peer lending protocol. A lender can create a lending UTxO holding a proposed loan's amount and specifying the loan properties including the loan's duration. A borrower can accept the loan by spending such UTxO and locking collateral into it. If he does not repay the loan until the specified time, the lender can liquidate the collateral. The following diagram shows the expected workflow: ![TH-2](../img/th-2.png) Time diagram of loan repayment The blue dot on 18.3. represents the moment when a lending UTxO is created. The lender specifies the loan duration as three days. On 20.3. a borrower accepts this loan, locks the collateral and has to note the date when the loan needs to be repaid as the current day plus 3 days. After that, if the loan is still not repaid, the lender can take the collateral. Time attacks come in two directions, and both reduce to picking the wrong bound of the validity interval. Proving "now is **after** a deadline" (an expiry, a vesting date, a loan's end) needs the **lower** bound; leaning on the upper bound instead lets an attacker widen it and act early, unlocking vested funds before they are due. Proving "now is **before** a deadline" needs the **upper** bound; leaning on the lower bound lets an attacker widen it downward and execute something that has already expired. The lending example below shows both. This section focuses purely on the time handling. There are two transactions in which the current timestamp must be considered in the smart contract validation logic: 1) When a borrower accepts the loan, he needs to write the time when the loan ends into the datum. To do this, he should take the "current time" and add the loan duration from the loan properties. The script needs to check that this is done properly. 2) When the lender tries to liquidate a collateral, the script needs to verify that the current timestamp is after the loan ending timestamp. It can not allow the lender to liquidate before this time. Consider the first of these points. Recall that the script has no access to the current timestamp on Cardano. The script can only access the validity interval specified by the transaction creator and the script knows that the current timestamp lies somewhere in this interval (otherwise the Phase 1 validation would fail and so the script can trust that it indeed lies in the interval). One common approach is to use only one side of the interval, either the lower bound or the upper bound and assume that's a good approximation of the current time. Consider what would happen if the script chose the end of the time interval: ![TH-3](../img/th-3.png) Attacker moves the upper bound of interval and tricks the smart contract into having more time to repay the loan. A borrower is creating the accept-loan transaction. The current date is March 20 (the green dot, 20.3.). The borrower specified the green time interval as the validity range of the transaction which contains this date. Phase 1 validation passes as the current timestamp is inside the interval. However, the application takes the end of the interval for the current time and therefore assumes that the loan begins on 24.3. and so it should end on 27.3. This is wrong. This represents an attack of the borrower on the lender in which the loan duration can be prolonged by abusing the time handling. The severity of such an attack depends on the Cardano network parameters (described in a later section). A similar attack would be possible by a lender in the collateral liquidation case: ![TH-4](../img/th-4.png) Lender moves the upper bound of the interval and tricks the smart contract into liquidating loan before it should be liquidated. This example looks at a liquidation transaction. The lender liquidates the collateral at the red dot. The lender specifies a time interval that ends after the supposed loan duration, and because the validator looks at this upper bound value the lender can liquidate the collateral before the loan ends. Always taking the upper bound is not a good solution. In both these examples, the lower bound would be a better solution. A lower bound tells the script that the current timestamp is definitely after it, which is the information needed in these examples. A liquidation can happen only after the loan ends. You can be sure that the current time is after that by comparing the lower bound to this timestamp. In the second example, since the borrower constructs the accept-loan transaction and is incentivized to have the loan for as long as possible, the script needs to protect the lender, by taking the lower bound of the current time to determine the start of the loan. However, just because in this case the lower bound was the better option does not mean that it is always the case. Everything depends on the context in which the timestamp is used. Another intuitive option when trying to convert an interval to a specific timestamp would be to use the middle of the interval. However, this does not make any sense, an attacker can just move sides of the interval to force the middle of the range to be anywhere. Even more importantly, just because the average is intuitive it does not mean it's right, the current timestamp could be closer to the lower bound of the validity range than the average. ## Considerations There are two general tips for developers: 1) Think about the use case and incentives. The developers should think about who specifies the time interval in which transaction and what their incentives are. Usually, a cheating party could manipulate either the lower bound or the upper bound or both, and a careful developer should always check the bound that makes more sense for a given use case. It may mean using different ends of the interval for different use cases even in the same transaction. 2) Check the length of the validity range. Developers can prevent many (but not all) of these edge cases by simply checking the length of the validity range (end - begin) and enforcing that it is fairly short (e.g. 1 hour). It is also necessary to check this in case a close enough time approximation is really necessary. Even if the length of the validity range is restricted, developers still should choose the correct end of the interval for the use case, though. ## Theory meets practice Although a transaction creator could choose any validity interval for his transaction in theory, it's not quite that way in practice. The inner representation of the validity interval in the raw transaction format consists of two integers representing the number of slots. However, as shown above, the script context that smart contracts have access to lists the validity interval as two POSIX timestamps. That means that those slots need to be translated into timestamps before the scripts are run. This translation currently errors out for slots that are more than a few days in the future. That's because the translation depends on the chain's network parameters that may themselves be changed during the longer time period. A direct consequence of this is that some of the attacks on the time-related smart contract vulnerabilities are limited in severity currently, as an attacker can not select a timestamp in the further future as his transaction's upper bound. Ultimately, relying on this is not recommended as the length of this period may change without any notice. From the smart contract security point of view, you can not take the upper bound of the validity interval for the current time! For a practical hands-on experience, try to solve the Cardano [CTF](https://github.com/Invariant-0/cardano-ctf) tasks. The vesting task requires a player to perform an attack similar to the one described above. For a deeper dive into this topic, see two blogs from IOG, [1](https://iohk.io/en/blog/posts/2022/12/07/time-handling-on-cardano-part-1-about-ouroboros-and-the-importance-of-determinism/) and [2](https://iohk.io/en/blog/posts/2022/12/08/time-handling-on-cardano-part-2-use-cases/). --- ## Token Security Native tokens, introduced in the Mary protocol upgrade, represent value that can be created and traded on Cardano in addition to ADA. They can also be more than a representation of value: many tokens exist purely as technical devices inside protocols. Handling either kind safely means knowing how tokens behave inside transactions and where the pitfalls lie, which is what this page covers from a smart contract developer's point of view. Throughout, the word token covers both ADA and native tokens. ADA is a special case whose handling sometimes differs, but it behaves like a native token in most contexts. ## Basics A token on the Cardano blockchain is an asset that can be stored inside an Unspent Transaction Output (UTxO). Tokens can be minted (new tokens are created), burned (existing tokens are destroyed), or transferred. Any token is defined by two parameters, a policy ID and an asset name sometimes called its token name. The rules detailing how and if new tokens can be minted or old tokens burned is written in a minting policy. A minting policy is a smart contract and it is linked to the tokens through the tokens' policy ID (the first identifier of a token). More specifically, the policy ID of a token is a cryptographic hash of its minting policy's code. Tokens that have different minting policies are very different. Tokens that are different only in the token name are somewhat related, they are governed by the same smart contract. The policy specifies the conditions under which tokens can be minted or burned. If any token is minted or burned in a transaction, the minting policy of that token must be part of the transaction and must successfully validate the operation. However, tokens' transfer alone does not execute the minting policy and the transfer therefore can't be controlled by the token's code. Token names are additional data associated with tokens. They are set by the minting party in the minting transactions. The minting policy governs the minting of all tokens with the same policy ID, even though they can have different token names. More often than not, it governs what token names can look like and under which circumstances. Note that in a single transaction, each minting policy is run only once. If the transaction mints and/or burns several tokens with the same policy ID but different token names, the corresponding minting policy is run only once and it needs to validate all the policy's minted and burned tokens. ```aiken validator { fn mint_and_burn(_redeemer: Void, ctx: ScriptContext) -> Bool { let ScriptContext { transaction, purpose } = ctx expect Mint(own_policy_id) = purpose let Transaction { mint, ..} = transaction let mint_value = value.from_minted_value(mint) let own_tokens = dict.to_list(value.tokens(mint_value, own_policy_id)) // Allowing only mint, not burn list.all( own_tokens, fn(token) { let (asset_name, amount) = token amount > 0 }, ) } } ``` The code above shows a simple minting policy that lets anyone mint any tokens, but does not allow burning them at all. Note that the transaction.mint contains all the tokens minted or burned in the transaction. The goal is therefore to filter out only tokens governed by this minting policy whose id was extracted to the policy_id variable. The resulting list can contain a number of records. Each is a pair consisting of an asset name and an amount of tokens of that asset name that were minted (representing a positive number) or burned (representing a negative number). Finally, the final check makes sure that all the amounts are positive, meaning that no token was burned. Note that it does not restrict anything else and you could mint tokens of any token name under this policy. However, naturally, you can not mint tokens of a different policy using this code. For that, you need the other policy to validate. It is therefore possible to create tokens that: - can be minted only with a specific token name. - can be minted only once, this can be used to create NFTs. - can be minted only if another token of the same policy ID and a different name is burned. - can be minted only if enough ADA is deposited into a specified UTxO. - can be burned only if the transaction is signed by a specified key. - cannot be burned under any circumstances. Note that the policy ID of ADA is an empty string. Therefore, there is no minting policy that corresponds to it. As a consequence, it is not possible to mint or burn ADA this way. ## Tokens as value Many tokens represent value. Whether it is ADA, stablecoins, NFTs or tokens minted by promising projects, they have value and are often stored and transferred between various UTxOs. As a developer, you need to ensure that you handle these tokens correctly when designing your dApp. Assume you have created a simple treasury to which value can be freely added by anyone, but can only be redeemed in a correctly signed multi-signature transaction. For example, such treasury could be used by a DAO that's selling NFTs on a marketplace. Users buying the NFTs would pay into this treasury, and they can withdraw the tokens only if multiple members of the DAO agree on how they want to use the funds received. This protocol can be demonstrated in these two simple transactions. ![TS-1](../img/ts-1.png) What are the security risks you want to prevent when implementing this treasury? Focus on the first transaction type, a transaction in which additional value is added to the treasury. The user submitting such a transaction should not be able to withdraw anything from the UTxO so you implement a simple check that "Amount of ADA present in the output UTxO is at least the amount of ADA present in the input UTxO". This works just fine for ADA (double satisfaction vulnerability aside), but there are no restrictions placed on any other tokens. In the example with the DAO selling NFTs, the DAO decides to sell an NFT for some tokens other than ADA, and someone buys such an NFT. Suppose that the trade went through and the DAO balance was updated correctly. Anyone can retrieve those tokens simply by spending the DAO's UTxO and withdrawing the tokens. As shown above, the validator only checks that no ADA is stolen. This is an example of a vulnerability in the token handling. Here is the fix: ## Value size and execution limits The treasury validator can be edited to check the following condition: For each token type present in the input treasury UTxO, at least that amount of the same tokens must be present in the output treasury UTxO. This is better, as it guarantees that the value in the treasury UTxO only accumulates, until the DAO members withdraw from the treasury via a multi-signature scheme. But there are glaring security issues with this approach which are apparent in a broader context of the Cardano blockchain. Each transaction needs to go through two validation phases. In the second phase, the scripts are executed. To avoid unproportional spending of resources there are several limits that Cardano enforces. The first important limit in this context is the maximum value size limit. This is a protocol parameter, which is currently set to 5000 bytes. All the tokens in the value contribute towards the value size, and therefore this limits the amount of different tokens that can be held in a single UTxO. To exploit this limit, an attacker could DDoS the treasury by minting many different worthless tokens into it, thus disabling other people from contributing valuable tokens. This attack is called "dust tokens". The multi-signature parties could mitigate this issue by withdrawing the worthless tokens and thus allowing others to contribute. However, this does not prevent the attacker from repeating the attack again. The other relevant limits are the execution limits. Execution limits limit the amount of time and memory units for each transaction's script execution. Again, these limits can not be exceeded. For example, parsing the value in the transaction validation counts towards both the time and the memory execution limits. By attacking these execution limits, an attacker could block even the withdrawal operation, depending on its complexity. The withdrawal transaction can be more complicated than the depositing transaction, as it needs to find and run two validators instead of one. If the validator runs code in which the resource consumption depends on the number of tokens (e.g. for each token type as in this case), the artificially enlarged UTxO can potentially stretch the execution limits to such an extent that the withdrawal validation can not fit in the execution limits. That effectively makes it impossible for the value contained within to be withdrawn unless an (unlikely) protocol change is made, e.g. increasing the limits. ## Correct value handling Theory is cleaner than practice. How close a transaction comes to the execution limits depends on the validators it runs, how complex and efficient their implementations are, and on the transaction itself: its structure and size. There is no simple catch-all solution for token handling. The best course of action is to first think about the types of tokens that can be deposited into the UTxO. If you only really care about ADA, enforce that no other tokens are added. If you need to support multiple policies, specify the set of policy IDs in the datum and test that the validator is efficient enough that it validates even when it contains all of them at the same time. If you need to be able to support any tokens, you could limit how many different tokens can be in a single UTxO. You might require a different solution altogether, though. Rigorous testing should be in place to test whether even the most extreme edge cases allowed by your validator fit into the execution limits. Remember to try enlarging the transaction as well when testing, e.g. increase the number of normal inputs and outputs, put tokens into those artificial inputs and outputs as well, etc. ### Value normalization and zero quantities Comparing values is another place handling goes wrong. An on-chain `Value` is a normalized, canonically sorted map that by construction never holds a zero-quantity entry. The transaction's **mint field** is not normalized the same way, which is why Aiken historically exposed it as a distinct type converted with `from_minted_value` (that separate type has since been merged into the unified `cardano/assets` `Value`, but the underlying distinction remains). The risk is trusting value structure you did not normalize: an externally supplied value carrying a zero-quantity token, or a non-canonical ordering, can make a structural equality (`==`) or subset check behave wrongly, so two economically equal values compare as unequal (bypassing a guard) or a required equality can never be met (locking the UTxO). Do not compare raw value maps for equality when any part is attacker-controlled; check specific quantities with `quantity_of`, and normalize before comparing. ## Beyond value: technical tokens Value is tokens' primary purpose, but not their only one; native tokens also serve as technical building blocks inside protocols. To demonstrate one use case, consider on-chain oracles. The main goal of an oracle is to reliably bring real-world data onto the Cardano blockchain. For example, you could want to know the value of a currency, a particular stock or the results of a real-world event. To do so, a special UTxO is often created and this UTxO contains the required information in its datum. The script validator of the UTxO can easily govern how updates are performed, usually, this is by allowing only certain trusted entities to spend the oracle UTxO, often requiring multiple of them to agree on the update. However, there could be multiple UTxOs created on the same script address and they could be created by anyone with any data (see the [Missing UTxO Authentication](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/missing-utxo-authentication) page). Therefore, there is a need to recognize the correct current oracle UTxO. Suppose the oracle's purpose is to bring just a single data point to the chain, meaning there's always just a single up-to-date UTxO valid. To make a UTxO uniquely recognizable, you can create a special kind of an NFT (without an image, just for development purposes) and lock it into the correct oracle UTxO. As there can be only one such NFT (by definition, a non-fungible token), there is always at most one oracle UTxO containing this NFT. When this UTxO is used in a transaction, either as a normal input or as a reference input, any validator can easily check whether the correct NFT is present. When the oracle information needs to be renewed, the old oracle UTxO is spent and the token is sent into the newly created oracle UTxO. This is probably the most simple technical use case for tokens. This token does not hold any value, it is part of the technical design of the protocol. Implemented correctly, the above-mentioned NFT can not be withdrawn from the protocol, and thus traded, or otherwise interacted with. The only purpose this NFT serves is to uniquely identify a UTxO carrying the correct data. ## Validation tokens A fairly common use case of native tokens is to provide UTxO validation: making sure a UTxO was created correctly, wasn't tampered with, or proving authenticity of some kind. The crux of the problem is that a validator is run only when a UTxO at its address is spent, not when it is created. It is therefore very simple to create malicious UTxOs with invalid data and present them as a real thing. Without further protection, it's impossible to distinguish between them on-chain. Validation tokens fill in this missing piece of the puzzle as they can validate the initial transaction. You can enforce that the tokens can only be minted during the UTxO creation, validating that the created UTxO is correct and in the proper initial state. This validation is part of the corresponding minting policy. Note, that the minting policy has access to the whole script context, and it can therefore look at the whole transaction, including all the transaction inputs, outputs, signatories, datums, other minted tokens, etc. The minting policy of the validation token ensures that it is placed into the correct UTxO. As a result, the validator can assume that any UTxO that contains the validation token was created according to the specification. Malicious UTxOs without the validation token may still exist, so the protocol must refuse to interact with them. The idea is simple but not yet complete. As described, it creates several security pitfalls. As stated above, the validation token serves as a proof that the UTxO that holds it is correct. But this would only be the case if the UTxO was unspendable, once it's spent, the validation token could freely move away, potentially into a malicious UTxO. The spending of the UTxO is solely in the hands of the UTxO validator. The mint of the token alone is thus only a part of this design pattern. It is essential for any validator that can hold a validation token to control where and how those tokens can be transferred. Consider a situation in which the validation token is correctly minted and after a series of transactions, due to a faulty implementation of the validator holding it, it ends up in a user's wallet. That user can create a fake UTxO that pretends to belong to the protocol and include this old validation token in it. For all purposes of the validator, it will appear as a valid UTxO. Similar attacks are common in more complex smart contracts and are often critical in nature, possibly leading to a complete drain of all protocol funds. It is therefore worth repeating that it is paramount for any smart contract with a validation token to take care of these tokens. The actual implementation varies depending on the use case. However, follow these two general recommendations: 1) Count the number of validation tokens across all inputs and outputs to prevent double satisfaction attacks that could result in a "loss" of a token. The code should check that the number of input tokens and output tokens is as expected. For example, this way, you can not merge two UTxOs with validation tokens in them into a single one and claim the other validation token for nefarious uses. Remember that any single token lost to an unknown contract or to a public key address can ultimately break the whole protocol. You really need to validate every single token. Note also that tokens can be minted or burned in the same transaction if you don't restrict it. These double satisfaction attacks can be especially tricky when the minting policy is combined with the validator, an example of such an attack is as described previously above. 2) Lock the validation tokens into very specific UTxOs. Make sure to check the whole UTxO (address, value, datum) of all output UTxOs holding your validation tokens. By checking the address, you can be sure that a script you trust keeps the control of the token. By checking the datum, you make sure that you do not validate malicious data. You need to be careful of double satisfaction attacks again here (e.g. do not validate two of the same correct outputs if only one should be validated). Lastly, you should check the value of the UTxO as well; e.g. to verify that there are no dust tokens present that could potentially prevent you from further spending the UTxO by maliciously increasing the execution units. Unless the design requires it, do not allow minting new validation tokens in a transaction that already has some on its inputs; it keeps the double satisfaction reasoning simple. ## Implementation The validation token needs to end up locked in the correct UTxO. Therefore, the minting policy of the validation token needs to know the script hash of the correct validator. It can be either written directly into the minting policy or used as a parameter of it. The parametrization is a better code practice. The compiled code, however, is almost identical. Additionally, the validator needs to check that the correct validation token is part of it in later transactions. Therefore, the validator needs to know the policy ID (hash of the minting policy) so that only that special validation token is supported. Once again, it can be known by the script by putting it as its parameter. As the hashes of both the validator and the minting policy depend on each other, there is a new problem, a cyclic dependency. To know the policy ID, the script hash needs to be known and for that, the policy ID is needed. This cycle needs to be broken as the code can not be compiled as described. There are several options for how to fix this. Perhaps the most elegant is to use the token name. Recall that the token name can be set arbitrarily by the minter, assuming the minting policy allows it. Therefore, for one minting policy, there are a lot of different token names, each possibly creating a unique token type. Keep the UTxO validator parametrized by the policy ID. The minting policy won't be parametrized and it will contain the following change: the validation token can be minted into any address, but its token name needs to match the payment credential part of the address of that UTxO. The script validator then needs to check that the validation token of the policy ID set as a parameter that is present in the UTxO has the corresponding name, that it matches its own script hash. By doing that, the validator can be sure that the token was minted to the same script. Different token names are untrustworthy and need to be considered invalid. ```aiken use aiken/dict.{to_list} use aiken/transaction.{ Mint, ScriptContext, Spend, Transaction, find_input, find_script_outputs, } use aiken/transaction/credential.{ScriptCredential} use aiken/transaction/value.{from_minted_value, quantity_of, tokens} // Minting policy of the validation token validator { fn validation_token_policy(_redeemer: Void, ctx: ScriptContext) -> Bool { let ScriptContext { transaction, purpose } = ctx expect Mint(policy_id) = purpose let Transaction { outputs, mint, .. } = transaction expect [(asset_name, amount)] = to_list(tokens(from_minted_value(mint), policy_id)) when amount is { 1 -> { expect [token_output] = find_script_outputs(outputs, asset_name) and { quantity_of(token_output.value, policy_id, asset_name) == 1, // ... other checks, including: // ... double satisfaction prevention // ... initial state validation (datum, value) } } -1 -> True _ -> False } } } // Validator expects to have the correct validation token validator(validation_token_policy: ByteArray) { fn multisig(_datum: Void, _redeemer: Void, ctx: ScriptContext) -> Bool { let ScriptContext { transaction, purpose } = ctx let Transaction { inputs, outputs, .. } = transaction expect Spend(output_reference) = purpose expect Some(own_input) = find_input(inputs, output_reference) expect ScriptCredential(own_hash) = own_input.output.address.payment_credential // expect to have the correct validation token expect quantity_of( own_input.output.value, validation_token_policy, own_hash, // expected token name == this script's hash ) == 1 expect [expected_token_output] = find_script_outputs(outputs, own_hash) expect quantity_of( expected_token_output.value, validation_token_policy, expected_token_name, ) == 1 // ... other checks, including: // ... double satisfaction prevention // ... correct state transition validation (datum, value) } } ``` The result of this is that the validation tokens can be minted freely into other script UTxOs, but such tokens are worthless as they don't have the correct name. So even if they are later deposited to the original script address, the name won't match and the validator will recognize it and invalidate any following transactions. The only way to mint a valid validation token is to mint it into the correct script address straight away. ## Transaction token There is one more common use of token minting and it is frequently used when dealing with batching or any other kind of running multiple validations in the same transaction. Assume you have a simple marketplace, anyone can create an offer and this offer can be filled by anyone. This marketplace would like to support batching in a sense that more than one offer can be filled in a single transaction. The code is secured against double satisfaction attacks. The validator checks the whole transaction, gathers all the offer UTxOs and computes the overall value that needs to be sent to each seller. While this double satisfaction prevention is secure, it is inefficient. Assume there are 5 offers that are put as inputs of the transaction. The aforementioned validator is run 5 times, once for each UTxO. However, the computation is always (almost) the same as the validator looks at the whole transaction. This puts a strain on the execution limits, making the transactions more expensive and limiting the number of offers that can be filled in a single transaction before hitting the limits. It would be much more efficient if this expensive validation could be run just a single time. This can be achieved by **delegating the whole transaction-level validation to a minting policy** of an artificially created token. The transaction token is a special token whose minting policy contains the validation logic. The actual validator now only needs to check that a correct transaction token is indeed minted, meaning that the validation is run in the transaction. It therefore really just delegates its validation to the transaction token's minting policy. Note that it is just an artificially created token for only this purpose. The token itself is not important, it can be freely moved or burned. The only important aspect of the token is that its mint provides the validation so that in this example, 5 inputs just check that the minting policy is run and the policy validates the whole transaction thoroughly. This is the [transaction-level minting](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/tx-level-minter) design pattern; [Plutonomicon](https://github.com/Plutonomicon/plutonomicon), which first described it, calls it the transaction token pattern. Every route to transaction-level validation is a workaround of one kind or another; this is one of them, and the alternatives below lean on other conventions. Another way would be to forward the validation to a single script input, e.g. the first one found in the transaction inputs as the order is set for the transaction and all the scripts see it in the same order. That would mean checking whether the current validation run is of the first script input, if so, run the transaction-level validation, otherwise validating straight away. Yet another approach is the [stake validator](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) (withdraw-zero) pattern. Be aware of the convention it stands on: it relies on the ledger running a withdrawal script even for a zero-amount withdrawal, which is arguably an accident of the rules rather than a designed feature (minting zero tokens, by contrast, is filtered out and never runs a policy). A ledger-level effort to change the behavior was dropped precisely because deployed protocols already depend on it, so the pattern is widely used in practice, but it validates through a quirk, not a purpose-built mechanism. As token security is an integral part of overall Cardano smart contract security, the [Capture the Flag](https://github.com/Invariant-0/cardano-ctf) game features not one, but two levels focused on it. Check the levels `07_multisig_treasury_v2` and `09_multisig_treasury_v3`. Both of these tasks feature validation tokens. You can look at their implementation, find in what way they break the general recommendations, and try to exploit the vulnerabilities. --- ## Unchecked Inputs A validator decides using data it is handed: a datum, a redeemer, a minted value, the set of inputs, sometimes a signature made off-chain. Each of those is chosen by whoever builds the transaction, so anything the validator does not explicitly check is something an attacker gets to choose freely. This class collects the five places that assumption is most often left implicit. They run from the narrowest scope to the widest: one datum field, then the redeemer, then the value minted under a policy, then the whole input set, and finally a signature that was never bound to the protocol it authorizes. ## Arbitrary Datum > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `arbitrary-datum` **Property statement:** Correctness of the datum is checked for all legit UTxOs locked by the protocol. **Test:** A transaction can successfully lock in the protocol a legit UTxO with an arbitrary datum, making consumption in a second transaction fail. **Impact:** - Unspendable outputs - Protocol halting **Further explanation:** It could be tempting to omit checks for the datum of an output being locked in a script when this datum is not going to be explicitly used in the validation of the future spending transaction. However, this is a dangerous practice as the type of the datum carried by a UTxO locked in a validator still needs to match the datum type expected by the validator. Otherwise, a transaction trying to consume the locked UTxO will fail, even if nothing was going to be checked about the information contained in the datum. The length of a `ByteArray` field deserves the same suspicion. A credential, a key hash, or a script hash is exactly 28 bytes (Blake2b-224), but nothing forces that length on a raw `ByteArray` supplied in a datum or redeemer; it is a convention, not a type constraint. If a validator trusts such bytes as an address or key and must later build a required output paying to it, an attacker who sets the field to length 0 or an over-long value can make that output impossible to construct, permanently locking the UTxO. Validate the length of untrusted key and script hashes (`== 28`), prefer the typed `Credential` and `Address` types over raw bytes, and fail safely on malformed data. ## Other Redeemer > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `other-redeemer` **Property statement:** Logic under one script redeemer that relies on the logic enforced by another redeemer (either from the same script or from another one) explicitly requires the presence of the redeemer under which the intended logic exists. **Test:** A transaction can successfully avoid some checks by spending a UTxO or minting a token using a different redeemer than the one expected by the script. **Impact:** By-passing checks **Further explanation:** Suppose a simple staking protocol that allows users to lock a certain amount of token X and later on receive an ADA reward, which increases based on the amount of time tokens have been locked. This protocol consists of two validators, `globalValidator` and `positionsValidator`. The `globalValidator`'s mission is to lock an NFT that carries as datum the global state of the pool (e.g. how much token X has been staked in aggregate by all participants) as well as the rewards pool (ADA to be distributed to stakers) and the `positionsValidator`'s mission is to lock one UTxO per each participant, holding the user's bag of token X and carrying as datum a timestamp stating when was the last time that the position was updated. To interact with the protocol, users can either open a position or update their position. To reflect that, the validators have the following logic: - `globalValidator` has one redeemer, `UpdateState`, which checks that a user's position is opened or updated correctly and that rewards are distributed correctly based on the user's stake size and timestamp, updating the global state accordingly. - `positionsValidator` has one redeemer, `UpdatePosition`, which just checks that the NFT locked in `globalValidator` has been consumed, therefore deferring all the checking to the `globalValidator`. Some time after, a new redeemer is added to `globalValidator` to allow anyone to add ADA to the rewards pool. This new redeemer, `AddRewards`, only verifies that the consumed UTxO is locked back in the same validator keeping the same datum, but with an increased amount of ADA. By adding this new redeemer, a vulnerability has been introduced. This is because by consuming the UTxO locked in the `globalValidator` with the `AddRewards` redeemer, nothing is checked regarding the correct update of the user's position. Therefore, in the same transaction a user could freely update their position, for instance changing the timestamp to some time far away in the past. This would allow the user to, in a second transaction, fool the `globalValidator` to unlock a big chunk of the rewards pool. In order to prevent this problem, the expected redeemers should be explicitly mentioned in the scripts wherever possible. The same mistake appears across scripts, not just within one. Many protocols enforce that another script runs by referencing its hash: minting from a policy, withdrawing from a staking credential, or spending from a script hash. If a validator only checks that the other script executes but not **which redeemer constructor** it runs with, an attacker can trigger a permissive "cold" branch of that script (an admin path, a rewards-top-up path) that allows arbitrary minting, burning, or spending. The rule is the same in stronger form: do not infer which branch ran from ledger values that "must be true in that branch", assert the exact redeemer constructor of the script you are forwarding to. ## Other Token Name > From [MLabs Common Plutus Vulnerabilities](https://www.mlabs.city/blog/common-plutus-security-vulnerabilities) **Identifier:** `other-token-name` **Property statement:** A minting policy checks that the total value minted of its 'own' currency symbol does not include unintended token names. **Test:** A transaction can successfully mint a token with token name different than the intended one. **Impacts:** - Leaking protocol tokens - Unauthorised protocol actions **Further explanation:** A common coding pattern that introduces such a vulnerability can be observed in the following excerpt: ```haskell myPolicy par red ctx = do ... assetClassValueOf txInfoMint ownAssetClass == someQuantity ... ``` Note that on Cardano, a token is defined by its asset class, which consists of two parts: the currency symbol and the token name. The currency symbol is the hash of the minting policy containing the rules controlling the minting and burning of the token. The token name can be any string with a maximum length of 32 bytes. The above minting policy checks that a specific asset class is found within the value minted by the transaction. Trusting that the minting policy is controlling that only someQuantity of tokens with the currency symbol controlled by the minting policy ('own' currency symbol) are being minted would be a big mistake. This is because the minting policy is only checking that someQuantity of tokens with 'own' currency symbol and a specific token name are being minted, but nothing is checked for other token names. Therefore, someone could maliciously mint a token with a different token name and use it, for instance, to impersonate the owner of the legit token. The most straight-forward coding pattern to use in order to prevent such a vulnerability can be observed in the following excerpt: ```haskell myPolicy rmr ctx = do ... txInfoMint == (assetClassValue ownAssetClass someQuantity) ... ``` The fixed minting policy checks that only someQuantity of tokens are being minted, and all of them have the same asset class. Of course, this might be too restrictive if tokens with other currency symbols need to be minted in the same transaction. If this is the case, a slightly more complex solution will be needed. --- ### Related: Infinite Mint > From [Mesh Bad Contracts](https://github.com/MeshJS/mesh) Infinite mint is the same bug seen from the minting side: a policy that does not strictly bound what it mints lets an attacker mint more tokens than intended in a single transaction. It usually comes from a validator that checks *that* a particular token was minted without prohibiting other tokens under the same policy. It is dangerous when an application trusts a policy ID for authentication, because an attacker can then put an uncontrolled supply of that policy's tokens into circulation and use them to forge that trust. #### Code examples - [Mesh: Infinite Mint Example](https://github.com/MeshJS/mesh/tree/main/packages/mesh-contract/src/giftcard/infinite-mint) ## Missed Input Validation > Concerns the utxo-indexer pattern documented by [Anastasia Labs design patterns](https://github.com/Anastasia-Labs/design-patterns). **Identifier:** `missed-input` **Property statement:** Every script input is provably validated: each spend binds its redeemer index to its own output reference, and the global validator checks that the indices cover the complete set of relevant inputs. **Test:** A transaction spends a script input that is never referenced by the indexed validation, so its rules are never enforced. **Impact:** - Inputs spent without their validation running - Value leaked from the protocol **Further explanation:** The **utxo-indexer** pattern makes global validation cheap. Instead of every input independently scanning the whole transaction (which is O(n²) as inputs grow), a single validator, a minting policy or a withdraw-zero staking script, validates the transaction once, and each spend input simply defers to it. The redeemer carries indices that pair each input to its corresponding output, so the global validator does O(1) lookups at the claimed positions rather than searching. The pattern has two footguns, both variants of trusting the indices without binding them to reality: - A spend validator that only checks "the global validator ran" (the staking script is present in the withdrawals, or the policy is in the mint field) without confirming that **its own input** is part of the index set the global validator processed. An attacker attaches an extra input at the same script address that the indices never reference. The global validator validates the inputs it was pointed at; the extra one rides along, spent with no rules enforced. - A global validator that trusts the redeemer indices without checking they cover **every** relevant input, with no gaps, no duplicates, and a count that matches. Indices that quietly skip an input let that input escape validation. This is closely related to [double satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction), where the pairing between inputs and outputs is what breaks. Prevent it by having each spend assert that its redeemer index points at its own output reference (using the script context's own-input information), and by having the global validator validate the complete input set, confirming that the input and output at each claimed index are really there. ## Missing Signature Domain Separation > Demonstrated in the [Invariant0 Banking Series CTF](https://github.com/Invariant-0/cardano-ctf) (levels 11-13); see also the replay weakness documented in [CIP-8](https://cips.cardano.org/cip/CIP-8). **Identifier:** `signature-domain-separation` **Property statement:** Every off-chain signature a validator accepts commits to who signed it, which protocol and instance it authorizes, and a value that is consumed on use, so a signature cannot be replayed against another key, another protocol, or the same protocol twice. **Test:** A transaction successfully authorizes an action with a signature that was produced for a different key, a different protocol or script instance, or that was already used in an earlier transaction. **Impact:** - Unauthorized withdrawals or state changes - Cross-protocol and repeated replay of a single signed intent **Further explanation:** Some protocols accept an off-chain authorization: an account owner signs a message with their Ed25519 private key, shares it (by message, email, in person), and anyone holding it can submit a transaction that redeems it on-chain, where a validator checks the signature with `verify_ed25519_signature`. This is the "cheque" or meta-transaction pattern. Getting the signature check right means binding three things into the signed message, and each one is a separate way to get it wrong: - **Whose signature is it?** Verifying that a signature is valid for a public key provided in the redeemer proves nothing if the attacker also chose the key. The signer's identity must be checked against something the attacker cannot control, such as the owner's key hash stored in the datum. - **What is it for?** A message that carries only an amount is valid anywhere the same shape is accepted. Without a **domain separator**, a protocol identifier (script hash or policy id) and the network, the same signed intent replays across every protocol and every deployment that accepts it. - **Can it be used twice?** A message with no unique, consumed element (a nonce, an incrementing id, an output reference, or an expiry) is a permanent pass: it can be redeemed repeatedly as long as the account has funds. The Banking CTF walks these three failures in order: one level verifies the signature against an attacker-supplied key (identity not bound), the next omits any id or nonce (unlimited reuse), and the last adds an id but leaves it out of the signed message, so one signature is valid for any id. That last case is the subtle one: **every security-relevant field must be part of what is signed.** Checking a field that the signature does not cover authenticates nothing. Prevent this by signing over the full tuple that defines the intent, a domain tag, the script or policy id, the network, the account, a unique nonce, and the amount, verifying the signer against the datum, and invalidating the nonce on-chain when the signature is used. --- ## Smart Contract Security Smart contract bugs are uniquely dangerous: deployed validators are immutable and they often guard significant value, so a single vulnerability can mean irreversible loss of funds. For web2 developers, the shift is stark: a bug here isn't an embarrassing hotfix, it's a permanent financial loss in an adversarial environment where anyone in the world can attempt the exploit. The good news: Cardano's [eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo) eliminates several of the worst attack classes by design. The rest you handle with careful validator logic and established patterns. This page covers what the platform protects you from, what it doesn't, and how to write validators that hold up. > Network-level threats (51%, long-range, eclipse attacks) target consensus, not your contract. Cardano's Ouroboros protocol defends against those. See [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros). The rest of this page is about application-level security. Your security instincts transfer directly: - **Reentrancy is like CSRF**: an action triggered at an unexpected point because origin/state wasn't verified. Cardano eliminates it structurally, the way `SameSite` cookies and CSRF tokens address it on the web. - **Datum hijacking is like SQL injection**: manipulated input data changes the meaning of an operation. Both are prevented by validating all data at the boundary; never trust that it's well-formed or authorized. - **Double satisfaction is like IDOR (insecure direct object reference)**: referencing someone else's resource to satisfy your own check. Prevention requires ensuring the resource you validate actually belongs to you. - **Audits are like penetration testing**: you'd pen-test a web app before launch; you audit a contract before mainnet. The irreversibility makes it even more critical. - **Formal verification is type systems on steroids**: not just "is this a number?" but "can this balance ever go negative?", proven for all inputs. ## What the eUTXO model protects you from ### Reentrancy is impossible The **reentrancy attack** is the most famous smart contract vulnerability, responsible for the 2016 DAO hack that drained tens of millions of dollars in ETH. In Ethereum's account model, a contract can call another contract, which can call back into the original before the first call finishes, exploiting state that hasn't been updated yet. **Cardano is structurally immune.** In the eUTXO model a transaction is a complete, atomic unit. A validator runs once per input, deciding whether that UTXO can be spent under the given conditions. There is no notion of a contract "calling" another contract mid-execution. The whole transaction, all inputs, outputs, and script runs, is validated as one unit: everything succeeds or everything fails. There is no mid-execution state for a reentrant call to exploit. ### Double-spending is prevented at the protocol level The ledger tracks every unspent output and removes it the instant it's consumed, so any second attempt to spend the same output is structurally invalid. ```mermaid graph TD UTXO_A["UTXO_A: 1,000 ADA (unspent)"] --> TX1["Transaction 1:\nspends UTXO_A"] TX1 --> UTXO_C["UTXO_C: 800 ADA (new)"] TX1 --> UTXO_D["UTXO_D: 200 ADA (new)"] UTXO_A -.->|"attempt to spend again"| TX2["Transaction 2:\nREJECTED"] TX2 -.->|"UTXO_A no longer exists"| FAIL[Invalid transaction] ``` This is simpler and more robust than the account model, where double-spend prevention relies on nonce tracking and careful state management. Here it's structural, not procedural. ### Determinism removes MEV levers On Ethereum a transaction's outcome depends on global state at execution time, which can differ from construction time: the root of **MEV** (Maximal Extractable Value), where block producers profit by reordering, inserting, or censoring transactions. On Cardano, transaction outcomes are [fully deterministic](/docs/developers/curriculum/smart-contracts/overview#deterministic-validation). A transaction names its exact inputs and outputs; if those inputs still exist when it reaches the chain, it executes exactly as built, otherwise it simply fails: no partial execution, no surprise. This removes entire categories of front-running and MEV attacks. ### Native assets share the ledger's security On Ethereum, tokens are smart contracts (ERC-20), and every token contract is its own attack surface. On Cardano, [native assets](/docs/developers/curriculum/native-tokens/overview) are handled by the ledger itself and share ADA's guarantees. The minting policy controls creation, but once tokens exist there is no token contract to exploit. ## Vulnerabilities you still have to guard against The platform removes some attacks; the rest are your responsibility. These are the big ones, each has a deep-dive in the [vulnerability reference](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview). ### Datum hijacking Occurs when a script output doesn't properly validate the datum attached to it, letting an attacker substitute a malicious datum that changes ownership or another critical field in the continuing UTXO. ```text Normal flow: Input UTXO: [Script Address, Datum: {owner: "Alice", amount: 100}] Output UTXO: [Script Address, Datum: {owner: "Alice", amount: 80}] (Alice withdrew 20) Attack: Input UTXO: [Script Address, Datum: {owner: "Alice", amount: 100}] Output UTXO: [Script Address, Datum: {owner: "Attacker", amount: 100}] (owner changed!) ``` **Prevention**: explicitly check that the output datum meets all expected constraints: immutable fields (like ownership) unchanged, mutable fields (like balances) changed only per the allowed rules, and the datum structure matching the expected schema. See [arbitrary datum](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#arbitrary-datum). ### Double satisfaction Occurs when a single output satisfies the conditions of *multiple* validators in the same transaction, letting an attacker fulfill two scripts' requirements with one output instead of two. ```text Script A (DEX pool): "valid if an output contains 100 USDx" Script B (lending): "valid if an output contains 100 USDx" Attacker's transaction: Inputs: DEX pool UTXO (A), lending pool UTXO (B) Outputs: ONE output with 100 USDx Both A and B see the 100 USDx output and consider themselves satisfied, but only one output exists. The attacker pays once for two obligations. ``` **Prevention**: tag outputs with a unique identifier (a state/beacon token) and validate that *your specific* output exists, rather than that "some output" meets the condition. See [double satisfaction](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/double-satisfaction). ### Token forgery A carelessly written minting policy can let an attacker mint unauthorized tokens: missing authorization checks, a "one-time" NFT policy that can actually run twice, or unvalidated policy parameters. The correct one-shot pattern ties minting to consuming a specific UTXO, which can never exist again: ```text Policy: "minting allowed ONLY if this specific UTXO is consumed as input" Tx 1 (mint): Inputs: [UTXO_Unique_123] Mints: [1 MyNFT] <- UTXO consumed Tx 2 (re-mint): Inputs: [???] Mints: [1 MyNFT] <- FAILS, UTXO gone ``` See [token security](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/token-security) and [other token name](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/unchecked-inputs#other-token-name). ### Resource exhaustion Validators have [ExUnits budgets](/docs/developers/curriculum/smart-contracts/choose-a-language#what-you-pay-for-execution-costs). An attacker can craft transactions that approach the limits, creating denial-of-service conditions for a protocol. Be conscious of worst-case execution cost; use parameterized scripts, bound loop iterations, and pre-compute expensive work off-chain. See [unbounded inputs](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/resource-exhaustion#unbounded-inputs) and related entries. ### Locked value Not every failure is an exploit. **Locked value** is a design where funds become permanently stuck in a UTXO with no way to spend them, the on-chain equivalent of burning them. Sometimes that is intentional: an untamperable UTXO can serve as a single, provable source of truth that no one, including its creator, can alter. The question is whether the value it traps is worth that guarantee. In Mesh's [Plutus NFT example](https://github.com/MeshJS/mesh/tree/main/packages/mesh-contract/src/plutus-nft/locked-value) only about 2 ADA stays locked in the oracle UTXO, an acceptable tradeoff rather than a bug. Weigh the economics before adopting a design that locks value: how much is trapped, and what the permanence buys you. ## Common security patterns Experienced Cardano developers reach for the same defensive patterns: - **State / beacon token**: require a unique NFT (minted with a one-time policy) in every UTXO at a script address. This prevents rogue UTXOs at the address and solves double satisfaction. - **Value-preservation check**: explicitly verify that total value in script outputs equals the expected value (inputs minus authorized withdrawals plus authorized deposits). Never rely on implicit preservation. - **Datum-continuity validation**: when a script UTXO continues (is consumed and recreated with updated state), validate *every* field of the output datum against the transition rules. Never assume the datum is correct just because it's present. - **Deadline enforcement**: use the transaction's [validity range](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/time-handling) for time-based conditions; it's checked at the protocol level, giving reliable time bounds. - **Minimal on-chain logic**: every line of on-chain code is potential attack surface. Keep validators small and focused; move complex logic off-chain and check only the critical invariants. ## Practice on a real target: the CTF The best way to internalize these is to attack them. The [Smart Contract CTF](/docs/developers/curriculum/smart-contracts/security/ctf) is an interactive Capture-the-Flag where you exploit deliberately vulnerable validators: the fastest way to develop an attacker's eye for your own code. ## Verification: testing, PBT, and audits Defense in depth, from cheapest to strongest: - **Unit tests** find the bugs you thought of. See [Testing](/docs/developers/curriculum/smart-contracts/testing). - **Property-based testing** generates thousands of random inputs against invariants like "no transaction can extract more value than was deposited" or "only the owner can withdraw", catching edge cases you'd never enumerate by hand. - **Audits** by specialized firms are standard practice before mainnet for any contract holding real value. - **Formal verification** proves a property holds for *all* inputs, not just the ones you tried. The last two are detailed below. ### Audits Testing and property-based checks find the bugs you thought of; an audit is where people whose job is to break contracts look for the ones you didn't. On Cardano the stakes are high in a specific way: a script's address *is* the hash of its compiled code, so a deployed validator cannot be changed. A protocol can be designed to evolve, by migrating users to a new script address or by delegating logic to a script hash it reads from state it controls, but that has to be built in before launch, not added after a bug. The security model resembles hardware more than software: once a faulty component ships, recalling it can be very difficult or impossible. #### What an audit checks A vulnerable validator can lead to money being stolen from the protocol or its users, protocol-only tokens being leaked, funds becoming permanently locked, or the protocol being stalled by a denial-of-service under the UTxO model. An audit exists to catch those outcomes before they happen: auditors confirm the contract behaves as intended, is resistant to malicious exploitation, and protects user funds. Because Cardano contracts often coordinate several UTxOs and scripts in one transaction, much of the work is reasoning about subtle interactions between components, exactly where the [vulnerabilities in this catalog](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview) tend to hide. #### How to prepare The single biggest lever you control is how ready the codebase is when the auditors arrive. Time they spend reconstructing what the protocol is supposed to do is time not spent finding bugs. Provide: - **A specification of intended behavior, independent of the code.** State the use cases, the assumptions and invariants, and the expected interactions between contracts. Without a spec that is separate from the implementation, auditors cannot tell intentional behavior from a bug, they only see what the code does, not what it should do. - **A runnable test suite** covering the core on-chain logic with unit tests, realistic transaction flows with property-based or scenario tests, and edge cases (minimum-ADA boundaries, unexpected datum values). Auditors verify behavior by writing and modifying tests, so a suite they can run and extend lets them explore quickly. - **A reproducible build.** Simple, documented steps to compile and deploy, so an auditor can make a small change to the on-chain code and run a test against it. This matters most when investigating a complex vulnerability. The length of the first phase is roughly inversely proportional to the quality of this material. Shortening it is in your interest: every hour saved there is an hour available for deeper analysis. #### The process An audit usually runs in four phases. ```mermaid graph LR A[Understandthe codebase] --> B[Securityanalysis] B --> C[Preparethe report] C --> D[Reviewthe fixes] style A fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style B fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style C fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style D fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` 1. **Understand the codebase.** Auditors study the documentation and the code until they know how the protocol is intended to work and how it works under the hood, and confirm the test suite runs. This is where good preparation pays off. 2. **Security analysis.** They first check whether the common vulnerability classes apply to this code, writing tests to confirm each finding (a vulnerability is considered confirmed once a test demonstrates it), then move to protocol-design issues, the mathematical assumptions behind incentives and fees, and the parameters used to instantiate the contracts on-chain. Confirmed issues are communicated as they are found, though it is sometimes better to hold a fix until the whole picture is clear, since vulnerabilities can combine and a complete view often leads to a simpler, more efficient fix. 3. **Prepare the report.** Findings are compiled into a report: a summary of each issue with suggested fixes, plus context, disclaimers, and a description of each issue type. By this point most issues have already been raised informally with the team. 4. **Review the fixes.** After the team addresses the issues, the auditors verify each fix is the suggested one or equally effective and introduces no new problems, and record the outcome of every issue. Only then is the report final and ready to share publicly. #### Certification standards Cardano has a community standard for how audits are conducted and certified: **[CIP-52 (Cardano Audit Best Practice Guidelines)](https://cips.cardano.org/cip/CIP-52)**. It defines three assurance levels a project can target: - **Level 1**: automated tooling and static analysis. - **Level 2**: a manual audit by an independent team. - **Level 3**: formal verification of critical properties (see [Formal verification](#formal-verification) below). There is no mandatory audit registry on Cardano; certification runs through the auditors and certification services themselves. [CIP-96](https://github.com/cardano-foundation/CIPs/pull/499) proposes an on-chain standard for publishing certification metadata (audit reports, test results, formal proofs), but it remains a draft rather than an adopted mechanism. ### Formal verification Testing shows a validator works on the cases you tried; **formal verification** proves it holds for all of them. Cardano's own ledger specification is formalized in Agda, and the Haskell and Aiken ecosystems are well suited to these techniques, so for high-value contracts machine-checked proofs are the strongest guarantee you can give. **Blaster.** [Blaster](https://github.com/input-output-hk/Lean-blaster) is proof automation for [Lean 4](https://lean-lang.org/): you hand it a theorem and it returns a proof, or a counterexample that shows why it is wrong. It simplifies the goal through a series of algebraic rewriting passes, emits a minimal SMT-Lib query, and discharges it with an SMT solver, so you can close goals with a single `blaster` tactic instead of writing proofs by hand. :::info In active development Blaster is under active development and not yet generally available. You can track progress and follow the documentation at the [Lean-blaster repository](https://github.com/input-output-hk/Lean-blaster). This page will be expanded as the tooling matures. ::: ## Key takeaways - **Cardano removes whole attack classes by design**: reentrancy is impossible, double-spends are structurally prevented, determinism removes entire categories of MEV, and native assets share the ledger's security. - **What remains is yours to handle**: datum hijacking, double satisfaction, token forgery, and resource exhaustion all come down to validating the whole transaction carefully. - **Use established patterns**, state tokens, value preservation, datum continuity, deadline enforcement, minimal logic, rather than inventing your own. - **Verify in layers**: unit tests, property-based testing, and an audit for anything holding real value. ## Next steps - [Vulnerability reference](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview): the full catalog with deep-dives - [Smart Contract CTF](/docs/developers/curriculum/smart-contracts/security/ctf): practice exploiting and fixing vulnerable validators - [Testing](/docs/developers/curriculum/smart-contracts/testing): build the test suite that catches these before deployment --- ## Testing Validators Because a Cardano validator is a [pure function](/docs/developers/curriculum/smart-contracts/overview#smart-contracts-are-validators-not-actors), `f(datum, redeemer, context) -> Bool`, it is unusually easy to test. There is no network, no global state, no deployment required: you hand the function some mock data and assert the result. And because deployed validators are immutable and guard real value, testing is not optional. This page covers on-chain testing in [Aiken](/docs/developers/curriculum/smart-contracts/choose-a-language), which ships a test runner in the toolchain. The same ideas apply to other languages. ## The test runner Define test functions with the `test` keyword, then run `aiken check` from the project root to execute every test it finds. A test passes when it returns `True`: ```aiken test always_true() { True } ``` `aiken check` is the Aiken equivalent of `npm test`: it discovers and runs all `test` functions in the project. Three keywords do most of the work: - **`expect`** enforces an exact pattern match on a value and crashes if the shape doesn't match, like a runtime schema check. For example, if `inputs_with_policy(reference_inputs, oracle_nft)` returns a list that should contain exactly one item, `expect [oracle_ref_input] = ...` safely destructures it. - **The `?` operator** is a tracing operator: when a validator fails, it reports which condition was `False`. Writing `is_app_owner_signed?` means a failure prints `is_app_owner_signed?`, pointing you straight at the broken check. - **`test ... fail`** marks a test as *expected to fail*. It passes only if the validator crashes or returns `False`, the equivalent of `expect(...).toThrow()`. ## A contract to test Take a withdrawal validator with two user actions, `ContinueCounting` (verify the owner signed, the app hasn't expired, and the count incremented by one) and `StopCounting` (verify the owner signed and the state-thread token is burned): ```aiken use aiken/crypto.{VerificationKeyHash} use cardano/address.{Address, Credential} use cardano/assets.{PolicyId, without_lovelace} use cardano/certificate.{Certificate} use cardano/transaction.{Transaction} use cocktail.{ input_inline_datum, inputs_at_with_policy, inputs_with_policy, key_signed, output_inline_datum, outputs_at_with_policy, valid_before, } pub type OracleDatum { app_owner: VerificationKeyHash, app_expiry: Int, spending_validator_address: Address, state_thread_token_policy_id: PolicyId, } pub type SpendingValidatorDatum { count: Int, } pub type MyRedeemer { ContinueCounting StopCounting } validator complex_withdrawal_contract(oracle_nft: PolicyId) { withdraw(redeemer: MyRedeemer, _credential: Credential, tx: Transaction) { let Transaction { reference_inputs, inputs, outputs, mint, extra_signatories, validity_range, .. } = tx expect [oracle_ref_input] = inputs_with_policy(reference_inputs, oracle_nft) expect OracleDatum { app_owner, app_expiry, spending_validator_address, state_thread_token_policy_id } = input_inline_datum(oracle_ref_input) expect [state_thread_input] = inputs_at_with_policy(inputs, spending_validator_address, state_thread_token_policy_id) let is_app_owner_signed = key_signed(extra_signatories, app_owner) when redeemer is { ContinueCounting -> { expect [state_thread_output] = outputs_at_with_policy(outputs, spending_validator_address, state_thread_token_policy_id) expect input_datum: SpendingValidatorDatum = input_inline_datum(state_thread_input) expect output_datum: SpendingValidatorDatum = output_inline_datum(state_thread_output) let is_app_not_expired = valid_before(validity_range, app_expiry) let is_count_added = input_datum.count + 1 == output_datum.count let is_nothing_minted = mint == assets.zero is_app_owner_signed? && is_app_not_expired? && is_count_added && is_nothing_minted? } StopCounting -> { let state_thread_value = state_thread_input.output.value |> without_lovelace() let is_thread_token_burned = mint == assets.negate(state_thread_value) is_app_owner_signed? && is_thread_token_burned? } } } } ``` The validator reads the oracle config from a reference input, finds the state-thread token's input and output, and checks the state transition. To test it, we need to construct realistic mock transactions. ## Building mock transactions with mocktail Building all the required Aiken types by hand is tedious. The `mocktail` module (from the `vodka` library) provides builders: start with `mocktail_tx()` for an empty transaction, chain modifier functions to add the pieces your test needs, and finish with `complete()`. ```aiken fn mock_continue_counting_tx() -> Transaction { mocktail_tx() |> ref_tx_in(True, mock_tx_hash(0), 0, mock_oracle_value, mock_oracle_address) |> ref_tx_in_inline_datum(True, mock_oracle_datum) |> tx_in(True, mock_tx_hash(1), 0, mock_state_thread_value, mock_spending_validator_address) |> tx_in_inline_datum(True, mock_datum(0)) |> tx_out(True, mock_spending_validator_address, mock_state_thread_value) |> tx_out_inline_datum(True, mock_datum(1)) |> required_signer_hash(True, mock_app_owner) |> invalid_hereafter(True, 999) |> complete() } test success_continue_counting() { complex_withdrawal_contract.withdraw( mock_oracle_nft, ContinueCounting, Credential.Script(#""), mock_continue_counting_tx(), ) } ``` This is a test fixture factory: you build a fake transaction the same way you'd build a mock HTTP request with headers, body, and auth. ## The boolean-toggle pattern The real power comes from the boolean parameter on each builder method, which includes or excludes a piece of the transaction. Define a struct of booleans, one per validation condition, and the success test sets them all `True`, while each failure test flips exactly one to `False`. This isolates a single failure mode per test, the way you'd write one web2 test each for "missing auth header", "expired token", "malformed body". ```mermaid flowchart TD TC["ContinueCountingTest struct\n(one bool per condition)"] --> BASE["mocktail_tx()"] BASE -->|"is_ref_input_presented"| REF["ref_tx_in(): oracle reference"] REF -->|"is_thread_input_presented"| TXI["tx_in(): state thread input"] TXI -->|"is_thread_output_presented"| TXO["tx_out(): state thread output"] TXO -->|"is_count_added"| DAT["datum: count 0 or 1"] DAT -->|"is_app_owner_signed"| SIG["required_signer_hash()"] SIG -->|"is_tx_not_expired"| EXP["invalid_hereafter()"] EXP --> DONE["complete()"] DONE --> VAL["withdraw() -> True / False"] ``` ```aiken type ContinueCountingTest { is_ref_input_presented: Bool, is_thread_input_presented: Bool, is_thread_output_presented: Bool, is_count_added: Bool, is_app_owner_signed: Bool, is_tx_not_expired: Bool, } fn mock_continue_counting_tx(test_case: ContinueCountingTest) -> Transaction { let ContinueCountingTest { is_ref_input_presented, is_thread_input_presented, is_thread_output_presented, is_count_added, is_app_owner_signed, is_tx_not_expired } = test_case let output_datum = if is_count_added { mock_datum(1) } else { mock_datum(0) } mocktail_tx() |> ref_tx_in(is_ref_input_presented, mock_tx_hash(0), 0, mock_oracle_value, mock_oracle_address) |> ref_tx_in_inline_datum(is_ref_input_presented, mock_oracle_datum) |> tx_in(is_thread_input_presented, mock_tx_hash(1), 0, mock_state_thread_value, mock_spending_validator_address) |> tx_in_inline_datum(is_thread_input_presented, mock_datum(0)) |> tx_out(is_thread_output_presented, mock_spending_validator_address, mock_state_thread_value) |> tx_out_inline_datum(is_thread_output_presented, output_datum) |> required_signer_hash(is_app_owner_signed, mock_app_owner) |> invalid_hereafter(is_tx_not_expired, 999) |> complete() } ``` The success test sets every field `True`. Each failure test flips one: ```aiken test fail_continue_counting_no_ref_input() fail { let test_case = ContinueCountingTest { is_ref_input_presented: False, // the only difference is_thread_input_presented: True, is_thread_output_presented: True, is_count_added: True, is_app_owner_signed: True, is_tx_not_expired: True, } complex_withdrawal_contract.withdraw( mock_oracle_nft, ContinueCounting, Credential.Script(#""), mock_continue_counting_tx(test_case), ) } ``` The pattern scales cleanly: when you add a validation condition to the contract, you add one boolean to the struct, set it `True` in the success test, and write one new failure test with it `False`. If you have written web2 tests, the workflow is familiar. `aiken check` discovers and runs every `test` function the way `npm test` or `bun test` does. You build a fake transaction with `mocktail_tx()` and a builder chain, much like assembling a request from a fixture factory. A `test ... fail` marks a test you expect to fail, like `expect(...).toThrow()`, and the boolean-toggle struct shown above plays the role of `test.each()` or table-driven tests, flipping one condition at a time so each test isolates a single failure. Production suites push the same idea past booleans: an **options record** whose fields are all `Option` types, a `default_options()` giving one canonical happy-path transaction, and each negative test overriding exactly one field with `Some(bad_value)` via record spread, `Options { ..default_options(), edit_fee: Some(0) }`. The default builder is written once, every test documents one invariant, and a new validator check costs one field plus one `fail` test. A second production idiom is **golden vectors**: tests whose only job is to `trace` the canonical CBOR of each datum and redeemer (`cbor.serialise` piped through `bytearray.to_hex`), so off-chain integrations and third parties can pin the exact bytes your contract expects rather than re-deriving them from source. ## Property-based testing The boolean-toggle pattern tests the failure cases you already thought of: every test names one condition you knew to check. Property-based testing inverts that. Instead of listing cases, you state an invariant that must hold for *every* input, then let the test runner generate hundreds of random inputs trying to break it. A unit test asks "does this specific transaction pass?"; a property asks "is there *any* input that violates this rule?". Good invariants read like the contract's real guarantees: no transaction releases more value than it locks, only the owner can withdraw, a counter only ever moves up by one. This is how you catch the boundary and double-satisfaction bugs you never wrote a case for, the classes catalogued in [Security](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview). Aiken's runner has this built in. A `test` whose argument is drawn `via` a fuzzer runs many times over, each with a fresh generated value, and when it finds a counterexample it *shrinks* it to the smallest input that still fails before reporting: ```aiken use aiken/fuzz // A vault's rule: withdraw a positive amount, never more than the locked balance. fn may_withdraw(locked: Int, amount: Int) -> Bool { amount > 0 && amount <= locked } // Property: for any amount the fuzzer generates, negative, zero, or larger than // the balance, an authorized withdrawal never leaves the vault overdrawn. test prop_never_overdraws(amount via fuzz.int()) { let locked = 1_000_000 if may_withdraw(locked, amount) { locked - amount >= 0 } else { True } } ``` `fuzz.int()` feeds every kind of integer through the rule, including the negatives and off-by-one boundaries a hand-picked case tends to skip. If a change let `may_withdraw` accept an amount above the balance, the runner shrinks the failure to the minimal breaking value (`1_000_001`) rather than a random large one, pointing you straight at the boundary. You build fuzzers for whole transactions the same way, composing the `aiken/fuzz` and `cardano/fuzz` primitives. The same fuzzer has a second, distinct use: [Optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization#using-fuzzer) uses it to generate *fixtures*, arbitrary-but-valid transaction parts to stand a test up. The difference is what you do with the generated value: a fixture is scaffolding for one case, a property is an assertion checked across thousands. ## Testing your off-chain code Your validator isn't the only thing that needs tests. The transaction-building code that locks, spends, and mints deserves them too, and it splits into two kinds of test, both covered in Module 2. **Unit tests** exercise the pure parts, datum and schema encoding, address parsing, and the shape of the transaction you build, with no chain at all, against the [in-memory emulator](/docs/developers/curriculum/start-building/local-testing#the-in-memory-emulator). **Integration tests** drive the whole build → sign → submit → confirm lifecycle against a [programmatic devnet](/docs/developers/curriculum/start-building/local-testing#programmatic-devnets): fund a wallet from genesis, submit, and assert on confirmation, with millisecond confirmations and fresh isolated state per run, offline and with no faucet. ## Audits Testing finds the bugs you thought of; audits find the ones you didn't. For any contract holding significant value, a professional audit is standard practice. See [Audits](/docs/developers/curriculum/smart-contracts/security#audits) for the process and how to prepare for one. ## Next steps - [Security](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/overview): the vulnerability classes your tests should target - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): exercise the validator end to end - [Contract library](/templates/contracts): read tested, production-grade validators --- ## Write a Validator You've [picked a language](/docs/developers/curriculum/smart-contracts/choose-a-language). Now you write the on-chain code: the validator. Remember the mental model: a validator is a **gatekeeper** that receives a transaction and returns `True` or `False`. It never moves funds or mutates state; it only decides whether a transaction is allowed. This page covers writing validators in Aiken, the simpler native-script alternative for multisig and time-locks, and the blueprint that connects your validator to off-chain code. The deep treatment of the three arguments a validator receives is in [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context); this page focuses on authoring. If you have written web middleware, this is familiar: a validator is like route middleware or an auth guard, a pure function that returns allow or deny without mutating state. The redeemer is the request body it branches on (`MintToken` vs `BurnToken`), and the blueprint (`plutus.json`) is the contract's OpenAPI spec that tools read to generate a typed client. ## What a validator sees Building validators means reasoning about transactions. A validator can inspect the whole `Transaction` it's validating (the full type is in the [Aiken stdlib](https://aiken-lang.github.io/stdlib/cardano/transaction.html)): its `inputs` and `outputs`, `reference_inputs`, `mint`, `extra_signatories`, and `validity_range`. For what each field means, see the [field breakdown in Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context#the-transaction-as-the-validator-sees-it); the examples below show how you read them in Aiken. ## The main validator types Most validators are one of a handful of types, distinguished by what triggers them: ```mermaid flowchart TD TX["Submitted transaction"] TX -->|"mints or burns under this policy"| MINT["mint: minting validator"] TX -->|"spends a UTXO at the script address"| SPEND["spend: spending validator"] TX -->|"withdraws rewards from the script stake address"| WITHDRAW["withdraw: withdrawal validator"] TX -->|"publishes a certificate for the script stake credential"| PUB["publish: certificate validator"] MINT --> R["returns True / False"] SPEND --> R WITHDRAW --> R PUB --> R ``` ### Minting validator Runs when a transaction mints or burns tokens under the validator's policy. The simplest possible one: ```aiken use cardano/assets.{PolicyId} use cardano/transaction.{Transaction, placeholder} validator always_succeed { mint(_redeemer: Data, _policy_id: PolicyId, _tx: Transaction) { True } else(_) { fail @"unsupported purpose" } } test test_always_succeed_minting_policy() { always_succeed.mint(Void, #"", placeholder) } ``` The validator's hash is the **policy ID** of the tokens it controls. Make it useful by adding a parameter (the owner's key) and a redeemer (which action), so minting requires the owner's signature before a deadline, and burning is always allowed: ```aiken pub type MyRedeemer { MintToken BurnToken } validator minting_policy(owner_vkey: VerificationKeyHash, minting_deadline: Int) { mint(redeemer: MyRedeemer, policy_id: PolicyId, tx: Transaction) { when redeemer is { MintToken -> { let before_deadline = valid_before(tx.validity_range, minting_deadline) let is_owner_signed = key_signed(tx.extra_signatories, owner_vkey) before_deadline? && is_owner_signed? } BurnToken -> check_policy_only_burn(tx.mint, policy_id) } } else(_) { fail @"unsupported purpose" } } ``` Helpers like `key_signed`, `valid_before`, and `check_policy_only_burn` come from the [vodka](https://github.com/sidan-lab/vodka) library. Minting policies are also covered, with off-chain minting, in [Native tokens > Minting policies](/docs/developers/curriculum/native-tokens/minting-policies). Note that `key_signed` checks that the owner's key is *among* the signatories, not that it is the only one. Prefer that membership form over exact equality like `[owner_vkey] == tx.extra_signatories`: requiring the signatory list to equal exactly one key means no other script in the same transaction can add its own required signer, so the validator stops composing with anything else. #### One-shot policies The owner-signature policy above can mint repeatedly. For a true NFT you want a policy that can succeed **exactly once in history**. The trick is to parameterize the policy by a specific UTXO and require that UTXO to be spent when minting. Because a UTXO can be consumed only once, the policy can fire only once: ```aiken use aiken/collection/list use cardano/assets.{PolicyId} use cardano/transaction.{Input, OutputReference, Transaction} validator one_shot(utxo_ref: OutputReference) { mint(_redeemer: Data, _policy_id: PolicyId, tx: Transaction) { // Succeeds only if the parameter UTXO is spent in this transaction. // That UTXO can be consumed once, so this policy can mint once. list.any(tx.inputs, fn(input: Input) { input.output_reference == utxo_ref }) } else(_) { fail @"unsupported purpose" } } ``` Off-chain you pick any UTXO from your wallet, apply it as the parameter (which bakes in a unique policy ID, see [parameterized scripts](/docs/developers/curriculum/smart-contracts/lock-and-spend#parameterized-scripts)), and spend that same UTXO in the minting transaction. This is the protocol-guaranteed uniqueness that native time-locks cannot give you, and the foundation of the multi-validator [NFT minting machine](/templates/contracts) that auto-increments token names from on-chain state. ### Spending validator Runs when a transaction spends a UTXO sitting at the validator's script address. It receives the UTXO's **datum**, a **redeemer**, the output reference, and the transaction. This one only allows a spend when a specific oracle token is present in the reference inputs (the state-thread / beacon-token pattern): ```aiken pub type Datum { oracle_nft: PolicyId, } validator hello_world { spend(datum_opt: Option, _redeemer: Data, _input: OutputReference, tx: Transaction) { when datum_opt is { Some(datum) -> when inputs_with_policy(tx.reference_inputs, datum.oracle_nft) is { [_ref_input] -> True _ -> False } None -> False } } else(_) { fail @"unsupported purpose" } } ``` ### Withdrawal validator Runs when a transaction withdraws from the script's reward account. Withdrawal validators must be **registered** on-chain first (the `publish` handler validates registration/deregistration). Their main use isn't staking. It's the **withdraw-zero trick**, where a spending validator delegates its logic to a withdrawal validator that runs once for the whole transaction instead of once per input. That is the principle of [avoiding redundant validation](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview#avoid-redundant-validation), implemented as the [Stake Validator](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) pattern. When the script's funds *are* delegated and earning, the handler has a second job: deciding what may happen to real rewards. A withdrawal just moves lovelace from the reward account into the transaction; nothing routes it anywhere by default. If the rewards belong to the protocol rather than to whoever built the transaction, the handler has to say so. This one requires every withdrawn lovelace to arrive in the script's treasury UTXO, identified by a beacon NFT minted under the script's own hash: ```aiken validator treasury { withdraw(_redeemer: Data, account: Credential, tx: Transaction) { expect Script(own_hash) = account // The treasury UTXO carries the beacon NFT of this script's own policy expect Some(treasury_in) = list.find( tx.inputs, fn(i) { quantity_of(i.output.value, own_hash, "treasury") == 1 }, ) expect Some(treasury_out) = list.find( tx.outputs, fn(o) { quantity_of(o.value, own_hash, "treasury") == 1 }, ) // Whatever leaves the reward account must arrive there expect Some(withdrawn) = pairs.get_first(tx.withdrawals, account) lovelace_of(treasury_in.output.value) + withdrawn == lovelace_of(treasury_out.value) } else(_) { fail @"unsupported purpose" } } ``` How the reward account came to be registered, and which pool it delegates to, are certificate events. Those belong to the `publish` handler. ### Certificate validator Runs when a transaction publishes a **certificate** involving the script's stake credential: registering it, delegating it to a pool, deregistering it. Funds at a script address stake like any other funds, so `publish` is where the script sets its own staking policy. That assumes the stake part of the script's address points at the script, which is a choice, not a given. A script address's stake credential can just as well be the **depositor's own key**: the locked funds then keep earning staking rewards for that user with no on-chain staking logic at all, because delegation and reward withdrawal are authorized by the stake witness independently of the payment credential. Production lending protocols stake their pools' idle liquidity exactly this way, and the one on-chain obligation falls on the *spend* handler: continuing outputs must preserve the full address, or the user's rewards stream silently ends. Point the stake credential at the script instead and staking becomes protocol policy, which is what the handlers here govern. Leave it unset, or unchecked on outputs, and you are donating the rewards, or inviting the [insufficient staking control](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/staking-and-certificates#insufficient-staking-control) vulnerability. A useful split for the script-controlled case: registration is open, it costs the sender a deposit and merely switches the reward account on, so someone else registering for you is usually a favor; delegation is privileged, because which pool the protocol's funds back is policy; and everything unplanned is refused: ```aiken validator stake_policy(admin: VerificationKeyHash) { publish(_redeemer: Data, certificate: Certificate, tx: Transaction) { when certificate is { // Open: costs the sender a deposit, enables the reward account RegisterCredential { .. } -> True // Privileged: only the admin chooses where the funds delegate DelegateCredential { .. } -> list.has(tx.extra_signatories, admin) // Everything else, deregistration included, is refused _ -> False } } else(_) { fail @"unsupported purpose" } } ``` The explicit `when` is doing more work than it looks. A `publish` handler that returns `True`, or a permissive `else`, lets anyone deregister the stake credential, which halts reward accrual and breaks every pattern that relies on the withdrawal being available, the denial-of-service dissected on the [certificate deregistration](/docs/developers/curriculum/smart-contracts/security/vulnerabilities/staking-and-certificates#unconstrained-certificate-operations) page. Refusing the certificates you did not plan for is the defense. "Registration is a favor" holds only for credentials that are *meant* to earn. A credential that exists purely for withdraw-zero forwarding must refuse registration and delegation too: if an attacker registers it and delegates it to a pool, real rewards start accruing, and every transaction builder that hardcoded a zero-amount withdrawal quietly breaks. Decide which kind of credential you are writing before choosing what the handler permits. ## One validator, many purposes, one hash These validator types are not mutually exclusive. A single `validator` block can define more than one handler, and they compile to **one script with one hash**. That hash is at once the **minting policy ID** (for the `mint` handler) and the **payment credential of the script address** (for the `spend` handler): the policy and the address are two faces of the same script. ```aiken validator protocol { // Mints the state NFT, only into a valid initial config mint(_redeemer: Data, _policy_id: PolicyId, _tx: Transaction) { todo } // Governs every later update of that config UTXO spend(_datum: Option, _redeemer: Data, _input: OutputReference, _tx: Transaction) { todo } else(_) { fail @"unsupported purpose" } } ``` ```mermaid flowchart TB V["validator protocolone script, hash = H"] V -->|"mint handler"| P["Policy ID = Hmints the state NFT"] V -->|"spend handler"| S["Payment credential = Hguards the config UTXO"] V -->|"withdraw / publish handlers"| W["Stake credential = Hgoverns rewards & delegation"] style V fill:#0033AD,stroke:#0033AD,stroke-width:2px,color:#FFFFFF style P fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style S fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 style W fill:#FFFFFF,stroke:#0033AD,stroke-width:2px,color:#000000 ``` Because both handlers share the hash, each can name the other by it, with no circular parameter to resolve first. The `mint` handler can require that the NFT it creates lands at its own script address, and the `spend` handler can require that same NFT be present in the UTXO it guards. That mutual reference is the backbone of the [on-chain configuration](/docs/developers/curriculum/smart-contracts/datum-redeemer-context#on-chain-configuration) pattern, and the same shared-hash idea drives [transaction-level minting](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/tx-level-minter) for [efficient batch validation](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/overview#avoid-redundant-validation). The hash has a third face: it is also a valid **stake credential**. Build the script's address with `H` as both the payment part and the stake part, and the `spend`, `withdraw`, and `publish` handlers all describe one self-contained protocol: the spend handler can recover its own stake credential from the input it is validating (`own_input.output.address.stake_credential`) and require that credential to appear in `tx.withdrawals`, which is precisely the forwarding shape the [Stake Validator](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) pattern generalizes, with no second script hash to wire in as a parameter. The treasury example above leans on the same identity from the other side: its `withdraw` handler finds the treasury UTXO by a beacon NFT whose policy ID is the script's own hash. One script serving several purposes was possible under Plutus V1 and V2 as well; Plutus V3 and Aiken's multi-handler syntax make it first-class and add the Conway governance purposes (`vote`, `propose`). ## Native scripts: multisig and time-locks without Plutus Not every rule needs a Plutus validator. **Native scripts** are Cardano's simpler, non-Turing-complete scripting, perfect for **multi-signature** and **time-locks**, and they cost no script-execution fees. They combine a few primitives: `sig` (a required key), `before` / `after` (slot bounds), and `all` / `any` / `atLeast` (logical combinations). A multisig native script makes a **shared treasury**: anyone can send funds *to* its address, but moving them *out* requires the k-of-n signatures the script encodes. A native script that requires the owner's signature and only allows minting before a slot: ```ts // Owner must sign AND the transaction must be before slot 99999999 const nativeScript = NativeScripts.makeScriptAll([ NativeScripts.makeScriptPubKey(Bytes.fromHex(keyHash)), NativeScripts.makeInvalidHereafter(99999999n), ]) // Wrap for the builder, then attach with .attachScript({ script }) const script = new NativeScripts.NativeScript({ script: nativeScript }) ``` For **multisig**, swap the combinator: `makeScriptAll([...])` (everyone signs), `makeScriptAny([...])` (any one), or `makeScriptNOfK(2n, [...])` (k-of-n). A 2-of-3 treasury, then spend through it: ```ts // 2-of-3: any two of the three keys must sign const treasuryScript = NativeScripts.makeScriptNOfK(2n, [ NativeScripts.makeScriptPubKey(Bytes.fromHex(h1)), NativeScripts.makeScriptPubKey(Bytes.fromHex(h2)), NativeScripts.makeScriptPubKey(Bytes.fromHex(h3)), ]) // Spend: attach the script, add the two approving signers, build const tx = await client .newTx() .collectFrom({ inputs: treasuryUtxos }) .attachScript({ script: treasuryScript }) .addSigner({ keyHash: new KeyHash.KeyHash({ hash: Bytes.fromHex(h1) }) }) .addSigner({ keyHash: new KeyHash.KeyHash({ hash: Bytes.fromHex(h2) }) }) .build() ``` In a real flow the unsigned CBOR is shared between signers, each partial-signs, and the combined transaction is submitted. No redeemer or collateral needed. ```ts const nativeScript: NativeScript = { type: "all", scripts: [ { type: "before", slot: "99999999" }, { type: "sig", keyHash }, ], } const forgingScript = ForgeScript.fromNativeScript(nativeScript) ``` **Multisig** is just a native script with multiple `sig` entries under `all` (everyone must sign) or `atLeast` (k-of-n). Each required party signs the *same* transaction with a **partial signature** (`wallet.signTx(tx, true)`); once all required signatures are attached, the transaction is valid. A common shape: a backend wallet partially signs, then the user's browser wallet partially signs, then it's submitted. The script is the same JSON, `{"type":"all","scripts":[{"type":"sig","keyHash":"..."},{"type":"after","slot":1000}]}`, supporting `sig`, `before`, `after`, `all`, `any`, and `atLeast`. Build its address with `cardano-cli address build --payment-script-file policy.json`, then spend by collecting one witness per signer plus the script witness and assembling them: ```bash cardano-cli latest transaction witness --tx-body-file tx.body --script-file policy.json --out-file script.wit cardano-cli latest transaction witness --tx-body-file tx.body --signing-key-file key1.skey --out-file key1.wit cardano-cli latest transaction assemble --tx-body-file tx.body --witness-file script.wit --witness-file key1.wit --out-file tx.signed ``` A time-locked script must be paired with a matching validity interval: an `after: N` script needs `--invalid-before` ≥ N, a `before: N` script needs `--invalid-hereafter` ≤ N (funds left past a `before` slot are locked forever). For attaching native scripts off-chain, see [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend) and the [Mesh smart contracts guide](https://meshjs.dev/apis/txbuilder/smart-contracts). ## From validator to blueprint When you run `aiken build`, the compiler produces a **[CIP-57](https://cips.cardano.org/cip/CIP-57) blueprint** at `plutus.json`, the bridge between your on-chain code and off-chain applications. Think of it as the OpenAPI spec for your contract. It has three parts: - **`preamble`**: metadata, including the `plutusVersion` (e.g. `v3`) that off-chain libraries must target. - **`validators`**: each validator's `title`, datum/redeemer/parameter schemas, `compiledCode` (hex CBOR), and `hash` (its on-chain address identifier). - **`definitions`**: reusable type schemas referenced by validators (via `$ref`, exactly like JSON Schema). ```json { "preamble": { "title": "my/contract", "plutusVersion": "v3", "compiler": { "name": "Aiken" } }, "validators": [ { "title": "mint.minting_policy.mint", "redeemer": { "schema": { "$ref": "#/definitions/MyRedeemer" } }, "compiledCode": "59...", "hash": "9c9666..." } ], "definitions": { "MyRedeemer": { "anyOf": [ { "title": "MintToken", "index": 0, "fields": [] } ] } } } ``` From the blueprint, tools generate type-safe off-chain code, the same way you'd generate an API client from an OpenAPI spec. Evolution's blueprint codegen and Mesh both read `plutus.json` to produce a typed client: Evolution's codegen emits a `.ts` file of `TSchema` definitions plus validator metadata (hashes, parameter schemas) you import and use with `Data.withSchema`: ```ts // Build step: turn plutus.json into typed schemas const blueprint = JSON.parse(fs.readFileSync("plutus.json", "utf-8")) const code = Blueprint.Codegen.generateTypeScript(blueprint, { optionStyle: "NullOr", // "NullOr" | "UndefinedOr" | "Union" unionStyle: "Variant", // "Variant" | "Struct" | "TaggedStruct" }) fs.writeFileSync("src/contract-types.ts", code) // In your app: import the generated schema, wrap it in a codec const RedeemerCodec = Data.withSchema(MyRedeemer) const redeemer = RedeemerCodec.toData({ MintToken: {} }) // type error if the shape is wrong ``` Mesh ships blueprint helper classes (`SpendingBlueprint`, `MintingBlueprint`, `WithdrawalBlueprint` from `@meshsdk/core`) that take the `compiledCode` and hand back the script hash, CBOR, and address: ```ts const compiledCode = blueprint.validators[0].compiledCode // V3 script, networkId 0 (testnet) const spending = new SpendingBlueprint("V3", 0) spending.paramScript(compiledCode, [], "Mesh") // or .noParamScript(compiledCode) when there are no params const scriptHash = spending.hash const scriptCbor = spending.cbor const scriptAddress = spending.address ``` `MintingBlueprint("V3")` exposes the policy ID as `.hash`; `WithdrawalBlueprint("V3", networkId)` gives the reward address. Pass parameters as the second arg of `paramScript` (Mesh data types like `mPubKeyAddress` when the third arg is `"Mesh"`). See the blueprint section of [Choose a language](/docs/developers/curriculum/smart-contracts/choose-a-language#blueprints-the-contracts-interface). ### Reading a blueprint by hand Codegen covers the common case, but you should be able to read a `plutus.json` directly: when you adopt a language with no generator, debug a type mismatch, or just want to understand what a tool handed you. Trace it top-down: - **`preamble.plutusVersion`** is the script version (`v3` here). Off-chain you must build the script as that exact version (`"V3"` in your SDK); it fixes the cost model and the available builtins. - **Each `validators[]` entry** is named `..` (here `mint.minting_policy.mint`). Its `hash` is the on-chain identifier: the **policy ID** for a `mint` validator, the script-address payment credential for a `spend` one. The `compiledCode` is the CBOR you wrap to get that address. - **A `$ref` is a pointer.** A `redeemer.schema` of `{ "$ref": "#/definitions/MyRedeemer" }` means "resolve `MyRedeemer` under `definitions`." Follow it; refs nest, so a field can itself be a `$ref`. - **A sum type is an `anyOf`.** Each entry is one constructor, with an `index` (its on-chain tag) and positional `fields`. So `MyRedeemer` with `MintToken` at `index: 0` and `BurnToken` at `index: 1` tells you exactly how to build the redeemer by hand: `MintToken` is constructor 0 with no fields, `BurnToken` is constructor 1. That is precisely what you pass as `mConStr0([])` (Mesh) or `Constr 0 []` (raw) when no generated codec is doing it for you. Read it that way (preamble, then a validator, then the schemas it names, then the definitions those point at) and a blueprint is a complete, language-agnostic description of how to talk to the contract. One consequence worth exploiting: the compiler only emits schemas for types reachable from some validator's parameter, datum, or redeemer signature. If a type *is* your integration contract, say, an order datum that any third-party contract may create and your protocol will accept from anywhere, you can force it into the blueprint with a deliberate no-op **documentation validator**: a `spend` handler that names the type in its signature and returns `True`, never meant to be deployed. The blueprint then carries the canonical, machine-readable schema of the datum for every integrator, which is exactly the OpenAPI role described above. ## Key takeaways Every validator is a **pure function**: it receives transaction context and returns `True` or `False`. No side effects, no storage writes, no network calls. The runtime only applies state changes if the validator approves, fundamentally different from a web2 backend that both validates and mutates. ## Next steps - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend): interact with your validator from off-chain code - [Testing](/docs/developers/curriculum/smart-contracts/testing): test validators with mock transactions before deploying - [Security](/docs/developers/curriculum/smart-contracts/security): the vulnerability classes to guard against - [Contract library](/templates/contracts): full validators to read, including the oracle-NFT minting machine --- ## Delegate and Withdraw This page is the first half of the [staking lifecycle](/docs/developers/curriculum/staking-governance/staking#the-staking-lifecycle) in code: register the stake credential, delegate it to a pool, and withdraw the rewards it earns. Each operation is a single transaction, shown in Evolution, Mesh, and cardano-cli. ## Before you start The snippets below set up a provider and a wallet once and reuse them; each transaction uses a fresh builder. ```typescript const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) const address = await client.address() const stakeCredential = address.stakingCredential! // staking certificates act on this ``` ```typescript const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY!) const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = preprod/preview testnet walletAddressType: AddressType.Base, fetcher: provider, submitter: provider, mnemonic: process.env.WALLET_MNEMONIC!.split(" "), }) // staking certificates act on the wallet's reward address; // each operation builds with a fresh new MeshTxBuilder({ fetcher: provider }) ``` You need `payment.skey` / `payment.addr` plus a registered stake key pair (`stake.vkey` / `stake.skey`). Operations that touch the stake credential are signed by both keys. ## Register and delegate Delegating for the first time means two things: registering the stake credential (a small refundable deposit) and delegating it to a pool. The Conway era added a combined certificate that does both in one step and saves a certificate fee, so either way it's a single transaction. ```typescript declare const stakeCredential: Credential.Credential declare const poolKeyHash: any const tx = await client .newTx() .registerAndDelegateTo({ stakeCredential, poolKeyHash }) .build() const signed = await tx.sign() await signed.submit() ``` You can also chain the two certificates explicitly, which is what you'd do with legacy (pre-Conway, no-deposit) registration: ```typescript // Conway certificates const tx = await client .newTx() .registerStake({ stakeCredential }) .delegateToPool({ stakeCredential, poolKeyHash }) .build() // Legacy certificates (no deposit), use registerStakeLegacy() instead const legacyTx = await client .newTx() .registerStakeLegacy({ stakeCredential }) .delegateToPool({ stakeCredential, poolKeyHash }) .build() ``` The deposit (currently 2 ADA) is fetched from protocol parameters automatically. Already registered? Drop the registration call and just `delegateToPool({ stakeCredential, poolKeyHash })`. ```typescript const utxos = await wallet.getUtxosMesh() const changeAddress = await wallet.getChangeAddressBech32() const rewardAddress = (await wallet.getRewardAddresses())[0] const poolIdHash = deserializePoolId("pool1...") const txBuilder = new MeshTxBuilder({ fetcher: provider }) const unsignedTx = await txBuilder .registerStakeCertificate(rewardAddress) .delegateStakeCertificate(rewardAddress, poolIdHash) .selectUtxosFrom(utxos) .changeAddress(changeAddress) .complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` `deserializePoolId` turns a bech32 `pool1...` ID into the hash the builder needs. Already registered? Use only `delegateStakeCertificate(rewardAddress, poolIdHash)`. Generate a registration certificate (the deposit comes from the `stakeAddressDeposit` protocol parameter) and a delegation certificate, then submit both in one transaction: ```shell # 1. Registration certificate cardano-cli latest stake-address registration-certificate \ --stake-verification-key-file stake.vkey \ --key-reg-deposit-amt 2000000 \ --out-file registration.cert # 2. Delegation certificate (needs the target pool ID) cardano-cli latest stake-address stake-delegation-certificate \ --stake-verification-key-file stake.vkey \ --stake-pool-id pool17navl486tuwjg4t95vwtlqslx9225x5lguwuy6ahc58x5dnm9ma \ --out-file delegation.cert # 3. Build, including both certificates (build handles fee + deposit) cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --certificate-file registration.cert \ --certificate-file delegation.cert \ --witness-override 2 \ --out-file tx.raw # 4. Sign with both payment and stake keys, then submit cardano-cli latest transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file stake.skey \ --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` The `--witness-override 2` flag tells `build` to budget for two signatures (payment + stake) so the fee is accurate. Already registered? Skip the registration certificate and build with just `delegation.cert`. ## Withdraw rewards Rewards accumulate to the reward address each epoch and must be **explicitly withdrawn**. You always withdraw the entire balance. Partial withdrawals aren't allowed. ```typescript const delegation = await client.getWalletDelegation() console.log("Available rewards:", delegation.rewards, "lovelace") const tx = await client .newTx() .withdraw({ stakeCredential, amount: delegation.rewards }) .build() const signed = await tx.sign() await signed.submit() ``` ```typescript const utxos = await wallet.getUtxosMesh() const changeAddress = await wallet.getChangeAddressBech32() const rewardAddress = (await wallet.getRewardAddresses())[0] // The full reward balance, from the provider const { rewards } = await provider.fetchAccountInfo(rewardAddress) const txBuilder = new MeshTxBuilder({ fetcher: provider }) const unsignedTx = await txBuilder .withdrawal(rewardAddress, rewards) .selectUtxosFrom(utxos) .changeAddress(changeAddress) .complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` `withdrawal` takes the lovelace amount as a string; pass the whole `rewards` balance. ```shell # Read the current reward balance rewards="$(cardano-cli query stake-address-info --address $(< stake.addr) | jq .[].rewardAccountBalance)" # Withdraw the full balance with the stakeAddress+lovelace syntax cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --withdrawal "$(< stake.addr)+$rewards" \ --change-address $(< payment.addr) \ --witness-override 2 \ --out-file tx.raw cardano-cli latest transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file stake.skey \ --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` ## Next steps - [Manage stake](/docs/developers/curriculum/staking-governance/manage-stake): query delegation status, deregister cleanly, and delegate stake and vote together --- ## DReps & Vote Delegation Governance participation starts with the two operations on this page: registering a **DRep**, the credential votes are cast with, and delegating voting power to one. The SDK snippets assume the same provider and wallet setup as [staking](/docs/developers/curriculum/staking-governance/delegate-and-withdraw#before-you-start): an Evolution `client`, or a Mesh `provider` and `wallet` with a fresh `MeshTxBuilder` per transaction. Each operation also has a **cardano-cli** tab with the key-based flow; the deeper cli ceremonies (script and Plutus DReps) continue beneath the relevant sections. ## Register as a DRep Becoming a DRep is a registration certificate with a refundable deposit (`drepDeposit`, currently 500 ADA) and an optional anchor describing who you are ([CIP-119](https://cips.cardano.org/cip/CIP-0119) metadata). ```typescript declare const drepCredential: Credential.Credential declare const anchor: Anchor.Anchor // Register (add `anchor` to attach metadata) const tx = await client.newTx().registerDRep({ drepCredential, anchor }).build() const signed = await tx.sign() await signed.submit() ``` Update metadata with `updateDRep({ drepCredential, anchor })` and step down with `deregisterDRep({ drepCredential })` (the deposit is refunded). The deposit is fetched from protocol parameters automatically. ```typescript const dRep = await wallet.getDRep() // Optional CIP-119 metadata describing the DRep, hashed and anchored on-chain const anchorUrl = "https://example.com/drep.jsonld" const anchorMetadata = { /* CIP-119 metadata object */ } const anchorDataHash = hashDrepAnchor(anchorMetadata) // hashes the metadata, not the URL txBuilder .drepRegistrationCertificate(dRep.dRepIDCip105, { anchorUrl, anchorDataHash }) .selectUtxosFrom(await wallet.getUtxosMesh()) .changeAddress(await wallet.getChangeAddressBech32()) const unsignedTx = await txBuilder.complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` Update metadata with `drepUpdateCertificate(dRepId, { anchorUrl, anchorDataHash })` and step down with `drepDeregistrationCertificate(dRepId)` (the deposit is refunded). See the [Mesh governance guide](https://meshjs.dev/apis/txbuilder/governance). Generate the DRep key pair: ```bash cardano-cli latest governance drep key-gen \ --verification-key-file drep.vkey \ --signing-key-file drep.skey ``` Build the registration certificate with the deposit and an optional metadata anchor. Query `dRepDeposit` rather than hardcoding it; the value below is the current 500 ADA: ```bash cardano-cli latest governance drep registration-certificate \ --drep-verification-key-file drep.vkey \ --key-reg-deposit-amt 500000000 \ --drep-metadata-url https://example.com/drep.jsonld \ --drep-metadata-hash a14a5ad4f36bddc00f92ddb39fd9ac633c0fd43f8bfa57758f9163d10ef916de \ --out-file drep-reg.cert ``` Build the transaction (`--witness-override 2` covers the payment and DRep signatures), sign with both keys, and submit: ```bash cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --certificate-file drep-reg.cert \ --witness-override 2 \ --out-file tx.raw cardano-cli latest transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file drep.skey \ --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` ### Script-based and Plutus DReps A DRep credential can also be a script hash instead of a key hash. The flow mirrors the key-based path, but the DRep ID is the hash of the script and the transaction carries a witness (multisig) or a redeemer (Plutus) instead of a plain DRep key signature. For a **simple-script (multisig) DRep**, write a native script (for example `type: atLeast` over the members' DRep key hashes), hash it for the DRep ID, and register against that script hash: ```bash cardano-cli hash script --script-file drep-multisig.json --out-file drep-multisig.id cardano-cli latest governance drep registration-certificate \ --drep-script-hash "$(< drep-multisig.id)" \ --key-reg-deposit-amt 500000000 \ --out-file drep-multisig-reg.cert ``` Build with `--certificate-script-file drep-multisig.json`, then collect one `transaction witness` per member and combine them with `transaction assemble`. A **Plutus-script DRep** registers the same way (`--drep-script-hash` from `cardano-cli hash script` on the `.plutus` file), but the transaction supplies collateral and a redeemer rather than script witnesses: ```bash cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --tx-in-collateral $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --certificate-file drep.cert \ --certificate-script-file drep.plutus \ --certificate-redeemer-value {} \ --change-address $(< payment.addr) \ --out-file tx.raw ``` Only the payment key signs; script validity comes from the redeemer, not a DRep key signature. ## Delegate your vote Voting power delegation is **separate from and independent of stake delegation**. You can delegate stake to one pool and your vote to a different DRep, and change either without affecting the other. There are also two built-in options for holders who don't want to pick a DRep: **Abstain** (not counted) and **No Confidence** (counts against the committee). In the Conway era, staking rewards keep accruing, but you cannot withdraw them until your stake credential has delegated its vote, to a DRep or to a predefined option (Abstain or No Confidence). Both delegations attach to your **stake credential**, the part of your address separate from the payment credential. See [Addresses](/docs/developers/curriculum/fundamentals/core-concepts/addresses) for how the two combine. ```typescript declare const stakeCredential: Credential.Credential declare const drepKeyHash: any // Delegate to a specific DRep const tx = await client .newTx() .delegateToDRep({ stakeCredential, drep: DRep.fromKeyHash(drepKeyHash) }) .build() // Or a built-in option: // drep: DRep.alwaysAbstain() // drep: DRep.alwaysNoConfidence() ``` To register stake and delegate the vote in one step, use `registerAndDelegateTo({ stakeCredential, drep })`; to do stake + vote together, see [Manage stake](/docs/developers/curriculum/staking-governance/manage-stake#delegate-stake-and-vote-together). ```typescript const dRepId = "drep1..." // a registered DRep (or use { type: "AlwaysAbstain" } / { type: "AlwaysNoConfidence" }) const rewardAddress = (await wallet.getRewardAddresses())[0] txBuilder .voteDelegationCertificate({ dRepId }, rewardAddress) .selectUtxosFrom(await wallet.getUtxosMesh()) .changeAddress(await wallet.getChangeAddressBech32()) const unsignedTx = await txBuilder.complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` Create the vote-delegation certificate from your stake key. Target a registered DRep with `--drep-key-hash` (or `--drep-script-hash`), or pick `--always-abstain` / `--always-no-confidence`: ```bash cardano-cli latest stake-address vote-delegation-certificate \ --stake-verification-key-file stake.vkey \ --drep-key-hash $(< drep.id) \ --out-file vote-deleg.cert ``` Build, sign with the payment and stake keys, and submit: ```bash cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --certificate-file vote-deleg.cert \ --witness-override 2 \ --out-file tx.raw cardano-cli latest transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file stake.skey \ --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` ## Next steps - [Vote & propose](/docs/developers/curriculum/staking-governance/vote-and-propose): cast votes with the DRep you registered, and put actions on-chain --- ## Governance Operations This appendix collects the governance operations most integrations touch only occasionally: the Constitutional Committee's credential ceremonies, the queries that power governance UIs, and the wallet and validator hooks for building governance features. ## Committee operations Constitutional Committee members use a **cold/hot credential model**: the cold credential identifies the seat and stays offline; an authorized hot credential does the day-to-day voting. If the hot key is compromised, authorize a new one (it overrides the old); if the cold key is compromised, the only recourse is to resign. Mesh's transaction builder exposes no committee-certificate helpers, so use Evolution or cardano-cli for these. ```typescript declare const coldCredential: Credential.Credential declare const hotCredential: Credential.Credential declare const anchor: Anchor.Anchor // Authorize a hot credential const authTx = await client.newTx().authCommitteeHot({ coldCredential, hotCredential }).build() // Resign the seat const resignTx = await client.newTx().resignCommitteeCold({ coldCredential, anchor }).build() ``` A member holds a **cold** credential (offline, the on-chain identity) and a **hot** credential (signs votes); an authorization certificate links them, and a new one overrides the previous. ```bash # cold key pair + its hash (what an update-committee action references) cardano-cli latest governance committee key-gen-cold \ --cold-verification-key-file cc-cold.vkey \ --cold-signing-key-file cc-cold.skey cardano-cli latest governance committee key-hash \ --verification-key-file cc-cold.vkey > cc-key.hash # hot key pair cardano-cli latest governance committee key-gen-hot \ --verification-key-file cc-hot.vkey \ --signing-key-file cc-hot.skey # authorize the hot credential cardano-cli latest governance committee create-hot-key-authorization-certificate \ --cold-verification-key-file cc-cold.vkey \ --hot-verification-key-file cc-hot.vkey \ --out-file cc-authorization.cert ``` Submit the certificate in a transaction signed by the payment and cold keys. To step down, submit a resignation certificate the same way: ```bash cardano-cli latest governance committee create-cold-key-resignation-certificate \ --cold-verification-key-file cc-cold.vkey \ --out-file cc-resign.cert ``` Script-based members use `--cold-script-hash` / `--hot-script-hash` instead of the key files. ## Query governance state To show proposals, DRep info, voting power, or committee state in a UI, query your node: ```bash cardano-cli latest query gov-state # committee, constitution, params, proposals cardano-cli latest query drep-state --all-dreps # DRep registration: deposit, expiry, anchor cardano-cli latest query drep-stake-distribution --all-dreps # voting power per DRep cardano-cli latest query committee-state # members, hot-key auth, expiry, threshold cardano-cli latest query proposals --all-proposals # actions eligible for ratification ``` `query constitution` returns the current constitution anchor and guardrails script hash, and `query gov-state | jq -r .nextRatifyState.nextEnactState.prevGovActionIds` gives the last-enacted action IDs you need for the `--prev-governance-action-*` flags. Query APIs (Blockfrost, Koios, Maestro) expose the same data over HTTP; see [Use a provider](/docs/developers/curriculum/production/use-a-provider). ## Browser wallet APIs (CIP-95) For a browser dApp, [CIP-95](https://cips.cardano.org/cip/CIP-0095) adds governance methods to the [wallet connector](/docs/developers/curriculum/dapps/connect-a-wallet), so you can read what you need to build governance transactions. It is an **optional extension**, not part of the CIP-30 core, so two things follow. You have to ask for it when you connect, and a wallet is free to say no. Plenty of wallets implement core CIP-30 only, and your dApp has to handle that rather than assume the methods are there. ```typescript // Request the extension at enable time const api = await window.cardano.eternl.enable({ extensions: [{ cip: 95 }] }) // A wallet without CIP-95 still connects, it just returns no cip95 namespace if (!api.cip95) throw new Error("This wallet does not support governance (CIP-95)") const dRepKey = await api.cip95.getPubDRepKey() // the user's DRep key const stakeKeys = await api.cip95.getRegisteredPubStakeKeys() // registered stake keys ``` ```typescript const wallet = await MeshCardanoBrowserWallet.enable("eternl", [{ cip: 95 }]) const dRepKey = await wallet.getPubDRepKey() ``` `signTx` and `signData` are extended rather than namespaced, so they keep working as before and gain the ability to witness Conway certificates and DRep credentials. For the connection itself, see [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet#governance-cip-95). ## Governance in your validators Plutus V3 added governance **script purposes**: a validator can run as a `Voting` or `Proposing` script, letting a contract participate in governance under script control. See the `ScriptPurpose` list in [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context#the-scriptpurpose). All the SDK governance operations in this module also support script-controlled credentials: pass a `redeemer` and `attachScript({ script })`, exactly as for [script-controlled stake](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator#submitting-the-trigger-off-chain). ## Next steps - [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview): the next module. Write the validation logic behind the script credentials and governance purposes this module kept pointing at. - [cardano.org/governance](https://cardano.org/governance): the participant hub. Delegate your vote, become a DRep, and read the constitution. --- ## Governance Cardano's on-chain governance ([CIP-1694](https://cips.cardano.org/cip/CIP-1694), the Voltaire era) lets ADA holders propose, vote on, and enact protocol changes. For developers, the key fact is that **governance actions are ordinary on-chain transactions**: DRep registration, vote delegation, and votes use the same wallets, providers, and transaction builders you already use, with a few extra certificate and procedure types. This page is the concept: why governance shapes what you build, who decides, what can be decided, and how a decision becomes protocol reality. Three practice pages then build it: [DReps & vote delegation](/docs/developers/curriculum/staking-governance/drep-and-delegation), [Vote & propose](/docs/developers/curriculum/staking-governance/vote-and-propose), and [Governance operations](/docs/developers/curriculum/staking-governance/governance-operations). Taking part in governance as a person, delegating your vote in a wallet, browsing actions, and reading the constitution, lives on the participant hub at [cardano.org/governance](https://cardano.org/governance). ## Why governance matters to developers Governance isn't just a user feature; it shapes the platform you build on: - **Protocol parameters affect your code.** Transaction size limits, execution-unit budgets, min-UTXO values, and fees are all governance-controlled. A parameter change can affect your contracts, so design with margin and watch proposals that touch technical parameters. - **Hard forks can change Plutus.** Upgrades may add Plutus versions with new capabilities: the Chang hard fork introduced Plutus V3 with built-ins for governance. Older scripts keep working, but new features may need the newer version. - **The treasury funds development.** The on-chain treasury (over a billion ADA) is allocated by governance vote, a direct, on-chain alternative to grants you can propose into. - **Your users participate.** If you build a wallet or dApp, your users are governance participants; they may expect to register a DRep, delegate a vote, or vote through your interface. ## The three governance bodies CIP-1694 distributes power across three bodies as checks and balances: - **Constitutional Committee (CC)**: verifies that actions comply with the Cardano Constitution (a constitutional court, not a decision-maker on merit). - **Delegated Representatives (DReps)**: the primary voice of ADA holders; anyone can register as a DRep or delegate their vote to one. - **Stake Pool Operators (SPOs)**: vote on specific action types (notably hard forks and certain parameters). Different action types require different combinations of these bodies, with the [thresholds and lifecycle](#ratification-and-lifecycle) below. The constitution and the broader participant model live at [cardano.org/governance](https://cardano.org/governance). ## The seven governance action types | Action | CC | DReps | SPOs | |---|---|---|---| | Motion of no-confidence | - | Yes | Yes | | Update committee / threshold | - | Yes | Yes | | New constitution or guardrails script | Yes | Yes | - | | Hard-fork initiation | Yes | Yes | Yes | | Protocol parameter change | Yes | Yes | * | | Treasury withdrawal | Yes | Yes | - | | Info action (non-binding) | - | Yes | Yes | `*` SPOs vote on specific parameter groups only. Each type has its own voting thresholds (themselves governance-controlled). ## Ratification and lifecycle Each action type is ratified by meeting a different mix of voting thresholds across the three bodies. The fractions below are the Conway defaults (themselves governance-controlled, set in the [Conway genesis](https://book.world.dev.cardano.org/environments/mainnet/conway-genesis.json)); a dash means that body does not vote on that type. | Governance action | CC | DReps | SPOs | |---|---|---|---| | Motion of no-confidence | - | 0.67 | 0.51 | | Update committee / threshold (normal) | - | 0.67 | 0.51 | | Update committee / threshold (no-confidence) | - | 0.60 | 0.51 | | New constitution or guardrails script | 2/3 | 0.75 | - | | Hard-fork initiation | 2/3 | 0.60 | 0.51 | | Protocol parameters (network / economic / technical) | 2/3 | 0.67 | - | | Protocol parameters (governance group) | 2/3 | 0.75 | - | | Treasury withdrawal | 2/3 | 0.67 | - | | Info action (non-binding) | 2/3 | 1 | 1 | Changing a **security-relevant** protocol parameter (block and transaction sizes, fees, `utxoCostPerByte`, `govActionDeposit`, and similar) needs an extra SPO vote at 0.51, even for groups SPOs do not normally vote on. A proposed action then runs a fixed lifecycle, which is what your tooling reads when it shows an action's status: 1. **Live for `govActionLifetime` epochs** (6 on mainnet); bodies vote during this window. 2. **Ratified** once it meets the thresholds for its type, and added to the enactment set at the epoch boundary. 3. **Enacted** at the next epoch boundary, when the change takes effect. 4. **Expired** if it never reaches its thresholds within its lifetime. Most action types also carry a pointer to the last enacted action of the same kind, so an action ratifies against the state it was proposed against (treasury withdrawals and info actions are exempt). The deposit is returned to the proposer's reward account once the action leaves the live state. ## Next steps - [DReps & vote delegation](/docs/developers/curriculum/staking-governance/drep-and-delegation): register a DRep (key- or script-based) and delegate voting power - [Vote & propose](/docs/developers/curriculum/staking-governance/vote-and-propose): cast votes on live actions and author each of the seven types - [Governance operations](/docs/developers/curriculum/staking-governance/governance-operations): committee credentials, state queries, and CIP-95 --- ## Manage Stake The delegation from [Delegate and withdraw](/docs/developers/curriculum/staking-governance/delegate-and-withdraw) is in place; this page manages it over its lifetime: reading status and rewards to show in a UI, unwinding the registration when a user leaves, and the combined flow that delegates stake and voting power in one transaction. The snippets reuse the [same setup](/docs/developers/curriculum/staking-governance/delegate-and-withdraw#before-you-start). ## Query delegation and rewards Read which pool a stake credential is delegated to and how many rewards have accrued, to show status in a UI, or to decide how much to withdraw. ```typescript const delegation = await client.getWalletDelegation() console.log("Pool:", delegation.poolId) // null if not delegated console.log("Rewards:", delegation.rewards) // lovelace ``` To query an arbitrary reward address instead of the wallet's, use `client.getDelegation(rewardAddress)`. Both return `{ poolId, rewards }`. ```typescript const rewardAddress = (await wallet.getRewardAddresses())[0] const info = await provider.fetchAccountInfo(rewardAddress) console.log("Registered:", info.active) // false if not registered console.log("Pool:", info.poolId) // delegated pool console.log("Rewards:", info.rewards) // lovelace, available to withdraw console.log("Balance:", info.balance) // total controlled stake ``` `fetchAccountInfo` returns `{ active, poolId, balance, rewards, withdrawals }`. ```shell cardano-cli query stake-address-info --address $(< stake.addr) ``` ```json [ { "address": "stake_test1ur453z5nxrgvvu9wfyuxut8ss0mrvca4n8ly44tcu8camlqaz98mh", "delegationDeposit": 2000000, "rewardAccountBalance": 10534638802, "stakeDelegation": "pool17xgtj7ayvsaju4clums0mfusla4pmtfm6t4fj6guqqlsvne2mwm", "voteDelegation": "scriptHash-59aa3f091b3bcef254abfb89aea64973a61b78fdb2ac44839c7ccba8" } ] ``` An empty array (`[]`) means the stake address isn't registered. ## Deregister and reclaim the deposit Deregistering removes the stake credential and refunds the registration deposit. **Withdraw rewards first**. Rewards are lost after deregistration. The best practice is to do both in the same transaction. ```typescript const delegation = await client.getWalletDelegation() const tx = await client .newTx() .withdraw({ stakeCredential, amount: delegation.rewards }) .deregisterStake({ stakeCredential }) .build() const signed = await tx.sign() await signed.submit() ``` Use `deregisterStakeLegacy({ stakeCredential })` if you registered with the legacy certificate. ```typescript const utxos = await wallet.getUtxosMesh() const changeAddress = await wallet.getChangeAddressBech32() const rewardAddress = (await wallet.getRewardAddresses())[0] // Withdraw the last rewards and deregister in one transaction const { rewards } = await provider.fetchAccountInfo(rewardAddress) const txBuilder = new MeshTxBuilder({ fetcher: provider }) const unsignedTx = await txBuilder .withdrawal(rewardAddress, rewards) .deregisterStakeCertificate(rewardAddress) .selectUtxosFrom(utxos) .changeAddress(changeAddress) .complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` `deregisterStakeCertificate` reclaims the deposit; pairing it with `withdrawal` in the same transaction avoids losing accrued rewards. ```shell # Deregistration certificate cardano-cli latest stake-address deregistration-certificate \ --stake-verification-key-file stake.vkey \ --out-file dereg.cert # Withdraw the last rewards and deregister in one transaction cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --withdrawal "$(< stake.addr)+$(cardano-cli query stake-address-info --address $(< stake.addr) | jq -r .[].rewardAccountBalance)" \ --certificate-file dereg.cert \ --witness-override 2 \ --out-file tx.raw cardano-cli latest transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file stake.skey \ --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` ## Delegate stake and vote together Conway-era Cardano has a second, **independent** delegation: governance voting power. You can delegate stake to one pool and your vote to a different DRep, and change either without affecting the other. The full DRep flow lives in [DReps & vote delegation](/docs/developers/curriculum/staking-governance/drep-and-delegation#delegate-your-vote), but because both are stake-credential certificates, you can combine them in a single transaction: ```typescript declare const stakeCredential: Credential.Credential declare const poolKeyHash: any declare const drepKeyHash: any // Register + delegate stake + delegate vote, all at once const tx = await client .newTx() .registerAndDelegateTo({ stakeCredential, poolKeyHash, drep: DRep.fromKeyHash(drepKeyHash) }) .build() ``` ```typescript const rewardAddress = (await wallet.getRewardAddresses())[0] const poolIdHash = deserializePoolId("pool1...") const dRepId = "drep1..." // or { type: "AlwaysAbstain" } / { type: "AlwaysNoConfidence" } // Register + delegate stake + delegate vote, chained in one transaction const txBuilder = new MeshTxBuilder({ fetcher: provider }) const unsignedTx = await txBuilder .registerStakeCertificate(rewardAddress) // first time only .delegateStakeCertificate(rewardAddress, poolIdHash) // stake -> pool .voteDelegationCertificate({ dRepId }, rewardAddress) // vote -> DRep .selectUtxosFrom(await wallet.getUtxosMesh()) .changeAddress(await wallet.getChangeAddressBech32()) .complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` ```bash # stake -> pool cardano-cli latest stake-address stake-delegation-certificate \ --stake-verification-key-file stake.vkey \ --stake-pool-id pool1... \ --out-file deleg.cert # vote -> DRep cardano-cli latest stake-address vote-delegation-certificate \ --stake-verification-key-file stake.vkey \ --drep-key-hash $(< drep.id) \ --out-file vote-deleg.cert # build both certificates into one transaction cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --certificate-file deleg.cert \ --certificate-file vote-deleg.cert \ --witness-override 2 \ --out-file tx.raw # sign with payment.skey + stake.skey, then submit ``` Already registered? Drop the registration step: Evolution `delegateToPoolAndDRep({ stakeCredential, poolKeyHash, drep })`, Mesh just the two delegation certificates. The DRep can also be an abstain or no-confidence option (`DRep.alwaysAbstain()` / `DRep.alwaysNoConfidence()` in Evolution). ## Next steps - [Governance](/docs/developers/curriculum/staking-governance/governance): what that vote delegation feeds into, the bodies, action types, and ratification rules of on-chain governance --- ## Staking & Governance You can already build transactions and manage native tokens. This module adds what an ADA holder's *stake* can do from your application: back the network's security (staking) and steer the protocol's evolution (governance). Everything here is the same build → sign → submit flow you know, with a few extra certificate types, and a dApp can build all of it. The module runs concept before practice, twice: - **[Staking](/docs/developers/curriculum/staking-governance/staking)**: the staking concept. Cardano's non-custodial delegation model, how rewards and timing work, and the lifecycle every integration follows. - **[Delegate and withdraw](/docs/developers/curriculum/staking-governance/delegate-and-withdraw)**: practice. Register a stake credential, delegate it to a pool, and withdraw rewards. - **[Manage stake](/docs/developers/curriculum/staking-governance/manage-stake)**: practice. Query delegation and rewards, deregister cleanly, and the combined certificate flow that bridges into governance. - **[Governance](/docs/developers/curriculum/staking-governance/governance)**: the governance concept. Why CIP-1694 shapes the platform you build on, the three bodies, the seven action types, and ratification. - **[DReps & vote delegation](/docs/developers/curriculum/staking-governance/drep-and-delegation)**: practice. Register a DRep, key- or script-based, and delegate voting power. - **[Vote & propose](/docs/developers/curriculum/staking-governance/vote-and-propose)**: practice. Cast votes on live actions and put actions of your own on-chain. - **[Governance operations](/docs/developers/curriculum/staking-governance/governance-operations)**: the appendix. Committee credentials, governance-state queries, CIP-95, and the handover to smart contracts. :::note Not covered here - **Running a stake pool** (relays, block producers, KES keys, pool registration, monitoring) is a separate discipline with its own section: [Operate a Stake Pool](/docs/operators/). - **Participating in governance** as an ADA holder, DRep, or committee member (delegating your vote, browsing actions, the constitution, submitting actions as a human) lives on the participant hub at [cardano.org/governance](https://cardano.org/governance). This module is about *building* staking and governance features, not operating a pool or participating by hand. ::: ## Next steps - Start with [Staking](/docs/developers/curriculum/staking-governance/staking), or jump straight to [Delegate and withdraw](/docs/developers/curriculum/staking-governance/delegate-and-withdraw) if you already know the model - The module ends by handing over to [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview), where the script credentials met here get their validation logic --- ## Staking Staking is how ADA holders earn rewards by backing the network's security: they delegate their stake to a pool, and the pool produces blocks proportional to the stake delegated to it. From a developer's point of view, this is something your wallet or dApp can offer with a few certificate types on top of an ordinary transaction. This page is the concept: what makes Cardano's model different, how rewards and timing work, and the lifecycle every integration follows. Two practice pages then build it: [Delegate and withdraw](/docs/developers/curriculum/staking-governance/delegate-and-withdraw) for the certificate flows and [Manage stake](/docs/developers/curriculum/staking-governance/manage-stake) for queries, deregistration, and combining stake with vote delegation. Running a pool as infrastructure (relays, block producers, KES keys, monitoring) is a separate discipline with its own section: [Operate a Stake Pool](/docs/operators/). ## What makes Cardano staking different Cardano's delegation is **non-custodial**, which is a strong selling point to surface in your UI: - **Your ADA never leaves your wallet.** You issue an on-chain certificate that counts your stake toward a pool; you keep full spending control. - **No lock-up.** Your ADA stays liquid, spendable at any time. - **No minimum to delegate.** Any amount counts toward the pool. Registering your stake key the first time costs a refundable deposit (`stakeAddressDeposit`, currently 2 ADA), returned when you deregister. - **No slashing.** Delegated ADA is never at risk. If a pool underperforms, you simply miss rewards for that epoch. You never lose principal. (Contrast Ethereum, where validators can be slashed.) - **Automatic re-delegation.** Add more ADA to the wallet and it's included from the next snapshot. The stake credential is separate from the payment credential. Delegating doesn't move funds, it just assigns the staking rights attached to your address. See [Addresses](/docs/developers/curriculum/fundamentals/core-concepts/addresses) for how payment and delegation credentials combine. ## How rewards and timing work Rewards don't arrive instantly. Because of how Ouroboros calculates slot leadership from a stake snapshot, there's a built-in delay before a fresh delegation starts earning: ```text Epoch N you delegate Epoch N+1 snapshot taken at the epoch boundary Epoch N+2 the pool produces blocks using your stake Epoch N+3 rewards calculated Epoch N+4 rewards distributed to your reward address ``` After this initial delay (~15 to 20 days), rewards arrive every epoch (~5 days) as long as the pool produces blocks. Two things worth showing users: - **Saturation.** Each pool has a saturation point (total stake ÷ `k0`, the [target number of pools](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros#how-do-rewards-and-incentives-drive-decentralization), currently 500). Past it, rewards *per ADA* drop, a built-in nudge toward smaller pools and decentralization. Do not confuse `k0` with the security parameter `k` (2160), which bounds rollback depth. - **Performance.** A pool that misses assigned blocks earns fewer rewards, which flows through to delegators. The deeper consensus mechanics (epochs, slots, VRF leader selection, the reward formula) are in [Consensus & Ouroboros](/docs/developers/curriculum/fundamentals/consensus-and-ouroboros). ## The staking lifecycle Every staking integration is some subset of the same five steps: ```mermaid flowchart LR R["Registerstake credential"] --> D["Delegateto a pool"] D --> E["Earn rewardseach epoch"] E --> W["Withdrawrewards"] W --> E D --> X["Deregister &reclaim deposit"] ``` 1. **Register**: create the stake credential on-chain (a small refundable deposit). 2. **Delegate**: assign the stake to a pool (and, separately, [a DRep for voting](/docs/developers/curriculum/staking-governance/drep-and-delegation#delegate-your-vote)). 3. **Earn**: rewards accrue to the reward address each epoch. 4. **Withdraw**: claim accumulated rewards into the wallet. 5. **Deregister**: optional; remove the credential and reclaim the deposit. ## Beyond keys and pools Two directions the practice pages point at when you need them: - **Script-controlled stake.** A stake credential can be a Plutus script instead of a key, which is how DeFi protocols run one validation for a whole transaction (the withdraw-zero trigger). The pattern, its off-chain submission, and the on-chain handlers live in the [Stake Validator design pattern](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) and [Write a validator](/docs/developers/curriculum/smart-contracts/write-a-validator#certificate-validator). - **Pool tooling.** If you build pool-management tooling, pools can be registered, updated, and retired from code (Evolution's `registerPool` and `retirePool`, with [CIP-6](https://cips.cardano.org/cip/CIP-0006) metadata); Mesh has no pool helpers. Running a pool itself is the [Operate a Stake Pool](/docs/operators/) discipline. ## Next steps - [Delegate and withdraw](/docs/developers/curriculum/staking-governance/delegate-and-withdraw): register, delegate, and withdraw rewards in code - [Manage stake](/docs/developers/curriculum/staking-governance/manage-stake): query status, deregister, and delegate stake and vote together --- ## Vote & Propose With a [registered DRep](/docs/developers/curriculum/staking-governance/drep-and-delegation) (or a committee hot credential or a pool key), what remains is using it: casting votes on live actions, and submitting actions of your own. The [seven action types and their thresholds](/docs/developers/curriculum/staking-governance/governance#the-seven-governance-action-types) are on the concept page; this page is the code. ## Vote on an action Registered DReps (and CC members and SPOs, for their action types) cast Yes / No / Abstain votes against a specific governance action, identified by the transaction that created it and its index. ```typescript declare const drep: DRep.DRep const voter = new VotingProcedures.DRepVoter({ drep }) declare const govActionTxHash: TransactionHash.TransactionHash const govActionId = new GovernanceAction.GovActionId({ transactionId: govActionTxHash, govActionIndex: 0n, }) const procedure = new VotingProcedures.VotingProcedure({ vote: VotingProcedures.yes(), // or .no() / .abstain() anchor: null, }) const votingProcedures = VotingProcedures.singleVote(voter, govActionId, procedure) const tx = await client.newTx().vote({ votingProcedures }).build() const signed = await tx.sign() await signed.submit() ``` The voter can be a DRep, a Constitutional Committee hot credential, or an SPO pool key hash. DRep and CC voters may be script-controlled; the builder detects this and requires a redeemer. ```typescript const dRep = await wallet.getDRep() txBuilder .vote( { type: "DRep", drepId: dRep.dRepIDCip105 }, { txHash: "aff2909f...c0867cc", txIndex: 0 }, // the governance action id { voteKind: "Yes" }, // optional rationale: { anchorUrl, anchorDataHash } ) .selectUtxosFrom(await wallet.getUtxosMesh()) .changeAddress(await wallet.getChangeAddressBech32()) const unsignedTx = await txBuilder.complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` Create the vote file, choosing `--yes`, `--no`, or `--abstain` and the governance action id (tx id + index). Sign as a DRep (`--drep-verification-key-file`), a CC member (`--cc-hot-verification-key-file`), or an SPO (`--cold-verification-key-file`): ```bash cardano-cli latest governance vote create \ --yes \ --governance-action-tx-id "df58f714c0765f3489afb6909384a16c31d600695be7e86ff9c59cf2e8a48c79" \ --governance-action-index 0 \ --drep-verification-key-file drep.vkey \ --out-file action.vote ``` Include the vote in a transaction with `--vote-file`, sign with the matching credential plus the payment key, and submit: ```bash cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --vote-file action.vote \ --witness-override 2 \ --out-file vote-tx.raw cardano-cli latest transaction sign \ --tx-body-file vote-tx.raw \ --signing-key-file drep.skey \ --signing-key-file payment.skey \ --out-file vote-tx.signed cardano-cli latest transaction submit --tx-file vote-tx.signed ``` ## Submit a proposal Anyone can submit any of the seven action types on-chain with a deposit (`govActionDeposit`, refunded to your reward account after the vote). ```typescript declare const governanceAction: GovernanceAction.GovernanceAction declare const rewardAccount: RewardAccount.RewardAccount declare const anchor: Anchor.Anchor const tx = await client .newTx() .propose({ governanceAction, rewardAccount, anchor }) .build() const signed = await tx.sign() await signed.submit() ``` Chain multiple `.propose(...)` calls to submit several actions in one transaction. The deposit is deducted automatically during balancing. ```typescript const rewardAddress = (await wallet.getRewardAddresses())[0] txBuilder .proposal( { kind: "InfoAction", action: {} }, // the governance action { anchorUrl: "https://example.com/proposal.jsonld", // CIP-108 metadata anchorDataHash: "a1b1c2d3e4f5..." }, rewardAddress, // deposit-return reward account ) .selectUtxosFrom(await wallet.getUtxosMesh()) .changeAddress(await wallet.getChangeAddressBech32()) const unsignedTx = await txBuilder.complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` `governanceAction` is a discriminated union: swap `InfoAction` for `TreasuryWithdrawalsAction`, `ParameterChangeAction`, `NoConfidenceAction`, `UpdateCommitteeAction`, `NewConstitutionAction`, or `HardForkInitiationAction` (the chaining types take a `govActionId` of the last enacted action of that kind). The deposit defaults to `govActionDeposit`; pass a fourth argument to override. For a Plutus-script proposal, add `proposalScript(cbor, "V3")` and `proposalRedeemerValue(redeemer)`. Authoring an action produces a proposal: a deposit, a deposit-return stake credential, an [anchor](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0108) (URL + hash), and the action itself. Hash the anchor, create the action (treasury withdrawal shown), then build, sign, and submit: ```bash cardano-cli hash anchor-data --file-text treasury-withdrawal.jsonld cardano-cli latest governance action create-treasury-withdrawal \ --testnet \ --governance-action-deposit $(cardano-cli latest query gov-state | jq -r '.currentPParams.govActionDeposit') \ --deposit-return-stake-verification-key-file stake.vkey \ --anchor-url https://example.com/treasury-withdrawal.jsonld \ --anchor-data-hash 311b148ca792007a3b1fee75a8698165911e306c3bc2afef6cf0145ecc7d03d4 \ --funds-receiving-stake-verification-key-file stake.vkey \ --constitution-script-hash fa24fb305126805cf2164c161d852a0e7330cf988f1fe558cf7d4a64 \ --transfer 50000000000 \ --out-file treasury.action cardano-cli latest transaction build \ --tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]') \ --change-address $(< payment.addr) \ --proposal-file treasury.action \ --out-file tx.raw # then transaction sign + submit as in the sections above ``` Treasury-withdrawal and protocol-parameter actions also reference the guardrails script: add `--proposal-script-file guardrails-script.plutus`, `--tx-in-collateral`, and `--proposal-redeemer-value {}` to the build. ### Authoring each action type Every action takes `--governance-action-deposit`, `--deposit-return-stake-verification-key-file`, `--anchor-url`, `--anchor-data-hash`, and `--out-file`. Types that share state (committee, constitution, hard fork, protocol parameters) also need `--prev-governance-action-tx-id` and `--prev-governance-action-index` once a prior action of that type was enacted; treasury withdrawals and info actions never do. - **Treasury withdrawal** (`create-treasury-withdrawal`): adds `--funds-receiving-stake-verification-key-file`, `--transfer `, and `--constitution-script-hash`. - **Protocol-parameter update** (`create-protocol-parameters-update`): the parameter flags being changed, plus `--constitution-script-hash`. - **Constitution / guardrails** (`create-constitution`): `--constitution-url`, `--constitution-hash`, and `--constitution-script-hash`. - **Update committee** (`governance action update-committee`): `--add-cc-cold-verification-key-hash ` paired with `--epoch `, `--remove-cc-cold-verification-key-hash `, and `--threshold `. - **No confidence** (`create-no-confidence`): the common flags plus the previous committee-action reference. - **Hard fork** (`create-hard-fork`): initiates a protocol upgrade. - **Info** (`create-info`): common flags only, with no on-chain effect. ## Next steps - [Governance operations](/docs/developers/curriculum/staking-governance/governance-operations): committee credential management, governance-state queries, and the CIP-95 wallet API --- ## Set up your AI assistant AI coding assistants are fast, but their training data on Cardano drifts: APIs change, libraries get renamed, and patterns evolve faster than models are retrained. The fix is to give your assistant current, authoritative context, and it costs a couple of minutes to set up. ## Cardano Dev Skills [Cardano Dev Skills](https://github.com/cardano-foundation/cardano-dev-skills) is the Cardano Foundation's toolkit for keeping an assistant current. It works with any AI coding agent that reads Markdown, bundling authoritative Cardano documentation and behavioral "skills" refreshed weekly from upstream project repositories, so your assistant answers from current sources rather than guessing from training data. It ships: - **Developer skills** for common workflows: writing validators, building transactions, governance, optimization, and debugging. - **Bundled documentation** pulled from active Cardano projects and auto-refreshed weekly. - **Hooks** that make the agent consult the bundled context before falling back on its training data. Its scope is the developer toolchain (SDKs, validator libraries, design patterns, language tooling, protocol specs, and reference implementations), not the product docs of specific deployed apps. ### Add it to your agent The skills are plain Markdown, so any agent that reads Markdown can use them. In Claude Code: ``` /plugin marketplace add cardano-foundation/cardano-dev-skills /plugin install cardano-dev-skills@cardano-dev-skills ``` Then run `/cardano-context` once per project to wire the directive into your `CLAUDE.md`. For Codex or any other agent, clone the repo and symlink the skills into your project's `.agents/skills` directory, or point the agent at the Markdown directly, then add the equivalent directive to whatever file that agent reads at startup. The [repository](https://github.com/cardano-foundation/cardano-dev-skills) has the full list of skills and setup details. ## Going deeper on a specific SDK Start with Cardano Dev Skills: it aggregates context across the whole toolchain and stays tool-agnostic while you are still deciding how to build. Once you've committed to a specific SDK, that SDK may ship its own AI context you can add on top, for depth on its API: correct method ordering, transaction patterns, and framework-specific mappings. [Mesh](https://meshjs.dev/ai) shows what that looks like: - **Agent Skills**: `npx skills add MeshJS/skills` installs deep SDK knowledge across `mesh-transaction` (MeshTxBuilder, minting, Plutus spending, staking, governance), `mesh-wallet` (CIP-30 and headless wallets, CIP-8 signing), and `mesh-core-cst` (CBOR and Plutus data serialization). The CLI detects your installed AI tools and drops the skills in the right place. - **MCP server**: the [`meshjs-mcp`](https://meshjs.dev/ai/mcp) server gives your assistant real-time access to Mesh docs and code generation in VS Code, Cursor, or Claude Desktop. - **llms.txt**: paste [`https://meshjs.dev/llms.txt`](https://meshjs.dev/llms.txt) into any assistant for a single, current file of the full Mesh API. Reach for these only when you're working in Mesh and want more than Cardano Dev Skills already gives you. ## Next steps - [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction): build, sign, and submit a payment on testnet, then read it back from the chain - [Connect an AI assistant with MCP](/docs/developers/curriculum/dapps/ai-agents/mcp): beyond writing code, let an assistant read your live Cardano state and draft transactions you sign --- ## Choose Your Tools You put three things in place before writing code: a **library (SDK)** that builds transactions in your language, a **provider** that connects it to the chain, and current Cardano context for your AI assistant, if you code with one. None of these lock you in. Every SDK produces transactions against the same ledger rules, so what you learn about UTXOs, datums, and fees carries over if you switch. Providers sit behind one interface in most SDKs, so changing one is a config line. ## Start from a template, or from scratch If you would rather have something running before you understand it, start from a template. The [templates gallery](/templates) has wallet-connected starters that already wire an SDK, a provider, and a frontend framework together, each with the command that copies it into a new project. The rest of this page is the setup those templates did for you. Skim it now and come back when you want to change one of the pieces. ## Your language, and its SDK An SDK handles the parts of Cardano you do not want to reimplement: assembling and balancing a transaction, coin selection, fee calculation, CBOR serialization, key derivation, and talking to a provider. You work in your own language and it produces bytes a node accepts. The code tabs across this curriculum use **Evolution** and **Mesh**, both TypeScript. That is the only reason they appear here: with one of them installed, every example on the site runs as written. Nothing you learn depends on either one. Cardano has SDKs in Python (PyCardano), Rust (Whisky), Go (Apollo), C# (Chrysalis), Java, Swift, and more, alongside lower-level serialization libraries. [Builder Tools](/tools/?tags=sdk) lists them by language and by what each one covers. ## Install it ```bash npm install @evolution-sdk/evolution ``` ```bash npm install @meshsdk/core ``` `cardano-cli` ships with the node. Install it from the [cardano-node releases](/docs/operators/node/installing-cardano-node), or skip local setup entirely by using a provider with an SDK. Anything else installs with its own package manager. Its [Builder Tools](/tools/?tags=sdk) entry links to the repository, where the install line lives. ## How your code reaches the chain Your SDK does not reach the chain on its own. It sits on top of a **provider**, which runs the node infrastructure and exposes the chain through an API, so you can read UTXOs and submit transactions without operating a node yourself. The full path is your code → SDK → provider → node → chain. [Query the chain](/docs/developers/curriculum/start-building/query-the-chain#choosing-a-provider) compares the providers on hosting, keys, and rate limits, and shows how to configure one on a client. [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain) maps the full range, from a hosted API to running the infrastructure yourself. To follow along you need a key, and Blockfrost has a free tier: 1. Sign up at [blockfrost.io](https://blockfrost.io/). 2. Create a project and select **Preprod**, the network these examples use. 3. Copy the project ID. That is your API key, and it starts with `preprod`. 4. Store it in an env var. Never commit it, and never ship it in client-side code: ```bash # .env BLOCKFROST_API_KEY=preprodYourProjectIdHere ``` ## Context for your AI assistant Model training data on Cardano drifts. APIs change, libraries get renamed, and patterns move faster than models are retrained, so an assistant left to its training data writes code against a version of the ecosystem that no longer exists. [Cardano Dev Skills](https://github.com/cardano-foundation/cardano-dev-skills) is the Cardano Foundation's answer: Markdown skills and bundled documentation refreshed weekly from upstream repositories, usable by any agent that reads Markdown. In Claude Code: ``` /plugin marketplace add cardano-foundation/cardano-dev-skills /plugin install cardano-dev-skills@cardano-dev-skills ``` Then run `/cardano-context` once per project. [Set up your AI assistant](/docs/developers/curriculum/start-building/ai-assisted-development) covers other agents, what the skills contain, and the extra context individual SDKs ship on top. :::tip Beyond writing code An assistant can also read live chain state and draft transactions for you to sign. [Connect an AI assistant with MCP](/docs/developers/curriculum/dapps/ai-agents/mcp) covers that. ::: ## What you are not choosing yet - **The language you write smart contracts in.** A separate toolchain, covered in [Smart contracts](/docs/developers/curriculum/smart-contracts/choose-a-language). Every smart contract example in this curriculum is Aiken. - **Where you run the chain.** Preprod is enough to start. You can also run a [local devnet](/docs/developers/curriculum/start-building/local-testing#local-devnets), with block times you set, later in this module. ## Next steps - [Choose a network](/docs/developers/curriculum/start-building/networks-and-test-ada): pick where your code runs, and get free test ADA to build with - [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction): wire the SDK and provider together and send ADA --- ## Local Testing On Preprod, every attempt costs a 20-second confirmation, faucet funds, and public visibility. Most of what goes wrong in transaction code needs none of that to catch: you selected the wrong UTXO, wrote the datum in the wrong shape, forgot a required signer, or set a validity window that has already passed. You can run the whole loop locally instead, in one of two ways: simulate the ledger in memory, or run a real private chain. | Option | What runs | Use it for | |---|---|---| | [In-memory emulator](#the-in-memory-emulator) | A simulated ledger in your test process | Unit tests and CI: submit, evaluate scripts, and assert on state in milliseconds | | [Programmatic devnet](#programmatic-devnets) | A real node in Docker, started by your tests | Integration tests over the full build, sign, submit, confirm lifecycle | | [Standalone devnet](#local-devnets) | A real node you leave running | A chain to develop and demo against, and to point a frontend at | The public testnets stay the final rehearsal, with real traffic and real timing ([Choose a network](/docs/developers/curriculum/start-building/networks-and-test-ada)). This page is about your transaction code; for testing validator logic itself, see [Testing](/docs/developers/curriculum/smart-contracts/testing) in the smart contracts module. The two SDKs in this curriculum divide the ground as mirror images: Mesh ships the in-memory emulator and drives Yaci DevKit when it needs a real chain; Evolution ships its own devnet. ## The in-memory emulator `ScalusEmulator` (`@meshsdk/scalus-emulator`) is an in-memory ledger that accepts submissions and evolves. It comes from [Scalus](https://scalus.org), a Cardano development platform whose ledger implementation compiles to JavaScript; Mesh wraps it in its fetcher, submitter, and evaluator interfaces, so the `MeshTxBuilder` code you wrote in [Transaction building](/docs/developers/curriculum/start-building/transaction-building) runs against it unchanged, where a real provider would go. Submitted transactions are validated under real ledger rules and mutate the emulator's state: scripts are evaluated, fees are charged, and a transaction with an expired validity window is rejected, all in your test process. ```typescript const provider = new ScalusEmulator( [ { input: { txHash: "0000000000000000000000000000000000000000000000000000000000000000", outputIndex: 0 }, output: { address, amount: [{ unit: "lovelace", quantity: "1000000000" }] }, }, ], SLOT_CONFIG_NETWORK["preview"], ); await provider.setSlot(unixTimeToEnclosingSlot(Date.now(), SLOT_CONFIG_NETWORK["preview"])); const txBuilder = new MeshTxBuilder({ fetcher: provider, submitter: provider, evaluator: provider }); const txHex = await txBuilder .txOut(address, [{ unit: "lovelace", quantity: "5000000" }]) .changeAddress(address) .selectUtxosFrom(await provider.fetchAddressUTxOs(address)) .complete(); const txHash = await provider.submitTx(await wallet.signTx(txHex)); // the ledger moved: the next fetch reflects the spend, minus fees const after = await provider.fetchAddressUTxOs(address); ``` Seed it with initial UTXOs and a slot configuration: `SLOT_CONFIG_NETWORK` gives you a public network's timing, and `setSlot` positions the clock, which is what makes validity-window tests meaningful. The emulator validates ledger rules but it is not a node: there is no networking, no real confirmation timing, and no provider API surface. When those matter, move down the table to a [programmatic devnet](#programmatic-devnets). ## Local devnets A **local devnet** is a private Cardano network running entirely on your own machine. It runs the same node software and enforces the same ledger rules as the public networks, but nothing is inherited: block time, epoch length, era, protocol parameters, and the initial balances are all yours to configure. That control is what a devnet is for. With 200-millisecond blocks, a test suite that takes minutes against Preprod finishes in seconds. With a short epoch length, rewards and epoch-boundary logic that would take days to observe on a testnet arrive in minutes. Rollbacks, custom protocol parameters, and specific eras can all be arranged on demand, which no public network offers. A devnet also works offline, needs no faucet, and keeps your work private until you choose to deploy it. The trade-off is realism: a network only you use has none of the traffic and contention of a shared one, so validate on Preprod once your application stabilizes. There are two ways to run one, suited to different jobs: - **Standalone devnet**: a process you start and leave running, then point a frontend, `cardano-cli`, or a provider API at. State persists across your app's runs, so this is the one you develop and demo against. - **Programmatic devnet**: a cluster your test code starts and tears down itself. Fresh state on every run makes it the right shape for automated integration tests. | | Yaci DevKit | Evolution devnet | | --- | --- | --- | | **Kind** | Standalone | Programmatic (Docker) | | **Setup** | Docker Compose, zip, or NPM | `npm install`, runs in your test code | | **Includes** | Indexer, viewer, Ogmios, Kupo, Blockfrost-compatible API | Node, Kupo, and Ogmios via Docker | | **Best for** | Integration testing, SDK development, a chain to point a frontend at | Automated integration tests over the full build, sign, submit, confirm lifecycle | ### Yaci DevKit [Yaci DevKit](https://devkit.yaci.xyz/introduction) is the quickest way to a standalone devnet. Alongside the node it bundles an indexer (Yaci Store), a browser viewer for transactions and blocks, Ogmios and Kupo, and a **Blockfrost-compatible API**, so an SDK configured for Blockfrost connects to your devnet unchanged. Run it with [Docker Compose](https://devkit.yaci.xyz/getting-started/docker), a [standalone zip](https://devkit.yaci.xyz/getting-started/zip), or the [NPM package](https://devkit.yaci.xyz/getting-started/npm), which is handy in CI. You create the chain from the DevKit's shell, and that is where you set its pace: ```shell devnet:default> create-node -o --start --block-time 0.2 --epoch-length 60 ``` `--block-time` and `--slot-length` accept sub-second values, `--epoch-length` (in slots) brings epoch boundaries and rewards around in seconds instead of days, `--era` selects `conway` or `babbage`, and `--enable-multi-node` runs several block producers so you can test that your code survives a rollback. The devnet starts with 20 addresses funded from a well-known test mnemonic, and `topup` funds any other address; the [CLI commands](https://devkit.yaci.xyz/commands) reference covers the rest. Connect your SDK through the Blockfrost-compatible endpoint: ```typescript // Yaci's API speaks Blockfrost const client = Client.make(preprod).withBlockfrost({ baseUrl: "http://localhost:8080/api/v1", projectId: "" }) ``` ```typescript const provider = new YaciProvider("http://localhost:8080/api/v1/") ``` ### Programmatic devnets Some SDKs launch a real local cluster from your test code: a node with Kupo and Ogmios in Docker containers that your code starts, funds from genesis, and removes when the suite ends. Because the network lives and dies with the test run, every run gets fresh, isolated state, and the full build → sign → submit → confirm lifecycle runs offline with no faucet in the loop. Evolution ships this as `@evolution-sdk/devnet`. An integration test spins the cluster up once, funds a wallet from genesis, and asserts on confirmation: ```typescript const mnemonic = "test test test ... sauce" const addressHex = Address.toHex(Address.fromSeed(mnemonic, { accountIndex: 0, networkId: 0 })) const genesisConfig = { ...Config.DEFAULT_SHELLEY_GENESIS, slotLength: 0.1, initialFunds: { [addressHex]: 10_000_000_000_000 } } const cluster = await Cluster.make({ clusterName: "test-suite", ports: { node: 3001, submit: 3002 }, shelleyGenesis: genesisConfig, kupo: { enabled: true, port: 1442 }, ogmios: { enabled: true, port: 1337 }, }) await Cluster.start(cluster) const client = Client.make(Cluster.getChain(cluster)) .withKupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" }) .withSeed({ mnemonic, accountIndex: 0 }) // genesis UTXOs aren't Kupo-indexed until first spent, so pass them explicitly const genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) const tx = await client.newTx() .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: Assets.fromLovelace(5_000_000n) }) .build({ availableUtxos: genesisUtxos }) const txHash = await (await tx.sign()).submit() await client.awaitTx(txHash, 1000) ``` The genesis object is where the chain's behaviour lives: `slotLength: 0.1` gives 100-millisecond slots, which is why `awaitTx` returns in the time a test can afford. Spread `Config.DEFAULT_SHELLEY_GENESIS` and override what you need. Give cluster startup a generous timeout (it launches Docker containers), keep `clusterName` unique to avoid port clashes in parallel runs, and tear it down with `Cluster.stop` and `Cluster.remove` when the suite ends. For the full reference see the [Evolution SDK devnet docs](https://intersectmbo.github.io/evolution-sdk/docs/devnet/getting-started/). Mesh ships no cluster of its own; its integration tests drive a Yaci devnet through `YaciProvider` ([above](#yaci-devkit)). ## Next steps - [When transactions fail](/docs/developers/curriculum/start-building/transaction-failures): the failure modes these tests catch before a network does - [Testing](/docs/developers/curriculum/smart-contracts/testing): unit- and integration-test the validators themselves - [Going to production](/docs/developers/curriculum/production/going-to-production): reliability and security before mainnet --- ## Choose a Network Every Cardano network runs the same node software and ledger rules, so code you develop against a testnet behaves the same on mainnet. The networks themselves are kept strictly separate: addresses carry a network tag (testnet addresses start with `addr_test`), so a testnet transaction can never land on mainnet by accident. What differs between them is how fast they run, who can see your activity, and what mistakes cost. Development moves through them in order: devnets and testnets to find things out, mainnet to launch. You never need real ADA to develop; the testnets use test ADA (tAda), which has no value and is free from a faucet. | Network | What it is | Use it for | |---|---|---| | **[Local devnet](/docs/developers/curriculum/start-building/local-testing#local-devnets)** | Private, fully configurable network on your own machine | Fast iteration, CI, offline work, and conditions the public networks can't provide | | **Preview** | Public testnet that receives protocol upgrades weeks before mainnet | Upcoming features; its 1-day epochs also make staking and reward testing faster | | **Preprod** | Public testnet running mainnet's current protocol and parameters | Day-to-day development and final pre-launch validation | | **Mainnet** | The production network; transactions spend real ADA and are irreversible | Launching, after testnet validation | Mainnet produces a block roughly every 20 seconds and the public testnets match it, so they give you real operating conditions: real confirmation times, other participants' transactions in the same blocks, and activity that anyone can inspect. A local devnet trades that realism for control and privacy. You set the block time, epoch length, and protocol parameters, and nothing you do leaves your machine; keep the default parameters and the ledger validates your transactions exactly as the public networks would. Develop on Preprod by default, and move to a [local devnet](/docs/developers/curriculum/start-building/local-testing#local-devnets) when confirmation times slow your loop. Devnets and the tests that need no chain at all are both covered in [Local testing](/docs/developers/curriculum/start-building/local-testing). Some tools identify networks by number ("network magic") rather than name: Preprod is `1` and Preview is `2`, as in cardano-cli's `--testnet-magic 1`, while mainnet tooling takes `--mainnet`. ## Get test ADA Request tAda for Preprod or Preview from the [Cardano Testnet Faucet](https://docs.cardano.org/cardano-testnets/tools/faucet): paste your wallet address, click "Request funds", and it arrives within a minute or two. You need a testnet address first, which your wallet or SDK generates ([Keys & Wallets](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys)). The faucet rate-limits per address and asks that you return unused tAda when a project ends. A [local devnet](/docs/developers/curriculum/start-building/local-testing#local-devnets) needs no faucet: you define the starting balances in its genesis configuration, so funds exist the moment the chain starts. ### Testnet wallets Most Cardano browser and mobile wallets support both testnets: switch the network to Preprod or Preview in settings ([cardano.org/apps](https://cardano.org/apps) lists them). Hardware devices work through the same browser extensions. When building programmatically, your SDK generates and manages addresses itself (see [Choose your tools](/docs/developers/curriculum/start-building/choose-your-tools)). ## Block explorers Inspect transactions, addresses, and blocks at [explorer.cardano.org](https://explorer.cardano.org/), which aggregates the major Cardano explorers and supports deeplinks. Append the network for the testnets: [/preprod](https://explorer.cardano.org/preprod) or [/preview](https://explorer.cardano.org/preview). ## Next steps - [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction): now build, sign, and submit one - [Set up your AI assistant](/docs/developers/curriculum/start-building/ai-assisted-development): what the Cardano context contains, and how to add it to any agent --- ## Start Building This is the hands-on on-ramp. By the end you will have a working environment, your tool of choice installed, and a real transaction submitted to a Cardano testnet, all without spending real ADA. It assumes you have the mental model from [Cardano Fundamentals](/docs/developers/curriculum/fundamentals/overview) and its [Core Concepts](/docs/developers/curriculum/fundamentals/core-concepts/overview); if a concept here is unfamiliar, those pages explain it. ## The path 1. **[Choose your tools](/docs/developers/curriculum/start-building/choose-your-tools)**: pick an SDK for your language and get a provider key 2. **[Choose a network](/docs/developers/curriculum/start-building/networks-and-test-ada)**: pick where your code runs, get free test ADA from the faucet, and find a block explorer 3. **[Set up your AI assistant](/docs/developers/curriculum/start-building/ai-assisted-development)**: what the Cardano context contains, how to add it to any agent, and the extra context each SDK ships 4. **[Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction)**: build, sign, and submit a payment, then read it back from the chain 5. **[Transaction building](/docs/developers/curriculum/start-building/transaction-building)**: the full builder toolkit, multi-asset outputs, metadata, and patterns beyond a simple payment 6. **[Query the chain](/docs/developers/curriculum/start-building/query-the-chain)**: read UTXOs, addresses, and history through a provider 7. **[Local testing](/docs/developers/curriculum/start-building/local-testing)**: speed up your loop with an in-memory emulator or a devnet you control 8. **[When transactions fail](/docs/developers/curriculum/start-building/transaction-failures)**: the failure modes, which ones are retryable, and how to triage them ## Where this leads Once you can send and query a transaction, you are ready to build real things: [mint native tokens and NFTs](/docs/developers/curriculum/native-tokens/overview), or move on to [smart contracts](/docs/developers/curriculum/smart-contracts/overview). --- ## Query the Chain Reading is the other half of building. Before you build a transaction you need UTXOs and protocol parameters; after you submit one you wait for confirmation; a dApp UI shows balances, datums, and delegation. All of it comes from **querying the chain** through a **provider**, so you don't have to run and index a node yourself. The conceptual model (UTXOs, datums) is in [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions) and [eUTXO](/docs/developers/curriculum/fundamentals/core-concepts/eutxo); this page is the read-side how-to. ## Choosing a provider A provider is the data source your SDK talks to. Most SDKs support several behind one unified interface, so the query methods stay the same no matter which you pick: | Provider | Hosting | API key | Rate limits | |---|---|---|---| | **Blockfrost** | Hosted | Required | Yes (free tier limited) | | **Maestro** | Hosted | Required | Yes (free tier limited) | | **Koios** | Hosted (community) or self-hosted | Optional | Yes (higher with a key) | | **Kupmios** | Self-hosted (Ogmios + Kupo) | Not applicable | None (your own infra) | Those are the common choices; [Builder Tools](/tools/?tags=api) lists the hosted providers in full. Configure one when you make the client: ```typescript // Blockfrost (hosted) const bf = Client.make(mainnet).withBlockfrost({ baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! }) // Kupmios (self-hosted Ogmios + Kupo) const kupmios = Client.make(mainnet).withKupmios({ ogmiosUrl: "http://localhost:1337", kupoUrl: "http://localhost:1442" }) // Maestro (hosted) const maestro = Client.make(mainnet).withMaestro({ baseUrl: "https://mainnet.gomaestro-api.org/v1", apiKey: process.env.MAESTRO_API_KEY! }) // Koios (community) const koios = Client.make(mainnet).withKoios({ baseUrl: "https://api.koios.rest/api/v1" }) ``` ```typescript // Blockfrost (hosted), network auto-detected from the key prefix const bf = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID!) // Koios (community), pass the network const koios = new KoiosProvider("mainnet") // Maestro (hosted) const maestro = new MaestroProvider({ network: "Mainnet", apiKey: process.env.MAESTRO_API_KEY! }) // Ogmios (self-hosted; Mesh has no single "Kupmios", pair it with Kupo for indexed reads) const ogmios = new OgmiosProvider("ws://localhost:1337") ``` In Mesh the read methods live on the **provider** (an `IFetcher`/`ISubmitter`), not on a unified client. You pass the provider to `MeshTxBuilder` and the wallet, and call its `fetch*` methods directly. Use the matching network base URL for Preprod/Preview (e.g. `https://cardano-preprod.blockfrost.io/api/v0`). For a **hosted Kupmios** like [Demeter](https://demeter.run), pass the API keys through the connection with the `headers` option on `withKupmios`: ```typescript const client = Client.make(mainnet).withKupmios({ ogmiosUrl: "https://ogmios.demeter.run", kupoUrl: "https://kupo.demeter.run", headers: { ogmiosHeader: { "dmtr-api-key": process.env.DEMETER_API_KEY! }, kupoHeader: { "dmtr-api-key": process.env.DEMETER_API_KEY! } } }) ``` Mesh has no single Kupmios provider; pair `OgmiosProvider` with Kupo and pass the Demeter keys through each provider's connection options. Because the interface is unified, switching provider (e.g. Blockfrost in dev, self-hosted Kupmios in prod) is a one-line change. The query calls stay the same. For setting up the provider infrastructure itself, see [Use a provider](/docs/developers/curriculum/production/use-a-provider) (Blockfrost, Koios, and Maestro projects) and [Self-hosting](/docs/developers/curriculum/production/self-hosting) (your own node + Kupo + Ogmios, a data node, Demeter). :::tip Privacy and trust A **hosted** provider sees every address you query and every transaction you submit, along with your IP. It's a third party in your data path, with rate limits and an uptime you don't control. **Self-hosting** (your own node + Kupo + Ogmios, or Kupmios) keeps that data private and removes the dependency, at the cost of running the infrastructure. Pick based on how sensitive your queries are and how much ops you want to own. ::: ## Provider-only, read-only, or signing client How you configure the client decides what it can do: | Client | Configured with | Query any address | Query own wallet | Build tx | Sign | |---|---|---|---|---|---| | **Provider-only** | provider | Yes | - | - | - | | **Read-only** | provider + address | Yes | Yes | Yes (unsigned) | - | | **Signing** | provider + wallet (seed/key/CIP-30) | Yes | Yes | Yes | Yes | A **provider-only** client is all you need to read the chain, a block explorer, a submission service, a monitor. Add a wallet address (**read-only**) to also build unsigned transactions for a specific user (the [backend-builds pattern](/docs/developers/curriculum/dapps/connect-a-wallet#frontend-signs-backend-builds-and-submits)); add a [wallet](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys#working-with-wallets-in-code) to sign. ## Querying chain data You'll read a handful of things off the chain, each a single query through the client. ### Off-chain helpers you'll reach for Querying gives you raw chain data; turning addresses, datums, and assets into the hashes and identifiers your code needs is the other half. Both SDKs ship the same family of pure helpers for this, so you can call them in a backend without a provider. The calls differ in name, not in what they return: ```typescript // Address -> credentials const { paymentCredential, stakingCredential, networkId } = Address.getAddressDetails("addr_test1...") const payment = Address.getPaymentCredential("addr_test1...") // payment credential only // Unit -> policy id + asset name const { policyId, assetName, label } = Unit.fromUnit(unit) // Time -> slot for a network const slot = Time.unixTimeToSlot(Date.now(), slotConfig) // CIP-14 fingerprint: compute from policy + name (no one-call helper) ``` ```typescript // Address -> credentials const { pubKeyHash, scriptHash, stakeCredentialHash } = deserializeAddress("addr_test1...") const paymentKeyHash = resolvePaymentKeyHash("addr_test1...") // payment key hash only // Unit -> policy id + asset name (slice; unit = policyId + assetNameHex) const policyId = unit.slice(0, 56) const assetNameHex = unit.slice(56) // Time -> slot for a network const slot = resolveSlotNo("preprod") // CIP-14 fingerprint const fingerprint = resolveFingerprint(policyId, assetNameHex) ``` Mesh additionally ships one-call helpers like `resolveDataHash` (datum hash), `serializeNativeScript`, and `resolveScriptHashDRepId`; in Evolution you reach the same results through its `Data`, `NativeScripts`, and credential modules. Either way these are pure (network-aware only for slot conversion), so they belong in a backend without a provider. ### UTXOs and balances ```typescript // Any address const utxos = await client.getUtxos(Address.fromBech32("addr_test1...")) // Your wallet, and its total ADA const mine = await client.getWalletUtxos() const balance = mine.reduce((sum, u) => sum + u.assets.lovelace, 0n) // Find UTXOs holding a specific asset, or the single UTXO holding an NFT const withToken = await client.getUtxosWithUnit(Address.fromBech32("addr_test1..."), unit) const nftUtxo = await client.getUtxoByUnit(unit) // unit = policyId + assetNameHex ``` ```typescript // Any address (pass a unit as the second argument to filter by asset) const utxos = await provider.fetchAddressUTxOs("addr_test1...") // Your wallet, and its total ADA const mine = await wallet.getUtxosMesh() const balance = (await wallet.getBalanceMesh()).find((a) => a.unit === "lovelace")?.quantity ?? "0" // UTXOs holding a specific asset, or the addresses holding an NFT const withToken = await provider.fetchAddressUTxOs("addr_test1...", unit) // unit = policyId + assetNameHex const holders = await provider.fetchAssetAddresses(unit) ``` ### Datums A UTXO with an **inline datum** carries it directly, on the UTXO you already fetched. A UTXO with only a **datum hash** needs a separate lookup to recover the datum behind it: ```typescript // Inline datum: already attached to the fetched UTXO const utxos = await client.getUtxos(scriptAddress) const inline = utxos[0].datumOption // present when the output carries an inline datum // Datum hash: resolve the datum behind it through the provider const datum = await client.getDatum(datumHash) ``` ```typescript // Inline datum: Mesh returns it directly on each fetched UTXO const utxos = await provider.fetchAddressUTxOs(scriptAddress) const inline = utxos[0].output.plutusData // the inline datum (CBOR hex), when present ``` Inline datums (Plutus V2+) avoid the extra round-trip. Prefer them when designing contracts. See [Datum, redeemer & context](/docs/developers/curriculum/smart-contracts/datum-redeemer-context). Mesh reads inline datums straight off the fetched UTXO and has no separate datum-hash lookup, so for a hash-only UTXO you supply the datum off-chain when you spend it, another reason to prefer inline datums. ### Protocol parameters The builder fetches these automatically, but you can read them, fees, size limits, deposits, Plutus costs: ```typescript const params = await client.getProtocolParameters() console.log(params.minFeeA, params.maxTxSize, params.keyDeposit, params.coinsPerUtxoByte) ``` ```typescript const params = await provider.fetchProtocolParameters() ``` ### Delegation and confirmation ```typescript // Which pool a reward address delegates to, and its reward balance const delegation = await client.getDelegation(rewardAddress) // { poolId, rewards } // Wait for a submitted transaction to appear on-chain (poll every 3s) const confirmed = await client.awaitTx(txHash, 3000) ``` ```typescript // Delegation and reward balance for a stake address const info = await provider.fetchAccountInfo(rewardAddress) // { active, poolId, balance, rewards, ... } // Call back once a submitted transaction is on-chain provider.onTxConfirmed(txHash, () => console.log("confirmed")) ``` Delegation queries underpin the [staking](/docs/developers/curriculum/staking-governance/staking) UI; `awaitTx` is the confirmation step after [your first transaction](/docs/developers/curriculum/start-building/your-first-transaction). ## Submitting transactions A provider also broadcasts signed transactions and can evaluate script costs before you submit: ```typescript // Submit signed CBOR (e.g. returned from a frontend wallet) const signedTx = Transaction.fromCBORHex(signedTxCbor) const txHash = await client.submitTx(signedTx) const confirmed = await client.awaitTx(txHash) // Estimate script execution units before submitting const redeemers = await client.evaluateTx(Transaction.fromCBORHex(unsignedTxCbor)) ``` ```typescript // Submit signed CBOR (e.g. returned from a frontend wallet) const txHash = await provider.submitTx(signedTxCbor) provider.onTxConfirmed(txHash, () => console.log("confirmed")) // Estimate script execution units before submitting const redeemers = await provider.evaluateTx(unsignedTxCbor) ``` Common rejection reasons from the node: | Error | Meaning | Retryable? | |---|---|---| | `BadInputsUTxO` | A chosen UTXO was already spent | No: rebuild with fresh UTXOs | | `OutsideValidityIntervalUTxO` | The transaction expired | No: rebuild with a new validity window | | `ValueNotConservedUTxO` | Inputs ≠ outputs + fee | No: fix the transaction | | `FeeTooSmallUTxO` | Fee too low | No: rebuild | | Network timeout | Provider unreachable | Yes: retry after a delay | `BadInputsUTxO` from indexer lag is the classic one. Handle it with the [retry-safe pattern](/docs/developers/curriculum/start-building/transaction-building#resilient-submission-retry-safe), which re-reads chain state on every attempt. These names are the ledger's own validation rules; when an unfamiliar code comes back, the [Cardano Blueprint's block validation page](https://cardano-scaling.github.io/cardano-blueprint/ledger/block-validation.html) maps the full set in the order the ledger applies them. ## Inspect a transaction Sometimes you have a transaction in hand (one you built, or one you pulled from the chain) and you want to read it back: its inputs, outputs, fee, mint, and validity interval. Both SDKs decode transaction CBOR into an inspectable structure. Evolution decodes CBOR straight into typed transaction objects: ```typescript const tx = Transaction.fromCBORHex(txHex) // the whole transaction const body = TransactionBody.fromCBORHex(bodyHex) // or just the body // read inputs, outputs, fee, mint, and the validity interval off the decoded body ``` Mesh's `TxParser` turns CBOR into a `MeshTxBuilderBody`. It needs a serializer (`CSLSerializer` from `@meshsdk/core-csl`) and, optionally, a fetcher so it can pull the input UTXO data the CBOR only references by hash: ```typescript const fetcher = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID!) const txParser = new TxParser(new CSLSerializer(), fetcher) // txHex from building, or fetcher.fetchTxInfo(txHash).tx.cborHex from chain const body = await txParser.parse(txHex) // pass providedUtxos as 2nd arg if no fetcher console.log("inputs:", body.inputs.length, "outputs:", body.outputs.length) console.log("fee:", body.fee, "mints:", body.mints?.length ?? 0) ``` Beyond reading, the parsed body can be rebuilt with `MeshTxBuilder`, or turned into a unit tester via `txParser.toTester()`. ## Next steps - [Transaction building](/docs/developers/curriculum/start-building/transaction-building), use what you query to build and submit - [Connect a wallet](/docs/developers/curriculum/dapps/connect-a-wallet), read a user's UTXOs and address in the browser - [Contract library](/templates/contracts), inspect real contracts' UTXOs and datums with what you just learned - [Connecting to the chain](/docs/developers/curriculum/production/connecting-to-the-chain), the infrastructure behind a provider and when to run your own --- ## Transaction Building [Your first transaction](/docs/developers/curriculum/start-building/your-first-transaction) showed the core loop: **build → sign → submit**. This page goes deeper: paying many recipients at once, understanding how the builder picks inputs and fees, distributing tokens to hundreds of addresses, chaining dependent transactions, and surviving the indexer lag that trips up most first real deployments. The conceptual model (UTXOs, inputs, outputs, fees, validity) is in [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions); this page is the build-side how-to. ## How the builder works When you build a transaction, a high-level SDK does several things so you don't have to. Understanding the phases helps when something doesn't balance: 1. **Coin selection**: picks UTXOs from your wallet to cover the outputs + fee (see below). 2. **Collateral**: for script transactions only, sets aside pure-ADA UTXOs to cover a failed script. 3. **Change**: returns the leftover (inputs − outputs − fee) to your change address, respecting the min-ADA per UTXO. 4. **Fee calculation**: sizes the fee from the final transaction, iterating because change and fee affect each other. 5. **Script evaluation**: for script transactions, runs the validators to compute execution-unit costs, which feed back into the fee. The result is an unsigned transaction. Signing adds the witnesses; submitting broadcasts it. A read-only wallet can build a transaction but not sign one. That's the [frontend signs, backend builds](/docs/developers/curriculum/dapps/connect-a-wallet#frontend-signs-backend-builds-and-submits) split. ## Coin selection Coin selection decides **which** UTXOs to spend. The usual default is **largest-first**: sort the wallet's UTXOs by ADA descending, then take from the top until the outputs and fee are covered. Fewer, larger inputs mean a smaller transaction and a lower fee than many small ones. - It tracks every required asset (lovelace and each native token) and stops as soon as all are covered. - It's deterministic. The same wallet state always selects the same inputs. - If you pass explicit inputs yourself, selection only kicks in to cover any shortfall. For privacy or fee-optimal strategies you can supply a custom selection function, but largest-first is the right default for most apps. :::info Going deeper Coin selection is an active research area. For a formal treatment of UTXO-based selection algorithms and their trade-offs, see this [Cardano research paper on UTXO-based coin selection](https://cardano.org/news/2024-05-23-research-paper-utxo-based-coin-select/). ::: ## Multiple outputs Pay several recipients in **one** transaction, one fee instead of many. Chain output calls on the builder: ```typescript const tx = await client .newTx() .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: Assets.fromLovelace(5_000_000n) }) .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: Assets.fromLovelace(3_000_000n) }) .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: Assets.fromLovelace(2_000_000n) }) .build() const signed = await tx.sign() await signed.submit() ``` To drain a wallet to a single address, use `.sendAll({ to })`. It collects every UTXO into one output minus fees. ```typescript const txBuilder = new MeshTxBuilder({ fetcher: provider }) const unsignedTx = await txBuilder .txOut("addr_test1...", [{ unit: "lovelace", quantity: "5000000" }]) .txOut("addr_test1...", [{ unit: "lovelace", quantity: "3000000" }]) .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() const signedTx = await wallet.signTx(unsignedTx) await wallet.submitTx(signedTx) ``` ```bash # Each --tx-out is one recipient; one fee covers the whole transaction cardano-cli latest transaction build \ --tx-in # \ --tx-out "addr_test1...+5000000" \ --tx-out "addr_test1...+3000000" \ --tx-out "addr_test1...+2000000" \ --change-address $(< payment.addr) \ --out-file tx.raw ``` Sign and submit as in [your first transaction](/docs/developers/curriculum/start-building/your-first-transaction#send-ada). ## Transaction metadata Any transaction can carry **metadata**: structured data stored permanently on-chain under a numeric **label**. It is used for transaction messages, NFT properties, certifications, timestamps, and supply-chain records. Metadata is stored as compact binary (CBOR), and the schema is deliberately simple: top-level keys are integers (0 to 2^64 − 1), and values are integers, UTF-8 strings (max 64 bytes), bytestrings, lists, or maps. Floats, booleans, and nulls must be encoded as one of those. Common standardized labels: | Label | CIP | Purpose | |---|---|---| | `674` | CIP-20 | Transaction messages / comments | | `721` | CIP-25 | NFT metadata | | `777` | CIP-27 | Royalties | Chain `attachMetadata` onto the transaction (the label is a `bigint`): ```typescript declare const message: TransactionMetadatum.TransactionMetadatum const tx = await client .newTx() .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: Assets.fromLovelace(2_000_000n) }) .attachMetadata({ label: 674n, metadata: message }) // CIP-20 message .build() ``` Chain multiple `attachMetadata` calls for different labels (e.g. a `674n` message plus `721n` NFT metadata). Add metadata with `metadataValue(label, metadata)` on the builder. This example attaches a CIP-20 (`674`) message: ```typescript const txBuilder = new MeshTxBuilder({ fetcher: provider }) const unsignedTx = await txBuilder .changeAddress(await wallet.getChangeAddressBech32()) .metadataValue(674, { msg: ["Invoice-No: 1234567890"] }) // CIP-20 message .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() ``` Use any label with your own structure for custom application data (e.g. `metadataValue(1337, { name: "hello world", completed: 0 })`). Put the metadata in a JSON file: ```json { "674": { "msg": ["Invoice-No: 1234567890"] } } ``` Reference it with `--metadata-json-file` when you build the transaction body (works with both `transaction build` and `build-raw`): ```bash cardano-cli latest transaction build \ --tx-in # \ --change-address $(< payment.addr) \ --metadata-json-file metadata.json \ --out-file tx.raw ``` Metadata is public: any provider can read it back. With Blockfrost, fetch every transaction carrying a label via `GET /metadata/txs/labels/{label}`, and a block explorer shows a transaction's metadata in its UI. To maintain your own index of a label, [a Yaci Store plugin](/docs/developers/curriculum/production/indexing-and-analytics#index-exactly-what-you-need-plugins) can filter for it at indexing time. Minting an NFT with CIP-25 (`721`) metadata is shown end to end in [Mint an NFT](/docs/developers/curriculum/native-tokens/mint-nft). ## Batching and airdrops A single transaction has a maximum size (`maxTxSize`, currently ~16 KB, and [governance-controlled](/docs/developers/curriculum/staking-governance/governance) like every protocol parameter). Each output adds ~60-100 bytes, so you fit roughly **20-30 ADA-only recipients** per transaction (fewer if outputs carry native tokens). To pay hundreds of recipients, chunk the list into transaction-sized batches: ```typescript function chunk(array: T[], size: number): T[][] { const chunks: T[][] = [] for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size)) return chunks } const BATCH_SIZE = 25 // conservative for ADA-only; lower it for token outputs const batches = chunk(recipients, BATCH_SIZE) ``` Then submit each batch, waiting for confirmation before the next: ```typescript for (let i = 0; i < batches.length; i++) { let builder = client.newTx() for (const r of batches[i]) { builder = builder.payToAddress({ address: r.address, assets: Assets.fromLovelace(r.lovelace) }) } const signed = await (await builder.build()).sign() const txHash = await signed.submit() await client.awaitTx(txHash, 3000) // wait before the next batch console.log(`Batch ${i + 1}/${batches.length} confirmed:`, txHash) } ``` ```typescript for (let i = 0; i < batches.length; i++) { let builder = new MeshTxBuilder({ fetcher: provider }) for (const r of batches[i]) { builder = builder.txOut(r.address, [{ unit: "lovelace", quantity: r.lovelace.toString() }]) } const unsignedTx = await builder .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() const txHash = await wallet.submitTx(await wallet.signTx(unsignedTx)) await new Promise((resolve) => provider.onTxConfirmed(txHash, resolve)) // wait before the next batch console.log(`Batch ${i + 1}/${batches.length} confirmed:`, txHash) } ``` For **native-token** airdrops, give each output enough ADA for the min-UTXO (tokens enlarge the UTXO. 2+ ADA per output is a safe floor; the builder computes the exact minimum). Waiting for each batch is simple but slow; the next two sections remove the wait. ## Chaining transactions Normally you can't build transaction #2 until #1 confirms, because #1's new UTXOs don't exist from the provider's view yet, a 10-30 s wait per step. **Chaining** removes it: once you have built transaction #1, you feed the UTXOs you still hold **plus** its new outputs (already tagged with its pre-computed hash) into the build of transaction #2. For the concept beneath this, why an unconfirmed output is safe to spend and where chaining fits among Cardano's scaling options, see [transaction chaining](/docs/developers/curriculum/production/transaction-chaining). ```typescript const alice = Address.fromBech32("addr_test1...") const bob = Address.fromBech32("addr_test1...") const tx1 = await client .newTx() .payToAddress({ address: alice, assets: Assets.fromLovelace(2_000_000n) }) .build() // Build tx2 immediately, spending from tx1's not-yet-confirmed outputs const tx2 = await client .newTx() .payToAddress({ address: bob, assets: Assets.fromLovelace(2_000_000n) }) .build({ availableUtxos: tx1.chainResult().available }) // Submit in order. The node rejects tx2 if tx1 hasn't arrived yet await (await tx1.sign()).submit() await (await tx2.sign()).submit() ``` :::warning Submit in order Each chained transaction spends an output of the previous one. If tx2 reaches the node before tx1, the node sees inputs that don't exist and rejects it. A sequential loop guarantees ordering. The `available` outputs are **not on-chain yet**. Don't pass them to a provider query. ::: Mesh has no built-in chain-tracking equivalent to Evolution's `chainResult().available`. To chain with Mesh you thread the previous transaction's outputs forward yourself, adding each as an explicit input on the next build with `.txIn(txHash, index, amount, address)` and tracking those unconfirmed UTXOs in your own code. You can also merge reusable Evolution builder fragments with `.compose(otherBuilder)` (e.g. a payment fragment + a validity fragment) into one transaction. ## Resilient submission (retry-safe) The single most common production bug: you submit a transaction, then immediately build the next one, but your provider's UTXO set hasn't caught up, so it still shows the **already-spent** inputs as available. The node rejects the new transaction with `BadInputsUTxO`. This isn't a bug; it's block propagation (10-30 s, longer under load). The fix: **read all chain state inside the retryable action**, not before it. Each retry re-queries UTXOs/datums/script state fresh, so it works from the latest view. The retry harness itself is plain TypeScript; only the build differs per SDK: ```typescript async function withRetry(action: () => Promise, retries = 3, delayMs = 3000): Promise { for (let attempt = 1; attempt <= retries; attempt++) { try { return await action() } catch (err) { if (attempt === retries) throw err await new Promise((r) => setTimeout(r, delayMs)) } } throw new Error("unreachable") } ``` The action fetches everything it needs at call time, so each attempt builds from fresh state: ```typescript async function sendPayment() { const tx = await client .newTx() .payToAddress({ address: recipient, assets: Assets.fromLovelace(2_000_000n) }) .build() return (await tx.sign()).submit() } const txHash = await withRetry(sendPayment) ``` ```typescript async function sendPayment() { const unsignedTx = await new MeshTxBuilder({ fetcher: provider }) .txOut(recipient, [{ unit: "lovelace", quantity: "2000000" }]) .changeAddress(await wallet.getChangeAddressBech32()) .selectUtxosFrom(await wallet.getUtxosMesh()) .complete() return wallet.submitTx(await wallet.signTx(unsignedTx)) } const txHash = await withRetry(sendPayment) ``` Querying chain state **outside** the action and passing it in defeats the retry. The same stale snapshot is reused every time. When collecting from a script address, fetch the script UTXOs inside the action too. With Effect, wrap the whole `Effect.gen` pipeline and apply `Effect.retry(Schedule.recurs(3)...)`, optionally narrowing to `err.message.includes("BadInputsUTxO")`. Retrying won't fix genuinely insufficient funds. Check balances first. ## Redeemer indexing Plutus validators can receive **input indices** in their redeemer for O(1) lookup instead of scanning every input on-chain (execution units are expensive). The catch: Cardano sorts inputs canonically by `(txHash, outputIndex)`, and coin selection adds wallet UTXOs *after* you specify script inputs, shifting every index. So the indices aren't known until the build is complete. SDKs solve this by **deferring redeemer construction**: you provide a redeemer *function*, and the builder calls it after coin selection has finalized and sorted the inputs. Three modes: | Mode | The function receives | Use case | |---|---|---| | **Batch** | all indexed inputs → one redeemer | a stake-validator coordinator that validates many contract inputs at once | | **Self** | called once per script UTXO, with its own index | a spend validator that looks up its own input | | **Static** | no indices, data used directly | a redeemer that doesn't depend on order | This is what powers the withdraw-zero coordinator: the [Stake Validator design pattern](/docs/developers/curriculum/smart-contracts/advanced/design-patterns/stake-validator) runs business logic once for the whole transaction. See [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend) for spending from scripts and [Write a validator](/docs/developers/curriculum/smart-contracts/write-a-validator) for the on-chain side. ## Offline builds (air-gapped) SDKs build a transaction against a live provider. `cardano-cli` can also build one **fully offline**, where you calculate the fee and balance the transaction yourself, for air-gapped signing and reproducible builds. Of its three build commands, `transaction build` is the everyday node-connected one, `build-raw` is the offline one, and `build-estimate` sizes a fee offline without balancing. ```bash # 1. Protocol parameters (needs a node, once) cardano-cli query protocol-parameters --out-file pparams.json # 2. Draft with fee 0 (the change output holds the full input for now) cardano-cli latest transaction build-raw \ --tx-in # \ --tx-out "$(< payment2.addr)+1000000000" \ --tx-out "$(< payment.addr)+8994790937" \ --fee 0 --protocol-params-file pparams.json --out-file tx.draft # 3. Compute the exact fee (deterministic) cardano-cli latest transaction calculate-min-fee \ --tx-body-file tx.draft --protocol-params-file pparams.json --witness-count 1 # 173993 Lovelace # 4. Rebuild: change = inputs - sent - fee cardano-cli latest transaction build-raw \ --tx-in # \ --tx-out "$(< payment2.addr)+1000000000" \ --tx-out "$(< payment.addr)+8994616944" \ --fee 173993 --protocol-params-file pparams.json --out-file tx.raw ``` `--witness-count` is how many signatures the transaction will carry. It affects the fee. Inspect any draft with `cardano-cli debug transaction view --tx-body-file tx.draft`. ## Spending from several keys To spend UTXOs owned by *different* keys in one transaction (combining two wallets, or a multisig), list each `--tx-in`, set the witness count to the number of signers, and pass every signing key at sign time. ```bash cardano-cli latest transaction build-raw \ --tx-in --tx-in \ --tx-out "$(< store-owner.addr)+999646250" \ --fee 179581 --out-file tx.draft cardano-cli latest transaction sign \ --tx-body-file tx.draft \ --signing-key-file payment1.skey \ --signing-key-file payment2.skey \ --out-file tx.signed ``` Then `submit` as usual. Parse CLI output with `jq` for scripted workflows, e.g. pick the first UTXO: `--tx-in $(cardano-cli query utxo --address $(< payment.addr) --output-json | jq -r 'keys[0]')`. The full command reference lives in the [cardano-cli repository](https://github.com/IntersectMBO/cardano-cli). ## Headless dApps Everything above builds transactions inside your application with an SDK. But that transaction-building logic is where a Cardano dApp's business logic actually sits: a validator only checks that a finished transaction is valid, it holds no logic of its own, so the real work is turning external events (a button in a UI, an API call, an on-chain condition, a scheduled job) into the transactions that represent them. That logic does not have to live in your frontend. It can run in the browser, in a cloud backend, or in an unattended process like a batcher. Left unmanaged, every app ends up wiring its own way to read the chain, build transactions, and submit them, so no two are alike and none can reuse another's work. A *headless dApp* pushes all of that behind one well-defined interface: the business logic becomes a standalone service, and every input and output flows through that interface instead of being baked into a particular UI. It is hexagonal architecture (or clean architecture) applied to Cardano, separating the core logic from the adapters that connect it to the outside world. The payoff is reuse: one transaction-building service can back several frontends, other applications can compose it programmatically, and nobody re-implements chain queries, transaction building, and submission from scratch. Making this concrete needs standard interfaces for the two things such a service does, reading chain data and resolving or submitting transactions; UtxoRPC is one emerging interface for that read-and-submit side. The [declarative transactions](#declarative-transactions) below are one shape of the build side: a protocol publishes its transactions as a machine-readable interface, and the client calls a named function against a resolver that materializes and submits the concrete transaction, while the client signs locally and never runs a node. ## Declarative transactions Everything on this page states *how* to assemble a transaction: pick inputs, add outputs, attach metadata, compute the change. An emerging alternative flips the model: you describe *what* must be true of the transaction, and a resolver works out the rest at runtime. A concrete take on this for Cardano is [Tx3](https://docs.txpipe.io/tx3), a small declarative language that treats a transaction as a reusable, parameterized template: ```text title="buy_product.tx3" party Buyer; party Seller; asset Product = 0x6b9b69."STUFF"; fn total_price(quantity: Int) -> AnyAsset { let unit_price = Ada(5000000); unit_price * quantity } tx buy_product(quantity: Int) { input payment { from: Buyer, min_amount: total_price(quantity) + fees, } mint { amount: Product(quantity), redeemer: (), } output { to: Buyer, amount: payment - total_price(quantity) - fees + Product(quantity), } output { to: Seller, amount: total_price(quantity), } } ``` Read it against the builder model above: - **Parties are roles, not addresses.** `Buyer` and `Seller` are placeholders bound to real addresses at resolution, so the same spec runs against any wallet and any deployment. - **Assets are named and typed.** `Product` wraps the policy ID and asset name once; everything after refers to it by name instead of a raw hex pair. - **Inputs are constraints, not UTXO references.** `payment` states its conditions (it must come from the Buyer and cover the price plus fees), and the resolver performs the coin selection you read about at the top of this page. - **Balancing is checked up front.** Minting sits in the same declaration, the asset math is type-checked at compile time, and fees and change are computed for you, so a transaction that doesn't balance never reaches submission. From a spec like this, the toolchain generates typed clients for TypeScript, Rust, Go, and Python, and a resolver materializes the concrete transaction over a wire protocol (TRP); your application signs locally and submits. Your code calls `buy_product(3)` instead of chaining builder calls. The spec is also an interface. The author of a protocol fills in the transaction bodies once; an integrator sees only the named transactions and their parameters, the way a web developer reads an API reference rather than a database schema. That split decouples an on-chain protocol from any single frontend: another application can integrate a protocol from its published definition instead of re-deriving datum shapes and input selection from its validators, and published definitions are collected in a public [registry](https://tx3.land) where deployed protocols are already browsable. Because everything chain-specific lives in the resolver, one definition can also be resolved against more than one target; the same protocol can run against layer 1 or a [Hydra head](https://github.com/tx3-lang/hydra-app-example). The project is young and its APIs still moving, but the paradigm is worth knowing as you design off-chain code: it is the same shift that constraint-based coin selection already made for inputs, applied to the whole transaction. See the [Tx3 documentation](https://docs.txpipe.io/tx3) and [repository](https://github.com/tx3-lang/tx3). ## Next steps - [Lock and spend](/docs/developers/curriculum/smart-contracts/lock-and-spend), build transactions that interact with validators - [Mint native tokens and NFTs](/docs/developers/curriculum/native-tokens/overview), outputs that carry new assets - [Going to production](/docs/developers/curriculum/production/going-to-production), the reliability checklist before mainnet --- ## When transactions fail Transactions fail, and on Cardano they fail in a small number of well-defined ways. The useful question is never just "what went wrong" but **where** and **why**: a failure caught while you build is different from one the node rejects, and a transient race is different from a logic error. Knowing which you are looking at tells you whether to retry, rebuild, or fix. This page is the map; each class links to the page that treats it in depth. ## The two-phase model The ledger validates a transaction in two phases, and the phase a failure lands in decides what it costs you. - **Phase 1** checks structure: the inputs exist, the value balances (inputs equal outputs plus fee), signatures are present, and the fee is sufficient. A phase-1 failure is rejected for free, the transaction never makes it on-chain. - **Phase 2** runs the Plutus scripts. It only happens if phase 1 passed. A phase-2 failure (a validator returns false, or exhausts its budget) is the one case where a *submitted* transaction costs you: the node consumes your [collateral](/docs/developers/curriculum/fundamentals/core-concepts/fees#collateral). A transaction that passes both phases never loses collateral. The split exists for an economic reason. An invalid transaction never reaches the chain, so it never pays a fee, which means the work of rejecting it is unpaid; if that work were unbounded, flooding nodes with expensive-to-reject transactions would be a cheap attack. Phase 1 is the bounded, inexpensive gate that protects the node. Phase 2 is where the expensive script work lives, and its failures land on-chain and consume collateral precisely so that heavy validation work is always paid for. The [Cardano Blueprint's validity page](https://cardano-scaling.github.io/cardano-blueprint/ledger/state-transition/validity.html) walks through the full argument. Most failures you hit are phase 1, and most of those never leave your machine. ## Build-time failures The SDK refuses to produce a transaction in the first place. Nothing is submitted, nothing is spent; you fix the inputs and rebuild. - **Below the minimum ADA.** Every output must carry a [minimum amount of ADA](/docs/developers/curriculum/native-tokens/overview#the-minimum-ada-requirement) that scales with its size, so an output of a bare token or a tiny lovelace amount is rejected before submission. - **Transaction too large.** Too many inputs or a large multi-asset bundle can push the transaction past the size limit. This is the downstream cost of [wallet fragmentation](/docs/developers/curriculum/start-building/transaction-building#coin-selection): many small UTXOs mean many inputs. Consolidate, or let coin selection prefer larger UTXOs. - **Insufficient funds.** Coin selection cannot cover the outputs plus fee from the available UTXOs. - **Over the script budget.** A Plutus transaction whose scripts exceed the per-transaction execution-unit limit cannot be built. See [what you pay for](/docs/developers/curriculum/smart-contracts/choose-a-language#what-you-pay-for-execution-costs) and [optimization](/docs/developers/curriculum/smart-contracts/advanced/optimization). ## Submit-time failures The transaction is well-formed but the node rejects it. The full list of node rejection codes is in [Submitting transactions](/docs/developers/curriculum/start-building/query-the-chain#submitting-transactions); the ones worth understanding by cause: - **`BadInputsUTxO`** (phase 1): a chosen UTXO is already spent. Either you read **stale** state (the indexer had not caught up) or another transaction **contended** for the same UTXO (a second browser tab, a double-clicked submit, or a concurrent backend build). This is the UTXO model's characteristic race: inputs are discrete and consumed exactly once. - **`OutsideValidityIntervalUTxO`** (phase 1): the transaction's validity window has passed before it landed. Rebuild with a fresh window. - **`ValueNotConservedUTxO`** / **`FeeTooSmallUTxO`** (phase 1): the balance or the fee is wrong, almost always a building bug rather than a transient condition. - **Script failure** (phase 2): a validator returned false or ran out of budget. Collateral is consumed. This is a logic problem, in the validator or in the datum/redeemer you supplied, not something a retry fixes. Reproduce it locally with an [emulator or devnet](/docs/developers/curriculum/start-building/local-testing) before resubmitting; if the bug is in the validator itself, see [testing validators](/docs/developers/curriculum/smart-contracts/testing). ## Congestion and the mempool Not every submit-time rejection means something is wrong with the transaction. A node's mempool is capped, and not by a transaction count: it enforces limits on four axes at once, total size in bytes, CPU execution units, memory execution units, and reference-script bytes. When any of them is full, new submissions are refused until blocks drain the backlog. This back-pressure is what preserves throughput during congestion; a "mempool full" rejection is a transient condition, not a verdict on your transaction. Two more mempool facts explain behavior that otherwise looks erratic: - **Nodes disagree.** Each node validates against its own recent tip plus its own mempool contents, so the same transaction can be accepted by one relay and rejected by another at the same moment. One path rejecting is not the network rejecting it. - **Pending is not a queue.** On every change of tip the node re-runs the state-dependent checks (are the inputs still unspent, is the validity window still open) and silently drops transactions that fail them. A transaction that vanished was not lost in transit; the chain moved underneath it. Resubmitting the unchanged transaction makes sense after a capacity rejection, or after a silent drop while the validity window is still open and the inputs are still unspent. Once the window has passed or an input is gone, only a rebuild helps. And if a resubmission returns `BadInputsUTxO`, check the chain before assuming failure: a transaction that was already included consumed its own inputs, so the same bytes are now invalid on that chain, and that error can simply mean it succeeded. The [Cardano Blueprint's mempool page](https://cardano-scaling.github.io/cardano-blueprint/mempool/index.html) documents these mechanics from the node's side. ## Retryable or fatal The triage that matters: re-sending an unchanged transaction only helps for **transient** failures. Everything else needs a rebuild or a fix. | Failure | When | Retry unchanged? | What to do | |---|---|---|---| | Network timeout / provider error | Submit | Yes | Retry after a short backoff | | Mempool full (congestion) | Submit | Yes | Wait for a few blocks to drain it, resubmit unchanged | | `BadInputsUTxO` (stale or contended) | Submit | No | Re-read fresh UTXOs and rebuild | | `OutsideValidityIntervalUTxO` | Submit | No | Rebuild with a new validity window | | `ValueNotConserved` / `FeeTooSmall` | Submit | No | Fix the build | | Below min-ADA / too large / insufficient funds | Build | No | Fix inputs or consolidate, rebuild | | Script failure (phase 2) | Submit | No | Debug the validator; collateral already spent | The important subtlety: `BadInputsUTxO` from indexer lag *looks* transient but a blind retry resubmits the same doomed transaction. The fix is to make every attempt read fresh chain state, which is exactly the [retry-safe pattern](/docs/developers/curriculum/start-building/transaction-building#resilient-submission-retry-safe): wrap read, build, sign, and submit together so a retry rebuilds against the current UTXO set rather than the stale one. ## Key takeaways - A failure's **phase** tells you its cost: phase 1 is free, a phase-2 script failure burns collateral. - A failure's **stage** tells you the fix: build-time means change the inputs; submit-time means the node judged a well-formed transaction. - Only **transient** failures (timeouts, lag-induced `BadInputsUTxO`) are worth retrying, and only if each attempt rebuilds from fresh state. ## Next steps - [Resilient submission](/docs/developers/curriculum/start-building/transaction-building#resilient-submission-retry-safe): the retry-safe pattern in code - [Submitting transactions](/docs/developers/curriculum/start-building/query-the-chain#submitting-transactions): the full rejection-code reference - [Collateral](/docs/developers/curriculum/fundamentals/core-concepts/fees#collateral): how phase-2 failures are paid for - [Mint Tokens & NFTs](/docs/developers/curriculum/native-tokens/overview): the next module, custom assets in the transactions you can now debug --- ## Your First Transaction Time to send real value (well, real test value). Every Cardano interaction follows the same three steps: **build** a transaction, **sign** it with your key, and **submit** it to the network. This page sends ADA on Preprod, then reads it back. Pick your tool below. ## Before you start - **Test ADA** in a wallet you control ([get it from the faucet](/docs/developers/curriculum/start-building/networks-and-test-ada#get-test-ada)) - **A tool installed** and a **provider key** ([choose your tools](/docs/developers/curriculum/start-building/choose-your-tools)); the SDK tabs use Blockfrost on Preprod - Optional background: [Transactions](/docs/developers/curriculum/fundamentals/core-concepts/transactions) explains what build/sign/submit is doing under the hood ## Send ADA ```typescript // Provider (Blockfrost) + wallet (seed phrase) = a signing client const client = Client.make(preprod) .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }) .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 }) // Build -> sign -> submit const tx = await client .newTx() .payToAddress({ address: Address.fromBech32("addr_test1..."), // recipient assets: Assets.fromLovelace(2_000_000n) // 2 ADA }) .build() const signed = await tx.sign() const txHash = await signed.submit() console.log("Transaction submitted:", txHash) ``` The builder selects UTXOs, calculates the fee, and adds a change output for you. `2_000_000n` is 2 ADA (amounts are in lovelace, as a bigint). ```typescript // Provider + wallet (from your mnemonic) const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY!); const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({ networkId: 0, // 0 = preprod testnet walletAddressType: AddressType.Base, fetcher: provider, submitter: provider, mnemonic: process.env.WALLET_MNEMONIC!.split(" "), }); // Build -> sign -> submit const utxos = await wallet.getUtxosMesh(); const changeAddress = await wallet.getChangeAddressBech32(); const txBuilder = new MeshTxBuilder({ fetcher: provider }); const unsignedTx = await txBuilder .txOut("addr_test1...", [{ unit: "lovelace", quantity: "1500000" }]) // recipient, 1.5 ADA .changeAddress(changeAddress) .selectUtxosFrom(utxos) .complete(); const signedTx = await wallet.signTx(unsignedTx, false); const txHash = await wallet.submitTx(signedTx); console.log("Transaction hash:", txHash); ``` `MeshCardanoHeadlessWallet.brew()` generates a fresh mnemonic if you need one. See [Keys & Wallets](/docs/developers/curriculum/fundamentals/core-concepts/wallets-and-keys#working-with-wallets-in-code) for creating wallets in code with either SDK. First point cardano-cli at a running node and generate a key + address (once): ```bash export CARDANO_NODE_SOCKET_PATH=~/node.socket export CARDANO_NODE_NETWORK_ID=1 # 1 = preprod, 2 = preview cardano-cli address key-gen --verification-key-file payment.vkey --signing-key-file payment.skey cardano-cli address build --payment-verification-key-file payment.vkey --out-file payment.addr ``` Fund `payment.addr` from the [faucet](/docs/developers/curriculum/start-building/networks-and-test-ada#get-test-ada), then build → sign → submit: ```bash # 1. Find a UTXO to spend cardano-cli query utxo --address $(< payment.addr) # 2. Build (automatic fee + change), sign, submit cardano-cli latest transaction build \ --tx-in # \ --tx-out addr_test1...+2000000 \ --change-address $(< payment.addr) \ --out-file tx.raw cardano-cli latest transaction sign \ --tx-body-file tx.raw --signing-key-file payment.skey --out-file tx.signed cardano-cli latest transaction submit --tx-file tx.signed ``` For the offline `build-raw` flow with manual fee calculation, and spending from several keys, see [Offline builds](/docs/developers/curriculum/start-building/transaction-building#offline-builds-air-gapped). The transaction hash is your receipt. Paste it into an [explorer](/docs/developers/curriculum/start-building/networks-and-test-ada#block-explorers) to watch it confirm. ## Query the chain Reading state is the other half of building. Check a balance, list UTXOs, or wait for confirmation: ```typescript // List your wallet's UTXOs and sum the balance const utxos = await client.getWalletUtxos() const totalLovelace = utxos.reduce((sum, u) => sum + u.assets.lovelace, 0n) console.log("Total balance:", totalLovelace, "lovelace") // Wait for a transaction to confirm (poll every 3s) const confirmed = await client.awaitTx(txHash, 3000) console.log("Confirmed:", confirmed) ``` ```typescript // List the wallet's UTXOs const utxos = await wallet.getUtxosMesh(); console.log(utxos); // Call back once the transaction is confirmed on-chain provider.onTxConfirmed(txHash, () => console.log("Confirmed")); ``` `provider.onTxConfirmed(txHash, cb)` polls the provider and fires the callback once the transaction lands; you can also paste the hash into an [explorer](/docs/developers/curriculum/start-building/networks-and-test-ada#block-explorers). ```bash cardano-cli query utxo --address $(< payment.addr) # TxHash TxIx Amount # -------------------------------------------------------------------------------------- # 262c7891...384fe6d 0 10000000000 lovelace ``` ## Next steps - [Mint native tokens and NFTs](/docs/developers/curriculum/native-tokens/overview): your first on-chain asset - [Smart Contracts](/docs/developers/curriculum/smart-contracts/overview): lock and unlock funds with validators - Reference: the full [Evolution SDK](https://github.com/IntersectMBO/evolution-sdk) and [Mesh SDK](https://meshjs.dev) docs --- ## Exchange Integrations ## Overview This guide is for exchanges, custodians, and other entities interested in or currently listing ada or Cardano native tokens. It outlines the main available components for integration purposes, providing step-by-step instructions and best practices. :::tip Need integration support? For tailored support, real-time updates, and integration queries, connect with the Cardano Foundation Core Integrations team at **[integrations@cardanofoundation.org](mailto:integrations@cardanofoundation.org)**. ::: ## Understanding Cardano's Accounting Model Cardano uses the **Extended UTXO (eUTXO)** model: value lives in discrete unspent outputs (UTXOs) that transactions consume and create, rather than in a mutable account balance. [The eUTXO model](/docs/developers/curriculum/fundamentals/core-concepts/eutxo) explains it in full. For an exchange, the implication to internalize early is that a customer's funds are a *set of UTXOs*, not a single number, so deposit tracking, coin selection, and withdrawal batching all operate at the UTXO level. The components below implement that. ## Integration Components - [**cardano-rosetta-java**](https://github.com/cardano-foundation/cardano-rosetta-java): Cardano Rosetta Java is a lightweight Java implementation of the Coinbase Mesh API (formerly Rosetta) for the Cardano blockchain. This implementation follows the [Mesh API specification](https://github.com/coinbase/mesh-specifications) and is compatible with the [Mesh CLI](https://github.com/coinbase/mesh-cli), while including specific extensions to accommodate Cardano's unique features. _(Recommended for exchanges)_ - Employs standardized APIs commonly used across blockchain platforms, promoting ease of understanding and implementation. - Handles tracking, building, and submitting transactions, providing all functionality needed for exchange operations. - All-in-one package with Cardano node, Submit API, Mesh API, and Yaci-Store indexer with Postgres database, streamlining your Cardano integration workflow. :::note Rosetta specification does not include transaction signing capabilities. This is done in a separate offline service for best security practices using any signing libraries available. See example using [CSL](https://github.com/Emurgo/cardano-serialization-lib/blob/master/doc/getting-started/singing_rosetta_tx.ts). For creating addresses, [cardano-addresses](https://github.com/IntersectMBO/cardano-addresses) provides mnemonic (backup phrase) creation, and conversion of a mnemonic to seed for wallet restoration, and address derivation functionalities. This can also be achieved using other libraries like [cardano-serialization-lib](https://github.com/Emurgo/cardano-serialization-lib) ::: - [**cardano-graphql**](https://github.com/cardano-foundation/cardano-graphql): GraphQL API for querying blockchain data. - GraphQL layer to access all blockchain data, runs on top of cardano-db-sync indexer. - Provides access to staking and all blockchain transaction data, easy to query using GraphQL language. - [**cardano-wallet**](https://github.com/cardano-foundation/cardano-wallet): Backend service providing APIs for wallet operations. - All-in-one solution for integration: address creation, automatic coin selection, transaction building, signing, and submission. - Great solution for smaller exchanges. :::note Does not support offline transaction signing; all keys are exposed online. Cardano Wallet is currently in maintenance-only mode. The Cardano Foundation is committed to maintaining it for the foreseeable future by upgrading to new versions of the cardano-node, fixing bugs, improving quality and stability of both the code and server stability, plus providing general user support. ::: - [**cardano-db-sync**](https://github.com/IntersectMBO/cardano-db-sync): Syncs blockchain data to a PostgreSQL database. - PostgreSQL database with the entire blockchain schema, queried with SQL. - Used with an indexer for GraphQL APIs. - [**cardano-node**](https://github.com/IntersectMBO/cardano-node): The core component for participating in the Cardano decentralized blockchain. - [**Cardano token registry**](/docs/developers/curriculum/native-tokens/metadata-registry): Register and query off-chain token metadata for native assets on Cardano. ## Wallet Management ### Address Handling A common and effective approach for exchanges integrating with Cardano involves using individual deposit addresses per customer and managing withdrawals from a centralized wallet. This model enables clear tracking, simplifies auditing, and enhances security and operational control. The typical workflow is as follows: - **Address Creation** - The exchange generates a unique deposit address for each customer using [cardano-address](https://github.com/IntersectMBO/cardano-addresses) - **Deposit Monitoring** - The exchange continuously monitors the blockchain for incoming transactions to these addresses. - **Customer Account Update** - Upon detecting a deposit, the exchange credits the corresponding customer account in its internal database. - **Consolidation of Funds** - The exchange periodically moves funds from individual deposit addresses to a centralized withdrawal wallet by creating and submitting a transaction. This consolidation step simplifies fund management and improves operational efficiency. - **Withdrawals** - When a customer requests a withdrawal, the exchange creates an outgoing transaction from the centralized withdrawal wallet and updates the customer's account in the internal database to reflect the withdrawal. ## Transaction Handling ### Creation and Submission Cardano offers multiple tools for transaction creation and submission, each designed to suit different integration architectures. The choice of tool depends on your infrastructure, security model, and level of control required. Most tools also support **fee estimation**, either explicitly or as part of the transaction construction process. | Tool | Create | Sign | Submit | Notes | |------|--------|------|--------|-------| | [`cardano-wallet`](https://github.com/cardano-foundation/cardano-wallet) | ✅ | ✅ | ✅ | Full-featured REST API with built-in fee calculation and UTxO management. | | [`cardano-rosetta`](https://github.com/cardano-foundation/cardano-rosetta-java) | ✅ | ❌ | ✅ | Rosetta does not handle key management or signing. Transactions must be signed offline. | | [`cardano-serialization-lib`](https://github.com/Emurgo/cardano-serialization-lib) | ✅ | ✅ | ❌ | Low-level library for custom workflows. Commonly used with `cardano-submit-api` for submission. | | [`cardano-submit-api`](https://github.com/IntersectMBO/cardano-node/tree/master/cardano-submit-api) | ❌ | ❌ | ✅ | Lightweight API for submitting signed transactions to a Cardano node. | :::tip The best practice for exchanges is to use `cardano-rosetta` for transaction construction and submission, and sign the transaction using signing libraries of their choice such as `cardano-serialization-lib`. ::: ### Fee Calculation The formula for calculating minimal fees for a transaction (tx) is: ```text a * size(tx) + b ``` Where: - `a` and `b` are protocol parameters. - `size(tx)` is the transaction size in bytes. ### Monitoring Transactions Once transactions are submitted to the Cardano network, exchanges must monitor their status to ensure successful inclusion in a block and confirmation over time. This step is critical for updating customer balances, handling retries, and maintaining overall system integrity. Several tools and interfaces are available to support transaction monitoring: | Tool | Monitoring Capability | Notes | |------|------------------------|-------| | [`cardano-rosetta-java`](https://github.com/cardano-foundation/cardano-rosetta-java) | ✅ Transaction status and block inclusion | Suitable for exchanges using the [Rosetta API](https://cardano-foundation.github.io/cardano-rosetta-java/api#tag/block) standard. Supports structured responses for [Account Balance](https://cardano-foundation.github.io/cardano-rosetta-java/api#tag/account/POST/account/balance) updates and transaction queries. | | [`cardano-graphql`](https://github.com/cardano-foundation/cardano-graphql) | ✅ Rich query support for entire blockchain data | Useful for querying confirmations, transaction metadata, and UTxO states using GraphQL. Cross-platform, typed, and queryable API for Cardano. | | [`cardano-wallet`](https://github.com/cardano-foundation/cardano-wallet) | ✅ Built-in tracking for submitted transactions | Automatically tracks transaction state, confirmation depth, and balances. Exposes these via a REST API. | Additional considerations: - **Confirmation depth**: For customer-facing actions like crediting a deposit, exchanges typically wait for a configurable number of block confirmations (e.g., 20–30 blocks) ## Native Assets Cardano supports **native assets** that can be stored and transferred directly in UTxOs alongside ada. These assets can be **fungible** (tokens) or **non-fungible** (NFTs), and are handled **natively by the ledger** without smart contracts. :::tip Native assets follow the same transaction and validation rules as ada and are treated as first-class citizens in the Cardano ledger. ::: #### Why This Matters for Exchanges - **No smart contract complexity**: Native assets do not require Plutus scripts, reducing operational complexity. - **Unified infrastructure**: The same transaction structure used for ada also supports native assets. - **Automatic deposits**: Deposit addresses may receive native assets, even if the exchange does not actively support them yet. ### Working with Native Assets Use tools like [`cardano-rosetta-java`](https://github.com/cardano-foundation/cardano-rosetta-java) or [`cardano-graphql`](https://github.com/cardano-foundation/cardano-graphql) to track native assets across UTxOs. These tools allow you to: - Detect native assets per address - Query balances for specific assets - Monitor transaction inclusion and confirmations 🔗 **See also:** [Using multi-assets with Rosetta](https://cardano-foundation.github.io/cardano-rosetta-java/docs/user-guides/multi-assets) ### Cardano Token Registry The **Cardano Token Registry** provides a way to register **off-chain metadata** for native assets on Cardano. This metadata is used by wallets, explorers, and exchanges to display human-readable and visual information about tokens. Registered metadata includes: - ✅ Human-readable name (e.g., "MyToken") - ✅ Ticker symbol (e.g., "MTK") - ✅ Description and project website URL - ✅ Logo or icon - ✅ Decimal places (important for allowing fractional token balances) This makes it easier for users and systems to interpret tokens consistently across the ecosystem. :::info The Cardano Token Registry data is included **by default** when using [`cardano-graphql`](https://github.com/input-output-hk/cardano-graphql), so exchanges using it can access token metadata without additional integration. ::: You can also self-host the token registry using the official GitHub repository: 🔗 [cf-token-metadata-registry – GitHub](https://github.com/cardano-foundation/cf-token-metadata-registry) :::tip Always check and validate the **decimal places** of a token using the registry to ensure accurate accounting and display of fractional amounts. ::: ### Minimum ada Requirement for Native Assets Every output must contain enough ada - the amount of ada depends on the **byte-size of the output**. This includes both the output being created and any change remaining. The minimum ada calculation is **simplified** (CIP-55) to be more transparent and predictable: **Current Formula:** `(160 + |serialized_output|) * coinsPerUTxOByte` Where: - `160` is the **constant overhead** in bytes (accounts for transaction input and UTxO map entry) - `|serialized_output|` is the size of the serialized output in bytes - `coinsPerUTxOByte` is the protocol parameter (converted from the previous `coinsPerUTxOWord` by dividing by 8) **Key Improvements:** - **Simpler calculation**: Switched from complex word-based formulas to straightforward byte-based calculation - **More predictable**: Linear relationship between output size and minimum ada required - **Easier to implement**: No need for complex asset counting - just measure serialized output size When a UTxO contains **native tokens (fungible or NFTs)**, the serialized output is larger due to: - Policy IDs - Asset names - Number of distinct assets in the output As these grow, so does the minimum ADA needed. :::note Transactions failing to meet the minimum ada requirement will be rejected by the network. ::: 🔗 **References**: - [CIP-55: The new minimum lovelace calculation](https://cips.cardano.org/cips/cip55/) #### Exchange Implementation Approaches **Deposits:** - Credit both ada and tokens when received together - Credit excess ada beyond the minimum requirement **Withdrawals:** Choose one approach: 1. **Deduct from ada balance**: Users must have sufficient ada to cover minimum requirement 2. **Auto-attach ada**: Automatically include required ada, deduct equivalent value in tokens #### Practical Examples (for simpler calculation fees are not considered) **Example 1: Token deposit** ``` User deposits: 1000 MyToken + 2.5 ada Minimum required: 1.25 ada Exchange credits: - MyToken: 1000 - ada: 2.5 (including 1 ada excess) ``` **Example 2: Direct token Buy (user deposit address has ada more than minimum required)** ``` User buy request: 1000 MyToken Minimum required: 1.25 ada Exchange credits: - MyToken: 1000 - ada: Use the ada from the user deposit address and create new utxos with ada + MyToken ``` **Example 3: Direct token Buy (user deposit address has no ada)** ``` User buy request: 1000 MyToken [conversion value: 1 ada ==> 100 MyToken] Minimum required: 1.25 ada Exchange credits: - MyToken: 875 - ada: 1.25 ada ``` **Example 4: Token withdrawal (user deposit address has ada more than minimum required)** ``` User withdraw request: 1000 MyToken Minimum required: 1.25 ada Exchange debited: - MyToken: 1000 - ada: 1.25 ada attached with utxo ``` **Example 5: Token withdrawal (user deposit address has no ada)** ``` User withdraw request: 1000 MyToken [conversion value: 1 ada ==> 100 MyToken] Minimum required: 1.25 ada Exchange debited: - MyToken: 875 - ada: 1.25 ada attached with utxo ``` **Calculation Methods:** 1. **Dynamic Calculation (Recommended)** - Use libraries like [`cardano-serialization-lib`](https://github.com/Emurgo/cardano-serialization-lib) for dynamic calculation 2. **Fixed Allocation (Simpler but less efficient)** - Single token: 1.5 ada - Multiple tokens: 2.0-2.5 ada - Complex multi-asset: 3.0 ada :::tip With the new CIP-55 formula, dynamic calculation is now much simpler and more predictable than before. Consider implementing it instead of fixed allocations for better efficiency. ::: ## Explorers All available explorers can be found [here](https://explorer.cardano.org). ## Handling Upgrades ### Upgrade Process - **Docker:** - Stop the containers. - Use the new docker-compose file to start the container again. - The volumes will ensure that any synced blockchain data will be maintained. - **Binaries:** - Build the new binary versions or use pre-built binaries. - Stop the service, swap in the new binary, start the service. - Make sure all [configuration files](https://book.world.dev.cardano.org/environments.html) and any command line arguments are up to date. ### Reliability of Upgrades Adopting a multi-environment strategy is essential for ensuring reliable and safe upgrades. Deploying changes first to a staging or pre-production environment allows for thorough validation before promotion to production. Implementing Infrastructure as Code (IaC) alongside CI/CD pipelines significantly reduces the risk of human error and enables consistent, repeatable deployments. To further enhance uptime and availability, it's recommended to maintain multiple instances of critical components. This ensures that a fully functional stack remains available during upgrades, minimizing or eliminating service interruptions. ### Testing Environment Running a dedicated testnet environment is highly recommended for exchanges to ensure robust testing and validation, especially when dealing with complex logic or preparing for events like hard forks. Testnets offer a safer and more flexible space to simulate real-world scenarios without risking production stability. They also require significantly less hardware and offer faster sync times compared to mainnet, making them ideal for continuous integration and testing workflows. There are two testnet environments: - **Preprod:** Configuration is the same as mainnet (5 days per epoch). - **Preview:** Configured to have one day per epoch. Faucets for [Test ada](https://docs.cardano.org/cardano-testnets/tools/faucet) ### Compatibility - Follow the Cardano [compatibility matrix](https://docs.cardano.org/developer-resources/release-notes/comp-matrix) for version alignment. ## Support and Resources - [Network configurations](https://book.world.dev.cardano.org/environments.html) --- ## Start building on Cardano Pick where you want to start. ## Want to jump into building? ## Want to learn how Cardano works? ## Coding with an AI agent? ## The full curriculum Seven modules, in order. Modules 1 and 2 get you transacting, 3 to 5 put your logic on-chain, 6 and 7 take an application to mainnet. | # | Module | What you can do after it | |---|---|---| | 1 | **[Learn the Fundamentals](/docs/developers/curriculum/fundamentals/overview)** | Read the ledger, plus the consensus and cryptography under it | | 2 | **[Start Building](/docs/developers/curriculum/start-building/overview)** | Build, sign, and submit transactions, and query the chain | | 3 | **[Mint Tokens & NFTs](/docs/developers/curriculum/native-tokens/overview)** | Mint tokens and NFTs under your own policy, with metadata wallets read | | 4 | **[Staking & Governance](/docs/developers/curriculum/staking-governance/overview)** | Delegate stake, claim rewards, and take part in CIP-1694 governance | | 5 | **[Write Smart Contracts](/docs/developers/curriculum/smart-contracts/overview)** | Write, test, and deploy a validator, and know how they get attacked | | 6 | **[Build a dApp](/docs/developers/curriculum/dapps/overview)** | Connect a wallet, authenticate users, and bring off-chain data on-chain | | 7 | **[Ship to Production](/docs/developers/curriculum/production/overview)** | Run the infrastructure, handle keys, and clear a pre-mainnet checklist | Module 6 also carries the applied tracks: [payments](/docs/developers/curriculum/dapps/listen-for-payments), [AI agents](/docs/developers/curriculum/dapps/ai-agents/overview), [oracles](/docs/developers/curriculum/dapps/oracles/overview), and an [Internet of Things](/docs/developers/curriculum/dapps/iot/) workshop. ## The rest of the portal - **[Builder Tools](/tools)**: SDKs, APIs, explorers, and libraries, filtered by language and by what they do. - **[Templates](/templates)**: dApp starters you scaffold in one command, plus a [contract library](/templates/contracts) of reference implementations by use case. - **[Cardano for Ethereum developers](/docs/developers/cardano-for-ethereum-developers)**: the model translated concept by concept from the EVM. - **[Exchange integrations](/docs/developers/exchange-integrations)**: deposits and withdrawals for custodial platforms. - **[Operator handbook](/docs/operators/)**: running a node or a stake pool. - **[Dev blog](/blog)**, the [developer community](/docs/community/cardano-developer-community), [grants and funding](/docs/community/funding), and the [talent pool](/talent). - **[Contributing](/docs/contribute/portal-contribute)**: this portal is open source. If a page here is wrong or missing, send a pull request. --- ## Cardano Key Pairs It's critical to understand the numerous cryptographic key pairs connected with Cardano, as well as the purpose of each key pair and best practices for securing those keys, before you start working with it. Every ambitious Cardano developer and stake pool operator should get a complete grasp of these key pairs, as well as the ramifications of a single secret (private) key being hacked. Any Cardano developer or stake pool operator must learn how to manage, safeguard, and store private keys in order to succeed. Cardano cryptographic keys are made up of `ed25519` key pairs, which include a `public verification key file` and a `secret (private) key file`. The public key file is commonly referred to as `keyname.vkey`, whereas the private key file is referred to as `keyname.skey`. The private key file, which is used to sign transactions, is extremely sensitive and should be adequately safeguarded. Under all circumstances, this entails limiting third-party access to your private keys. The most effective technique to prevent private key exposure is to guarantee that the necessary private key is never held for any length of time on any internet-connected machine (hot node). Please note that key pair filenames are completely random and can be named whatever you want. :::danger Use extreme caution to avoid losing or overwriting secret (private) keys. ::: ## Wallet address key pairs Currently, Cardano wallet addresses only have two parts: a payment address and a counterpart staking address. A payment address (together with its associated key pairs) is used to store, receive, and send money. A stake address (and related keys) is used to store and withdraw rewards, as well as to define the stake pool owner and rewards accounts, as well as the wallet's target stake pool delegation. `payment.vkey` is the public verification key file for the payment address (not sensitive; may be shared publicly). `payment.skey` is a highly sensitive payment address secret (private) signing key file. The private signing key file gives you access to monies in your payment address and should be kept safe at all times. :::danger Never place payment signing keys on a hot node. ::: `stake.vkey` - stake address public verification key file (not sensitive; may be shared publicly). `stake.skey` - It is a sensitive stake address secret (private) signing key file. This private signing key file gives you access to any awards cash held in the stake address, as well as the ability to delegate the wallet to a pool. It's also a good idea to keep an eye on the stake.skey. `payment.addr` - This is a Cardano wallet payment address that is usually generated with the help of both a payment.vkey and a stake. As inputs, use the vkey file. If a payment address is merely going to be used to send and receive money, no crucial components need to be staked. In addition, there is a single payment. Multiple unique stake.vkey files can be coupled with vkey to establish different payment addresses that can be staked independently. `stake.addr` - stake address for a Cardano wallet and is generated using the stake.vkey file ## Cardano stake pool key pairs ### Stake pool cold keys `cold.skey` - secret (private) signing key file for a Cardano stake pool (extremely sensitive). The `cold.skey` is required to register a stake pool, to update a stake pool registration certificate parameters, to rotate a stake pool KES keys and to retire a stake pool. `cold.vkey` - public verification key file for a stake pool's cold.skey private signing key file (cold.vkey is not sensitive; can be shared publicly). `cold.counter` - incrementing counter file that tracks the number of times an operational certificate (opcert) has been generated for the relevant stake pool. :::danger Always rotate KES keys using the latest `cold.counter`. ::: ### VRF hot keys `vrf.skey` - secret (private) signing key file for a Cardano stake pool's VRF key (required to start a stake pool's block producing node; sensitive but must be placed on a hot node in order to start a stake pool). `vrf.vkey` - public verification key file for a Cardano stake pool's vrf.skey (not sensitive and is not required to start a stake pool's block producing node). ### KES hot keys `kes.skey`- secret (private) signature key file for the stake pool's KES key (needed to start the stake pool's block producing node; sensitive, but must be placed on a hot node to start a stake pool and rotated on a regular basis). KES keys are needed to establish a stake pool's operating certificate, which expires 90 days after the opcert's defined KES period has passed. As a result, fresh KES keys must be generated along with a new opcert every 90 days or sooner for a Cardano Stake pool to continue minting blocks. `kes.vkey` - public verification key file for a Cardano stake pool's corresponding `kes.skey` (not sensitive and is not required to a block producer). ## References - [CIP 19 Cardano Addresses](https://cips.cardano.org/cip/CIP-0019) --- ## Consensus & Staking ### Understanding Consensus Consensus is the process by which a majority opinion is reached by everyone who is involved in running the blockchain. Agreement must be made on which blocks to produce, which chain to adopt, and to determine the single state of the network. The consensus protocol determines how individual nodes assess the current state of the ledger system and reach a consensus. It has three main responsibilities; to perform a leader check and decide if a block should be produced, to handle chain selection, and to verify blocks that are produced. Blockchains create consensus by allowing participants to bundle transactions that others have submitted to the system in _blocks_, and add them to their _chain_ (sequence of blocks). Determining who is allowed to produce a block when, and what to do in case of conflicts, (such as two participants adding different blocks at the same point of the chain), is the purpose of the different consensus protocols. Our ground-breaking proof-of-stake consensus protocol [Ouroboros](https://iohk.io/en/blog/posts/2020/06/23/the-ouroboros-path-to-decentralization/) is proven to have the same security guarantees that proof of work has. Rigorous security guarantees are established by Ouroboros and it was delivered with several peer-reviewed papers that were presented in top-tier conferences and publications in the area of cybersecurity and cryptography. Different [implementations of Ouroboros](https://iohk.io/en/blog/posts/2020/03/23/from-classic-to-hydra-the-implementations-of-ouroboros-explained/) have been developed. For further details on each flavour of Ouroboros, you can read the technical specifications for [Classic](https://www.iog.io/papers/ouroboros-a-provably-secure-proof-of-stake-blockchain-protocol), [Byzantine Fault Tolerance (BFT)](https://www.iog.io/papers/ouroboros-bft-a-simple-byzantine-fault-tolerant-consensus-protocol), [Genesis](https://www.iog.io/papers/ouroboros-genesis-composable-proof-of-stake-blockchains-with-dynamic-availability), [Praos](https://www.iog.io/papers/ouroboros-praos-an-adaptively-secure-semi-synchronous-proof-of-stake-protocol), and more recently the scalability solution [Hydra](https://eprint.iacr.org/2020/299.pdf). ### Stake Pools By running a Cardano node, users participate in and contribute to the network. A stake pool is a reliable server node that focuses on maintenance and holds the combined stake of various stakeholders in a single entity. Stake pools are responsible for processing transactions and producing new blocks and are at the core of Ouroboros, the Cardano proof-of-stake protocol. To be secure, Ouroboros requires a good number of ada holders to be online and maintaining sufficiently good network connectivity at any given time. This is why Ouroboros relies on stake pools, entities committed to run the protocol 24/7, on behalf of the contributing ada holders. While Ouroboros is cheaper to run than a proof of work protocol, running Ouroboros still incurs some costs. Therefore, stake pool operators are rewarded for running the protocol in the form of incentives that come from the transaction fees and from inflation of the circulating supply of ada. ### How Are New Blocks Produced? The goal of blockchain technology is the production of an independently-verifiable and cryptographically-linked chain of records (blocks). A network of block producers works to collectively advance the blockchain. A consensus protocol provides transparency and decides which candidate blocks should be used to extend the chain. Submitted valid transactions might be included in any new block. A block is cryptographically signed by its producer (the stake pool) and linked to the previous block in the chain. This makes it impossible to delete transactions from a block, alter the order of the blocks, remove a block from the chain (if it already has a number of other blocks following it), or to insert a new block into the chain without alerting all the network participants. This ensures the integrity and transparency of the blockchain expansion. #### Slots and Epochs The Cardano blockchain uses the Ouroboros Praos protocol to facilitate consensus on the chain. Ouroboros Praos divides time into epochs. Each Cardano epoch consists of a number of slots, where each slot lasts for one second. A Cardano epoch currently includes 432,000 slots (5 days). In any slot, zero or more block-producing nodes might be nominated to be the slot leader. On average, one node is expected to be nominated every 20 seconds, for a total of 21,600 nominations per epoch. If randomly elected slot leaders produce blocks, one of them will be added to the chain. Other candidate blocks will be discarded. #### Slot Leader Election The Cardano network consists of a number of stake pools that control the aggregated stake of their owners and other delegators, also known as stakeholders. Slot leaders are elected randomly from among the stake pools. The more stake the pool controls, the greater the chance it has of being elected as a slot leader to produce a new block that is accepted into the blockchain. This is the concept of proof-of-stake (PoS). #### Transaction Validation When validating a transaction, a slot leader needs to ensure that the sender has included enough funds to pay for that transaction and must also ensure that the transaction’s parameters are met. Assuming that the transaction meets all these requirements, the slot leader will record it as a part of a new block, which will then be connected to other blocks in the chain. ### Ouroboros Protocol #### Consensus Blockchains require an agreement mechanism between the participants of the network on how to add new transactions to the ledger and its state at any given moment. This mechanism is known as a consensus protocol. The goal of the consensus protocol is to ensure that only one chain is adopted and followed, otherwise, the system would collapse immediately. #### The Proof-of-work consensus algorithm Bitcoin implemented a Proof-of-work consensus algorithm. In this protocol, for a new block to be added to the blockchain, the node that attempts it must provide a proof-of-work, which is expressed by the solution of a mathematical puzzle. This process is known as mining. The node that solves the puzzle gets the right to create the new block and is rewarded for it. This scheme puts all nodes into a race against each other, and since only one node is rewarded, wastes a lot of computational power and energy. Such waste has raised concerns about the Bitcoin’s environmental impact. Currently, the Bitcoin mining process consumes as much energy as countries like the Netherlands or Iceland. Apart from the environmental concerns, the rewards scheme of the proof-of-work algorithm has also led to the centralization of the Bitcoin network. Up to 75% of the Bitcoin network computing power is located in China. And a single player, Bitmain, controls over 40% of the network hash rate. The underlying problem is that Bitcoin makes a clear distinction between the actual users of the network and the miners. Owning Bitcoins does not grant you any control over the network, nor any power over the decisions on the evolution of it. The system is controlled by a small pool of developers and miners. #### Ouroboros, a Proof-of-stake consensus algorithm In Ouroboros, there is no race between stakeholders to produce a block. Instead, a slot leader is randomly selected, proportionally to the amount of tokens he owns (the stake), to get the opportunity to produce a new block. So it is not hashing power what gives you the opportunity to produce a new block (and get rewarded for it), it is your stake what increases your chances to be elected. Since there is no race to mine a block, there is no waste of energy or computational resources. In that sense, Ouroboros is a more efficient and cheaper protocol to run than Bitcoin’s proof-of-work, while keeping all the security guarantees. #### What if you are not online? (Stake pools) To produce a block you have to be online, but asking everyone to be online at every moment is impractical and unrealistic. This is why Ouroboros introduces the figure of _Stake Delegation_. As stakeholder, you can delegate your stake to a third party to act on your behalf whenever you are elected slot leader. Such delegates are known as _staking pools_. They are members of the community that commit to run the protocol on your behalf and to be online close to 100% of the time. An important thing to notice is that you only delegate your rights to participate in the protocol, not your actual funds. Your ada are still secure and under your control in your wallet, and funds are not locked, you can still make transactions. #### What about the incentives? Stakeholders that issue blocks are incentivized to participate in the protocol by collecting transaction fees. But Ouroboros does not incentivize stakeholders to invest computational resources to issue blocks. Rather, availability and transaction verification are preferred. Rewards come from two sources: transaction fees and funds drawn from the ada Reserve. In Ouroboros, incentives are not block-dependant, instead, rewards from an epoch are collected in a pool and distributed among the stakeholders and stake pools that participated during these slots proportional to their stake. In the case of stake pools, those get a fraction of the rewards to cover operational costs and a profit margin. The rest is distributed among the pool members, including the pool owners, proportionally to the stake that they contributed to the pool. To participate in the protocol, you can choose a staking pool or choose to act on your own at any moment creating your own stake pool. #### What if for some reason there is a fork? Given that stakeholders are not always online, they come and go (a.k.a. dynamic availability), and sometimes they are offline for long periods, it is important for them to be able to resynchronize with the correct chain when they come back online. The key feature of Ouroboros Genesis is that thanks to a unique chain selection rule, it allows new or re-joining parties to synchronize to the “good chain” with only a trusted copy of the genesis block. This makes the protocol secure against the so-called “long-range attack”. #### Self-produced randomness Making the slot leader selection fair and secure **(staking procedure)** requires a good source of randomness. Ouroboros protocol (specifically Ouroboros Praos and Ouroboros Genesis) incorporates a Global Random Oracle feature that produces new and fresh randomness at every epoch. This is achieved by the implementation of a Verifiable Random Function. When evaluated with the key of a stakeholder, It returns a random value which is stored in every new block produced. The hashing of all values from the previous epoch becomes the random seed for the staking procedure. The blockchain itself becomes its source of new randomness. This is why the protocol is named Ouroboros, the snake that eats its own tail. #### Promoting Decentralization Finally, the Ouroboros incentives mechanism promotes the decentralization of the system in a better way than Proof-of-work does. Because Ouroboros considers two key scenarios: In one hand, a staking pool can only act as a delegate if it represents a certain number of stakeholders whose aggregate stake exceeds a given threshold, for example, 0.1% of all the stake in the blockchain. This prevents a fragmentation attack, where someone tries to affect the performance of the protocol by increasing the delegates population. At the same time, when the aggregate stake of a stake pool grows beyond a certain threshold, rewards become constant. This makes that particular stake pool less attractive since stakeholders would not be maximizing their rewards. For example, if the threshold is set to 1%, a stake pool with a stake of 2% would gain the same rewards as other that has a stake of only 1%. All these functionalities make Ouroboros the best proof of stake ledger protocol to date. And its only implementation is currently in the Cardano blockchain. ### How it works 1. **Time** is divided into epochs and slots and begins at Genesis. At most one block is produced in every slot. Only the slot leader can sign a block for a particular slot. 2. **Register:** The first thing a user needs to do to participate in the protocol is registering to: 1. a network to synchronize with the ledger 2. a global clock that indicates the current slot 3. a global random oracle that produces random values \(v\) and delivers them to the user 3. **Staking procedure** 1. At the beginning of every epoch, the online stakeholders fetch \(from the blockchain\) the **stake distribution** from the last block of 2 epochs ago. For example, if the current epoch is epoch 100, the stake distribution used is the distribution as it was in the last block of epoch 98. 2. **Random Oracle**: Is a hashing function that takes the random values “v” \(included in each block by the slot leader for this purpose\) from the first ⅔ slots in previous epoch and hash them together and use it as the random seed to select the slot leaders. 3. Stakeholders evaluate with their **secret key** the **Verifiable Random Function \(VRF\)** at every slot. If the output value \(v\) is below a certain threshold, the party becomes slot leader for that block. 1. **Certificate:** The **VRF** produces two outputs: **a random value \(v\)** and a **proof \(π\)** that the slot leader will include in the block he produces to certify that he is the legitimate slot leader for that particular slot. 2. Slot leader performs the following duties 3. Collects the transactions to be included in his block. 4. Includes in his block the random value \(v\) and proof \(π\) obtained from the VRF output. 5. Before broadcasting the block, the slot leader generates a new secret key **\(Key-evolving signature\)**. The public key remains the same, but the secret key is updated in every step and the old key is erased. 6. It is impossible to forge old signatures with new keys. And it is also impossible to derive previous keys from new ones. 7. Finally, the slot leader broadcast the new block to the network. 8. The **rewards** obtained by the slot leaders are calculated at the end of the epoch. Rewards come from transaction fees and funds from the ada reserve. **What happens in the case of a fork in the chain?** A key aspect of the procedure described above is that from time to time, it will produce slots without a slot leader and slots with multiple slot leaders. Meaning that nodes might receive valid chains from multiple sources. To determine which chain to adopt, each party collects all valid chains and applies the Chain Selection Rule. The same thing is done by users that have been offline for a while and need to synchronize with the blockchain. The node filters all valid chains (chains whose signatures are consistent with the genesis block and with the keys recorded in the Key Evolving Signature protocol, the variable random function and the global random oracle. Then applies the Chain Selection Rule: pick the longest chain as long as it grows more quickly (is denser) in the slots following the last common block to both competing chains. This chain selection rule allows for a party that joins the network at any time to synchronize with the correct blockchain, based only on a trusted copy of the genesis block and by observing how the chain grows for a sufficient time. ### Reference material [Ouroboros: A Provably Secure Proof-of-Stake Blockchain Protocol](https://eprint.iacr.org/2016/889.pdf) [Ouroboros Praos: An adaptively-secure, semi-synchronous proof-of-stake blockchain](https://eprint.iacr.org/2017/573.pdf) [Ouroboros Genesis: Composable Proof-of-Stake Blockchains with Dynamic Availability](https://eprint.iacr.org/2018/378.pdf) ### Video: What’s an Ouroboros and how you cook it? #### Slot Lottery In this video, we describe exactly how a stake pool on Cardano gets elected to make a block. #### Slot Battles On Cardano, slot battles happen when two pools try to make a block in the same slot (at the same time). We break down how the blockchain determines which block should win and what is the "correct" source of truth on the blockchain. #### Epoch Nonce The epoch nonce allows you to calculate leaderlogs for your stake pool on Cardano. --- ## Minimum hardware requirements to run a stake pool The latest technical specifications and supported platforms can be found on the [Cardano Node release page](https://github.com/IntersectMBO/cardano-node/releases). :::info version reference As of May 2026 the following specifications are recommended on Mainnet: ::: - Servers: 1 for block producer node + at least 2 for relay nodes - CPU: An Intel or AMD x86 processor with two or more cores at 2GHz or faster - 24GB of RAM when running with the InMemory backend, 8GB when running with the OnDisk backend (pending confirmation) - Storage: 250GB of free storage (350GB recommended for future growth) - Operating system: see the [releases page](https://github.com/IntersectMBO/cardano-node/releases/latest) for supported platforms - Broadband: a good network connection with about 1 GB of bandwidth per hour on a public IP4 address - [Air-gapped environment](/docs/operators/security/air-gap) for key security For testnet pools, some requirements are smaller: - Memory: 4GB of RAM - Storage: 20GB of free storage - Air-gapped environment not required --- ## Understanding the Relay and Block Producer topology Before we start with the stake pool installation and configuration, it is essential to understand the logical topology of stake pools. Typically every stake pool has one block producer and at least one relay node. To ensure fast propagation of blocks the pool produces, it is recommended to have relays in different geographical locations. To secure the block producer from the internet it should only connect to its own relays, or relays you trust. The relays then connect to the rest of the Cardano network. To minimise the risk of [height battles](https://forum.cardano.org/t/how-to-figure-out-when-pool-wins-slot-battles-or-causes-height-battles/90639), it is recommended to regularly monitor the propagation time of the relays. We will talk more on this topic in our pool monitoring section later. In the example below, one block producer is connected to three relays and they are connected to other relays of the Cardano network. ![Block producer node is connected with 3 relays. Those relays are connected with the cardano network.](./img/stake-pool-network.jpg) For the operation of a stake pool, some additional nodes in the topology might be useful. For example, a back-up node for block producer is helpful in case the main block producer has an issue e.g. during an upgrade. Depending on the number of stake pools that are being managed, a monitoring node can be used to monitor them and generate alarms. Stake pool operators also need a [air-gapped system](/docs/operators/security/air-gap) to store the pool cold keys. Some pool operators use a hardware wallet for storing the pool pledge. A logical topology of all the components, without considering firewalls and other security aspects can be shown as follows. ![3 relays are connected with 1 block producer, one backup block producer and a monitoring node. The monitoring node is connected with the home PC. There is a air-gapped pc, but it is not connected with other computers.](./img/stake-pool-setup.jpg) --- ## Key Generation :::info version reference This document was written in May 2026 with reference to cardano-node and cardano-cli v11 ::: A block producer requires three key pairs and an operational certificate: | Key | Purpose | Where it lives | |-----|---------|----------------| | Cold key (`cold.skey` / `cold.vkey`) | Authorizes pool registration and KES rotation | Air-gapped machine only — never transferred | | KES key (`kes.skey` / `kes.vkey`) | Signs blocks; rotated every ~90 days | Block producer | | VRF key (`vrf.skey` / `vrf.vkey`) | Proves slot leadership | Block producer | | Operational certificate (`node.cert`) | Binds KES key to cold key for the node | Block producer | :::danger Cold key security The cold signing key (`cold.skey`) must be generated and used exclusively on your air-gapped machine. It must never exist on any internet-connected computer. If your cold key is compromised, an attacker can re-register your pool to their reward address. ::: For background on what these keys do, see [Cardano Key Pairs](/docs/operators/basics/cardano-key-pairs). ## Step 1 — Generate all keys on the air-gapped machine Run all of the following on your **air-gapped machine**. ### Cold keys ```bash cardano-cli node key-gen \ --cold-verification-key-file cold.vkey \ --cold-signing-key-file cold.skey \ --operational-certificate-issue-counter cold.counter ``` ### KES keys ```bash cardano-cli node key-gen-KES \ --verification-key-file kes.vkey \ --signing-key-file kes.skey ``` ### VRF keys ```bash cardano-cli node key-gen-VRF \ --verification-key-file vrf.vkey \ --signing-key-file vrf.skey ``` ## Step 2 — Determine the current KES period The operational certificate must be issued for the correct KES period. You need the current slot number from an online node to calculate it. On your **online relay or another synced node**, run: ```bash slotsPerKESPeriod=$(jq -r '.slotsPerKESPeriod' /etc/cardano/shelley-genesis.json) currentSlot=$(cardano-cli query tip | jq -r '.slot') kesPeriod=$(( currentSlot / slotsPerKESPeriod )) echo "Current KES period: $kesPeriod" ``` Transfer this number to your air-gapped machine (write it down or copy it on a USB drive). Next, follow [Deployment](/docs/operators/block-producer/deployment) to issue the op cert and securely copy credentials to the block producer. ## KES key rotation KES keys expire after about 90 days on both mainnet and preprod, which share the same KES parameters (62 evolutions of 36 hours each). When they expire the node stops minting blocks. Set a calendar reminder well before expiry. To rotate: 1. Generate new KES keys on the air-gapped machine (repeat Step 1 above for KES only) 2. Get the current KES period from an online node (Step 2 above) 3. Follow the [Deployment — op cert and transfer](/docs/operators/block-producer/deployment) steps to issue a new cert and copy it to the block producer 4. Send `SIGHUP` to reload credentials without a full restart: `pkill -HUP cardano-node` :::caution Counter must be strictly increasing The `cold.counter` file tracks how many op certs have been issued. Never copy an old counter back — an op cert with a lower counter than what the chain has seen will be rejected. The counter increments automatically each time you run `issue-op-cert`. ::: For a more secure KES key deployment that keeps the signing key out of persistent storage entirely, see [KES Agent](/docs/operators/block-producer/kes-agent). --- ## Block Producer Deployment :::info version reference This document was written in May 2026 with reference to cardano-node and cardano-cli v11 ::: This page covers issuing the operational certificate and securely moving credentials from your air-gapped machine to the block producer. Complete [Key Generation](/docs/operators/block-producer/block-producer-keys) first. :::note Configuration management is out of scope This page describes manual deployment steps suitable for a single operator managing a small number of nodes. Production operators running larger infrastructures typically manage configuration with dedicated tooling — [cardano-parts](https://github.com/input-output-hk/cardano-parts) (IOG's own Nix-based deployment framework), [sops-nix](https://github.com/Mic92/sops-nix) or [sops](https://github.com/getsops/sops) for secret management, Ansible, Puppet, or similar. Those workflows are out of scope here; consult the relevant project documentation. ::: ## Issue the operational certificate The op cert is signed on the air-gapped machine using the cold key. You need the KES verification key — where it comes from depends on your setup: - **Standard setup** — `kes.vkey` was generated in [Key Generation](/docs/operators/block-producer/block-producer-keys#step-1--generate-all-keys-on-the-air-gapped-machine) - **KES agent** — export it from the running agent: see [KES Agent — key generation workflow](/docs/operators/block-producer/kes-agent#key-generation-workflow) On your **air-gapped machine**, using the KES period calculated in [Step 2 of Key Generation](/docs/operators/block-producer/block-producer-keys#step-2--determine-the-current-kes-period): ```bash cardano-cli node issue-op-cert \ --kes-verification-key-file kes.vkey \ --cold-signing-key-file cold.skey \ --operational-certificate-issue-counter cold.counter \ --kes-period \ --out-file node.cert ``` ## Securely transfer credentials to the block producer The following files must be copied from the air-gapped machine to the block producer: | File | Required without KES agent | Required with KES agent | |------|:-:|:-:| | `node.cert` | ✓ | ✓ | | `vrf.skey` | ✓ | ✓ | | `kes.skey` | ✓ | — (agent holds it) | The cold key (`cold.skey`) and counter (`cold.counter`) **stay on the air-gapped machine**. ### Transfer methods **Encrypted USB stick** Write the credential files to a USB stick encrypted with LUKS or VeraCrypt. Mount it on the block producer, copy the files, then unmount and wipe the stick. **magic-wormhole** [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) transfers files end-to-end encrypted using a short human-pronounceable code. No shared secrets or SSH keys required. This is suitable for transfers between networked machines (e.g. a build host to the block producer), not from a true air-gapped machine: ```bash # on the sending machine wormhole send node.cert vrf.skey kes.skey # on the block producer wormhole receive ``` **sops / age** Encrypt the files with [age](https://github.com/FiloSottile/age) before moving them, using the block producer's public key: ```bash # on the block producer — generate a key pair once and store securely install -d -m 700 /root/.age age-keygen -o /root/.age/key.txt # copy the public key (printed to stdout) to the air-gapped machine # on the air-gapped machine — encrypt tar czf - node.cert vrf.skey kes.skey | age -r -o credentials.tar.gz.age # on the block producer — decrypt sudo mkdir -p /run/secrets age -d -i /root/.age/key.txt credentials.tar.gz.age | sudo tar xz -C /run/secrets/ ``` [sops](https://github.com/getsops/sops) is a higher-level option that integrates with age, PGP, or cloud KMS and works well if you manage server secrets in a git repository. ## Set file permissions ```bash sudo chown cardano:cardano /run/secrets/{node.cert,vrf.skey,kes.skey} sudo chmod 400 /run/secrets/{vrf.skey,kes.skey} ``` ## Configure the systemd unit Add the credential flags to the `ExecStart` line in `/etc/systemd/system/cardano-node.service`: ```ini ExecStart=/usr/local/bin/cardano-node run \ --config /etc/cardano/config.json \ --topology /etc/cardano/topology.json \ --database-path /var/lib/cardano/db \ --socket-path /run/cardano/node.socket \ --host-addr 0.0.0.0 \ --port 6000 \ --shelley-kes-key /run/secrets/kes.skey \ --shelley-vrf-key /run/secrets/vrf.skey \ --shelley-operational-certificate /run/secrets/node.cert ``` If using the KES agent, replace `--shelley-kes-key /run/secrets/kes.skey` with `--shelley-kes-agent-socket /run/kes-agent/service.socket`. Reload and start: ```bash sudo systemctl daemon-reload sudo systemctl restart cardano-node ``` ## Block producer topology The block producer must not be reachable from the public internet. Its topology connects only to your own relays, with ledger peer discovery disabled: ```json { "localRoots": [ { "accessPoints": [ { "address": "YOUR-RELAY-1-IP", "port": 3001 }, { "address": "YOUR-RELAY-2-IP", "port": 3001 } ], "advertise": false, "hotValency": 2, "warmValency": 2, "trustable": false } ], "bootstrapPeers": null, "publicRoots": [], "useLedgerAfterSlot": -1 } ``` --- ## Generating Wallet Keys :::info version reference This document was written in May 2026 with reference to cardano-node and cardano-cli v11 ::: A stake pool registration requires a funded wallet address. The address is a combination of a payment key (pays fees and deposits) and a stake key (receives rewards and anchors the pool registration). :::warning Mainnet key management These pages show the raw `cardano-cli` approach, which is appropriate for testnet. For mainnet, never generate payment or stake keys on an internet-connected machine. Production options: - **[cardano-addresses](https://github.com/IntersectMBO/cardano-addresses) with a GPG-encrypted mnemonic on an air-gapped machine** — most secure; fully offline, hardware-independent, recoverable from mnemonic - **Hardware wallet** (Ledger or Trezor via [cardano-hw-cli](https://github.com/vacuumlabs/cardano-hw-cli)) — keys never leave the device; convenient for signing transactions ::: Make sure `CARDANO_NODE_SOCKET_PATH` and `CARDANO_NODE_NETWORK_ID` are set before running any `cardano-cli` commands. See [Running cardano-node](/docs/operators/node/running-cardano#querying-the-node). ## Generate payment keys ```bash mkdir -p $HOME/pool-keys cd $HOME/pool-keys cardano-cli address key-gen \ --verification-key-file payment.vkey \ --signing-key-file payment.skey ``` ## Generate stake keys ```bash cardano-cli stake-address key-gen \ --verification-key-file stake.vkey \ --signing-key-file stake.skey ``` Build the stake address: ```bash cardano-cli stake-address build \ --stake-verification-key-file stake.vkey \ --out-file stake.addr ``` ## Build the payment address The payment address combines your payment key with your stake key so that ADA sent to it accrues rewards to your stake address: ```bash cardano-cli address build \ --payment-verification-key-file payment.vkey \ --stake-verification-key-file stake.vkey \ --out-file payment.addr ``` ## Fund the address Query the balance: ```bash cardano-cli query utxo --address $(cat payment.addr) ``` On testnet, use the [Cardano faucet](https://docs.cardano.org/cardano-testnet/tools/faucet) to get test ADA. Select the Pre-Production testnet and paste your `payment.addr`. Once funded, the balance query should show something like: ``` TxHash TxIx Amount -------------------------------------------------------------------------------------- 531f4bec36af503654c3c6fa34ecf07e5c29f67da8e2b84c8923b8c735b011c9 0 10000000000 lovelace ``` Next: [Register your stake address](/docs/operators/block-producer/register-stake-address). --- ## KES Agent By default, `cardano-node` reads the KES signing key directly from disk. The KES agent is a separate process that holds the signing key in mlocked RAM instead — it never touches persistent storage. When the agent evolves the key at the start of each KES period, the previous evolution is deleted from memory. An attacker who later compromises the host cannot recover past signing keys. Requires `cardano-node` 10.7.1 or later. :::note System hardening For the forward secrecy guarantee to hold, the signing key must not reach disk through other paths. Before running the KES agent, disable swap, hibernation, and core dumps on the block producer host. See the [KES agent guide](https://github.com/input-output-hk/kes-agent/blob/master/doc/guide.markdown) for full hardening recommendations. ::: ## Setup Install `kes-agent` and `kes-agent-control` from the [kes-agent releases](https://github.com/input-output-hk/kes-agent/releases). The agent needs `cold.vkey` on the block producer to validate op certs before activating new keys. Transfer it from the air-gapped machine — it is a public key and safe to copy to the block producer. Start the agent — it exposes two Unix domain sockets, one for the node and one for management: ```bash kes-agent run \ --service-address /run/kes-agent/service.socket \ --control-address /run/kes-agent/control.socket \ --cold-verification-key /etc/cardano/cold.vkey \ --genesis-file /etc/cardano/shelley-genesis.json ``` For production, run it as a systemd service. A unit file template is available in the [kes-agent repository](https://github.com/input-output-hk/kes-agent/tree/master/systemd). ## Key generation workflow **On the block producer**, ask the agent to generate a new KES key. Only the verification key is written to disk — the signing key stays in mlocked RAM: ```bash kes-agent-control \ --control-address /run/kes-agent/control.socket \ gen-staged-key \ --kes-verification-key-file kes.vkey ``` Transfer `kes.vkey` to the air-gapped machine, then follow [Deployment](/docs/operators/block-producer/deployment) to issue the op cert and transfer `node.cert` back to the block producer. The agent validates the cert against `cold.vkey` and activates the staged key. ## Node configuration Replace `--shelley-kes-key` with `--shelley-kes-agent-socket` in your systemd unit: ```ini ExecStart=/usr/local/bin/cardano-node run \ --config /etc/cardano/config.json \ --topology /etc/cardano/topology.json \ --database-path /var/lib/cardano/db \ --socket-path /run/cardano/node.socket \ --host-addr 0.0.0.0 \ --port 6000 \ --shelley-kes-agent-socket /run/kes-agent/service.socket \ --shelley-vrf-key /run/secrets/vrf.skey \ --shelley-operational-certificate /run/secrets/node.cert ``` ## KES rotation 1. Run `gen-staged-key` on the block producer to generate a new key in the agent 2. Transfer `kes.vkey` to the air-gapped machine 3. Follow [Deployment](/docs/operators/block-producer/deployment) to issue a new op cert and transfer `node.cert` back 4. The agent activates the new key automatically on receipt of the cert — no restart needed The signing key never touches disk at any point in this process. ## Multi-agent setups The KES agent supports backup agents and SSH socket forwarding for high-availability deployments where the agent runs on a separate host from the block producer. See the [KES agent guide](https://github.com/input-output-hk/kes-agent/blob/master/doc/guide.markdown) for linear, ring, and web topologies. --- ## Mithril Signer Configuration The Mithril signer is the component that signs snapshots of the blockchain state. It runs on the block producer because it needs access to the pool's `operational certificate` and `KES secret key`, which it uses to compute your `PoolId`, prove pool ownership, and participate in the multi-signature process. :::important The Mithril signer must **never** connect directly to the internet. In the **production** deployment (required on `mainnet`), all outbound traffic goes exclusively through the Mithril relay. See [Relay Configuration](/docs/operators/relay-configuration/relay-node-configuration#mithril-relay-optional-required-for-mithril-signing) for more details. ::: ## Overview Setting up the signer involves four stages: 1. **Prerequisites** -- ensure the Cardano node is fully synchronized and running a [compatible version](https://github.com/IntersectMBO/mithril/blob/main/networks.json), with active KES keys. The signer requires read access to the node's database directory and read/write access to the node socket. 2. **Installation** -- download the pre-built `mithril-signer` binary or build it from source, then register it as a `systemd` service running as the same user as the Cardano node. 3. **Credential configuration** -- create an environment file at `/opt/mithril/mithril-signer.env` pointing to your `KES secret key`, `operational certificate`, Cardano node database, node socket, and `cardano-cli` path. 4. **Connecting the pipeline** -- set the `RELAY_ENDPOINT` variable to your internal Mithril relay address (for example, `http://192.168.1.50:3132`). The signer sends signatures through the relay, which forwards them to the Mithril aggregator. ## Key operational details - The signer is lightweight: less than `5%` CPU and under `200 MB` of memory during normal operation - It sends a new signature roughly every `10 minutes` and a new registration every `5 days` - On first launch, a pre-loading phase runs with higher CPU usage for approximately `5 hours` - When KES keys are rotated, update `KES_SECRET_KEY_PATH` and `OPERATIONAL_CERTIFICATE_PATH` in the environment file if needed, then restart the service :::note For the complete step-by-step instructions, including build commands, environment file examples, and service configuration, see [Set up the Mithril signer node](https://mithril.network/doc/manual/operate/run-signer-node#set-up-the-mithril-signer-node) in the Mithril documentation. ::: ## Resources - [Become a Mithril SPO](https://mithril.network/doc/manual/operate/become-mithril-spo) - [Run a Mithril signer node](https://mithril.network/doc/manual/operate/run-signer-node) - [Mithril documentation](https://mithril.network/doc/) - [GitHub repository](https://github.com/IntersectMBO/mithril) --- ## Registering a Stake Address :::info version reference This document was written in May 2026 with reference to cardano-node and cardano-cli v11 ::: Before registering a pool, the stake address must be registered on-chain. This costs a deposit (currently 2 ADA on mainnet, returned when you deregister) plus transaction fees. This page assumes you have completed [Generating Wallet Keys](/docs/operators/block-producer/generating-wallet-keys) and that `CARDANO_NODE_SOCKET_PATH` and `CARDANO_NODE_NETWORK_ID` are set. ## Create the registration certificate ```bash cardano-cli stake-address registration-certificate \ --stake-verification-key-file stake.vkey \ --out-file stake.cert ``` ## Build, sign, and submit the transaction Query the current slot (used for `--invalid-hereafter`): ```bash currentSlot=$(cardano-cli query tip | jq -r '.slot') ``` Build the transaction — `transaction build` calculates fees and change automatically: ```bash cardano-cli conway transaction build \ --tx-in $(cardano-cli query utxo --address $(cat payment.addr) --out-file /dev/stdout | jq -r 'keys[0]') \ --change-address $(cat payment.addr) \ --certificate-file stake.cert \ --invalid-hereafter $(( currentSlot + 1000 )) \ --witness-override 2 \ --out-file tx.raw ``` :::note `--witness-override 2` tells the fee estimator that two keys will sign (payment + stake). If you have more signers, adjust accordingly. ::: Sign with both the payment and stake signing keys: ```bash cardano-cli conway transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file stake.skey \ --out-file tx.signed ``` Submit: ```bash cardano-cli conway transaction submit --tx-file tx.signed ``` Next: [Register your pool](/docs/operators/block-producer/register-stake-pool). --- ## Registering a Pool :::info version reference This document was written in May 2026 with reference to cardano-node and cardano-cli v11 ::: Pool registration requires: 1. A metadata JSON file hosted at a public HTTPS URL 2. A pool registration certificate (signed with cold keys on the air-gapped machine) 3. A delegation certificate (pledging your stake to your own pool) 4. A transaction submitting both certificates This page assumes you have completed [Generating Wallet Keys](/docs/operators/block-producer/generating-wallet-keys), [Registering a Stake Address](/docs/operators/block-producer/register-stake-address), and [Key Generation](/docs/operators/block-producer/block-producer-keys). You will need your cold keys (`cold.vkey`, `cold.skey`), which live on your air-gapped machine. ## Create pool metadata ```bash cat > poolMetaData.json << EOF { "name": "Your Pool Name", "description": "Your pool description", "ticker": "TICK", "homepage": "https://yourpool.example.com" } EOF ``` - `ticker`: 3–9 characters, A–Z and 0–9 only - `description`: 255 characters maximum - `homepage`: your pool's website Hash the file: ```bash cardano-cli stake-pool metadata-hash \ --pool-metadata-file poolMetaData.json \ --out-file poolMetaDataHash.txt ``` Host `poolMetaData.json` at a public HTTPS URL with no redirects. The URL must be 64 characters or fewer. Verify the hosted file matches your local hash: ```bash cardano-cli stake-pool metadata-hash \ --pool-metadata-file <(curl -s -L https://YOUR_METADATA_URL) cat poolMetaDataHash.txt ``` Both hashes must be identical. If they differ, re-upload the file (extra whitespace or encoding differences are common causes). :::tip SPO identity — Calidus keys After registering your pool, consider registering a [Calidus key](/docs/operators/operator-tools/calidus-keys). It lets explorers (Cardanoscan, Cexplorer, AdaStat), governance tools, and APIs verify your SPO identity with a hot key — without ever touching your cold key again. ::: ## Generate the pool registration certificate Do this on your **air-gapped machine** where `cold.skey` lives. Fetch current protocol parameters on an online node first: ```bash cardano-cli query protocol-parameters --out-file protocol.json minPoolCost=$(jq -r '.minPoolCost' protocol.json) echo "Minimum pool cost: $minPoolCost lovelace" ``` Transfer `protocol.json`, `vrf.vkey`, `stake.vkey`, and `poolMetaDataHash.txt` to the air-gapped machine, then generate the certificate: ```bash cardano-cli stake-pool registration-certificate \ --cold-verification-key-file cold.vkey \ --vrf-verification-key-file vrf.vkey \ --pool-pledge 10000000000 \ --pool-cost 340000000 \ --pool-margin 0.01 \ --pool-reward-account-verification-key-file stake.vkey \ --pool-owner-stake-verification-key-file stake.vkey \ --single-host-pool-relay relay1.yourpool.example.com \ --pool-relay-port 3001 \ --metadata-url https://YOUR_METADATA_URL \ --metadata-hash $(cat poolMetaDataHash.txt) \ --out-file pool.cert ``` | Parameter | Notes | |-----------|-------| | `--pool-pledge` | Amount in lovelace you commit to keep delegated. Higher pledge improves desirability. | | `--pool-cost` | Fixed fee per epoch in lovelace taken before margin. Minimum is `minPoolCost` (currently 170 ADA on mainnet). | | `--pool-margin` | Your variable fee as a fraction (e.g. `0.01` = 1%). | ### Relay address You can register your relay(s) using either the customary single host method of DNS A or AAAA records, or as multi-host corresponding to a DNS SRV record (supported since cardano-node 10.6). You can't provide both. #### Single host method To register using a domain name via a DNS A or AAAA record, use `--single-host-pool-relay DNS_NAME` + `--pool-relay-port PORT` pair per relay. For IP-based relays use `--pool-relay-ipv4`. :::note Multiple relays Add one flag pair per relay: ```bash --single-host-pool-relay relay1.yourpool.example.com --pool-relay-port 3001 \ --single-host-pool-relay relay2.yourpool.example.com --pool-relay-port 3001 \ ``` ::: #### Multi host method (SRV record) For added flexibility, you can provide your domain name whose DNS zone file contains SRV records per table below. To register using this method, use `--multi-host-pool-relay` option in the `cardano-cli` command above instead. This method also provides a mechanism for exposing related decentralised protocols co-deployed with a Cardano node, such as Mithril or Hydra. You only need to specify the records for the services your pool provides. | Service | Required SRV record | | --- | --- | | Cardano node | `_cardano._tcp` | | DMQ node (Mithril protocol) | `_dmq._mithril._cardano_.tcp` | | Mithril aggregator | `_aggregator._mithril._cardano._tcp` | :::warning[Important] The prefix from the second column is not part of the pool registration certificate and is not entered on the CLI. It should only be a part of your DNS record which is looked up when resolving your domain name. ::: For eg, to specify access to your Cardano node relays, there should be a _cardano._tcp SRV entry in the DNS record for your registration domain `yourpool.example.com`. For details, see [CIP-0155](https://cips.cardano.org/cip/CIP-0155) and current SRV [registry](https://raw.githubusercontent.com/cardano-foundation/CIPs/master/CIP-0155/registry.json). :::note `--pool-relay-port` is not used with this approach since your SRV record specifies which ports to use. ::: ## Generate the delegation certificate Also on the **air-gapped machine**, create a delegation certificate that pledges your stake to your pool: ```bash cardano-cli latest stake-address stake-delegation-certificate \ --stake-verification-key-file stake.vkey \ --cold-verification-key-file cold.vkey \ --out-file deleg.cert ``` Transfer `pool.cert` and `deleg.cert` back to your online machine. ## Submit the certificates Query the current slot: ```bash currentSlot=$(cardano-cli query tip | jq -r '.slot') ``` Build the transaction: ```bash cardano-cli conway transaction build \ --tx-in $(cardano-cli query utxo --address $(cat payment.addr) --out-file /dev/stdout | jq -r 'keys[0]') \ --change-address $(cat payment.addr) \ --certificate-file pool.cert \ --certificate-file deleg.cert \ --invalid-hereafter $(( currentSlot + 1000 )) \ --witness-override 3 \ --out-file tx.raw ``` Sign with payment, cold, and stake keys. The cold signing key must be available — bring it from the air-gapped machine for this step only, or sign in two passes using `--signing-key-file` once per key on separate machines: ```bash cardano-cli conway transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --signing-key-file cold.skey \ --signing-key-file stake.skey \ --out-file tx.signed ``` Submit: ```bash cardano-cli conway transaction submit --tx-file tx.signed ``` ## Verify registration Get your pool ID: ```bash cardano-cli stake-pool id \ --cold-verification-key-file cold.vkey \ --output-format hex \ > stakepoolid.txt cat stakepoolid.txt ``` Check it has appeared on-chain: ```bash cardano-cli query stake-snapshot --stake-pool-id $(cat stakepoolid.txt) ``` A non-empty result means registration was successful. It may take one epoch boundary to appear in tools and explorers. You can also verify on a [block explorer](/docs/developers/curriculum/start-building/networks-and-test-ada). --- ## Audit your node In order to check if your node configuration is correct, you can run an audit script that checks SecOps basic settings, and Cardano node compliance (topology, version, key files...) ## What the script does : The script runs several checks on your Cardano stake pool node. It works on various types of Cardano installation (CNODE Guild Operatos, Coincashew, others...). Configuration files and services are parsed and analysed by the script : **Cardano compliance** - Cardano-node version verification - Cardano bootstrap check - Environment variables - Systemd cardano-node file verification and parsing - Node operation mode (Block Producer / Relay) - Topology configuration file parsing and compliance checks - Cardano security checks (hot keys permissions, cold keys detection) - KES keys rotation alert **Security and system checks** - SSHD hardening - Null passwords check - Important services running (ufw, fail2ban, ntp server...) - Firewalling rules extract - sysctl.conf hardening check Please note that this script is only intended to help you identify configuration and basic security issues. It does not guarantee that your server is fully protected. ## Pre-Requisites : 1- The script is 100% shell bash. It works on Linux systems. 2- cardano-node up and running. See [Stake Pool Operation](/docs/operators/) for setup guides. 3- Several bash commands are necessary (tput, date, grep, awk, jq). A check is performed when the script starts. 4- cardano-cli is also used for KES key rotate check. ## How to use : ### Download the script and make it executable : The script can be found on this [GitHub repository](https://github.com/Kirael12/cardano-node-audit) You can directly download the repository from your Cardano Nodes : ```bash wget --show-progress -q https://github.com/Kirael12/cardano-node-audit/releases/latest/download/audit-cardano-node.sh chmod +x audit-cardano-node.sh ``` ### Run the script The script must be ran with sudo and the -E option, to include your environment variables. ```bash sudo -E ./audit-cardano-node.sh ``` A selection menu allow you to select your Cardano installation type. You can also choose to perform Security Checks only. You can then choose to export the results to a file. ## Results It takes around 20 seconds for the script to complete. You'll get information about your node and will immediately be able to check whether your configuration is good or not, and make appropriate changes. --- ## Hardening the server This guide covers the baseline security hardening for a Cardano stake pool server running Ubuntu 22.04 LTS. Apply it to each machine in your setup: relays, block producer, and monitoring host. ## 1. Non-root user Never operate as root. Create a dedicated operator account with sudo access: ```bash sudo useradd -m -s /bin/bash cardano-op sudo passwd cardano-op sudo usermod -aG sudo cardano-op ``` Log out and reconnect as `cardano-op` for all subsequent steps. Lock the root account: ```bash sudo passwd -l root ``` ## 2. SSH key authentication On your **local machine**, generate an ED25519 key pair: ```bash ssh-keygen -t ed25519 -C "stake-pool-ops" ``` Copy the public key to the server: ```bash ssh-copy-id -i ~/.ssh/id_ed25519.pub cardano-op@ ``` Verify you can log in with the key before continuing. Then harden `/etc/ssh/sshd_config` on the server: ``` Port 2222 # change to any unprivileged port PubkeyAuthentication yes PasswordAuthentication no PermitRootLogin without-password PermitEmptyPasswords no X11Forwarding no AllowTcpForwarding no AllowAgentForwarding no Compression no TCPKeepAlive no KbdInteractiveAuthentication no MaxAuthTries 3 LoginGraceTime 30 ``` Validate and reload: ```bash sudo sshd -t && sudo systemctl reload ssh ``` :::caution Back up your private key before disabling password authentication. If you lose the key you will be locked out. ::: ## 3. System updates ```bash sudo apt-get update -y && sudo apt-get upgrade -y && sudo apt-get autoremove -y sudo reboot ``` Enable automatic security updates (security patches only — will not reboot): ```bash sudo apt-get install -y unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades ``` ## 4. Firewall — nftables Ubuntu ships with nftables. Create `/etc/nftables.conf` appropriate for your node role. :::note Replace `2222` with your actual SSH port, and adjust Cardano ports and IP ranges to match your setup. :::
Relay node ``` #!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; # established / related ct state established,related accept # loopback iifname "lo" accept # ICMP (needed for path MTU discovery) ip protocol icmp accept ip6 nexthdr icmpv6 accept # SSH — restrict to your management IP if possible tcp dport 2222 accept # Cardano P2P — accept from any peer tcp dport 3001 accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } ```
Block producer — same-datacenter relays (no WireGuard) ``` #!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; ct state established,related accept iifname "lo" accept ip protocol icmp accept ip6 nexthdr icmpv6 accept # SSH — management IP only ip saddr tcp dport 2222 accept # Cardano — relays only ip saddr { , } tcp dport 6000 accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } ```
Block producer — relays in a different datacenter (WireGuard) See [section 5 — WireGuard](#5-wireguard--relay--bp-across-datacenters) for the WireGuard setup first, then use this ruleset which accepts Cardano traffic only from the WireGuard interface: ``` #!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; ct state established,related accept iifname "lo" accept ip protocol icmp accept ip6 nexthdr icmpv6 accept # SSH — management IP only ip saddr tcp dport 2222 accept # WireGuard tunnel — accept the UDP handshake port udp dport 51820 accept # Cardano — only from WireGuard addresses iifname "wg0" ip saddr { 10.0.0.2, 10.0.0.3 } tcp dport 6000 accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } ```
Enable and apply: ```bash sudo systemctl enable nftables sudo nft -f /etc/nftables.conf sudo nft list ruleset # verify ``` ## 5. WireGuard — relay ↔ BP across datacenters If your relays and block producer are in different datacenters, **do not expose the block producer's Cardano port to the public internet**. Run a WireGuard VPN between them and route Cardano traffic through the tunnel. Install on each machine: ```bash sudo apt-get install -y wireguard ``` Generate a key pair on each machine: ```bash wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key sudo chmod 600 /etc/wireguard/private.key ``` **Block producer** — `/etc/wireguard/wg0.conf`: ```ini [Interface] Address = 10.0.0.1/24 ListenPort = 51820 PrivateKey = [Peer] # relay-1 PublicKey = AllowedIPs = 10.0.0.2/32 PersistentKeepalive = 25 [Peer] # relay-2 PublicKey = AllowedIPs = 10.0.0.3/32 PersistentKeepalive = 25 ``` **Relay 1** — `/etc/wireguard/wg0.conf`: ```ini [Interface] Address = 10.0.0.2/24 PrivateKey = [Peer] PublicKey = Endpoint = :51820 AllowedIPs = 10.0.0.1/32 PersistentKeepalive = 25 ``` Enable on each machine: ```bash sudo systemctl enable --now wg-quick@wg0 ``` Verify the tunnel is up: ```bash sudo wg show ping 10.0.0.1 # from relay, should reach BP ``` Update your block producer's Cardano topology to use the WireGuard addresses (`10.0.0.2`, `10.0.0.3`) instead of public IPs. The nftables ruleset in section 4 already restricts Cardano traffic to the `wg0` interface. ## 6. fail2ban fail2ban detects repeated login failures and bans the source IP. ```bash sudo apt-get install -y fail2ban sudo systemctl enable --now fail2ban ``` Create `/etc/fail2ban/jail.local`: ```ini [DEFAULT] bantime = 1h bantime.increment = true bantime.factor = 2 bantime.maxtime = 5w findtime = 10m maxretry = 3 [sshd] enabled = true port = 2222 mode = aggressive maxretry = 3 ``` ```bash sudo systemctl restart fail2ban sudo fail2ban-client status sshd # verify ``` ## 7. sysctl hardening These settings harden the kernel's network stack for a server role. Edit `/etc/sysctl.d/99-cardano.conf`: ```ini # SYN flood protection net.ipv4.tcp_syncookies = 1 # Ignore ICMP broadcast requests (smurf protection) net.ipv4.icmp_echo_ignore_broadcasts = 1 # Ignore bogus ICMP error responses net.ipv4.icmp_ignore_bogus_error_responses = 1 # Reject source-routed packets net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 # Reject ICMP redirects net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 # Reverse path filtering — drop packets that appear to come from an unexpected interface net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 # Restrict dmesg to root kernel.dmesg_restrict = 1 ``` Apply: ```bash sudo sysctl -p /etc/sysctl.d/99-cardano.conf ``` ## 8. systemd unit hardening The systemd service files in [Running cardano-node](/docs/operators/node/running-cardano) already run the node as a dedicated `cardano` user with a `RuntimeDirectory`. Add these directives to the `[Service]` section for defense-in-depth: ```ini NoNewPrivileges=yes PrivateTmp=yes ProtectHome=yes ProtectSystem=strict ReadWritePaths=/var/lib/cardano /run/cardano PrivateDevices=yes ``` Reload after editing: ```bash sudo systemctl daemon-reload && sudo systemctl restart cardano-node ``` ## Verification checklist ```bash # SSH key auth works, password auth rejected ssh -o PasswordAuthentication=yes cardano-op@ # should fail # nftables active sudo nft list ruleset # WireGuard up (if applicable) sudo wg show # fail2ban watching SSH sudo fail2ban-client status sshd # sysctl applied sysctl net.ipv4.tcp_syncookies net.ipv4.conf.all.rp_filter ``` --- ## Improve Grafana Security By default Grafana listens on port 3000 with HTTP and local password auth. This page covers three layers of improvement: restricting access via SSH tunnel, adding TLS with an Nginx reverse proxy, and optionally replacing password auth with Google OAuth. ## Option 1 — SSH tunnel (simplest, no public exposure) If only you and your team need Grafana access, the easiest hardening is to **not expose port 3000 at all** and reach it through an SSH tunnel: ```bash # On your local machine — forward localhost:3000 to the monitoring server ssh -L 3000:localhost:3000 cardano-op@ ``` Then open `http://localhost:3000` in your browser. No certificate, no domain name, no public port required. This is the recommended approach for most operators. ## Option 2 — Nginx reverse proxy with TLS If you need Grafana accessible from multiple machines or want a proper HTTPS endpoint, use Nginx as a reverse proxy with a Let’s Encrypt certificate. ## Prerequisites NGINX Reverse Proxy : - You must have a proper domain name; and FQDN set to your grafana’s public IP address - You can buy a domain name basically on any hosting site like namecheap. 2FA Google OAuth (optional — see note below) : - You need to have your own domain mail address (and of course a secure mail server). For example, you could have "grafana@yourdomain.com" - You need to create a Google account with this mail address. :::note Google OAuth requires a custom-domain email address. If you do not have one, use Grafana's built-in user management with a strong password, combined with the SSH tunnel from Option 1, instead of OAuth. ::: ## Nginx Reverse Proxy The main issue if you want to access your Grafana Dashboard from anywhere, out-of-the-box, is that you have to expose the application port (http:3000 by default) on the public address of your server. To avoid that, a popular solution is to simply create an SSH tunnel with a port forwarding option to your Grafana Server. A more elegant and still secure solution is to configure a reverse proxy, with an SSL certificate. ### Nginx installation **Install nginx** ```shell sudo apt install nginx ``` **Check nginx status** ```shell sudo systemctl status nginx ``` **Create Firewall Rules on your server** Ports 80 and 443 need to be opened on your Grafana server — 80 for Certbot’s HTTP-01 renewal challenge, 443 for HTTPS. Add them to your nftables ruleset alongside your existing SSH and Cardano ports: ```bash # Add to the input chain in /etc/nftables.conf tcp dport { 80, 443 } accept ``` Then reload: ```bash sudo nft -f /etc/nftables.conf ``` Now you should be able to visit your server's public IP address : `http://your-ip-address` which should lead to the default Nginx page. **Create and edit an nginx config file for your Grafana server** (change with your actual FQDN like grafana.yourdomain.com) ```shell cd /etc/nginx/sites-enabled sudo nano .conf ``` Add this block ```shell server { listen 80; server_name ; location / { proxy_set_header Host $http_host; proxy_pass http://localhost:3000/; } } ``` **Save your file and restart Nginx** ```shell sudo systemctl restart nginx ``` Now access your monitoring server `http://your-FQDN` : you should see the Grafana login page. **Nginx cleanup : remove the default enabled site** ```shell rm /etc/nginx/sites-enabled/default ``` ### SSL certificate installation Now that we have a working Reverse Proxy on our monitoring server, we are going to add SSL layer to encrypt properly access to your Cardano Stakepool Grafana dashboard. To do this, we are going to use a free SSL certificate provider, Let’s Encrypt, with Certbot. **Use snap to install certbot** ```shell sudo snap install core; sudo snap refresh core sudo snap install --classic certbot ``` **Make a sym link so you can use certbot command anywhere** ```shell sudo ln -s /snap/bin/certbot /usr/bin/certbot ``` **Start the installation and follow the instructions** ```shell sudo certbot --nginx ``` Respond to prompts to configure your HTTPS settings (FQDN,email..). At the end of installation, you should be able to access your Grafana Server with HTTPS : `https://your-FQDN` ### Post install nginx hardening **Block any unwanted HTTP method, except PUT POST GET and HEAD and configure websocket** ```shell sudo nano /etc/nginx/sites-enabled/ ``` Paste this line inside your "location /" block : ```shell limit_except PUT GET HEAD POST { deny all; } ``` Paste this sub-block inside the first `server {` block ```shell # Proxy Grafana Live WebSocket connections. location /api/live/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $http_host; proxy_pass http://grafana; } ``` Save and close. **Nginx configuration file hardening** ```shell sudo nano /etc/nginx/nginx.conf ``` Remove old cipher suites TLSv1.0 and TLSv1.1 : the SSL Settings should look like this : ```shell ## # SSL Settings ## ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ``` Prevent DoS and Buffer Oversized attacks : add this lines inside "http" block : ```shell ## Start: Size Limits & Buffer Overflows ## client_body_buffer_size 3K; client_header_buffer_size 3k; client_max_body_size 80k; large_client_header_buffers 2 10k; ## END: Size Limits & Buffer Overflows ## ### Directive describes the zone, in which the session states are stored i.e. store in slimits. ### ### 1m can handle 32000 sessions with 32 bytes/session, set to 5m x 32000 session ### limit_conn_zone $binary_remote_addr zone=addr:5m; ### Control maximum number of simultaneous connections for one session i.e. ### ### restricts the amount of connections from a single ip address ### limit_conn addr 10; ``` Save and close. **Restart Nginx server** ```shell sudo systemctl restart nginx ``` ## Google OAuth setup We are going to replace the local login/password access to Grafana server, by a much more robust authentication : Google OAuth. Remember : you must have your mail address on your own domain (see Pre-Requisites), and create a Google account with that address. ### Activate 2FA on your Google Account Connect to the Google account you created with your own mail address, and activate 2FA Authentication : 1- In the navigation panel, select Security. 2- Under “Signing in to Google,” select 2-Step Verification. Get started. 3- Follow the on-screen steps. ### Create Google API credentials 1- Go to https://console.developers.google.com/apis/credentials and log in with the Google account you created with your own mail address. 2- Click on "Create Credentials" on top of the page, and then click OAuth Client ID . (You may have to setup a “Consent page” first. Use defaults, it’s not very important in our case). 3- Enter these settings : - Application Type: Web Application - Name: Grafana - Authorized JavaScript Origins: `https://your-FQDN-used-to-access-your-grafana-server` - Authorized Redirect URLs: `https://your-FQDN-used-to-access-your-grafana-server/login/google` 4- Click on Create 5- You’ll get a Client ID and Client Secret. Copy them. ### Edit Grafana config file **Open grafana.ini** ```shell sudo nano /etc/grafana/grafana.ini ``` **In the Server section, find this setting and modify it** ```shell root_url = https://your-FQDN-used-to-access-your-grafana-server/ ``` **Next, go to the Google Auth section and modify these settings** ```shell [auth.google] enabled = true client_id = client_secret = scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email auth_url = https://accounts.google.com/o/oauth2/auth token_url = https://accounts.google.com/o/oauth2/token allowed_domains = allow_sign_up =false ``` Save and close the file **Restart grafana server** ```shell sudo systemctl restart grafana-server.service ``` Access to your Grafana FQDN : `https://your-FQDN-used-to-access-your-grafana-server` You should now have a “Sign-in with Google” option on the login page. You can now use the account you created with your own domain name to access your Grafana Cardano dashboards. ### Optional: Admin configuration **Give the Google Account the administrator role, and then remove the local Admin/Password account** 1- Access your Grafana UI with your local Admin account 2- Go to "Users", and make your Google Account "Admin" by changing its role 3- Log in with your Google Account, go to "Users", and remove the local Admin account. **Disable login form to allow only Google OAuth** ```shell sudo nano /etc/grafana/grafana.ini ``` In the [auth] section : ```shell disable_login_form = true ``` Save and close the file --- ## On-Chain Polls :::warning Legacy mechanism On-chain polls (CIP-0094) are superseded by the full on-chain governance introduced in the Conway era (CIP-1694). For current SPO governance — including voting on hard fork initiation — see [SPO Governance (CIP-1694)](../spo-governance). ::: In the 8.0.0 version of Cardano-node, we incorporated a new group of commands to facilitate voting among stake pool operators. An "official" poll is characterized by being endorsed with a genesis delegate key. :::important This tutorial requires cardano-node 8.0.0 https://github.com/IntersectMBO/cardano-node/releases/tag/8.0.0 ::: ## CIP-0094 - Poll participation ### Pre-requisites ​For this guide, you require a Cardano-cli that has the `governance poll` subcommands. You can use anything from the Cardano-node [release v8.0.0](https://github.com/IntersectMBO/cardano-node/releases) or a specially [backported 1.35.7 version](https://github.com/CardanoSolutions/cardano-node/releases/tag/1.35.7%2Bcip-0094). Once a Genesis Delegate Key holder has signed and posted a new poll question on the chain, it will appear in this Cardano Foundation [CIP-0094-polls repository](https://github.com/cardano-foundation/CIP-0094-polls). You can find the JSON file containing the poll question in CBOR format by navigating into the specific subfolder. For instance, the first [PreProd network Test question](https://github.com/cardano-foundation/CIP-0094-polls/tree/main/networks/preprod/d8c1b1d871a27d74fbddfa16d28ce38288411a75c5d3561bb74066bcd54689e2) appears like this: **poll.json** ```json { "type": "GovernancePoll", "description": "An on-chain poll for SPOs: How satisfied are you with the current rewards and incentives scheme?", "cborHex": "a1185ea200827840486f77207361746973666965642061726520796f752077697468207468652063757272656e74207265776172647320616e6420696e63656e74697665732073636568656d653f0183816c646973736174697366696564816a6e6f206f70696e696f6e8169736174697366696564" } ``` :::info The signature from the genesis delegate key isn't included in this metadata but is employed as an additional signatory on the initiating transaction. ::: Download this file to your node. ## Creating answer ​ From that point, you can generate a metadata entry to respond to the poll using the `governance answer-poll` command in the following way: ​ ```bash $ cardano-cli governance answer-poll --poll-file poll.json ``` ​ This command will invite an interactive response from you. If you prefer not to respond interactively, you can employ `--answer` along with the index of the answer. Executing this command will present the survey in a format easy to comprehend and will ask for your answer, as demonstrated below: ​ ``` How satisfied are you with the current rewards and incentives scheme? [0] dissatisfied [1] no opinion [2] satisfied ​ Please indicate an answer (by index): _ ``` You can move forward by entering one of the possible answer indices (in this case, `0`, `1`, or `2`) followed by a newline. This will generate witnessed metadata in the form of a detailed JSON schema, which should be subsequently posted on-chain in any transaction and **signed with your stake pool's cold key**: ideally, this is achieved by constructing a basic transaction directed to yourself that carries the metadata. Here is a sample of metadata where the answer `2` is selected: **answer.json** ```json { "94": { "map": [ { "k": { "int": 2 }, "v": { "bytes": "62c6be72bdf0b5b16e37e4f55cf87e46bd1281ee358b25b8006358bf25e71798" } }, { "k": { "int": 3 }, "v": { "int": 2 } } ] } } ``` ## Publishing answer ​ From this point, you can utilize the `transaction build` command to generate a transaction for posting on-chain. You will require a signing key linked to a UTxO possessing sufficient funds to facilitate the transaction (approximately 0.2 Ada if you're making a basic transaction to yourself). Assuming you have stored the metadata generated from the previous step in a file named `answer.json`, the command to construct the transaction would appear as follows: ``` $ cardano-cli conway transaction build \ --babbage-era \ --cardano-mode \ --mainnet \ --tx-in $TXID#$IX \ --change-address $ADDRESS \ --metadata-json-file answer.json \ --json-metadata-detailed-schema \ --required-signer-hash $POOL_ID \ --out-file answer.tx ``` :::caution Please be aware that adding `--required-signer-hash` is crucial for the response to be considered valid for the survey; this serves as your identification as a stake pool operator. ::: You can produce the `$POOL_ID` hash from the Bech32 formatted pool ID using the Bech32 command: ​ ``` $ bech32 <<< pool1.... ``` To submit the response to the chain, you need to provide the respective values for `--tx-in` & `--change-address` from one of your wallets. From this point, you can sign `answer.tx` using your stake pool's cold key and any necessary payment key, then submit the result as usual. If everything proceeds correctly, the cardano-cli should present a transaction id that you can monitor on-chain to confirm your survey response was correctly published. SPO-Poll Dashboards where your transaction should now be visible: - Cardanoscan.io [[PreProd](https://preprod.cardanoscan.io/spo-polls/)] [[Mainnet](https://cardanoscan.io/spo-polls/)] ​ ## Verifying Answers ​ Lastly, you can validate answers observed on-chain using the `governance verify-poll` command. The term 'verify' here has a dual meaning: - It verifies that an answer is valid in the context of a specific survey - It provides a list of the signatory key hashes found in the transaction; in the case of a valid submission, one key hash will correspond to a recognized stake pool id. Assuming you still have the original `poll.json` file, and a signed transaction carrying a survey answer as `answer.signed`, you can confirm its validity using: ​ ``` $ cardano-cli governance verify-poll \ --poll-file poll.json \ --tx-file answer.signed ``` ​ Upon successful execution, this should produce something like: ​ ``` Found valid poll answer, signed by: [ "f8db28823f8ebd01a2d9e24efb2f0d18e387665770274513e370b5d5" ] ``` ​ Alternatively, the command will identify a problem with the answer and/or poll. ## References - [Entering Voltaire: on-chain poll for SPOs](https://forum.cardano.org/t/entering-voltaire-on-chain-poll-for-spos/117330?u=adatainment) - [Cardano Node 8.0.0 release](https://github.com/IntersectMBO/cardano-node/releases/tag/8.0.0) - [Cardano Node documentation: Governance](https://github.com/input-output-hk/cardano-node-wiki/wiki/cardano-governance) --- ## SPO Governance Cardano's Conway era introduced decentralized on-chain governance via [CIP-1694](https://cips.cardano.org/cip/CIP-1694). Three governance bodies share decision-making power: the **Constitutional Committee (CC)**, **Delegated Representatives (DReps)**, and **Stake Pool Operators (SPOs)**. Each body votes on a different subset of governance actions. ## What SPOs vote on SPOs vote with their **cold verification key** and require **>51% of active stake** to ratify an action (unless noted). | Governance action | SPO threshold | Notes | |---|---|---| | Motion of no-confidence | 51% | Removes the current CC | | Update committee / threshold | 51% | Adds, removes, or reweights CC members | | Hard-fork initiation | 51% | Triggers a protocol upgrade | | Protocol parameter changes relevant to security:maxBlockBodySizemaxTxSizemaxBlockHeaderSizemaxValueSizemaxBlockExecutionUnitstxFeePerBytetxFeeFixedutxoCostPerBytegovActionDepositminFeeRefScriptCostPerByte | 51% | Changes a protocol parameter | | Info | 100% | Advisory only — no on-chain effect | SPOs **do not** vote on treasury withdrawals or constitutional amendments; those require DRep and CC approval. They also do not vote on most protocol-parameter changes, with one exception: the security-relevant parameters (block and transaction sizes, `maxBlockExecutionUnits`, fee parameters, `utxoCostPerByte`, `govActionDeposit`, and similar) need an additional SPO vote to change. Find out more about the different roles at this dedicated [Governance Actions insight page](https://cardano.org/insights/governance-actions/). Hard-fork initiation is the most common action requiring SPO votes. When the community is ready to upgrade the network, a hard fork proposal is submitted on-chain and SPOs cast an explicit on-chain vote (Yes, No, or Abstain) with their cold key; a Yes signals readiness. Running the upgraded node software is also required, but does not substitute for the on-chain vote. ## Step 1 — Find active proposals Query your node for all proposals currently eligible for ratification: ```shell cardano-cli conway query proposals --all-proposals ``` To filter for only hard-fork proposals: ```shell cardano-cli conway query proposals --all-proposals \ | jq '[.[] | select(.proposalProcedure.govAction.tag == "HardForkInitiation")]' ``` For a full governance state dump (includes vote tallies): ```shell cardano-cli conway query gov-state ``` You can also browse active proposals on [Cardano GovTool](https://gov.tools), [CardanoScan](https://cardanoscan.io/govActions), [Adastat](https://adastat.net/governances) or [CGOV](https://app.cgov.io/). ## Step 2 — Review the proposal Every governance action must include an **anchor** — a URL pointing to a document describing the rationale, and a hash of that document. Verify the content before voting: ```shell # Get the anchor URL and hash from the proposal cardano-cli conway query proposals --all-proposals \ | jq '.[] | {id: .actionId, url: .proposalProcedure.anchor.url, hash: .proposalProcedure.anchor.dataHash}' # Download the document and verify its hash wget -O proposal.jsonld b2sum -l 256 proposal.jsonld # Hash must match the dataHash in the proposal ``` ## Cold key security :::danger Your cold key must never touch an internet-connected machine Your pool's cold signing key (`cold.skey`) is the most sensitive credential you hold. If it is ever on a live system — even briefly — your pool is at risk. There are no exceptions. ::: Keys must be: - **Kept off live systems at all times.** Build transactions online, sign them offline, submit the signed result. - **Encrypted at rest.** Store your cold key on an encrypted data volume (LUKS on Linux, or an encrypted container). A plaintext key on even an offline drive is a single point of failure. - **Backed up in at least two independent encrypted locations.** ### Recommended: cardano-airgap [cardano-airgap](/docs/operators/security/air-gap) is a Nix-built bootable ISO maintained by IntersectMBO. It ships pre-loaded with all Cardano tooling and has **never made a network request** — not during build, not during setup, not ever. It is already the tool of choice for many SPOs and Constitutional Committee members. Alternatives: the [Frankenwallet](/docs/operators/security/air-gap) (encrypted bootable USB) or a [manually configured air-gapped machine](/docs/operators/security/air-gap). ### Signing workflow 1. **Online** — build the unsigned transaction (`vote-tx.raw`) 2. Transfer `vote-tx.raw` to the air-gapped machine via USB 3. **Air-gapped** — sign the transaction (see below) 4. Transfer only `vote-tx.signed` back to the online machine 5. **Online** — submit ## Step 3 — Cast your vote You will need: - Your pool's cold verification key (`cold.vkey`) to create the vote - Your pool's cold signing key (`cold.skey`) to sign the transaction (on the air-gapped machine) - A funded payment key to cover the transaction fee (~0.2 ADA) **Create the vote file** (can be done online, uses only the public `cold.vkey`): ```shell cardano-cli conway governance vote create \ --yes \ --governance-action-tx-id "" \ --governance-action-index 0 \ --cold-verification-key-file cold.vkey \ --out-file spo.vote ``` Replace `--yes` with `--no` or `--abstain` as appropriate. **Build the unsigned transaction** (online): ```shell cardano-cli conway transaction build \ --tx-in "$(cardano-cli query utxo --address "$(< payment.addr)" --output-json | jq -r 'keys[0]')" \ --change-address "$(< payment.addr)" \ --vote-file spo.vote \ --witness-override 2 \ --out-file vote-tx.raw ``` **Sign on the air-gapped machine** (cold key never leaves the air gap): ```shell cardano-cli conway transaction sign \ --tx-body-file vote-tx.raw \ --signing-key-file cold.skey \ --signing-key-file payment.skey \ --out-file vote-tx.signed ``` **Submit** (back online): ```shell cardano-cli conway transaction submit --tx-file vote-tx.signed ``` ## Step 4 — Verify your vote After submission, confirm your vote was recorded by querying the proposal: ```shell cardano-cli conway query proposals \ --governance-action-tx-id "" \ --governance-action-index 0 \ | jq '.[0].stakePoolVotes' ``` Your pool ID should appear in the `stakePoolVotes` object with your chosen vote. ## Opting out — delegating to alwaysAbstain By default, an SPO who does not vote on a proposal has their stake counted against ratification. Because the ratification threshold requires more than 51% of active stake to vote **yes**, non-participating SPOs drag the effective participation rate down — the same practical effect as voting no. If you do not intend to follow governance closely, you can change this behaviour by delegating your reward account to the `alwaysAbstain` DRep. This removes your stake from both the numerator and denominator of the ratification calculation, turning your non-participation into a genuine abstain rather than an implicit no. :::warning Hard-fork votes still require an explicit on-chain vote Hard-fork initiation requires SPOs to cast an explicit on-chain vote (Yes, No, or Abstain) with their cold key; the `alwaysAbstain` delegation does not cover it. If you delegate to `alwaysAbstain` and do not vote on a hard-fork proposal, your stake counts as a no vote on that action. ::: This delegation must be made with the **reward account stake key** — the key registered as `--pool-reward-account-verification-key-file` in your pool registration certificate. Delegating owner stake keys changes the DRep delegation for the stake associated with your pledge, but has no effect on the pool's governance default behaviour. Create the DRep delegation certificate using the reward account stake key (uses only the public key, so this can be done online): ```bash cardano-cli conway stake-address vote-delegation-certificate \ --stake-verification-key-file reward-stake.vkey \ --drep-always-abstain \ --out-file drep-abstain.cert ``` Build and submit a transaction including the certificate: ```bash # Online — build cardano-cli conway transaction build \ --tx-in "$(cardano-cli query utxo --address "$(< payment.addr)" --output-json | jq -r 'keys[0]')" \ --change-address "$(< payment.addr)" \ --certificate-file drep-abstain.cert \ --witness-override 2 \ --out-file drep-tx.raw # Air-gapped — sign with the reward account stake key and payment key cardano-cli conway transaction sign \ --tx-body-file drep-tx.raw \ --signing-key-file reward-stake.skey \ --signing-key-file payment.skey \ --out-file drep-tx.signed # Online — submit cardano-cli conway transaction submit --tx-file drep-tx.signed ``` To reverse the decision, submit a new delegation certificate to a specific DRep or remove the delegation entirely. ## Proving your identity to governance tools Many governance platforms, voting interfaces, and SPO-aware services need to verify that you are the operator of a given pool before letting you take action. Rather than asking you to sign with your cold key, they use **Calidus keys** — on-chain registered hot keys that act on behalf of your pool. Register a Calidus key once (using a cold-key signature from your air-gapped machine), then use it freely as a hot key for governance tools, explorer profiles, and dApp interactions — without the cold key ever leaving the air gap. See [Calidus Keys](../../operator-tools/calidus-keys) for setup instructions. ## Key points - **One vote per proposal per pool.** Submitting a second vote overwrites the first. - **Votes expire with the proposal.** Proposals expire after a set number of epochs if the ratification threshold is not met. - **No vote = implicit no.** Non-participating stake is excluded from the yes count but included in the total, which drags the ratification rate down. Delegate to `alwaysAbstain` to opt out genuinely. - **Cold key security.** Your cold key is your pool's most sensitive credential. Never expose it on an internet-connected machine. ## Further reading - [Submitting votes (cardano-cli full reference)](/docs/developers/curriculum/staking-governance/vote-and-propose#vote-on-an-action) - [Governance queries](/docs/developers/curriculum/staking-governance/governance-operations#query-governance-state) - [CIP-1694 specification](https://cips.cardano.org/cip/CIP-1694) - [Cardano GovTool](https://gov.tools) --- ## Monitoring with openBlockPerf ## Why global monitoring? Cardano was built on a scientific foundation, developed by engineering teams, and is now operated by stake pool operators as a global decentralized network. The Cardano Blockchain infrastructure is available to applications and is governed by Drep's. The telemetry data collected at the operational level is available to all these layers and functions for further development. multiple Layers benefiting from openBlockPerf telemetry data ## What does it observe? OpenBlockPerf analyzes how quickly newly minted Cardano blocks propagate through the global peer-to-peer network. Instead of measuring only the final adoption time, it observes four distinct propagation stages in millisecond precision: - when a relay node first receives a new block header - how long it takes to request the block body - how long the block body download takes - when the local node validates and adopts the block block propagation perception from multiple nodes in a gantt diagramm These measurements reflect real-world factors such as geographic distance, latency, peering topology, bandwidth, hardware performance, and block size. It also records the relay node’s peering connections, its node version, and other data to provide the best possible context for the measured propagation times. openBlockPerf on relay nodes has no access to or influence over stake pool credentials or other operational security-related data. The operator has full insight into the code and full control over what is submitted to the global blockperf database. Read the full explanation here: [openBlockPerf Documentation](https://github.com/cardano-foundation/openblockperf/blob/main/docs/blockperf-client.md#what-the-client-reports) ## What happens to the submitted data? Telemetry data is collected in the Blockperf backend. IP addresses are geolocated and mapped to ISP/ASN networks. In addition, public on-chain data from stake pools (active stake, pledge, relays, etc.) is combined to make the data as analyzable as possible. ## How is this data published? openBlockPerf itself focuses entirely on data collection and deliberately refrains from creating its own exclusive presentations, which would always contain a certain interpretation and opinion. The collected data is generally made available to everyone as raw data - sufficiently anonymized - so that everyone can conduct his own analyses and form own opinions. Actively participating stake pool operators receive, in return, performance data exclusively related to their own relays, which they can integrate into their own monitoring systems. ## How can I participate? Stake pool operators can obtain an openBlockPerf API key using a Calidus proof, which they can use simultaneously on all their relays. Anyone else who does not have a stake pool (Calidus key) can register their node using its public IP and receive a valid API key for it. The API key should not be understood as a permissioned system, but rather as a way to uniquely identify or even filter submitted data. ## openBlockPerf open source repository [https://github.com/cardano-foundation/openblockperf](https://github.com/cardano-foundation/openblockperf) --- ## Monitoring Overview Running a stake pool means being on-call for a live system. A node that silently falls behind the chain, misses its slot, or has expiring KES keys will cost you and your delegators rewards. Good monitoring catches these problems before they become expensive. ## What to monitor | Metric | Why it matters | |--------|---------------| | Sync progress / slot height | A node that has fallen behind will not mint blocks | | Block production | Are you winning and minting your assigned slots? | | KES key expiry | Node stops forging when KES expires (~90 days on mainnet) | | Memory and CPU | Sustained high usage is often a warning sign | | Disk space | The chain database grows continuously — running out kills the node | | Process liveness | Is the node process actually running? | | Peer connections | Too few hot peers degrades block propagation | | Block propagation | Blocks must reach a large portion of the network in a timely manner | | Unexpected errors | It is impossible to anticipate every possible error. But keep an eye out for an unusually high number of errors. | ## Real-time CLI monitoring — gLiveView [gLiveView](https://cardano-community.github.io/guild-operators/Scripts/gliveview/) is a bash script from the [Guild Operators](https://cardano-community.github.io/guild-operators/) community that gives you a live terminal dashboard of your node's current state. It connects to the node's local metrics endpoint, detects whether the node is a relay or block producer, and adjusts its output accordingly. ![gLiveView dashboard showing node metrics, peer connections, and block production status](./img/guild_gliveview.png) **gLiveView is good for:** - Quickly checking node health without leaving the command line - Seeing live peer connection counts (hot/warm/cold) - Watching block production in real time during an epoch **gLiveView does not provide:** - Alerting — there is no way to be notified when something goes wrong - Historical data — you can only see the current moment - Multi-node views — one terminal per node For installation and configuration, see the [Guild Operators gLiveView guide](https://cardano-community.github.io/guild-operators/Scripts/gliveview/). gLiveView is part of the Guild Operators script suite and maintained by that community. ## Full observability stack — Prometheus, Grafana, and Alertmanager For production monitoring you need metrics persistence, dashboards, and alerting. The standard stack is: - **[cardano-tracer](https://github.com/IntersectMBO/cardano-node/tree/master/cardano-tracer)** — the node forwards traces to `cardano-tracer` over a local socket; the tracer exposes a Prometheus scrape endpoint - **[Prometheus](https://prometheus.io)** — scrapes and stores time-series metrics - **[Grafana](https://grafana.com)** — dashboards and visualisation - **[Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/)** (optional but recommended) — routes alerts to email, PagerDuty, Slack, etc. This stack persists locally observable metrics over time, lets you set alert thresholds (KES expiry window, peer count floor, disk usage ceiling), and gives you historical views to diagnose incidents after the fact. See [Prometheus and Grafana setup](/docs/operators/monitoring/monitoring-prometheus-grafana) for the full guide. For a deep dive into tracing configuration — backends, namespaces, severity filters, and `cardano-tracer` options — see the [New Tracing System reference](/docs/operators/monitoring/new-tracing-system/new-tracing-system). ## Global Network Monitoring - openBlockPerf In a global, decentralized environment like the Cardano blockchain, in addition to monitoring your own local resources, you can also measure and record what you see and receive from the rest of the network—that is, all other stake pools and relays. When these views are consolidated into a joint database, they provide a unique insight into the dynamics, performance, and potential risks within the dynamic network managed by so many different operators. [openBlockPerf](https://github.com/cardano-foundation/openblockperf/blob/main/README.md) is a project that addresses precisely this aspect of monitoring. Any stake pool operator can participate using one or more of their relay nodes, allowing the tool to track the propagation times of all blocks generated by other pools. In return, the operator receives metrics showing how their own blocks were experienced by all other participants. This data, collected over extended periods from various nodes and across protocol updates, is then useful for - Research validation - Engineering paths - Operational monitoring - Application design - Governance decisions ## Which to use | | gLiveView | Prometheus + Grafana | openBlockPerf | |--|:---------:|:-------------------:|:--------------:| | Real-time node status | ✓ | ✓ | — | | Historical data | — | ✓ | ✓ | | Alerting | — | ✓ | — | | Multi-node dashboards | — | ✓ | ✓ | | No extra services required | ✓ | — | — | | Works over SSH | ✓ | ✓ (with tunnel) | — | | External perspectives | — | — | ✓ | Most operators run both: gLiveView for quick manual checks when SSH'd into a node, and Prometheus/Grafana for persistent monitoring and alerting. openBlockperf is operated by some Operators since 2023 and open for any voluntary participation. Ideally on the stake pools relay nodes. --- ## Monitoring with Prometheus and Grafana This guide sets up end-to-end monitoring for a Cardano stake pool using the new tracing system (Node 10.2+): nodes forward metrics to `cardano-tracer`, which exposes a Prometheus endpoint, which Grafana visualizes. :::note This guide covers **Node 10.2 and later**, which uses the new tracing system by default. The legacy tracing system (EKG/Prometheus directly from the node on port 12798) is no longer supported. ::: ## Architecture ```mermaid flowchart LR R1["relay-1"] -->|"Unix socket over SSH tunnel"| TRACER R2["relay-2"] --> TRACER BP["block-producer"] --> TRACER TRACER["cardano-tracerexposes /metrics per node"] --> PROM["Prometheus"] --> GRAF["Grafana"] ``` `cardano-tracer` acts as an aggregator: one process collects traces and metrics from all your nodes, and exposes them via a single Prometheus HTTP endpoint. Prometheus scrapes that endpoint, and Grafana queries Prometheus. ## Prerequisites - Cardano nodes running Node 10.2+ (new tracing system enabled by default) - A monitoring machine (can be one of your relays, or a dedicated host) - `cardano-tracer` binary — build it alongside `cardano-node`: ```shell # Cabal cabal build cardano-tracer && cabal install cardano-tracer --installdir=$HOME/.local/bin --overwrite-policy=always # Nix nix build github:IntersectMBO/cardano-node#cardano-tracer cp result/bin/cardano-tracer $HOME/.local/bin/ ``` ## Step 1 — Configure each node Edit your node's `config.json` to enable the `Forwarder` and `EKGBackend` backends, and set a node name: ```json { "UseTraceDispatcher": true, "TraceOptionNodeName": "relay-1", "TraceOptions": { "": { "severity": "Notice", "detail": "DNormal", "backends": [ "EKGBackend", "Forwarder" ] } } } ``` Set `TraceOptionNodeName` to a unique, descriptive name for each node (`relay-1`, `relay-2`, `block-producer`, etc.). This becomes the path component in the Prometheus endpoint URL and the `node_name` label in Prometheus. Then add the tracer socket flag to your node's startup command: ```shell cardano-node run \ ... \ --tracer-socket-path-connect /run/cardano/tracer.sock ``` :::caution Enable `Forwarder` only when `cardano-tracer` is running and reachable. If traces accumulate without being consumed, the node buffers them in RAM. The buffer is bounded, but sustained disconnection will increase memory usage. ::: ## Step 2 — Configure cardano-tracer On your **monitoring machine**, create `/etc/cardano/tracer-config.json`: ```json { "networkMagic": 764824073, "network": { "tag": "AcceptAt", "contents": "/run/cardano/tracer.sock" }, "logging": [ { "logRoot": "/var/log/cardano-tracer", "logMode": "FileMode", "logFormat": "ForMachine" } ], "rotation": { "rpFrequencySecs": 3600, "rpKeepFilesNum": 14, "rpLogLimitBytes": 104857600, "rpMaxAgeHours": 24 }, "hasPrometheus": { "epHost": "127.0.0.1", "epPort": 12789 } } ``` Replace `764824073` with your network's magic (mainnet shown). For other networks, find it in your `shelley-genesis.json`: ```shell jq .networkMagic /path/to/shelley-genesis.json ``` Run `cardano-tracer`: ```shell cardano-tracer --config /etc/cardano/tracer-config.json ``` Verify it is listening: ```shell curl -s http://127.0.0.1:12789/ # Should list connected node names as hyperlinks ``` ## Step 3 — Connect nodes to the tracer via SSH tunnels For **same-machine** setups (tracer and node on the same host), no tunnel is needed — the socket path is shared directly. For **remote nodes**, forward the tracer's socket over SSH from each node's machine. Run this on each node host, replacing the IP with your monitoring machine's address: ```shell ssh -nNT \ -L /run/cardano/tracer.sock:/run/cardano/tracer.sock \ -o "ExitOnForwardFailure yes" \ monitoring@ ``` :::tip Start the SSH tunnel **before** starting the node. The node connects to the socket at startup; if the socket does not exist yet, it will fail to connect to the tracer. ::: Add the SSH tunnel as a systemd service or include it in your node startup script so it reconnects automatically on restart. ## Step 4 — Install prometheus-node-exporter on each node `prometheus-node-exporter` provides host-level metrics (CPU, memory, disk, network) that complement the Cardano application metrics from `cardano-tracer`. ```shell # Debian / Ubuntu sudo apt-get install -y prometheus-node-exporter sudo systemctl enable --now prometheus-node-exporter # Allow Prometheus to scrape it (replace with your monitoring machine IP) sudo ufw allow proto tcp from to any port 9100 ``` ## Step 5 — Install and configure Prometheus On the **monitoring machine**: ```shell sudo apt-get install -y prometheus ``` Replace the contents of `/etc/prometheus/prometheus.yml`: ```yaml global: scrape_interval: 15s external_labels: environment: mainnet # matches the $environment variable in the Grafana dashboard scrape_configs: # Cardano application metrics — via cardano-tracer HTTP service discovery # Automatically discovers all connected nodes; no config change needed when adding nodes - job_name: cardano-node http_sd_configs: - url: http://127.0.0.1:12789/targets # Host metrics from each node machine - job_name: node-exporter static_configs: - targets: - relay-1:9100 - relay-2:9100 - block-producer:9100 ``` The `environment` external label is picked up by the Grafana dashboard's `$environment` variable. Restart Prometheus and verify it is scraping: ```shell sudo systemctl restart prometheus # Open http://localhost:9090/targets — all cardano-node targets should show "UP" ``` ## Step 6 — Install Grafana ```shell sudo apt-get install -y apt-transport-https software-properties-common wget -q -O - https://apt.grafana.com/gpg.key | sudo apt-key add - echo "deb https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list sudo apt-get update && sudo apt-get install -y grafana sudo systemctl enable --now grafana-server ``` Open `http://:3000` and log in (default: `admin` / `admin` — **change this immediately**). Add Prometheus as a data source: 1. **Configuration → Data sources → Add data source → Prometheus** 2. Set URL to `http://localhost:9090` 3. Click **Save & test** Note the data source name you chose (e.g. `Prometheus`). You will need it when importing the dashboard. For hardening Grafana (HTTPS, disabling public registration), see [Improve Grafana Security](../../deployment-scenarios/improve-grafana-security). ## Step 7 — Import the dashboard Download the dashboard JSON: **[cardano-node-application-metrics.json](/grafana/cardano-node-application-metrics.json)** In Grafana: **Dashboards → Import → Upload JSON file**, select the downloaded file, then click **Import**. **Important — set the data source**: The dashboard panels reference a datasource named `mimir` (the name used in the IOG internal setup). Grafana will prompt you to map it during import — select your Prometheus datasource from the dropdown. After import, use the **Environment** and **Instance** dropdowns at the top of the dashboard to select your network and nodes. ### Dashboard panels | Row | What it shows | |---|---| | Blocks, Slots, Epochs, and Quality | Chain tip, slot height, density, forks, block replay, late blocks | | Forging | Leader slots, blocks forged, KES periods remaining, missed slots | | Mempool and Transactions | Mempool size, tx submission rates, rejection rate | | CPU, Memory, Disk, Info | GC, memory residency, host CPU/memory, node build info | ## Troubleshooting **Panels show "No data"** - Confirm `cardano-tracer` is running: `curl http://127.0.0.1:12789/` - Confirm your node is connected: the tracer index page should list your node name - Confirm Prometheus is scraping: check `http://localhost:9090/targets` - Check metric names — some names changed between the old and new tracing systems. See the [metrics migration guide](/docs/operators/monitoring/new-tracing-system/metrics-migration) for the full rename table. **Tracer shows no connected nodes** - Check the socket path matches between node CLI (`--tracer-socket-path-connect`) and tracer config (`contents`) - If using SSH tunnels, confirm the tunnel is up before the node starts - Check firewall: the socket path must be accessible to both processes (same host or via tunnel) **Node memory keeps growing** - The `Forwarder` backend is enabled but `cardano-tracer` is not consuming traces - Check the tracer is running and the socket connection is established ## Further reading - [New Tracing System quick start](/docs/operators/monitoring/new-tracing-system/new-tracing-system) - [cardano-tracer reference](/docs/operators/monitoring/new-tracing-system/cardano-tracer) - [Metrics migration guide](/docs/operators/monitoring/new-tracing-system/metrics-migration) - [Improve Grafana Security](../../deployment-scenarios/improve-grafana-security) --- ## Cardano Tracer `cardano-tracer` is a service for logging and monitoring over Cardano nodes. After it is connected to the node, it periodically asks the node for different information, receives it, and handles it. ## Contents 1. [Introduction](#introduction) 1. [Motivation](#motivation) 1. [Overview](#overview) 1. [Build and run](#build-and-run) 1. [Configuration](#configuration) 1. [Settings in Cardano Node configuration file](#settings-in-cardano-node-configjson-file) 1. [Distributed Scenario](#distributed-scenario) 1. [Local Scenario](#local-scenario) 1. [Network Magic](#network-magic) 1. [Requests](#requests) 1. [Logging](#logging) 1. [Logs Rotation](#logs-rotation) 1. [Prometheus](#prometheus) 1. [Prometheus HTTP Service Discovery](#prometheus-http-service-discovery) 1. [EKG Monitoring](#ekg-monitoring) 1. [Verbosity](#verbosity) ## Introduction ### Motivation Previously, the node handled all the logging by itself. It provides two web-servers for application monitoring: Prometheus and EKG. `cardano-tracer` is the result of _moving_ all the logging and monitoring-related components from the node to a separate service. As a result, the node becomes smaller, faster, and simpler once the current system is deprecated. ### Overview You can think of Cardano node as a **producer** of logging and monitoring information, and `cardano-tracer` as a **consumer** of this information. After a network connection between them is established, `cardano-tracer` periodically asks for such information, and the node replies with it. There are 3 such kinds of information: 1. **Trace object**, contains logging data. `cardano-tracer` periodically queries for new trace objects, receives them and stores them in the log files and/or in Linux `systemd`'s journal. 2. **EKG metric**, contains system metrics. [Consult the EKG documentation](https://hackage.haskell.org/package/ekg-core) for more info. `cardano-tracer` periodically queries for new EKG metrics, receives and displays them using monitoring tools. 3. **Data point**, contains arbitrary information about the node. `cardano-tracer` does not poll periodically for new data points, only by _explicit_ request when it needs it. `cardano-tracer` can work as an aggregator as well: _one_ `cardano-tracer` process can receive the information from _multiple_ nodes. ## Build and run For how to build `cardano-tracer`, refer to the [New Tracing System quick start](/docs/operators/monitoring/new-tracing-system/new-tracing-system). ## Configuration The way how to configure `cardano-tracer` depends on your requirements. There are two basic scenarios: 1. **Distributed** (real-life) scenario, when `cardano-tracer` is working on one machine, and your nodes are working on another machine(s). 2. **Local** (testing) scenario, when `cardano-tracer` and your nodes are working on the same machine. Distributed scenario is for real-life case. You may have `N` nodes, running on `N` different hosts, and you want to collect all the logging and monitoring information from these nodes using one `cardano-tracer` process working on your machine. Local scenario is best for testing and debugging. For example, you want to try your new infrastructure from scratch so you run `N` nodes and one `cardano-tracer` process on your machine. ### Settings in Cardano Node config.json file Backends can be a combination of `Forwarder`, `EKGBackend`, `PrometheusSimple <...>`, and one of `Stdout MachineFormat`, `Stdout HumanFormatColoured` and `Stdout HumanFormatUncoloured`. Tracing options that can be given based on a namespace are `severity`, `detail` and `maxFrequency`. ```json { "UseTraceDispatcher": true, "TraceOptions": { "": { "severity": "Notice", "detail": "DNormal", "backends": [ "Stdout MachineFormat", "EKGBackend", "Forwarder" ] }, "ChainDB": { "severity": "Info", "detail": "DDetailed" }, "ChainDB.AddBlockEvent.AddedBlockToQueue": { "maxFrequency": 2.0 } }, } ``` For further node-side configuration explanations, refer to the [New Tracing System quick start](/docs/operators/monitoring/new-tracing-system/new-tracing-system). ### Distributed Scenario This is an example with 3 nodes and one `cardano-tracer`: ```mermaid flowchart TD subgraph MA["machine A"] N1["node 1"] end subgraph MB["machine B"] N2["node 2"] end subgraph MC["machine C"] N3["node 3"] end subgraph MD["machine D"] T["cardano-tracer"] end N1 --> T N2 --> T N3 --> T ``` The minimalistic configuration file for `cardano-tracer` would be: ``` { "networkMagic": 764824073, "network": { "tag": "AcceptAt", "contents": "/tmp/forwarder.sock" }, "logging": [ { "logRoot": "/tmp/cardano-tracer-logs", "logMode": "FileMode", "logFormat": "ForMachine" } ] } ``` The `network` field specifies the way how `cardano-tracer` will be connected to your nodes. Here you see `AcceptAt` tag, which means that `cardano-tracer` works as a server: it _accepts_ network connections by listening the local socket `/tmp/forwarder.sock`. Your nodes work as clients: they _initiate_ network connections using their local sockets. It can be shown like this: ```mermaid flowchart TD subgraph MA["machine A"] N1["node 1"] --> S1["/tmp/forwarder.sock"] end subgraph MB["machine B"] N2["node 2"] --> S2["/tmp/forwarder.sock"] end subgraph MC["machine C"] N3["node 3"] --> S3["/tmp/forwarder.sock"] end subgraph MD["machine D"] T["cardano-tracer"] --> SD["/tmp/forwarder.sock"] end ``` To establish the real network connections between your machines, you need SSH forwarding: ```mermaid flowchart TD subgraph MA["machine A"] N1["node 1"] --> S1["/tmp/forwarder.sock"] end subgraph MB["machine B"] N2["node 2"] --> S2["/tmp/forwarder.sock"] end subgraph MC["machine C"] N3["node 3"] --> S3["/tmp/forwarder.sock"] end subgraph MD["machine D"] T["cardano-tracer"] --> SD["/tmp/forwarder.sock"] end S1 -->|"SSH"| SD S2 -->|"SSH"| SD S3 -->|"SSH"| SD ``` The idea of SSH forwarding is simple: we do not connect directly to the process but to their network endpoints instead. You can think of it as a network channel from the local socket on one machine to the local socket on another machine: ```mermaid flowchart LR subgraph MA["machine A"] N1["node 1"] --> S1["/tmp/forwarder.sock"] end subgraph MD["machine D"] SD["/tmp/forwarder.sock"] T["cardano-tracer"] --> SD end S1 <-->|"SSH channel"| SD ``` Neither your nodes nor `cardano-tracer` know anything SSH, they only know about their local sockets. Using SSH forwarding mechanism they work together between machines. Since you already have your SSH credentials the connection between your nodes and `cardano-tracer` will be secure. Path `/tmp/forwarder.sock` is just an example. You can use any other name in any other directory where you have read/write permissions. To connect `cardano-node` working on machine `A` with `cardano-tracer` working on machine `D`, run this command on machine `A`: ``` ssh -nNT -L /tmp/forwarder.sock:/tmp/forwarder.sock -o "ExitOnForwardFailure yes" john@109.75.33.121 ``` where: - `/tmp/forwarder.sock` is a path to the local socket on machine `A` _and_ a path to the local socket on machine `D`, - `john` is a user name you use to login on machine `D`, - `109.75.33.121` is an IP-adress of machine `D`. :::tip Important Make sure you run `ssh`-command **before** you start your node. Since `ssh` creates the channel and `cardano-node` uses that channel, you should _create_ it before _using_ it. ::: Now run the same command on machines `B` and `C` to connect corresponding nodes with the same `cardano-tracer` working on machine `D`. Nodes working on machines `A`, `B` and `C` should specify paths `/tmp/forwarder.sock` using node's CLI-parameter `--tracer-socket-path-connect` or `--tracer-socket-path-accept` (see explanation below). There is another CLI-parameter `--socket-path` as well, but it's **not** related to `cardano-tracer`. ### Local Scenario As was mentioned above, local scenario is for testing, when your nodes and `cardano-tracer` reside on the same machine. In this case all processes can see the same local sockets so we don't need `ssh`. The configuration file for 3 local nodes would look like this (same as before): ``` { "networkMagic": 764824073, "network": { "tag": "AcceptAt", "contents": "/tmp/forwarder.sock" }, "logging": [ { "logRoot": "/tmp/cardano-tracer-logs", "logMode": "FileMode", "logFormat": "ForMachine" } ] } ``` `cardano-tracer` works as a server: it _accepts_ network connections by listening the local socket `/tmp/forwarder.sock`. Your local nodes work as clients: they _initiate_ network connections using the _same_ local socket `/tmp/forwarder.sock`. There is another way to connect `cardano-tracer` to your nodes: the `cardano-tracer` can work as _initiator_, this is an example of configuration file: ``` { "networkMagic": 764824073, "network": { "tag": "ConnectTo", "contents": [ "/tmp/cardano-node-1.sock" "/tmp/cardano-node-2.sock" "/tmp/cardano-node-3.sock" ] }, "logging": [ { "logRoot": "/tmp/cardano-tracer-logs", "logMode": "FileMode", "logFormat": "ForMachine" } ] } ``` As you see, the tag in `network` field is `ConnectTo` now, which means that `cardano-tracer` works as a client: it _establishes_ network connections with your local nodes via the local sockets `/tmp/cardano-node-*.sock`. In this case each socket is used by a particular node. It is **highly recommended** to use `AcceptAt` for easier maintainance. Use `ConnectTo` only if you really need it. `AcceptTo` and `ConnectTo` are mirrored by the reciprocal option on the node `--tracer-socket-path-connect` / `--tracer-socket-path-accept`. If you choose one on the node, you choose the opposite on the tracer. This only makes a difference to which entity initiates the handshake; after the handshake the configuration is identical. Suppose you have 3 working nodes, and they are connected to the same `cardano-tracer`. And then you want to connect a 4th node to it. If `cardano-tracer` is configured using `AcceptAt`, you don't need to change its configuration - you just connect the additional node to it. But if `cardano-tracer` is configured using `ConnectTo`, you'll need to add a 4th socket path to its configuration file and restart the `cardano-tracer` process. ### Network Magic The field `networkMagic` specifies the value of network magic. It is an integer constant from the genesis file, the node uses this value for the network handshake with peers. Since `cardano-tracer` should be connected to the node, it needs that network magic. The value from the example above, `764824073`, is taken from the Shelley genesis file for [Mainnet](https://book.world.dev.cardano.org/environments.html). Take this value from the genesis file your nodes are launched with. ### Requests The optional field `loRequestNum` specifies the number of log items that will be requested from the node. For example, if `loRequestNum` is `10`, `cardano-tracer` will periodically ask 10 log items in one request. This value is useful for fine-tuning network traffic: it is possible to ask 50 log items in one request, or ask them in 50 requests one at a time. `loRequestNum` is the *maximum* number of log items. For example, if `cardano-tracer` requests 50 log items but the node has only 40 at that moment, these 40 items will be returned, the request won't block to wait for additional 10 items. The optional field `ekgRequestFreq` specifies the period of how often EKG metrics will be requested, in seconds. For example, if `ekgRequestFreq` is `10`, `cardano-tracer` will ask for new EKG metrics every ten seconds. There is no limit as `loRequestNum`, so every request returns _all_ the metrics the node has _in this moment of time_. The reliable default values are `loRequestNum: 100` and `ekgRequestFreq: 1`, which will be used when these fields are left out of your configuration file. ### Logging Logging is one of the most important features of `cardano-tracer`. The field `logging` describes logging parameters: ``` "logging": [ { "logRoot": "/tmp/cardano-tracer-logs", "logMode": "FileMode", "logFormat": "ForMachine" } ] ``` The field `logRoot` specifies the path to the root directory. This directory will contain all the subdirectories with the log files inside. Remember that each subdirectory corresponds to the particular node. If the root directory does not exist, it will be created. This is an example of log structure: ``` /rootDir /subdirForNode0 node-2021-11-25T10-06-52.json node.json -> /rootDir/subdirForNode0/node-2021-11-25T10-06-52.json ``` In this example, `subdirForNode0` is a subdirectory containing log files with items received from the node `0`. And `node-2021-11-25T10-06-52.json` is the _current_ log: it means that currently `cardano-tracer` is writing items in this log file. The field `logMode` specifies logging mode. There are two possible modes: `FileMode` and `JournalMode`. `FileMode` is for storing logs to the files, `JournalMode` is for storing them in `systemd`'s journal. If you choose `JournalMode`, the field `logRoot` will be ignored. The field `logFormat` specifies the format of logs. There are two possible modes: `ForMachine` and `ForHuman`. `ForMachine` is for JSON format, `ForHuman` is for human-friendly text format. `logging` field accepts the list, so you can specify more than one logging section. For example, for both log formats: ``` "logging": [ { "logRoot": "/tmp/cardano-tracer-logs-json", "logMode": "FileMode", "logFormat": "ForMachine" }, { "logRoot": "/tmp/cardano-tracer-logs-text", "logMode": "FileMode", "logFormat": "ForHuman" } ] ``` In this case log items will be written in JSON format (in `.json`-files) as well as in text format (in `.log`-files). ### Logs Rotation An optional field `rotation` describes parameters for log rotation. If you skip this field, all the log items will be stored in one single file, and usually it's not what you want. These are rotation parameters: ``` "rotation": { "rpFrequencySecs": 30, "rpKeepFilesNum": 3, "rpLogLimitBytes": 50000, "rpMaxAgeHours": 1 } ``` The field `rpFrequencySecs` specifies rotation period, in seconds. In this example, `rpFrequencySecs` is `30`, which means that rotation check will be performed every 30 seconds. The field `rpLogLimitBytes` specifies the maximum size of the log file, in bytes. In this example, `rpLogLimitBytes` is `50000`, which means that once the size of the current log file is 50 KB, a new log file will be created. The field `rpKeepFilesNum` specifies the number of log files that will be kept. In this example, `rpKeepFilesNum` is `3`, which means that 3 _last_ log files will always be kept. The fields `rpMaxAgeMinutes`, `rpMaxAgeHours` specify the lifetime of the log file, in minutes, or hours. If both fields are specified, `rpMaxAgeMinutes` takes precedence. In this example, `rpMaxAgeHours` is `1`, which means that each log file will be kept for 1 hour only. After that, the log file is considered outdated. N _last_ log files (specified by `rpKeepFilesNum`) will be kept even if they are outdated. All other outdated files will be deleted by `cardano-tracer`. ### Prometheus The optional field `hasPrometheus` specifies the host and port of the web page with metrics. For example: ``` "hasPrometheus": { "epHost": "127.0.0.1", "epPort": 3000 } ``` Here the web page is available at `http://127.0.0.1:3000`. If you skip this field, no Prometheus endpoint will be started. After you open `http://127.0.0.1:3000` in your browser, you will see the list of identifiers of connected nodes (or the warning message, if there are no connected nodes yet), for example: ``` * KindStar_3001 ``` This identifier corresponds to the `TraceOptionNodeName` in the node config, or the fallback `_` if no such value is provided. Each identifier is a hyperlink to the page where you will see the **current** list of metrics received from the corresponding node, in such a format: ``` # TYPE Mem_resident_int gauge # HELP Mem_resident_int Kernel-reported RSS (resident set size) Mem_resident_int 103792640 # TYPE rts_gc_max_bytes_used gauge rts_gc_max_bytes_used 5811512 # TYPE rts_gc_gc_cpu_ms counter rts_gc_gc_cpu_ms 50 # TYPE RTS_gcMajorNum_int gauge # HELP RTS_gcMajorNum_int Major GCs RTS_gcMajorNum_int 4 # TYPE rts_gc_num_bytes_usage_samples counter rts_gc_num_bytes_usage_samples 4 # TYPE remainingKESPeriods_int gauge remainingKESPeriods_int 62 # TYPE rts_gc_bytes_copied counter rts_gc_bytes_copied 17114384 ``` That page from the example can of course be directly accessed by `http://127.0.0.1:3000/kindstar-3001`. #### Prometheus HTTP Service discovery The `/targets` path can be used for Prometheus HTTP service discovery. This lets Prometheus dynamically discover all connected nodes, and scrape their metrics. Below is a minimal example of a corresponding job definition that goes into the `prometheus.yml` configuration: ```yaml - job_name: "cardano-tracer" http_sd_configs: - url: 'http://127.0.0.1:3200/targets' # <-- Your cardano-tracer's real hostname:prometheus port ``` Each target will have a label `node_name` which corresponds to the `TraceOptionNodeName` setting in the respective node config. In `cardano-tracer`'s config, you can optionally provide additional labels to be attached to *all* targets (default is no additional labels): ```json "prometheusLabels": { "": "", ... } ``` ### EKG Monitoring The optional field `hasEKG` specifies the host and port of the web page with EKG metrics. For example: ``` "hasEKG": { "epHost": "127.0.0.1", "epPort": 3100 } ``` Just as with Prometheus, the root path `/` on EKG shows a list of connected nodes. The response is either human-readable names (HTML) with clickable links, or JSON mapping from connected node names to relative URLs, depending on desired content type (`Accept:` header of the request). The URL routes dynamically depend on the connected nodes _in this moment of time_; the node names are [sluggified](https://hackage.haskell.org/package/slugify). For a node with a specified name in its configuration: ``` { TraceOptionNodeName: "foo-node" } ``` and another connection that does not specify a node name, the list of clickable identifiers of connected nodes will be available at `http://127.0.0.1:3100` as: ``` * foo-node * KindStar_3001 ``` Just as with Prometheus, the fallback for `TraceOptionNodeName` is `_`. Clicking an identifier will take you to its monitoring page. Clicking on `foo-node` (`http://localhost:3100/foo-node`) and `KindStar_3001` (`127.0.0.1:3100/kindstar-3001`) takes you to the respective metrics monitoring. Sending a HTTP GET request with a JSON Accept header gives the metrics of an identifier as JSON. `jq '.'` pretty-prints the JSON object. ``` $ curl --silent -H 'Accept: application/json' '127.0.0.1:3100/kindstar-3001' | jq '.' { "Mem": { "resident_int": { "type": "g", "val": 790822912 } }, "RTS": { "alloc_int": { "type": "g", "val": 159054205680 }, "gcHeapBytes_int": { "type": "g", "val": 750780416 [...] ``` ### Verbosity ``` { "networkMagic": .., .. "verbosity": "ErrorsOnly" } ``` The `verbosity` field (optional) specifies the verbosity level for the `cardano-tracer` itself. There are 3 levels: 1. `Minimum` - `cardano-tracer` will work as silently as possible. 2. `ErrorsOnly` - messages about problems will be shown in standard output. 3. `Maximum` - all the messages will be shown in standard output. **Caution**: the number of messages can be huge. If you skip this field, `ErrorsOnly` verbosity will be used by default. --- ## Metrics migration guide - [Migrating metrics names](#migrating-metrics-names) - [Full suffixes variant](#full-suffixes-variant) - [Renamed metrics](#renamed-metrics) - [Removed metrics](#removed-metrics) - [Added metrics](#added-metrics) - [No suffix variant](#no-suffix-variant) - [Renamed metrics](#renamed-metrics-1) - [Removed metrics](#removed-metrics-1) - [Added metrics](#added-metrics-1) ## Migrating metrics names This is a comprehensive migration guide for metrics names. When switching from legacy to new tracing, the following changes in metrics names have to be accounted for in all places where metrics are processed. Within the new system, the names are stable; thus, migrating them is a one-time effort. The new tracing system exposes metrics in two naming variants, controlled by the `MetricsPrefix` setting in your tracer config: - **Full suffixes variant** (default) — metric names include a `_total`, `_bytes`, or similar suffix to follow Prometheus conventions. This is the variant used in the official Grafana dashboard. - **No suffix variant** — metric names match the legacy system more closely, useful if you are migrating existing dashboards or alert rules. ### Full suffixes variant In the legacy system, the metrics naming schema isn't fully consistent wrt. name suffixes. Hence, the new system includes several name changes to improve that, as well as be more compliant with existing standards. This is what the migration looks like when scraping from `cardano-tracer`, or from the Node's `PrometheusSimple` backend with the `suffix` switch (default). #### Renamed metrics The following metrics have been **renamed**: ``` cardano_node_metrics_Forge_adopted_int --> cardano_node_metrics_Forge_adopted_counter cardano_node_metrics_Forge_forge_about_to_lead_int --> cardano_node_metrics_Forge_about_to_lead_counter cardano_node_metrics_Forge_forged_int --> cardano_node_metrics_Forge_forged_counter cardano_node_metrics_Forge_node_is_leader_int --> cardano_node_metrics_Forge_node_is_leader_counter cardano_node_metrics_Forge_node_not_leader_int --> cardano_node_metrics_Forge_node_not_leader_counter cardano_node_metrics_Stat_threads_int --> cardano_node_metrics_RTS_threads_int cardano_node_metrics_blockfetchclient_blockdelay_cdfFive --> cardano_node_metrics_blockfetchclient_blockdelay_cdfFive_real cardano_node_metrics_blockfetchclient_blockdelay_cdfOne --> cardano_node_metrics_blockfetchclient_blockdelay_cdfOne_real cardano_node_metrics_blockfetchclient_blockdelay_cdfThree --> cardano_node_metrics_blockfetchclient_blockdelay_cdfThree_real cardano_node_metrics_blockfetchclient_blockdelay_s --> cardano_node_metrics_blockfetchclient_blockdelay_real cardano_node_metrics_blockfetchclient_blocksize --> cardano_node_metrics_blockfetchclient_blocksize_int cardano_node_metrics_blockfetchclient_lateblocks --> cardano_node_metrics_blockfetchclient_lateblocks_counter cardano_node_metrics_blocksForgedNum_int --> cardano_node_metrics_blocksForged_int cardano_node_metrics_connectionManager_duplexConns --> cardano_node_metrics_connectionManager_duplexConns_int cardano_node_metrics_connectionManager_fullDuplexConns --> cardano_node_metrics_connectionManager_fullDuplexConns_int cardano_node_metrics_connectionManager_incomingConns --> cardano_node_metrics_connectionManager_inboundConns_int cardano_node_metrics_connectionManager_outgoingConns --> cardano_node_metrics_connectionManager_outboundConns_int cardano_node_metrics_connectionManager_unidirectionalConns --> cardano_node_metrics_connectionManager_unidirectionalConns_int cardano_node_metrics_forging_enabled --> cardano_node_metrics_forging_enabled_int cardano_node_metrics_forks_int --> cardano_node_metrics_forks_counter cardano_node_metrics_inboundGovernor_cold --> cardano_node_metrics_inboundGovernor_cold_int cardano_node_metrics_inboundGovernor_hot --> cardano_node_metrics_inboundGovernor_hot_int cardano_node_metrics_inboundGovernor_idle --> cardano_node_metrics_inboundGovernor_idle_int cardano_node_metrics_inboundGovernor_warm --> cardano_node_metrics_inboundGovernor_warm_int cardano_node_metrics_nodeIsLeaderNum_int --> cardano_node_metrics_nodeIsLeader_int cardano_node_metrics_nodeStartTime_int --> cardano_node_metrics_node_start_time_int cardano_node_metrics_peerSelection_ActiveBigLedgerPeers --> cardano_node_metrics_peerSelection_ActiveBigLedgerPeers_int cardano_node_metrics_peerSelection_ActiveBigLedgerPeersDemotions --> cardano_node_metrics_peerSelection_ActiveBigLedgerPeersDemotions_int cardano_node_metrics_peerSelection_ActiveBootstrapPeers --> cardano_node_metrics_peerSelection_ActiveBootstrapPeers_int cardano_node_metrics_peerSelection_ActiveBootstrapPeersDemotions --> cardano_node_metrics_peerSelection_ActiveBootstrapPeersDemotions_int cardano_node_metrics_peerSelection_ActiveLocalRootPeers --> cardano_node_metrics_peerSelection_ActiveLocalRootPeers_int cardano_node_metrics_peerSelection_ActiveLocalRootPeersDemotions --> cardano_node_metrics_peerSelection_ActiveLocalRootPeersDemotions_int cardano_node_metrics_peerSelection_ActiveNonRootPeers --> cardano_node_metrics_peerSelection_ActiveNonRootPeers_int cardano_node_metrics_peerSelection_ActiveNonRootPeersDemotions --> cardano_node_metrics_peerSelection_ActiveNonRootPeersDemotions_int cardano_node_metrics_peerSelection_ActivePeers --> cardano_node_metrics_peerSelection_ActivePeers_int cardano_node_metrics_peerSelection_ActivePeersDemotions --> cardano_node_metrics_peerSelection_ActivePeersDemotions_int cardano_node_metrics_peerSelection_ColdBigLedgerPeersPromotions --> cardano_node_metrics_peerSelection_ColdBigLedgerPeersPromotions_int cardano_node_metrics_peerSelection_ColdBootstrapPeersPromotions --> cardano_node_metrics_peerSelection_ColdBootstrapPeersPromotions_int cardano_node_metrics_peerSelection_ColdNonRootPeersPromotions --> cardano_node_metrics_peerSelection_ColdNonRootPeersPromotions_int cardano_node_metrics_peerSelection_ColdPeersPromotions --> cardano_node_metrics_peerSelection_ColdPeersPromotions_int cardano_node_metrics_peerSelection_EstablishedBigLedgerPeers --> cardano_node_metrics_peerSelection_EstablishedBigLedgerPeers_int cardano_node_metrics_peerSelection_EstablishedBootstrapPeers --> cardano_node_metrics_peerSelection_EstablishedBootstrapPeers_int cardano_node_metrics_peerSelection_EstablishedLocalRootPeers --> cardano_node_metrics_peerSelection_EstablishedLocalRootPeers_int cardano_node_metrics_peerSelection_EstablishedNonRootPeers --> cardano_node_metrics_peerSelection_EstablishedNonRootPeers_int cardano_node_metrics_peerSelection_EstablishedPeers --> cardano_node_metrics_peerSelection_EstablishedPeers_int cardano_node_metrics_peerSelection_KnownBigLedgerPeers --> cardano_node_metrics_peerSelection_KnownBigLedgerPeers_int cardano_node_metrics_peerSelection_KnownBootstrapPeers --> cardano_node_metrics_peerSelection_KnownBootstrapPeers_int cardano_node_metrics_peerSelection_KnownLocalRootPeers --> cardano_node_metrics_peerSelection_KnownLocalRootPeers_int cardano_node_metrics_peerSelection_KnownNonRootPeers --> cardano_node_metrics_peerSelection_KnownNonRootPeers_int cardano_node_metrics_peerSelection_KnownPeers --> cardano_node_metrics_peerSelection_KnownPeers_int cardano_node_metrics_peerSelection_RootPeers --> cardano_node_metrics_peerSelection_RootPeers_int cardano_node_metrics_peerSelection_WarmBigLedgerPeersDemotions --> cardano_node_metrics_peerSelection_WarmBigLedgerPeersDemotions_int cardano_node_metrics_peerSelection_WarmBigLedgerPeersPromotions --> cardano_node_metrics_peerSelection_WarmBigLedgerPeersPromotions_int cardano_node_metrics_peerSelection_WarmBootstrapPeersDemotions --> cardano_node_metrics_peerSelection_WarmBootstrapPeersDemotions_int cardano_node_metrics_peerSelection_WarmBootstrapPeersPromotions --> cardano_node_metrics_peerSelection_WarmBootstrapPeersPromotions_int cardano_node_metrics_peerSelection_WarmLocalRootPeersPromotions --> cardano_node_metrics_peerSelection_WarmLocalRootPeersPromotions_int cardano_node_metrics_peerSelection_WarmNonRootPeersDemotions --> cardano_node_metrics_peerSelection_WarmNonRootPeersDemotions_int cardano_node_metrics_peerSelection_WarmNonRootPeersPromotions --> cardano_node_metrics_peerSelection_WarmNonRootPeersPromotions_int cardano_node_metrics_peerSelection_WarmPeersDemotions --> cardano_node_metrics_peerSelection_WarmPeersDemotions_int cardano_node_metrics_peerSelection_WarmPeersPromotions --> cardano_node_metrics_peerSelection_WarmPeersPromotions_int cardano_node_metrics_peerSelection_churn_DecreasedActiveBigLedgerPeers --> cardano_node_metrics_peerSelection_churn_DecreasedActiveBigLedgerPeers_int cardano_node_metrics_peerSelection_churn_DecreasedActiveBigLedgerPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedActiveBigLedgerPeers_duration_real cardano_node_metrics_peerSelection_churn_DecreasedActivePeers --> cardano_node_metrics_peerSelection_churn_DecreasedActivePeers_int cardano_node_metrics_peerSelection_churn_DecreasedActivePeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedActivePeers_duration_real cardano_node_metrics_peerSelection_churn_DecreasedEstablishedBigLedgerPeers --> cardano_node_metrics_peerSelection_churn_DecreasedEstablishedBigLedgerPeers_int cardano_node_metrics_peerSelection_churn_DecreasedEstablishedBigLedgerPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedEstablishedBigLedgerPeers_duration_real cardano_node_metrics_peerSelection_churn_DecreasedEstablishedPeers --> cardano_node_metrics_peerSelection_churn_DecreasedEstablishedPeers_int cardano_node_metrics_peerSelection_churn_DecreasedEstablishedPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedEstablishedPeers_duration_real cardano_node_metrics_peerSelection_churn_DecreasedKnownBigLedgerPeers --> cardano_node_metrics_peerSelection_churn_DecreasedKnownBigLedgerPeers_int cardano_node_metrics_peerSelection_churn_DecreasedKnownBigLedgerPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedKnownBigLedgerPeers_duration_real cardano_node_metrics_peerSelection_churn_DecreasedKnownPeers --> cardano_node_metrics_peerSelection_churn_DecreasedKnownPeers_int cardano_node_metrics_peerSelection_churn_DecreasedKnownPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedKnownPeers_duration_real cardano_node_metrics_peerSelection_churn_IncreasedActiveBigLedgerPeers --> cardano_node_metrics_peerSelection_churn_IncreasedActiveBigLedgerPeers_int cardano_node_metrics_peerSelection_churn_IncreasedActivePeers --> cardano_node_metrics_peerSelection_churn_IncreasedActivePeers_int cardano_node_metrics_peerSelection_churn_IncreasedEstablishedBigLedgerPeers --> cardano_node_metrics_peerSelection_churn_IncreasedEstablishedBigLedgerPeers_int cardano_node_metrics_peerSelection_churn_IncreasedEstablishedPeers --> cardano_node_metrics_peerSelection_churn_IncreasedEstablishedPeers_int cardano_node_metrics_peerSelection_churn_IncreasedKnownBigLedgerPeers --> cardano_node_metrics_peerSelection_churn_IncreasedKnownBigLedgerPeers_int cardano_node_metrics_peerSelection_churn_IncreasedKnownPeers --> cardano_node_metrics_peerSelection_churn_IncreasedKnownPeers_int cardano_node_metrics_peerSelection_cold --> cardano_node_metrics_peerSelection_Cold_int cardano_node_metrics_peerSelection_coldBigLedgerPeers --> cardano_node_metrics_peerSelection_ColdBigLedgerPeers_int cardano_node_metrics_peerSelection_hot --> cardano_node_metrics_peerSelection_Hot_int cardano_node_metrics_peerSelection_hotBigLedgerPeers --> cardano_node_metrics_peerSelection_HotBigLedgerPeers_int cardano_node_metrics_peerSelection_warm --> cardano_node_metrics_peerSelection_Warm_int cardano_node_metrics_peerSelection_warmBigLedgerPeers --> cardano_node_metrics_peerSelection_WarmBigLedgerPeers_int cardano_node_metrics_served_block_count_int --> cardano_node_metrics_served_block_counter cardano_node_metrics_served_block_latest_count_int --> cardano_node_metrics_served_block_latest_int cardano_node_metrics_served_header_counter_int --> cardano_node_metrics_served_header_counter cardano_node_metrics_txsProcessedNum_int --> cardano_node_metrics_txsProcessedNum_counter ``` #### Removed metrics The following metrics have been **removed**: ``` ekg_server_timestamp_ms ``` #### Added metrics The following metrics have been **added**: ``` cardano_node_metrics_ChainSync_HeadersServed_counter cardano_node_metrics_GSM_state_int cardano_node_metrics_RTS_alloc_int cardano_node_metrics_Stat_blkIOticks_int cardano_node_metrics_Stat_fsRd_int cardano_node_metrics_Stat_fsWr_int cardano_node_metrics_Stat_netRd_int cardano_node_metrics_Stat_netWr_int cardano_node_metrics_basicInfo cardano_node_metrics_blockReplayProgress_real cardano_node_metrics_cardano_version_major_int cardano_node_metrics_cardano_version_minor_int cardano_node_metrics_cardano_version_patch_int cardano_node_metrics_forgedSlotLast_int cardano_node_metrics_haskell_compiler_major_int cardano_node_metrics_haskell_compiler_minor_int cardano_node_metrics_haskell_compiler_patch_int cardano_node_metrics_localInboundGovernor_cold_int cardano_node_metrics_localInboundGovernor_hot_int cardano_node_metrics_localInboundGovernor_idle_int cardano_node_metrics_localInboundGovernor_warm_int cardano_node_metrics_nodeCannotForge_int cardano_node_metrics_peerSelection_HotLocalRoots_int cardano_node_metrics_peerSelection_WarmLocalRoots_int cardano_node_metrics_slotsMissed_int cardano_node_metrics_submissions_accepted_counter cardano_node_metrics_submissions_rejected_counter cardano_node_metrics_submissions_submitted_counter cardano_node_metrics_tipBlock cardano_node_metrics_txsMempoolTimeoutHard_counter cardano_node_metrics_txsMempoolTimeoutSoft_counter ``` ### No suffix variant For those who prefer a simpler migration and do not require suffixes, the Node's `PrometheusSimple` backend allows for dropping them with the `nosuffix` switch. This is what the migration looks like in that case. #### Renamed metrics The following metrics have been **renamed**: ``` cardano_node_metrics_Forge_adopted_int --> cardano_node_metrics_Forge_adopted cardano_node_metrics_Forge_forge_about_to_lead_int --> cardano_node_metrics_Forge_about_to_lead cardano_node_metrics_Forge_forged_int --> cardano_node_metrics_Forge_forged cardano_node_metrics_Forge_node_is_leader_int --> cardano_node_metrics_Forge_node_is_leader cardano_node_metrics_Forge_node_not_leader_int --> cardano_node_metrics_Forge_node_not_leader cardano_node_metrics_Mem_resident_int --> cardano_node_metrics_Mem_resident cardano_node_metrics_RTS_gcHeapBytes_int --> cardano_node_metrics_RTS_gcHeapBytes cardano_node_metrics_RTS_gcLiveBytes_int --> cardano_node_metrics_RTS_gcLiveBytes cardano_node_metrics_RTS_gcMajorNum_int --> cardano_node_metrics_RTS_gcMajorNum cardano_node_metrics_RTS_gcMinorNum_int --> cardano_node_metrics_RTS_gcMinorNum cardano_node_metrics_RTS_gcticks_int --> cardano_node_metrics_RTS_gcticks cardano_node_metrics_RTS_mutticks_int --> cardano_node_metrics_RTS_mutticks cardano_node_metrics_Stat_cputicks_int --> cardano_node_metrics_Stat_cputicks cardano_node_metrics_Stat_threads_int --> cardano_node_metrics_RTS_threads cardano_node_metrics_blockNum_int --> cardano_node_metrics_blockNum cardano_node_metrics_blockfetchclient_blockdelay_s --> cardano_node_metrics_blockfetchclient_blockdelay cardano_node_metrics_blocksForgedNum_int --> cardano_node_metrics_blocksForged cardano_node_metrics_connectionManager_incomingConns --> cardano_node_metrics_connectionManager_inboundConns cardano_node_metrics_connectionManager_outgoingConns --> cardano_node_metrics_connectionManager_outboundConns cardano_node_metrics_currentKESPeriod_int --> cardano_node_metrics_currentKESPeriod cardano_node_metrics_delegMapSize_int --> cardano_node_metrics_delegMapSize cardano_node_metrics_density_real --> cardano_node_metrics_density cardano_node_metrics_epoch_int --> cardano_node_metrics_epoch cardano_node_metrics_forks_int --> cardano_node_metrics_forks cardano_node_metrics_mempoolBytes_int --> cardano_node_metrics_mempoolBytes cardano_node_metrics_nodeIsLeaderNum_int --> cardano_node_metrics_nodeIsLeader cardano_node_metrics_nodeStartTime_int --> cardano_node_metrics_node_start_time cardano_node_metrics_operationalCertificateExpiryKESPeriod_int --> cardano_node_metrics_operationalCertificateExpiryKESPeriod cardano_node_metrics_operationalCertificateStartKESPeriod_int --> cardano_node_metrics_operationalCertificateStartKESPeriod cardano_node_metrics_peerSelection_churn_DecreasedActiveBigLedgerPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedActiveBigLedgerPeers_duration cardano_node_metrics_peerSelection_churn_DecreasedActivePeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedActivePeers_duration cardano_node_metrics_peerSelection_churn_DecreasedEstablishedBigLedgerPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedEstablishedBigLedgerPeers_duration cardano_node_metrics_peerSelection_churn_DecreasedEstablishedPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedEstablishedPeers_duration cardano_node_metrics_peerSelection_churn_DecreasedKnownBigLedgerPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedKnownBigLedgerPeers_duration cardano_node_metrics_peerSelection_churn_DecreasedKnownPeers_duration --> cardano_node_metrics_peerSelection_churnDecreasedKnownPeers_duration cardano_node_metrics_peerSelection_cold --> cardano_node_metrics_peerSelection_Cold cardano_node_metrics_peerSelection_coldBigLedgerPeers --> cardano_node_metrics_peerSelection_ColdBigLedgerPeers cardano_node_metrics_peerSelection_hot --> cardano_node_metrics_peerSelection_Hot cardano_node_metrics_peerSelection_hotBigLedgerPeers --> cardano_node_metrics_peerSelection_HotBigLedgerPeers cardano_node_metrics_peerSelection_warm --> cardano_node_metrics_peerSelection_Warm cardano_node_metrics_peerSelection_warmBigLedgerPeers --> cardano_node_metrics_peerSelection_WarmBigLedgerPeers cardano_node_metrics_remainingKESPeriods_int --> cardano_node_metrics_remainingKESPeriods cardano_node_metrics_served_block_count_int --> cardano_node_metrics_served_block cardano_node_metrics_served_block_latest_count_int --> cardano_node_metrics_served_block_latest cardano_node_metrics_served_header_counter_int --> cardano_node_metrics_served_header cardano_node_metrics_slotInEpoch_int --> cardano_node_metrics_slotInEpoch cardano_node_metrics_slotNum_int --> cardano_node_metrics_slotNum cardano_node_metrics_txsInMempool_int --> cardano_node_metrics_txsInMempool cardano_node_metrics_txsProcessedNum_int --> cardano_node_metrics_txsProcessedNum cardano_node_metrics_txsSyncDuration_int --> cardano_node_metrics_txsSyncDuration cardano_node_metrics_utxoSize_int --> cardano_node_metrics_utxoSize ``` #### Removed metrics The following metrics have been **removed**: ``` ekg_server_timestamp_ms ``` #### Added metrics The following metrics have been **added**: ``` cardano_node_metrics_ChainSync_HeadersServed cardano_node_metrics_GSM_state cardano_node_metrics_RTS_alloc cardano_node_metrics_Stat_blkIOticks cardano_node_metrics_Stat_fsRd cardano_node_metrics_Stat_fsWr cardano_node_metrics_Stat_netRd cardano_node_metrics_Stat_netWr cardano_node_metrics_basicInfo cardano_node_metrics_blockReplayProgress cardano_node_metrics_cardano_version_major cardano_node_metrics_cardano_version_minor cardano_node_metrics_cardano_version_patch cardano_node_metrics_forgedSlotLast cardano_node_metrics_haskell_compiler_major cardano_node_metrics_haskell_compiler_minor cardano_node_metrics_haskell_compiler_patch cardano_node_metrics_localInboundGovernor_cold cardano_node_metrics_localInboundGovernor_hot cardano_node_metrics_localInboundGovernor_idle cardano_node_metrics_localInboundGovernor_warm cardano_node_metrics_nodeCannotForge cardano_node_metrics_peerSelection_HotLocalRoots cardano_node_metrics_peerSelection_WarmLocalRoots cardano_node_metrics_slotsMissed cardano_node_metrics_submissions_accepted cardano_node_metrics_submissions_rejected cardano_node_metrics_submissions_submitted cardano_node_metrics_tipBlock cardano_node_metrics_txsMempoolTimeoutHard cardano_node_metrics_txsMempoolTimeoutSoft ``` --- ## Quick start - [Introduction](#introduction) - [Configuration and use of **Cardano Tracer**](#configuration-and-use-of-cardano-tracer) - [Advanced Configuration](#advanced-configuration) - [Node-side configuration of new tracing](#node-side-configuration-of-new-tracing) - [Node-side configuration of new tracing: `TraceOptions`](#node-side-configuration-of-new-tracing-traceoptions) - [Node-side configuration of new tracing: other fields](#node-side-configuration-of-new-tracing-other-fields) - [Configuration formats and fallback](#configuration-formats-and-fallback) - [Old Tracing and New Tracing](#old-tracing-and-new-tracing) - [You're set!](#youre-set) - [Feedback and Reporting](#feedback-and-reporting) - [Developers only: developing new tracers during transition time](#developers-only-developing-new-tracers-during-transition-time) - [Documentation and References](#documentation-and-references) ## Introduction This document provides an overview of the **New Tracing System**'s functionality, configuration, and modes of operation. The system has been designed to offer flexible and efficient monitoring of Cardano nodes through trace forwarding and hierarchical trace message handling. #### Functionality Split between Node and Tracer The new system separates monitoring responsibilities between the **Cardano Node** and the **Cardano Tracer** services. This modular approach ensures the node focuses on core operations, while `cardano-tracer` manages logging, monitoring, and external communication. #### Introducing Trace Forwarding Trace forwarding enables seamless transmission of trace data and metrics from nodes to a centralized tracer service. This feature simplifies remote monitoring and supports scenarios where multiple nodes are monitored by a single `cardano-tracer` instance. Forwarding via Unix domain sockets or Windows named pipes is the preferred option, although support for forwarding over TCP/IP exists. #### Hierarchical Namespaces for Trace Messages To streamline configuration, the system now uses **hierarchical namespaces** for trace messages instead of directly referencing tracers. This change improves manageability and aligns trace configuration with logical groupings. #### Modes of Operation The new tracing system supports several modes of operation to suit different deployment scenarios: - **Without Forwarding:** The node operates independently and writes trace messages to `stdout`. Metrics can be exposed by using the `PrometheusSimple` backend. - **One Node, One Cardano Tracer:** The tracer connects to a single node over socket (or loopback device). Trace output and metrics are forwarded to the tracer, and are available according to its configuration. - **One Tracer, Many Nodes:** A single tracer connects to multiple nodes over socket (possibly via SSH tunnel between hosts) or over IPv4 / IPv6. Forwarding works exactly as described under the previous point. #### Two-Part Configuration The configuration for the new tracing system is distributed across two key components: 1. **Node Configuration File** - Specifies the settings required to enable and manage trace forwarding from the node to the tracer. - Includes parameters such as: - Enabling the new tracing system. - Defining trace message filtering and granularity. - Additional details about trace forwarding. 2. **Cardano Tracer Configuration File** - Configures the tracer service itself with settings such as: - Communication endpoints (e.g., socket paths). - Logging formats and destinations. - Metrics, and where to query them (e.g., HTTP ports for EKG or Prometheus). - Directories for log storage and rotation. These configurations work together to ensure efficient communication between the node and the tracer, providing a flexible and robust monitoring setup. ## Configuration and use of **Cardano Tracer** `cardano-tracer` is a key component of the new tracing infrastructure. It operates as a standalone service that consumes trace messages from `cardano-node`, processes them, and provides outputs for monitoring and analysis. **Key Features:** - **Logging to File or System Services**: Write logs in JSON format for machine processing or as text for human readability. - **Metrics Exposure**: Provides EKG and Prometheus metrics endpoints for system monitoring. Below is an example of configuring a simple use case: a node and `cardano-tracer` running on the same machine. --- #### Step 1: Transport from Node to Tracer Add the following option to the Cardano node's CLI arguments: ```bash --tracer-socket-path-connect /tmp/forwarder.sock ``` This instructs the node to forward trace messages to `cardano-tracer` via the specified socket path. The system only supports its own transport layer, the 'forwarding protocol'. --- #### Step 2: Build and Run `cardano-tracer` Build and run `cardano-tracer` using either `cabal` or `nix`. Below are examples for both methods. **Using Cabal:** ```bash cabal build cardano-tracer && cabal install cardano-tracer --installdir=PATH_TO_DIR --overwrite-policy=always cd PATH_TO_DIR ./cardano-tracer --config PATH_TO_CONFIG ``` **Using Nix:** ```bash nix build github://github.com/IntersectMBO/cardano-node#cardano-tracer ./result/bin/cardano-tracer --config PATH_TO_CONFIG ``` Replace `PATH_TO_CONFIG` with the path to your `cardano-tracer` configuration file (see next step). --- #### Step 3: Minimal Example Configuration Below is an example configuration for a single-node-to-single-tracer setup. The system's forwarding protocol encodes the network magic, so it is mandatory to provide one. Both ends of trace forwarding require the same magic; the example uses the one for mainnet: **Minimal Example:** ```yaml networkMagic: 764824073 network: tag: AcceptAt contents: "/tmp/forwarder.sock" logging: - logRoot: "/tmp/cardano-tracer-logs" logMode: FileMode logFormat: ForMachine ``` - `network`: Specifies the socket path for communication between the node and the tracer. - `logging`: Configures logs to be written to the `/tmp/cardano-tracer-logs` directory in JSON format. --- #### Step 4: Running the Setup Starting with Node `10.2`, the new tracing system is chosen by default. On previous versions, it can be explicitly enabled in the config by setting `UseTraceDispatcher: true`. Go through the adjustments for your Node configuration file (next chapter). When this is done, and `cardano-tracer` is running, start the Node. It will establish a connection to `cardano-tracer` and begin forwarding trace messages. The tracer will process these messages and generate logs in the specified directory (`/tmp/cardano-tracer-logs`). --- ### Advanced Configuration For more complex setups, such as monitoring multiple nodes or exposing metrics via Prometheus, additional configuration examples are available. Please refer to the [Cardano Tracer Documentation] for detailed guidance on advanced setups and use cases. #### Forwarding over TCP In addition to forwarding over sockets, forwarding over TCP/IP is supported. In both cases, the 'forwarding protocol' is identical. For TCP forwarding, adjust the following: _From Step 1_ - replace node CLI option: ```bash --tracer-socket-network-connect 10.0.0.2:34567 ``` _From Step 3_ - adjust value for `network` in `cardano-tracer`'s configuration: ```yaml network: tag: AcceptAt contents: "0.0.0.0:34567" ``` In this example, `cardano-tracer` listens on port 34567. Nodes can connect via IPv4 for forwarding, with `10.0.0.2` being `cardano-tracer`'s IP in that example. :::tip Important On same-host setups sockets are always preferrable due to less overhead and better performance. On multi-host setups, socket connection via SSH tunnels is always preferrable due to increased security. Use TCP forwarding **if and only if** you control each and every aspect of the environment, such as port mapping or firewalls, or virtual network setup - the 'forwarding protocol' does not implement encrypting traffic nor authentication methods. ::: ## Node-side configuration of new tracing ### Node-side configuration of new tracing: `TraceOptions` The new tracing system uses **namespaces** for configuration values, enabling fine-grained control down to individual messages. More specific configuration values will override general ones, allowing for a flexible hierarchical setup. The values are provided in the new `TraceOptions` object in the node configuration file, which we'll further inspect here: --- #### 1. Specify Message Severity Filter Define the **severity level** of messages you want to be included in trace output. More specific namespaces override more general ones. **Example:** ```yaml # namespace root - applies to all dependent messages (*): show messages of severity Notice or higher "": severity: Notice # ChainDB messages: show messages of severity Info or higher # Overrides setting from namespace root, being more specific, and applies to all dependent messages (ChainDB.*). ChainDB: severity: Info ``` To suppress all messages pertaining to a namespace, use the severity level `Silence`. For a map of the entire namespace of trace messages, please refer to the [Cardano Trace Documentation]. --- #### 2. Specify Message Detail Level Configure the **detail level** for the messages to control the amount of information rendered for each message. **Example:** ```yaml "": severity: Notice detail: DNormal ``` - Supported detail levels: - `DMinimal`: minimal message verbosity - `DNormal`: default verbosity - `DDetailed`: extended message verbosity - `DMaximum`: be extremely verbose - only recommended for development or debugging *Note*: Trace messages might choose to not support every detail level in their implementation - or only one; the highest matching detail level will then be chosen for rendering. --- #### 3. Specify Message Frequency Limiters Use **frequency limiters** to control how often messages are displayed. This replaces the old 'eliding tracers' functionality. **Example:** ```yaml ChainDB.AddBlockEvent.AddedBlockToQueue: # Show a maximum of 2 messages per second maxFrequency: 2.0 ``` Setting `maxFrequency: 0.0` disables frequency limiting - which is the default. --- #### 4. Specify backends for trace data Define the **backends** that will be enabled inside the Node to process trace data. **Example:** ```yaml "": severity: Notice detail: DNormal backends: - Stdout MachineFormat - EKGBackend - Forwarder - PrometheusSimple 127.0.0.1 12798 ``` - Write to standard output (only one can be used): - `Stdout MachineFormat`: in JSON format - `Stdout HumanFormatColoured`: in color-coded text format - `Stdout HumanFormatUncoloured`: in plain text format - `EKGBackend`: Have the node collect metrics. Required to forward metrics or expose them via PrometheusSimple - `Forwarder`: Forwards trace messages and metrics to `cardano-tracer` - `PrometheusSimple` (with connection string): Have the node expose Prometheus metrics directly; in the example under URL `localhost:12798/metrics` *Note*: For standard output, trace messages that do not implement a text format might be displayed as JSON. *Note*: Metrics, although being based on trace data, are **independent** of trace messages. This means, you can access all metrics even if their corresponding trace messages are filtered out or silenced in your configuration. It also means, they can be forwarded to `cardano-tracer` even when you don't forward their corresponding trace messages :::tip Important Please make sure to enable the `Forwarder` backend **if and only if** you intend to consume the trace ouput with a running `cardano-tracer` instance. In case of unreliable forwarding connections, the Node generously buffers traces that have not been consumed; and though the buffer is bounded, you will experience permanently increased RAM usage if traces are never consumed at all. Please make sure to enable the `PrometheusSimple` backend **if and only if** you intend to scrape the node process itself for metrics. This way, you avoid exposing the node over an open port unnecessarily. ::: --- #### 5. Example: tracing Ouroboros Genesis sync To debug a Genesis-mode sync (a node stuck in `PreSyncing` or `Syncing`), raise the Genesis component namespaces to `Debug`. The GSM transition events already appear at the default `Notice` severity; the CSJ jump events and per-peer BlockFetch decisions are `Info` or `Debug` and stay hidden until you raise them: ```yaml Consensus.GSM: { severity: Debug } Consensus.CSJ: { severity: Debug } Consensus.GDD: { severity: Debug } Consensus.DevotedBlockFetch: { severity: Debug } BlockFetch.Decision: { severity: Debug, detail: DMaximum } ChainSync.Client: { severity: Debug } Net.PeerSelection: { severity: Debug } Net.ConnectionManager: { severity: Debug } ``` Debug-level Genesis tracers are noisy. Rate-limit the high-volume namespaces and silence the low-signal ones: ```yaml BlockFetch.Decision.PeersFetch: { maxFrequency: 1.0 } ChainSync.Client.DownloadedHeader: { maxFrequency: 1.0 } ChainSync.Client.ValidatedHeader: { maxFrequency: 1.0 } ChainDB.AddBlockEvent.TrySwitchToAFork: { severity: Silence } Net.ConnectionManager.Remote.ConnectionManagerCounters: { severity: Silence } ``` For what these events mean and how to read a Genesis sync stall against the components that emit them, see the consensus reference [Observing and Debugging Genesis Sync](https://ouroboros-consensus.cardano.intersectmbo.org/docs/references/miscellaneous/genesis_observability). --- ### Node-side configuration of new tracing: other fields In addition to providing a `TraceOptions` entry, the new tracing system introduces additional configuration values in the node configuration file: - `TraceOptionNodeName`: (string) This is used by `cardano-tracer` as the base for creating logging subdirectories and URL paths to query metrics. By default, the hostname is chosen. - `TraceOptionMetricsPrefix`: (string) Adds a prefix to all metrics names. For increased compatibility with names in the old system, you could use `"cardano.node.metrics."`. - `UseTraceDispatcher`: (boolean) Enables / disables the new tracing system --- ### Configuration formats and fallback Configurations can be written in both **JSON** and **YAML**. The examples in this document are provided in **YAML** for readability. A full example of a mainnet node config file utilizing various settings for the new tracing system can be found here: [mainnet-config.json] There's a sensible **fallback** configuration hard-coded inside a Haskell module of the Node: [Cardano.Node.Tracing.DefaultTraceConfig]. It is important to state the `TraceOptions` from this fallback will be used if and only if the `TraceOptions` object in your Node configuration is empty. --- ### Old Tracing and New Tracing The **old tracing system** is slated for decommissioning but will coexist with the **new tracing system** during a transitional grace period of approximately **3 to 6 months**. During this time, both systems will remain part of the default `cardano-node` build, ensuring compatibility and easing the migration process. #### Switching Between Tracing Systems - To enable the **new tracing system**, set the Node's configuration value `UseTraceDispatcher: true`. - To continue using the **old tracing system**, you need to explicitly set `UseTraceDispatcher: false` on Node 10.2 and onwards. #### Deprecation of the `kind` field in trace messages Certain legacy features will be deprecated to simplify and unify the tracing infrastructure. Specifically: - The **`kind` field** will be **deprecated** and removed when decomissioning the old tracing system. - We strongly recommend using **namespaces** (provided in the new `ns` field; see below) instead for any analysis tools or automations involving traces. ## You're set! ### Feedback and Reporting Your feedback is invaluable during this transition. Please help us improve the system by reporting any regressions, issues or difficulties integrating with existing automations while using the new tracing infrastructure. ### Developers only: developing new tracers during transition time During the transition from old to new tracing system, we recommend the following best practices for developing tracers: 1. **Avoid Using Strictness Annotations for Trace types** Trace messages are either discarded immediately or quickly converted into another format for processing. They are never stored for long durations. Using strictness annotations can make the code less efficient without adding any tangible benefits. Additionally, strictness annotations do not align well with the prototype requirements for messages in the new framework. 2. **Prioritize Developing New Tracers** Focus on developing new tracers first and then map them to the old tracers. This approach ensures compatibility while future-proofing your work, as the new tracers will remain in use after the old system is decommissioned. You can find numerous examples in `cardano-nodes` source code in module `Cardano.Node.Tracing.Tracers`. 3. **Leverage Expertise** If you have questions or need reviews for your tracers, reach out to the **Performance & Tracing Team**. 4. **`kind` Fields** As described above, keep in mind the `kind` field will be removed eventually; please rely on namespaces when analysing trace messages. --- ### Documentation and References To support users, administrators and developers, the following documentation provides comprehensive insights into trace messages, metrics, and data points: - **Trace Messages and Default Configuration**: This periodically regenerated document catalogs all trace messages, metrics, and data points in `cardano-node`. It also illustrates how these messages are handled with the current default configuration: [Cardano Trace Documentation] - **Trace-Dispatcher Library**: This document describes the underlying library powering the new tracing system. It provides details about its design, flexibility, and efficiency: [trace-dispatcher: Efficient, Simple, and Flexible Program Tracing] - **Cardano Tracer**: For information about the `cardano-tracer` service, which facilitates logging and monitoring, refer to its dedicated documentation: [Cardano Tracer Documentation] [//]: # (references) [Cardano Trace Documentation]: https://github.com/input-output-hk/cardano-node-wiki/blob/main/docs/new-tracing/tracers_doc_generated.md [Cardano Tracer Documentation]: https://github.com/intersectmbo/cardano-node/blob/master/cardano-tracer/docs/cardano-tracer.md [Cardano.Node.Tracing.DefaultTraceConfig]: https://github.com/intersectmbo/cardano-node/blob/master/cardano-node/src/Cardano/Node/Tracing/DefaultTraceConfig.hs [mainnet-config.json]: https://github.com/IntersectMBO/cardano-node/blob/master/configuration/cardano/mainnet-config.json [trace-dispatcher: Efficient, Simple, and Flexible Program Tracing]: https://github.com/IntersectMBO/hermod-tracing/tree/master/trace-dispatcher --- ## Installing cardano-node :::info version reference This document was written in May 2026 for the current stable release **11.0.1**. Always check the [releases page](https://github.com/IntersectMBO/cardano-node/releases) for the latest version before installing. ::: :::tip Cardano Node Course For a comprehensive video course on the Cardano Node and CLI as an end user, stake pool operator, and governance actor, see the [Cardano Node Course](https://www.youtube.com/playlist?list=PLNEK_Ejlx3x2ut-Pq-hi0NFVsgKB3EddR). ::: ## Hardware requirements | Network | CPU Cores | Free RAM | Free storage | | :---: | :---: | :---: | :---: | | Mainnet | 2 | 24GB | 300GB minimum (500GB+ recommended — chain grows over time) | | Testnet | 2 | 4GB | 20GB | Stake pool block producers should run Linux. The node runs on macOS and Windows but those platforms are not used in production. ## Installation Choose whichever method fits your environment. For block producers, building from source lets you verify the binary matches the code. ---
Release binaries — quickest, no build required Each [GitHub release](https://github.com/IntersectMBO/cardano-node/releases) ships statically-linked tarballs for Linux amd64 and arm64, built by the same Nix musl pipeline as the Nix build below. ```bash VERSION=11.0.1 # check releases page for latest wget https://github.com/IntersectMBO/cardano-node/releases/download/${VERSION}/cardano-node-${VERSION}-linux.tar.gz tar -xzf cardano-node-${VERSION}-linux.tar.gz -C ~/.local/ ``` The tarball unpacks into `bin/` and `share/` (configuration files for mainnet, preprod, and preview). Ensure `~/.local/bin` is on your `$PATH`. :::note Security consideration Pre-built binaries require trusting the build pipeline. For block producers holding hot keys, many operators prefer building from source so they can verify the binary themselves. The Nix build below produces the same static artifacts reproducibly. :::
---
Docker / GHCR images Container images for `cardano-node`, `cardano-tracer`, and `cardano-submit-api` are published to the GitHub Container Registry: ```bash VERSION=11.0.1 # check releases page for latest docker pull ghcr.io/intersectmbo/cardano-node:${VERSION} ``` See the [cardano-node packages page](https://github.com/IntersectMBO/cardano-node/pkgs/container/cardano-node) for all available tags. To build and load your own image from the upstream flake instead of pulling: ```bash VERSION=11.0.1 # check releases page for latest # Build the node image (outputs a tarball) nix build github:IntersectMBO/cardano-node/${VERSION}#dockerImage/node # Load it into Docker docker load -i result ``` :::warning Security consideration Docker images require trusting the build pipeline, the base image, and the container runtime. For block producers holding hot keys, many operators prefer building from source so they can verify the binary themselves — building the image yourself with `nix build` above produces a reproducible image from the same pipeline used for official releases. If you do run a containerised node, do not mount key files or any sensitive host paths into the container. :::
---
Build with Nix — recommended for operators who want a verified build If you don't have Nix installed, use the [Determinate Systems installer](https://determinate.systems/posts/determinate-nix-installer/) — it enables flakes by default and handles uninstallation cleanly. **Set up the IOG binary cache before building.** Without it, Nix will compile GHC and all Haskell dependencies from scratch, which can take many hours. Follow the [IOGX Nix setup guide](https://github.com/input-output-hk/iogx/blob/main/doc/nix-setup-guide.md). Build the statically-linked musl release tarball directly from the upstream flake — no clone needed: **x86_64 (amd64):** ```bash VERSION=11.0.1 # check releases page for latest nix build github:IntersectMBO/cardano-node/${VERSION}#hydraJobs.x86_64-linux.musl.cardano-node-linux ``` **aarch64 (arm64):** ```bash VERSION=11.0.1 # check releases page for latest nix build github:IntersectMBO/cardano-node/${VERSION}#hydraJobs.aarch64-linux.musl.cardano-node-linux ``` Replace `11.0.1` with the version you want. `result/` will contain a tarball with the same layout as the release binaries — extract it the same way: ```bash tar -xzf result/*.tar.gz -C ~/.local/ ``` ### NixOS deployments The flake exposes a `nixosModules.cardano-node` output for managing the node declaratively as a systemd service with all configuration in Nix. See [nix/nixos/cardano-node-service.nix](https://github.com/IntersectMBO/cardano-node/blob/master/nix/nixos/cardano-node-service.nix) for the available module options.
---
Build with GHCup / cabal — for systems without Nix Building with cabal requires manually installing several C libraries that Nix would otherwise handle. Use the Nix method unless you have a specific reason not to. ### System libraries Check the [cardano-node repository](https://github.com/IntersectMBO/cardano-node) for the GHC and cabal versions required by the release you're building. For **11.0.1**: GHC `9.6.7`, cabal `3.12.1.0`. ```bash sudo apt-get update -y sudo apt-get install automake build-essential pkg-config libffi-dev libgmp-dev libssl-dev libncurses-dev libsystemd-dev zlib1g-dev make g++ tmux git jq wget libtool autoconf liblmdb-dev libsnappy-dev protobuf-compiler liburing-dev -y ``` ```bash sudo yum update -y sudo yum install git gcc gcc-c++ tmux gmp-devel make tar xz wget zlib-devel libtool autoconf liburing-devel snappy-devel protobuf-compiler systemd-devel ncurses-devel ncurses-compat-libs which jq openssl-devel lmdb-devel -y ``` Install [Xcode Command Line Tools](https://developer.apple.com/xcode/features/) if you haven't already: ```bash xcode-select --install ``` Install [Homebrew](https://brew.sh), then: ```bash brew install jq libtool autoconf automake pkg-config openssl lmdb snappy protobuf ``` On Apple Silicon, also install LLVM (used by GHC as a backend): ```bash brew install llvm ``` :::caution macOS OpenSSL location Homebrew installs OpenSSL in a non-standard location. If you see `setup: Can't find OpenSSL library` when building, add these symlinks: ```bash sudo mkdir -p /usr/local/opt/openssl sudo ln -s /opt/homebrew/opt/openssl@3/lib /usr/local/opt/openssl/lib sudo ln -s /opt/homebrew/opt/openssl@3/include /usr/local/opt/openssl/include ``` ::: :::caution Windows instructions may fall out of date. If something is off, please submit a PR. ::: Install Git via [Chocolatey](https://community.chocolatey.org/) (`choco install git`) or [Scoop](https://scoop.sh) (`scoop install git`). Avoid Winget — it installs Git for Windows which runs in a separate environment from MSYS2 and causes confusion. GHCup can install an MSYS2 environment automatically. Run this in PowerShell: ```powershell Set-ExecutionPolicy Bypass -Scope Process -Force;[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; try { & ([ScriptBlock]::Create((Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -Interactive -DisableCurl -ExistingMsys2Dir C:\msys64 -Msys2Env CLANG64 } catch { Write-Error $_ } ``` Then install these packages in MSYS2 (prefix with `ghcup run --mingw-path --` if using GHCup's MSYS2): ```console pacman -S autoconf autotools ca-certificates mingw-w64-clang-x86_64-toolchain mingw-w64-clang-x86_64-gmp mingw-w64-clang-x86_64-libtool mingw-w64-clang-x86_64-libffi mingw-w64-clang-x86_64-openssl mingw-w64-clang-x86_64-zlib mingw-w64-clang-x86_64-lmdb ``` :::info Pre-built C libraries for Windows Downloading pre-built `sodium`, `secp256k1`, and `blst` from iohk-nix releases (as done in the [base CI action](https://github.com/input-output-hk/actions/blob/latest/base/action.yml)) is easier than building them yourself on Windows. Set these in your shell profile after downloading: ```bash export PKG_CONFIG_PATH=/mingw64/opt/cardano/lib/pkgconfig:$PKG_CONFIG_PATH export LD_LIBRARY_PATH=/mingw64/opt/cardano/bin:$LD_LIBRARY_PATH export PATH=/mingw64/opt/cardano/bin:$PATH ``` ::: If you hit this linker error during `cabal build`: ``` ld.lld: error: undefined symbol: __local_stdio_printf_options ``` Comment out `extra-lib-dirs` and `extra-include-dirs` in `~/AppData/Roaming/cabal/config`. See [this issue](https://github.com/haskell/process/issues/340). ### Installing GHCup, GHC, and cabal Install [GHCup](https://www.haskell.org/ghcup/) using its installer, then install the required toolchain versions: ```bash ghcup install ghc 9.6.7 --set ghcup install cabal 3.12.1.0 --set ``` Verify you're using the GHCup-managed tools (not a system installation): ```bash which cabal # should return /home//.ghcup/bin/cabal ``` ### C library dependencies Cardano requires specific versions of `sodium`, `secp256k1`, and `blst`. Determine the correct versions from the node's own lock file: ```bash CARDANO_NODE_VERSION='11.0.1' IOHKNIX_VERSION=$(curl -s https://raw.githubusercontent.com/IntersectMBO/cardano-node/$CARDANO_NODE_VERSION/flake.lock | jq -r '.nodes.iohkNix.locked.rev') ``` :::caution These three libraries must match the versions pinned in `iohkNix` for the specific node release. Wrong versions cause cryptographic failures at runtime. ::: Create a working directory and build each library: ```bash mkdir -p ~/src cd ~/src ``` **sodium** (Cardano uses a custom fork with additional cryptographic functions): ```bash SODIUM_VERSION=$(curl -s https://raw.githubusercontent.com/input-output-hk/iohk-nix/$IOHKNIX_VERSION/flake.lock | jq -r '.nodes.sodium.original.rev') git clone https://github.com/intersectmbo/libsodium cd libsodium && git checkout $SODIUM_VERSION ./autogen.sh && ./configure make && sudo make install cd ~/src ``` **secp256k1**: ```bash SECP256K1_VERSION=$(curl -s https://raw.githubusercontent.com/input-output-hk/iohk-nix/$IOHKNIX_VERSION/flake.lock | jq -r '.nodes.secp256k1.original.ref') git clone --depth 1 --branch ${SECP256K1_VERSION} https://github.com/bitcoin-core/secp256k1 cd secp256k1 ./autogen.sh && ./configure --enable-module-schnorrsig --enable-experimental make && sudo make install cd ~/src ``` **blst**: ```bash BLST_VERSION=$(curl -s https://raw.githubusercontent.com/input-output-hk/iohk-nix/$IOHKNIX_VERSION/flake.lock | jq -r '.nodes.blst.original.ref') git clone --depth 1 --branch ${BLST_VERSION} https://github.com/supranational/blst cd blst && ./build.sh cat > libblst.pc << EOF prefix=/usr/local exec_prefix=\${prefix} libdir=\${exec_prefix}/lib includedir=\${prefix}/include Name: libblst Description: Multilingual BLS12-381 signature library URL: https://github.com/supranational/blst Version: ${BLST_VERSION#v} Cflags: -I\${includedir} Libs: -L\${libdir} -lblst EOF sudo cp libblst.pc /usr/local/lib/pkgconfig/ sudo cp bindings/blst_aux.h bindings/blst.h bindings/blst.hpp /usr/local/include/ sudo cp libblst.a /usr/local/lib sudo chmod u=rw,go=r /usr/local/{lib/{libblst.a,pkgconfig/libblst.pc},include/{blst.{h,hpp},blst_aux.h}} cd ~/src ``` Add the library paths to your shell profile (`~/.bashrc` or `~/.zshrc`) and reload it: ```bash export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH" export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" ``` :::tip Dynamic linker On some distributions the node binary links against the right `libsodium.so` but the dynamic linker loads the wrong one at runtime. If you suspect this, check with `pldd` on the running process — if it shows the wrong library path, run `ldconfig`. ::: ### Building the node ```bash VERSION=11.0.1 # check releases page for latest cd ~/src git clone https://github.com/intersectmbo/cardano-node.git cd cardano-node git switch -d tags/${VERSION} ``` Pin the GHC version to avoid accidentally using a system-installed GHC: ```bash echo "with-compiler: ghc-9.6.7" >> cabal.project.local ``` On Apple Silicon, add these options before building: ```bash echo "package trace-dispatcher" >> cabal.project.local echo " ghc-options: -Wwarn" >> cabal.project.local echo "" >> cabal.project.local echo "package HsOpenSSL" >> cabal.project.local echo " flags: -homebrew-openssl" >> cabal.project.local echo "" >> cabal.project.local ``` Build: ```bash cabal update cabal build exe:cardano-node cardano-cli ``` Copy the built binaries to your `$PATH`: ```bash mkdir -p ~/.local/bin cp -p "$(cabal list-bin cardano-node)" ~/.local/bin/ cp -p "$(cabal list-bin cardano-cli)" ~/.local/bin/ ``` We copy rather than use `cabal install` because `cabal install` strips the git revision from the binary, breaking `cardano-node --version` output. Verify: ```bash cardano-node --version cardano-cli --version ``` :::note Ledger state snapshots on upgrade If the ledger serialization format changed between versions, the node will delete snapshots in `db/ledger/` on first startup. Back those up before upgrading if you want to be able to roll back. :::
--- ## How to run cardano-node This guide covers running `cardano-node` as a passive (non-block-producing) node and querying the chain with `cardano-cli`. If you haven't installed the node yet, see [Installing cardano-node](/docs/operators/node/installing-cardano-node) first. For running a stake pool, see [Stake Pool Operation](/docs/operators/). ## Networks and configuration files Cardano runs on three public networks. See [Networks](/docs/developers/curriculum/start-building/networks-and-test-ada) for a full description of each. Download the configuration files for the network you want to run: **Mainnet** (NetworkMagic: `764824073`) ```bash curl -O -J "https://book.play.dev.cardano.org/environments/mainnet/{config,db-sync-config,submit-api-config,topology,byron-genesis,shelley-genesis,alonzo-genesis,conway-genesis,checkpoints}.json" ``` **Preprod testnet** (NetworkMagic: `1`) ```bash curl -O -J "https://book.play.dev.cardano.org/environments/preprod/{config,db-sync-config,submit-api-config,topology,byron-genesis,shelley-genesis,alonzo-genesis,conway-genesis}.json" ``` **Preview testnet** (NetworkMagic: `2`) ```bash curl -O -J "https://book.play.dev.cardano.org/environments/preview/{config,db-sync-config,submit-api-config,topology,byron-genesis,shelley-genesis,alonzo-genesis,conway-genesis}.json" ``` All current environment configurations are listed at [book.play.dev.cardano.org/environments.html](https://book.play.dev.cardano.org/environments.html). ## Bootstrap with Mithril Syncing from genesis takes over 24 hours on mainnet. [Mithril](https://mithril.network/doc/) provides stake-certified snapshots that get a node synced in under 30 minutes. For the full guide see [Bootstrap a Cardano node](https://mithril.network/doc/manual/getting-started/bootstrap-cardano-node) in the Mithril documentation. Install the Mithril client: ```bash curl --proto '=https' --tlsv1.2 -sSfL \ https://raw.githubusercontent.com/IntersectMBO/mithril/refs/heads/main/mithril-install.sh \ | sh -s -- -c mithril-client -d latest -p $HOME/.local/bin ``` Set environment variables for mainnet (see [network configurations](https://mithril.network/doc/manual/getting-started/network-configurations) for preprod/preview): ```bash export CARDANO_NETWORK=mainnet export AGGREGATOR_ENDPOINT=https://aggregator.release-mainnet.api.mithril.network/aggregator export GENESIS_VERIFICATION_KEY=$(wget -q -O - \ https://raw.githubusercontent.com/IntersectMBO/mithril/main/mithril-infra/configuration/release-mainnet/genesis.vkey) export ANCILLARY_VERIFICATION_KEY=$(wget -q -O - \ https://raw.githubusercontent.com/IntersectMBO/mithril/main/mithril-infra/configuration/release-mainnet/ancillary.vkey) ``` Download and verify the snapshot: ```bash mithril-client cardano-db download latest --include-ancillary ``` This unpacks a certified database into `db/`. Point `--database-path` at it when starting the node and it will sync only the few minutes of blocks produced since the snapshot. ## Running the node Create a directory for your chosen network and put the configuration files there: ```bash mkdir -p $HOME/cardano/mainnet/db cd $HOME/cardano/mainnet # download config files here (see Networks section above) ``` ``` $HOME/cardano/mainnet/ ├── db/ ├── config.json ├── topology.json ├── byron-genesis.json ├── shelley-genesis.json ├── alonzo-genesis.json └── conway-genesis.json ``` Start the node: ```bash cardano-node run \ --config $HOME/cardano/mainnet/config.json \ --database-path $HOME/cardano/mainnet/db \ --socket-path $HOME/cardano/mainnet/db/node.socket \ --host-addr 0.0.0.0 \ --port 3001 \ --topology $HOME/cardano/mainnet/topology.json ``` | Flag | Description | |------|-------------| | `--config` | Main config file; references the genesis files in the same directory | | `--database-path` | Directory where chain data is stored | | `--socket-path` | Unix socket for IPC with `cardano-cli`, wallets, and other tools | | `--host-addr` | IP to listen on; `0.0.0.0` accepts connections on all interfaces | | `--port` | Port to listen on (3001 is conventional) | | `--topology` | Peer topology file | For the full list of options run `cardano-node run --help`. Block producers can pass `--start-as-non-producing-node` alongside their credential flags to start without minting blocks immediately. Sending `SIGHUP` later (`pkill -HUP cardano-node`) triggers the node to read the credential files and begin forging. This is useful for bringing up a standby block producer safely before cutting over to it. ## Running as a systemd service For production use, run `cardano-node` under systemd so it restarts automatically on failure or reboot. Create a dedicated system user and directories: ```bash sudo useradd -r -m -d /var/lib/cardano -s /sbin/nologin cardano sudo mkdir -p /etc/cardano /var/lib/cardano/db sudo cp config.json topology.json *-genesis.json /etc/cardano/ sudo chown -R cardano:cardano /etc/cardano /var/lib/cardano ``` If you will be receiving credentials (KES key, VRF key, op cert) encrypted with [age](https://github.com/FiloSottile/age), generate the server's key pair now and keep the public key handy for your air-gapped machine: ```bash sudo install -d -m 700 /root/.age sudo age-keygen -o /root/.age/key.txt # public key is printed to stdout — copy it ``` Create `/etc/systemd/system/cardano-node.service`. The `[Unit]`, `[Service]` boilerplate, and `[Install]` section are the same for both roles — only `ExecStart` differs:
Relay node See [Relay Configuration](/docs/operators/relay-configuration/relay-node-configuration) for the topology file. ```ini [Unit] Description=Cardano Node Wants=network-online.target After=network-online.target [Service] User=cardano Group=cardano Type=simple WorkingDirectory=/var/lib/cardano ExecStart=/usr/local/bin/cardano-node run \ --config /etc/cardano/config.json \ --topology /etc/cardano/topology.json \ --database-path /var/lib/cardano/db \ --socket-path /run/cardano/node.socket \ --host-addr 0.0.0.0 \ --port 3001 ExecReload=pkill -HUP cardano-node KillSignal=SIGINT RestartKillSignal=SIGINT TimeoutStopSec=300 LimitNOFILE=131072 Restart=always RestartSec=5 SyslogIdentifier=cardano-node RuntimeDirectory=cardano RuntimeDirectoryMode=0750 [Install] WantedBy=multi-user.target ```
Block producer Complete [Key Generation](/docs/operators/block-producer/block-producer-keys) and [Deployment](/docs/operators/block-producer/deployment) first to generate and transfer credentials. See [Deployment](/docs/operators/block-producer/deployment) for the block producer topology file. ```ini [Unit] Description=Cardano Node Wants=network-online.target After=network-online.target [Service] User=cardano Group=cardano Type=simple WorkingDirectory=/var/lib/cardano ExecStart=/usr/local/bin/cardano-node run \ --config /etc/cardano/config.json \ --topology /etc/cardano/topology.json \ --database-path /var/lib/cardano/db \ --socket-path /run/cardano/node.socket \ --host-addr 0.0.0.0 \ --port 6000 \ --shelley-kes-key /run/secrets/kes.skey \ --shelley-vrf-key /run/secrets/vrf.skey \ --shelley-operational-certificate /run/secrets/node.cert ExecReload=pkill -HUP cardano-node KillSignal=SIGINT RestartKillSignal=SIGINT TimeoutStopSec=300 LimitNOFILE=131072 Restart=always RestartSec=5 SyslogIdentifier=cardano-node RuntimeDirectory=cardano RuntimeDirectoryMode=0750 [Install] WantedBy=multi-user.target ``` To start without forging blocks immediately (useful when cutting over a standby node), add `--start-as-non-producing-node` to the `ExecStart` line and send `SIGHUP` when ready to activate: `pkill -HUP cardano-node`. If using the KES agent, replace `--shelley-kes-key /run/secrets/kes.skey` with `--shelley-kes-agent-socket /run/kes-agent/service.socket`.
`RuntimeDirectory=cardano` creates `/run/cardano` at startup and cleans it up on stop, so the socket path is always valid. Enable and start: ```bash sudo systemctl daemon-reload sudo systemctl enable --now cardano-node.service ``` To allow your own user to query the node via the socket, add yourself to the `cardano` group: ```bash sudo usermod -aG cardano $USER # log out and back in to pick up the group export CARDANO_NODE_SOCKET_PATH=/run/cardano/node.socket ``` ## Querying the node `cardano-cli` and other tools locate the node socket via `CARDANO_NODE_SOCKET_PATH`. Setting `CARDANO_NODE_NETWORK_ID` removes the need to pass `--mainnet` or `--testnet-magic` on every command — it handles both mainnet and testnet magic automatically. Add both to your shell profile: ```bash export CARDANO_NODE_SOCKET_PATH=/run/cardano/node.socket # adjust if not using the systemd setup above export CARDANO_NODE_NETWORK_ID=mainnet # or 1 for preprod, 2 for preview ``` Query the current chain tip to verify the node is running and check sync progress: ```bash cardano-cli query tip ``` ```json { "block": 11142430, "epoch": 574, "era": "Conway", "hash": "a9e4413a38aaec6ef89f8a687a58acd01a7e73675d79e9f418f6c41d2e2a7b53", "slot": 49630712, "syncProgress": "100.00" } ``` :::important Do not submit transactions until `syncProgress` is `"100.00"`. ::: Cross-reference the block number against a [public explorer](/docs/developers/curriculum/start-building/networks-and-test-ada).
Advanced — RTS options `cardano-node` is a Haskell program and exposes the GHC runtime system (RTS) for tuning. The IOG-released binaries ship with these defaults compiled in: ``` -T -I0 -A16m -N2 --disable-delayed-os-memory-return ``` | Flag | Effect | |------|--------| | `-T` | Collect GC statistics (accessible via `GHC.Stats`; no output by itself) | | `-I0` | Disable idle GC | | `-A16m` | Allocation area size for the generational GC | | `-N2` | Use 2 OS threads for parallel execution | | `--disable-delayed-os-memory-return` | Return memory to the OS immediately, so RSS in `top`/`htop` reflects actual usage | You can extend or override these at runtime by appending `+RTS ... -RTS` to the node command: ```bash cardano-node run +RTS -N4 -A64m -RTS \ --config /etc/cardano/config.json \ ... ``` Runtime-supplied flags are merged with the compiled-in defaults; where they conflict, the runtime flag wins. **Practical notes for stake pool operators:** - `-N` should not exceed the number of physical cores available to the node process. On a dedicated 4-core machine, `-N4` is reasonable; going higher adds scheduler overhead without benefit. - Increasing `-A` (e.g. `-A64m`) reduces GC frequency at the cost of higher peak memory. Useful on machines with ample RAM. - The non-moving GC (`--nonmoving-gc`) can reduce GC pause times at the cost of higher overall memory use. Worth testing on machines with 32 GB+ RAM. For the full list of available flags run `cardano-node +RTS -? -RTS`, or see the [GHC RTS documentation](https://downloads.haskell.org/ghc/latest/docs/users_guide/runtime_control.html).
--- ## Topology The topology file tells `cardano-node` where to find peers. It specifies local roots (peers to always stay connected to), a syncing strategy (bootstrap peers or a Genesis snapshot), public roots as fallbacks, and when to switch to ledger-based peer discovery. ## Topology file reference A complete topology file for mainnet using bootstrap peers (Praos mode, recommended): ```json { "localRoots": [ { "accessPoints": [ { "address": "x.x.x.x", "port": 3001 } ], "advertise": false, "hotValency": 1, "warmValency": 2, "trustable": false, "behindFirewall": false, "diffusionMode": "InitiatorAndResponder" } ], "bootstrapPeers": [ { "address": "backbone.cardano.iog.io", "port": 3001 }, { "address": "backbone.mainnet.emurgornd.com", "port": 3001 }, { "address": "backbone.mainnet.cardanofoundation.org", "port": 3001 } ], "publicRoots": [ { "accessPoints": [ { "address": "y.y.y.y", "port": 3002 } ], "advertise": false } ], "useLedgerAfterSlot": 128908821 } ``` For Genesis mode, replace `bootstrapPeers` with `peerSnapshotFile` — see [Ouroboros Genesis](#ouroboros-genesis) below. ### Local roots Local roots are peers the node **always** keeps as hot or warm connections — typically your own relays (for the block producer) or your block producer (for relays). These connections are private and not advertised to the network. | Field | Description | |-------|-------------| | `hotValency` | Number of hot (active) connections to maintain from this group. The deprecated `valency` field is an alias. | | `warmValency` | Number of warm connections to maintain. Defaults to `hotValency`. Recommend `hotValency + 1` so there is always a ready backup for promotion. | | `advertise` | Whether to share this peer's address via peer sharing. Set `false` for your block producer. | | `trustable` | Marks this group as a trusted source when bootstrap peers are enabled. Default `false`. | | `behindFirewall` | If `true`, the node will not initiate connections to these peers — they must connect in. Available since `cardano-node 10.7`. Default `false`. | | `diffusionMode` | `"InitiatorAndResponder"` (default) or `"InitiatorOnly"`. Available since `cardano-node 10.2`. Overrides `DiffusionMode` in `config.json` for peers in this group only. | :::tip Block producer topology Your block producer must connect **only** to your own relays. Set `"useLedgerAfterSlot": -1` and `"bootstrapPeers": null` in its topology to disable all outbound peer discovery. Its `localRoots` should list only your relays. ::: **Reloading topology without restarting:** send `SIGHUP` to the node process: ```bash pkill -HUP cardano-node ``` This re-reads the topology file, restarts DNS resolution, and re-fetches block forging credential paths. If credential files are missing after the reload, block forging is disabled until the files are present. ### Ledger peers and public roots `useLedgerAfterSlot` controls when the node switches to discovering peers from the ledger stake distribution. Before that slot it uses `publicRoots` (or `bootstrapPeers`, if configured). Set to `-1` to disable ledger peer discovery entirely. `publicRoots` are fallback peers used before `useLedgerAfterSlot` is reached or when ledger peers are unavailable. **Big ledger peers** are the subset of ledger peers whose pools collectively hold 90% of total stake. They are used preferentially during Genesis sync due to their stronger economic incentive to remain honest. ## Syncing strategy ### Bootstrap peers — Praos mode (recommended for mainnet) Praos is the default consensus mode. In Praos mode, the node uses a fixed list of trusted relays from the founding organizations to sync before it has enough chain state to discover peers from the ledger on its own. This is the `bootstrapPeers` list. ```json "bootstrapPeers": [ { "address": "backbone.cardano.iog.io", "port": 3001 }, { "address": "backbone.mainnet.emurgornd.com", "port": 3001 }, { "address": "backbone.mainnet.cardanofoundation.org", "port": 3001 } ] ``` Set `"bootstrapPeers": null` to disable. When enabled, the node requires at least one trustable peer source — either a non-empty `bootstrapPeers` list or a local root group with `"trustable": true` — or it will refuse to start. The node traces two sync states: - **`TooOld`** — the node's chain is more than 20 minutes behind. The node disconnects from all non-trusted peers and syncs only from bootstrap peers and trustable local roots. - **`YoungEnough`** — the node is caught up and connects to the wider network normally. ### Ouroboros Genesis — trustless sync (experimental) {#ouroboros-genesis} Ouroboros Genesis is a trustless syncing protocol that supersedes bootstrap peers. It is available as an experimental feature from `cardano-node 10.2`, disabled by default, and is expected to become the mainnet default in a future release. To enable Genesis mode, set in `config.json`: ```json "ConsensusMode": "GenesisMode" ``` Genesis mode is incompatible with bootstrap peers. When enabled it overrides the `bootstrapPeers` setting. Replace `bootstrapPeers` in your topology with a peer snapshot file: ```json "peerSnapshotFile": "path/to/big-ledger-peer-snapshot.json" ``` Generate a snapshot from a fully synced node: ```bash cardano-cli query ledger-peer-snapshot --out-file big-ledger-peer-snapshot.json ``` The snapshot contains big ledger peers at a specific slot. The node ignores the file once its own ledger state is more recent, so it is not strictly required for ongoing operation — but it should be refreshed periodically as part of regular maintenance. :::warning Genesis bug in 10.2–10.4 A bug in releases 10.2, 10.3, and 10.4 makes caught-up nodes susceptible to an eclipse attack when Genesis mode is enabled. If you are running one of those versions with Genesis enabled, disable Genesis (`ConsensusMode: PraosMode` or remove the field) and restart once the node finishes syncing. ::: ## Peer connection targets Peer targets are set in `config.json`, not in the topology file. The defaults are: ```json { "TargetNumberOfRootPeers": 60, "TargetNumberOfActivePeers": 15, "TargetNumberOfEstablishedPeers": 40, "TargetNumberOfKnownPeers": 85, "TargetNumberOfActiveBigLedgerPeers": 5, "TargetNumberOfEstablishedBigLedgerPeers": 10, "TargetNumberOfKnownBigLedgerPeers": 15 } ``` These **deadline targets** apply when the node considers itself caught up. In Praos mode they are always in effect; in Genesis mode they apply once the node has synced. **Constraint:** `known >= established >= active >= 0` must hold for both the regular and big-ledger sets, or the node will fail to start. | Target | Description | |--------|-------------| | `TargetNumberOfActivePeers` | Hot connections to local roots, ledger/public root peers, and peer-sharing peers (excludes big ledger peers). Should be at least the number of hot local roots configured in the topology file. | | `TargetNumberOfEstablishedPeers` | Warm + hot connections (same peer set as above). | | `TargetNumberOfKnownPeers` | Cold + warm + hot connections (same peer set). | | `TargetNumberOfActive/Established/KnownBigLedgerPeers` | Same three tiers, but for big ledger peers specifically. | | `TargetNumberOfRootPeers` | Minimum known peers filled from local roots and ledger/public roots before peer sharing is used to fill the rest. | When using bootstrap peers, all targets must be large enough to accommodate the full bootstrap peer list. ### Peer sharing Peer sharing lets nodes exchange peer addresses with each other, helping the network self-heal and fill peer slots without relying solely on ledger-registered nodes. Enable it in `config.json`: ```json "PeerSharing": true ``` Peer sharing has two levels of permission before an address is disclosed: - **Node level** — `PeerSharing: true` must be set, or the node will not respond to peer requests at all. - **Per-peer level** — `advertise: true` must be set on a local root entry for that peer's address to be shareable. The remote node must also consent during the handshake. Both conditions must hold for an address to be shared. Set `advertise: false` for your block producer and any private infrastructure. :::note Peer sharing is an organic discovery mechanism on top of ledger peers and local roots — it is not a replacement for them. If your node has spare peer capacity after filling ledger and root peers, peer sharing fills the remainder up to `TargetNumberOfKnownPeers`. ::: For good block propagation, relays benefit from connections to peers distributed globally. Consider [arranging reciprocal local root connections](https://forum.cardano.org/c/staking-delegation/156) with operators in under-represented regions (South America, Asia Pacific). Do **not** mark these peers as `trustable` — that designation is only for your own infrastructure. ### Genesis sync targets These apply automatically in Genesis mode when the local ledger state is detected to be out of date: ```json { "SyncTargetNumberOfActivePeers": 0, "SyncTargetNumberOfActiveBigLedgerPeers": 30, "SyncTargetNumberOfEstablishedBigLedgerPeers": 50, "SyncTargetNumberOfKnownBigLedgerPeers": 100, "MinBigLedgerPeersForTrustedState": 5 } ``` During sync, the node bulk-downloads and validates blocks from big ledger peers. `SyncTargetNumberOfActiveBigLedgerPeers` should not be a small number — Ouroboros Genesis guarantees convergence to the honest chain as long as at least one active peer is honest. If active big ledger peers drop below `MinBigLedgerPeersForTrustedState`, the node pauses until enough connections are re-established. The sync targets must independently satisfy `known >= established >= active >= 0`. Additionally, `SyncTargetNumberOfActivePeers` must not exceed `TargetNumberOfEstablishedPeers` from the deadline set. Once the node deems itself caught up, it transitions back to the deadline targets. :::note Small or private networks The sync targets above are mainnet-scale. On a small or private network (for example a testnet with a handful of relays) they cannot be met, and the node stays in `PreSyncing`: `MinBigLedgerPeersForTrustedState` (default 5) is never reached, so the node never enters the trusted state that releases sync. Lower `MinBigLedgerPeersForTrustedState` to at most the number of big ledger peers the network has, and lower the big-ledger sync targets to match. Lowering `SyncTargetNumberOfActiveBigLedgerPeers` alone is not enough: `MinBigLedgerPeersForTrustedState` is the gate. ```json { "MinBigLedgerPeersForTrustedState": 1, "SyncTargetNumberOfActiveBigLedgerPeers": 1, "SyncTargetNumberOfEstablishedBigLedgerPeers": 1, "SyncTargetNumberOfKnownBigLedgerPeers": 1 } ``` This weakens the Genesis guarantee, which relies on a larger active big-ledger set, so use it only on networks you control. Keep `known >= established >= active >= 0`, and raise the values to match your real peer pool. ::: ### Binding addresses and IPv6 reachability `--host-addr` and `--host-ipv6-addr` are command-line options, not `config.json` fields: ```bash cardano-node run --host-addr 0.0.0.0 --host-ipv6-addr :: ``` They set the local addresses the node binds its listening sockets to. `--host-ipv6-addr` also selects the DNS lookup family: without it the node resolves only A records, so peers given as domain names never yield IPv6 addresses. Neither option gates outbound connections to literal IPv6 addresses. If a Genesis peer snapshot lists a peer by its IPv6 address, the node dials it whether or not `--host-ipv6-addr` is set. On a host with no IPv6 route that dial fails with `Network is unreachable`. :::tip Diagnostic Repeated `Net.ConnectionManager.Remote.ConnectError` against IPv6 addresses, while every `HandshakeSuccess` is IPv4, means the host has no IPv6 route to those peers. Give the host an IPv6 route, or remove the IPv6 peers from the snapshot. ::: --- ## Calidus Pool Keys Calidus keys (from Latin *calidus*, "hot") are Ed25519 key pairs that stake pool operators register on-chain to act on behalf of their pool. Once registered with a one-time cold-key signature, the Calidus key can be used for authentication, governance tool interaction, and dApp signing — **without ever touching the cold key again**. They are defined in [CIP-88 v2](https://cips.cardano.org/cip/CIP-0088) and [CIP-151](https://cips.cardano.org/cip/CIP-0151). ## Why they matter Before Calidus keys, SPOs had two bad options when a governance platform or service needed to verify their identity: 1. Sign something with the cold key — dangerous, defeats the purpose of keeping it offline 2. Hope the service had some other way to verify them — unreliable Calidus keys solve this cleanly. Register once with a cold-key signature, then use the Calidus key freely as a hot key. It is already supported by Koios, Blockfrost, CN-Tools, Cardanoscan, AdaStat, and Cexplorer. For governance specifically: when a voting platform or governance tool needs to confirm you are who you say you are as an SPO, it will look for a registered Calidus key rather than asking you to sign with your cold key. ## Generating the key pair Calidus key operations use [cardano-signer](https://github.com/gitmachtl/cardano-signer) (v1.34.0+): ```shell cardano-signer keygen \ --out-skey calidus.skey \ --out-vkey calidus.vkey ``` ## Registering on-chain Registration requires a one-time signature from your cold key — do this on your air-gapped machine: ```shell cardano-signer sign \ --cip88 \ --calidus-public-key calidus.vkey \ --secret-key cold.skey \ --out-file calidus-registration.json ``` Then submit the registration metadata in a transaction from your online machine. The registration can be updated (higher nonce supersedes the previous) or revoked (submit an all-zeroes key). ## After registration The Calidus key acts as a hot key for: - **Governance tool authentication** — prove your SPO identity on governance platforms and voting interfaces - **Explorer profiles** — update pool metadata and interact with Cardanoscan, Cexplorer, AdaStat - **API authentication** — authenticate with Koios, Blockfrost, and other SPO-aware APIs - **dApp signing** — compatible with CIP-30 light wallets and CIP-8 message signing, so hardware wallets and browser wallets can be used Keep `calidus.skey` secure — it represents your pool's online identity. Unlike your cold key, it can be kept on a relatively secured hot machine, but it should still be treated as sensitive. If compromised, rotate it by submitting a new registration with a higher nonce. ## Further reading - [CIP-88: SPO On-Chain Registration](https://cips.cardano.org/cip/CIP-0088) - [CIP-151: On-Chain Registration — Stake Pools](https://cips.cardano.org/cip/CIP-0151) - [cardano-signer on GitHub](https://github.com/gitmachtl/cardano-signer) - [Calidus Pool Keys announcement (Blockfrost blog)](https://blog.blockfrost.io/calidus-pool-keys/) - [SPO Governance — voting with your cold key](../../governance/spo-governance) --- ## Get Started with Guild Operators Tools ## Guild Operators Suite The Guild-Operators suite is a set of tools and scripts for setting up, managing, and monitoring Cardano stake pools, as well as managing tokens and keys. It's the outcome of a community collaborative effort by long-time active community members to make common chores for operators easier. We'll try to provide a fast run-through of the tools involved and a high-level overview of procedures to get you started since complete documentation for the suite is hosted [here][guild-website]. ### Tools #### CNTools CNTools is a swiss army knife for pool operators who want to make routine tasks easier. It's a menu-driven bash GUI application for creating and managing wallets, sending ada and tokens, and just about any pool function. In addition, the tool has been enhanced with new features and improvements since its initial release in July 2020, coinciding with the introduction of the Cardano Shelley MainNet. More information regarding CNTools can be found [here](https://cardano-community.github.io/guild-operators/Scripts/cntools/). ![img](./img/guild_cntools.png) #### gLiveView Guild LiveView, often known as gLiveView, is a local bash CLI monitoring utility with an easy-to-use interface for monitoring node status. It connects to the locally running node via the specified EKG/Prometheus node endpoints to collect and show node metrics, network information, and other information in real time. The program recognizes whether the node is being used as a relay or a block producer and adjusts the output accordingly. More information regarding gLiveView can be found [here](https://cardano-community.github.io/guild-operators/Scripts/gliveview/). ![img](./img/guild_gliveview.png) #### Topology Updater Topology Updater was built as a workaround to allow stake pool relays to auto-discover and pair with peers on the network. While P2P implementation was put on hold owing to other priorities, this script has become one of the most important tools for avoiding having to manually contact friends and request that individual nodes be included to topology files. More information about the tool may be found [here](https://cardano-community.github.io/guild-operators/Scripts/topologyupdater/). ![img](./img/guild_topologyupdater.png) #### Guild Network and Support for other networks Guild Network is a brief (60-minute epoch) network that functions similarly to the Cardano testnets but is entirely governed by the community. It's excellent for experimenting with things in the sandbox, as well as testing out viable features before releasing them to other networks. This network is already supported by all of the tools in the repo, including Mainnet, testnets, and staging. #### Others.. Other utility scripts on a lesser scale include creating core components from source for particular components, setting up environment pre-requisites, and so on. Starting here, you can read about specifics as you go across the homepage [here][guild-website]. :::note Please ensure to read the disclaimers on guild website before continuing ::: ### Setting Up Pre-Requisites.. For installing OS Packages, dependencies, setting up a [sample directory structure](https://cardano-community.github.io/guild-operators/basics/#folder-structure) used as an example template input (customisable) for guild tools, fetching of configuration, genesis artifacts, downloading tool scripts, etc , you can use the commands below. The script does have quite a few options (you can use `-h` to check any optional components/arguments you'd want to include). ``` bash mkdir "$HOME/tmp";cd "$HOME/tmp" curl -sS -o guild-deploy.sh https://raw.githubusercontent.com/cardano-community/guild-operators/master/scripts/cnode-helper-scripts/guild-deploy.sh chmod 755 guild-deploy.sh ./guild-deploy.sh -b master -n mainnet -s pdl . "$HOME"/.bashrc ``` :::note `-s pdl` installs (p)rerequisites for the operating system, (d)ownloads the precompiled binaries and also compiles and installs the IOG fork of (l)ibsodium. consider guild-deploy.sh --help for options when you update the system or want to install additional modules ::: ### Build of Node/DBSync components We assume you'd have already seen the guide [here](/docs/operators/node/installing-cardano-node). There are similar build scripts/instructions available for building different cardano-node, cardano-db-sync, offline-metadata-tools and setting up postgres+postgREST with dbsync) on guild documentations. You can navigate instructions for each of them [here](https://cardano-community.github.io/guild-operators/build/). The instructions will also deploy these as a systemd service, which is recommended to avoid manually managing services. ### Customise configuration Now that you've set-up your OS dependencies and built/installed node binaries, it's time for you to customise your configuration, paths, names, etc for your node. You can use [env](https://cardano-community.github.io/guild-operators/Scripts/env/) file to modify these. Each line contains default value, and a simple explanation about what variable means. ### Contributions/Feedback.. Issue/PRs are welcome on the [github repository][guild-github]. ### Support Requests We do have the [Koios Discussions telegram channel for support requests][koios-general-tg] , but note that we intend to have support channel only for baseline skillset highlighted on [homepage][guild-website]. [guild-github]: https://github.com/cardano-community/guild-operators [guild-website]: https://cardano-community.github.io/guild-operators [koios-general-tg]: https://t.me/CardanoKoios/1 --- ## Mithril: Fast Bootstrap & Light Clients Mithril is a **stake-based multi-signature protocol** that provides lightweight certification of Cardano blockchain data. Stake pool operators collectively sign snapshots of the chain state, and an aggregator combines those signatures into a certificate that anyone can verify cryptographically. The result: you can trust certified data backed by a large fraction of total stake **without running and syncing a full node yourself**. This unlocks two things that matter for scaling and production. ## Fast node bootstrap Syncing a `cardano-node` from genesis takes many hours to days. With Mithril, a node can **download and verify a certified database snapshot** and be ready in minutes instead. The verification is what makes this safe: the snapshot is only accepted if it matches a certificate signed by enough stake. This is the recommended way to stand up a node, see [installing cardano-node](/docs/operators/node/installing-cardano-node) and [self-hosting chain access](/docs/developers/curriculum/production/self-hosting). ## Trustless light clients Applications can verify chain data, **transactions** and **stake distributions**, with cryptographic proofs, instead of trusting an API to tell them the truth. The TypeScript client runs in the browser via WebAssembly, so even a web app can verify a transaction against a Mithril certificate without a backend node. ```javascript await initMithrilClient(); const client = new MithrilClient(AGGREGATOR_ENDPOINT, GENESIS_VERIFICATION_KEY, { unstable: true }); // fetch the latest certified stake distribution and verify its certificate chain const [latest] = await client.list_mithril_stake_distributions(); const distribution = await client.get_mithril_stake_distribution(latest.hash); const certificate = await client.get_mithril_certificate(distribution.certificate_hash); const verified = await client.verify_certificate_chain(certificate.hash); const message = await client.compute_mithril_stake_distribution_message(distribution); console.log("Verified:", await client.verify_message_match_certificate(message, verified)); ``` The same capabilities exist in Rust (with snapshot download for node bootstrap): ```rust let client = ClientBuilder::aggregator(AGGREGATOR_ENDPOINT, GENESIS_VERIFICATION_KEY).build()?; let proof = client.cardano_transaction().get_proofs(&[tx_hash]).await?; let verified = proof.verify()?; let certificate = client.certificate().verify_chain(&proof.certificate_hash).await?; ``` Full references: [Mithril TypeScript client](https://mithril.network/doc) and [Mithril Rust client](https://mithril.network/doc). ## How it works ```mermaid graph LR SPO[Stake pool operators\nrun Mithril signers] -->|sign state snapshots| AGG[Mithril aggregator] AGG -->|multi-signature certificate| CLIENT[Mithril client] CLIENT -->|verify certificate chain| APP[Your app / node] ``` Signers run alongside block producers, signing snapshots roughly every few minutes; the aggregator combines enough signatures (by stake) into a certificate; clients verify the certificate chain before trusting the data. Because the threshold is stake-weighted, forging a certificate would require controlling a large fraction of total stake. ## Run a signer (stake pool operators) If you operate a stake pool, running a Mithril signer contributes to the certification network. The signer is lightweight (under ~5% CPU, under ~200 MB RAM) and, in production, must route all traffic through a Mithril relay rather than connecting directly to the internet. See [Mithril signer configuration](/docs/operators/block-producer/mithril-signer-configuration). ## Next steps - [Self-hosting chain access](/docs/developers/curriculum/production/self-hosting): where fast bootstrap fits in your stack - [Mithril documentation](https://mithril.network/doc/): the full protocol, networks, and node references --- ## Operate a Stake Pool A Cardano stake pool is the infrastructure that produces blocks on behalf of delegators. Running one means operating live servers, managing sensitive cryptographic keys, and participating in network governance. It is not a set-and-forget task. This section walks you through the full lifecycle in order. Follow the steps sequentially the first time — each one builds on the last. ## The path | Step | What you'll do | |------|---------------| | [1. Before You Start](basics/hardware-requirements) | Understand the requirements, the networking model, the key types, and set up your air-gapped signing machine | | [2. Install](/docs/operators/node/installing-cardano-node) | Install `cardano-node` and `cardano-cli` | | [3. Configure](relay-configuration/relay-node-configuration) | Set up your relay and block producer topology, configure Mithril | | [4. Run](/docs/operators/node/running-cardano) | Start your nodes and verify they sync | | [5. Register Your Pool](block-producer/generating-wallet-keys) | Generate keys, register your stake address, submit your pool certificate | | [6. Monitor](monitoring/monitoring-overview) | Monitor node health, block production, and KES expiry | | [7. Security & Hardening](deployment-scenarios/hardening-server) | Harden your servers, secure your key workflow, audit your setup | | [8. Governance](governance/spo-governance) | Understand your role in on-chain governance and how to vote | ## What you're building A minimal stake pool has three machines: ```mermaid flowchart TD NET["Internet"] --> R1["Relay node 1public IP, accepts peer connections"] NET --> R2["Relay node 2public IP, for redundancy"] R1 --> BP["Block producerno public IP, connected only to your relays"] R2 --> BP AG["Air-gapped machinenever online, cold-key operations only"] ``` The block producer holds your hot KES and VRF keys and mints blocks. Your cold key — the one that authorizes pool registration and rotation — stays on the air-gapped machine and never touches any networked computer. ## Before you dive in A few things that catch new operators off guard: - **You need a second machine for cold key operations.** This is not optional. If your cold key is on an internet-connected machine, your pool is at risk. See [Air Gap Environment](/docs/operators/security/air-gap) for setup options. - **Test on a testnet first.** The [Preview or Pre-Production testnets](/docs/developers/curriculum/start-building/networks-and-test-ada) let you run through the full registration flow without spending real ADA. - **Pool registration costs a deposit.** Currently 500 ADA, returned when you retire the pool. - **KES keys must be rotated** before they expire (~90 days on mainnet). Missing rotation means your node stops minting blocks. ## Community resources - [Guild Operators](https://cardano-community.github.io/guild-operators) — CNTools, gLiveView, and extensive operator documentation - [CoinCashew SPO Guide](https://www.coincashew.com/coins/overview-ada/guide-how-to-build-a-haskell-stakepool-node) — detailed setup walkthrough - [Stake Pool Operator Scripts](https://github.com/gitmachtl/scripts) — step-by-step scripts for pool management - [SPO Telegram workgroup](https://t.me/CardanoStakePoolWorkgroup) — active community for operators - [Cardano Forum — SPO](https://forum.cardano.org/c/staking-delegation/156) — long-form discussions --- ## Relay Node Configuration A relay node accepts connections from the network and forwards blocks and transactions to your block producer. It has a public IP address; your block producer does not. Before continuing, complete [Installing cardano-node](/docs/operators/node/installing-cardano-node) and download your network's configuration files as described in [Running cardano-node](/docs/operators/node/running-cardano). :::tip Test on preprod first Run through the full setup on the [Pre-Production testnet](/docs/developers/curriculum/start-building/networks-and-test-ada) before touching mainnet. Swap `mainnet` for `preprod` in every path and URL below. ::: ## Relay topology Your relay connects outward to: - Your **block producer** — as a private `localRoots` entry, never advertised - **Bootstrap peers** — trusted relays from founding organizations, used for initial sync (Praos mode) - The **wider network** — via ledger peer discovery once synced Example relay topology for mainnet: ```json { "localRoots": [ { "accessPoints": [ { "address": "YOUR-BLOCK-PRODUCER-IP", "port": 6000 } ], "advertise": false, "hotValency": 1, "warmValency": 2, "trustable": false } ], "bootstrapPeers": [ { "address": "backbone.cardano.iog.io", "port": 3001 }, { "address": "backbone.mainnet.emurgornd.com", "port": 3001 }, { "address": "backbone.mainnet.cardanofoundation.org", "port": 3001 } ], "publicRoots": [ { "accessPoints": [ { "address": "relays-new.cardano-mainnet.iohk.io", "port": 3001 } ], "advertise": false } ], "useLedgerAfterSlot": 128908821 } ``` Key points: - **`advertise: false`** on the block producer entry — its address must never be shared with the network. - **`useLedgerAfterSlot`** should match the value in the official `topology.json` for your network. Do not set it to `-1` on a relay. - For preprod, use the bootstrap peers and `useLedgerAfterSlot` value from the [downloaded preprod topology](https://book.play.dev.cardano.org/environments/preprod/topology.json). See [Topology](/docs/operators/node/topology) for full field documentation, Genesis mode configuration, and peer connection targets. ## Mithril relay (optional, required for Mithril signing) :::note This section only applies if you are running a Mithril signer on your block producer to participate in Mithril snapshot certification. It is not required to operate a stake pool. ::: The Mithril relay is a Squid forward proxy that runs on the Cardano relay machine. It routes traffic between the Mithril signer on your block producer and the external Mithril aggregator, keeping the block producer isolated from the public internet. Key configuration points: - **Listening port** — `3132` is recommended - **Source restriction** — only the block producer's internal IP is allowed to connect - **Destination restriction** — only HTTPS traffic to `*.mithril.network` is permitted - **Header anonymization** — request headers are stripped to avoid disclosing information about the block producer - **Caching** — disabled; the proxy only forwards traffic After setting up the proxy, point the Mithril signer on the block producer at it by setting `RELAY_ENDPOINT=http://:3132` in the signer's environment. For the complete setup including build commands, Squid configuration, and the systemd service unit, see [Set up the Mithril relay node](https://mithril.network/doc/manual/operate/run-signer-node/#set-up-the-mithril-relay-node) in the Mithril documentation. **Further reading:** - [Become a Mithril SPO](https://mithril.network/doc/manual/operate/become-mithril-spo) - [Run a Mithril signer node](https://mithril.network/doc/manual/operate/run-signer-node) --- ## Air Gap Environment An air-gapped machine is one that has never made a network connection and never will. Cold keys — pool registration keys, Constitutional Committee signing keys, any key that authorizes high-value operations — must be handled on a machine that meets this bar. If your cold key ever touches an internet-connected machine, it should be considered compromised. You have three paths to an air-gapped environment:
cardano-airgap — Nix bootable ISO (recommended) [cardano-airgap](https://github.com/IntersectMBO/cardano-airgap) is an IntersectMBO-maintained, Nix-built bootable ISO designed for air-gapped Cardano operations. It is already in use by many SPOs and Constitutional Committee members. ## Why use it The fundamental requirement for cold key operations is that the machine handling your keys has **never touched the internet** — not during setup, not during updates, not ever. Rolling this yourself (installing Ubuntu, patching it, installing Cardano tooling) means the machine is online for at least some of that time. `cardano-airgap` eliminates that window entirely: - Built with Nix: the entire system is defined declaratively and built offline. The ISO you boot has never made a network request - Ships with all necessary Cardano tooling pre-installed (`cardano-cli`, key generation utilities, etc.) - Read-only by design: no persistent state that could be contaminated between sessions - Auditable: the full build is reproducible from the public source ## Who should use it - **SPOs** signing pool registration certificates, voting transactions, or any operation requiring the cold key - **Constitutional Committee members** authorizing hot key credentials or voting - Any operator handling high-value keys who needs a clean, verifiable environment ## Getting started Download the latest ISO release from the [cardano-airgap releases page](https://github.com/IntersectMBO/cardano-airgap/releases) on a trusted, internet-connected machine. Verify the hash of the downloaded ISO before writing it to a USB drive. Write it to a USB drive: ```shell # Linux / macOS sudo dd if=cardano-airgap-*.iso of=/dev/sdX bs=4M status=progress # Or use Balena Etcher / Raspberry Pi Imager for a GUI option ``` Boot the target machine from the USB drive. From the moment it boots, the machine has never been and will never be online. ## Key storage: encrypt at rest Your cold keys should **never** sit in plaintext on any storage medium — even one that stays offline. When booting from an ISO (such as `cardano-airgap`), the boot environment is read-only — keys are not stored on the machine. Instead, keep your keys on a **separate encrypted USB drive** that you plug in only during signing operations and store in a physically secure location (vault or safe) when not in use. Best practices for key storage: - Use a dedicated encrypted USB for keys (LUKS on Linux, or an encrypted container) - Use a passphrase that has never been typed on an internet-connected machine - Keep multiple encrypted copies on separate USB sticks — at least one copy securely offsite - Store copies in fireproof, physically secure locations - Never copy keys to an unencrypted drive, even temporarily ## Workflow overview The standard cold-signing workflow: 1. **Online machine** — build the unsigned transaction (e.g. `vote-tx.raw`) 2. Transfer the unsigned transaction to a USB drive (no keys on this drive) 3. **Air-gapped machine** — mount your encrypted key volume, sign the transaction, unmount 4. Transfer only the signed transaction (`vote-tx.signed`) back to the online machine 5. **Online machine** — submit the signed transaction Never transfer anything from the air-gapped machine to the online machine except signed transactions and public keys. ## Further reading - [cardano-airgap on GitHub](https://github.com/IntersectMBO/cardano-airgap) - [Secure Workflow](/docs/operators/security/secure-workflow) - [SPO Governance — voting with your cold key](/docs/operators/governance/spo-governance)
Manual setup — install Ubuntu on a dedicated machine "Air gap" originally meant a computer or subnetwork was surrounded by "air" — as defined by no data cable connections in or out — so it would be isolated from other computers & networks. These days it also means there are no radio-based network connections either (WiFi, Bluetooth, etc.). Developers & Cardano stake pool operators generally need an air gap environment in which to work with payment keys, stake pool keys and other cryptocurrency resources that offer high-value targets for hackers. Some specialised hardware (e.g. hardware wallets) may also perform this function. If you believe you have such a device, please be certain that it offers isolation features for your stake pool or development *and* that you feel secure entrusting your assets to those who have implemented these features. Otherwise, generally **you need a second computer** to create this air-gapped environment, and the rest of this guide is to help you do that. :::tip Linux veterans only If you don't have an extra computer, or want to try building a standalone Linux environment on a USB drive, [skip to Option 2](#option-2-install-your-air-gap-environment-on-a-persistent-usb-drive). ::: ## Option 1: Install your air gap environment on a standalone computer ### Choose the right computer You will get better results from an Intel PC than a Mac: - Mac booting has peculiarities that are too complicated to generally address here: therefore the rest of this document assumes you'll be using a PC and not a Mac. You will need this computer's whole disc: - Any second drive should be removed if you don't know how to completely disable it in the Linux installation process. - The modern minimum drive size of 80GB will be enough for the Linux installation *and* all your Cardano support files, even if you are building them from scratch. You can use an older machine, even a *very* old one: - Linux, although well supported on most new machines, is less likely to have missing device drivers on older machines: so you might do better with an older machine than a newer one. - This suits many developers & SPOs since an old or retired extra machine, or one with damaged software which will be replaced in the installation process, will be a good candidate to devote to the single purpose of an air gap environment. ### Confirm Ubuntu as installation OS, or choose differently We choose Ubuntu here because: - It's a common choice on servers, so if you're building a stake pool you'll have the option of copying your `cardano-cli` binary from a stake pool server to the air gap machine instead of compiling it again. - The Ubuntu desktop environment & commands are arguably better documented on the Internet than any other Linux distribution. Getting help needs to be as easy as possible since you won't be able to search the Internet for help on the air gap machine itself. The rest of these instructions will assume the choice of **Ubuntu** for your air gap environment OS. If installing a different variant of Linux, please remember: - When you read the term Ubuntu or show screenshots of its installer, look for equivalents on your own chosen Linux variant. - There may be better choices than Ubuntu now or in the future: please feel free to share your results with others in the Cardano community, perhaps [contributing](/docs/contribute/portal-contribute) your findings & procedures here on the Developer Portal. ### Prepare to follow Ubuntu installation instructions Read through the standard Ubuntu installation steps here (external link): [Ubuntu Tutorials > Install Ubuntu desktop](https://ubuntu.com/tutorials/install-ubuntu-desktop) #### Decide in advance whether to encrypt your air gap machine's files. When setting up the Ubuntu filesystems, you'll be given the option of creating a Volume Group so it can encrypt your entire partition contents with a variant of the AES algorithm. :::caution Your boot and UEFI partitions might not be encrypted, depending on the type of computer you have & version of the GRUB software with your OS installer. Therefore, as a precaution, never attach a USB drive to your air gap machine unless you've either formatted the drive or built it as installation media. ::: The main advantage to encrypting your air gap system: - Someone gaining physical access to your machine, or stealing it, will be prevented from violating your address (e.g. stealing your stake pool pledge\!) or stake pool (e.g. cloning your stake pool) security. 😎 The main disadvantage to encrypting it: - If you lose your disk encryption password, or set it incorrectly to something you can't reproduce, you will effectively lose all the data on the air gap machine's disk... including any account information or keys stored there. 😖 #### (optional, if encrypting your partition) Choose encryption password Suggested password requirements: - has never been transmitted over, or stored in, cleartext on the Internet, or stored in cleartext on your computer itself (just in case your air gap is accidentally broken) - has length & complexity enough to hash to about 2^128 possible values: this means at least 20 apparently random characters. ### Begin standard Ubuntu installation (with some modifications) As you follow the standard procedure (also linked above), stop at the points in the headings below to ensure you're installing your air gap environment correctly. Before starting, there is no need to physically disconnect your chosen air gap machine from the Internet, or do anything to your home router to disable WiFi. :::note The Internet will be unconfigured and disconnected after the OS is installed & patched and a small number of initial packages are installed (including the Cardano CLI). ::: ### Follow instructions: [Ubuntu Tutorials > Install Ubuntu desktop](https://ubuntu.com/tutorials/install-ubuntu-desktop) ... paying particular attention to these steps: #### Wireless (if asked) If your computer doesn't have a cabled connection, it is acceptable under our security model to add it to the WiFi network during OS installation. - Whatever wireless key you enter *will* be retained on the installed system, *but* you will be reminded to disconnect the Internet before the end of our own procedure. #### Updates and other software ![img](./img/10-software-choices.png) Select **Minimal installation**, since this is the least likely to leave you with security intrusive applications and services. - The "Normal" installation has cloud based services and games which tend to initiate Internet connections. - LibreOffice software is not included in the "minimal" packages but is recommended to add later (since it helps encrypt password & mnemonic backups). **Do not select** (as you normally would) the option for **third-party software for graphics and WiFi** because of the potential for institutional spyware. - Your graphics will be stable & high enough resolution without the performance enhancements of proprietary graphics drivers (otherwise you wouldn't see this installation screen). - WiFi performance enhancements are likewise unnecessary because you generally won't be using WiFi, and if you need a network cable you'll be disconnecting it soon & won't be using it again. #### Installation type ![img](./img/20-installation-type.png) Tick **Erase disk and install Ubuntu**.... you've already confirmed there's nothing else that needs to be kept on this computer, and that it won't have any other operating systems or working disks. :::caution The air gap installation should not be a part of any conventional dual-booting environment because of the inevitable security risks that would create. ::: Before you hit **Continue**, if you've chosen to encrypt your files: ##### (optional) Set up the hard drive for encryption ![img](./img/30-encrypt-disk.png) Hit the button below the *Erase disk* option: **Advanced Features** which will at first say *None selected*. - Tick the feature **Use LVM with the new Ubuntu installation**. - Tick the option below it: **Encrypt the new Ubuntu installation for security**. Don't hit the **Continue** button unless you can verify it now says ***LVM and encryption selected*** under Advanced options: ![img](./img/35-disk-encrypted.png) Enter the password you have prepared earlier as a **volume decryption key.** - At this point you might want to check a few times that you can type this password properly: either with consistency from written notes, or from memory. - To double check in this installation environment: move over to the left (the "dock") where you'll see a text editor icon, in which you can practice typing the password without leaving a record. - At this point the disk is only emulated in RAM: but just to be safe, don't save this file anywhere! #### Finish & reboot Confirm the installation drive, click **Install now** and **Continue**. - The rest of the options (user name & information, login method, etc.) can be set according to your inclination. Ubuntu will finish installing and then you'll be prompted to remove the installation media & reboot. When rebooting, you will see two things you may never have seen before: - If you followed these recommendations to only install one single OS on one single disk, the boot menu you see (from [GRUB](https://www.gnu.org/software/grub/)) will have only one choice: **Ubuntu**, with the software you just installed, which will be selected by default after a few seconds whenever the system starts. - If you selected the encryption option for your Ubuntu system, you will need to enter the encryption password every time you start the system. ### Configure Ubuntu according to security recommendations At the screen "Welcome to Ubuntu" (which new users are currently *forced* to interact with), _refuse **everything**_ it offers you: - no online accounts - no Canonical Livepatch - no sending any system information, ever! - no Location Services #### Basic security tightening at command line ##### Remove packages requiring routine network access: ``` bash sudo apt remove cups sudo apt remove unattended-upgrades ``` ##### (optional) Remove Snap software subsystem. [Snap](https://snapcraft.io) is questionable for security reasons because (like [AppImage](https://appimage.org) and [Flatpak](https://flatpak.org)) it links application components with libraries that don't have to be compiled from source or security-vetted like the libraries that come with your OS itself. Removing Snap is optional because default snaps on the Ubuntu installation media have the same security provenance as the default packages on that same release... yet snaps will also be upgraded in the next part of this procedure, and these upgraded snaps may not be subjected to the same security vetting. To proceed with removing Snap, follow these instructions (the exact procedure changes often & these instructions may be the best maintained to date): - **[How do I turn off snap in Ubuntu?](https://linuxhint.com/turn-off-snap-ubuntu/)** #### Update system software & all packages to current time This will upgrade everything on your system from what you received on installation media: ``` bash sudo apt update sudo apt upgrade ``` #### Install minimal set of packages for encrypting files/folders & text documents ##### (optional) Install LibreOffice This is recommended because it will give you a means of taking password-encrypted notes that can move between your air gap and computer host environments *in both directions*, so you can: - record transaction details from your home computer environment & Internet connected machines, for use in the air gap (as per [Secure Workflow](/docs/operators/security/secure-workflow)): - your Cardano account balances, UTxO addresses & payment addresses - notes from personal files & web sites about the work you will be doing within the air gap (since you won't have Internet access there); - take notes in the air gap environment (problems, error messages) to copy back to your computer, since you can't upload them through the air gap. LibreOffice documents saved with a password are entirely AES-encrypted with a key deriving from that password, which produces arguably the best commercially available security for files & data. To install: ``` bash sudo apt install libreoffice ``` #### Install encrypting archiver Whether a developer or a stake pool operator, at some point you will also need to encrypt files & folders so they can be extracted on your stake pool or application server, where LibreOffice will generally not run but you can use the installable command `p7zip` instead: ``` bash apt install p7zip-full p7zip-rar ``` Adding the extra package `p7zip-rar` should make saving files with encryption & compression an option in your file manager (`nautilus`). #### Install secure deletion tools You might need to erase any trace of an unencrypted file that could lead to loss of your funds or Cardano enterprises if it were reconstructed (since ordinary file deletions don't delete data blocks). Therefore you should [install the `secure-delete` tools](https://www.unixmen.com/securely-delete-hard-drive-data-with-secure-delete/) to allow you to zero-write files & their metadata or drive contents & empty disk space: ``` bash apt install secure-delete ``` ### Reboot again This confirms that your system will start properly after having updated your system software. ### Install `cardano-cli` Use the standard instructions here at the Developer Portal: - **[Installing the node from source](/docs/operators/node/installing-cardano-node)** Note this will build `cardano-node` as well as `cardano-cli`, but don't worry: you won't be running a node inside the air gap. 😜 ### Unplug from Internet FOREVER We will leave the definition of "forever" up to your understanding of Internet threats and whether these can come from OS package repositories, etc., with this in mind: - Software updates at 6-month intervals (e.g. after the Ubuntu "point releases") will patch security problems identified during that period: as well as install new software which may introduce *new* security problems. - Any spyware or backdoor deliberately placed in the package upgrades on Ubuntu or any other version of Linux could generally just as easily have been placed on the packages used to build your installation media. ### Precautions to avoid accidental connection to the Internet #### BIOS settings: disable WiFi and Ethernet connection See your computer instructions to review how to get into the BIOS, if you're interested in disabling the network adapters at a very low level so they can't accidentally (or due to a hack) be turned on in software. - If there's no BIOS setting, WiFi can usually be disabled almost as easily on laptops by opening them up to remove, or disconnect the leads to, the WiFi card. #### Put Ubuntu in [Airplane mode](https://help.ubuntu.com/stable/ubuntu-help/net-wireless-airplane.html) This will disable any Bluetooth services as well as WiFi, and shows as an Airplane on Ubuntu & other GNOME desktops as an airplane icon in the upper right corner of the screen. With Airplane Mode always engaged, you would need the obvious Internet cable plugged in to have any network access (unlike WiFi which can often be connected by accident). #### Add your computer's WiFi MAC address to the blacklist on your Internet router Some routers maintain a list of MAC addresses which will not be given an IP address by DHCP, which isolates them from the Internet unless that network interface is configured manually. Therefore, you can [find your WiFi MAC address](https://help.ubuntu.com/stable/ubuntu-help/net-macaddress.html.en) and add it to your router's blacklist: usually in its DNS, DHCP, or LAN settings. ### Congratulations, your air gap environment is complete! You now have a safe place you can use for your [Secure Transaction Workflow](/docs/operators/security/secure-workflow). ## Option 2: Install your air gap environment on a persistent USB drive :::caution Linux veterans only\! (otherwise please [follow option 1](#option-1-install-your-air-gap-environment-on-a-standalone-computer)) ::: This option may suit more demanding users, especially those: - who travel a lot and need to maintain their Cardano operations "on the road"; - who need the convenience of booting in an air gap environment which has direct access to all their files on the host computer (as you would when booting off from an installer USB drive); - who, instead of using a USB drive to transfer unencrypted files in & out of the air gap, would rather use that same USB drive to store these files with encryption while also providing the Cardano CLI for use on any machine supporting the same boot method; - who want to make encrypted backups or their keys, passwords and other records from their air gap environment directly to the host computer. If this appeals to you, and you don't mind following a more complicated and error-prone installation procedure, you might want to install the air gap environment on a bootable USB drive instead. You can then boot a computer from this drive to have access to your secure resources and `cardano-cli` while isolating that computer from the Internet as well as any malicious software that might be installed on that computer.
Frankenwallet — bootable USB air gap ## An encrypted, air-gapped Linux bootable USB drive for Cardano (and other) secure operations Frankenwallet is not a package, library or product, but rather a set of installation guides, security standards and templates that allow Cardano SPOs, token minters, users with funds in bare addresses, and smart contract creators to configure an ordinary USB drive to boot Linux with a level of security isolation and software prerequisites appropriate to their use case. When one's primary computer is booted from this removable drive, the secure ("cold") configuration & workflow conventions allow operators to: - store and work securely and flexibly with private keys - sign transactions and securely keep records of transaction details - keep encrypted records & backups without ever revealing keys or passwords in the insecure host environment :::warning warning - Linux veterans only These instructions may be difficult or unsafe to follow unless you have experience with "dual boot" Linux installations and other custom OS & booting configurations. Operators needing a safer path can use the cardano-airgap or Manual Setup options above. ::: ### How to use this guide This tool has been developed by the [COSD stake pool](https://cexplorer.io/pool/pool1e98xlcgj80c3rdmm27v5hnvrdtut52e65uk0ema7ctfag596vr2), beginning as a publication of their own operating environment when scared to death of losing their pool pledge and not being able to come by a second machine for the conventional air gap environment (see origin story: [Why was the Frankenwallet developed?](https://frankenwallet.com/intro/history)). At the time of this writing, the full instructions for: - the reasons you would want to use this tool - how to provision & build your own Frankenwallet - how to use the tool for stake pool operations & secure transactions … are in the online book at this external link: [The Frankenwallet](https://frankenwallet.com). If you see any problems with this material, please submit an issue at: - [github:rphair/frankenwallet](https://github.com/rphair/frankenwallet) if you find an error in the material in the externally linked web site - [github:cardano-foundation/developer-portal](https://github.com/cardano-foundation/developer-portal) with any updates or corrections to this page itself. This is a one-page summary of those external instructions to help you (the operator) decide if the Frankenwallet is something you might use in your workflow according to your own level of interest & expertise. ### Use cases for the Frankenwallet ➤ Anyone working with private keys & [secure transaction signing](/docs/operators/security/secure-workflow), seed phrases, or other high value resources targeted by hackers (e.g., [stake pool keys](/docs/operators/basics/cardano-key-pairs)). ➤ Anyone wishing to work in high security with these resources without either a second computer (e.g. perpetual travellers, students, and hardware minimalists) or a hardware wallet ([Frankenwallet vs. Hardware wallets](https://frankenwallet.com/intro/hardware-wallets)) ➤ Anyone wanting or needing direct access to all their own files on their main computer in the air-gapped environment. ➤ Anyone who has wondered how you might get the same (or better) features as a hardware wallet on an easily obtainable & anonymous USB drive: including a full featured operating system with applications that can edit encrypted and richly formatted files and prepare encrypted document archives. ➤ Anyone using memory sticks to store or back up private keys who has worried about an unencrypted memory stick being lost or stolen. ➤ Anyone wanting to prepare an off-site or even a network backup of their keys, wallet seed phrases, and other cryptocurrency asset records… given that AES based encryption is considered unbreakable when properly used (i.e. never entering the passphrase on a network-connected machine). ### If so universally useful, why the build instructions & not just a downloadable ISO image? **TL;DR** because then all Frankenwallets would be the same, and any security flaw found in one of them might allow all of them to be exploited before a response could be mounted (see [Why is there no ISO image for Frankenwallet?](https://frankenwallet.com/intro/no-iso)). ### Some other use cases & limitations of this material ➤ You *can* use the Frankenwallet instructions to set up an Air Gap node on a full computer… but since the time of its development, this procedure has been adapted to a more appropriate page on the Dev Portal (the aforementioned Air Gap Environment). From [Frankenwallet > Miscellaneous FAQ's](https://frankenwallet.com/intro/faq): ➤ Your VirtualBox or other VM software on your host computer *does not* isolate you from the network, even if you have the network device disabled… nor can it be ever assumed that the screen or keyboard are isolated either… so VMs are generally unsuitable to create an air gap *or* to implement these instructions. ➤ Ubuntu + GNOME, though heavyweight and tainted by default with proprietary software, are chosen for their universal documentation especially when it comes to issues of OS installation (_without_ that proprietary software!) and dual booting. ➤ Read more about the [Evil Maid](http://theinvisiblethings.blogspot.com/2009/01/why-do-i-miss-microsoft-bitlocker.html) to see what she, he, or it can & cannot do with your Frankenwallet by compromising your host computer's BIOS in a way to which all commercial computers are vulnerable. ## Preparing to build the Frankenwallet From [Frankenwallet > Preparation](https://frankenwallet.com/prepare): #### Planning your communication with the host computer You will avoid moving files around on memory sticks *and* transferring them over a network (impossible with Air Gap machines) because, when you boot from a USB device based operating system, the main disk on that computer is *also* accessible as if *it* were an external device. Therefore you can plan an area on your host computer (called here the Host Folder) which the Frankenwallet will use to store any encrypted files… as well as read the raw data for the transactions that you will prepare in the air gapped environment. :::warning warning Remember early & often that nothing should be stored on the host computer that is not saved an encrypted document or archive. ::: ### Procuring your hardware Though regularly used Frankenwallets have been built on cheap & slow USB drives, to make this tool a dependable part of your workflow you should get either: - a memory stick with a high benchmark for reading and writing speed, or - (for best results in author's experience) a SATA SSD drive plus a SATA-to-USB adapter cable. Users who have built dual-boot configurations before will also know you should **familiarise yourself with the computer's BIOS settings** in anticipation of the same type of setup. Note there are limitations about using a Mac as host computer which stem from the different means of booting (see [Frankenwallet > Hardware Requirements](https://frankenwallet.com/prepare/hardware) > What if I have a Mac?). ### Choosing passwords (from Frankenwallet passwords > [low security](https://frankenwallet.com/prepare/password-low) & [high security](https://frankenwallet.com/prepare/password-high)) The [low security password](https://frankenwallet.com/prepare/password-low) can be one you've already used to encrypt files on the host computer… strong enough you feel comfortable backing up files over the net. The [high security password](https://frankenwallet.com/prepare/password-high)… called the Frankenwallet password itself… should also be strictly long & complex, but should never have been used in a network environment, not even on a network connected machine… otherwise you will be defeating the purpose of using the Air Gap for any purposes of file storage or backup of files to the host computer See each of these web links to see which of the Cardano asset & stake pool files it would typically be used to encrypt. :::info optional If you intend to use the ["cool" Frankenwallet](#the-cool-frankenwallet-a-sandbox-for-crypto-wallets) configuration (supporting light browser-based wallets) with a Chrome-based browser like Brave, you should be ready with a second high-security password used only to encrypt your most confidential data… since by default you will have to enter the user account password in the browser UI to unlock the GNOME keyring and therefore expose it in an uncertain security context. ::: :::tip For ease of use, you can separate the "low security" and "high security" stake pool files into two subdirectories, so they can be backed up as two separately password-encrypted archives. ::: ## Installing the OS onto the USB device (from [Frankenwallet > Host computer & media](https://frankenwallet.com/prepare/computer) though end of [Installation](https://frankenwallet.com/install) section) The full instructions mainly document the [installation of Ubuntu](https://ubuntu.com/tutorials/install-ubuntu-desktop#1-overview) in the common "dual boot" configuration: something the target audience should feel comfortable with, and can probably improvise for themselves if also following these checklists during the installation & setup or the installed environment: ### Installation notes: software No need to disconnect from the Internet yet because you will be using it to do your first package updates & software installation. - Purists might want to do this without Internet access at all: if feeling comfortable with the baseline OS alone (no upgrades) + getting your packages by saving them in your computer's & installing them from there. Select the Minimal software installation (no network hungry apps & games) and plan to install the LibreOffice package later. Don't tick **third party hardware for graphics and WiFi** because the proprietary vendor software provided for these devices can contain institutional spyware. ### Installation notes: partitioning When you select **erase disk and install Ubuntu** you will get the options under Advanced Features for: - use LVM (the Logical Volume Manager), allowing more flexible disk usage - select Encrypt the new Ubuntu Installation - enter the "High Security" password you chose as the drive encryption password Note the password you chose will be required now to boot the OS as well as decrypt the the partition it creates on any other devices (so your drive is secure when not booting). :::warning warning At the next screen Erase disk and install Ubuntu, watch out that you don't accidentally select your computer's own drive… this can be very easy to do! ::: ### Setup notes: operating system - Don't let Ubuntu link with any online accounts in its initialisation process: refuse everything like location services, "livepatch", etc. - Disable lots of little services & settings which might leak your information (see [Frankenwallet > First boot: Secure system settings](https://frankenwallet.com/install/settings)) ### Setup notes: packages (details: [Frankenwallet > First boot: Package installation](https://frankenwallet.com/install/packages)) - Remove all "snaps" and disable Snap. - Remove CUPS (network printer service). - Disable unattended upgrades. - Upgrade the remainder of the system (`apt update; apt upgrade; apt autoremove`) ### Install document & security-oriented packages - `secure-delete` (in case you accidentally write unencrypted keys or secure data to your host computer drive) - `LibreOffice` (supporting AES256 encrypted documents) - `p7zip` (supporting AES256 encrypted archives) ### Tune browser & turn off network access FOREVER Lock down the browser settings, just in case, even if you think you'll never use it ([Frankenwallet > Securing Firefox browser](https://frankenwallet.com/install/browser)) At this point you disable Wi-Fi and all other networks in the system settings, and go on without any future connection to the Internet in your new environment. ## What to use the Frankenwallet for From a growing body of material beginning at [Frankenwallet > Usage](https://frankenwallet.com/usage): ### Prepare and submit secure transactions You can now follow the instructions recommended in [Secure Transaction Workflow](/docs/operators/security/secure-workflow), with the following modifications: - Create a file on your networked host computer in the Host Folder, encrypted with the Low Security password (so you feel safe backing it up over the Internet, but won't store any keys or wallet passphrases there). - When planning your transaction, save the transaction details and any commands to cut-and-paste, in this file. - Boot into the Frankenwallet and navigate to your Host Folder. - Copy-paste the transaction commands and/or transaction data into the Frankenwallet command line. - Save the resulting transaction file to your Host Folder. - Reboot into the host computer, upload your transaction file if necessary, and submit it. This means of implementing the [Secure Transaction Workflow](/docs/operators/security/secure-workflow) process is outlined specifically in [Frankenwallet > Transaction flow](https://frankenwallet.com/cardano/model). ### Making & verifying backups of assets & keys from [Frankenwallet > Backups to host machine](https://frankenwallet.com/usage/backups): For [highly secure stake pool & asset files](https://frankenwallet.com/prepare/password-high), and any documents storing wallet key phrases or raw private key data: - First create the file archive (with 7z) or text document (with LibreOffice) using your "high security" password. - Then copy it to your host folder, where it can remain stored or backed up (over the network if desired) along with all your other computer's data. - This is safe (pending the usual arguments) because **you never have entered, and never will enter, the Frankenwallet (high security) password on your host computer or any other machine**. - This means you can only verify these backups on this or another Frankenwallet… never on the host computer environment itself! For [less secure stake pool & asset files](https://frankenwallet.com/prepare/password-low), and documents with general transaction records & source data: - First create the file archive (with 7z) or text document (with LibreOffice) using your "high security" password. - These files you might feel comfortable verifying on your host computer. - NOTE for less urgently secure stake pool pool files (e.g. verification keys, operational certificate counters) you might provide a second dedicated password… with "security level" between your general encryption password and the "high security" password… which you only use for the purposes of your assets & stake pool public keys. ### The "cool" Frankenwallet: a sandbox for crypto wallets from [Frankenwallet > Cool environments](https://frankenwallet.com/cool): Relaxing the Internet environment (meaning **this device should no longer be used for cold, unencrypted key storage**) allows you to use this device for node- or browser-based wallets. Even low-bandwidth memory sticks have been tested in use with the resource intensive Daedalus node wallet, and they still work. But keep in mind that a node wallet will be considered very slow to sync… especially when your "daily driver" computer is booted from your Frankenwallet and can be used for no other purpose until booted normally again. For browser-based wallets, the performance will be better… although the Firefox (or other browser) configuration becomes vital to avoid some institutional or extension spyware possibly compromising your keys. In either case, you can still use the Frankenwallet to **copy the wallet key phrases to an encrypted file** on your host computer: so you can keep them encrypted with a password that has never been entered on your host machine. Also keep in mind your security isolation can never be considered complete once you've allowed Internet connection from this "cool" environment… though this "sandbox" is still better than the complete exposure you'd have by running a node or browser based wallet on your network-connected, daily-use machine.
--- ## Secure Transaction Workflow The core rule for all Cardano key operations is simple: :::warning Private keys — payment keys, cold keys, stake keys — must never exist on an internet-connected machine. ::: This page describes the workflow pattern that enforces that rule: build transactions on an online machine, sign on the air-gapped machine, submit from the online machine. The signing key never moves; only unsigned and signed transaction files cross the boundary. ## The three-step pattern ```mermaid flowchart LR A["Online machinequery + build(unsigned tx)"] --> B["Air-gapped machinereview + sign(signed tx)"] --> C["Online machinesubmit"] ``` 1. **Build** — on your online node, query the chain and build an unsigned transaction. No key required. 2. **Sign** — transfer the unsigned transaction to the air-gapped machine via a dedicated USB drive. Inspect it, then sign it with the private key. 3. **Submit** — transfer the signed transaction back to the online machine and submit. The private key never leaves the air gap. The online machine never sees a signed transaction until after signing. ## Before you start Set these environment variables on your online node so you don't have to repeat them on every command: ```bash export CARDANO_NODE_SOCKET_PATH=/run/cardano/node.socket export CARDANO_NODE_NETWORK_ID=mainnet # or 1 for preprod, 2 for preview ``` For your air-gapped environment options, see [Air Gap Environment](/docs/operators/security/air-gap). ## Transfer media Keep a dedicated USB drive for moving transaction files. Format it on the air-gapped machine before first use. Use exFAT or FAT32 for compatibility between Linux and other systems. **Never put keys on this drive.** Keys live on the air-gapped machine's encrypted volume. The USB drive carries only: - Unsigned transaction files (`.raw`) going **in** to the air gap - Signed transaction files (`.signed`) coming **out** of the air gap - Public keys and addresses (safe to copy in either direction) - Protocol parameters and UTxO data going **in** ## Key generation You can generate keys in two ways: random key generation (simpler, no mnemonic) or mnemonic-based derivation (recoverable from a seed phrase). Both methods should be run **on the air-gapped machine**. ### Random key generation (cardano-cli) ```bash # Payment key pair cardano-cli address key-gen \ --verification-key-file payment.vkey \ --signing-key-file payment.skey # Stake key pair cardano-cli stake-address key-gen \ --verification-key-file stake.vkey \ --signing-key-file stake.skey ``` Back up the `.skey` files to at least two independent encrypted locations. If they are lost, there is no recovery path. ### Mnemonic-based derivation Deriving keys from a mnemonic means your keys can be re-derived from the seed phrase at any time, and the keys are compatible with standard Cardano wallets. :::danger Never store your mnemonic on a cloud server, in email, or in any internet-connected storage. Treat it with the same care as the signing keys themselves — the mnemonic is the master secret from which all keys can be re-derived. ::: #### cardano-signer — derive keys directly to .skey/.vkey files [cardano-signer](https://github.com/gitmachtl/cardano-signer) produces cardano-cli-compatible `.skey`/`.vkey` files directly from a mnemonic and a named derivation path: ```bash # Generate new mnemonic and derive payment keys in one step cardano-signer keygen --path payment \ --out-skey payment.skey \ --out-vkey payment.vkey \ --out-mnemonics phrase.prv # Or derive from an existing mnemonic cardano-signer keygen --path payment \ --mnemonics "word1 word2 ... word24" \ --out-skey payment.skey \ --out-vkey payment.vkey # Stake key cardano-signer keygen --path stake \ --mnemonics "word1 word2 ... word24" \ --out-skey stake.skey \ --out-vkey stake.vkey # Pool cold key cardano-signer keygen --path pool \ --mnemonics "word1 word2 ... word24" \ --out-skey cold.skey \ --out-vkey cold.vkey \ --out-id pool.id ``` Named paths (`payment`, `stake`, `pool`, `drep`, `cc-cold`, `cc-hot`, `calidus`) expand to the standard BIP44/CIP derivation paths automatically. #### cardano-addresses — wallet-compatible key and address derivation [cardano-addresses](https://github.com/IntersectMBO/cardano-addresses) is the lower-level pipeline used by wallets. Use it when you need full BIP44 address derivation or want to generate addresses compatible with a specific account index or address index: ```bash # Generate a 24-word mnemonic cardano-address recovery-phrase generate --size 24 > phrase.prv # Derive root key cardano-address key from-recovery-phrase Shelley < phrase.prv > root.xsk # Derive payment verification key (account 0, address 0) cardano-address key child 1852H/1815H/0H/0/0 < root.xsk \ | cardano-address key public --with-chain-code > addr.xvk # Derive stake verification key cardano-address key child 1852H/1815H/0H/2/0 < root.xsk \ | cardano-address key public --with-chain-code > stake.xvk # Generate a base address (payment + staking, mainnet) cardano-address address payment --network-tag mainnet < addr.xvk \ | cardano-address address delegation $(cat stake.xvk) > base.addr ``` The resulting `base.addr` is identical to the address a standard wallet would show for account 0, address 0 when restoring the same mnemonic. ## Payment transaction ### 1. Build (online machine) ```bash # Get current UTxO for your payment address cardano-cli query utxo --address $(cat payment.addr) # Build the unsigned transaction — transaction build handles fee calculation automatically cardano-cli conway transaction build \ --tx-in # \ --tx-out + \ --change-address $(cat payment.addr) \ --out-file tx.raw ``` Copy `tx.raw` to the USB drive. ### 2. Sign (air-gapped machine) Mount the USB drive and inspect the transaction before signing: ```bash cardano-cli conway transaction view --tx-file tx.raw ``` Verify the outputs match what you intended. Then sign: ```bash cardano-cli conway transaction sign \ --tx-body-file tx.raw \ --signing-key-file payment.skey \ --out-file tx.signed ``` Copy `tx.signed` to the USB drive. Unmount and remove the drive. ### 3. Submit (online machine) ```bash cardano-cli conway transaction submit --tx-file tx.signed ``` Verify on a [block explorer](/docs/developers/curriculum/start-building/networks-and-test-ada). ## Cold key operations Pool registration, re-registration, and retirement all follow the same pattern but require the cold key (`cold.skey`) for signing and may require additional files on the air-gapped machine. The full procedures are documented in: - [Registering a Pool](/docs/operators/block-producer/register-stake-pool) — pool registration certificate and delegation certificate - [Key Generation](/docs/operators/block-producer/block-producer-keys) — KES rotation and op cert issuance - [Deployment](/docs/operators/block-producer/deployment) — securely transferring credentials to the block producer ## Governance voting Constitutional Committee members and SPO voting operations also follow this pattern: 1. Build the vote transaction on an online machine (or use a governance tool) 2. Transfer to the air-gapped machine for signing with the cold key or CC hot key 3. Submit the signed transaction See [SPO Governance](/docs/operators/governance/spo-governance) for the full voting workflow. ## Extended signing with cardano-signer [cardano-signer](https://github.com/gitmachtl/cardano-signer) handles signing operations that go beyond plain transactions. All of these should be run on the **air-gapped machine** when they involve a private key. ### CIP-8 message signing (identity proof) Governance tools, dApps, and explorer profiles often ask you to prove ownership of a key by signing a challenge message. This uses the CIP-8 COSE_Sign1 format: ```bash cardano-signer sign --cip8 \ --data "I am the operator of this pool" \ --secret-key stake.skey \ --address $(cat stake.addr) \ --json ``` The `--address` flag causes cardano-signer to verify that the key actually belongs to that address before signing, catching key/address mismatches. ### Governance metadata signing (CIP-100/108/119) SPOs, DReps, and Constitutional Committee members publishing rationale documents or poll responses must sign the JSON-LD metadata file. cardano-signer adds the author signature directly to the document: ```bash # Sign a governance metadata document (e.g., rationale, DRep statement, SPO statement) # On air-gapped machine cardano-signer sign --cip100 \ --data-file governance-metadata.jsonld \ --secret-key stake.skey \ --author-name "My Pool Name" \ --out-file governance-metadata-signed.jsonld ``` Verify the signatures before publishing: ```bash cardano-signer verify --cip100 \ --data-file governance-metadata-signed.jsonld \ --json ``` The signed file is the one you upload as the governance anchor document and hash for on-chain submission. ### Calidus key registration (CIP-88v2) [Calidus keys](/docs/operators/operator-tools/calidus-keys) are hot keys that prove you control a pool without exposing the cold key. Registration requires a cold-key signature, so it must be done on the air-gapped machine: ```bash # 1. Generate the Calidus hot key (on air-gapped machine) cardano-signer keygen --path calidus \ --out-skey calidus.skey \ --out-vkey calidus.vkey \ --out-id calidus.id # 2. Generate the CIP-88v2 registration metadata (cold key signs for the calidus key) cardano-signer sign --cip88 \ --calidus-public-key calidus.vkey \ --secret-key cold.skey \ --json-extended \ --out-file calidus-registration.json \ --out-cbor calidus-registration.cbor ``` Copy `calidus-registration.cbor` to the online machine and submit it as transaction metadata. The `calidus.skey` stays on the air-gapped machine; `calidus.vkey` and `calidus.id` can be copied anywhere and used freely as a hot key. ## Key backup Your private keys should be encrypted at rest on the air-gapped machine. Keep at minimum two independent encrypted backups in separate physical locations. See [Air Gap Environment — key storage](/docs/operators/security/air-gap) for recommended practices. --- ## Get Started in the Cardano Developer Community Thousands of Cardano developers and enthusiasts gather across these online communities to share knowledge, discuss technical developments, get support, and collaborate on building the future of Cardano. :::tip Join Weekly Developer Office Hours Get your questions answered directly by Cardano Foundation engineers every week! Join [**Developer Office Hours**](https://www.addevent.com/calendar/TG807216) - a opportunity to interact live with CF engineers and community experts. Each weekly 1-hour session features a different topic presented by the creators behind it, followed by an open Q&A where you can ask anything. All presentation portions are recorded and available on the [Cardano Community YouTube channel](https://www.youtube.com/playlist?list=PLCuyQuWCJVQ3IZiQQvHtczEM-cFAqoHBr). If you prefer not to ask your questions during the live call, you can submit them anonymously through this [form](https://cardanocommunity.typeform.com/DevOfficeHours)! ::: ## Forums & Q&A [**Cardano Stack Exchange**](https://cardano.stackexchange.com) The primary question-and-answer platform for Cardano developers. Get technical help, share knowledge, and find solutions to specific development challenges. [**Cardano Forum - Developers**](https://forum.cardano.org/c/developers/29) Long-form discussions, technical debates, and community support. Perfect for in-depth conversations and sharing detailed insights about Cardano development. [**r/CardanoDevelopers**](https://www.reddit.com/r/CardanoDevelopers/) Reddit community dedicated to everyone building on the Cardano blockchain. Share projects, ask questions, and connect with fellow developers. ## Chat Communities [**Cardano Developers Telegram**](https://t.me/CardanoDevelopersOfficial) One of the oldest Cardano developer community groups on Telegram. Good to get in touch with the general developer community, have active conversations and get community support. [**CIP Biweekly Meetings**](https://discord.com/invite/Jy9YM69Ezf) Join Cardano Improvement Proposal discussions every two weeks. Participate in technical standards development and ongoing Cardano protocol improvement conversations. [**Engineering and Development Discord**](https://discord.gg/MmeqpAzKbp) Chat with general developer community channels. Great for quick questions, community discussions, and staying up-to-date with the latest developments. [**IOG Technical Discord**](https://discord.com/invite/w6TwW9bGA6) Official [Input Output](https://iohk.io/) Discord server. Home to [Plutus Pioneers](https://github.com/input-output-hk/plutus-pioneer-program) and technical discussions with IOG developers. ## Talent Pool & Hackathons Sign up for the Cardano developer talent pool to hear about hackathons, jobs, and grants in the ecosystem. :::info Hear About New Opportunities Sign up once and we'll reach out when relevant hackathons, jobs, or grants come up. [**Join the Talent Pool →**](/talent) ::: ## Jobs and careers Many teams across the ecosystem hire remotely. To find roles and the organizations behind them: - [Browse Cardano ecosystem entities](https://cardano.org/entities) to see who is building, across DeFi, NFTs, gaming, identity, and more. - [Cardano Foundation careers](https://cardanofoundation.org/careers), [EMURGO careers](https://emurgo.io/careers/), and [Input Output careers](https://apply.workable.com/io-global/) for roles at the founding entities. - Sign up for the [Talent Pool](/talent) to hear about jobs as they come up. ## Developer Surveys An annual survey to assess the state of the Cardano developer ecosystem was conducted. This survey comes as part of our commitment to both empower the Cardano community and foster the open source maturity of the Cardano ecosystem. - You can find the annual survey reports in this [repository](https://github.com/cardano-foundation/state-of-the-developer-ecosystem). --- ## Funding and grants Cardano funds builders through several independent channels. They change over time. Rounds open and close, programs move between stewards, and new ones appear, so treat this as a map of the landscape and follow each link for the current status. There is no single right door. Pick the avenues that fit what you are building and how far along you are. ## Project Catalyst [Project Catalyst](https://projectcatalyst.io/) is Cardano's community innovation fund. Builders submit proposals, ada holders vote, and funded projects deliver against milestones. It has distributed over $150M across thousands of proposals since it began. Catalyst is moving stewardship to the Cardano Foundation, and rounds can pause between funds while that settles. Check the [Catalyst site](https://projectcatalyst.io/) for whether a round is open and what the current process looks like before you plan around it. Good fit for new ideas, prototypes, and community-facing projects that can rally voter support. ## On-chain treasury Cardano's treasury is governed on-chain under [CIP-1694](https://cips.cardano.org/cip/CIP-1694). Anyone can submit a treasury withdrawal as a governance action, which [DReps](https://cardano.org/governance) and the Constitutional Committee then vote on. This is the route for larger, ecosystem-level work with clear community benefit. Draft and submit an action with [GovTool](https://gov.tools), and see [staking and governance](/docs/developers/curriculum/staking-governance/governance) for how the process works under the hood. Organizations like [Intersect](https://www.intersectmbo.org/) and [PRAGMA](https://pragma.builders) often help coordinate due diligence and disbursement for approved proposals. Good fit for mature projects, shared infrastructure, and protocol-level improvements. ## Intersect grants [Intersect](https://www.intersectmbo.org/grants) administers grants and contracts for work that keeps the network running and growing, from core infrastructure to tooling. Members can apply for funding tied to specific initiatives. See [Intersect's funding opportunities](https://www.intersectmbo.org/grants) for what is currently open. Good fit for infrastructure, tooling, and continuity work the ecosystem depends on. ## Maintainer Retainer Program If you maintain an open-source repository the ecosystem relies on, Intersect's [Maintainer Retainer Program](https://opensourcecommittee.docs.intersectmbo.org/about/paid-open-source-model-posm/maintainer-retainer) provides recurring funding so critical projects stay secure and actively maintained. It is part of Intersect's Paid Open Source Model, with Core and Community Maintainer roles, overseen by the Open Source and Technical Steering Committees. Good fit for maintainers of established libraries and tools, rather than one-off builds. ## Accelerators Accelerators take early-stage teams through a fixed-term, cohort-based program of mentorship, technical and go-to-market support, and investor access, usually with a milestone contribution or investment attached rather than a grant you keep. [Orion Fund](https://orion.draperdragon.com/) is an $80M ecosystem fund run by Draper Dragon, with the Cardano Foundation as constitutional administrator. It backs Cardano-native and Cardano-integrated companies from acceleration stage through Series A, focuses on real-world assets and institutional DeFi, and develops founders through Draper University's residencies. It is the largest accelerator initiative in the ecosystem. The Cardano Foundation's [Cardano Accelerator Program](https://cardanofoundation.org/venture-hub/cardano-accelerator-program), part of its Venture Hub, runs themed cohorts of early-stage startups through technical sessions, go-to-market and regulatory guidance, and mentorship, from an in-person kickoff week to a Demo Day in front of investors. Each team receives a milestone contribution alongside hands-on support. Good fit for startups ready to raise and scale, not just prototype, and able to commit to an intensive cohort. ## Other paths worth watching - **Hackathons and bounties.** Prize money, bounties, and follow-on grants come around regularly. Join the [Talent Pool](/talent) to hear about them as they open. - **Founding entities.** The [Cardano Foundation](https://cardanofoundation.org), [EMURGO](https://emurgo.io), and various regional hubs run their own programs from time to time. - **Grassroots pools and DAOs.** Community-run funding comes and goes. Ask in the [developer community](/docs/community/cardano-developer-community) what is active right now. Not sure where to start? The [developer community](/docs/community/cardano-developer-community) is the fastest way to find out what is currently funding work like yours. --- ## How to contribute to the developer portal We wanted to build a developer portal as open and inclusive as Cardano - a portal in the hands of the Cardano community that can be constantly evolved by it. ## Why Contribute? ### Build Your Resume Each contribution you make acts as a precious notch on your belt towards career development or job searches within the Cardano ecosystem. It is also a way for people to find examples of your work and verify your abilities. By contributing to open source projects, you will not only gain a lot of valuable experience, but if your profile reaches a certain level of attention and recognition, you are also more likely to get professional opportunities further down the line. ### Build Your Reputation Contributions to the developer portal will give your GitHub name and profile higher visibility as more and more people come across your work online. As visibility increases, so too will the reputation of your name and brand. ### Build Your Confidence Creating tutorials and showing fellow community members how to create will not only elevate your knowledge of your own skills and processes, but will also bestow you with greater confidence in your abilities as you interact with others. Since everything is public, people typically pay greater attention to how well something is written or programmed. This will afford you with an invaluable set of eyes on your contributions that will serve as a crucial peer-reviewed tool to catch errors and refine your work. ## Quick Contributions **Fix typos, update links, small edits:** - Use GitHub's web editor directly on any file - Click the pencil icon ("Edit this page") at the end of any page - Make your changes and submit a pull request **Report issues or suggest improvements:** - [Create an issue](https://github.com/cardano-foundation/developer-portal/issues) - Anything from a simple suggestion to a fully elaborated plan. You can think of it as creating a topic in a forum. - [Start a discussion](https://github.com/cardano-foundation/developer-portal/discussions) - Appropriate for finding consensus on fundamental changes - [Share on the Cardano Forum](https://forum.cardano.org/c/developers/29) - For those who prefer forum discussions **Spread the word:** - Link to the [Cardano Apps](https://cardano.org/apps/) when someone asks about projects built on Cardano - Share the [Builder Tools](https://developers.cardano.org/tools) page with developers looking for SDKs and libraries ## Add Your Tool ### General Submitter Requirements **For all tool submissions:** - Your GitHub account ideally should have some contribution history or be known in the Cardano community - Brand new GitHub accounts may face additional scrutiny - All submissions must pass `yarn build` without errors before submission ### Add to Builder Tools Builder Tools is a curated directory, not a database. The goal is to keep it as useful as possible for any Cardano developer, which sometimes means maintainers declining a tool that works but isn't the right fit yet. The points below are what maintainers look for, not a checklist you tick to earn a spot. It's a list of things you build Cardano applications *with*: SDKs and libraries, APIs and providers, indexers and data infrastructure, node and operations tooling, wallets and connectivity, developer environments, and testing tools. End-user applications (wallets as products, dApps, DEXes, marketplaces, games) belong on [cardano.org/apps](https://cardano.org/apps/), not here. Mostly it comes down to signal. A tool that's genuinely useful, novel, or has gained real community adoption usually carries enough signal for maintainers to recognize it and say yes. A tool that works but hasn't found meaningful adoption, or isn't yet relied on by larger projects and applications, often hasn't generated that signal. That's not a no forever; things stand the test of time, so keep iterating and traction tends to show. Cardano is a vast sea of open-source repositories that work together, and a directory listing every one-off tool would point developers in every direction at once. So if you've built something niche for a specific workflow, the strongest move is often to contribute it into one of the established repositories already serving that domain, where it reaches more developers and is maintained alongside the rest. A tool built for one specific need, even a quick one-off that does exactly what it claims, isn't always enough on its own. Open source is encouraged (set `repository`); hosted and closed services are welcome too (`repository: null`). **Step-by-Step Process:** 1. **Add your tool entry** - Edit: `src/data/builder-tools/tools.js` - Add your entry to the **END** of the BuilderTools array - Use this format: ```javascript { title: "Your Tool Name", description: "Brief description of what your tool does", category: "sdk", // exactly ONE — see Categories in tags.js properties: ["typescript"], // language + interface facets — see tags.js website: "https://your-tool.com", repository: "https://github.com/owner/repo", // public source repo, or null docs: "https://docs.your-tool.com/getting-started", // or null if no docs } ``` 2. **Choose a category and properties** **Important:** - **Title**: use the project's own name, styled how the project styles it (e.g. lowercase `cardano-cli`, `gOuroboros`). Do not add descriptors or parentheticals, or re-case it for uniformity. - **Description**: one or two factual sentences, sentence case, ending with a period. No superlatives. Describe what the tool does and how it differs from similar tools (its language/interface), rather than restating its name. - Pick exactly **one** primary `category` that best describes what the tool *is*. The 12 categories live in `src/data/builder-tools/tags.js`. If the tool reads, serves, or indexes chain data, or runs/talks to a node, see "How the data & node categories relate" below to pick the right layer. - `properties` = the language(s) the tool is written in, plus its interface (`rest` / `graphql` / `grpc` / `websocket`) where relevant. - Open source is encouraged: set `repository` to your public repo (it adds an "Open Source" badge + a GitHub link on the tool's page). Hosted/closed services are welcome too — use `null`. - Do NOT set `maintainerPick` yourself (maintainers choose those). 3. **Test your submission** - Run `yarn build` (must complete without errors) - Check that your tool displays correctly 4. **Submit your pull request** - Use the "Add Builder Tool" GitHub PR template - Fill out the checklist in the template ### How the data & node categories relate If your tool reads, serves, or indexes chain data, or runs/talks to a node, it sits at one layer of a stack. Pick the layer the tool *operates at*: - **`node` (Nodes & Clients)** - the node software itself: run or be a node (a full node, an alternative client implementation, an L2 node). - **`node-access` (Node Access & RPC)** - talk to a node: RPC bridges and protocol libraries that expose a node you run. - **`indexer` (Indexers & Data)** - self-host a queryable store: ingest chain data and serve it back (full indexers, lightweight indexes, data nodes, streaming pipelines). - **`api` (APIs & Providers)** - hosted: a managed endpoint you call, with no infrastructure to run. `sdk` (SDKs & Libraries) sits across the top: a library that wraps the `node-access` / `indexer` / `api` layers so you build from code. Rules of thumb: do you call a hosted endpoint (`api`) or run it yourself (`indexer`)? Does the tool build its own queryable data store (`indexer`) or just relay the node's protocol (`node-access`)? Is it the node itself (`node`) or something that talks to one (`node-access`)? ### Curation and removal The list is curated and pruned, not append-only, so it stays useful over time. Maintainers remove a tool when it stops serving developers. What usually prompts that: - the website is down, or the source repository is archived or unreachable, - a long stretch (roughly two years or more) with no commits, releases, or community activity and no sign of life, - it's been superseded by a better-maintained tool for the same job, - a hosted service that's been discontinued, - an end-user application that belongs on [cardano.org/apps](https://cardano.org/apps/), - or it no longer meets a basic quality and trust bar. These are signals, not a scorecard. Maintainers weigh them with judgment and often talk through borderline cases. A removed tool can come back any time through the normal submission, which is also a good moment to refresh its information; a past listing isn't a fast-track. ### Maintainer picks A few tools carry a maintainer pick (the star): a steer toward what a developer should reach for in a given area today. It's a curated call, not a popularity contest. - It leans on signals (registry downloads, recent activity, maintenance, real-world fit) weighed with judgment. Stars alone mislead; a legacy tool often out-stars its maintained successor. - Roughly one clear leader per category, a couple more where it's genuinely warranted, and none where nothing stands out. - Maintainers choose the picks, so don't set `maintainerPick` on your own submission. If you think something deserves a pick, open an issue and make the case: which category it leads, and why. ### FAQ **Q: I don't know how to use GitHub or run `yarn build`. Can I still contribute?** A: Yes! You can: - [Open an issue](https://github.com/cardano-foundation/developer-portal/issues) with your tool details and someone from the community can help - [Start a discussion](https://github.com/cardano-foundation/developer-portal/discussions) to get guidance - Let the community know about your contribution idea in [the forum](https://forum.cardano.org/c/developers/cardano-projects/151) **Q: How long does it take for my tool to be approved?** A: Pull requests require **3 reviewer approvals**. This typically takes a few days to a week, depending on reviewer availability. After approval, changes are merged to the **staging branch** first (visible at [staging-dev-portal.netlify.app](https://staging-dev-portal.netlify.app)), then later pushed to production. This process causes a small delay between staging and production deployment. **Q: Can I update my tool information later?** A: Yes! Submit a new pull request with the updates to your tool entry. **Q: Why was my tool rejected?** A: Common reasons for rejection include: - **Domain issues** - Using temporary hosting domains, URL shorteners, or unstable domains - **New GitHub account** - Submitter account lacks contribution history or community recognition - **Incomplete submission** - Missing required fields, broken links, or build errors - **Marketing-focused description** - Using claims like "the best," "the first," or "the only" - **Out of scope, or not enough signal yet** - an end-user app (belongs on cardano.org/apps), or a tool that works but hasn't yet found the adoption that earns a spot. This isn't a permanent no; tools can earn a place as they gain traction. If your submission was rejected, reviewers will typically provide specific feedback in the pull request comments. **Q: Should I commit yarn.lock changes?** A: No, never commit `yarn.lock` changes. This file is managed by maintainers. If you accidentally committed it, remove it with: `git checkout staging -- yarn.lock && git commit -m 'revert yarn.lock'` For more details on the GitHub workflow, see [CONTRIBUTING.md](https://github.com/cardano-foundation/developer-portal/blob/staging/CONTRIBUTING.md). ## Add a dApp Template or Contract Two more curated surfaces live under [/templates](https://developers.cardano.org/templates): - **App starters** are runnable front-end dApp projects you scaffold in one command. To add one, follow [`examples/templates/README.md`](https://github.com/cardano-foundation/developer-portal/blob/staging/examples/templates/README.md). - **The contract library** is a use-case index (escrow, vesting, HTLC...) that links out to on-chain and off-chain implementations. To add an entry, follow [`src/data/contracts/README.md`](https://github.com/cardano-foundation/developer-portal/blob/staging/src/data/contracts/README.md). Both are curated like Builder Tools: canonical and maintained, pointing at the established source rather than forking code into the portal. Each entry is validated by `yarn build`, and the same submitter requirements and review process above apply. ## Contributing Documentation For **content writers and developers** who want to work on documentation, blog posts, or improve existing content. ### Local Development Setup **Requirements:** - [Node.js](https://nodejs.org/en/download/) >= 20.0 (check with `node -v`) - [Yarn](https://yarnpkg.com/en/) >= 1.20 (check with `yarn --version`) - On macOS: Xcode and Command Line Tools **Setup:** ```bash # Fork the repo on GitHub, then clone your fork git clone https://github.com//developer-portal.git cd developer-portal yarn install yarn build # Required at least once - pulls missing files yarn start # Development server at http://localhost:3000 ``` :::info Development vs Production - `yarn start` - Fast development with some limitations (blurry images, search issues) - `yarn build` - Full production build required before submitting PRs ::: ### Project Structure ```bash developer-portal/ ├── docs/ # Documentation content (you'll edit these) ├── blog/ # Developer blog posts ├── src/data/ # Builder tools data ├── static/img/ # Images and assets ├── sidebars.js # Navigation structure └── docusaurus.config.js ``` **Key locations:** - `/docs/` - All documentation content - `/src/data/builder-tools/tools.js` - Developer tools data - `/sidebars.js` - Controls documentation navigation ### Writing Content **Formatting:** See [Style Guide](portal-style-guide.md) for Markdown syntax and Docusaurus components. **Essential rules:** - Use `## Level 2` headings as top-level (page title is auto-generated) - Include frontmatter with `id`, `title`, `description` - Test with `yarn build` before submitting ### Troubleshooting **Node.js version error:** `[ERROR] Minimum Node.js version not met` **Solution:** Use Node.js >= 20.0. Use `nvm use 20` if you have multiple versions. **Sidebars loading error:** `[ERROR] Sidebars file failed to be loaded` **Solution:** Run `yarn build` first - this pulls missing auto-generated files. **Token Registry error:** `[ERROR] Sidebar category Token Registry has no subitem` **Solution:** Run `yarn build` first - same as above. ## More Ways to Contribute ### Improve Text Content Fix typos and improve texts, especially if you are a native speaker and have strong writing skills. ### Create Graphics If you are a talented graphic designer, you can improve various charts and diagrams. We should always use graphics that work well in both light mode and dark mode for the portal. You can also make one graphic for each. ### Blog Contributions When contributing blog posts, please follow these guidelines: **Tag naming conventions:** - Use lowercase tags only (e.g., `ai`, `defi`, `dex`, `dao`) - Tags must be defined in `blog/tags.yml` before use - Check existing tags in `blog/tags.yml` before adding new ones **Truncation markers:** - Most blog posts should include `` markers for better previews - **Exception:** Posts tagged with `media` (short video content) should NOT include truncation markers to preserve video visibility in blog listings **Social cards:** - The preview image shown when a post is shared on social media is generated at build time from the post title. You do not need to make one. - Do not add an `image:` field to the frontmatter. Every post gets a generated card, so the field is ignored. ### Review Pull Requests If you have excellent technical understanding and mistakes catch your eye, you can review pull requests. You should have made contributions before and have a GitHub account with some reputation. If you are unsure about if you are a good fit, participating in the active discussions that take place in developer portal github issues/pull requests is always a good place to start to have your name visible. ## Getting Help - **Technical issues:** [GitHub Issues](https://github.com/cardano-foundation/developer-portal/issues) - **Content questions:** [GitHub Discussions](https://github.com/cardano-foundation/developer-portal/discussions) - **Developer community:** [Cardano Forum](https://forum.cardano.org/c/developers/29) - **Connect with developers:** [Developer community overview](/docs/community/cardano-developer-community) --- ## Style Guide You can write content using [GitHub-flavored Markdown syntax](https://github.github.com/gfm/). [Markdown](https://github.github.com/gfm/) is a way to style text on the web. You control the display of the document; formatting words as bold or italic, adding images, and creating lists are just a few of the things we can do with Markdown. Mostly, Markdown is just regular text with a few non-alphabetic characters thrown in, like `#` or `*`. ## Front matter Every docs page starts with a front matter block between two `---` lines. The portal uses these fields: | Field | Required | Purpose | | --------------- | ----------- | -------------------------------------------------------------------------- | | `id` | yes | The page identifier used in URLs and sidebar references. | | `title` | yes | The page title. It renders as the top heading and names the browser tab. | | `description` | yes | One or two sentences for search engines and link previews. | | `sidebar_label` | recommended | A shorter name for the sidebar. Falls back to `title` if omitted. | | `slug` | optional | Overrides the URL path when it needs to differ from `id`. | | `keywords` | optional | Extra terms for search engines. | ```md --- id: my-page title: My Page Title sidebar_label: My Page description: One sentence saying what this page covers. --- ``` Internal links are checked at build time. A link that points to a page or file that does not exist fails the build, so run `yarn build` before opening a pull request. ## Markdown Examples This page will help you learn about the Markdown used in the Cardano Developer Portal, but the list is not intended to be exhaustive. Read the [docusaurus Markdown features](https://docusaurus.io/docs/next/markdown-features) for more examples. Let's start with the basics: ```text Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ You can even [link to the Forum!](https://forum.cardano.org) ``` Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ You can even [link to the Forum!](https://forum.cardano.org) :::note Avoid top-level headings `#Level 1` headings are rendered automatically from the `title` property of your `frontmatter`. Therefore use `## Level 2` headings as the top most heading in the docs. ::: ```md ## Structured documents Start a row with `##` to create a heading. Adding more `#` characters creates deeper, smaller headings. ### This is a level 3 heading #### This is a level 4 heading Heading levels go down to `######` (level 6). ``` The rendered headings are not shown live here because they would land in this page's own table of contents. The block between the `---` lines at the top of a page is covered in the [Front matter](#front-matter) section. ```text [I'm an inline-style link](https://forum.cardano.org) [I'm an inline-style link with title](https://forum.cardano.org "Cardano Forum") [I'm a reference-style link][arbitrary case-insensitive reference text] [You can use numbers for reference-style link definitions][1] Or leave it empty and use the [link text itself]. URLs and URLs in angle brackets will automatically get turned into links. http://www.cardano.org or . Some text to show that the reference links can follow later. [arbitrary case-insensitive reference text]: https://www.cardano.org [1]: https://forum.cardano.org [link text itself]: https://www.cardano.org ``` [I'm an inline-style link](https://forum.cardano.org) [I'm an inline-style link with title](https://forum.cardano.org "Cardano Forum") [I'm a reference-style link][arbitrary case-insensitive reference text] [You can use numbers for reference-style link definitions][1] Or leave it empty and use the [link text itself]. URLs will automatically get turned into links. Example: https://www.cardano.org Some text to show that the reference links can follow later. [arbitrary case-insensitive reference text]: https://www.cardano.org [1]: https://forum.cardano.org [link text itself]: https://www.cardano.org ```text If you'd like to quote someone, use the > character before the line: > It’s not about who’s first to market or how quickly we can upgrade something. It’s about what’s fit for purpose. - **Charles Hoskinson** ``` If you'd like to quote someone, use the > character before the line: > It’s not about who’s first to market or how quickly we can upgrade something. It’s about what’s fit for purpose. - **Charles Hoskinson** ```text Here's is the Plutus logo (hover to see the title text): Inline-style: ![alt text](./img/logo-plutus-small.png 'This is the Plutus logo inline-style') Reference-style: ![alt text][logo] [logo]: https://raw.githubusercontent.com/adam-p/markdown-here/master/src/common/images/icon48.png 'This is a logo reference-style' Images from any folder can be used by providing path to file. Path should be relative to Markdown file: ![alt text](./img/logo-plutus.png) ``` Here's is the Plutus logo (hover to see the title text): Inline-style: ![alt text](./img/logo-plutus-small.png 'This is the Plutus logo inline-style') Reference-style: ![alt text][logo] [logo]: https://raw.githubusercontent.com/adam-p/markdown-here/master/src/common/images/icon48.png 'This is a logo reference-style' Images from any folder can be used by providing path to file. Path should be relative to Markdown file: ![alt text](./img/logo-plutus.png) ```text 1. First ordered list item 2. Another item - Unordered sub-list. 3. Actual numbers don't matter, just that it's a number 1. Ordered sub-list 4. And another item. * Unordered list can use asterisks - Or minuses + Or pluses ``` 1. First ordered list item 1. Another item - Unordered sub-list. 1. Actual numbers don't matter, just that it's a number 1. Ordered sub-list 1. And another item. * Unordered list can use asterisks - Or minuses + Or pluses --- ## Code In the developer portal, you will often have to display code. You can display code with different syntax highlighting: ```javascript var s = 'JavaScript syntax highlighting'; alert(s); ``` ```javascript var s = 'JavaScript syntax highlighting'; alert(s); ``` ```python s = "Python syntax highlighting" print(s) ``` ```python s = "Python syntax highlighting" print(s) ``` ```aiken fn add_one(n: Int) -> Int { n + 1 } ``` ```aiken fn add_one(n: Int) -> Int { n + 1 } ``` ```csharp using System; var s = "c# syntax highlighting"; Console.WriteLine(s); ``` ```csharp using System; var s = "c# syntax highlighting"; Console.WriteLine(s); ``` ```json { "json_number": 225, "json_boolean": true, "json_string": "JSON syntax highlighting" } ``` ```json { "json_number": 225, "json_boolean": true, "json_string": "JSON syntax highlighting" } ``` ```shell ls echo "Shell syntax highlighting" sudo dmesg top ``` ```shell ls echo "Shell syntax highlighting" sudo dmesg top ``` ```diff fn add_one(n: Int) -> Int { - n + 2 + n + 1 } ``` ```diff fn add_one(n: Int) -> Int { - n + 2 + n + 1 } ``` ``` No language indicated, so no syntax highlighting. But let's throw in a tag. ``` ``` No language indicated, so no syntax highlighting. But let's throw in a tag. ``` ### Supported languages Syntax highlighting works out of the box for common web languages such as `javascript`, `typescript`, `jsx`, `python`, `rust`, `go`, `css`, and `markdown`. The portal additionally enables `aiken`, `bash` (also usable as `sh` or `shell`), `csharp`, `diff`, `haskell`, `java`, `json`, `php`, and `yaml`. If your language is not in either list, ask in your pull request; adding one is a one-line config change. ### Code block titles You can add a title to the code block by adding a `title` key after the language (leave a space between them). Use it when the reader needs to know which file the code belongs in. ```jsx title="/src/components/HelloCodeTitle.js" function HelloCodeTitle(props) { return Hello, {props.name}; } ``` ```jsx title="/src/components/HelloCodeTitle.js" function HelloCodeTitle(props) { return Hello, {props.name}; } ``` ### Highlighting lines Highlighting draws the reader's eye to the lines that matter, which makes it ideal for step-by-step tutorials. The preferred way is a highlight comment in the code itself. The comment is removed from the rendered output and the line after it gets a highlight: ```javascript function highlightMe() { // highlight-next-line console.log('This line gets highlighted'); } ``` ```javascript function highlightMe() { // highlight-next-line console.log('This line gets highlighted'); } ``` For a block of lines, wrap them in `highlight-start` and `highlight-end` comments: ```javascript function highlightRange() { // highlight-start console.log('These lines'); console.log('are all highlighted'); // highlight-end } ``` ```javascript function highlightRange() { // highlight-start console.log('These lines'); console.log('are all highlighted'); // highlight-end } ``` You can also highlight by line number in the code fence, for example ` ```javascript {2,3} `. Prefer the comment form: line numbers silently point at the wrong lines after the snippet is edited, while comments move with the code. ```javascript {2,3} function highlightMe() { console.log('This line can be highlighted!'); console.log('You can also highlight multiple lines'); } ``` ### Line numbers Add `showLineNumbers` to the fence when prose refers to specific lines, for example ` ```javascript showLineNumbers `: ```javascript showLineNumbers function numberedLines() { console.log('This block'); console.log('shows line numbers'); } ``` --- ## Tabs You can use tabs to display code examples in different languages. Import the two components once, below your front matter, then wrap each variant in a `TabItem`: ````jsx ```js function helloWorld() { console.log('Hello, world!'); } ``` ```php ``` ```py def hello_world(): print('Hello, world!') ``` ```` This renders as: ```js function helloWorld() { console.log('Hello, world!'); } ``` ```php ``` ```py def hello_world(): print('Hello, world!') ``` :::note Note that the empty lines above and below each language block (in the *md file) is intentional. ::: --- ## Syncing tab choices You can also switch multiple tabs at the same time based on user input. Give the tab groups the same `groupId` and the same `value`s, and the reader's choice syncs across them (and persists across pages): ```jsx Use Ctrl + C to copy. Use Command + C to copy. Use Ctrl + C to copy. Use Ctrl + V to paste. Use Command + V to paste. Use Ctrl + V to paste. ``` Use Ctrl + C to copy. Use Command + C to copy. Use Ctrl + C to copy. Use Ctrl + V to paste. Use Command + V to paste. Use Ctrl + V to paste. The portal standardizes on a few shared group ids so choices carry across the whole site. Use `groupId="sdk"` for SDK and CLI variants and `groupId="operating-systems"` for platform instructions. ## Components Beyond tabs, two theme components are worth knowing. ### DocCardList `DocCardList` renders a card grid of all pages in the current sidebar category. Use it on overview pages so they stay current without hand-maintained link lists; the curriculum overview pages use this pattern. ```jsx ``` ### Optimized images For large images, `IdealImage` generates responsive sizes at build time and lazy-loads them with a low-quality placeholder. Import the image as a module and pass it to the component: ```jsx ``` Plain Markdown images stay fine for icons and small screenshots. ## Concepts, code, and tools Most pages teach a concept and then show it in code. Keep those two jobs separate, and in that order. **Explain the concept tool-agnostically first.** A reader should grasp what something is and why it works that way with zero knowledge of any SDK or CLI. Never teach a concept *through* a tool: walking the reader through one SDK's method calls explains that SDK, not the concept. If the only material you have is tool-specific (a single SDK's API surface), extract the general truth from it and write *that* as the concept. **Then show code as illustration, baked in beneath.** Code is welcome and valued; it shows the reader what touching the network actually looks like. But it sits under the explanation as an example, not as the teaching itself. The concept should still stand if you mentally delete every code block. **Keep the prose lean.** Don't announce code ("here's how you do it in the SDK:"). The heading, the `import` line, and the tab label already say what it is. Don't add balancer asides ("the other SDK also exposes equivalent helpers...") to even things out. Let the code and the tabs speak. **Show only the examples you're confident in.** When the same operation appears in more than one tool (two SDKs, or an SDK versus the CLI), put the variants in a `` block so the reader's choice persists across the page. Present only the tools you can show well, with a clean, copy-runnable example. Don't pad a tab in for symmetry, and don't keep a link-only stub ("see the other tool's docs") sitting beside two full examples. A page may stay single-tool, and that is fine. A missing tool is an honest gap for a contributor to fill, not something to paper over. Parallel alternatives belong in tabs, never in a stray blockquote or a bolted-on "with the CLI" section. Use a shared `groupId="sdk"` and the same tab `value`s on every page so a reader's pick syncs across the whole portal: ```jsx // first SDK example // second SDK example // CLI example ``` ## Explaining a system or component When a page maps a system, a protocol, or a multi-part component, a few habits keep it readable as the content gets dense: - **Lead with a diagram.** Show the shape before the prose, so the reader has a frame to hang the details on. - **Keep sections the same weight.** A predictable rhythm makes dense material scannable; avoid a twenty-line section sitting next to a two-line one. - **For each part, say what it is, why it matters, and where it sits**, in that order. Position in the system is as important as the definition. - **Name the concrete component, but separate the concept from its implementation.** "The consensus layer, implemented by `ouroboros-consensus`" is clearer than treating the package as the concept, and it stays true across implementations. - **Define a part by what it does NOT do.** "The ledger does not know about the network" draws the boundary, which is often exactly what a reader is unsure about. ## Video embedding Use this code to embed YouTube videos: ```html ``` --- ## Tables ### When to use a table Use a table only as a lookup: something a reader scans to find a specific value (CLI flags, parameters, endpoints, protocol versions, thresholds, key inventories). If a reader would read it top to bottom like a paragraph, write a paragraph. Do not put these in a table: - Analogy or "in Web2 this is X" concept maps. One useful analogy belongs in a sentence. - Comparisons that need caveats to be true. A grid hides the nuance and tends to overclaim; explain the trade-off in prose. - Anything that just restates the text next to it. ### Formatting Colons can be used to align columns: ```text | Tables | Are | Cool | | ------------- | :-----------: | -----: | | col 3 is | right-aligned | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | ``` | Tables | Are | Cool | | ------------- | :-----------: | -----: | | col 3 is | right-aligned | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown. ```text | Markdown | Less | Pretty | | -------- | --------- | ---------- | | _Still_ | `renders` | **nicely** | | 1 | 2 | 3 | ``` | Markdown | Less | Pretty | | -------- | --------- | ---------- | | _Still_ | `renders` | **nicely** | | 1 | 2 | 3 | --- ## Inline HTML Inline HTML is basically possible, but should be avoided for various reasons. ```html
Definition list
Is something people use sometimes.
Markdown in HTML
Does *not* work **very** well. Use HTML tags.
```
Definition list
Is something people use sometimes.
Markdown in HTML
Does *not* work **very** well. Use HTML tags.
--- ## Line Breaks ```text Here's a line for us to start with. This line is separated from the one above by two newlines, so it will be a _separate paragraph_. This line is a separate line in the _same paragraph_, created either by two blank spaces or explicit tag at the end of the previous line. ``` Here's a line for us to start with. This line is separated from the one above by two newlines, so it will be a _separate paragraph_. This line is a separate line in the _same paragraph_, created either by two blank spaces or explicit `` tag at the end of the previous line. --- ## Admonitions Admonitions are the callout boxes of the portal. These are the available types. As a general rule: don't overdo it and avoid using admonitions in a row. ```text :::note This is a note ::: ``` :::note This is a note ::: ```text :::tip This is a tip ::: ``` :::tip This is a tip ::: ```text :::info This is background information ::: ``` :::info This is background information ::: ```text :::caution This is a caution ::: ``` :::caution This is a caution ::: ```text :::warning This is a warning ::: ``` :::warning This is a warning ::: ```text :::danger This action is irreversible ::: ``` :::danger This action is irreversible ::: ```text :::tip[Custom Title] This is a tip admonition with a custom title ::: ``` :::tip[Custom Title] This is a tip admonition with a custom title ::: The older form without brackets (`:::tip Custom Title`) also works. Two aliases exist: `:::caution` is an alias of `warning`, and `:::important` renders in the info family. Both are fine to use when the wording fits better. Reserve `danger` for real hazards such as loss of funds or keys. ### Collapsible details Long optional content, such as full command output or a deep dive, can sit in a collapsible block so it does not break the reading flow. The portal uses the HTML `details` element for this: ```html
Show the full output The content is hidden until the reader expands it. It supports regular Markdown, including `code` and **emphasis**.
```
Show the full output The content is hidden until the reader expands it. It supports regular Markdown, including `code` and **emphasis**.
## Mermaid To use Mermaid diagram, add a code block with language `mermaid`. See the [Mermaid syntax documentation](https://mermaid-js.github.io/mermaid/#/./n00b-syntaxReference) for more information on the Mermaid syntax and the different diagrams. Some examples: ```mermaid mindmap root((Cardano)) Technology Blockchain Proof of Stake Ouroboros Smart Contracts Aiken Marlowe Community Developers Stake Pool Operators Ambassadors Use Cases Decentralized Finance Identity Management KERI Supply Chain Research Peer-Reviewed Papers Academic Collaboration Formal Methods Ecosystem Native Tokens dApps Catalyst ``` ```mermaid flowchart LR A[Start] --> B{Decision} B -->|Yes| C[Continue] B -->|No| D[Stop] ``` ```mermaid pie "Metadata" : 81 "Smart Contracts" : 62 "Simple transactions" : 231 ``` ## Other style elements Please try to avoid other style elements, and always keep in mind that people with visual handicaps should also be able to cope with your content. ## Editor extensions and configurations Last but not least, let's talk about editors, extensions and configurations. You can use any text editor you like to write Markdown. [Visual Studio Code](https://code.visualstudio.com/), [Sublime](https://www.sublimetext.com/) and others have plugins that help you adhere to style guides by displaying warnings if you break the rules. Below are some extensions for these editors that help you write clean guides for the developer portal. All of them are optional local helpers; the repository does not ship a Markdown linter configuration and the build does not run one. ### markdownlint Displays configurable warnings for invalid Markdown formatting. * Install the extension via *Command Palette (Ctrl+P)* using `ext install DavidAnson.vscode-markdownlint` * If you want to tune it for this project, add a local `.markdownlint.json` (it is not tracked in the repository) with a configuration like this: ```json { "line-length": false, "MD004" : false, "MD033":{ "allowed_elements": ["Tabs", "TabItem", "DocCardList", "Image", "details", "summary", "br", "iframe", "dl", "dt", "dd", "em"] }, "MD034" : false, "MD046" : false } ``` 1. Install SublimeLinter as described [here](http://www.sublimelinter.com/en/stable/) 2. Install [Node.js](https://nodejs.org) 3. Install `markdownlint` by using `npm install -g markdownlint-cli` 4. Within Sublime Text's *Command Palette (Ctrl+Shift+P)* type `install` and select `Package Control: Install Package`. 5. When the plug-in list appears, type `markdownlint` and select `SublimeLinter-contrib-markdownlint`. 6. If you want to tune it for this project, add a local `.markdownlint.json` (it is gitignored) with the same configuration as shown in the Visual Studio Code tab. ### markdowntables Helps you work with tables * Install the extension via *Command Palette (Ctrl+P)* using `ext install pharndt.vscode-markdown-table` | Keybindings | | | --------------- | -------------------------- | | `Ctrl+Q Ctrl+F` | format table under cursor. | | `Ctrl+Q Space` | clear cell under cursor. | | `Ctrl+Q Ctrl+Q` | toggle table mode | * In table mode | Keybindings | | | -------------- | ---------------------------------------------- | | `Tab` | navigate to the next cell in table | | `Shift+Tab` | navigate to the previous cell in table | | `Alt+Numpad +` | Create new column left to the current position | | `Alt+Numpad -` | delete current column | ### rest-book When you write guides for `cardano-wallet` or other components with an API, you might want to include the response for a certain request in your guide. It can be useful not to leave the environment of your editor as to not lose focus or get distracted. `rest-book` allows you to execute HTTP requests within your editor. * Install the extension via *Command Palette (Ctrl+P)* using `ext install tanhakabir.rest-book` * Open or create a `.restbook` file to use the extension. ## Editorial Style Guide To make everything consistent we should agree on spellings and terms here. | Spelling/Term | Comment | | ---------------- | -------------------------- | | `ada` | When talking about the cryptocurrency, do not capitalize, unless at the beginning of a sentence. The idea behind this is to treat it like dollars or euros. If you are in doubt, in English, prefer ada over ADA. Capitalised ADA stands for the ticker symbol only. | | `ADA` | The ticker symbol for ada, like EUR or USD. | | `tAda` | Test ada is tAda, not tADA or TADA. See `ada`. | | `Basho` | The fourth era of the Cardano development focused on performance. Named after Matsuo Basho, a Japanese poet and the master of haiku. | | `Byron` | First era in Cardano development. Named after the Romantic poet who was the father of Ada Lovelace. | | `the Cardano Foundation` | Always use **the** Cardano Foundation. | | `DApp` | Note the capitalization: Decentralized Application. | | `dcSpark` | Creators of Flint Wallet and Milkomeda. Capitalized S, everything else lower case. | | `DRep` | Note the capitalization: Delegated Representative. `DRep` as an abbreviation for Delegated Representative follows standard practices for abbreviations in English: taking the first letter of each word. This makes it intuitive and clear in most contexts. It is also in line with the `DApp` abbreviation. In crypto, the lowercase “d“ is often used to signify “decentralized,” as in dApp (decentralized application) or dGov (decentralized governance). Using “dRep” might imply “decentralized representative”.| | `EMURGO` | All caps in line with EMURGO’s branding. | | `the Foundation` | Interchangeable with `the Cardano Foundation`, the is not capitalized, but Foundation should be. | | `GitHub` | Note the capitalized H. | | `Goguen` | The third era of the Cardano development focused on smart contracts. Named in honour of Joseph Goguen, an US computer scientist. | | `hard fork` | Two words. | | `IOHK` | IOHK is now IOG. | | `IOG` | IOG was IOHK. | | `Mainnet` | One word. Capitalise when it's a noun (the _Mainnet_) but not when it's an adjective (_mainnet_ functionality), qualified by another proper name (the Cardano _mainnet_), or used as a symbol (e.g. enable Marlowe on `mainnet`). | | `Ouroboros` | Ouroboros is a family of Cardano's consensus protocols. There are different flavors: Classic, Praos, Genesis, Chronos | | `sidechains` | One word. | | `stake pool` | Two words. | | `staking` | Try to avoid term `staking` without context as it is ambiguous. `staking` refers to the whole process of both delegating and setting up a pool but many people confuse this with the actual process of creating blocks. `delegating` means that people delegate their stake to a stake pool. | | `Strica` | Creators of Typhon Wallet, Cardanoscan and Flac Finance. Capitalized S, everything else lower case. | | `proof of stake` | Lower case. Hyphenate when followed by a noun: proof-of-stake systems. | | `proof of work` | Lower case. Hyphenate when followed by a noun: proof-of-work systems. | | `Testnet` | One word. Capitalise when it's a particular testnet (e.g. Preview _testnet_) but not when it's an adjective (e.g. _testnet_ functionality) or referring to more than one (e.g. new iterations of the _testnets_). | | `use case` | Not use-case. | | `Voltaire` | The fifth era of the Cardano development focused on governance and treasury. Named after the French philosopher who prized criticism and argued for the separation of church and state. | | `white paper` | Two words. | --- ## Documentation The Cardano Developer Portal documentation is organized by what you want to do. Pick your path below. ## Build on Cardano **[Start here](/docs/developers/)** if you want to build applications on Cardano. The docs are a curriculum of seven modules that build on each other, from blockchain fundamentals through transactions, tokens, staking and governance, and smart contracts to production dApps. There is also a standalone [Exchange integrations](/docs/developers/exchange-integrations) guide for custodial platforms. ## Operate a stake pool **[The operator handbook](/docs/operators/)** covers the full lifecycle of running a Cardano stake pool: hardware and key management basics, installing and running the node, registering your pool, monitoring, security hardening, and your role in on-chain governance. ## Community - [Developer community](/docs/community/cardano-developer-community) lists the forums, chats, and weekly office hours where Cardano developers gather. - [Funding and grants](/docs/community/funding) maps the ways to fund your project, from community grants to the on-chain treasury. ## Contribute The portal is open source and evolved by the community. [How to contribute](/docs/contribute/portal-contribute) explains the workflow, and the [style guide](/docs/contribute/portal-style-guide) covers writing conventions. ## More on the portal Beyond the documentation: - [Builder Tools](/tools) is a curated directory of the tools, SDKs, and APIs the ecosystem builds with. - [Templates](/templates) offers starter templates to clone and build on. - [Developer Blog](/blog) carries announcements, technical deep dives, and community spotlights.