Skip to content

Composite profile governance

This runbook is for operators evolving the deterministic composite state machine of a long-lived app ledger. The short rule is:

Deploy the dormant target code everywhere first; authorize one exact profile and activation height on the ledger second.

Profile governance changes deterministic application composition. It is not a plugin hot-reload mechanism and it does not dynamically assemble components from YAML. A reviewed composite bundle owns a bounded catalog of executable profiles. Governance can select only an exact canonical profile already in that catalog.

A proposal moves through three statuses, STAGING, SEALED, and SCHEDULED. Activation and voiding are events: when either happens, the proposal record is deleted. Only one proposal can be open at a time.

Changing a live profileIllustrative data

What you're seeing: three members with a threshold of 2 move a governed composite ledger to a new profile. The status names are the runtime's own. Try a “What if” for the ways a proposal is voided.

  1. Deploy the target bundle Before any command, every member deploys the same reviewed composite bundle. Its catalog holds the active profile and the dormant target profile. Members restart one at a time. Nothing about the ledger changes yet.
  2. Begin Member A submits BEGIN with the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. A BEGIN that breaks any rule below is ignored.
    ./profile-governance.py --chain evidence-chain begin --encode-only …
    • passes: No other proposal is open
    • passes: Membership digest is current, and still current at H
    • passes: Base digest is the active profile
    • passes: H ≥ height + min-activation-lag (default 20)
    • passes: Expiry ≤ height + proposal-ttl-blocks (default 600)
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  3. Stage the chunks Member A submits the exact canonical bytes of the target profile in chunks, waiting for each to finalize. Only the author can stage. Replaying an identical chunk does nothing; a conflicting one voids the proposal.
    ./profile-governance.py --chain evidence-chain chunk --index 0 …
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  4. Seal Member A seals the proposal. The staged bytes must match the declared length and target digest, the transition from the active profile must pass the compatibility and quota rules, and H must still be far enough ahead. The proposal gets its proposal hash.
    • passes: Bytes match the declared length and target digest
    • passes: Transition passes compatibility and quota rules
    • passes: Activation lag still holds
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED0 of 20 of 3
  5. Approve Approval authorizes the exact intent. Each approving member submits APPROVE with the proposal hash, through its own node and member key. A threshold of members must approve: here 2 of 3.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED2 of 20 of 3
  6. Attest readiness Readiness is separate from approval: every member in the bound membership must attest, with READY, that the target digest is in its own executable catalog. READY --dry-run fails on a node that lacks it.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED2 of 23 of 3
  7. Scheduled With threshold approvals and readiness from all members finalized before H, the proposal becomes SCHEDULED. Status and the proposal.state metric show it. A threshold of CANCEL commands can still void it.
    • passes: Approvals ≥ threshold
    • passes: Every member is ready
    • passes: Reached before the activation height
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SCHEDULED2 of 23 of 3
  8. Activate at H Every block before H runs the old profile. Block H runs the target profile for results, expiries, workflows, and messages. The runtime appends a new profile epoch, moves the marker, and deletes the proposal. There is no local override.
    After block H
    Active profileEpochProposal
    targetn + 1none

What if: A member is not ready

Member C never deployed the target bundle.

  1. Deploy the target bundle Before any command, every member deploys the same reviewed composite bundle. Its catalog holds the active profile and the dormant target profile. Members restart one at a time. Nothing about the ledger changes yet.
  2. Begin Member A submits BEGIN with the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. A BEGIN that breaks any rule below is ignored.
    ./profile-governance.py --chain evidence-chain begin --encode-only …
    • passes: No other proposal is open
    • passes: Membership digest is current, and still current at H
    • passes: Base digest is the active profile
    • passes: H ≥ height + min-activation-lag (default 20)
    • passes: Expiry ≤ height + proposal-ttl-blocks (default 600)
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  3. Stage the chunks Member A submits the exact canonical bytes of the target profile in chunks, waiting for each to finalize. Only the author can stage. Replaying an identical chunk does nothing; a conflicting one voids the proposal.
    ./profile-governance.py --chain evidence-chain chunk --index 0 …
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  4. Seal Member A seals the proposal. The staged bytes must match the declared length and target digest, the transition from the active profile must pass the compatibility and quota rules, and H must still be far enough ahead. The proposal gets its proposal hash.
    • passes: Bytes match the declared length and target digest
    • passes: Transition passes compatibility and quota rules
    • passes: Activation lag still holds
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED0 of 20 of 3
  5. Approve Approval authorizes the exact intent. Each approving member submits APPROVE with the proposal hash, through its own node and member key. A threshold of members must approve: here 2 of 3.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED2 of 20 of 3
  6. Attest readiness Members A and B attest readiness, but Member C's catalog lacks the target, so it cannot honestly send READY. Without readiness from every member the proposal cannot be scheduled.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED2 of 22 of 3
  7. Proposal voided Once the expiry height passes, or if the conditions are only met at or after H, the proposal is deleted. Deploy the bundle on Member C and propose again.
    Proposal record
    Proposal statusApprovalsReady
    none (deleted)——

