jcp §4.13: a retry is not a second call, where the caller names the call #588
Loading…
Reference in a new issue
No description provided.
Delete branch "jcp/replay-class"
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?
§4.11 makes a restart give no budget back. This is the case one layer out: the
same call delivered twice.
The gap, precisely
An endpoint already recognised a repeat within its freshness window, by the
request's content address, and answered it from the record without debiting
again — so this narrows an existing mechanism rather than inventing one. The
gap is what that key is made of.
The content address covers the caller's stamp. So it catches a redelivery that
reproduced the request's bytes exactly, and misses the one that matters: a
transport whose retry is slow enough that the first stamp has aged out, where
the caller must re-stamp to be admitted at all. That retry has a different
address, is admitted as a new call, and is debited again. The overspend §4.11
exists to prevent happens anyway, for a reason §4.11 does not reach.
The cause is that identity was incidental — derived from the bytes — rather
than declared.
What changes
WireRequest::call_id— 32 bytes the caller chose, the same across everydelivery of one call and different between calls. Signed with the rest of the
request;
Optionwithserde(default, skip_serializing_if), so a requestwithout one has the bytes it always had and still verifies against the old
signable bytes. Tests pin both, and that editing the id breaks the signature —
otherwise a relay could retarget one call as another.
admitrecognises a repeat by(caller, call_id)before it looks at thebytes, and answers what the first delivery was answered, without debiting.
with_journalalready performs — or a restart would forget which calls it decided and debit
their retries, which is §4.11's own failure one layer out.
Endpoint::with_replay_horizonsets how long a call id is remembered,defaulting to
DEFAULT_SKEW_SECSso an endpoint that sets nothing behaves asit did. An entry is retained by when it was decided, not by the request's
stamp — an entry pruned by the original stamp would already be gone when the
re-stamped retry arrives, which is the whole case.
Three claims the code holds
What this deliberately does not add
Whether an action may be re-dispatched is a property of the action, not of the
id, and §4.11 already governs the unresolved case well: no silent retry, no
reporting an in-flight action as done or failed, surfaced naming the party that
held it. A declared id is what makes a re-dispatch decidable for a dispatcher
that does classify its actions — "safe to re-dispatch" means nothing until the
re-dispatch can be recognised as the same call. So no second replay class is
introduced beside the one the dispatching layer already has.
The spec states the authority instead: the classification is the implementing
party's, the decision to act on it is the enforcement point's, and a model is
never asked whether an action already ran — it would be guessing about state it
cannot observe, and a guess is not evidence.
Incidental tidying
The signable-bytes variants collapse into one.
signed_bytes(&self)builds thecanonical bytes from whichever optionals a request carries,
verify_callerreadsit instead of branching on
challenge, andsign_partstakes both optionalswith
sign/sign_challengeddelegating. Adding a third optional field wouldotherwise have doubled the number of ways to ask the same question.
Conformance
Class C29. The pack reports
✓ C29 a retry is not a second calland allchecks passed.
Evidence
On the Framework box, deliberately not the CI host (mid-matrix for another PR):
named for the properties above;
cargo clippy -p jcp-wire -p jcp-conformance --all-targets --no-deps -- -D warnings: clean;check-comparative-prose.shclean over 4,063 files;check-no-vanity-metrics.shclean over 517 Markdown files.
The two
unused_mutwarnings clippy reports injcp-energyare pre-existing, ina crate this change does not touch, and left for their own change.
🤖 Generated with Claude Code