PlatformJoint Authorization
Swarm
FROST Quorum
Roster, epoch checkpoint, one-shot slot, rotation, and the DKG ceremony: the five objects an operator runs to authorize a quorum-required action.
A quorum-required action needs two separate things: something to invoke it with, and something to have been assembled before it can be invoked at all. The invocation half is Bilateral Co-Sign, which covers the DSSE envelope and the Statement inside it, the strict verifier that refuses a co_sign: n_of_m predicate without an accompanying authorization, and the offline walk. What follows is the assembly half: the roster, the epoch checkpoint, the one-shot slot, rotation, and the ceremony that produces the group key. The seven registered classes appear in both places, there as ladder rows and here as signing domains with typed action preimages.
What a quorum-required class requires
The governance ladder marks an action class quorum-required and anchors it at frost-quorum. That declaration is inert on its own. Before such an action can run, an operator group has to hold a FROST Ed25519 group key produced by a distributed key-generation ceremony, publish a signed roster describing it, keep a signed epoch checkpoint in rollback-independent storage, and reserve a single-use authorization slot for the exact resource version being acted on. What the verifier finally accepts is one chio.frost.authorization.v1 envelope: a schema, a body, a suite id, and one aggregated group signature.
Contracts, verification, and the slot and rotation state machines live in chio-federation::frost; the ceremony, share verification, aggregation, and the signer in chio-federation-authority; ceremony, coordinator, signer, and rotation state in chio-store-sqlite::frost_store, with every custody payload encrypted under an operator-supplied FrostCustodyKey; a lease-based coordinator and an HTTPS anchor client in chio-control-plane. frost-ed25519 is a non-optional dependency, and the only suite a roster accepts is FROST-ED25519-SHA512-v1.
No CLI, and the anchor is somebody else's server
chio subcommand touches the ceremony, the roster, the slot, or rotation: there is no frost reference anywhere under crates/products/chio-cli/src. chio-control-plane does export frost_coordinator_router, an axum router over seven POST paths under /v1/frost/coordinator/sessions/, but no binary in the repo mounts it. The compare-and-swap anchor is external in the stronger sense: the repo ships the client and no handler for any path it calls.Five objects, three pinned authorities
| Object | Schema id | Signed by | What it fixes |
|---|---|---|---|
| Roster | chio.frost.roster.v1 | Roster authority | Participants, verification shares, group key, threshold, allowed domains, epoch, validity window. |
| Epoch checkpoint | chio.frost.epoch-checkpoint.v1 | Epoch anchor | Which roster is active now, at what sequence, activation fence, and clock high-water. |
| Authorization slot | chio.frost.authorization-slot-checkpoint.v1 | Slot anchor | One execution slot and its state: bound, completed, or burned. |
| Epoch advance | chio.frost.session-burn-summary.v1 plus a roster-rotate authorization | The old roster’s quorum | The successor roster, the burned old-epoch sessions, the new activation fence. |
| Ceremony transcript | chio.frost.dkg-package.v1 | Each sender’s transport key | One participant set at one epoch, and the transcript digest the roster carries. |
That column is the schema id the Rust type carries. It says nothing about whether a JSON Schema exists for it. 4 FROST artifacts have one, with fixtures, under spec/schemas/chio-frost/v1/, and conformance pins the artifact registry to exactly that set: roster, epoch checkpoint, authorization-slot checkpoint, and the authorization envelope. The other two have no published file and do not even share a crate: the burn summary’s schema string is a pub const in chio-federation/src/frost/rotation.rs, while the DKG package’s is a private const in chio-federation-authority/src/frost_ceremony.rs, so nothing outside the ceremony crate can even name it.
The first three verify against FrostArtifactTrustStore, which pins Ed25519 keys per role: Roster, EpochAnchor, AuthorizationSlotAnchor. It requires at least one root and refuses construction if the same public key appears twice, even under different roles, so one operator key cannot serve as both roster authority and its own anchor.
Roster
A roster carries three digests, each over a domain-prefixed canonical preimage. rosterId covers the body; the authority signature covers rosterId plus the body; rosterDigest covers all of that plus the signature. Since an authorization names a rosterDigest, resigning an otherwise identical roster yields a different digest and breaks every authorization pinned to the old one.
A roster is single-quorum by construction. allowedDomains must be non-empty, sorted, and unique, and every entry must be a registered action whose quorum_n, quorum_m, and quorum scope equal the roster’s threshold, participant count, and authorityScope. A 2-of-3 roster therefore cannot also authorize governance.case_enforce_sanction, registered 3-of-5. Participants must be sorted by unique id with unique shares, and the group key and every share must deserialize as real FROST Ed25519 material rather than pass a length check. Rosters chain too: predecessorRosterDigest is absent only at key epoch 1 and required after.
pub fn validate_for_active_resolution(&self) -> Result<(), FrostRosterError> {
self.validate()?;
if self.key_origin != FrostRosterKeyOrigin::DistributedDkg {
return Err(FrostRosterError::ContractMismatch(
"active rosters must originate from distributed DKG",
));
}
Ok(())
}Every live path calls it: resolution, epoch advance, coordinator validation, aggregation. Historical evidence verification validates the roster and its pinned signature but never the key origin, so a dealer_fixture roster can back an audit and can never become signing authority.
Epoch checkpoint
resolve_active_roster_for_execution is the only constructor of a VerifiedActiveFrostRoster. It refuses unless every one of these holds:
- The candidate roster verifies against a pinned
Roster-role key, passes active-resolution validation, names the requested scope, and is inside[validFrom, validUntil). classify_scope(scope_id)equals the roster’sauthorityScope. The classification comes from trusted configuration, not from the roster’s claim about itself.- The anchor’s checkpoint agrees on scope, active roster id, active roster digest, and key epoch.
groupPublicKeyDigestequals the SHA-256 of the decoded group key bytes.nowis at or past the checkpoint’sclockHighWater.
Checkpoints chain. predecessorDigest is absent only at sequence 1 and rotationAuthorizationDigest only at key epoch 1, so every later checkpoint names the FROST authorization that produced it. Later steps re-read it through verify_current_epoch: a roster that rotates between resolution and use fails rather than signing under a retired epoch.
Authorization slot
The one-shot property is enforced by an identifier, not a convention. The slot id is a SHA-256 over a prefixed canonical preimage of five fields; the session id is a SHA-256 over the authorization id, the signing-message digest, and the roster digest:
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FrostAuthorizationSlotIdPreimage<'a> {
domain: FrostAuthorizationDomain,
scope_id: &'a str,
resource_id: &'a str,
resource_version: u64,
resource_fence: u64,
}The action digest is deliberately absent from that preimage. Two bodies that differ only in what they authorize collide onto the same slot, which is the point: the second cannot be signed, because the slot is already bound to the first message.
The scheme guarantees the collision. The refusal is the operator's. One slot id addresses one (domain, scope, resource, version, fence) tuple, so a second action against the same resource version has nowhere else to go, and the crate's FrostAuthorizationSlotAnchor is a trait with one resolver: the compare-and-swap that refuses to re-bind a bound slot belongs to the anchor an operator supplies. The conformance suite defines a single-binding signing head and a compare-and-swap resource head of its own to prove what a correct anchor buys, asserting that a conflicting body computes the identical slot id, that its signature is refused, and that a rolled-back local projection cannot re-execute against the external resource head.
A slot moves through three states, and FrostAuthorizationSlotState has no fourth. Each transition is a compare-and-swap on the anchor that takes a Verified* value only the matching verification function can produce, so no path writes an unverified transition:
| State | Written by | slotVersion | predecessorDigest | Proof payload |
|---|---|---|---|---|
bound | compare_and_swap_bind | 1 | absent | None. The aggregate signature digest, the authorization blob digest, and the availability receipt are all absent. |
completed | compare_and_swap_complete | 2 | the bound checkpoint’s checkpointDigest | All three: the aggregate digest, the byte-exact canonical blob, and an availability receipt. |
burned | compare_and_swap_burn | 2 | the bound checkpoint’s checkpointDigest | None, the same three fields absent. Neither terminal state has a successor. |
Bind compares the checkpoint against the body field by field: scope, slot id, domain, ladder class, resource id, version, fence, authorization id, signing-message digest, action digest, roster digest, key epoch, session id. Completion additionally requires the anchored blob to be the byte-exact canonical JSON of the proof, its digest and the aggregate signature digest to match the checkpoint, and an availability receipt to be present. Burn carries none of those three. The two terminal paths compare against different things. The burn verifier walks the burned checkpoint field by field against the bound one and refuses a clock high-water behind it; the live completion verifier compares against the authorization body and requires only that predecessorDigest be the expected bound digest, leaving the full field-by-field successor check to verify_historical_completed_authorization.
The action registry, as the signer sees it
REGISTERED_FROST_ACTIONS is a const array of seven entries. Each pairs a signing domain with a ladder action class, a quorum, and one typed action preimage schema. All seven carry quorum scope treaty.
| Signing domain | Ladder action class | Quorum | Action preimage schema |
|---|---|---|---|
chio.frost.settle-commitment.v1 | settle.commitment | 2-of-3 | chio.frost.action.settle-commitment.v1 |
chio.frost.clearing-round-finalize.v1 | clearing.round_finalize | 2-of-3 | chio.frost.action.clearing-round-finalize.v1 |
chio.frost.channel-close.v1 | channel.close | 2-of-3 | chio.frost.action.channel-close.v1 |
chio.frost.pouncer-revoke-credential.v1 | pouncer.revoke_credential | 2-of-3 | chio.frost.action.pouncer-revoke-credential.v1 |
chio.frost.governance-case-enforce-sanction.v1 | governance.case_enforce_sanction | 3-of-5 | chio.frost.action.governance-case-enforce-sanction.v1 |
chio.frost.credentials-passport-revoke.v1 | credentials.passport_revoke | 2-of-3 | chio.frost.action.credentials-passport-revoke.v1 |
chio.frost.roster-rotate.v1 | governance.roster_rotate | 3-of-5 | chio.frost.action.roster-rotate.v1 |
The ladder row each registration commits to is a literal in the crate, not a read of the operator’s manifest. Every entry hard-codes mode: receipt_backed, destructive: true, cross_org_visibility: federated, an evidence list, the co-sign quorum, consistency_model: quorum-required, and consistency_anchor: frost-quorum, and the SHA-256 of that canonical entry is the ladderContractDigest every body must carry. So a deployment that words one of these rows differently computes a different digest and fails with ContractMismatch until the registration itself changes.
An eighth domain, chio.frost.adjudication-panel-decision.v1, exists in the enum with no registration, and every gate refuses it in its own vocabulary: an authorization body fails with DisabledDomain, a roster listing it fails allowedDomains as a reserved or unregistered domain, a slot checkpoint fails as a disabled checkpoint domain. Nothing falls through to a default. Conformance pins that it stays disabled until its ladder contract is registered.
resourceId, resourceVersion, and resourceFence are not free-form. Each typed preimage names which of its own fields fills them: channel.close uses the channel id with channelStateVersion and lifecycleFence, governance.roster_rotate the scope id with the current key epoch and the activation fence. Producers derive the preimage from verified state: chio-settle’s channel_close_frost_action reads every field but one out of a verified effective close, takes the publisher fence as a caller argument, and validates the preimage before returning it (see Payment Channels and Clearing Rounds).
The ceremony, and what a transcript proves
begin_frost_ceremony, advance_frost_ceremony, and complete_frost_ceremony wrap the three upstream DKG rounds. Every package is a chio.frost.dkg-package.v1 whose sender signs, with its Ed25519 transport key, a prefixed canonical preimage of the schema, ceremony id, participant set digest, key epoch, round, sender, optional recipient, package digest, and transport key id. The ceremony id derives from the participant set digest, scope, and epoch, so a package minted for one participant set cannot be replayed into another.
Round one is broadcast and must contain exactly one package from every participant. Round two is directed and must contain every ordered sender-recipient pair exactly once, none self-addressed. Both transcripts are validated in full before any secret is opened, and completion re-checks the upstream result: the public key package must still report the configured threshold and participant count and cover every participant.
Two functions verify a transcript without opening any custody record. verify_frost_ceremony_round1_transcript checks the broadcast round; verify_frost_ceremony_transcript checks both together. Each returns the transcript digest, ordered by round, sender, and recipient, which is what a roster carries as ceremonyTranscriptDigest.
A round-two transcript is not audit material
Zeroizing and redacted in Debug. Confidentiality of delivery is the caller’s. Round one is safe to hand an outside auditor; a full transcript is not.The ceremony is stricter than the roster contract
Custody records are typed by round, so a round-one record cannot be handed to completion. The same care continues at signing time: a nonce is consumed by exactly one share, and a share is refused if the signing package’s message digest or commitment differs from what was authorized. The doc comment gives the reason: a FROST nonce that signs two different messages discloses the signing share, so a retry takes a fresh nonce.
Rotation
Rotating a roster is one of the seven registered classes, so it consumes a slot under the outgoing roster before the incoming one exists. verify_frost_epoch_advance takes the predecessor checkpoint, target roster, verified rotation authorization, typed action, and burn summary, and returns the VerifiedFrostEpochAdvance the anchor writer consumes. It refuses on any of:
- A rotation authorization that is not the registered 3-of-5 treaty class, or is not current at the anchor clock.
- A target roster that fails its pinned signature or does not originate from distributed DKG. Rotation cannot install a dealer fixture.
- A scope disagreement between predecessor, target roster, action, and burn summary.
- Epoch discontinuity, or a predecessor mismatch across the roster digest chain, checkpoint sequence, or checkpoint digest.
- An activation fence that did not strictly advance, or a clock high-water behind the predecessor or outside the target roster’s validity window.
- Group key reuse. If the SHA-256 of the target group key equals the predecessor’s
groupPublicKeyDigest, the advance is rejected.
The burn summary is what makes the old epoch quiet. It lists the burned session ids, sorted and unique, commits them under a prefixed burn root over scope, key epoch, and the id set, and carries a liveSessionCount that must be zero, with a dedicated LiveSessionsRemain error when it is not. The action’s oldSessionBurnRoot must equal the recomputed root, so the abandoned sessions are committed inside the FROST-signed rotation rather than asserted alongside it.
if rotation_authorization.domain() != FrostAuthorizationDomain::RosterRotate
|| rotation_authorization.ladder_action_class() != "governance.roster_rotate"
|| rotation_authorization.quorum_n() != 3
|| rotation_authorization.quorum_m() != 5
|| rotation_authorization.quorum_scope() != "treaty"
{
return Err(FrostEpochAdvanceError::Invalid(
"rotation authorization is not the registered 3-of-5 governance class",
));
}The literal quorum is restated here even though the body already validated against the registry. Rotation is the one action that changes who signs everything else, so the check does not delegate.
Verification at execution
pub fn verify_for_execution(
proof: &FrostAuthorizationV1,
expected: &ExpectedFrostAuthorization<'_>,
active_roster: &VerifiedActiveFrostRoster,
epoch_anchor: &dyn FrostEpochAnchor,
slot_anchor: &dyn FrostAuthorizationSlotAnchor,
artifact_trust: &FrostArtifactTrustStore,
now: u64,
) -> Result<VerifiedFrostAuthorization, FrostVerificationError>The order matters. It validates the proof, matches all eight fields of ExpectedFrostAuthorization against the body, checks both validity windows, re-reads the epoch checkpoint, checks the roster binding including that the roster allows this domain and agrees on the quorum, resolves the slot and requires it to be completed already, and only then verifies the group signature. So this function does not authorize an action. It confirms that a slot was consumed to authorize one.
Auditing uses two other entry points. verify_historical_evidence checks a proof against a retired roster resolved by digest and epoch, with no slot requirement; verify_historical_completed_authorization adds the bound and completed checkpoints and pins the completion time inside both the authorization window and the roster window. A retired epoch is evidence, not authority.
What holds the guarantee
Four different things carry the properties above: crate code, a conformance or unit test, a published schema, and the operator. Which one holds a given property decides what an auditor can check without running the deployment.
| Held by | Property | Where |
|---|---|---|
| Crate code | Contracts, verification, and the slot and rotation state machines for all five objects. | crates/trust/chio-federation/src/frost/ |
| Crate code | DKG ceremony, share verification, aggregation, and signer; SQLite state with custody payloads encrypted under an operator-supplied key; an HTTPS anchor client and a mountable lease-based coordinator router. | crates/trust/chio-federation-authority/src/, crates/platform/chio-store-sqlite/src/frost_store/, crates/platform/chio-control-plane/src/trust_control/frost.rs |
| Test | The registry is closed at seven entries with exactly these classes and quorums. | action_registry_is_closed_and_covers_every_current_n_of_m_class in crates/trust/chio-federation/tests/frost_authorization.rs |
| Test | Three participants running the ceremony independently derive the same transcript digest and group key. A reordered participant set, a duplicate round-one package, and one flipped hex character are each rejected. | crates/trust/chio-federation-authority/tests/frost_ceremony.rs |
| Test | A conflicting body computes the identical slot id and is refused, and a rolled-back local projection cannot re-execute against the external resource head. Both heads are modelled in the test. | external_slot_and_resource_heads_prevent_conflicting_signatures_and_reexecution in crates/tooling/chio-conformance/tests/frost_quorum.rs |
| Test | An upstream official FROST Ed25519 vector verifies, and a retired epoch verifies as historical evidence but not as execution authority. These fixtures sign with the group key directly; real share aggregation is exercised by the coordinator tests. | crates/trust/chio-federation/tests/frost_vectors.rs, crates/platform/chio-store-sqlite/tests/frost_coordinator.rs |
| Schema | 4 JSON Schemas with fixtures, asserted to be exactly the FROST rows in the artifact registry. Schema and signature are separate gates: a tampered roster passes the schema and fails the trust store. | spec/schemas/chio-frost/v1/, crates/tooling/chio-conformance/tests/frost_quorum.rs |
| Crate code | Refusals: the adjudication-panel domain; active rosters not from distributed DKG; group key reuse across an epoch advance; rotation with any live old-epoch session; a roster whose allowed domains disagree with its own threshold; a body whose ladder contract digest is not the crate’s literal entry. | crates/trust/chio-federation/src/frost/registry.rs, crates/trust/chio-federation/src/frost/roster.rs, crates/trust/chio-federation/src/frost/rotation.rs |
| The operator | The anchor itself. The repo carries the client and no handler for the five paths it calls: /v1/frost/epochs/{scopeId}, its compare-and-swap child, /v1/frost/authorization-slots/{scopeId}/{slotId} and that path’s bind child, and /v1/frost/authorization-slot-transitions/{boundDigest}/{complete|burn}. The router that does ship, frost_coordinator_router, serves a disjoint set under /v1/frost/coordinator/sessions/: it drives the DKG ceremony, not the anchor. Nothing under crates/products/chio-cli/src mentions frost, so the family is driven through the crates rather than a subcommand. | crates/platform/chio-control-plane/src/trust_control/frost.rs, .../trust_control/frost/coordinator.rs |
| The operator | Rollback independence of the anchor, confidentiality of round-two DKG delivery, and the custody key itself. The traits and the upstream contract require all three; nothing in the crate can enforce them. | Trait docs on FrostEpochAnchor and FrostAuthorizationSlotAnchor; FrostCustodyKey::new |
One limit follows from the shape rather than any single line. Everything above assumes a live anchor: while it is unreachable the slot cannot be bound and the epoch cannot be re-read, so no quorum-required action can be authorized. There is no degraded mode and no offline fallback in the code.
Next steps
- Bilateral Co-Sign · the counterpart page: the co-signed receipt, the strict verifier, and the offline walk
- Governance Ladder · where a class is declared
quorum-requiredin the first place - Swarm Overview · the rung, its filing test, and where joint authorization sits inside it
- Payment Channels · the
channel.closeproducer, fences included - Clearing Rounds · the
clearing.round_finalizeproducer