What if: A threshold cancels

The members change their minds after sealing.

  1. Deploy the target bundle Before any command, every member deploys the same reviewed composite bundle. Its catalog holds the active profile and the dormant target profile. Members restart one at a time. Nothing about the ledger changes yet.
  2. Begin Member A submits BEGIN with the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. A BEGIN that breaks any rule below is ignored.
    ./profile-governance.py --chain evidence-chain begin --encode-only …
    • passes: No other proposal is open
    • passes: Membership digest is current, and still current at H
    • passes: Base digest is the active profile
    • passes: H ≥ height + min-activation-lag (default 20)
    • passes: Expiry ≤ height + proposal-ttl-blocks (default 600)
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  3. Stage the chunks Member A submits the exact canonical bytes of the target profile in chunks, waiting for each to finalize. Only the author can stage. Replaying an identical chunk does nothing; a conflicting one voids the proposal.
    ./profile-governance.py --chain evidence-chain chunk --index 0 …
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  4. Seal Member A seals the proposal. The staged bytes must match the declared length and target digest, the transition from the active profile must pass the compatibility and quota rules, and H must still be far enough ahead. The proposal gets its proposal hash.
    • passes: Bytes match the declared length and target digest
    • passes: Transition passes compatibility and quota rules
    • passes: Activation lag still holds
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED0 of 20 of 3
  5. Cancel Members A and C each submit CANCEL with the proposal hash. Two cancellations meet the threshold.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED0 of 20 of 3
  6. Proposal voided A threshold of cancellations voids the proposal before activation. The active profile is unchanged.
    Proposal record
    Proposal statusApprovalsReady
    none (deleted)——

What if: Membership changes

A membership change takes effect before H.

  1. Deploy the target bundle Before any command, every member deploys the same reviewed composite bundle. Its catalog holds the active profile and the dormant target profile. Members restart one at a time. Nothing about the ledger changes yet.
  2. Begin Member A submits BEGIN with the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. A BEGIN that breaks any rule below is ignored.
    ./profile-governance.py --chain evidence-chain begin --encode-only …
    • passes: No other proposal is open
    • passes: Membership digest is current, and still current at H
    • passes: Base digest is the active profile
    • passes: H ≥ height + min-activation-lag (default 20)
    • passes: Expiry ≤ height + proposal-ttl-blocks (default 600)
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  3. Stage the chunks Member A submits the exact canonical bytes of the target profile in chunks, waiting for each to finalize. Only the author can stage. Replaying an identical chunk does nothing; a conflicting one voids the proposal.
    ./profile-governance.py --chain evidence-chain chunk --index 0 …
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  4. Seal Member A seals the proposal. The staged bytes must match the declared length and target digest, the transition from the active profile must pass the compatibility and quota rules, and H must still be far enough ahead. The proposal gets its proposal hash.
    • passes: Bytes match the declared length and target digest
    • passes: Transition passes compatibility and quota rules
    • passes: Activation lag still holds
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED0 of 20 of 3
  5. Approve Approval authorizes the exact intent. Each approving member submits APPROVE with the proposal hash, through its own node and member key. A threshold of members must approve: here 2 of 3.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED2 of 20 of 3
  6. Attest readiness Readiness is separate from approval: every member in the bound membership must attest, with READY, that the target digest is in its own executable catalog. READY --dry-run fails on a node that lacks it.
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SEALED2 of 23 of 3
  7. Scheduled With threshold approvals and readiness from all members finalized before H, the proposal becomes SCHEDULED. Status and the proposal.state metric show it. A threshold of CANCEL commands can still void it.
    • passes: Approvals ≥ threshold
    • passes: Every member is ready
    • passes: Reached before the activation height
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    SCHEDULED2 of 23 of 3
  8. Proposal voided The proposal is bound to one membership epoch, now and at H. When the effective membership changes, the proposal is deleted, even when scheduled. Propose again against the new membership.
    Proposal record
    Proposal statusApprovalsReady
    none (deleted)——

What if: A conflicting chunk

