Skip to content

Tutorial 4 — Publish and Verify Immutable Evidence

Open the role-aware evidence recipe in App-Chain Studio

  • Level: beginner to advanced
  • Time: about 30 minutes for the first build
  • Outcome: publish one immutable document through a threshold-approved workflow, preserve it in S3-compatible storage and IPFS, notify Kafka, and verify the chain, connector, proof, and Cardano-anchor evidence.

This is Yano’s most complete no-code vertical scenario. Docker Compose starts three Yano members plus Kafka, RustFS (S3-compatible object storage), Kubo IPFS, and the Evidence Explorer.

Before you start:

  • You need JDK 25, Docker with Compose v2, curl, jq, openssl, and Python 3. The stack is tested with Docker Desktop. On Colima with a virtiofs mount, RustFS refuses its owner-only secret files and the stack rolls back (#36).
  • The harness ships in the extracted Yano X JVM distribution at examples/evidence. It finds the distribution’s yano.jar by itself.
  • It uses ports 7070–7072 for the members and 7080 for the Explorer, plus 9092, 9000, and 5001 for Kafka, RustFS, and Kubo. A tutorial cluster that is still running holds 7070–7072. Stop it first from the distribution directory, with the same YANO_CLUSTER_DIR you started it with: ./yano.sh appchain cluster stop.
  • The harness never moves a busy port. If another program holds one, export a free port before the first command and keep it set for the whole tutorial. DEMO_HTTP_BASE moves the members to that port and the next two, and DEMO_UI_PORT, DEMO_KAFKA_PORT, DEMO_S3_PORT and DEMO_IPFS_PORT move the other services. For example, export DEMO_HTTP_BASE=7370 puts the members on 7370–7372, and the URLs the harness prints use those ports.

The harness runs the default evidence-v1-gated profile, in which validator members approve each release. The Studio link above opens the evidence-ledger recipe, which selects the role-aware role-evidence profile, where named business actors approve instead; the harness runs that profile when you add --machine role to every command.

Publish one piece of evidenceIllustrative data

What you're seeing: one ./demo.sh publish in the default evidence-v1-gated profile on devnet. The “What if” scenarios cover replay, a wrong version number, and a failed connector.

  1. Stage The runner copies the exact document bytes into the RustFS staging area and adds them to Kubo without pinning, which computes the CID. Nothing is on the ledger yet.
  2. Approve The runner registers the document hash, proposes an approval for the exact evidence command, and approves it, waiting for each message to be final. In this profile the approvers are members, and the demo requires one approval.
  3. Release The runner submits evidence.release.v1. The composite admits it only with that registry entry and approval, applies the document-trail append and the evidence version atomically, and emits two effects: object.put and ipfs.pin. Status: STORAGE_PENDING.
  4. Store and pin After finality, the effect owner copies the staged object into the versioned archive, which never overwrites a different object, and pins the CID in Kubo.
  5. Incorporate Both results return as ~fx/result. Status is derived from the receipts: each must match the command’s size, SHA-256, destination fingerprint, and CID. Status: STORAGE_READY. The runner then re-reads the archived object and the IPFS content itself.
  6. Notify With --continuation direct, the second storage result emits kafka.publish itself; in the default explicit mode the runner submits a notify command. The Kafka receipt records partition and offset. Status: READY.
  7. Agree and anchor The runner waits until all three members hold the same READY state and a devnet script anchor covers it.
  8. Verify Finally, the runner checks finality, the anchor, the proofs, and Kafka, and prints PASS command=publish. Run ./demo.sh verify later to repeat the reads without writing anything.
    • passes: Members agree on the state THREE_NODE_STATE_AGREEMENT
    • passes: Threshold finality of the workflow messages THRESHOLD_FINALITY_BUNDLES
    • passes: The anchor transaction is visible to every member PORTABLE_ANCHOR_TXS_VISIBLE_ON_ALL_MEMBERS
    • passes: The anchor datum commits to the certified root PORTABLE_ANCHOR_DATUM_COMMITMENTS_VERIFIED
    • passes: State and effect proofs against one root COMPOSED_EFFECT_PROOFS
    • passes: Kafka acknowledgement and event bytes KAFKA_ACKNOWLEDGEMENT_AND_EVENT

What if: Replay a version

./demo.sh replay resubmits a release that already happened.

  1. Compare the bytes The runner reads version 2 from the members and compares your file with the recorded SHA-256 and size. Different bytes stop here with REPLAY_INPUT_MISMATCH.
  2. Release again The runner submits the same release command, and it becomes final like any other message.
  3. Nothing changes The evidence record and its effects are unchanged, and the Kafka end offset has not moved. The runner then repeats the read-only verification.
    • passes: Replay finalized as a no-op REPLAY_FINALIZED_AS_DETERMINISTIC_NOOP

What if: Republish the wrong version

Versions are immutable and strictly sequential.

  1. Read the head You run republish --business-version 3, but the latest version is 1. The runner reads the head record before it stages anything.
  2. Refuse The only acceptable next version is 2, so the runner stops with VERSION_CONFLICT. Other refusals: publish of an existing id gives EVIDENCE_ALREADY_EXISTS, and a republish while the previous version is still in progress gives PRIOR_VERSION_NOT_TERMINAL.
    • fails: The version is exactly the latest plus one VERSION_CONFLICT

What if: One connector fails

Status is derived from receipts, never asserted.

  1. Stage The runner copies the exact document bytes into the RustFS staging area and adds them to Kubo without pinning, which computes the CID. Nothing is on the ledger yet.
  2. Approve The runner registers the document hash, proposes an approval for the exact evidence command, and approves it, waiting for each message to be final. In this profile the approvers are members, and the demo requires one approval.
  3. Release The runner submits evidence.release.v1. The composite admits it only with that registry entry and approval, applies the document-trail append and the evidence version atomically, and emits two effects: object.put and ipfs.pin. Status: STORAGE_PENDING.
  4. Store and pin After finality, the effect owner archives the object, but Kubo rejects the pin.
  5. Derive the status One storage result is a matching confirmation and the other is not, so the status is PARTIAL. The runner stops with FAIL code=STORAGE_FAILED. PARTIAL is terminal, so the owner may republish the document as the next version.
    Storage status from the two receipts
    object.putipfs.pinStatus
    confirmedconfirmedSTORAGE_READY
    confirmedfailedPARTIAL
    expiredfailedEXPIRED
    failedfailedSTORAGE_FAILED
  • demo runner
  • app ledger members
  • effect execution
  • connectors
  • Cardano
  • final or verified
  • failed or refused

The source document itself is not put into consensus state. The chain commits its business identity, version, hashes, workflow decisions, effect intents, and acknowledged outcomes.

1. Prepare and start a fresh direct-continuation profile

Section titled “1. Prepare and start a fresh direct-continuation profile”
Terminal window
cd examples/evidence
./demo.sh prepare \
--instance tutorial-evidence \
--continuation direct
./demo.sh up \
--instance tutorial-evidence \
--continuation direct

prepare stages the plugins, runner, and images and generates private configuration and keys. It ends with:

Prepared compose demo instance 'tutorial-evidence'.
Secrets: <path> (values are not printed)

up starts the services, bootstraps the devnet script anchor, and runs a read-only readiness probe. Near the end of its output, look for:

PASS command=probe
...
Yano status: http://127.0.0.1:7070/ui/app-chain/ (nodes: 7070, 7071, 7072)
Evidence UI: http://127.0.0.1:7080/
API key file: <path>

direct means the deterministic workflow emits the next result-driven transition directly. explicit, the harness default, keeps the original Milestone 1 notify command that advances that continuation. The choice is part of the fresh chain’s committed profile; pass the same option to every later command.

If startup fails, the launcher rolls back the partial deployment. Use the reported container health/log command rather than repeatedly deleting random directories; retained L1 and app ledger identities are deliberately checked.

Terminal window
./demo.sh publish \
--instance tutorial-evidence \
--continuation direct \
--evidence-id product-passport-001 \
--sample-file samples/inspection-certificate.json

The runner waits for the whole dependency-ordered workflow shown above, from staging to the anchor. On success it prints:

PASS command=publish scenario=<scenario id>

On failure it prints FAIL code=<CODE> instead; for example, STORAGE_FAILED when a storage connector did not confirm.

Terminal window
./demo.sh verify \
--instance tutorial-evidence \
--continuation direct \
--evidence-id product-passport-001 \
--business-version 1

verify is read-only. It does not submit an app message, stage an object, pin content, publish Kafka data, or force another anchor. It re-reads the immutable object and IPFS content, checks their hashes, checks the Kafka acknowledgement, and validates application finality, state/effect proofs, and anchor linkage. It ends with PASS command=verify scenario=<scenario id>.

Open the Evidence Explorer and select the record. The JSON preview is a bounded presentation copy produced only after the runner re-downloads and verifies the external bytes; it is not the browser’s original submission.

4. Demonstrate immutability and versioning

Section titled “4. Demonstrate immutability and versioning”

An evidence version never changes. publish refuses an evidence id that already exists with EVIDENCE_ALREADY_EXISTS, and the guided run command stops with REPUBLISH_REQUIRED, before any connector write, when the bytes differ from the retained version. To add a legitimate revision, create the exact next immutable version:

Terminal window
./demo.sh republish \
--instance tutorial-evidence \
--continuation direct \
--evidence-id product-passport-001 \
--business-version 2 \
--sample-file samples/inspection-certificate-product-a-v2.json
./demo.sh verify \
--instance tutorial-evidence \
--continuation direct \
--evidence-id product-passport-001 \
--business-version 1
./demo.sh verify \
--instance tutorial-evidence \
--continuation direct \
--evidence-id product-passport-001 \
--business-version 2

Both versions remain independently selectable and verifiable. Any other version number fails with VERSION_CONFLICT: the next version must be exactly the latest plus one.

Terminal window
./demo.sh replay \
--instance tutorial-evidence \
--continuation direct \
--evidence-id product-passport-001 \
--business-version 2 \
--sample-file samples/inspection-certificate-product-a-v2.json

The replay envelope finalizes, but the accepted business record, effect set, and logical external outcomes do not duplicate: the runner checks that the record and the Kafka end offset are unchanged, then prints PASS command=replay scenario=<scenario id>. A file whose bytes differ from version 2 stops with REPLAY_INPUT_MISMATCH.

Start small and use a fresh prefix:

Terminal window
./demo.sh load \
--instance tutorial-evidence \
--continuation direct \
--load-mode pipeline \
--count 8 \
--concurrency 8 \
--max-in-flight 8 \
--id-prefix tutorial-load \
--sample-file samples/inspection-certificate.json
./demo.sh verify \
--instance tutorial-evidence \
--continuation direct \
--evidence-id tutorial-load-000008 \
--business-version 1

This measures a full workflow with approvals, external actions, proofs, and anchors—not raw HTTP admission TPS. The load command prints PASS with a summary when every item succeeded, and the Explorer shows the aggregate report.

Terminal window
./demo.sh status --instance tutorial-evidence --continuation direct
./demo.sh stop --instance tutorial-evidence --continuation direct

Run up with the same instance/profile to resume retained data. Use the launcher’s explicit clean --scope ... --yes workflow only when you intend to retire that chain identity and have chosen a replacement instance.

The scenario proves that the approved bytes and connector outcomes are bound to threshold-finalized application state and an L1 anchor. It does not prove that an inspection statement is factually true, that an actor had a legal credential, or that every future copy of a referenced document remains available. Those require domain onboarding, custody, retention, and possibly independent real-world auditing.

Next: domain-role authorization.