Chio/Docs
LOGIN · JOIN

Revocation Epochs

Withdraw a subject or a task from delegated work with a signed epoch that every continuation commits to.

Concept chio@5f63e6e

On this page

An issuer withdraws a signer or a task from delegated work by signing a revocation epoch: two lists, a validity window, and a digest of the lists. Every continuation commits to the epoch it was minted under, so the evidence of what was revoked travels with the bundle to any operator that verifies it.

The verifier checks the supplied epoch’s signature, window, list root, and membership rules each time it verifies a bundle. The operator distributes epochs and chooses which view is current. The examples use the recursive-delegation fixture and its revoked-task variant, which lists task-child-a.

A continuation signs the id and list root of the epoch it was minted under

The task graph names the epoch, and each continuation carries the epoch’s id and list root inside its own signature body.

Revocation epochsChoose a case.
tE1revocation-epoch-swarm-validE2replacementissuedvalidUntilno member listedR = 144bab…nowepoch refepoch reflist rootbundle verifiedκcKtask graphcontinuation-child-akerneltE1E2issuedvalidUntilrevocation-epoch-swarm-validno member listedR = 144bab…nowverifiedκcKgraphcontinuationkernel

κtask graphccontinuation-child-aEepochRlist root

The continuation binds the epoch id and its list root

The task graph names revocation-epoch-swarm-valid, and continuation-child-a carries that id and the epoch’s list root inside its signature body. The verifier checks the epoch’s signature, that issued ≤ now < validUntil, that the root recomputes from the two sorted lists, and that neither list names the graph’s issuer, planner, a witness issuer, or a task. Epoch commitments.

The fixture graph’s epoch reference and a continuation’s bindings on an illustrative time axis; the revoked list is the revoked-task fixture, and the replacement epoch is illustrative. Terminal receipts, which bind the epoch id only, and the runtime’s whole-artifact digest are left out.

The terminal graph receipt names the epoch id and carries no root. At runtime, the request’s epoch evidence pair binds the id and canonical whole-artifact digest to the stored epoch before the bundle is verified. The verifier recomputes the root before any continuation check, so each token is compared with a root already checked against the lists.

The list root covers the lists, and the epoch’s signature covers its whole body. A continuation’s id and root fields leave the rest of the epoch uncommitted: a newly signed epoch with the same id and lists and a different window keeps the same root. Its window and signature face their own checks, and a runtime evidence reference to the old whole-artifact digest is rebuilt for it.

A listed subject or task refuses the whole bundle

An epoch carries revokedSubjects and revokedTaskIds. The subject list is matched against the graph’s issuer, its planner subject, and every witness-hop issuer; the task list is matched against every node in the graph.

One match refuses the bundle whole. The verifier returns no accepted subset with the revoked branch cut away, and work already dispatched keeps running. Capability ids in node and cluster stores are withdrawn through Revocation Propagation, and federated revocation through the Revocation Oracle, which answers with membership or non-membership evidence against an accumulator root.

The list root is the digest of two sorted lists

Sort each list, serialize this object as canonical JSON, and take its SHA-256. The epoch id, timestamps, issuer, schema, and signature stay out of it.

list-root material · revoked-task fixturejson
{"revokedSubjects":[],"revokedTaskIds":["task-child-a"]}
SHA-256
1d8a51dddeecdf187968776b181b57d45367737df9de95433ed4dfaeff6093e3

Two empty lists produce 144bab856830b54b1d138a96909cfac3f8c92cd33090cf6918de3949ac9fb5b2, the root of the valid fixture. Reordering a list leaves the root unchanged; a duplicate entry changes it, because sorting keeps duplicates. The root is a digest of the full lists, so verification needs the lists themselves and offers no Merkle inclusion proof.

The epoch stage stops the verifier at its first failed check

The epoch stage runs after the graph, route, join, and budget checks and before terminal receipts, continuations, and witnesses. Inside it, five checks run in order.

  1. Shape and reference. The schema tag, non-empty fields, and the graph’s revocationEpochRef equal to epochId.
  2. Window. The evaluation time falls in the half-open window: issued at or before now, and now strictly before validUntilUnixMs.
  3. Root. rootHash equals the digest recomputed from the two sorted lists.
  4. Signature. The issuer parses to a pinned key, and the signature verifies over the canonical epoch.
  5. Membership. No graph signer is a revoked subject, and no graph node is a revoked task.

At validUntilUnixMs the epoch is stale, and continuing takes a replacement epoch. The verifier keeps no record of newer epochs, so a superseded epoch passes while its window is open: the operator chooses the current view and bounds how long a window may run.

The prepared example revokes a child and re-signs its continuations

