FROST Quorum
Authorize a consequential action with a threshold of signers, and check that authorization against the current roster, epoch, and resource state.
On this page
A settlement, a sanction, or a credential revocation can require several operators to agree before it runs. Each participant holds one signing share, a threshold of them signs one authorization message, and the kernel checks that authorization against the current roster, key epoch, and resource state before the effect.
The authorization names this action, for this resource, under this roster and key epoch. For an n_of_m invocation it travels beside the two passport signatures of Bilateral Co-Sign as a separate artifact.
Signers authorize together, and the kernel verifies at the point of action
Each selected participant contributes a signature share over the same authorization message. The coordinator verifies the shares against the roster and aggregates them into one signature under the roster’s group public key.
ecurrent epochscompleted slotxexpected inputsΣgroup signature
Two of three signers authorize one message
Each selected signer contributes a share over the same authorization message. The coordinator verifies the shares against the roster and aggregates them into one signature under the roster’s group public key; C stays in the roster without signing this session. The registered action class fixes the quorum, 2 of 3 or 3 of 5, scoped to a treaty. FROST Quorum.
The registered action class fixes the quorum, 2 of 3 or 3 of 5, scoped to a treaty. In the 2-of-3 session drawn here, A and B sign while C stays on the roster, and the commitment and signing-package exchange is folded into the resulting shares. The group signature authenticates the authorization body.
To execute now, verify_for_execution adds the checks on the figure’s rail: the active roster and current epoch, the completed slot holding the exact proof bytes, and eight expected inputs taken from the intended action and resource state. The application then claims the resource at its current fence and performs the effect with an idempotency key. Verification itself consumes nothing.
The action class fixes the quorum and its contract
The registry fixes each class’s quorum, treaty scope, and typed action schema together. Five classes take 2 of 3: settle.commitment, clearing.round_finalize, channel.close, pouncer.revoke_credential, and credentials.passport_revoke. governance.case_enforce_sanction and governance.roster_rotate take 3 of 5.
An authorization’s ladderContractDigest must equal the canonical hash of the registered ladder entry, so a changed class or quorum without a changed contract is refused. A roster’s nonempty allowedDomains list holds only registered actions with its exact threshold, participant count, and authority scope, and a 2-of-3 roster can never authorize the 3-of-5 sanction class.
Each class signs under its own domain, such as chio.frost.settle-commitment.v1, and commits to a typed action, such as chio.frost.action.settle-commitment.v1. The typed action serializes as a domain and action wrapper, and actionDigest is the SHA-256 of its canonical JSON after validation, so the digest binds the action’s parameters as well as its class.
Read more in Governance Ladder, which classifies actions across operators.
One resource position accepts one authorization message
The slot address commits to domain, scopeId, resourceId, resourceVersion, and resourceFence, and leaves out actionDigest, so two conflicting actions at the same resource position compete for one slot. The slot anchor’s compare-and-swap decides which message owns it.
- bound. Fixes the authorization id, signing message, action, roster, epoch, and session.
- completed. Commits to the aggregate signature digest and the exact canonical authorization blob.
- burned. Refuses completion of the slot.
The conformance source shows why the address omits the action. A settlement of 12,500 base units for operation settlement.conformance sits at resource version 1 and fence 11. Changing the amount to 12,501 changes the action digest and the authorization id but keeps the slot id, so the signing head refuses the competing message. Retrying the same authorization returns the same signed proof.
After execution, a separate resource head keeps the consumed authorization, and a restored local projection cannot execute it again. The protection holds while the slot anchor, the resource store, and the effect path each keep their state and atomicity contracts.
Execution compares the proof with inputs from the intended action
verify_for_execution compares eight ExpectedFrostAuthorization inputs with the signed body: the domain, ladder action class, ladder contract digest, scope, resource id, resource version, resource fence, and action digest. Derive them from the action being performed and from authoritative resource state, because values copied out of an untrusted proof make the comparison circular.
The returned VerifiedFrostAuthorization exposes verify_action_preimage, which checks a typed action’s domain, resource, version, fence, and digest. The caller then claims current resource state atomically and coordinates the external effect with an idempotency key.
Evidence and permission are separate checks. verify_historical_evidence resolves the roster named at issuance and checks its signature, binding, and group signature without current authority or a completed slot, so a proof can stay evidence after it stops authorizing execution.
Read more in Verification modes.
Key generation and signing run as separate protocols
Distributed key generation gives each participant a key package and the group a shared public-key package. Signing sessions later use those shares to authorize one message.
begin_frost_ceremony. Creates the local round-one secret and the authenticated broadcast package.advance_frost_ceremony. Validates the round-one transcript and creates recipient-specific round-two packages.complete_frost_ceremony. Validates both rounds and produces the local key package, group public key, verification shares, and transcript digest.
Package signatures authenticate the round packages. Confidential delivery of the round-two material and durable custody of each key share come from the deployment; secret buffers use zeroizing storage and redacted Debug output inside the process.
Worked Rust example: Run DKG and produce a threshold authorization
Run DKG and produce a threshold authorization
Three participant objects exchange authenticated DKG packages and obtain matching group keys. Two signers prepare one-use nonces, sign the selected package, and contribute verified shares to an aggregate authorization.
Use the Rust example setup, or download the complete source project. The files below belong to that project; shared constructors and configuration are included in the download.
Run it
cargo run --locked --features frost --bin frostImplementation
use chio_core_types::{sha256_hex, Keypair};
use chio_federation::frost::*;
use chio_federation_authority::*;
use rand_core::OsRng;
use std::collections::BTreeMap;
fn ceremony_and_roster() -> Result<
(
FrostRosterV1,
FrostAuthorizationBodyV1,
Vec<FrostCeremonyCompletion>,
),
Box<dyn std::error::Error>,
> {
let registration = frost_action_registration(FrostAuthorizationDomain::SettleCommitment)
.ok_or("unregistered action")?;
let participant_ids: Vec<_> = (1..=registration.quorum_m)
.map(|i| format!("operator-{i}"))
.collect();
let keys: Vec<_> = participant_ids
.iter()
.map(|_| Keypair::generate())
.collect();
let participants: Vec<_> = participant_ids
.iter()
.zip(&keys)
.map(|(id, key)| FrostCeremonyParticipant {
participant_id: id.clone(),
transport_key_id: format!("{id}.transport"),
transport_public_key: key.public_key(),
})
.collect();
let configs: Vec<_> = participant_ids
.iter()
.map(|id| FrostCeremonyConfig {
scope_id: "settlement.atlantic.v1".into(),
key_epoch: 1,
threshold: registration.quorum_n,
predecessor_roster_digest: None,
participants: participants.clone(),
local_participant_id: id.clone(),
})
.collect();
// Each participant holds its own secret. Only packages cross these boundaries.
let mut round1 = Vec::new();
let mut secrets1 = Vec::new();
for (config, key) in configs.iter().zip(&keys) {
let transition = begin_frost_ceremony(config, key, &mut OsRng)?;
round1.push(transition.package);
secrets1.push(transition.secret);
}
let mut round2 = Vec::new();
let mut secrets2 = Vec::new();
for ((config, key), secret) in configs.iter().zip(&keys).zip(secrets1) {
let transition = advance_frost_ceremony(config, key, secret, &round1)?;
round2.extend(transition.packages);
secrets2.push(transition.secret);
}
let mut completed = Vec::new();
for (config, secret) in configs.iter().zip(secrets2) {
completed.push(complete_frost_ceremony(config, secret, &round1, &round2)?);
}
assert!(completed
.windows(2)
.all(|pair| pair[0].group_public_key == pair[1].group_public_key));
let roster_authority = Keypair::generate();
let mut roster = FrostRosterV1 {
schema: CHIO_FROST_ROSTER_SCHEMA.to_string(),
roster_id: String::new(),
roster_digest: String::new(),
authority_scope: registration.quorum_scope.to_string(),
scope_id: "settlement.atlantic.v1".to_string(),
allowed_domains: vec![FrostAuthorizationDomain::SettleCommitment],
key_epoch: 1,
threshold: registration.quorum_n,
participant_count: registration.quorum_m,
participants: participant_ids
.iter()
.map(|participant_id| FrostParticipantV1 {
participant_id: participant_id.clone(),
verification_share: completed[0].verification_shares[participant_id].clone(),
})
.collect(),
group_public_key: completed[0].group_public_key.clone(),
suite_id: FROST_ED25519_SHA512_SUITE_ID.to_string(),
key_origin: FrostRosterKeyOrigin::DistributedDkg,
ceremony_transcript_digest: completed[0].transcript_digest.clone(),
predecessor_roster_digest: None,
valid_from: swarm_authority_examples::now_ms() / 1000 - 1,
valid_until: swarm_authority_examples::now_ms() / 1000 + 60,
roster_authority_key_id: "roster-authority.v1".to_string(),
roster_authority_signature: String::new(),
};
roster.roster_id = roster
.recompute_roster_id()
.unwrap_or_else(|error| panic!("roster id: {error}"));
roster.roster_authority_signature = roster_authority
.sign(
&roster
.signing_bytes()
.unwrap_or_else(|error| panic!("roster bytes: {error}")),
)
.to_hex();
roster.roster_digest = roster
.recompute_roster_digest()
.unwrap_or_else(|error| panic!("roster digest: {error}"));
let action = FrostActionPreimageV1::SettleCommitment(FrostSettleCommitmentActionV1 {
schema: CHIO_FROST_SETTLE_COMMITMENT_ACTION_SCHEMA.to_string(),
settlement_body_digest: "94".repeat(32),
payer_id: "payer-1".to_string(),
payee_id: "payee-1".to_string(),
amount_base_units: "100".to_string(),
asset_id: "usd.test".to_string(),
operation_id: "settlement-operation-1".to_string(),
rail_idempotency_key: "rail-settlement-operation-1".to_string(),
resource_version: 1,
resource_fence: 7,
});
let mut body = FrostAuthorizationBodyV1 {
schema: CHIO_FROST_AUTHORIZATION_BODY_SCHEMA.to_string(),
authorization_id: String::new(),
domain: FrostAuthorizationDomain::SettleCommitment,
ladder_action_class: registration.ladder_action_class.to_string(),
ladder_contract_digest: registration
.ladder_contract_digest()
.unwrap_or_else(|error| panic!("ladder digest: {error}")),
quorum_n: registration.quorum_n,
quorum_m: registration.quorum_m,
quorum_scope: registration.quorum_scope.to_string(),
scope_id: roster.scope_id.clone(),
resource_id: "settlement-operation-1".to_string(),
resource_version: 1,
resource_fence: 7,
action_digest: action
.action_digest()
.unwrap_or_else(|error| panic!("action digest: {error}")),
roster_digest: roster.roster_digest.clone(),
key_epoch: roster.key_epoch,
issued_at: swarm_authority_examples::now_ms() / 1000 - 1,
expires_at: swarm_authority_examples::now_ms() / 1000 + 60,
};
body.authorization_id = body
.recompute_authorization_id()
.unwrap_or_else(|error| panic!("authorization id: {error}"));
Ok((roster, body, completed))
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (roster, body, completed) = ceremony_and_roster()?;
println!(
"DKG complete: {} participants; threshold {}",
roster.participant_count, roster.threshold
);
let mut commitments = BTreeMap::new();
let mut preparations = BTreeMap::new();
for (participant, completion) in roster.participants.iter().zip(&completed) {
let prepared = prepare_frost_signer(&completion.key_package, &mut OsRng)?;
validate_frost_signing_commitment(
&roster,
&participant.participant_id,
prepared.signer_identifier_bytes(),
prepared.commitment_bytes(),
)?;
commitments.insert(
participant.participant_id.clone(),
prepared.commitment_bytes().to_vec(),
);
preparations.insert(participant.participant_id.clone(), prepared);
}
let package = build_frost_signing_package(&body, &roster, &commitments)?;
let message_digest = sha256_hex(&body.signing_bytes()?);
let mut shares = BTreeMap::new();
for id in package.participant_ids() {
let index = roster
.participants
.iter()
.position(|p| &p.participant_id == id)
.ok_or("unknown signer")?;
let prepared = preparations.remove(id).ok_or("missing nonce")?;
// Consuming this nonce prevents accidental reuse within this process.
let share = create_frost_signature_share(
&completed[index].key_package,
prepared.into_nonce_secret(),
package.bytes(),
&message_digest,
&commitments[id],
)?;
verify_frost_signature_share(&body, &roster, package.bytes(), id, share.share_bytes())?;
shares.insert(id.clone(), share.share_bytes().to_vec());
}
let authorization = aggregate_frost_authorization(&body, &roster, package.bytes(), &shares)?;
println!("aggregate authorization: {} shares", shares.len());
std::fs::write(
"frost-authorization.json",
serde_json::to_vec_pretty(&authorization)?,
)?;
let mut changed = body.clone();
changed.resource_fence += 1;
changed.authorization_id = changed.recompute_authorization_id()?;
let error = aggregate_frost_authorization(&changed, &roster, package.bytes(), &shares)
.expect_err("changed action must refuse");
println!("changed resource fence: {error}");
Ok(())
}Captured result and refusal
DKG complete: 3 participants; threshold 2
aggregate authorization: 2 shares
changed resource fence: invalid FROST coordinator input: signing package message differs from the authorizationThis performs real cryptography inside one process. A deployed signer needs separate custody, authenticated transport, durable nonce burning, a trusted active roster and the required live slot anchor. Aggregating an authorization does not settle the declared payment.
Exit 0 indicates a successful example run. Expected refusals shown in the output are checked by the program; invalid command inputs exit nonzero. Keys, digests, timestamps and run-directory names vary.
The downloadable project runs the ceremony across three participant objects in one process. Two signers then prepare one-use nonces and contribute verified shares to an aggregate authorization, and a signing package built for a changed resource fence is refused.
Roster, slot, and verification reference
Try roster_rotate, rosterDigest, or verify_for_execution.
Registered action quorums
| Ladder action class | Required quorum |
|---|---|
settle. | 2 of 3 · treaty |
clearing. | 2 of 3 · treaty |
channel.close | 2 of 3 · treaty |
pouncer. | 2 of 3 · treaty |
governance. | 3 of 5 · treaty |
credentials. | 2 of 3 · treaty |
governance. | 3 of 5 · treaty |
Signing domains and typed action schemas
The signing domain names the authorization class; the typed action schema names the action preimage. Use each registered domain with its matching schema.
| Signing domain | Typed action schema |
|---|---|
chio. | chio. |
chio. | chio. |
chio. | chio. |
chio. | chio. |
chio. | chio. |
chio. | chio. |
chio. | chio. |
Authority records
- Roster. A pinned roster-authority key signs the participants, verification shares, group key, threshold, allowed domains, scope, key origin, and validity window.
- Epoch checkpoint. A pinned epoch-anchor key signs the active roster and group-key commitments for a scope.
- Authorization slot checkpoint. A pinned slot-anchor key signs the binding between one resource position, one authorization message, and its bound, completed, or burned state.
- Authorization proof. The FROST group signature covers the canonical authorization body, including the action digest, resource version and fence, roster digest, and key epoch.
- DKG package. A participant’s transport key authenticates a ceremony round package and its sender, recipient where applicable, ceremony, and participant-set commitments.
- Session burn summary. A canonical commitment to the old epoch’s burned session ids; a registered roster-rotation authorization binds its root into the epoch advance.
The group signs a domain-separated message over the body, separate from the transport envelope. The body includes authorizationId, which is recomputed from the other body fields:
CHIO-FROST-AUTHORIZATION-V1
+ one NUL byte
+ canonical JSON (validated body)FrostArtifactTrustStore pins Ed25519 keys to the Roster, EpochAnchor, and AuthorizationSlotAnchor roles and refuses reuse of one public key, across roles included. Key separation is what the store enforces; independent operators and failure domains come from how the keys are deployed.
Wire schema identifiers
| Record | Schema identifier |
|---|---|
Roster | chio. |
Epoch checkpoint | chio. |
Slot checkpoint | chio. |
Authorization proof | chio. |
Authorization body | chio. |
DKG package | chio. |
Burn summary | chio. |
The published chio-frost/v1 schema directory holds four standalone JSON Schema files: roster, epoch checkpoint, slot checkpoint, and authorization. The body is defined inside the authorization schema. A Rust type’s schema string implies no separate file, and an external implementation may use any wire identifier whatever the Rust constant’s visibility.
Roster commitments
Participants have sorted, unique ids and unique verification shares. The group key and shares must decode as FROST Ed25519 material, and the supported suite is FROST-ED25519-SHA512-v1. Key epoch 1 has no predecessor roster digest; later epochs require one.
rosterId. SHA-256 of the roster body prefixed withchio.frost.roster.id.v1and a NUL byte.- Authority signature. The roster authority signs
rosterIdplus the body, prefixed withCHIO-FROST-ROSTER-V1and a NUL byte. rosterDigest. SHA-256 ofrosterId, body, and authority signature, prefixed withchio.frost.roster.digest.v1and a NUL byte.
An authorization pins rosterDigest, so a changed signed roster changes its commitment; re-signing identical bytes with the same deterministic Ed25519 key can reproduce the same digest. Active resolution requires keyOrigin: distributed_dkg, while historical evidence can use a valid dealer_fixture roster. The origin label and transcript digest are authenticated roster assertions, and active resolution reads them as such without rerunning the ceremony.
Active roster resolution
resolve_active_roster_for_execution combines the signed roster with the current signed epoch checkpoint. It checks the trusted scope classification, roster id and digest, key epoch, decoded group-key digest, and the roster’s validity window, and evaluation time cannot precede the checkpoint’s clock high-water mark.
The resulting VerifiedActiveFrostRoster carries that resolved authority. Live proof verification reads the epoch anchor again and requires the binding to remain current. The anchor’s contract is authoritative state that a restored local database cannot roll back.
Slot transitions
bound is version 1 with no predecessor and no completion payload. completed and burned are version 2, each naming the bound checkpoint digest; completed also carries an availability-receipt identifier, and both are terminal.
FrostAuthorizationSlotAnchorWriter exposes compare_and_swap_bind, compare_and_swap_complete, and compare_and_swap_burn with verified transition inputs. Exclusive ownership comes from the anchor keeping that compare-and-swap contract; a hash-shaped slot id alone carries no ownership.
Live verification retrieves the completed artifact and requires its proof blob to equal the exact canonical authorization bytes, with matching signature and blob digests. availabilityReceipt is checked as an identifier, separate from any proof of a storage service’s availability.
Signing sessions
Each signer prepares a fresh nonce and commitment. The coordinator validates the submitted commitments, selects the first threshold participants in its ordered map, constructs the signing package over the authorization body’s signing bytes, verifies shares against the roster, and aggregates exactly the selected set.
The signer checks the package’s message digest and its own prepared commitment before creating a share, and a nonce never signs a different message. The SQLite signer store persists the prepared share and package digest before exposing it, so an identical-package retry returns the stored share. A changed package is refused, and the live-session conflict path burns the session.
The trust control plane’s coordinator router runs signing sessions through claim, renew, commitments, package, shares, complete, and cancel. Key generation runs through the ceremony helpers, and the Rust ceremony, signer, coordinator, store, and anchor interfaces are where a deployment wires them together.
Epoch rotation
Rotation connects a prepared, signed target roster to the active checkpoint. The target has the next key epoch, the predecessor roster digest, and a different group key, and a registered governance.roster_rotate authorization at 3 of 5 in treaty scope binds the typed rotation action.
verify_frost_epoch_advance checks the predecessor checkpoint, target roster, current rotation authorization, and old-session burn summary. The action names the current checkpoint sequence and digest, both roster digests, and the burn root; its activation fence advances, and the anchor clock moves forward only, inside the target roster’s validity window.
The verified result is the input to epoch advancement; the remote anchor’s commit is a separate step. The incoming roster is prepared before that activation check, and old proofs remain historical evidence after they stop authorizing execution.
Expected authorization inputs
| Input | Derive from |
|---|---|
domain | The typed action being performed. |
ladder_ | The action’s explicit registry classification. |
ladder_ | The canonical registered ladder entry. |
scope_id | The intended treaty scope. |
resource_id | The operation, round, channel, credential, case, passport, or rotation scope named by the action. |
resource_ | The current version required by this action. |
resource_fence | The current fence required by this action. |
action_digest | The validated, canonical typed action preimage. |
Verification modes
| Entry point | Boundary |
|---|---|
verify_ | Expected action/state, current authority and time, completed slot and exact proof blob, plus group signature. |
verify_ | Resolves the named roster at issuance and checks its signature, binding, and group signature. Does not require active execution authority or a completed slot. |
verify_ | Adds the exact bound- |
Historical evidence checks a roster that may still be active; it makes a different claim from permission to act now. The live completion checker binds the proof body and expected predecessor digest, and the historical completed checker additionally compares the full bound and completed successor fields.
Partitions
A partition can leave one component with enough participants to form a quorum. Signing stops when the selected quorum cannot complete, and execution verification refuses unavailable or inconsistent authority state. Global ordering, operator independence, and exactly-once external effects come from the anchor, the deployment, and the effect path; the threshold signature authenticates the authorization.