Two different byte strings are staged for the same chunk index.

  1. Deploy the target bundle Before any command, every member deploys the same reviewed composite bundle. Its catalog holds the active profile and the dormant target profile. Members restart one at a time. Nothing about the ledger changes yet.
  2. Begin Member A submits BEGIN with the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. A BEGIN that breaks any rule below is ignored.
    ./profile-governance.py --chain evidence-chain begin --encode-only …
    • passes: No other proposal is open
    • passes: Membership digest is current, and still current at H
    • passes: Base digest is the active profile
    • passes: H ≥ height + min-activation-lag (default 20)
    • passes: Expiry ≤ height + proposal-ttl-blocks (default 600)
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  3. Stage the chunks Chunk 0 is staged, then a chunk 0 with different bytes arrives.
    ./profile-governance.py --chain evidence-chain chunk --index 0 …
    Proposal record (threshold 2 of 3)
    Proposal statusApprovalsReady
    STAGING——
  4. Proposal voided Conflicting staging voids the proposal. So do staged bytes that exceed the declared size, and a seal whose bytes, digest, or transition rules do not check out.
    Proposal record
    Proposal statusApprovalsReady
    none (deleted)——
  • member node
  • governance state
  • activated
  • void or not ready

Governed mode is a consensus-affecting choice for a new chain:

yano.app-chain.chains[0].state-machine=composite
yano.app-chain.chains[0].machines.composite.preset=evidence-v1-gated
yano.app-chain.chains[0].membership.mode=governed
yano.app-chain.chains[0].machines.composite.profile-mode=governed
yano.app-chain.chains[0].machines.composite.profile-governance.min-activation-lag=20
yano.app-chain.chains[0].machines.composite.profile-governance.proposal-ttl-blocks=600
yano.app-chain.chains[0].machines.composite.profile-governance.max-epochs=1024

Do not turn a retained fixed chain into governed by editing this setting. The authenticated governance configuration and epoch-0 marker make that fail closed. Start a new chain or, while Yano remains preview and history is disposable, reset that chain’s app-state on every member.

The stock bundle currently contains one executable preset per selected stock composition. It exercises governed genesis, proofs, status, packaging, and operations, but it cannot authorize a profile that is not in its catalog. A domain bundle enables real evolution by packaging the current and dormant target CompositeProfileCatalog.Entry values together.

One v1 bundle may contain 1-64 distinct canonical profiles. max-epochs bounds the total authenticated epoch history, including transitions back to an already packaged profile; it does not raise that 64-profile catalog bound. Keep every historical profile in the catalog. V1 has no authenticated replay floor that would make removing old executable code safe.

Before proposing anything:

  1. Build a composite provider whose catalog contains both the active profile and the target profile, including every component/workflow generation present in retained epoch history or needed for late effect results.
  2. Run replay, restart, snapshot, effect-result, quota-drain, and profile golden-vector tests for that catalog.
  3. Export the exact CompositeProfile.canonicalBytes() for the target as a binary review artifact. Its domain-separated digest is the proposal target.
  4. Deploy the same manifested bundle to every JVM member.
  5. Rolling-restart one member at a time. A member without the target may keep following the old profile, but it cannot honestly attest readiness.

Plugin JARs are deployed through the documented JVM plugin-directory flow. Yano X does not publish a native composite extension.

The distribution includes a dependency-free operator helper, appchain-cluster/profile-governance.py. It encodes each governance command as canonical CBOR:

Terminal window
cd appchain-cluster
./profile-governance.py --chain evidence-chain begin --encode-only \
--proposal-id "$PROPOSAL_ID" \
--base-digest "$ACTIVE_PROFILE_DIGEST" \
--membership-digest "$MEMBERSHIP_EPOCH_DIGEST" \
--profile target-profile.cbor \
--activation-height 1200 --expiry-height 1500 > begin.json

The output contains targetProfileDigest, chunkCount, proposalHash, and bodyHex. Read the active profile digest and currentMembershipDigest from the chain status. Always verify that the same membership epoch remains effective through the planned activation height.

Known issue. The helper’s online mode, which runs without --encode-only (status, submit, and --dry-run), calls /api/v1/app-chain/chains/{chain}/profile-governance routes that the node does not serve. Use --encode-only, then send the encoded bytes to the composite plugin’s route, as shown below.

The composite plugin serves governance under /api/v1/plugins/org.yanoproject.x.composite/chains/{chain}/profile-governance: a GET returns the governance status as CBOR, and a POST to …/commands takes one command as a raw CBOR body. The POST is a privileged route, so it needs the member’s full API key, and ?dry-run=true runs local validation without submitting. The member’s own node submits the command with its member key:

Terminal window
NODE=https://member-a.example:7070
ROUTE=/api/v1/plugins/org.yanoproject.x.composite/chains/evidence-chain/profile-governance/commands
# Once: an owner-only header file. printf is a shell builtin and cat is given
# only the file name, so the key never appears in a process's arguments.
(umask 077; { printf 'X-API-Key: '; cat /secure/member-a.key; } > /secure/member-a.headers)
jq -r .bodyHex begin.json | xxd -r -p > begin.cbor
curl -sS -X POST -H @/secure/member-a.headers \
-H 'Content-Type: application/octet-stream' --data-binary @begin.cbor \
"$NODE$ROUTE?dry-run=true" # {"validated":true}
curl -sS -X POST -H @/secure/member-a.headers \
-H 'Content-Type: application/octet-stream' --data-binary @begin.cbor \
"$NODE$ROUTE" # {"messageId":"…"}