The downloadable Rust project verifies the original bundle, adds task-child-a to the revoked set, recomputes the sorted root, signs the epoch, and rebinds the continuations to the new root.

Worked Rust example: Publish a revoked task in a signed epoch

Publish a revoked task in a signed epoch

Verify the original bundle, add task-child-a to the revoked set, recompute the sorted revocation root and sign the epoch. Rebind the continuations to the new root before checking again.

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

bash
cargo run --locked --bin revocation

Implementation

revocation.rsrust
use chio_swarm_authority::verify_swarm_authority_bundle;
use swarm_authority_examples::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut bundle = build_bundle()?;
    verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
    println!("before revocation: verified");
    bundle
        .revocation_epoch
        .revoked_task_ids
        .push("task-child-a".into());
    // Hash sorted subject/task lists, sign the epoch, then rebind continuations.
    // This isolates revocation from a stale-digest or bad-signature failure.
    refresh_revocation_epoch_root(&mut bundle)?;
    println!(
        "revoked child: {}",
        verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())
            .expect_err("revoked child cannot verify")
    );
    Ok(())
}

Open src/bin/revocation.rs

lib.rs · revocation_epoch_root_hashrust
pub fn revocation_epoch_root_hash(
    revoked_subjects: &[String],
    revoked_task_ids: &[String],
) -> Result<String, Box<dyn Error>> {
    let mut subjects = revoked_subjects
        .iter()
        .map(String::as_str)
        .collect::<Vec<_>>();
    subjects.sort_unstable();
    let mut task_ids = revoked_task_ids
        .iter()
        .map(String::as_str)
        .collect::<Vec<_>>();
    task_ids.sort_unstable();
    canonical_hash(&serde_json::json!({
        "revokedSubjects": subjects,
        "revokedTaskIds": task_ids,
    }))
}

Open src/lib.rs

lib.rs · refresh_revocation_epoch_rootrust
pub fn refresh_revocation_epoch_root(
    bundle: &mut SwarmAuthorityBundle,
) -> Result<(), Box<dyn Error>> {
    let root_hash = revocation_epoch_root_hash(
        &bundle.revocation_epoch.revoked_subjects,
        &bundle.revocation_epoch.revoked_task_ids,
    )?;
    bundle.revocation_epoch.root_hash = root_hash.clone();
    sign_revocation_epoch(&mut bundle.revocation_epoch)?;
    for token in &mut bundle.continuation_tokens {
        token.revocation_epoch_root_hash = root_hash.clone();
        sign_continuation_token(token)?;
    }
    Ok(())
}

Open src/lib.rs

Captured result and refusal

Captured executable outputtext
before revocation: verified
revoked child: swarm task is revoked: task-child-a

The refused child is rejected under the supplied signed epoch. A deployed verifier must obtain its current trusted revocation view; the constructor does not distribute epochs.

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.

You should see the original bundle verify and the rebound bundle refused with swarm task is revoked: task-child-a. Re-signing the continuations clears the root comparison; the membership check still refuses the revoked child.

A revoked child takes its siblings’ authority with it

The negative fixture lists task-child-a in revokedTaskIds while the graph still contains that node, and both continuation tokens carry the epoch’s new root. Verification reaches the membership check and refuses the bundle.

Revoked task in the supplied graphtranscript
$ chio proof verify ./swarm/negatives/revoked-task --trust ./swarm/trust.toml
REFUSED   revoked-task  swarm task is revoked: task-child-a
trust     ./swarm/trust.toml (fixture keys)
exit 1

The task id in the message names the membership that refused the bundle; the siblings are refused with it. When siblings must continue independently after one branch is revoked, give them separate graphs.

A replacement view re-signs every continuation that commits to it

Changing who is revoked changes the root, and every artifact that carries the root or the id moves with it.

  1. Build the new subject and task lists and compute their canonical root.
  2. Choose the epoch id and window, then sign the epoch with a pinned key.
  3. Update each continuation’s epoch reference and root and sign it again. A new epoch id also means rebuilding the graph’s reference.
  4. Rebuild the whole-artifact evidence references and verify the complete replacement bundle before admission.

Old tokens presented with a changed root fail their root comparison. A replacement that revokes a task still in the graph refuses first at membership, even after every token is re-signed. The admission stores hold one bundle per task-graph id and refuse a different bundle under an existing id with duplicate_swarm_authority_bundle_mismatch, so a replacement view arrives as a new bundle.

Read more in Task Graphs, which defines the graph boundary a revocation reaches.

Epoch fields, diagnostics, and boundaries

schema

StringRequired

Must equal chio.swarm.revocation-epoch.v1.

