jcp-wire: a request presented again gets its first answer, so a lost response is recoverable #349
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/jcp-wire-retry-and-single-writer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What was wrong
The jcp/1 endpoint remembered each request it decided inside the freshness window, and it refused a repeat of that request as
replayed. That stopped a captured request from being decided and debited twice. It also left a caller stuck if its response was lost:So the guard against double debits produced one.
What this changes
unrecorded) is decided again when presented again.revoked.DenyReason::Replayedstays, so receipts sealed with it still decode.This complements #346, which keeps a second endpoint from writing the same journal; this PR covers a second send of the same request. It is rebased onto #346, and the 21 jcp-wire tests from both PRs pass together.
Verification (build host)
cargo test -p jcp-wire: 21 pass. Of these, 4 are new and 2 were rewritten from the oldreplayedexpectation. The new ones include a lost response recovered over a live stream, and a byte-identical answer after a restart.🤖 Generated with Claude Code
The endpoint remembered each request it decided inside the freshness window and refused a repeat as `replayed`. That stopped a captured request being decided and debited twice. It also stranded a caller whose response was lost: the identical request was refused, so the caller could not learn the decision or get its receipt. Its only recourse was to stamp the request again, which is a new request, so the same action was decided and debited a second time. The guard against double debits caused one. Now the endpoint remembers the answer as well as the request. A repeat inside the window gets that answer unchanged: the same decision and the same receipt bytes. It is not decided, debited, recorded or sealed a second time. This holds across a restart. The journal replay re-decides each request at the time it was decided, and both the decision and the Ed25519 seal are deterministic, so the replay rebuilds each response byte for byte. A captured request is worth nothing beyond what its first exchange carried. A request refused before it was decided (stale, unauthenticated, or `unrecorded`) is not remembered, and is decided when presented again. A repeat after a revocation still gets the answer it got before the revocation; the next new request is refused `revoked`. SPEC §11 ("A request is decided once") and the C8 conformance check now say this. `DenyReason::Replayed` stays, so that receipts sealed with it still decode. Tests: 4 new and 2 rewritten. All 7 mutations of the new logic are killed. Co-Authored-By: Claude Opus 5.5 <[email protected]>