Skip to content

Tutorial 2 — A Provable Shared Registry

Open this outcome in App-Chain Studio

  • Level: beginner to intermediate
  • Time: about 15 minutes
  • Outcome: write owner-controlled data, show that an unauthorized write is a final no-op, and prove the current value with an MPF proof.

The default cluster hosts registry-chain, which runs the stock kv-registry state machine. The first member to write a key owns it; only that member can update or delete it.

You need curl, jq and python3. Run every command from the top-level directory of the extracted release, the directory that contains yano.sh.

Prove a registry entryIllustrative data

What you're seeing: the commands of this tutorial, one step at a time. Watch the state table: the root changes with every block, but the proven entry changes only when its owner writes.

  1. Write as node 1 The launcher sends PUT supplier-42 = active through node 1. Node 1 signs the envelope, so its member key becomes the owner when the block is final.
    ./yano.sh appchain cluster kv registry-chain set supplier-42 active --node 1
  2. Wait for the proof Poll the proof endpoint until the key is PRESENT. The response carries the height, the state root, the value and an MPF proof of that value under that root.
    What the proof shows (illustrative heights and roots)
    HeightState rootsupplier-42
    41a2b…[node 1 key, "active"]
  3. Write as node 2 You send PUT supplier-42 = suspended through node 2 over REST, then wait until that message is final. Node 2 does not own the key, so the decision is KV_NOT_OWNER and nothing is written.
    • fails: The key has no entry, or the sender owns it KV_NOT_OWNER
  4. Compare Read the proof again. The entry is byte-for-byte the same. The root is not: every block also writes a record of its own messages root, so the root moves with every block. Compare the proven entry, not the root.
    Before and after the rejected write
    HeightState rootsupplier-42
    41a2b…[node 1 key, "active"]
    57c9d…[node 1 key, "active"]
  5. Update as owner Node 1 writes suspended. It owns the key, so the value changes, and the next proof shows it.
    ./yano.sh appchain cluster kv registry-chain set supplier-42 suspended --node 1
    • passes: The key has no entry, or the sender owns it
    After the owner’s write
    HeightState rootsupplier-42
    41a2b…[node 1 key, "active"]
    57c9d…[node 1 key, "active"]
    6e04f…[node 1 key, "suspended"]
  • your shell
  • member node
  • deterministic state machine
  • no effect

Skip this section if Tutorial 1’s cluster is still running.

Terminal window
export YANO_CLUSTER_DIR=/tmp/yano-tutorial-registry
./yano.sh appchain cluster start 3

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 launcher printed a line such as HTTP range 7070-7072 unavailable; using 7073-7075:

Terminal window
HTTP_BASE=7070

Write through node 1, so node 1 becomes the owner:

Terminal window
./yano.sh appchain cluster kv registry-chain set supplier-42 active --node 1

The command body is CBOR, but the launcher builds it for you. 202 means only that node 1 queued it. Wait until the key appears in a proof:

Terminal window
BASE=http://127.0.0.1:$HTTP_BASE/api/v1/app-chain/chains/registry-chain
KEY_HEX=$(python3 -c 'print("supplier-42".encode().hex())')
until curl -sf "$BASE/state/proof/$KEY_HEX" | jq -e '.presence == "PRESENT"' >/dev/null; do
sleep 1
done

The state key is the UTF-8 business key itself:

Terminal window
curl -s "$BASE/state/proof/$KEY_HEX" | jq '{committedHeight, stateRoot, presence, valueHex}'

valueHex is CBOR [owner, value]: 82 opens an array of two, 5820 is followed by node 1’s 32-byte member key, and 46616374697665 is the 6-byte value active. The response also carries proofWireHex, an MPF proof that this value sits under stateRoot. Showing JSON from one node is not independent verification; a verifier checks the proof against a root it trusts.

Record the proven entry and the root, then write the same key through node 2 over REST, so you can wait for exactly that message:

Terminal window
ENTRY_BEFORE=$(curl -s "$BASE/state/proof/$KEY_HEX" | jq -r .valueHex)
ROOT_BEFORE=$(curl -s "$BASE/state/proof/$KEY_HEX" | jq -r .stateRoot)
TOOL=docs/appchain/tutorials/tools/stdlib_command.py
PUT_HEX=$(python3 "$TOOL" kv-registry put supplier-42 --value-text suspended)
MESSAGE_ID=$(curl -sS -X POST \
"http://127.0.0.1:$((HTTP_BASE + 2))/api/v1/app-chain/chains/registry-chain/messages" \
-H 'Content-Type: application/json' \
-d "{\"topic\":\"kv-registry.command.v1\",\"bodyHex\":\"$PUT_HEX\"}" | jq -r .messageId)
until curl -sf "$BASE/messages/$MESSAGE_ID" >/dev/null; do sleep 1; done
curl -s "$BASE/messages/$MESSAGE_ID" | jq '{height, sender}'

The message is final at the printed height, sent by node 2. Now compare:

Terminal window
ENTRY_AFTER=$(curl -s "$BASE/state/proof/$KEY_HEX" | jq -r .valueHex)
ROOT_AFTER=$(curl -s "$BASE/state/proof/$KEY_HEX" | jq -r .stateRoot)
[ "$ENTRY_BEFORE" = "$ENTRY_AFTER" ] && echo "entry unchanged"
[ "$ROOT_BEFORE" != "$ROOT_AFTER" ] && echo "state root changed"

You should see both lines. The entry did not change: node 2 does not own the key, so the decision was KV_NOT_OWNER. The root did change, and it would have changed anyway: every block also records its own messages root in state. So compare the proven entry, never the root, to decide whether a command had an effect. A final message, a 202, or a growing height is not the authorization decision; the resulting state is.

Terminal window
./yano.sh appchain cluster kv registry-chain set supplier-42 suspended --node 1
until [ "$(curl -s "$BASE/state/proof/$KEY_HEX" | jq -r .valueHex)" != "$ENTRY_AFTER" ]; do
sleep 1
done
curl -s "$BASE/state/proof/$KEY_HEX" | jq '{committedHeight, stateRoot, valueHex}'

The value now ends in 73757370656e646564, suspended. The owner key is the same.

Terminal window
./yano.sh appchain cluster clean
unset YANO_CLUSTER_DIR
  • allow-lists shared by several organizations;
  • product, asset, credential or DID-document registries;
  • shared configuration with explicit ownership;
  • a current pointer whose exact value must be proved to a third party.

It does not provide organization roles, multi-party governance or document indexing. Use authenticated-map, the role workflow, or a plugin when the authorization is richer than “first writer owns this key”.