Skip to content

Tutorial 1 — Your First App Ledger

Open this outcome in App-Chain Studio

  • Goal: run three members on one machine and watch them finalize the same event.
  • You’ll learn: how a submission becomes final, what AGREED means, how to get a proof for a message, and what a restart keeps.
  • Before you start: an extracted Yano X JVM distribution, Java 25, curl, jq, and python3. The launcher uses HTTP ports 7070–7072 and node ports 13337–13339; if they are busy it picks the next free range. No earlier tutorial is needed.
  • Level: beginner
  • Time: about 15 minutes for sections 1–5. The optional extras add 10–15 minutes.
  • Outcome: three members finalize the same ordered event, expose the same state root, and keep both across a restart.

This tutorial uses Yano’s self-contained Cardano devnet and the built-in ordered-log state machine. It needs no external Cardano node, wallet, funds, Kafka, or plugin.

Yano’s tooling calls an app ledger an app chain, so the commands below say appchain.

Here is the whole tutorial in one picture. Each step shows the command you run at that point. The “What if” scenarios show what happens when a member is down.

Your first finalized eventIllustrative data

What you're seeing: the three-node cluster from this tutorial, with the command you run at each step. Pick a step, press Play, or switch to “Your terminal” to see only what the commands print.

  1. Start The launcher starts node 0 first. It produces blocks for the private Cardano devnet and is also a member. After a 25-second warm-up, nodes 1 and 2 join it. The threshold defaults to a majority, N/2 + 1 in whole numbers: 2 of 3.
    ./yano.sh appchain cluster start 3
  2. Check agreement Status asks every running node for each chain's tip and state root, then compares the roots. AGREED means all of them report the same root for that chain. It is a comparison, not a vote.
    ./yano.sh appchain cluster status
  3. Submit The launcher sends a topic and a body to node 1. Node 1 checks the command, signs the envelope with its own member key, keeps it in its pending pool, and answers 202 with a message id. You sign nothing, and 202 means queued, not final.
    ./yano.sh appchain cluster submit orders-chain orders \
      '{"event":"order-created","orderId":"A-1001","quantity":4}' --node 1
  4. Gossip Node 1 gossips the envelope to nodes 0 and 2. Each checks the signature and that node 1 is a member, then holds the message in memory until a block includes it.
  5. Propose orders-chain uses a fixed sequencer, and the launcher names node 0 as its proposer, so node 0 leads. On its next tick (every second on this chain) it builds block h, applies it to compute the state root, and sends the block with its PREPARE vote.
  6. Vote Nodes 1 and 2 check the block and apply it themselves. Each votes PREPARE only if its own state root matches byte for byte. Two PREPARE votes reach the threshold and form a PreparedQC, and the members then sign COMMIT.
  7. Certify When COMMIT votes reach 2 of 3, the member holding them assembles the finality certificate and shares it. Every node writes block h and its new root in one atomic batch. The block is final, and there is no rollback below finality.
    curl -s http://127.0.0.1:7070/api/v1/app-chain/chains/orders-chain/blocks | jq .
  8. Agree again Every node now reports the new tip of orders-chain and the same root, so status prints AGREED again. Agreement after new traffic is the result this tutorial is about.
    ./yano.sh appchain cluster status

What if: One member is down

With a threshold of 2, any two of the three members can make a block final.

  1. Node 2 stops Suppose node 2's process stops after the cluster started. Two members remain, and the threshold is still 2.
  2. Submit You submit through node 1 as before. Node 1 signs the envelope, answers 202, and gossips it to node 0.
    ./yano.sh appchain cluster submit orders-chain orders \
      '{"event":"order-created","orderId":"A-1001","quantity":4}' --node 1
  3. Propose Node 0 builds block h and sends it with its PREPARE vote. Only node 1 can answer.
  4. Vote Node 1 re-executes the block and votes. Node 0's vote and node 1's vote are two of three, so they form a PreparedQC and then a COMMIT quorum.
  5. Final without node 2 Block h is final on nodes 0 and 1. When node 2 runs again, it fetches the missing blocks from a peer, verifies each block and its certificate, and re-applies them before it catches up.

What if: Threshold 3, one member down

