Chio/Docs
LOGIN · JOIN

Replay protection

Name the store that refuses each kind of replay and see what an exact retry returns in place of a second effect.

Concept chio@5f63e6e

On this page

A client that loses a response can ask again and get its finished work back. Someone who copies a proof or an approval gets a refusal.

Both repeat a message; the kernel tells them apart by what is repeated. A proof nonce records one presentation, an operation key names work whose result may be fetched again, and a retained request binds that key to its original arguments so a caller cannot swap the work while claiming to recover it. One-use authority is consumed under its own identity while the original operation and its result stay retained.

Replay pathsChoose a repeated input.
repeated inputoutcomenonce replayedσcopied proofDPoP nonce storerefusedconsumedτreused approvalapproval replay storerefusedExactrexact retryretained operationρoriginalConflictr′changed payloadretained operationrefusedKkernelnonce replayedσproofrefusedconsumedτapprovalrefusedExactrexactρoriginalConflictr′changedrefusedKkernelanswered by theretained operation

An exact retry returns the original result

A retry whose request commitment matches the retained operation is classified Exact. For a completed operation the kernel returns the original receipt and output from the terminal record; the tool is not invoked again.

Four repeated inputs, each looked up in its own store by the kernel. Store names are shortened; the execution nonce store, clock-anomaly refusals, and captured operations with an unknown outcome are left out.

Each kind of single-use authority has its own store

Identify the repeated object first. Each has a responsible store, its own binding, and an explicit lifetime, because each answers a different question.

Single-use objectStoreA repeat returns
Proof-of-possession nonceThe DPoP nonce store and its durable domainA refusal
Execution nonceThe host's ExecutionNonceStore; an operation-owned nonce is reserved with its admission operationA refusal
Governed approvalGovernedApprovalReplayStore, in memory unless the host installs a durable oneA refusal
Completed operationThe retained admission operation and its terminal recordThe original result

Design the retry path with the first request. Save the operation identity and the request bytes before submission, and keep the consumption history through restarts. Payment nonces are a separate mechanism scoped to settlement: EIP-3009 transferWithAuthorization nonces are refused by their own store, as Economy describes.

A consumed nonce stays consumed for its whole signed lifetime

The replay contract is the trait ExecutionNonceStore: reserving a nonce identifier returns true exactly once, and every later reservation of the same identifier returns false.

An execution nonce's consumed marker across a process restart, in memory and in a persistent store061218243036t (s)in memoryLRU cache10marker lost at restartaccepted again
cmem(t)=1[6≤t<12]\htmlClass{role-seal}{c_{\mathrm{mem}}}(t) = \mathbf{1}[6 \le t < \htmlClass{role-grant}{12}]
cmem(20)=0⇒accepted\htmlClass{role-seal}{c_{\mathrm{mem}}}(\htmlClass{role-call}{20}) = 0 \Rightarrow \htmlClass{role-call}{\text{accepted}}
persistentSQLite10Err(Replayed)
cdisk(t)=1[t≥6]\htmlClass{role-seal}{c_{\mathrm{disk}}}(t) = \mathbf{1}[t \ge 6]
cdisk(20)=1⇒Replayed\htmlClass{role-seal}{c_{\mathrm{disk}}}(\htmlClass{role-call}{20}) = 1 \Rightarrow \htmlClass{role-deny}{\texttt{Replayed}}
tp=20\htmlClass{role-call}{t_p = 20}
issued_atexpires_atsigned window, 30 sexpiredfirst use Ok(true)
restart tr=12\htmlClass{role-grant}{\text{restart } t_r = 12}
accept(tp)  ⟺  tp<exp  ∧  c(tp)=0\mathrm{accept}(\htmlClass{role-call}{t_p}) \iff \htmlClass{role-call}{t_p} < \htmlClass{cor-parent}{\mathrm{exp}} \;\wedge\; \htmlClass{role-seal}{c}(\htmlClass{role-call}{t_p}) = 0
An execution nonce's consumed marker across a process restart, in memory and in a persistent store061218243036marker lost at restartaccepted againErr(Replayed)
tp=20\htmlClass{role-call}{t_p = 20}
issued_atexpires_atexpiredin memoryLRU cachepersistentSQLitefirst use Ok(true)
restart tr=12\htmlClass{role-grant}{\text{restart } t_r = 12}

