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 avirtiofsmount, 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’syano.jarby 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_DIRyou 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_BASEmoves the members to that port and the next two, andDEMO_UI_PORT,DEMO_KAFKA_PORT,DEMO_S3_PORTandDEMO_IPFS_PORTmove the other services. For example,export DEMO_HTTP_BASE=7370puts 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.
How one publication flows
Section titled “How one publication flows”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.
- 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.
- 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.
- 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.putandipfs.pin. Status:STORAGE_PENDING. - 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.
- 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. - Notify With
--continuation direct, the second storage result emitskafka.publishitself; in the default explicit mode the runner submits a notify command. The Kafka receipt records partition and offset. Status:READY. - Agree and anchor The runner waits until all three members hold the same
READYstate and a devnet script anchor covers it. - Verify Finally, the runner checks finality, the anchor, the proofs, and Kafka, and prints
PASS command=publish. Run./demo.sh verifylater 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
- passes: Members agree on the state
What if: Replay a version
./demo.sh replay resubmits a release that already happened.
- 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. - Release again The runner submits the same release command, and it becomes final like any other message.
- 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
- passes: Replay finalized as a no-op
What if: Republish the wrong version
Versions are immutable and strictly sequential.
- 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. - Refuse The only acceptable next version is 2, so the runner stops with
VERSION_CONFLICT. Other refusals:publishof an existing id givesEVIDENCE_ALREADY_EXISTS, and a republish while the previous version is still in progress givesPRIOR_VERSION_NOT_TERMINAL.- fails: The version is exactly the latest plus one
VERSION_CONFLICT
- fails: The version is exactly the latest plus one
What if: One connector fails
Status is derived from receipts, never asserted.
- 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.
- 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.
- 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.putandipfs.pin. Status:STORAGE_PENDING. - Store and pin After finality, the effect owner archives the object, but Kubo rejects the pin.
- Derive the status One storage result is a matching confirmation and the other is not, so the status is
PARTIAL. The runner stops withFAIL code=STORAGE_FAILED.PARTIALis terminal, so the owner may republish the document as the next version.Storage status from the two receipts object.putipfs.pinStatus confirmed confirmed STORAGE_READYconfirmed failed PARTIALexpired failed EXPIREDfailed failed STORAGE_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”cd examples/evidence
./demo.sh prepare \ --instance tutorial-evidence \ --continuation direct
./demo.sh up \ --instance tutorial-evidence \ --continuation directprepare 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.
2. Publish version 1
Section titled “2. Publish version 1”./demo.sh publish \ --instance tutorial-evidence \ --continuation direct \ --evidence-id product-passport-001 \ --sample-file samples/inspection-certificate.jsonThe 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.
3. Verify without changing anything
Section titled “3. Verify without changing anything”./demo.sh verify \ --instance tutorial-evidence \ --continuation direct \ --evidence-id product-passport-001 \ --business-version 1verify 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:
./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 2Both versions remain independently selectable and verifiable. Any other
version number fails with VERSION_CONFLICT: the next version must be exactly
the latest plus one.
5. Demonstrate idempotent replay
Section titled “5. Demonstrate idempotent replay”./demo.sh replay \ --instance tutorial-evidence \ --continuation direct \ --evidence-id product-passport-001 \ --business-version 2 \ --sample-file samples/inspection-certificate-product-a-v2.jsonThe 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.
6. Optional bounded parallel workload
Section titled “6. Optional bounded parallel workload”Start small and use a fresh prefix:
./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 1This 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.
7. Stop and retain the scenario
Section titled “7. Stop and retain the scenario”./demo.sh status --instance tutorial-evidence --continuation direct./demo.sh stop --instance tutorial-evidence --continuation directRun 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.
What is and is not proven
Section titled “What is and is not proven”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.
Go deeper
Section titled “Go deeper”- Read the plain-language evidence flow.
- Compare lifecycle and pipeline scheduling.
- Stop one member and exercise catch-up using the isolated E2E gate.
- Replace RustFS with a tested S3-compatible production service while keeping
the
object.putcontract unchanged. - Review the optional connector packaging and security matrix before translating the demo into a deployment.
Next: domain-role authorization.