When every member must vote, one missing member stops finality.

  1. Start with threshold 3 The threshold is part of the cluster's identity, so a retained cluster refuses a different one. Start a second cluster in a new data directory instead. Every block now needs three COMMIT votes.
    export YANO_CLUSTER_DIR=/tmp/yano-tutorial-threshold-3
    ./yano.sh appchain cluster start 3 --threshold 3
  2. Node 2 stops Suppose node 2's process stops after the cluster started. Two members remain, and the threshold is still 2.
  3. Submit You submit through node 1 as before. Node 1 signs the envelope, answers 202, and gossips it to node 0.
    ./yano.sh appchain cluster submit orders-chain orders \
      '{"event":"order-created","orderId":"A-1001","quantity":4}' --node 1
  4. Propose Node 0 builds block h and sends it with its PREPARE vote. Only node 1 can answer.
  5. Stalled Only two PREPARE votes exist, so no PreparedQC forms. The round times out, but moving to a new view also needs three signed timeouts. Nothing becomes final until node 2 returns, and the message expires from the pools after 10 minutes by default.
    • fails: PREPARE votes reach the threshold of 3 (only 2 arrive)
    • fails: Signed timeouts reach 3 for a new view (only 2 arrive)
  • your terminal
  • proposer for orders-chain
  • member node
  • final
  • offline or stalled

Every tutorial runs ./yano.sh from the directory that contains it. Choose one setup.

From an extracted release, use its top-level directory, the one containing yano.sh and yano.jar:

Terminal window
cd /path/to/extracted/yano-x-jvm-<version>
./yano.sh appchain help

From a source checkout, follow the Yano X distribution build, extract the JVM archive it produces, and use that top-level directory. Yano X has no :app task. For the curated thirteen-chain demo, use the showcase quickstart instead.

The remaining commands are the same for both setups:

Terminal window
export YANO_CLUSTER_DIR=/tmp/yano-tutorial-first-chain
./yano.sh appchain cluster start 3

The launcher starts:

  • node 0 as the local Cardano devnet producer and a member. It is also the proposer for orders-chain;
  • nodes 1 and 2 as members that follow node 0’s devnet; and
  • the three chains defined in config/application-appchain.yml: orders-chain (ordered-log), registry-chain (kv-registry, used in tutorial 2), and effects-chain (approvals, used by the optional effects demo).

With three members, the threshold defaults to a majority: 2. Startup takes a minute or two, because node 0 runs for 25 seconds before the other nodes join.

✓ You should see

Starting 3-node app-chain cluster
...
chains : orders-chain registry-chain effects-chain
members : 3 threshold: 2
data : /tmp/yano-tutorial-first-chain
ports : http 7070-7072 n2n 13337-13339
...
Cluster up.

The commands below reach node n on HTTP port HTTP_BASE + n. Set HTTP_BASE to the first port of the range the launcher printed. It is 7070 unless the default ports are busy and the launcher printed a line such as HTTP range 7070-7072 unavailable; using 7073-7075:

Terminal window
HTTP_BASE=7070
Terminal window
./yano.sh appchain cluster status

✓ You should see each node marked [ready], then:

Consistency (per chain, roots must match across nodes):
orders-chain: AGREED (...)
registry-chain: AGREED (...)
effects-chain: AGREED (...)

AGREED means every running node reports the same authenticated state root for that chain. It is more than “three processes are running”, but it is a comparison, not a vote: it compares current roots, not heights.

Open the status pages if you prefer a UI. With the default ports they are:

Submit through node 1 rather than through the proposer:

Terminal window
./yano.sh appchain cluster submit orders-chain orders \
'{"event":"order-created","orderId":"A-1001","quantity":4}' \
--node 1

✓ You should see submitted <message id>... to orders-chain.

You did not sign anything. Node 1 checked the command, signed the envelope with its own member key, and gossiped it. Node 0 put it in a block, two of the three members voted for that block, and every member applied the same bytes. The submission itself only means “queued on node 1”.

After a few seconds, inspect the finalized blocks and agreement:

Terminal window
curl -s "http://127.0.0.1:$HTTP_BASE/api/v1/app-chain/chains/orders-chain/blocks" | jq .
./yano.sh appchain cluster status

✓ You should see a block with "messageCount": 1 and certSignatures of at least 2, then orders-chain: AGREED (...) with the same, higher tip on every node.