The process restarts at 12 seconds. A replay at 20 seconds: in memory, the replay is accepted, a second dispatch; in a persistent store, the replay is rejected as Replayed.

accept(tp)  ⟺  tp<exp∧c(tp)=0cmem(t)=1[6≤t<12]cdisk(t)=1[t≥6]\begin{aligned} \mathrm{accept}(\htmlClass{role-call}{t_p}) &\iff \htmlClass{role-call}{t_p} < \htmlClass{cor-parent}{\mathrm{exp}} \wedge \htmlClass{role-seal}{c}(\htmlClass{role-call}{t_p}) = 0 \\[6pt] \htmlClass{role-seal}{c_{\mathrm{mem}}}(t) &= \mathbf{1}[6 \le t < \htmlClass{role-grant}{12}] \\ \htmlClass{role-seal}{c_{\mathrm{disk}}}(t) &= \mathbf{1}[t \ge 6] \end{aligned}
In Rust · execution_nonce.rs:11119 lines
    /// Reserve a nonce while telling the store when the nonce stops
    /// being cryptographically valid. Durable implementations (SQLite,
    /// remote KV stores) MUST retain the consumed marker until at least
    /// `nonce_expires_at` so replay protection covers the nonce's full
    /// validity window.
    // …
    fn reserve_until(&self, nonce_id: &str, _nonce_expires_at: i64) -> Result<bool, KernelError> {
// …
pub fn consume_execution_nonce(
    nonce_store: &dyn ExecutionNonceStore,
    nonce_id: &str,
    nonce_expires_at: i64,
) -> Result<(), ExecutionNonceError> {
    match nonce_store.reserve_until(nonce_id, nonce_expires_at) {
        Ok(true) => Ok(()),
        Ok(false) => Err(ExecutionNonceError::Replayed),
        Err(error) => Err(ExecutionNonceError::Store(error.to_string())),
    }
}
Illustrative seconds with the default 30-second nonce lifetime, for a call that presents a nonce; with require_nonce at its default of false, callers may omit one. A durable store keeps the consumed marker through the signed expiry. Single-use authorization still does not establish exactly-once effects inside an external tool.What survives a crash

SqliteExecutionNonceStore is a durable implementation an embedding host can install; it keeps the consumed marker in SQLite, so a restart leaves the window closed while the signed nonce is still valid. The in-memory implementation keeps the marker for the life of the process. The test replayed_nonce_is_rejected_by_store presents one kernel-signed nonce twice: the first verification consumes it and the second is rejected as a replay.

A governed approval is consumed at the same dispatch boundary through a second trait, GovernedApprovalReplayStore. It reserves a subject, request, and intent together for one dispatch attempt. Reserving the same tuple again returns false, and the kernel denies the transaction instead of dispatching twice under one vote.

In Rust: the execution nonce store
crates/kernel/chio-kernel/src/execution_nonce.rs98–158rust
pub trait ExecutionNonceStore: Send + Sync {
    /// Attempt to reserve (consume) the given nonce identifier.
    ///
    /// * `Ok(true)`  -- nonce was fresh; it is now marked consumed.
    /// * `Ok(false)` -- nonce has already been consumed (replay detected).
    /// * `Err(_)`    -- the store is unreachable or corrupted; fail-closed.
    ///
    /// Prefer [`Self::reserve_until`] when the caller knows the signed
    /// expiry of the nonce: durable stores need to retain the consumed
    /// marker at least as long as the signed nonce is valid, otherwise
    /// the row may be pruned and the nonce can be replayed within its
    /// remaining validity window.
    fn reserve(&self, nonce_id: &str) -> Result<bool, KernelError>;

    /// Reserve a nonce while telling the store when the nonce stops
    /// being cryptographically valid. Durable implementations (SQLite,
    /// remote KV stores) MUST retain the consumed marker until at least
    /// `nonce_expires_at` so replay protection covers the nonce's full
    /// validity window.
    ///
    /// The default implementation falls back to [`Self::reserve`] for
    /// in-memory / best-effort stores that already track retention
    /// internally. `nonce_expires_at` is wall-clock unix seconds.
    fn reserve_until(&self, nonce_id: &str, _nonce_expires_at: i64) -> Result<bool, KernelError> {
        self.reserve(nonce_id)
    }

    /// Whether this store can create and conditionally roll back an owned
    /// reservation before tool dispatch begins.
    fn supports_dispatch_reservations(&self) -> bool {
        false
    }

    /// Reserve a nonce for one dispatch attempt. Stores advertising dispatch
    /// reservation support must retain the owner and permit only that owner to
    /// roll the reservation back.
    fn reserve_for_dispatch(
        &self,
        nonce_id: &str,
        nonce_expires_at: i64,
        _reservation_id: &str,
    ) -> Result<bool, KernelError> {
        self.reserve_until(nonce_id, nonce_expires_at)
    }

    /// Remove an owned reservation after a failure known to precede any tool
    /// side effect.
    fn rollback_dispatch_reservation(
        &self,
        _nonce_id: &str,
        _reservation_id: &str,
    ) -> Result<bool, KernelError> {
        Err(KernelError::Internal(
            "execution nonce store does not support dispatch reservation rollback".to_string(),
        ))
    }

    fn is_consumed(&self, _nonce_id: &str) -> Result<bool, KernelError> {
        Ok(false)
    }
}

Read more in Threshold approvals, which covers how the eligible set assembles a vote before it reaches this store.

An exact retry returns the original result and runs nothing

AdmissionReplayClassification separates a retry from a second attempt. An Exact retry matches the retained operation's request commitment and may carry its recorded AdmissionTerminalReplay outcome: a receipt, an incident, or an economic mutation. A Conflict reuses the same identity for a different payload; the original stays intact, and the new task is authorized separately under its own identity.

crates/kernel/chio-kernel/src/admission_operation.rs1168–1173rust
pub enum AdmissionReplayClassification {
    Exact {
        terminal_replay: Option<AdmissionTerminalReplay>,
    },
    Conflict,
}

An exact retry of a completed operation returns the original outcome. An exact retry in any other state continues that same operation or is refused. The test top_level_durable_admission_commits_before_dispatch_and_blocks_replay shows both halves: an identical retry of a completed request returns the same receipt and output without invoking the tool, and a retry that changes the arguments under the same request id is denied, also without an invocation.

A refusal is a different record. In the family-budget race the winner's completion is retained, while a new presentation of the exhausted loser can produce a new refusal receipt, so assert idempotent replay against completed-operation evidence.

Exact retry after a host crash: the swarm example

The Security swarm example commits a mailbox send, kills the serving host, and restarts it from the retained state. It then presents the original call again and a distinct writer call:

run.pypython
def scenario_host_crash(sock_root):
    swarm = Swarm("host-crash", sock_root, family_budget=PLANNED_CALLS)
    swarm.host.start()
    try:
        before = swarm.run_planned("triage-417")
    finally:
        swarm.host.kill()
    swarm.host.start()
    try:
        replayed = swarm.run_planned("triage-417")
        after = swarm.run_planned("fixer-417")
    finally:
        swarm.host.stop()
    same_receipt = json.loads(replayed["receipt_json"])["id"] == json.loads(before["receipt_json"])["id"]
    if not (sent_sequence(before) == "1" and replayed == before and same_receipt and sent_sequence(after) == "2"):
        raise ScenarioFailure(f"restart did not return the original outcome once: {before} {replayed} {after}")
    retain_results(swarm, {"before": before, "replayed": replayed, "after": after})
    print("RECOVER host-crash: restart resumed without repeating an effect")
Terminalbash
python3 run.py
python3 inspect.py
Output
host-crash: original sequence=1, next sequence=2; mailbox contains 2 messages

The first operation and its replay return identical response and receipt bytes, the next legitimate send receives sequence two, and the separate inspector counts two mailbox messages. The crash falls after a returned committed send, so the fixture covers retained completion replay after SIGKILL; other lost-acknowledgement windows are covered by unknown outcomes.

The prepared example refuses a copied proof and returns finished work once

The proof exchange in the Execution application serializes a subject-signed request and verifies it against the receiver's issuer pin and the received argument bytes. Run it from the example directory with the prepared example commands:

Terminalbash
proof
Output
Original request         Accepted
Changed arguments        Rejected
Repeated proof           Rejected
Different signing key    Rejected
Fresh proof              Accepted

Tool invocations: 0

You should see the original request accepted once and its repetition rejected; a changed path fails its binding before the nonce is consumed. The fresh proof uses another nonce and a new subject signature and passes the proof check, yet the program performs zero tool invocations. Freshness cannot tell whether an application wants old work back or a distinct operation authorized; that question belongs to the operation's identity.

Inspect the proof record
Terminalbash
proof --json > proof.json
jq '{first_proof, repeated_proof, fresh_proof, tool_invocations}' proof.json
Output
{
  "first_proof": "accepted",
  "repeated_proof": "DPoP proof verification failed: nonce replayed: this nonce has already been used during the proof validity window",
  "fresh_proof": "accepted",
  "tool_invocations": 0
}

The same application's review-note program answers the identity question. After the Admission example completes review-1, save the completed evidence, repeat the original executor and report exchange, and compare the bytes:

Terminalbash
cp .admission/requests/review-1/report.json original-report.json
cp .admission/requests/review-1/completed.receipt.json original-receipt.json
review-note execute review-1 --state .admission
review-note report review-1 --state .admission
cmp original-report.json .admission/requests/review-1/report.json
cmp original-receipt.json .admission/requests/review-1/completed.receipt.json
wc -l .admission/resource/review-notes.jsonl

You should see both comparisons exit 0 and the journal still hold one note. The executor reopened its own ledger and returned its original signed report, and the kernel reconciled that report to the same receipt, with no fresh execution nonce and no new operation.

A replayed result is released only under live authority

Handing a retained result back to an authenticated caller is a release. When a persisted checkpoint carries a caller-delivery report, require_caller_output_release checks that the deployment is not under an active emergency stop and rechecks the capability's current revocation status before returning it.

A capability that was live when the operation ran can be withdrawn before its result is delivered back, and the recheck then refuses the delivery. A retained return with no caller-delivery report passes through this function without either check. Emergency stop covers the other check this release point runs.

Renaming an operation never revives consumed authority

Signed authority for one call cannot become fresh work by changing its operation_key. In the swarm example's authority scenario, the attempt fails with request_binding_mismatch and the original valid send stays available under its original identity:

run.pypython
planner = swarm.calls["triage-417"]
original = swarm.run_planned("triage-417")
reused = swarm.invoke("triage-417", planner, operation_key="reused-authority")

assert runtime_failure_code(reused) == "request_binding_mismatch"

A tool call receives every replay refusal below as a deny reason. An ExecutionNonceStore without dispatch reservations is consulted only at the effect boundary, where the same text appears inside the reason for the failed credential-retention step. Only verify_presented_execution_nonce, the embedder helper a tool server calls to verify a presented nonce itself, returns a typed store error.

RefusalCause
"internal error: execution nonce has already been consumed"
The installed ExecutionNonceStore already consumed the nonce
"governed transaction denied: approval token has already been consumed (replay detected)"
GovernedApprovalReplayStore already holds that subject, request, and intent
"replay clock <rollback or forward_jump> detected in <store>: observed <seconds>, high-water <seconds>, maximum tolerated skew <seconds>s", from KernelError::ReplayClockAnomaly
A replay store's clock moved in a way that would reopen a replay window
ExecutionNonceError::Replayed, from verify_presented_execution_nonce
A tool server verifying a presented nonce finds it consumed

For a consumed nonce or approval, inspect the original operation before deciding whether anything may be submitted again; reconnecting restores no single-use grant. A clock anomaly needs an operator to investigate the clock and the retained high-water state, since clearing the replay store would reopen the window it protects. When a provider's acknowledgement is lost before acceptance is known, inspect the original captured operation, keep its captured charge, and authenticate downstream status: a timeout is evidence for neither a resend nor a refund, and operation recovery takes it from there.

Next: recover an uncertain operation