PlatformThe Delegation Graph
Swarm
Swarm Authority
One pure function verifies a delegation bundle whole. What it checks, in what order, and what it refuses to answer.
The rung framing lives on the Overview
Source
This page reflects spec/schemas/chio-swarm/v1/authority-verifier-report.schema.json and spec/PROTOCOL.md section 6.4.2 in the chio repository. The Swarm Protocol reference carries the full set of artifacts and the verification order.
One entry point, no state
chio-swarm-authority exports one verification function. It holds no state, performs no I/O, and reads no clock: every fact it checks comes from the bundle it was handed or from the key slice the caller passed, and the current time arrives as the bundle field now_unix_ms.
pub fn verify_swarm_authority_bundle(
bundle: &SwarmAuthorityBundle,
trusted_witness_issuer_keys: &[PublicKey],
) -> Result<SwarmAuthorityVerifierReport, SwarmAuthorityError> {Verification is all or nothing. Either the whole bundle verifies and the function returns a report whose verdict is the literal string verified, or it returns SwarmAuthorityError and there is no report at all. There is no partial verdict, no per-artifact error list, and no warning state. The rest of the crate’s public API is constructors: mint_*, sign_*, and the budget fan-out and fan-in helpers. Their only callers in the workspace are tests, in chio-swarm-authority, chio-runtime-core, and the CLI proof contract suite. This crate specifies verification; issuance stays with whoever operates the planner.
The bundle
SwarmAuthorityBundle is nine fields. Every type in the crate that crosses the wire carries deny_unknown_fields, down to the nested AttenuationWitness it borrows from chio-core-types, so a bundle with an extra key fails deserialization before the verifier sees it. The *MintRequest structs are plain Rust and are never deserialized.
| Field | Carries | Required non-empty |
|---|---|---|
task_graph | The signed plan: one root, tasks with depth and scope hash, delegation edges, joins, and the max_depth / max_fanout ceilings. Task Graphs | Yes, at least one task |
continuation_tokens | One per non-root task. The artifact a child actually runs against. Continuation Tokens | Yes |
witness_chains | One per graph edge, each a sequence of hops carrying a scope-subset proof. Delegation Witnesses | Yes |
join_receipts | One per declared join: parent set hash, DAG ordinal, join predicate | Exactly the graph’s joins |
route_plan_receipts | Selected route, candidate-set digest, registry snapshot hash, bridge, protocol target, egress contract and constraints | Exactly the graph’s route_plan_refs |
budget_pool | Pool total plus per-task allocations across five states. Sub-Agent Budgets | Yes, and its id must equal budget_pool_ref |
revocation_epoch | Revoked subjects and revoked task ids inside a validity window | Yes, and its id must equal revocation_epoch_ref |
terminal_receipts | Graph closure: completed tasks, joins, routes, per-dimension budget rollups. One or more; each must close the whole graph | Yes. serde(default) lets the field be absent from JSON, and both an absent field and an empty list are rejected with missing swarm terminal graph receipt |
now_unix_ms | The evaluation instant every freshness check runs against | Supplied by the caller |
Issuers are strings. An issuer prefixed did:chio: must be self-certifying: exactly 64 lowercase hex characters after the prefix, parsed as the Ed25519 public key that signed the artifact. A bare hex key is also accepted. Either way the key that comes out must appear in trusted_witness_issuer_keys, which is what makes the DID a pin rather than a name.
Closed vocabularies and one gate
Several fields accept a fixed set of values rather than free strings. These are the limits most likely to surprise someone minting a bundle from the schemas alone.
- A route-plan receipt’s
egress_constraintsmust be non-empty, and every entry must be the literaldeny-private-network. Any other value isunsupported swarm route-plan egress constraint. Itsattenuation_decisionmust be the literalaccepted. - A route-plan receipt’s
selected_routeandegress_contract_idmust each start with its ownbridge_idfollowed by:, andprotocol_targetmust start with the same id followed by://. join_predicatemust beall_success,any_success, orquorum:NwithNbetween one and the expected parent count. A graph join needs at least two parents.- A witness chain of more than one hop is rejected unless the signed graph sets
multiHopWitnessChains. Multi-hop delegation is a per-graph gate carried inside the signed artifact, not a verifier default. - The allocation a continuation names must be in state
active, and the pool’s summedmax_unitsmay not exceed itstotal_units.
The order of checks
The entry point is a straight sequence of fallible calls. Order is load bearing, so it is worth reading literally. This is the whole body up to the point where the claim list is assembled:
require_trusted_witness_issuer_keys(trusted_witness_issuer_keys)?;
validate_task_graph(&bundle.task_graph, bundle.now_unix_ms)?;
verify_task_graph_signature(&bundle.task_graph, trusted_witness_issuer_keys)?;
require_signed_swarm_delegation_evidence(bundle)?;
let graph_sha256 = canonical_sha256(&bundle.task_graph)?;
let task_by_id = task_index(&bundle.task_graph)?;
let edge_set = edge_set(&bundle.task_graph.edges);
let route_by_id =
validate_route_plan_receipts(bundle, &task_by_id, trusted_witness_issuer_keys)?;
let join_by_id = validate_join_receipts(bundle, &task_by_id, trusted_witness_issuer_keys)?;
let allocation_by_id = validate_budget_pool(bundle, &task_by_id)?;
validate_revocation_epoch(bundle, &task_by_id, trusted_witness_issuer_keys)?;
validate_terminal_graph_receipts(
bundle,
&task_by_id,
&route_by_id,
&join_by_id,
&allocation_by_id,
trusted_witness_issuer_keys,
)?;
validate_continuation_tokens(&ContinuationValidationContext {
bundle,
graph_sha256: &graph_sha256,
task_by_id: &task_by_id,
edge_set: &edge_set,
route_by_id: &route_by_id,
join_by_id: &join_by_id,
allocation_by_id: &allocation_by_id,
trusted_witness_issuer_keys,
})?;
validate_witness_chains(bundle, &task_by_id, &edge_set, trusted_witness_issuer_keys)?;Trust comes first, so an unconfigured verifier stops before reading a single artifact. Graph structure and the graph signature come next, because every later check indexes into a graph that has already been proved acyclic, single-rooted, depth-consistent, and signed by a pinned issuer. Routes, joins, allocations, and the epoch are validated and indexed before continuations, because a continuation token names all four and its checks are lookups into those indexes. Witness chains run last, over the edge set, as a cover check.
Totality, not spot checks
Routes, joins, witness chains, and terminal receipts are checked in both directions: no declared reference may be unbacked, and no supplied artifact may be undeclared. That symmetry is what makes the bundle atomic. Continuation tokens are the one asymmetric family, noted below.
- Every
route_plan_refsentry must have a receipt, and every receipt must match a ref. Duplicates are rejected on sight. - Every graph join must have a receipt whose
next_task_idequals the join’s, and whose expected-parent count equals the join’s parent count. - Every edge must be covered by exactly one witness chain. A second chain over the same edge is a duplicate; an uncovered edge is
missing swarm delegation witness chain. - Every task with a parent must name a continuation token, and that token’s
child_task_idmust be the naming task. Token ids and nonces must be unique within the bundle. Each token binds either aparent_task_idor ajoin_receipt_id, never both and never neither. - The reverse does not hold for continuations.
validate_continuation_tokenswalks tasks to find missing tokens, but never walks tokens to find unreferenced ones. A second, fully valid token for a child that already has one is accepted; it only inflatescontinuation_countin the report. Deciding which token a child may present is the caller’s job, and the runtime does it by naming one token id in the request. - The terminal receipt’s completed-task set must equal the graph’s entire task set, its join set every join, its route set every route. A terminal receipt cannot close part of a graph.
Digests are recomputed, never trusted
Five digest fields are recomputed from the bundle’s own contents and compared, rather than accepted as declared.
| Field | Recomputed from |
|---|---|
continuation.graph_sha256 | Canonical JSON of the whole task graph, signature field included |
continuation.witness_chain_sha256 | Canonical JSON of the referenced chain, whose parent and child task ids must also match the token |
revocation_epoch.root_hash | Canonical JSON of the sorted revoked-subject and revoked-task lists |
join.parent_set_hash | Canonical JSON of the chain id plus the sorted actual parent receipt ids |
terminal.budget_rollups | Per-dimension sums over every allocation in the pool, compared as whole maps. Each rollup’s five state counts must also sum to its own declared total_units, and the sum across dimensions may not exceed the pool total |
The first row closes an obvious attack. A tampered graph invalidates both the graph signature and every token digest, and the two are computed over different bytes: the signature body drops the signature field, the token digest keeps it. Repairing one does not repair the other. swarm_authority_stage0_rejects_task_graph_tampering_after_continuations_are_resealed widens max_fanout, reseals every continuation against the new digest, and still lands on swarm task graph signature invalid.
Two refusals worth stating exactly
Claim: an unconfigured verifier verifies nothing
| Field | Value |
|---|---|
| Status | Shipped, proved by test. |
| Claim | An empty trusted-key slice rejects the bundle on the first line, before any artifact is parsed or any structural check runs. Most artifact families then repeat the same emptiness check ahead of their own pin comparison; the revocation epoch skips the repeat and fails the comparison instead, because no key can match an empty set. |
| Subject | One call to verify_swarm_authority_bundle, any bundle. |
| Evidence | require_trusted_witness_issuer_keys; swarm_authority_stage0_rejects_unpinned_witness_issuer and ..._rejects_root_only_bundle_without_trusted_witness_keys. The CLI reads the slice from CHIO_SWARM_TRUSTED_WITNESS_KEYS as comma-separated hex and errors when the variable is absent. |
| Limit | Trust is entirely caller-supplied. The crate does not resolve, rotate, or expire keys, and it has no opinion on which issuer signs which artifact class: one slice pins the graph, the routes, the joins, the epoch, the terminal receipts, the continuations, and every witness hop alike. |
Claim: a root-only bundle is denied, not partially verified
A graph with one task and no edges is structurally valid. It is still rejected, and the reason is the shape of the report rather than the shape of the graph:
fn require_signed_swarm_delegation_evidence(
bundle: &SwarmAuthorityBundle,
) -> Result<(), SwarmAuthorityError> {
if bundle.continuation_tokens.is_empty() || bundle.witness_chains.is_empty() {
return Err(rejected(
"signed swarm delegation evidence missing: continuation tokens and witness chains are required",
));
}
Ok(())
}The verified-claim list is assembled conditionally. Four of the eight claims are pushed only when their collection is non-empty, so a bundle with no continuations and no witness chains would otherwise return a report reading verified while silently carrying fewer claims than a reader expects. Denying the root-only case is what keeps verified from meaning two different things. swarm_authority_stage0_rejects_root_only_bundle_without_signed_swarm_evidence pins it. Downstream, both proof paths compare the report’s verified_claims against the required_claims in the verifier policy shipped with the bundle and fail when a required claim is missing, so a claim that quietly stopped being emitted fails there too. That list is per policy, not per crate: the recursive-delegation fixture requires seven of the eight swarm claims and omits claim.swarm.terminal_graph_receipt_bound. spec/registries/proof-manifest.v1.json registers all eight against test, fixture, and schema references; it is a registry checked for liveness, not the enforcement path.
Per-hop flags are a rendering, not a per-hop verdict
SwarmAuthorityHopReport carries authority_verified, attenuation_verified, lineage_verified, route_verified, and budget_verified. All five are written as the literal true while building the report, which is only reachable after the entire bundle verified. They are a per-hop presentation of one bundle-wide verdict. Do not read a hop report as an independently checked hop, and do not expect a mixed report: on any failure there is no report to read.Three consumers, one definition
The runtime path is pre-dispatch and trusted-input. It engages only for a request whose governed intent carries a chioSwarm context naming seven evidence references. chio-runtime-core loads the bundle from its own admission store by task-graph evidence id, overwrites now_unix_ms with the request clock, checks that each referenced evidence id and its canonical SHA-256 match the stored artifact, compares live route metadata against the route-plan receipt, calls the verifier, and only then consumes the continuation id. A verifier rejection becomes the denial code chio_swarm_authority_rejected, carrying the verifier’s own message as detail; an evidence id or hash that does not match the stored artifact becomes chio_swarm_authority_ref_mismatch instead. Consumption is conditional on the token’s mode: only single_use tokens are consumed, and a resumable token is presented again without replay accounting.
The runtime arm is an embedder API, not a default deployment
ChioRuntimeAdmissionHook starts with an empty witness key vector and fills it only through chio-runtime’s with_swarm_witness_keys builder. No product binary in the repo calls that builder, and nothing outside tests calls insert_swarm_authority_bundle to populate the admission store. In a default deployment a swarm-bound request is therefore denied, for a missing bundle or an empty key slice, rather than admitted. Verification is shipped and exercised by chio-runtime-core’s admission tests; wiring it into a running kernel is work an embedder does, not a flag an operator flips.The proof paths are after the fact and untrusted-input. chio proof verify runs the swarm arm of its local family dispatch, and chio-proof-room calls the same function on an embedded bundle reconstructed from an evidence graph. Both resolve their key slice from CHIO_SWARM_TRUSTED_WITNESS_KEYS and neither adds a check the runtime has and the crate lacks: the reference hashing, the route-metadata comparison, and continuation consumption are all runtime-side. A fourth caller, chio-conformance’s r_t03_recursive_swarm_conformance, is a test. What the call sites share is that the definition of a valid delegation is one function, not three.
Run the verifier
The shipped fixture bundle exercises the whole order of checks: a four-task graph, three witness chains, three route plans, one join, one pool, one epoch. chio proof verify writes its report to stdout as one JSON object, so jq is the readable way to take the verdict and the claims out of it.
$ source scripts/proof-room-quickstart-env.sh
$ chio proof verify \
fixtures/proof-room/swarm-authority/valid-recursive-delegation/transaction-passport.json \
| jq '{verdict, passport_id, verified_claims}'{
"verdict": "verified",
"passport_id": "passport-swarm-valid",
"verified_claims": [
"claim.swarm.task_graph_bound",
"claim.swarm.continuation_fresh",
"claim.swarm.attenuation_witness_chain_bound",
"claim.swarm.route_plan_bound",
"claim.swarm.join_receipt_bound",
"claim.swarm.budget_pool_bound",
"claim.swarm.revocation_epoch_bound",
"claim.swarm.terminal_graph_receipt_bound"
]
}Eight claims, one per artifact family the bundle carries. The full report also carries a per-hop hopReports array under family_reports, with attenuationVerified, routeVerified, budgetVerified, and lineageVerified per child task, plus the SHA-256 of the claim set, the evidence graph, and the verifier policy.
Point the same command at a sibling directory and the verifier refuses. The bundle at fixtures/proof-room/swarm-authority/revoked-task/ is signed and internally consistent; its epoch carries "revokedTaskIds":["task-child-a"] where the valid epoch carries an empty list. Its paired descriptor at negatives/revoked-task.json names the claim it targets, claim.swarm.revocation_epoch_bound.
$ source scripts/proof-room-quickstart-env.sh
$ chio proof verify \
fixtures/proof-room/swarm-authority/revoked-task/transaction-passport.jsonerror [urn:chio:error:cli:other]: proof verify: swarm task is revoked: task-child-a
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.No report is written. The verifier returns on the first failing check, so the seven claims that would have verified are not reported either, and the refusal names the task rather than the epoch. This is the shape every refusal on this rung takes; the Denial Codes page prints all 11 from one loop.
Guarantees and limits
| Status | Claim | Evidence |
|---|---|---|
| Shipped | Verification is a pure function of the bundle and the key slice. No filesystem, no network, no clock, no interior mutability. | crates/kernel/chio-swarm-authority/src/verifier.rs |
| Proved by test | A widened child scope inside a signed witness hop is rejected, for generated parent limits and overshoots. | swarm_authority_stage0_rejects_generated_recursive_scope_widening, a 32-case proptest asserting swarm attenuation witness invalid |
| Proved by test | The stage-0 suite covers the rejection paths one at a time: tampered or resealed signatures on the graph, continuations, join receipts, route plans, witness hops, and the epoch; stale and future timestamps; nonce replay; ungated multi-hop chains; released and oversubscribed allocations; revoked tasks and revoked subjects. | crates/kernel/chio-swarm-authority/tests/swarm_authority_stage0.rs |
| Proved by fixture replay | 11 negative fixture bundles are replayed on every run of chio proof doctor --scenario proof-package: each directory must fail verification, and the failure must match the expected_failure_code read out of its paired descriptor. A match is the raw error text containing that code, or the slug derived from the error carrying it at a : or whitespace boundary, so a reworded rejection reads as a mismatch rather than a silent pass. | fixtures/proof-room/catalog.json; package_negative_expected_failure and check_transaction_passport_rejects in chio-cli/src/cli/dispatch/proof/doctor.rs; proof_doctor_accepts_proof_package_evidence |
| Declared, not signed | The descriptors in negatives/ are unsigned JSON: schema chio.swarm-authority.negative-fixture.v1, a claim_ref, the base_fixture the case mutates, a case name, and the expected code. The signed material lives in the fixture directory beside them. Nine of the 11 are also registered as claim evidence in the proof manifest; egress-constraint-unsupported and max-depth-exceeded are not. | fixtures/proof-room/swarm-authority/negatives/*.json; spec/registries/proof-manifest.v1.json |
| Shipped, not wired | The runtime admission arm is a library API. The facade hook’s trusted key slice starts as an empty vector and is filled only by its own with_swarm_witness_keys builder, which no product binary calls, and only tests insert bundles into the admission store. The forwarding call inside core_hook() is production code, and it forwards that empty vector. The proof paths are the ones an operator can run today, through chio proof verify and the proof room. | chio-runtime/src/lib.rs:151,188,226; chio-runtime-core/src/admission_hook.rs:203,240; call sites of insert_swarm_authority_bundle |
| Not claimed | Stable machine-readable error codes from the crate. SwarmAuthorityError has two variants, Rejected(String) and Canonical(String). Callers that need a code map the whole error: the runtime to one denial code, the proof room to a slug. Match on the message only where a fixture already pins it. | crates/kernel/chio-swarm-authority/src/error.rs |
| Unsupported | Cross-bundle replay detection. Continuation nonce and token-id uniqueness are checked within one bundle. A second presentation of the same token in a second bundle is the admission store’s problem, and the store only tracks tokens the runtime asked it to consume, which excludes resumable ones. | validate_continuation_tokens; consume_swarm_continuation and the runtime denial code chio_swarm_continuation_replay |
| Unsupported | Issuance in production. Every constructor in the crate is exercised only by tests, and nothing in the crate mints authority at request time. | Call sites of mint_*, sign_*, reserve_swarm_budget_fanout, release_swarm_budget_fanin |
Next Steps
- Task Graphs · the first artifact in the bundle: one root, depth arithmetic, and the ceilings
- Continuation Tokens · the artifact a child runs against, and the id the runtime burns before dispatch
- Delegation Witnesses · the per-hop attenuation proof the verifier recomputes for every edge
- Sub-Agent Budgets · the pool split at fan-out, reclaimed at fan-in, and reconciled by the terminal receipt
- Swarm Overview · the rung, the nine schemas, and the admission sequence this verifier sits inside
- Capabilities · the scope and attenuation model a witness hop proves a subset of
- Authority & Rotation · the rung below, where authority is one keypair you hold rather than a chain you verify
- Proof Room · where the recursive-delegation fixtures and the negative cases live
- 3-Vendor Walkthrough · one delegated scenario carried end to end