Skip to content

role-approvals

role-approvals lets business actors approve things when those actors are not the app ledger’s member nodes. Governed organizations, actors, keys, roles and policies decide whether an exact payload hash is approved. The proposal and its decision trail can be proved against the state root.

Machine idrole-approvals: the composite profile role-approvals-v1, with components domain-actors and role-approvals
Maturitypreview, JVM distribution
Topicsrole-approvals.command.v1 (actor statements and policy governance); actors.command.v1 (organizations, actors and keys)
ActionsPROPOSE, APPROVE, REJECT, CANCEL, each an Ed25519-signed actor statement
StatusesPENDING, APPROVED, REJECTED, CANCELLED, EXPIRED
Stateproposal, policy, actor and organization records inside the composite; use the domain API’s proofKey
Proof subjectsrole-approval-outcome-v1 (claims status, payload-digest); actor-role-assignment-v1 (claims status, has-role)
Outcome codesRoleWorkflowResultCode; this profile does not store them per command
Effectsnone
A role-approved decisionIllustrative data

What you're seeing: policy order-release: a buyer proposes; two reviewer actors from distinct organizations must approve. The checks run in evaluation order. A failed check makes the command a final no-op; this profile keeps no per-command result, so read the proposal to see what happened.

  1. Sign offline Actor buyer-a hashes the exact order bytes and signs a PROPOSE statement in its own wallet or HSM. The statement binds the chain, proposal id, policy order-release revision 1, payload domain and hash, deadline height, actor revision and key.
    ./yano.sh appchain role sign --action propose --chain role-approval-chain --proposal order-1001 --policy order-release --policy-revision 1 --payload-domain com.example.order.v1 --payload-hash <hex> --deadline-height <tip + 90> --actor buyer-a --actor-revision 1 --key buyer-key-v1 --seed-file <file>
  2. Relay Any member can relay the bytes on role-approvals.command.v1. The member signs the envelope, but it gains no business authority: the actor’s signature inside decides. 202 only means queued.
  3. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  4. First review reviewer-a from organization audit-one signs APPROVE for clause reviewers. The decision is accepted: one organization of the two the clause needs.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)
  5. Second review reviewer-b from audit-two approves. Two distinct organizations satisfy the clause, so the proposal becomes APPROVED, a terminal status.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001APPROVEDreviewer-a (audit-one), reviewer-b (audit-two)
  6. Verify A verifier reads the proposal through the domain API and checks its proof. Subject role-approval-outcome-v1 supports the claims status = APPROVED and payload-digest = the hash of the exact order bytes. The machine stores the hash, not the order.

What if: Same organization twice

reviewer-a2 also works for audit-one.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. First review reviewer-a from organization audit-one signs APPROVE for clause reviewers. The decision is accepted: one organization of the two the clause needs.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)
  3. Second review reviewer-a2 approves. Its organization already has an accepted decision in this clause, so it does not count.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • fails: No accepted decision in this clause from the same organization DISTINCTNESS_DUPLICATE
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)

What if: A different payload

reviewer-b signs a hash that is not the proposal’s.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. First review reviewer-a from organization audit-one signs APPROVE for clause reviewers. The decision is accepted: one organization of the two the clause needs.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)
  3. Second review reviewer-b’s statement carries another payload hash. It does not match the proposal, so it cannot count toward it.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • fails: Policy, payload and deadline match the proposal CONFLICT
    • not reached: This actor has not decided on it yet
    • not reached: The actor has the clause’s role
    • not reached: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)

What if: A revoked actor

reviewer-b’s actor record was revoked through governance before it signed.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. First review reviewer-a from organization audit-one signs APPROVE for clause reviewers. The decision is accepted: one organization of the two the clause needs.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)
  3. Second review The current revision of reviewer-b is not ACTIVE, so the actor is not eligible. The signature is never checked.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • fails: The actor, its organization and key are current and active UNAUTHORIZED_ACTOR
    • not reached: The signature verifies
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)

What if: The relay edits the bytes

A relay member changes the clause in the statement before relaying it.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. First review reviewer-a from organization audit-one signs APPROVE for clause reviewers. The decision is accepted: one organization of the two the clause needs.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)
  3. Second review The edited statement no longer matches reviewer-b’s signature. A relay cannot change a signed decision.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • fails: The signature verifies INVALID_SIGNATURE
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)

What if: Signed for another chain