For a proof, call the same public API directly. This time, submit through node 2:

Terminal window
RESPONSE=$(curl -s -X POST \
"http://127.0.0.1:$((HTTP_BASE + 2))/api/v1/app-chain/chains/orders-chain/messages" \
-H 'Content-Type: application/json' \
-d '{"topic":"orders","body":"{\"event\":\"packed\",\"orderId\":\"A-1001\"}"}')
echo "$RESPONSE" | jq .
MESSAGE_ID=$(echo "$RESPONSE" | jq -r .messageId)

✓ You should see a JSON object with messageId, "chainId": "orders-chain", and "topic": "orders".

Wait until the message is final. A finalized message has a block height:

Terminal window
HEIGHT=0
for attempt in $(seq 1 30); do
HEIGHT=$(curl -s "http://127.0.0.1:$HTTP_BASE/api/v1/app-chain/chains/orders-chain/messages/$MESSAGE_ID" \
| jq -r '.height // 0' 2>/dev/null)
[ "${HEIGHT:-0}" -gt 0 ] && break
sleep 1
done
echo "final at height $HEIGHT"

✓ You should see final at height followed by a number greater than 0.

Then request a typed proof that the message is recorded:

Terminal window
curl -s -X POST \
"http://127.0.0.1:$HTTP_BASE/api/v1/app-chain/chains/orders-chain/proof-subjects/finalized-message-v1/proof" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg id "$MESSAGE_ID" '
{coordinates:{"message-id":$id}, view:"latest",
claim:{claimId:"recorded",operands:{}}, includeEvidence:false}')" \
| jq '{stateRoot:.proof.stateRoot,presence:.proof.presence,position:.fact.fields,claim:.claimResult.satisfied}'

✓ You should see "presence": "PRESENT", "claim": true, and a position with the message’s height, index, topic, and sender.

The typed subject resolves the public message ID to its namespaced physical state key and connects that record to the member’s committed state root. The sender is node 2’s member key, because node 2 accepted and signed the envelope; ./yano.sh appchain cluster keys prints each node’s key. Tutorial 7 connects a state root to a Cardano anchor.

stop keeps both the L1 and the app ledger data:

Terminal window
./yano.sh appchain cluster stop
./yano.sh appchain cluster start 3
./yano.sh appchain cluster status

✓ You should see stopped 3 node(s), then Cluster up., then the same tips and AGREED roots as before the stop.

The retained tips and roots return unchanged before any new traffic is finalized. Explore what the launcher keeps on disk:

What the cluster keeps on diskConcept
CLUSTER RECORDSINSIDE EACH NODE FOLDERcluster-identity.jsonL1 identitycluster-appchain-identity.jsonmembers, threshold, chainscluster.envports and networknodeN.pidprocess recordschainstate/Cardano L1 stateshelley-genesis.jsondevnet genesis copyappchain-chainstate/one store per chainorders-chain/registry-chain/effects-chain/appchain-indexers/rebuildable indexesnode.logthis node's logCLUSTER RECORDSINSIDE EACH NODE FOLDERcluster-identity.jsonL1 identitycluster-appchain-identity.jsonmembers, chainscluster.envports and networknodeN.pidprocess recordschainstate/Cardano L1 stateshelley-genesis.jsondevnet genesis copyappchain-chainstate/one store per chainorders-chain/registry-chain/effects-chain/appchain-indexers/rebuildable indexesnode.logthis node's log
  • Cluster records: cluster-identity.json, cluster-appchain- identity.json, cluster.env, nodeN.pid.
  • Inside each node folder: chainstate/, shelley-genesis.json, appchain-chainstate/, orders-chain/, registry-chain/, effects-chain/, appchain-indexers/, node.log.
  • appchain-chainstate/ → orders-chain/.
  • appchain-chainstate/ → registry-chain/.
  • appchain-chainstate/ → effects-chain/.
