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.
The lifecycle
Section titled “The lifecycle”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.
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.
- 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.
- Begin Member A submits
BEGINwith the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. ABEGINthat 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 status Approvals Ready STAGING — — - 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 status Approvals Ready STAGING — — - 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 status Approvals Ready SEALED 0 of 2 0 of 3 - Approve Approval authorizes the exact intent. Each approving member submits
APPROVEwith 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 status Approvals Ready SEALED 2 of 2 0 of 3 - 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-runfails on a node that lacks it.Proposal record (threshold 2 of 3) Proposal status Approvals Ready SEALED 2 of 2 3 of 3 - Scheduled With threshold approvals and readiness from all members finalized before H, the proposal becomes
SCHEDULED. Status and theproposal.statemetric show it. A threshold ofCANCELcommands 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 status Approvals Ready SCHEDULED 2 of 2 3 of 3 - 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 profile Epoch Proposal target n + 1 none
What if: A member is not ready
Member C never deployed the target bundle.
- 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.
- Begin Member A submits
BEGINwith the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. ABEGINthat 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 status Approvals Ready STAGING — — - 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 status Approvals Ready STAGING — — - 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 status Approvals Ready SEALED 0 of 2 0 of 3 - Approve Approval authorizes the exact intent. Each approving member submits
APPROVEwith 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 status Approvals Ready SEALED 2 of 2 0 of 3 - 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 status Approvals Ready SEALED 2 of 2 2 of 3 - 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 status Approvals Ready none (deleted) — —
What if: A threshold cancels
The members change their minds after sealing.
- 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.
- Begin Member A submits
BEGINwith the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. ABEGINthat 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 status Approvals Ready STAGING — — - 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 status Approvals Ready STAGING — — - 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 status Approvals Ready SEALED 0 of 2 0 of 3 - Cancel Members A and C each submit
CANCELwith the proposal hash. Two cancellations meet the threshold.Proposal record (threshold 2 of 3) Proposal status Approvals Ready SEALED 0 of 2 0 of 3 - Proposal voided A threshold of cancellations voids the proposal before activation. The active profile is unchanged.
Proposal record Proposal status Approvals Ready none (deleted) — —
What if: Membership changes
A membership change takes effect before H.
- 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.
- Begin Member A submits
BEGINwith the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. ABEGINthat 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 status Approvals Ready STAGING — — - 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 status Approvals Ready STAGING — — - 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 status Approvals Ready SEALED 0 of 2 0 of 3 - Approve Approval authorizes the exact intent. Each approving member submits
APPROVEwith 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 status Approvals Ready SEALED 2 of 2 0 of 3 - 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-runfails on a node that lacks it.Proposal record (threshold 2 of 3) Proposal status Approvals Ready SEALED 2 of 2 3 of 3 - Scheduled With threshold approvals and readiness from all members finalized before H, the proposal becomes
SCHEDULED. Status and theproposal.statemetric show it. A threshold ofCANCELcommands 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 status Approvals Ready SCHEDULED 2 of 2 3 of 3 - 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 status Approvals Ready none (deleted) — —
What if: A conflicting chunk
Two different byte strings are staged for the same chunk index.
- 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.
- Begin Member A submits
BEGINwith the proposal id, the active profile's digest, the membership digest, the target digest, the activation height H, and an expiry. ABEGINthat 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 status Approvals Ready STAGING — — - 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 status Approvals Ready STAGING — — - 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 status Approvals Ready none (deleted) — —
- member node
- governance state
- activated
- void or not ready
Start a governed chain
Section titled “Start a governed chain”Governed mode is a consensus-affecting choice for a new chain:
yano.app-chain.chains[0].state-machine=compositeyano.app-chain.chains[0].machines.composite.preset=evidence-v1-gatedyano.app-chain.chains[0].membership.mode=governedyano.app-chain.chains[0].machines.composite.profile-mode=governedyano.app-chain.chains[0].machines.composite.profile-governance.min-activation-lag=20yano.app-chain.chains[0].machines.composite.profile-governance.proposal-ttl-blocks=600yano.app-chain.chains[0].machines.composite.profile-governance.max-epochs=1024Do 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.
Prepare the target bundle
Section titled “Prepare the target bundle”Before proposing anything:
- 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.
- Run replay, restart, snapshot, effect-result, quota-drain, and profile golden-vector tests for that catalog.
- Export the exact
CompositeProfile.canonicalBytes()for the target as a binary review artifact. Its domain-separated digest is the proposal target. - Deploy the same manifested bundle to every JVM member.
- 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.
Inspect and encode commands
Section titled “Inspect and encode commands”The distribution includes a dependency-free operator helper,
appchain-cluster/profile-governance.py. It encodes each governance command as
canonical CBOR:
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.jsonThe 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-governanceroutes 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:
NODE=https://member-a.example:7070ROUTE=/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.cborcurl -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:
./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.
Approve, attest readiness, and activate
Section titled “Approve, attest readiness, and activate”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:
./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:
./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.
Observe and verify
Section titled “Observe and verify”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(0none,1staging,2sealed,3scheduled);yano.appchain.composite.governance.approvals;yano.appchain.composite.governance.readiness;yano.appchain.composite.governance.local.ready; andyano.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.
Recovery rules
Section titled “Recovery rules”- 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.