-H @file (curl 7.55 or later) reads the header from the file, so curl’s arguments name only the file. Never put the key itself in a command, where other users and monitoring agents can read the process arguments, or in shell history. Keep both files owner-only. Use HTTPS for every non-loopback member endpoint and terminate it only at an operator-trusted proxy.

Stage each reported chunk and seal the proposal, waiting for each command to finalize before the next step:

Terminal window
./profile-governance.py --chain evidence-chain chunk --encode-only \
--proposal-id "$PROPOSAL_ID" --profile target-profile.cbor --index 0
./profile-governance.py --chain evidence-chain seal --encode-only \
--proposal-id "$PROPOSAL_ID"

Repeat chunk for indices 0 .. chunkCount-1. Conflicting or duplicate non-identical chunks void the proposal; identical replay is a no-op.

Approval and readiness are deliberately separate:

  • threshold member approvals authorize the exact intent;
  • every member in the bound membership epoch must attest that the exact target digest exists in its local executable catalog.

Each approving member encodes its commands and submits them through its own node, as above, so its own member key signs them:

Terminal window
./profile-governance.py --chain evidence-chain approve --encode-only \
--proposal-hash "$PROPOSAL_HASH"
./profile-governance.py --chain evidence-chain ready --encode-only \
--proposal-hash "$PROPOSAL_HASH" --target-digest "$TARGET_DIGEST"

A dry run of READY is the final local diagnostic. It fails when that node’s catalog lacks the target. Once the threshold and all-member readiness are finalized, status becomes SCHEDULED. Every block before H uses the old profile; block H uses the target for results, expiries, workflows, and normal messages. There is no local break-glass override.

Cancel before activation when necessary:

Terminal window
./profile-governance.py --chain evidence-chain cancel --encode-only \
--proposal-hash "$PROPOSAL_HASH"

Threshold cancellations void a proposal. It is also voided by expiry, by a change of the bound membership epoch, by malformed or conflicting staging, by a seal whose bytes or digest do not match, by invalid profile compatibility or quota rules, and by approvals and readiness that complete only at or after H. A BEGIN whose base digest is not the active profile is ignored rather than recorded.

The chain status/UI projects the active epoch, digest, proposal state, activation height, approvals, readiness, local readiness, and retired drains. Prometheus exports:

  • yano.appchain.composite.profile.epoch;
  • yano.appchain.composite.governance.proposal.state (0 none, 1 staging, 2 sealed, 3 scheduled);
  • yano.appchain.composite.governance.approvals;
  • yano.appchain.composite.governance.readiness;
  • yano.appchain.composite.governance.local.ready; and
  • yano.appchain.composite.governance.retired.drains.

Health reports {chain}.scheduledProfileMissing=true and goes down if a node has a scheduled target absent from its local catalog. This is a last-resort alarm, not permission to schedule before all members are ready.

Proof clients have two policies:

  • strict clients verify an MPF proof for the active marker at a trusted finalized root, then pin its expected digest; and
  • governance-aware clients verify finality and MPF inclusion for the current epoch pointer, every retained epoch record, and the active marker at the same root. They then check byte linkage with CompositeProfileEpochChainVerifier.verifyStructure(...) and independently enforce their membership/approval policy from finalized block history.

The dependency-free structural verifier does not fetch proofs or prove member authorization; it only checks the already-proven bytes supplied by the caller. For a complete offline trust-boundary check, use yano-x-composite-client’s GovernedCompositeVerifier: it first verifies portable block finality against caller-pinned membership, then verifies the pointer, every epoch, and the marker as MPF inclusions at that exact root, checks canonical linkage, and finally invokes the caller’s mandatory membership/approval-history policy. Applications with a fully reviewed proposal schedule can use requirePinnedProposalHashes(...); it fails unless every non-genesis epoch has exactly the expected proposal hash, with no missing or extra epoch. Neither policy makes unreviewed component code trustworthy.

  • Missing bundle before readiness: deploy it and retry local dry-run/READY.
  • Membership changes before activation: the proposal is void; repropose against the new membership epoch.
  • Missed activation after falsely claiming readiness: restore the exact bundle and catch up from finalized history; do not edit profile state.
  • Bad profile already activated: authorize a corrected future epoch. Never rewrite the marker, epoch records, or local database to create a hidden fork.
  • Lost governance threshold: use the membership-governance recovery policy first; profile governance cannot bypass membership authority.

The complete consensus contract is recorded in the repository’s ADR-015.