cluster-identity.json
Records the Cardano network and, on devnet, the genesis source. Starting this directory again with a different network or devnet genesis is refused instead of overwriting state.
cluster-appchain- identity.json
Records the network, member keys, threshold, proposer, chain ids, and anchor settings. Starting again with a different node count or --threshold fails with “app-chain identity differs from retained state”. Use a new YANO_CLUSTER_DIR for a different cluster.
cluster.env
The network, ports, and anchor settings chosen by start. Later commands such as status and submit read it, so they find a cluster that moved to a free port range.
nodeN.pid
node0.pid, node0.pid.meta, and the same for each node. stop uses them to stop exactly the processes it started, and clean refuses to delete data while a record it cannot trust remains.
chainstate/
The node's authoritative Cardano L1 state. Node 0 produces the private devnet; nodes 1 and 2 sync it from node 0.
shelley-genesis.json
The devnet genesis. Node 0 writes it on first start and the launcher copies it to each follower. A retained follower whose genesis differs from node 0's is refused.
appchain-chainstate/
Authoritative app ledger state, kept apart from the L1 state. It holds one store per hosted chain.
orders-chain/
One store for one chain: its blocks, certificates, and the authenticated state behind its root. Each chain's genesis and history are independent of the others. This chain runs ordered-log.
registry-chain/
One store for one chain: its blocks, certificates, and the authenticated state behind its root. Each chain's genesis and history are independent of the others. This chain runs kv-registry.
effects-chain/
One store for one chain: its blocks, certificates, and the authenticated state behind its root. Each chain's genesis and history are independent of the others. This chain runs approvals.
appchain-indexers/
Read indexes for indexer plugins, such as the EUTxO indexer. They are derived from finalized blocks: delete and rebuild them, and never restore them as authoritative state.
node.log
Everything the node logs. ./yano.sh appchain cluster logs 1 prints the last 60 lines of node 1's log; add -f to follow it.
The launcher writes everything below $YANO_CLUSTER_DIR, which defaults to /tmp/yano-appchain-cluster. Each node has its own folder, node0/, node1/, and node2/, with the same layout. stop keeps all of it; clean stops the nodes and deletes the whole directory.
  • cluster record
  • Cardano L1
  • app ledger state
  • node-local, rebuildable

This is a useful distinction:

  • restart: the same chain identity and retained state;
  • clean: delete the data and start a new identity and history next time.

The node count and threshold are part of the retained identity. To try different values, use a new YANO_CLUSTER_DIR (see Go deeper).

  • A submission can enter through any member, and that member signs it.
  • A threshold of members, not one REST server, finalizes the ordered block.
  • All members deterministically derive the same state root.
  • A message has a proof against that root.
  • A retained restart recovers the same application history.

You did not yet prove Cardano settlement or the truth of the order fields. Those are separate trust layers.

These sections reuse the running cluster. Skip them if you are short of time.

The distribution includes a parallel load driver for the running local cluster. Start with a bounded workload of 500 messages, 10 concurrent submitters, and payloads of about 256 bytes:

Terminal window
./yano.sh appchain cluster loadtest orders-chain -n 500 -c 10 -s 256

Plain load-test mode suits any-bytes machines such as ordered-log. It submits numbered UTF-8 bodies on the load topic, waits for the pending pool to drain, and reports two different rates:

==================== throughput ====================
submitted attempts : 500
accepted (2xx) : 500
dropped (429 pool): 0
errors : 0
SUBMIT rate : ... msg/s
finalized msgs : 500 in ... block(s)
FINALIZE rate : ... msg/s
end-to-end rate : ... msg/s
====================================================
  • SUBMIT rate measures how quickly an ingress REST API accepts messages.
  • FINALIZE rate measures how quickly messages enter threshold-certified blocks; this is the meaningful chain-throughput measurement.
  • dropped (429 pool) means backpressure worked because the pending pool filled. Reduce concurrency or tune the pool and block limits deliberately.
  • errors are non-backpressure request failures and should be investigated.

Spread submissions across all ready members to exercise gossip from every ingress path, then confirm that every member still exposes the same root:

Terminal window
./yano.sh appchain cluster loadtest orders-chain -n 1000 -c 20 -s 256 --spread
./yano.sh appchain cluster status

This is a functional throughput exercise, not a production benchmark. Results depend on the machine, the JVM, the devnet producer, the block interval, the payload size, and the member count. Record those inputs when comparing runs. The test appends real finalized messages to the retained orders-chain history.

For capacity settings and workload boundaries, see the ordered-log reference.

