Replay protection
Name the store that refuses each kind of replay and see what an exact retry returns in place of a second effect.
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.
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.
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 object | Store | A repeat returns |
|---|---|---|
| Proof-of-possession nonce | The DPoP nonce store and its durable domain | A refusal |
| Execution nonce | The host's ExecutionNonceStore; an operation-owned nonce is reserved with its admission operation | A refusal |
| Governed approval | GovernedApprovalReplayStore, in memory unless the host installs a durable one | A refusal |
| Completed operation | The retained admission operation and its terminal record | The 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.
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.
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())),
}
}
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
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.
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:
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")python3 run.py
python3 inspect.pyhost-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:
proofOriginal 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
proof --json > proof.json
jq '{first_proof, repeated_proof, fresh_proof, tool_invocations}' proof.json{
"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:
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.jsonlYou 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:
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.
| Refusal | Cause |
|---|---|
"internal error: execution nonce has already been consumed" | The installed Execution already consumed the nonce |
"governed transaction denied: approval token has already been consumed (replay detected)" | Governed 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 Kernel | A replay store's clock moved in a way that would reopen a replay window |
Execution, from verify_ | 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.