Chio/Docs
LOGIN · JOIN

Revocation Oracle

Carry a revocation from the operator who made it to a partner's door, as a signed root the receiver verifies before installing.

Concept chio@5f63e6e

On this page

When one operator revokes a grant or a passport, its partners refuse it at their own doors, without a restart. The revoking authority commits each revocation to an append-only history and signs the result, and each receiving operator checks that signature against a key it pinned before its kernel acts on the root.

The revocation oracle is the authority's half: a Merkle accumulator of revocations whose history survives a restart, and signed epoch roots that partners receive as a live feed. The receiving half checks each root on arrival, installs a newer snapshot, and consults it before every delegated dispatch.

Revocation rootsFollow one revocation.
Revoking operatorYour operatorσrevocation authorityRepoch rootVsnapshotKkerneldtoolaagentRevoking operatorYour operatorσrevocation authorityRepoch rootVsnapshotKkerneldtoolaagent

Each revocation adds one leaf and one signed root

The revocation authority inserts a revocation key, a subject and an epoch nonce, as one new leaf, and the epoch advances by one. It signs the new epoch root, which binds the epoch, the root hash, the leaf count, and the issuance time, with its Ed25519 key. Revocation.

One revocation from the revoking authority's accumulator to the receiving kernel's door. Four leaves stand in for the authority's history; catch-up by range, the partner status pull, and the receipt the refusal leaves are not drawn.

A revocation becomes a leaf under a signed root

The accumulator keys each revocation by subject and epoch nonce. A new key appends one leaf and advances the epoch by one; a repeat returns AlreadyRevoked and leaves the epoch where it was. No method removes a leaf, so a revocation is permanent for the life of the history. Revoke the grant you gave your partner, then revoke it again:

Terminalbash
chio trust revoke --capability-id cap-01a9…7e40
chio trust revoke --capability-id cap-01a9…7e40
Output
revoked          cap-01a9…7e40, epoch 12, root 5e0b…c71a
feed             signed root queued for partner on the revocation lane
already revoked  cap-01a9…7e40, epoch stays 12

After each insert the authority signs an epoch root: the epoch, the root hash, the leaf count, and the issuance time. The signature covers a domain-separated canonical encoding of those four values.

crates/trust/chio-revocation-oracle/src/signer.rsrust
pub const DOMAIN_SEPARATION_CONTEXT: &[u8] = b"chio-revocation-oracle:v1:epoch-root";

fn signing_message(root: &EpochRoot) -> Result<Vec<u8>> {
    let canonical = canonical_json_bytes(root)
        .map_err(|err| RevocationOracleError::Serialization(err.to_string()))?;
    let mut message = Vec::with_capacity(DOMAIN_SEPARATION_CONTEXT.len() + canonical.len());
    message.extend_from_slice(DOMAIN_SEPARATION_CONTEXT);
    message.extend_from_slice(&canonical);
    Ok(message)
}

Signing and verifying are separate types. EpochRootSigner holds the signing key; EpochRootVerifier holds only the pinned public key and the expected signer id, so a receiver verifies roots it could never mint. Verification refuses an unexpected signer id, an algorithm tag other than ed25519, a signature that is not 64 bytes, or a failed Ed25519 check, with SignatureVerificationFailed.

Partners receive roots as a live feed and catch up by range

The authority enqueues each signed root for its subscribed peers. Each peer's queue coalesces within a tick: a higher epoch evicts the lower ones still waiting, and catch-up recovers anything a peer missed. The revocation lane runs its checks on your side before a single root merges:

  1. Endpoint. The connection's authenticated endpoint resolves to an admitted kernel.
  2. Recipient. The batch names your kernel, so a misrouted or replayed batch changes nothing.
  3. Signer. Each root's signer id resolves to a pinned binding whose endpoint is the one that presented it.
  4. Signature. Each signed root verifies against that binding's verify-only key.

The sink then merges the whole batch or none of it. The lane is one of the optional iroh lanes beside signals and co-sign, carried through self-hosted relays and served by the process that hosts your revocation view. Your kernel also pulls each partner's issuer-signed lifecycle status from the status resolve URL on its partner card, keeps it for its TTL, and fails closed when it goes stale.

A peer that fell behind asks for a range of at most 4,096 epochs. The answer is a contiguous run of signed roots, each one epoch past the last, and the requester checks every signature against its pin before any of them counts.