The default effects-chain can demonstrate the full emit, external-worker, result, and proof lifecycle without Kafka, S3, IPFS, or a real webhook:

Terminal window
./yano.sh appchain cluster effect demo
./yano.sh appchain cluster effect demo "order A-1001 approved"

✓ You should see Effect emitted effects-chain height=..., Delivery CONFIRMED, and Proof AVAILABLE (...).

With no argument, the demo uses hello from Yano effects; one quoted argument replaces that message. Briefly, the command:

  1. creates a unique one-approval item on the separate effects-chain;
  2. wraps your text in a JSON payload, then submits PROPOSE and APPROVE commands to its approvals state machine;
  3. keeps the item decision APPROVED and emits one generic app-final demo.webhook effect when the approval threshold is reached;
  4. acts as a simulated external worker that claims the effect and reports a synthetic successful delivery; no real webhook is called; and
  5. feeds the result back through the effect lifecycle and checks that the finalized effect proof is available.

The supplied text is illustrative data. It is not linked to an event on orders-chain, even if it contains an order id. A production workflow should carry an explicit business id or finalized source message id in its committed command or effect payload. The proof establishes that the effect intent was committed; a real external action also depends on a trusted executor and a verifiable receipt.

For the complete lifecycle and production webhook configuration, continue with webhook effects.

You can also govern, start, catch up, and verify a fourth node on the same host:

Terminal window
./yano.sh appchain cluster node join 3
./yano.sh appchain cluster status

With the default 2-of-3, a fourth member would leave 2-of-4, which cannot certify blocks (a threshold must be more than half the members). The launcher therefore raises each chain’s threshold to 3 first, advances the idle chains until that change is active, and only then records the new member with three approvals.

An idle chain makes no blocks, so the launcher advances it with one command per block. Each command names a fresh random key or item (cluster.epoch-advance.<random>), so it cannot touch your records: on registry-chain it deletes a key that does not exist, and on effects-chain it rejects an item that does not exist. orders-chain has no command that changes nothing, so the launcher appends entries on the cluster.epoch-advance topic; your existing entries are unchanged.

✓ You should see raising 'orders-chain' threshold 2 -> 3 first, then membership approval 3/3 for each chain, and finally node 3 joined and caught up with member key .... The new member votes from the activation height each chain reports.

For an externally managed node, the lower-level appchain cluster member add <public-key> command records membership but does not configure or start the external process. Same-host node join performs both steps.

Terminal window
./yano.sh appchain cluster clean
unset YANO_CLUSTER_DIR

clean stops the nodes and deletes everything under YANO_CLUSTER_DIR, including the chain identities.

You seeWhat it means and what to do
HTTP range 7070-7072 unavailable; using ...Another process holds the default ports. Set HTTP_BASE to the first port of the printed range.
app-chain identity differs from retained state; restore the original profile or use a new data directoryYou started the same YANO_CLUSTER_DIR with a different node count or --threshold. Start it with the original values, or export a new YANO_CLUSTER_DIR.
node 1 is already runningThe cluster is still running. Run ./yano.sh appchain cluster stop first.
node 0 not ready within 180s or node 2 not ready after 3 attemptsThe message names the node’s log file. Read its first error, for example with ./yano.sh appchain cluster logs 2.
orders-chain: MISMATCHA node can be one block behind while status reads it. Run status again after a few seconds. If it persists, check that node’s log.
config not found: .../config/application-appchain.ymlYANO_HOME points somewhere other than the distribution. Run unset YANO_HOME, then retry from the distribution’s top-level directory.
  • Try a stricter threshold. The threshold is part of the cluster’s identity, so start a second cluster in a new directory:

    Terminal window
    ./yano.sh appchain cluster stop
    export YANO_CLUSTER_DIR=/tmp/yano-tutorial-threshold-3
    ./yano.sh appchain cluster start 3 --threshold 3

    The start banner shows threshold: 3, and every block now needs all three votes. The “Threshold 3, one member down” scenario above shows why one stopped member then halts finality. Remove this cluster with ./yano.sh appchain cluster clean.

  • Read Yano’s consensus guide for the proposer, votes, certificates, replay, and catch-up.

Next: Tutorial 2 — registry ownership and state proofs.