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.
Your terminal./yano.sh appchain
Node 0devnet producer · proposer
Node 1receives your event
Node 2member
Your terminal → Node 1: POST {topic, body}
Node 1 → Your terminal: 202 + messageId
Node 1 → Node 0: gossip envelope
Node 1 → Node 2: gossip envelope
Node 0 → Node 1: block h + PREPARE
Node 0 → Node 2: block h + PREPARE
Node 1 → Node 0: PREPARE, then COMMIT
Node 2 → Node 0: PREPARE, then COMMIT
Node 0 → Node 1: finality certificate
Node 0 → Node 2: finality certificate
Cluster up.threshold: 2
AGREEDthree chains
202 Acceptednot final yet
AGREEDsame root on 3 nodes
Readyhttp 7070
Pending pool1 message
Block h · view 0own root 7f3a…
Final at hroot 7f3a…
Readyhttp 7071
Envelopesigned by node 1
Re-executed ✓own root 7f3a…
Final at hroot 7f3a…
Readyhttp 7072
Pending pool1 message
Re-executed ✓own root 7f3a…
Final at hroot 7f3a…
StartThe 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.Prints members : 3 threshold: 2, then Cluster up.
./yano.sh appchain cluster start 3
Check agreementStatus 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.Prints orders-chain: AGREED (…), and the same for registry-chain and effects-chain.
./yano.sh appchain cluster status
SubmitThe 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.Prints submitted <message id>... to orders-chain.
GossipNode 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.Nothing new yet. The message is waiting in the pools.
Proposeorders-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.Still pending.
VoteNodes 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.Still pending. Every member is re-running the block.
CertifyWhen 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.The newest block summary shows certSignatures of at least 2.
Agree againEvery 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.Prints orders-chain: AGREED (…) with a higher tip on every node.
./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.
Your terminal./yano.sh appchain
Node 0devnet producer · proposer
Node 1receives your event
Node 2member
Your terminal → Node 1: POST {topic, body}
Node 1 → Your terminal: 202 + messageId
Node 0 → Node 1: block h + PREPARE
Node 1 → Node 0: PREPARE, then COMMIT
Node 0 → Node 1: finality certificate
202 Acceptednot final yet
Block h · view 0own root 7f3a…
Final at h2 of 3
Envelopesigned by node 1
Re-executed ✓own root 7f3a…
Final at h2 of 3
Offlineno votes
Offlineno votes
Behindcatches up later
Node 2 stopsSuppose node 2's process stops after the cluster started. Two members remain, and the threshold is still 2.Status no longer shows node 2 as ready.
SubmitYou submit through node 1 as before. Node 1 signs the envelope, answers 202, and gossips it to node 0.Prints submitted <message id>... to orders-chain.
ProposeNode 0 builds block h and sends it with its PREPARE vote. Only node 1 can answer.Still pending.
VoteNode 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.Still pending. Every member is re-running the block.
Final without node 2Block 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.Your message is final. Node 0 and node 1 agree.
What if: Threshold 3, one member down
When every member must vote, one missing member stops finality.
Your terminal./yano.sh appchain
Node 0devnet producer · proposer
Node 1receives your event
Node 2member
Your terminal → Node 1: POST {topic, body}
Node 1 → Your terminal: 202 + messageId
Node 0 → Node 1: block h + PREPARE
Cluster up.threshold: 3
202 Acceptednot final yet
Not finaltip unchanged
Ready
Block h · view 0own root 7f3a…
Waiting2 of 3 votes
Ready
Envelopesigned by node 1
Waiting2 of 3 votes
Ready
Offlineno votes
Offlineno votes
Start with threshold 3The 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.Prints members : 3 threshold: 3.
Node 2 stopsSuppose node 2's process stops after the cluster started. Two members remain, and the threshold is still 2.Status no longer shows node 2 as ready.
SubmitYou submit through node 1 as before. Node 1 signs the envelope, answers 202, and gossips it to node 0.Prints submitted <message id>... to orders-chain.
ProposeNode 0 builds block h and sends it with its PREPARE vote. Only node 1 can answer.Still pending.
StalledOnly 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.The submission was accepted, but the tip of orders-chain does not move.
✗fails: PREPARE votes reach the threshold of 3 (only 2 arrive)
✗fails: Signed timeouts reach 3 for a new view (only 2 arrive)
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.shappchainhelp
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:
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.
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:
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:
✓ You should seesubmitted <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:
✓ 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.
✓ 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.
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).
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:
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:
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.
✓ You should seeEffect 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:
creates a unique one-approval item on the separate effects-chain;
wraps your text in a JSON payload, then submits PROPOSE and APPROVE
commands to its approvals state machine;
keeps the item decision APPROVED and emits one generic app-final
demo.webhook effect when the approval threshold is reached;
acts as a simulated external worker that claims the effect and reports a
synthetic successful delivery; no real webhook is called; and
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.shappchainclusternodejoin3
./yano.shappchainclusterstatus
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 seeraising '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.
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.