Read more in Transport lanes.

The receiving kernel refuses from a snapshot it installs

Your host turns verified roots into a RevocationSnapshot: an epoch, a root hash, an issuance time, and the set of revoked capability IDs. The oracle keys revocations by subject, the kernel queries by capability ID, and the host owns the mapping between the two.

install_if_newer swaps the snapshot in atomically, and only for a strictly greater epoch, so a replayed root never rewinds the view. On each delegated call the kernel checks freshness first: a snapshot older than 500 ms, or stamped in the future, refuses with KernelError::DelegationInvalid. It then checks the leaf capability and every delegation link in the chain.

The signature covers the epoch commitment. The revoked set inside a snapshot is the host's construction, and install_if_newer takes it as supplied, so build it from verified roots and the complete subject list, as the example below does. With no snapshot installed, the kernel falls through to the node's revocation store.

Read more in Revocation store.

Install verified revocation state in a kernel

Verify a signed root and its complete subject set, install a monotone snapshot, and observe the kernel allow or refuse the same capability.

Complete the project setup, then run the program from federation-guide/. The download includes every constructor and input.

Terminalbash
cargo run --locked --bin revocation
Output on darwin/arm64 · exit 0
no trusted snapshot: DENY; handler calls 0
fresh snapshot without this capability: ALLOW; handler calls 1
incomplete subject list: refused before installation
revoked capability: DENY; handler calls 1
rollback and stale root: refused
three kernel receipt signatures verified

This bounded receiver rebuilds the full accumulator from the supplied key set. It uses capability IDs as its explicit subject namespace. Large or persistent deployments need their own durable authority history, authenticated delivery, and restart recovery.

Read the complete program 110 lines · complete file
src/bin/revocation.rsrust
use chio_core_types::Keypair;
use chio_kernel::Verdict;
use chio_kernel_core::{RevocationSnapshot, RevocationView, RevocationViewSubject};
use chio_revocation_oracle::*;
use federation_guide::*;
use std::{
    collections::BTreeSet,
    path::Path,
    sync::{atomic::Ordering, Arc},
};

// This bounded example carries the COMPLETE key set. Rebuilding and comparing
// its root prevents an unverified subject list from being installed in the kernel.
fn receive(
    root: &SignedEpochRoot,
    keys: &[RevocationKey],
    verifier: &impl EpochRootVerifier,
    view: &RevocationView,
    clock: u64,
) -> std::result::Result<(), Error> {
    verifier.verify_epoch_root(&root.root, &root.signature)?;
    verify_fresh_epoch_root(
        &root.root,
        clock,
        FreshnessConfig {
            max_staleness_ms: 5000,
            offline_grace_ms: 0,
        },
    )?;
    let mut replica = InMemoryRevocationOracle::new();
    for key in keys {
        replica.insert(key.clone(), root.root.issued_at_unix_ms)?;
    }
    let rebuilt = replica.epoch_root();
    if rebuilt.root_hash != root.root.root_hash || rebuilt.leaf_count != root.root.leaf_count {
        return Err("subject set does not match signed root".into());
    }
    // Explicit namespace: oracle subjects in this application are capability IDs.
    let revoked: BTreeSet<_> = keys
        .iter()
        .map(|k| RevocationViewSubject::new(k.subject_id.as_str()))
        .collect();
    view.install_if_newer(RevocationSnapshot {
        epoch: root.root.epoch,
        root_hash: root.root.root_hash,
        issued_at_unix_ms: root.root.issued_at_unix_ms,
        revoked,
    })?;
    Ok(())
}
#[tokio::main]
async fn main() -> std::result::Result<(), Error> {
    let directory = Path::new("revocation-run");
    let mut host = host(directory)?;
    let view = Arc::new(RevocationView::new());
    host.kernel.set_revocation_view(view.clone());
    let request = request(&host.capability, "revocation-check");
    let initial = host.kernel.evaluate_tool_call(&request).await?;
    assert_ne!(initial.verdict, Verdict::Allow);
    assert_eq!(host.calls.load(Ordering::SeqCst), 0);
    println!("no trusted snapshot: DENY; handler calls 0");
    let signer = Ed25519RootSigner::new("authority.review", Keypair::generate());
    let verifier = signer.verifier();
    let mut oracle = InMemoryRevocationOracle::new();
    // Whole-second timestamps match this kernel's dispatch clock resolution.
    let mut keys = vec![RevocationKey::new("retired-capability", EpochNonce::new(0))];
    oracle.insert(keys[0].clone(), now() * 1000)?;
    let first = oracle.signed_epoch_root(&signer)?;
    receive(&first, &keys, &verifier, &view, now() * 1000)?;
    let allowed = host.kernel.evaluate_tool_call(&request).await?;
    assert_eq!(allowed.verdict, Verdict::Allow);
    assert_eq!(host.calls.load(Ordering::SeqCst), 1);
    println!("fresh snapshot without this capability: ALLOW; handler calls 1");
    keys.push(RevocationKey::new(
        host.capability.id.clone(),
        EpochNonce::new(0),
    ));
    oracle.insert(keys[1].clone(), now() * 1000)?;
    let second = oracle.signed_epoch_root(&signer)?;
    assert!(receive(&second, &keys[..1], &verifier, &view, now() * 1000).is_err());
    println!("incomplete subject list: refused before installation");
    receive(&second, &keys, &verifier, &view, now() * 1000)?;
    let denied = host.kernel.evaluate_tool_call(&request).await?;
    assert_ne!(denied.verdict, Verdict::Allow);
    assert_eq!(host.calls.load(Ordering::SeqCst), 1);
    println!("revoked capability: DENY; handler calls 1");
    assert!(receive(&first, &keys[..1], &verifier, &view, now() * 1000).is_err());
    assert!(receive(
        &second,
        &keys,
        &verifier,
        &RevocationView::new(),
        second.root.issued_at_unix_ms + 5001
    )
    .is_err());
    println!("rollback and stale root: refused");
    for (name, response) in [
        ("initial", initial),
        ("allowed", allowed),
        ("revoked", denied),
    ] {
        record(directory, name, &response)?;
    }
    std::fs::write(
        directory.join("signed-root.json"),
        serde_json::to_vec_pretty(&second)?,
    )?;
    println!("three kernel receipt signatures verified");
    Ok(())
}

