Skip to content

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.

Which state machine?Decision aid

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 payloads
    ordered-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, claim recorded
    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 enough
      kv-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, claims owner-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 batches
      authenticated-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, claims status, 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
  • Quantities that move between accounts
    balances

    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, claims exact, 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 case
    doc-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, claims revision-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 themselves
      approvals

      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, claims status, 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 members
      role-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-approvals component
      Proof subject
      role-approval-outcome-v1, claims status, payload-digest
      Proves
      the policy outcome for one payload hash
      Cannot prove
      what the payload says; the machine stores and runs no payload
MachineUse it whenWho may change stateWhat a proof shows
ordered-logYou need one agreed order of opaque eventsAny member’s messageA message’s height, index, topic and sender
kv-registryYou need mutable named recordsThe first writer of a keyCurrent owner and value per key
authenticated-mapYou need several collections with their own rulesPer collection: open, owner, member, role or approvalStatus, revision and value digest per entry
approvalsMember nodes are the approversDistinct member keysStatus, approval count and payload hash
role-approvalsApprovers are business actors, not member nodesGoverned actors, organizations and rolesPolicy outcome and payload hash
balancesYou need internal credits or nettingA minter mints; a member spends its own accountBalance per account
doc-trailYou need an ordered history per product or caseAny member appendsEntry count and chained head

Two stock workflows are composites rather than single machines:

Machine idUse it whenNotes
composite with machines.composite.preset: evidence-v1-gatedApproval coordinates S3, IPFS and Kafka publicationOne root across components and effects; see Tutorial 4
role-evidenceThe evidence flow needs business actors and rolesGoverned actors, organizations and policies; see Tutorial 5

Stock machines read bounded, canonical CBOR commands. Use their Java contracts or the tutorial helper; never serialize arbitrary Java objects.

From command to bytesIllustrative data

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.

  1. Write the command A kv-registry PUT is a CBOR array: operation 0, 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
  2. Encode 83 opens an array of three items. 00 is operation 0. 4b starts an 11-byte string, the key. 46 starts a 6-byte string, the value. Stock contracts accept only this shortest, canonical form.
  3. 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
  4. 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
    KeyValue
    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.

  1. Write the command A DELETE of supplier-42 that also carries the value x.
  2. 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.

  1. Write the command A PUT whose key is 300 bytes long.
  2. 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.

  1. Write the command The PUT again, but operation 0 is written as 18 00, a one-byte integer, instead of 00.
  2. Refused at admission The decoder re-encodes what it read and compares the bytes. 18 00 is 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

What if: Amount 0 (balances)

Every stock machine uses the same strict decoder. Here a balances MINT carries a zero amount.

  1. Write the command A balances MINT [0, "alice", 0]. The helper refuses it (“amount must be positive”), so this body was written by hand.
  2. Refused at admission The balances decoder 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:

Terminal window
TOOL=docs/appchain/tutorials/tools/stdlib_command.py
python3 "$TOOL" kv-registry put supplier-42 --value-text active
python3 "$TOOL" approvals propose release-7 --required 2 --payload-text go
python3 "$TOOL" balances mint alice 200
python3 "$TOOL" doc-trail product-42 abcd --reference ''

The command shapes are:

kv-registry [0, key, value] PUT
[1, key, h''] DELETE
approvals [0, itemId, payload, requiredApprovals, deadline] PROPOSE
[1, itemId] / [2, itemId] APPROVE / REJECT
balances [0, account, positiveAmount] MINT
[1, account, positiveAmount] TRANSFER from the sender's account
doc-trail [entityId, entryHash, reference] APPEND; reference "" when none

authenticated-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: 1000

Do 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:

Terminal window
./yano.sh appchain config validate --mode template \
--template-contract builtin:cluster config/application-appchain.yml

✓ You should see a last line starting with VALID_TEMPLATE and errors=0. Then ./yano.sh appchain cluster start 3 lists workflow-chain among 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:

  1. stock configuration when one machine or profile already fits;
  2. declarative bindings when stock components must react to each other’s events; see declarative bindings;
  3. a composite plugin when components need a new committed order or transition; or
  4. a custom state-machine plugin for new business state or rules.
  • 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.