A statement signed for a staging chain is replayed here.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. Replay The statement names a different chain id, so it is refused before any other check. A signature cannot be reused across chains.
    • fails: The statement names this chain WRONG_GENESIS
    • not reached: Its deadline height has not passed
    • not reached: The block’s signature-work budget has room
    • not reached: The actor, its organization and key are current and active
    • not reached: The signature verifies
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone

What if: A deadline too far away

order-release allows proposals to stay open for at most 100 blocks.

  1. Propose buyer-a proposes with a deadline 500 blocks ahead. That exceeds the policy’s lifetime, so no proposal is created.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • fails: The deadline is within the policy’s lifetime LIMIT_EXCEEDED
    • not reached: The actor holds a proposer role
    • not reached: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    empty
  2. Propose again A new statement with a deadline inside the lifetime creates the proposal.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone

What if: Nobody approves in time

Expiry runs at the start of every block, not when someone touches the proposal.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. First review reviewer-a from organization audit-one signs APPROVE for clause reviewers. The decision is accepted: one organization of the two the clause needs.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: This actor has not decided on it yet
    • passes: The actor has the clause’s role
    • passes: No accepted decision in this clause from the same organization
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGreviewer-a (audit-one)
  3. The deadline passes The next block above the deadline height starts with a maintenance pass. It marks every pending proposal whose deadline is below the block height EXPIRED, whatever messages the block carries.
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001EXPIREDreviewer-a (audit-one)
  4. A late review reviewer-b approves after the deadline. The statement’s own deadline has passed, so it is refused at the second check.
    • passes: The statement names this chain
    • fails: Its deadline height has not passed EXPIRED
    • not reached: The block’s signature-work budget has room
    • not reached: The actor, its organization and key are current and active
    • not reached: The signature verifies
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001EXPIREDreviewer-a (audit-one)

What if: Cancel

Only the proposer can cancel its pending proposal.

  1. Propose In the final block every member evaluates the statement. All checks pass, so proposal order-1001 is created PENDING. The proposer is recorded but is not an approver.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: No proposal with this id exists
    • passes: The deadline is above the current height
    • passes: The policy exists and the revision is current
    • passes: The policy is ACTIVE
    • passes: The deadline is within the policy’s lifetime
    • passes: The actor holds a proposer role
    • passes: Pending-proposal capacity has room
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  2. A reviewer cancels reviewer-a sends CANCEL. Only the proposer may cancel.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • fails: The sender is the proposer UNAUTHORIZED_ACTOR
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001PENDINGnone
  3. The proposer cancels buyer-a sends CANCEL for its own proposal. It becomes CANCELLED, a terminal status.
    • passes: The statement names this chain
    • passes: Its deadline height has not passed
    • passes: The block’s signature-work budget has room
    • passes: The actor, its organization and key are current and active
    • passes: The signature verifies
    • passes: The proposal exists
    • passes: The proposal is PENDING
    • passes: Policy, payload and deadline match the proposal
    • passes: The sender is the proposer
    role-approvals state
    ProposalStatusAccepted decisions
    order-1001CANCELLEDnone
  • business actor
  • relay member
  • deterministic state machine
  • verifier
  • approved
  • no-op or terminal
StatusReached byTerminal
PENDINGan accepted PROPOSEno
APPROVEDan accepted APPROVE that satisfies every clauseyes
REJECTEDan accepted REJECT, when the policy allows rejectionyes
CANCELLEDCANCEL by the proposer, or a governed cancellationyes
EXPIREDthe first block whose height is above the deadline heightyes

Expiry runs at the start of every block: a maintenance pass marks each pending proposal whose deadline height is below the block height EXPIRED, whatever the block contains. A block needs at least one message on the chain, so with no traffic the status changes at the next block.

Each actor makes one decision per proposal. Sending the same statement again is EXACT_REPLAY; sending a different one is CONFLICT. The proposer is not an approver unless it also signs an APPROVE.

Every statement passes these checks first:

OrderCheckOutcome when it fails
1The statement names this chainWRONG_GENESIS
2Its deadline height is not below the block heightEXPIRED
3The block’s signature-work budget has roomCRYPTO_WORK_EXCEEDED
4The actor revision, its organization and its key are current and activeUNAUTHORIZED_ACTOR
5The signature verifiesINVALID_SIGNATURE

Then, by action:

ActionChecks, in order
PROPOSEno proposal with this id (EXACT_REPLAY or CONFLICT); deadline above the current height (EXPIRED); the policy exists (UNKNOWN_RECORD) and the revision is current (WRONG_REVISION); the policy is active (UNAUTHORIZED_ACTOR); the deadline is within the policy’s lifetime (LIMIT_EXCEEDED); the actor holds a proposer role (ROLE_MISMATCH); pending capacity has room (CAPACITY_EXCEEDED)
APPROVE, REJECTthe proposal exists (UNKNOWN_RECORD) and is pending (TERMINAL); policy, payload and deadline match it (CONFLICT); the actor has not decided yet (EXACT_REPLAY or CONFLICT); the actor has the clause’s role (ROLE_MISMATCH); no decision in this clause from the same organization when the clause is distinct by organization (DISTINCTNESS_DUPLICATE); for REJECT, the policy allows rejection (ROLE_MISMATCH)
CANCELthe proposal exists (UNKNOWN_RECORD) and is pending (TERMINAL); it matches (CONFLICT); the sender is the proposer (UNAUTHORIZED_ACTOR)

A failed check makes the command a final no-op. This profile keeps no per-command result, so read the proposal to see what happened: its status and its accepted decisions.

Use role-approvals when:

  • an employee, auditor, regulator, service or device must sign independently of the member node that relays its command;
  • a policy needs roles, minimum counts or distinct organizations;
  • actor keys and policies must rotate through governed revisions; and
  • the application can act on a provably approved payload hash.

Use approvals when member nodes themselves are the approvers. Use role-evidence for the complete evidence release and publication flow, and a reviewed composite when an approval must atomically drive another transition.

The machine stores no payload, runs nothing and emits no effect. An approval proves authorization of (payloadDomain, payloadHash); your application binds those bytes to its next action.

outer envelope sender = the member that relayed the command
signed actor identity = the business actor authorizing the payload hash

REST authentication controls access to an endpoint. It is neither a member vote nor an actor signature. An actor statement binds its action, chain, proposal, policy and revision, payload domain and hash, deadline, actor revision, key and clause, so a signature cannot be reused for another chain, payload or decision.

From the top-level directory of the extracted release:

Terminal window
./yano.sh appchain init --non-interactive \
--recipe role-approval --network devnet --members 3 \
--runtime jvm --deployment host \
--http-port-base 7070 --server-port-base 13337 \
--name role-approval-chain --chain-id role-approval-chain \
--output role-approval-chain

On devnet, prepare generates the member keys, pins their public keys in the blueprint and renders the project. On a public network, add the reviewed public member keys to appchain.yaml and run ./yano.sh appchain render instead.

Terminal window
./yano.sh appchain prepare role-approval-chain
./yano.sh appchain doctor role-approval-chain --distribution "$PWD"
export YANO_HOME="$PWD"
role-approval-chain/scripts/start

doctor reports DOCTOR_WARNINGS. APPLICATION_BOOTSTRAPPED stays PENDING even after the bootstrap below: doctor runs offline and never queries the chain, so the line is a reminder. Reading every record back is the check.

Bootstrap organizations, actors and policies

Section titled “Bootstrap organizations, actors and policies”

bootstrap/role-approvals-plan.yaml is a non-secret plan for organizations, actors, proof-of-possession, policies, governance and verification. Fill it with public values only; actor private keys belong in the actor’s application, KMS, HSM or vault.

Organizations, actors and policies are created through member governance. Each record is proposed by one member, which counts as its first approval, approved by further members up to the chain’s genesis threshold, then activated. Bootstrap is fail-closed and idempotent:

  1. Query the committed record and verify its proof.
  2. If the exact revision already exists, record the proof and skip it.
  3. If it is absent, submit PROPOSE, collect member approvals, then ACTIVATE.
  4. If an existing revision differs, stop; never replace it silently.

1. Create each actor’s key and its proof of possession. On devnet you can make disposable seeds; in a real deployment each actor does this in its own wallet or HSM and sends you only the public key and the proof:

Terminal window
mkdir -p actors
for actor in proposer-a reviewer-a; do
(umask 077; openssl rand -hex 32 > "actors/$actor.seed")
done
PROPOSER_KEY=$(./yano.sh appchain role public-key --seed-file actors/proposer-a.seed)
REVIEWER_KEY=$(./yano.sh appchain role public-key --seed-file actors/reviewer-a.seed)
./yano.sh appchain role key-proof --chain role-approval-chain \
--actor proposer-a --actor-revision 1 --key proposer-key-v1 \
--public-key "$PROPOSER_KEY" --valid-from-height 1 --valid-until-height 0 \
--seed-file actors/proposer-a.seed > actors/proposer-a.proof
./yano.sh appchain role key-proof --chain role-approval-chain \
--actor reviewer-a --actor-revision 1 --key reviewer-key-v1 \
--public-key "$REVIEWER_KEY" --valid-from-height 1 --valid-until-height 0 \
--seed-file actors/reviewer-a.seed > actors/reviewer-a.proof