Open the source file. The shared kernel host registers the tool, issues the capability, and persists receipts. Each program asserts its expected refusals; keys, timestamps, and digests change between runs.

You should see the kernel refuse before any snapshot is trusted, allow the capability under a fresh snapshot that omits it, refuse an incomplete subject list before installation, refuse the capability once it is revoked, refuse a rollback and a stale root, and verify three receipt signatures.

Portable absence comes from the finding-status map

Absence against the revocation accumulator is a live query. Its leaves form an ordered vector with no empty slot to point at, so a NonInclusionProof carries a key, a root, and a timestamp, and only the oracle that issued it can check it.

Finding status uses a second structure in the same crate: a full-depth sparse map keyed by SHA-256 finding ids. Its absence proof starts from the fixed empty-leaf hash, folds in 256 sibling hashes, and must reach a trusted root.

crates/trust/chio-revocation-oracle/src/finding_status_sparse.rs233-253rust
/// Verify a portable sparse non-inclusion proof without consulting local
/// state. The exact empty-leaf hash starts the path computation.
pub fn verify_finding_status_non_inclusion(
    root_hash: &Hash,
    finding_id: &str,
    proof: &FindingStatusSparseProof,
) -> Result<()> {
    let key_hash = finding_status_key_hash(finding_id)?;
    if proof.key_hash != key_hash
        || proof.leaf.is_some()
        || proof.siblings.len() != FINDING_STATUS_SPARSE_DEPTH
    {
        return Err(RevocationOracleError::InvalidProof);
    }
    verify_path(
        root_hash,
        &key_hash,
        finding_status_empty_leaf_hash(),
        &proof.siblings,
    )
}

verify_finding_status_non_inclusion is a free function that takes a root hash, a finding id, and a 256-sibling path, and requires the proof to carry no leaf. Its only trust input is the root hash; it needs no live map and returns a typed refusal, so a holder anywhere can check the proof. Authenticating that root is the caller's step: a matching path establishes absence under that root alone.

Revocation oracle reference

Accumulator records

The types below are from chio-revocation-oracle. The accumulator in sparse_merkle.rs is an append-only Merkle tree over an ordered leaf vector: append_leaf pushes onto layer zero and rebuilds parents upward, and proof_bytes_for_index collects siblings by position.

