Revocation Epochs
Withdraw a subject or a task from delegated work with a signed epoch that every continuation commits to.
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.
κ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 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.
{"revokedSubjects":[],"revokedTaskIds":["task-child-a"]}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.
- Shape and reference. The schema tag, non-empty fields, and the graph’s
revocationEpochRefequal toepochId. - Window. The evaluation time falls in the half-open window: issued at or before now, and now strictly before
validUntilUnixMs. - Root.
rootHashequals the digest recomputed from the two sorted lists. - Signature. The issuer parses to a pinned key, and the signature verifies over the canonical epoch.
- 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
cargo run --locked --bin revocationImplementation
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(())
}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,
}))
}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(())
}Captured result and refusal
before revocation: verified
revoked child: swarm task is revoked: task-child-aThe 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.
$ chio proof verify ./swarm/negatives/revoked-task --trust ./swarm/trust.tomlREFUSED revoked-task swarm task is revoked: task-child-a trust ./swarm/trust.toml (fixture keys)
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.
- Build the new subject and task lists and compute their canonical root.
- Choose the epoch id and window, then sign the epoch with a pinned key.
- Update each continuation’s epoch reference and root and sign it again. A new epoch id also means rebuilding the graph’s reference.
- 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
Try validUntilUnixMs, root mismatch, or high-water mark.
schema
Must equal chio.swarm.revocation-epoch.v1.
epochId
A non-empty opaque identifier matching the graph’s revocationEpochRef. It carries no ordering.
rootHash
A lowercase SHA-256 digest, recomputed from the two sorted lists.
Example144bab856830b54b…49ac9fb5b2
issuedAtUnixMs, validUntilUnixMs
A non-empty half-open window that contains the evaluation time: issued ≤ now < validUntil.
revokedSubjects
Non-empty strings compared exactly with the graph issuer, the planner subject, and every witness-hop issuer.
revokedTaskIds
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
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
| Artifact | What it binds here |
|---|---|
Task graph | revocation |
Every continuation token | revocation |
Terminal graph receipt | revocation |
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 fragment | Inspect |
|---|---|
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 | valid |
swarm revocation epoch is stale | now must be strictly before valid |
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.
epochIdhas 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
- Epoch validation, list-root computation, and continuation bindings
- Canonical signature-body helpers and the epoch wire schema
- The revoked-task epoch, at
./swarm/negatives/revoked-task/revocation-epoch.jsonafterchio proof fixture generate recursive-runtime-swarm --with-negatives --out ./swarm, and verification tests - Key encodings: Delegation Witnesses. The node-local capability set: Revocation Store.