Tutorial 3 — Choose a Stock State Machine
Open a stock approval workflow in App-Chain Studio
- Level: beginner for selection, advanced for wire integration
- Outcome: choose the smallest built-in deterministic model that matches your application, encode its commands, and configure a chain for it.
Selecting a state machine is a consensus decision. Every member must use the same machine id and settings. For a new chain it is configuration; for an existing chain, changing it needs a governed profile activation or a new chain.
1. Answer three questions
Section titled “1. Answer three questions”What you're seeing: two or three questions about what the ledger must keep and who decides. Each answer names a machine, its maturity and recipe, where it keeps state, and what its proofs can and cannot show.
What must the ledger keep for you?
- Only the order of events; my applications read the payloadsordered-log
The built-in machine. It records where each message was finalized and never rejects one for business reasons.
- Maturity
- stable
- Recipe
audit-log- State key
sha256("~yano/finalized-message/v1/" ‖ message id)- Proof subject
finalized-message-v1, claimrecorded- Proves
- the height, index, topic and sender of a finalized message
- Cannot prove
- anything about what the payload means
- Current records that writers can change
Who may change a record?
- The member that wrote it first; that is enoughkv-registry
The first member to write a key owns it until the key is deleted.
- Maturity
- stable
- Recipe
owned-registry- State key
- the key bytes; value
[owner, value] - Proof subject
registry-entry-v1, claimsowner-equals,value-digest-equals- Proves
- the current owner and value, or that the key is absent
- Cannot prove
- earlier values, or any transfer of ownership
- It depends on the collection: open, owner, member, a role or an approval; with revisions, tombstones and atomic batchesauthenticated-map
Named collections, each with its own authorization, bounds, encoding and optional validator, all fixed at genesis.
- Maturity
- preview
- Recipe
authenticated-map- State key
- collection and key, inside the composite state
- Proof subject
authenticated-map-entry-v1, claimsstatus,revision-exact,value-digest- Proves
- ACTIVE, REVOKED or ABSENT, the revision, and the value digest
- Cannot prove
- rules that relate two entries; it has no secondary indexes
- The member that wrote it first; that is enough
- Quantities that move between accountsbalances
A configured minter mints. A member spends only from its own account. Balances never go negative.
- Maturity
- stable
- Recipe
- none; configure
state-machine: balances - State key
b/<account>- Proof subject
account-balance-v1, claimsexact,minimum,maximum- Proves
- the balance of one account
- Cannot prove
- total supply, or anything about Cardano assets
- An ordered history of document hashes per product or casedoc-trail
Each append chains an entry hash and its author into a per-entity head. Documents stay off the ledger.
- Maturity
- stable
- Recipe
document-trail- State key
e/<entityId>- Proof subject
document-head-v1, claimsrevision-exact,revision-minimum,digest-equals- Proves
- the entry count and the chained head hash
- Cannot prove
- that a referenced document is still available
- A decision that several approvers make
Who approves?
A member is a node that votes on blocks. A business actor is a person, service or device with its own key.
- The member nodes themselvesapprovals
A proposal becomes APPROVED when enough distinct members approve it. One member rejection ends it.
- Maturity
- stable
- Recipe
approval-workflow- State key
i/<itemId>- Proof subject
basic-approval-outcome-v1, claimsstatus,quorum-reached,payload-digest- Proves
- the status, approval count and payload hash of one item
- Cannot prove
- which person stands behind a member key
- Business actors with roles and organizations, separate from the membersrole-approvals
Actors sign statements over a payload hash; a governed policy decides. Any member can relay them.
- Maturity
- preview
- Recipe
role-approval- State key
- proposal record in the
role-approvalscomponent - Proof subject
role-approval-outcome-v1, claimsstatus,payload-digest- Proves
- the policy outcome for one payload hash
- Cannot prove
- what the payload says; the machine stores and runs no payload
- The member nodes themselves
What must the ledger keep for you?
Who may change a record?
Who approves?
A member is a node that votes on blocks. A business actor is a person, service or device with its own key.
The built-in machine. It records where each message was finalized and never rejects one for business reasons.
- Maturity
- stable
- Recipe
audit-log- State key
sha256("~yano/finalized-message/v1/" ‖ message id)- Proof subject
finalized-message-v1, claimrecorded- Proves
- the height, index, topic and sender of a finalized message
- Cannot prove
- anything about what the payload means
The first member to write a key owns it until the key is deleted.
- Maturity
- stable
- Recipe
owned-registry- State key
- the key bytes; value
[owner, value] - Proof subject
registry-entry-v1, claimsowner-equals,value-digest-equals- Proves
- the current owner and value, or that the key is absent
- Cannot prove
- earlier values, or any transfer of ownership
Named collections, each with its own authorization, bounds, encoding and optional validator, all fixed at genesis.
- Maturity
- preview
- Recipe
authenticated-map- State key
- collection and key, inside the composite state
- Proof subject
authenticated-map-entry-v1, claimsstatus,revision-exact,value-digest- Proves
- ACTIVE, REVOKED or ABSENT, the revision, and the value digest
- Cannot prove
- rules that relate two entries; it has no secondary indexes
A proposal becomes APPROVED when enough distinct members approve it. One member rejection ends it.
- Maturity
- stable
- Recipe
approval-workflow- State key
i/<itemId>- Proof subject
basic-approval-outcome-v1, claimsstatus,quorum-reached,payload-digest- Proves
- the status, approval count and payload hash of one item
- Cannot prove
- which person stands behind a member key
Actors sign statements over a payload hash; a governed policy decides. Any member can relay them.
- Maturity
- preview
- Recipe
role-approval- State key
- proposal record in the
role-approvalscomponent - Proof subject
role-approval-outcome-v1, claimsstatus,payload-digest- Proves
- the policy outcome for one payload hash
- Cannot prove
- what the payload says; the machine stores and runs no payload
A configured minter mints. A member spends only from its own account. Balances never go negative.
- Maturity
- stable
- Recipe
- none; configure
state-machine: balances - State key
b/<account>- Proof subject
account-balance-v1, claimsexact,minimum,maximum- Proves
- the balance of one account
- Cannot prove
- total supply, or anything about Cardano assets
Each append chains an entry hash and its author into a per-entity head. Documents stay off the ledger.
- Maturity
- stable
- Recipe
document-trail- State key
e/<entityId>- Proof subject
document-head-v1, claimsrevision-exact,revision-minimum,digest-equals- Proves
- the entry count and the chained head hash
- Cannot prove
- that a referenced document is still available
2. Compare the stock machines
Section titled “2. Compare the stock machines”| Machine | Use it when | Who may change state | What a proof shows |
|---|---|---|---|
ordered-log | You need one agreed order of opaque events | Any member’s message | A message’s height, index, topic and sender |
kv-registry | You need mutable named records | The first writer of a key | Current owner and value per key |
authenticated-map | You need several collections with their own rules | Per collection: open, owner, member, role or approval | Status, revision and value digest per entry |
approvals | Member nodes are the approvers | Distinct member keys | Status, approval count and payload hash |
role-approvals | Approvers are business actors, not member nodes | Governed actors, organizations and roles | Policy outcome and payload hash |
balances | You need internal credits or netting | A minter mints; a member spends its own account | Balance per account |
doc-trail | You need an ordered history per product or case | Any member appends | Entry count and chained head |
Two stock workflows are composites rather than single machines:
| Machine id | Use it when | Notes |
|---|---|---|
composite with machines.composite.preset: evidence-v1-gated | Approval coordinates S3, IPFS and Kafka publication | One root across components and effects; see Tutorial 4 |
role-evidence | The evidence flow needs business actors and roles | Governed actors, organizations and policies; see Tutorial 5 |
3. Encode a command
Section titled “3. Encode a command”Stock machines read bounded, canonical CBOR commands. Use their Java contracts or the tutorial helper; never serialize arbitrary Java objects.
What you're seeing: one kv-registry command from encoding to state. Try a “What if” to see which admission rule refuses a bad body, in the order the decoder checks it.
- Write the command A
kv-registryPUT is a CBOR array: operation0, the key bytes, and the value bytes. The machine reads only the body; the topic is a label.python3 docs/appchain/tutorials/tools/stdlib_command.py kv-registry put supplier-42 --value-text active - Encode
83opens an array of three items.00is operation 0.4bstarts an 11-byte string, the key.46starts a 6-byte string, the value. Stock contracts accept only this shortest, canonical form. - Admit The receiving member decodes the body against the next block. Every rule passes, so it answers 202 and gossips the message.
- passes: One canonical CBOR array of three items
- passes: Key has 1 to 256 bytes
- passes: Operation is 0 (PUT) or 1 (DELETE), and a DELETE carries an empty value
- passes: A PUT value is not empty and matches
value-format
- Apply In the final block every member runs the same decision: the key has no entry, so the sender becomes its owner. Only now has state changed.
- passes: The key has no entry, or the sender owns it
kv-registry state after the block Key Value supplier-42[member 1 key, "active"]
What if: A DELETE with a value
Operation 1 must carry an empty value. The helper always sends one; this body was written by hand.
- Write the command A DELETE of
supplier-42that also carries the valuex. - Refused at admission The decoder rejects a DELETE whose value is not empty. Admission rejections keep only a safe code, so REST answers 400 with
APPLICATION_REJECTED. Nothing is pooled.- passes: One canonical CBOR array of three items
- passes: Key has 1 to 256 bytes
- fails: Operation is 0 (PUT) or 1 (DELETE), and a DELETE carries an empty value
APPLICATION_REJECTED - not reached: A PUT value is not empty and matches
value-format
What if: A 300-byte key
Stock keys are bounded so that every entry can be proved through the standard proof endpoint.
- Write the command A PUT whose key is 300 bytes long.
- Refused at admission Keys must have 1 to 256 bytes. The member refuses the body before it reaches a block.
- passes: One canonical CBOR array of three items
- fails: Key has 1 to 256 bytes
APPLICATION_REJECTED - not reached: Operation is 0 (PUT) or 1 (DELETE), and a DELETE carries an empty value
- not reached: A PUT value is not empty and matches
value-format
What if: Non-canonical CBOR
The same values in a longer encoding are different bytes, so they are refused.
- Write the command The PUT again, but operation 0 is written as
18 00, a one-byte integer, instead of00. - Refused at admission The decoder re-encodes what it read and compares the bytes.
18 00is not the shortest form of 0, so the body is not canonical.- fails: One canonical CBOR array of three items
APPLICATION_REJECTED - not reached: Key has 1 to 256 bytes
- not reached: Operation is 0 (PUT) or 1 (DELETE), and a DELETE carries an empty value
- not reached: A PUT value is not empty and matches
value-format
- fails: One canonical CBOR array of three items
What if: Amount 0 (balances)
Every stock machine uses the same strict decoder. Here a balances MINT carries a zero amount.
- Write the command A
balancesMINT[0, "alice", 0]. The helper refuses it (“amount must be positive”), so this body was written by hand. - Refused at admission The
balancesdecoder requires a positive amount, so the receiving member refuses the body.- passes: One canonical CBOR array of three items
- passes: Operation is 0 (MINT) or 1 (TRANSFER)
- passes: Account is text, and
b/<account>fits in 256 bytes - fails: Amount is positive
APPLICATION_REJECTED
- your application
- member node
- deterministic state machine
- refused
Try the helper from the top-level directory of the extracted release:
TOOL=docs/appchain/tutorials/tools/stdlib_command.py
python3 "$TOOL" kv-registry put supplier-42 --value-text activepython3 "$TOOL" approvals propose release-7 --required 2 --payload-text gopython3 "$TOOL" balances mint alice 200python3 "$TOOL" doc-trail product-42 abcd --reference ''The command shapes are:
kv-registry [0, key, value] PUT [1, key, h''] DELETEapprovals [0, itemId, payload, requiredApprovals, deadline] PROPOSE [1, itemId] / [2, itemId] APPROVE / REJECTbalances [0, account, positiveAmount] MINT [1, account, positiveAmount] TRANSFER from the sender's accountdoc-trail [entityId, entryHash, reference] APPEND; reference "" when noneauthenticated-map and role-approvals carry signed actions and evidence; use
their CLI commands and Java authoring helpers described on their reference
pages. ordered-log takes any non-empty body.
4. Configure a chain for the local launcher
Section titled “4. Configure a chain for the local launcher”The cluster launcher reads config/application-appchain.yml in the release’s
top-level directory ($YANO_HOME/config/application-appchain.yml when you set
YANO_HOME). Under its single yano.app-chain root it already defines
orders-chain, registry-chain and effects-chain as chains[0] to
chains[2]. Add a fourth chain before you start a fresh cluster. Paste this
block inside that root, just above the # --- Add more chains here comment, at
the same four-space indentation as chains[2]:
chains[3]: chain-id: "workflow-chain" state-machine: approvals state: commitment-profile: mpf-blake2b256-v1 format-fingerprint: 91ee14091200f1e24659112d640e877e9177779dcc81dd06117f013e9190082b genesis-id: <output of: openssl rand -hex 32> membership: mode: governed block: interval-ms: 1000Do not start a second yano: block: YAML keeps only the last one, so the
nodes would start without any app chain. Check the file before you start:
./yano.sh appchain config validate --mode template \ --template-contract builtin:cluster config/application-appchain.yml✓ You should see a last line starting with
VALID_TEMPLATEanderrors=0. Then./yano.sh appchain cluster start 3listsworkflow-chainamong its chains.
The host refuses a chain without all three state.* settings, and the
validator does not catch that yet. Copy the profile and fingerprint from an
existing chain and give each new chain its own genesis id. The launcher injects
members, threshold, signing keys, peers and the fixed proposer; do not put that
material in the shared file.
A generated project, created with ./yano.sh appchain init, is different:
appchain render writes every chain setting, including the state identity,
and you never edit the rendered files by hand. To add a chain there, use the
add-chain workflow.
5. Configuration is not arbitrary composition
Section titled “5. Configuration is not arbitrary composition”Configuration selects and parameterizes semantics that already exist. It cannot safely express new component order or terminal transitions, because those change every member’s state root. Use, in order of preference:
- stock configuration when one machine or profile already fits;
- declarative bindings when stock components must react to each other’s events; see declarative bindings;
- a composite plugin when components need a new committed order or transition; or
- a custom state-machine plugin for new business state or rules.
Common mistakes
Section titled “Common mistakes”- Treating a REST API key as an approval identity.
- Changing machine settings on one member only.
- Treating
202, or a final message, as proof that state changed. - Putting large or secret documents in replicated command bodies.
- Adding network, clock, DNS or random behavior inside
apply(). - Reusing a machine id after changing its deterministic behavior.
Go deeper
Section titled “Go deeper”- State machines lists every machine with its maturity, state key and proof subject.
- Each reference page, for example
kv-registry, covers REST, Java, proofs and design choices. - The Yano consensus guide explains how blocks are ordered, re-executed and certified.
- For business actors, continue with domain-role approvals; for coordinated publication, with the evidence scenario.