RevocationKey

subject_id: SubjectId, epoch_nonce: EpochNonceOne leaf

The unit of revocation. Equality covers both fields, so the same subject can be revoked again under a different nonce. Every lookup needs both: contains and inclusion_proof take a whole key, and there is no query by subject alone.

EpochRoot

epoch: u64, root_hash: [u8; 32], leaf_count: usize, issued_at_unix_ms: u64The commitment

The whole commitment: four scalars, and the only thing a signature covers.

RootSignature

signer_id, algorithm, signature_bytesDetached

A detached signature. algorithm is the wire tag ed25519; any other tag is refused.

SignedEpochRoot

root, signatureCrosses the boundary

The artifact that crosses the operator boundary. Everything downstream carries it verbatim.

InclusionProof

key, epoch_root, leaf_index, leaf_hash, proof_bytesCheckable offline

A Merkle audit path, checked by the static verify_inclusion over the path, the leaf hash, the leaf index, and the leaf count. It binds a leaf to the epoch_root the proof carries, and that embedded root is unsigned: pair the proof with a SignedEpochRoot for the same epoch, or it proves only self-consistency.

NonInclusionProof

key, epoch_root, checked_at_unix_msLive query

Carries no cryptographic absence evidence. verify_non_inclusion is a method on the live oracle that re-queries membership and compares the current root. It returns a bare bool, so a refusal carries no reason, and it turns false the moment the oracle moves.

FreshnessConfig

max_staleness_ms, offline_grace_msReader's policy

The reader's freshness policy, set by the receiver. The default sets both values to zero.

Leaf hash and key validation

A leaf is a domain-free SHA-256 over a length-prefixed subject and a big-endian nonce. An insert is the only operation that moves the epoch.

crates/trust/chio-revocation-oracle/src/sparse_merkle.rsrust
fn leaf_hash(key: &RevocationKey) -> Result<[u8; 32]> {
    Self::validate_key(key)?;
    let subject = key.subject_id.as_str().as_bytes();
    let mut bytes = Vec::with_capacity(subject.len() + 16);
    bytes.extend_from_slice(&Self::subject_len_prefix(subject)?);
    bytes.extend_from_slice(subject);
    bytes.extend_from_slice(&key.epoch_nonce.get().to_be_bytes());
    Ok(Sha256::hash(&bytes))
}

validate_key refuses an empty or whitespace-padded subject_id with InvalidRevocationKey, so every leaf has a canonical subject and " fixer" never collides with "fixer".

Passport revocations

apply_passport_revocation maps a repeat key's AlreadyRevoked to AlreadyApplied. The epoch stays where it was, so there is no new root to push, and the bridge is idempotent.

PassportRevocationEvent::to_revocation_key builds the key from the event's subject and a nonce that is a fixed salt XORed with revoked_at_unix_ms. The passport_id is validated as non-empty and unpadded and stays out of the key, so two passports for the same subject revoked in the same millisecond share one leaf, and the second reads as AlreadyApplied.

Epochs and issuance time

insert appends exactly one leaf and advances epoch by one, so the epoch counter equals leaf_count. An epoch is one revocation, never an interval of time. Broadcast runs on a tick, DEFAULT_EPOCH_TICK_MS (250), a cadence that trades gossip-storm avoidance against the 500 ms median revoke-to-deny budget; a unit test pins its value.

The one field an insert does not take from the caller unchanged is time:

crates/trust/chio-revocation-oracle/src/sparse_merkle.rsrust
// Clamp issued_at to a monotone non-decreasing floor. Wall-clock
// skew (NTP correction, leap-second roll-back) MUST NOT make a
// newer epoch carry an earlier `issued_at_unix_ms` than a prior
// one: downstream freshness caches that track "latest seen
// issued_at" as a replay-protection floor would otherwise accept
// a root issued strictly before the previous epoch as still
// fresh. Fail-closed on the time field by taking the max.
self.issued_at_unix_ms = self.issued_at_unix_ms.max(now_unix_ms);

issued_at_is_monotone_under_clock_skew inserts at 1000 and then at 500 and asserts that the second root's stamp did not go backwards. The property test epoch_monotone asserts the same pair of invariants over generated subjects with forward-moving stamps, so it covers the ordering rule; the unit test covers the clamp.