2. Fill in the plan. Replace every REPLACE_* value: here, empty metadata commitments and the two public keys, in the order the plan lists the actors:

Terminal window
python3 - role-approval-chain/bootstrap/role-approvals-plan.yaml \
role-approvals-plan.yaml "$PROPOSER_KEY" "$REVIEWER_KEY" <<'PY'
import sys
source, target, proposer, reviewer = sys.argv[1:]
text = open(source).read().replace("REPLACE_64_HEX_OR_EMPTY", '""')
text = text.replace("REPLACE_64_HEX", proposer, 1).replace("REPLACE_64_HEX", reviewer, 1)
open(target, "w").write(text)
PY

3. Encode the governance commands. role bootstrap turns the plan into one step per record, in dependency order, with the mutation, its hash, and the encoded propose, approve and activate commands for that record’s topic. It refuses a plan with placeholders left, and an actor key without a valid proof:

Terminal window
API0=http://127.0.0.1:7070/api/v1/app-chain/chains/role-approval-chain
API1=http://127.0.0.1:7071/api/v1/app-chain/chains/role-approval-chain
HEIGHT=$(curl -s "$API0/status" | jq -r .tipHeight)
./yano.sh appchain role bootstrap --plan role-approvals-plan.yaml \
--expiry-height $((HEIGHT + 500)) \
--key-proof actors/proposer-a.proof --key-proof actors/reviewer-a.proof \
> bootstrap.json
jq '[.steps[] | {record, id, topic}]' bootstrap.json

The expiry must be above the current height and at most machines.composite.roles.maximum-mutation-lifetime-blocks (1000 by default) beyond it. One expiry covers every step, so leave room for all of them: a propose that arrives after it is ignored. Organizations go to actors.command.v1 before the actors that belong to them; policies go to role-approvals.command.v1. If you created the project without --http-port-base, its members listen on 8080 and up: adjust API0 and API1.

4. Submit each step. A member’s approval is the envelope it relays, so send the approve through a different member than the propose. With the generated threshold of 2, one extra approval is enough:

Terminal window
submit() { # <member API> <topic> <command hex>; waits until final
local id attempt
id=$(curl -sf -X POST "$1/messages" -H 'Content-Type: application/json' \
-d "$(jq -nc --arg t "$2" --arg b "$3" '{topic:$t, bodyHex:$b}')" \
| jq -er .messageId) || { echo "submission refused by $1" >&2; return 1; }
for attempt in $(seq 1 120); do
curl -sf "$1/messages/$id" | jq -e .height >/dev/null && return 0
sleep 1
done
echo "message $id not final after 120 s" >&2
return 1
}
jq -c '.steps[]' bootstrap.json | while read -r step; do
topic=$(jq -r .topic <<<"$step")
submit "$API0" "$topic" "$(jq -r .propose <<<"$step")"
submit "$API1" "$topic" "$(jq -r .approve <<<"$step")"
submit "$API0" "$topic" "$(jq -r .activate <<<"$step")"
echo "activated $(jq -r '.record + " " + .id' <<<"$step")"
done

A final activate is not proof that the record exists. An activate below the threshold does nothing, and a later one can still succeed. An activate that reaches the threshold but cannot apply, for example an actor whose organization is not active yet, marks that attempt failed for good; so does any approve or activate after the expiry. Read each record back, from any member:

Terminal window
BUNDLE=http://127.0.0.1:7072/api/v1/plugins/org.yanoproject.x.role-workflow
for record in organizations/organization-a organizations/organization-b \
actors/proposer-a actors/reviewer-a policies/application-approval; do
curl -s "$BUNDLE/$record?chain=role-approval-chain" | jq -c '.record'
done

✓ You should see five records with "revision":1; the organizations and actors also show "status":"ACTIVE".

If a record is missing after its activate, the attempt expired or failed and its mutation id is spent. Fix the cause, then run role bootstrap again with --attempt 2 (then 3, and so on) and a fresh expiry, and resubmit only the steps whose records are missing.

To change a record later, raise its revision in a copy of the plan that holds only that record, and run the same steps. For an actor, the new revision must keep every existing key with the same public key and validFromHeight; mark each kept key proofOfPossession: OPTIONAL and pass --key-proof only for the keys the revision adds, signed with --actor-revision set to the new revision.