epochId

StringRequired

A non-empty opaque identifier matching the graph’s revocationEpochRef. It carries no ordering.

rootHash

StringRequired

A lowercase SHA-256 digest, recomputed from the two sorted lists.

Example144bab856830b54b…49ac9fb5b2

issuedAtUnixMs, validUntilUnixMs

u64Required

A non-empty half-open window that contains the evaluation time: issued ≤ now < validUntil.

revokedSubjects

String[]Required

Non-empty strings compared exactly with the graph issuer, the planner subject, and every witness-hop issuer.

revokedTaskIds

String[]Required

Matched against this graph’s node ids. An id that names no node in the graph has no effect on it.

Example["task-child-a"]

issuer, signature

PublicKeyRequired

A pinned public key from the same trusted set as the other swarm artifacts, and a valid signature over the canonical epoch with the signature field removed. Accepted key encodings are covered on Delegation Witnesses.

Schema and verifier

Both refuse unknown epoch fields. The JSON Schema also requires non-empty entries in both lists and a 128-character lowercase hexadecimal signature. The Rust verifier refuses a blank revokedSubjects entry, accepts an empty revokedTaskIds entry by shape, and parses every accepted key algorithm. Neither layer requires unique entries, and duplicates stay in the root computation even though a repeated subject leaves membership unchanged.

Epoch commitments

ArtifactWhat it binds here
Task graph
revocationEpochRef equals epochId.
Every continuation token
revocationEpochRef equals epochId; revocationEpochRootHash equals the already verified rootHash. Both fields are in the token’s signature body.
Terminal graph receipt
revocationEpochRef equals epochId. It has no epoch-root field.
Runtime admission reference
The epoch evidence pair binds its id and canonical whole-artifact digest to the stored epoch before bundle verification.

The full bundle requires non-empty continuation and witness lists.

Epoch diagnostics

Diagnostic fragmentInspect
unsupported swarm revocation epoch schema
The schema tag.
swarm revocation epoch ref mismatch
The graph’s epoch reference against epochId.
swarm revocation epoch is from the future
issuedAtUnixMs must be at or before now.
swarm revocation epoch window is empty
validUntilUnixMs must be strictly after issuance.
swarm revocation epoch is stale
now must be strictly before validUntilUnixMs.
swarm revocation epoch root mismatch
The exact two sorted arrays and canonical digest.
swarm revocation epoch issuer is not pinned
The parsed issuer key against the supplied pins.
swarm revocation epoch signature invalid
Signature parsing and the canonical epoch body.
swarm authority subject is revoked
The graph issuer, planner subject, or witness-hop issuer named in the message.
swarm task is revoked
The named task’s presence in this graph.
swarm continuation revocation epoch root mismatch
A continuation still carrying a different list root.

Empty epoch ids, issuers, signatures, or revoked-subject strings, and malformed root digests, also refuse. These fragments identify the failed relationship; they are message text, separate from the stable CLI registry codes.

Operating boundaries

  • Open window, no newest-epoch check. A superseded signed epoch passes its time check while a newer one exists. epochId has no sequence, and the verifier keeps no high-water mark.
  • Issuer-chosen duration. The issuer sets the window with no maximum; an operator that needs a tighter freshness bound imposes it at admission.
  • Exact subject strings. Two accepted encodings of one key are different strings, so a revocation uses the issuer’s exact representation.
  • Three subject roles. The revoked-subject check reads the graph issuer, the planner subject, and witness-hop issuers. Continuation, route-plan, join, terminal-receipt, and epoch issuers are checked against the trust set.
  • Whole-bundle decision. A matching task refuses the supplied graph. Stopping running work and publishing a new epoch are separate operations.
  • Operator-owned admission state. The operator chooses the trusted keys, the stored bundles, and the evaluation time the runtime admission hook uses.

Clocks and fixture times

The CLI and the Proof Room use the system clock. The runtime admission hook overwrites the stored bundle’s time with the request’s evaluation time before verification. The positive fixture’s epoch expires at 2027-01-15T08:01:01Z; its window of about 463 days is fixture material.

Signing helpers

The library exports epoch and continuation signing helpers. Its list-root helper is private, so an external issuer implements the canonical digest recipe above. The memory, JSON, and SQLite admission stores insert whole bundles, and a replacement view is inserted as a new bundle.

Evidence behind the examples

The revoked-task transcript records a fixture verification command. Source tests cover a revoked task, a revoked authority subject, a future epoch, a root that disagrees with its lists, and a recomputed root under an invalid signature. Both fixture roots on this page were recomputed from the stored lists.

Wire contract and source

Next: carry the epoch into each child’s admission