Root gossip envelope and push queue

The wire envelope is RevocationRootGossip in chio-federation: a schema tag pinned to chio.federation-revocation-root-gossip.v1, the signed root, and two values duplicated outside the signature for routing, the epoch and the signer_id. Serde is deny_unknown_fields, so a protocol upgrade bumps the schema.

validate_envelope checks the schema and that both duplicated values agree with the signed body. The check is structural; the caller still verifies the signature. A fifth field, ts_unix_ms, carries the sender's emission time outside the signature, where a sender can set it freely. Freshness reads issued_at_unix_ms, inside the signed EpochRoot.

RevocationGossipPushQueue holds a bounded FIFO per subscribed peer and coalesces inside a tick: a strictly higher epoch evicts every queued lower one, an equal epoch replaces, and a lower epoch is dropped. A full queue evicts its oldest entry. The oracle's InMemoryEpochBroadcaster applies the same rule, and in both the catch-up path recovers a peer that fell behind. Both refuse a zero capacity at construction.

tick_and_broadcast publishes to its broadcasters in slice order and returns the first error, so earlier broadcasters keep the root after a mid-list rejection: it fails closed on reporting and is not atomic.

Revocation lane checks

The lane in chio-federation-transport-iroh carries pushes and catch-up on one ALPN, chio/federation/revocation-root/1, capped at 16 MiB per frame. Revocation wire types carry an opaque signer_id and neither an endpoint nor a key, so the lane requires a signer_id to (EndpointId, verifying key) directory, projected from the issuer-signed transport directory. The key that authenticates a root and the endpoint allowed to originate it come from the same signed entry.

Every peer-dependent accept step is bounded per phase and shed above an in-flight cap, so a slowloris peer resets the connection instead of holding an accept slot. verify_batch runs these gates in order and returns the verified roots without merging them:

  1. The authenticated endpoint resolves to an admitted kernel, a re-resolve after the admission gate.
  2. The batch schema and every frame envelope pass the structural check from chio-federation.
  3. The batch names this responder in recipient_kernel_id.
  4. Each frame's signer_id resolves to a pinned binding whose endpoint presented the frame.
  5. The envelope consistency check runs again per frame, before any cryptography.
  6. Each SignedEpochRoot verifies against the bound verify-only key.

The lane then calls merge_batch on the host's sink. The trait's default refuses the whole batch with SinkRejected; a sink implements an atomic stage-then-commit merge, because looping the single-root merge would leave earlier roots applied when a later one failed.

Catch-up requests and blob manifests

RevocationCatchupRequest refuses an inverted range and a span above REVOCATION_CATCHUP_MAX_EPOCHS (4096) at construction, so one stalled peer never makes a responder materialize unbounded history. The request's requester_kernel_id is self-asserted and informational; before serving either catch-up shape, the lane refuses with RequesterMismatch unless the claimed requester is the kernel admitted at the authenticated endpoint, and nothing is served or published.

respond_to_catchup walks the range against the responder's RevocationCatchupHistory. A missing epoch before the first hit is skipped as pre-history; a missing epoch after it ends the run. The responder never fabricates a root, and validate_response re-checks that every frame is exactly one epoch past its predecessor. The answer is a contiguous available run that can begin after the requested start and stop before the requested end.

For a long history the lane answers with a manifest of (signer_id, epoch, blob_hash) entries instead, and the root bytes ride iroh-blobs. The address is one derivation shared by three call sites: the BLAKE3 hash of the RFC 8785 canonical JSON of the SignedEpochRoot. The follower's walk is all or nothing and checks the manifest schema, the entry cap, the single-signer binding, strict contiguity, the pinned signer and its authority endpoint, a 1 MiB per-blob size cap read from the stored blob status before the bytes are materialized, a BLAKE3 re-check, the pinned signer's signature, and a per-entry epoch pin.

BLAKE3 verified streaming proves the bytes match the requested hash; the signature check proves who signed the root inside them, so every fetched blob is deserialized and verified against the pinned key before it counts. The authority writes each root into the store it serves blobs from before it advertises that root's hash. Without a publisher, it advertises no manifest and answers inline.

Root freshness window