Hash the exact canonical application bytes before actors sign them. The payload domain names that byte contract, for example com.example.release.v1. Each statement binds the proposal, the policy revision, the payload hash and a deadline height:

Terminal window
printf 'release-7' > release-7.bin
PAYLOAD_HASH=$(openssl dgst -sha256 -binary release-7.bin | xxd -p -c 256)
DEADLINE=$(( $(curl -s "$API0/status" | jq -r .tipHeight) + 200 ))
sign() {
./yano.sh appchain role sign --chain role-approval-chain \
--proposal release-7 --policy application-approval --policy-revision 1 \
--payload-domain com.example.release.v1 --payload-hash "$PAYLOAD_HASH" \
--deadline-height "$DEADLINE" --actor-revision 1 "$@"
}
submit "$API0" role-approvals.command.v1 "$(sign --action propose \
--actor proposer-a --key proposer-key-v1 --seed-file actors/proposer-a.seed)"
submit "$API0" role-approvals.command.v1 "$(sign --action approve \
--actor reviewer-a --key reviewer-key-v1 --clause reviewers \
--seed-file actors/reviewer-a.seed)"

Any member can relay an actor statement; the actor’s signature, not the member, is what counts.

When API authentication is enabled (yano.app-chain.api.auth.enabled=true), add -H "X-API-Key: $API_KEY" to every request. A generated project keeps its local key as YANO_APPCHAIN_API_KEYS in secrets/node0.env.

202 means queued, not approved. submit waits for finality; then read the proposal below. The reviewers clause needs one reviewer, counted once per organization, so reviewer-a’s approval makes the status "APPROVED".

The bundle exposes read-only routes:

Terminal window
BASE=http://127.0.0.1:7070/api/v1
BUNDLE=org.yanoproject.x.role-workflow
curl -sS "$BASE/plugins/$BUNDLE/proposals/release-7?chain=role-approval-chain" | jq .record.status
curl -sS "$BASE/plugins/$BUNDLE/stats?chain=role-approval-chain" | jq .

Organizations, actors and policies follow the same pattern:

organizations/{id}?chain={chainId}[&revision=N]
actors/{id}?chain={chainId}[&revision=N]
policies/{id}?chain={chainId}[&revision=N]

A response includes committedHeight, stateRoot, proofKey and recordValue. A route that resolves the current revision also returns the current-pointer key and value. Verify both proofs at the same height and root: the record proof shows that a revision exists, and the pointer proof shows it was current. The physical keys are namespaced by the composite profile; always prove the returned proofKey.

implementation "org.yanoproject.x:yano-x-client:${yanoXVersion}"
implementation "org.yanoproject.x:yano-x-role-workflow-contracts:${yanoXVersion}"
var statement = new ActorStatementV1(
ActorStatementV1.Action.APPROVE,
"role-approval-chain", "release-7", "application-approval", 1,
"com.example.release.v1", payloadHash, deadlineHeight,
"reviewer-a", 1, "reviewer-key-v1", "reviewers");
byte[] command = SignedActorCommandV1.sign(statement, actorSeed).encode();
var client = AppChainClient.builder("http://127.0.0.1:7070/api/v1")
.chainId("role-approval-chain")
.build();
var submitted = client.submit("role-approvals.command.v1", command);

In production, sign ActorStatementV1.signingPreimage() in a KMS or HSM rather than loading a raw seed into application memory.

  • Roles are normalized strings, not a fixed enum.
  • Policies have proposer roles, at most 16 clauses, minimum counts, distinctness by actor or organization, a rejection mode and a maximum lifetime.
  • Actor and policy revisions are immutable; current pointers are governed.
  • An actor keeps at most 16 key epochs.
  • Pending proposals are bounded in total and per actor, policy and deadline.

The profile emits no effect, because an approved hash is not executable bytes. An application can:

  1. query the terminal proposal and act idempotently off the ledger;
  2. submit a separately validated command bound to the proposal; or
  3. install a reviewed composite that consumes the approval atomically, as authenticated-map does for approval collections.

Do not attach an arbitrary executor and assume it knows which bytes were approved.

  • Monitor pending, rejected and expired proposal counts.
  • Rotate a key by governing the actor’s next revision with proof-of-possession.
  • Suspend or revoke a compromised actor, then govern cancellation of its pending proposals.
  • Approved decisions stay immutable after rotation or revocation.
  • A changed profile digest, component order, route or state identity is a consensus upgrade: it needs governed activation or a new chain.

The full wire, governance and recovery model is in Domain Actors and Role-Aware Approvals, the portable contracts, and the implementation guide.