verify_fresh_epoch_root refuses a root issued after the supplied current time. Otherwise it accepts an age up to the saturating sum of max_staleness_ms and offline_grace_ms, inclusive. The default, fail_closed(), sets both to zero, so configure an explicit receiving window.

ConfigRoot issued atNowOutcome
default(), 0 and 0
1000
1000
Ok; only an age of zero passes
default()
1000
1001
StaleRoot after 1 ms
with_offline_grace(100, 900)
1000
2000
Ok at exactly the 1000 ms budget
with_offline_grace(100, 900)
1000
2001
StaleRoot
any
1000
999
InvalidEpochTransition
crates/trust/chio-revocation-oracle/src/freshness.rsrust
pub fn verify_fresh_epoch_root(
    root: &EpochRoot,
    now_unix_ms: u64,
    config: FreshnessConfig,
) -> Result<()> {
    if now_unix_ms < root.issued_at_unix_ms {
        return Err(RevocationOracleError::InvalidEpochTransition);
    }

    let age = now_unix_ms.saturating_sub(root.issued_at_unix_ms);
    if age <= config.allowed_age_ms() {
        Ok(())
    } else {
        Err(RevocationOracleError::StaleRoot)
    }
}

This configurable check is separate from the kernel's fixed snapshot limit. The lane delivers verified roots, and the receiving integration applies its freshness window to their signed issuance times.

Kernel snapshot

Install and consult a kernel snapshot through the kernel's revocation view. RevocationSnapshot holds an epoch, a root hash, an issuance time, and the set of revoked capability IDs. install_if_newer atomically installs only a strictly greater epoch, and takes the supplied set as given, without checking a signature or recomputing the set against the root hash. The root-hash field is diagnostic; the snapshot has no signed_root_epoch field, and monotone epoch advancement is the installation check.

An installed view is checked for freshness before membership of the leaf capability and each delegation link. A future-dated snapshot, or one older than the fixed 500 ms limit, returns KernelError::DelegationInvalid, and an installed view that was never populated refuses as stale (empty_view_denies_as_stale). With no view installed, consult_revocation_view_at returns Ok and the per-row revocation-store lookup continues (no_view_installed_returns_ok).

The revocation-view feature uses standard-library support for atomic snapshot exchange; the portable no-std build uses the explicit revocation context its embedder supplies. The consultation sits behind the delegation cargo feature, in chio-kernel's default set, in chio-kernel/src/kernel/delegation.rs.

Finding-status sparse map

finding_status_sparse.rs defines a full-depth sparse map with fixed hashing semantics. It shares only the crate's error type with the accumulator: the accumulator keys on RevocationKey { subject_id, epoch_nonce }, and this map keys on chio.finding.status.v1 finding ids, which are SHA-256 hex.

ConstantValueFixes
FINDING_STATUS_SPARSE_DEPTH
256
One slot for every possible key
FINDING_STATUS_KEY_DOMAIN_NONCE
0x0b_c9f6_f005_59b6
The key domain every key hash mixes in
FINDING_STATUS_MAP_VERSION
sparse_map_v1
The signed status-map version
FINDING_STATUS_PROOF_SEMANTICS
siblings_leaf_to_root_v1
Sibling order, leaf first
Four domain labels
chio.finding.status.v1:key, :empty-leaf, :occupied-leaf, :branch
The role prefix on every hash

The depth consumes every bit of the SHA-256 key digest, so an empty slot is a fact a path can show. The key domain nonce is a wire constant, never derived at run time, so a path in this map cannot be replayed in a map over another key domain. A status epoch that declares any other map version is refused before a path is walked, so an epoch minted over the accumulator never reads as this map. The first sibling is adjacent to the leaf and the last to the root, and each hash carries a NUL-terminated role label, so a leaf hash never reads as a branch hash.

finding_status_key_hash hashes, once with SHA-256, the key domain label, the nonce big-endian, the finding id's length big-endian, and the id, which must be 64 lowercase hex characters. The 256 bits of that digest are the path. An occupied leaf hashes the occupied-leaf label, the key hash, the literal retracted, and the retraction intent digest. An empty leaf hashes the empty-leaf label alone, so one precomputed value stands for every unoccupied slot at a given height.

verify_finding_status_inclusion is the mirror of the absence check: it demands the leaf, checks its finding id and retraction intent digest, and walks the same path from the occupied-leaf hash. chio-finding wraps both verifiers in verify_status_proof_input, which checks a chio.finding.status-proof-input.v1 document and requires the signed chio.finding.status-epoch.v1 envelope to declare this map version, proof semantics, key domain nonce, depth, and the four labels before any path is walked.

The control plane's status publisher rebuilds the map from its sticky leaves, signs one advancing epoch, and persists the epoch, the proof, and any new leaf together. The CLI reaches the same verifiers on its finding-status path, pins the proof bytes by digest, and refuses anything that is not strict canonical input.

Tests, measurements, and models

  • Inclusion. The property test inclusion_proof_soundness runs over generated multi-leaf key sets, beside inclusion_proof_rejects_tampered_key, in sparse_merkle.rs.
  • Absence. non_inclusion_proof_fails_closed_after_insert covers the accumulator's live check; the sparse map's portable check is consumed through chio_finding::verify_status_proof_input by the status publisher and the CLI.
  • Signatures. wrong_key_is_rejected, wrong_signer_id_is_rejected, wrong_algorithm_tag_is_rejected, and short_signature_bytes_fail_closed, in signer.rs.
  • Batches. RevocationHandler::verify_batch and RevocationRootSink::merge_batch in lanes/revocation.rs: a batch that fails any gate merges nothing.
  • Replay. verifier_view_is_monotonically_advancing_after_burst and verifier_rejects_replayed_root_after_advance, in chio-federation/tests/oracle_gossip_e2e.rs: three peers advance monotonically under a burst and refuse a replay of an installed epoch.
  • Revoke to deny, measured in process. three_tier_swarm_revoke_to_deny_under_500ms_median holds the median under 500 ms across 100 trials with a 1500 ms hard per-trial timeout, so a hang fails. Its three kernels are oracle, view, and receipt-log triples in one process. The companion receipt_chain_proof re-runs 8 trials and asserts every allow receipt has seen_epoch < revoke_epoch. Both run behind the oracle crate's dev-only delegation feature.
  • What that measurement spans. Transport is an in-process queue. Each child consults the test's own snapshot.is_revoked mirror of the kernel chain walk, so no freshness gate runs, and install_frame lifts each frame's epoch and root hash into a locally built snapshot without a signature check. It bounds the oracle, queue, and view-install path; a network link and the kernel's dispatch gate are outside it.
  • Throughput. benches/oracle_throughput.rs asserts an insert plus inclusion proof at a p99 of 200 microseconds over 10,000 seeded subjects, before the Criterion run: a 64-sample p99 on one machine, used as a regression bound.
  • Model. formal/tla/RevocationPropagation.tla models unbounded propagation delay, out-of-order delivery, and stale or duplicate messages. NoAllowAfterRevoke holds at every reachable state: no receipt carries allow once its issuer has observed any revocation epoch for that capability. It is checked as part of SafetyInv at PROCS=4, CAPS=8, DEPTH_MAX=4 by formal/tla/MCRevocationPropagation.cfg.
  • Model scope. The model has one shared clock and no action drops a message, so peer clock skew, loss, and partition are outside it. formal/proof-manifest.toml states the boundary to the code: the distributed production gate checks deterministic scalar trace projections for one pinned origin and one view. Full-state refinement, fault injection into the transport, and multi-origin isolation in Rust are outside that gate, and matching mirror hashes detect drift without proving semantic equivalence.

Embedding the lane

The process that hosts the revocation view drives the lane. It calls set_revocation_view, install_if_newer, tick_and_broadcast, enqueue_signed_root, and flush_batches_at, and implements RevocationRootSink (chio-kernel/src/kernel/construction.rs). RevocationOracle is the trait an authority's store implements.

A relay hosts neither a revocation view nor a catch-up history, so chio pheromone relay serve refuses --iroh-lanes revocation at load (parse_iroh_lanes in chio-cli/src/cli/chio/dispatch/pheromone/iroh_mount.rs): a sink with nothing behind it would be fail-open.

The signer is crates/trust/chio-revocation-oracle/src/signer.rs, the lane is crates/trust/chio-federation-transport-iroh/src/lanes/revocation.rs, the kernel's read view is crates/kernel/chio-kernel-core/src/revocation_view.rs, and the sparse map is crates/trust/chio-revocation-oracle/src/finding_status_sparse.rs.

Next: connect the receiving boundary