This proposal defines the execution plan: a self-contained JSON document that describes an ordered sequence of calls one sender is asked to make on one chain, together with a keccak256 plan digest over exactly the fields that determine what gets broadcast.
A plan is produced by software that holds no keys — a routing service, a
protocol front end, a scheduled job, an AI agent's tool server — and is consumed
by a wallet that holds the key and decides, on its own, whether to sign. The
document is transport-neutral: it can be returned by an HTTP endpoint, embedded
in a data: URI, stored in a queue, or carried across a process boundary,
without changing meaning or digest.
The digest is the artifact's contribution to safety. Because it covers the broadcast-determining fields and nothing else, a human approval, a policy decision, and a later audit record can all name the same bytes, and a preparer cannot alter what executes after the moment of approval without producing a different digest.
The proposal also defines the artifact reference: a small envelope that names a stored plan by URL and keccak256 digest instead of carrying it, so that a relaying party — typically an AI agent between the producer and the wallet — holds authority over which plan is used and none over what it says. Because a wallet that fetches a caller-supplied URL can be aimed at things, the network admission rules for that fetch are part of the specification.
A second document type, the typed-data signature request, carries a concrete EIP-712 message for the wallet to sign. It uses the same reference envelope and has its own request digest. It is separate from the execution-plan object and supports flows that end with an offchain signature rather than a wallet-broadcast transaction.
Wallets increasingly execute calls they did not construct. The construction is done by a swap router, a lending front end, a yield optimizer, or an autonomous agent, and the wallet's job is reduced to deciding whether to sign. That split is now the normal shape of an onchain interaction, but the object that crosses the split has no agreed form.
Today it crosses as an ad-hoc JSON blob whose fields are whatever the producer's SDK happened to emit, or as an in-page JSON-RPC call that exists only for the lifetime of a browser tab. Both work. Neither gives the wallet anything to hold onto:
This proposal fixes the shape of that object, fixes what it means to be the same object, and fixes how it may be handed across an untrusted relay. How a wallet decides whether to sign, and what interface a producer is called through, are deliberately out of scope.
The relay deserves its own word. Plans are increasingly assembled by one party, relayed by a second, and executed by a third, and the middle party is often the least trustworthy link — a browser extension, an automation runner, or an agent acting on text from an untrusted source. Handing it the full bytes and asking it to forward them faithfully means it can rewrite them, can corrupt them by re-serializing rather than copying (a failure mode disproportionately common when the relay is a language model), and destroys any record of where they came from. Content addressing solves all three, and wallets have no agreed way to express it.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Machine-readable schemas are provided for the execution plan, the typed-data signature request, and the artifact reference.
A quantity is a JSON string holding a canonical unsigned decimal integer: the
single character 0, or an ASCII digit string with no leading zero. A quantity
MUST fit in a uint256. Decimal strings are used rather than JSON numbers
because IEEE-754 doubles cannot represent the full uint256 range, and rather
than hexadecimal because these values are read by humans on approval screens.
A byte string is a JSON string of the form 0x followed by an even number of
hexadecimal digits. An address is a byte string of exactly 20 bytes. Producers
MUST emit both in lowercase. Consumers MUST normalize both to lowercase before
computing the digest, and MAY additionally reject input that is not already
lowercase; the mixed-case checksum form is a display convention, not part of the
artifact's identity.
| Field | Type | Required | Description |
|---|---|---|---|
schema_version |
string | yes | MUST be "1" for this revision |
chain_id |
quantity | yes | EIP-155 chain identifier the plan executes on |
caip2_chain_id |
string | yes | MUST equal eip155: concatenated with chain_id |
sender |
address | yes | Account expected to send every step |
ordered_steps |
array | yes | 1 to 4096 steps, in execution order |
required_capabilities |
array of string | no | Behaviors the consumer must implement |
simulation_failure_policy |
object | no | What the producer advises on failure |
extensions |
object | no | Producer-defined data the consumer ignores |
A document containing a member not listed here MUST be rejected. Silently ignoring unknown members would let a producer place meaning where a consumer cannot see it, in a document whose whole purpose is that both sides agree on what it says.
Both chain_id and caip2_chain_id are present, and redundant, on purpose. The
quantity is what the consumer computes with; the CAIP-2 identifier is what it
matches against multi-ecosystem configuration and displays. Requiring both, and
requiring that they agree, means a plan cannot carry a namespace and a number
that disagree about which chain it is for.
Each entry of ordered_steps is an object:
| Field | Type | Required | Description |
|---|---|---|---|
step |
number | yes | 1-indexed position; entry n MUST have step equal to n |
kind |
string | yes | One of the step kinds below |
transaction |
object | yes | The call to make |
revert_decode |
object | no | ABI for rendering a revert from this step |
transaction has members chain_id (quantity), from (address), to
(address), data (byte string), value (quantity), and an OPTIONAL gas
(quantity). to is REQUIRED: a step is a call to an existing account, and
contract creation is not expressible in this format. Every step's chain_id MUST equal the plan's chain_id and every
step's from MUST equal the plan's sender. A plan is for one account on one
chain; a document that mixes them MUST be rejected rather than partially
executed.
gas is a producer hint. A consumer MUST NOT broadcast a producer-supplied gas
limit without its own estimation, because the value is chosen by a party that
does not pay for it.
kind explains why a step is in the plan, and MUST be one of:
execution — a call the user asked for.approval — grants a spending allowance a later step needs.allowance_cleanup — revokes an allowance an earlier step granted.signature_dependent_execution — spends a signature approved earlier.other — none of the above.The value is presentation-affecting rather than semantic: it lets a consumer
tell a reviewer why a step they did not ask for is present. Consumers MUST NOT
derive authority from kind. It is a producer's claim, and a producer that
mislabels a transfer as allowance_cleanup has changed only the caption, never
the calldata that the digest covers and the reviewer sees.
revert_decode, when present, is an object with kind equal to error_result,
an abi array holding a JSON ABI fragment declaring at least one error, and an
OPTIONAL boolean required defaulting to false. It exists so a consumer can
render a custom revert as its declared error rather than as an opaque byte
string. required records whether the producer considers a successful decode
necessary to present the failure meaningfully; it is advisory, and a consumer
MAY ignore it. Consumers MUST treat the decoded result as display text only, and MUST
bound the fragment they accept; the implementation this proposal is drawn from
allows at most 128 entries and 65536 bytes.
required_capabilities lists behaviors the plan needs from whatever executes it.
A consumer MUST reject a plan naming any capability it does not implement, rather
than executing the plan without that behavior. This is the inverse of the usual
extension-negotiation default, and it is deliberate: the interesting capabilities
are ones whose absence silently breaks a plan's safety, not ones whose absence
merely degrades it.
This proposal defines one:
atomic_batch — every step executes in a single transaction that reverts as a
whole. A plan that grants an allowance, spends it, and revokes it needs this;
executed as separate transactions, a failure between steps two and three leaves
a live allowance the user never agreed to keep.Consumers commonly satisfy atomic_batch through an account-abstraction batch
executor, or through an EIP-7702 authorization delegating the
sender to a batching implementation for the duration of the transaction.
A multi-step plan that does not list atomic_batch MAY be executed as
separate transactions. A consumer doing so MUST execute steps in order, MUST NOT
begin a step until the previous one has been mined successfully, and MUST stop
at the first failure. A consumer that can execute atomically MAY do so
regardless of whether the plan asks for it.
Capability names MUST be at most 64 printable ASCII characters, and a plan MUST NOT list more than 32. Further names are expected to be registered by later proposals.
When present, simulation_failure_policy MUST contain all three of
rpc_error, execution_reverted, and simulation_setup_error. Each is an
object with an action and a human-readable instruction of 1 to 2000
characters.
action MUST be one of:
retry_same_plan — the failure is transient; the identical document may be
retried.reprepare_plan — the document is stale; ask the producer for a new one.user_review — do not retry automatically; surface it to a human.execution_reverted and simulation_setup_error MUST NOT specify
retry_same_plan. Calldata that reverted against current state will revert
again, and an automatic retry loop over unchanged bytes is a way to spend fees
without making progress.
This block is advice from the party that knows why the plan is shaped the way it
is; a stale quote wants reprepare_plan where a rate-limited endpoint wants
retry_same_plan. Consumers MAY ignore it entirely, and MUST NOT treat it as
authorization to broadcast anything.
extensions is an object reserved for producer-defined data. Consumers MUST
ignore its contents and MUST bound its serialized size; 65536 bytes is
RECOMMENDED. It is excluded from the digest, so a producer MUST NOT place in it
anything that changes what executes or what a reviewer needs to see.
A consumer MUST enforce limits on any plan it accepts, and MUST reject rather
than truncate. The following are RECOMMENDED: at most 4096 steps, at most 8 MiB
of data summed across steps, and at most 16 MiB of serialized document.
The plan digest is the keccak256 hash of the UTF-8 serialization of a canonical JSON object built from the plan:
{
"schema_version": "1",
"chain_id": "1",
"sender": "0x...",
"ordered_steps": [
{
"step": 1,
"kind": "approval",
"transaction": {
"chain_id": "1",
"from": "0x...",
"to": "0x...",
"data": "0x...",
"value": "0"
}
}
]
}
The serialization MUST place members in exactly the order shown, MUST contain no
whitespace between tokens, and MUST encode addresses and data as lowercase byte
strings. step MUST be serialized as a JSON number and every other scalar as a
JSON string.
This member order is normative and is not lexicographic. A generic
canonicalizer such as RFC 8785 sorts members by name and would order the outer
object chain_id, ordered_steps, schema_version, sender, producing
different bytes and a different digest. Implementations MUST emit the order given
here rather than delegating to a general-purpose canonicalization routine.
The digest covers schema_version, chain_id, sender, and each step's step,
kind, and the five broadcast-determining transaction members. It excludes
caip2_chain_id (derivable from chain_id), gas, revert_decode,
simulation_failure_policy, required_capabilities, and extensions.
The exclusions define what the digest is for. It is the identity of the bytes that will hit the chain and the labels shown beside them, so that two documents share a digest exactly when approving one is the same act as approving the other. A producer that re-serializes a plan with a different gas hint has not changed what the user agreed to. A producer that changes a single calldata byte has.
Consumers SHOULD display the digest wherever they display a plan for approval, SHOULD record it against the resulting approval, and SHOULD record it against the broadcast transaction hash.
The digest is not a signature and carries no authenticity claim. It answers "are these the same bytes", never "did anyone vouch for them".
A typed_data_signature_request is a separate document carrying one concrete
EIP-712 message and the account expected to sign it. The
typed-data signature request specification
is normative for this document type. It defines the request and result shapes,
the request digest, and optional delivery of the signature to the producer.
Supporting signature requests is OPTIONAL. A consumer that supports execution
plans only MUST reject signature requests. A signature request MUST NOT be
encoded as an execution-plan step or placed in its ignored extensions field.
The execution-plan document and plan digest are unchanged.
Producers MAY return several signature requests, receive their results, and then return another concrete signature request or an execution plan. Each new document requires its own validation and authorization. No execution plan is required when the workflow ends with an offchain signature and producer relay.
A plan or typed-data signature request MAY be handed to a consumer inline, or by an artifact reference: an envelope that names the stored document and lets the consumer fetch and verify it itself.
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | yes | MUST be "artifact_reference" |
artifact_type |
string | yes | "execution_plan" or "typed_data_signature_request" |
url |
string | yes | https URL, data: URI, or file URI locating the bytes |
integrity |
object | conditional | Digest over the exact stored bytes |
bytes |
number | conditional | Exact byte length of the stored bytes |
instruction |
string | no | Human-readable note from the producer |
integrity and bytes are REQUIRED when url is an https or file
URL, and OPTIONAL when it is a data: URI; in a data: URI the bytes are the
reference. When either is supplied for a data: URI it MUST still be checked.
Consumers MUST
ignore unrecognized top-level members of the envelope, so producers can enrich
it without stranding deployed wallets.
integrity is an object with algorithm, which MUST be "keccak256" for this
revision, and value, 0x followed by 64 hexadecimal digits. Unlike the
envelope, integrity is strict: a consumer MUST reject an integrity object
carrying any other member. An integrity block containing something the consumer
does not understand is one it cannot claim to have verified.
artifact_type MUST be exactly execution_plan or
typed_data_signature_request. No other artifact types are defined or accepted
by this revision. A consumer MUST reject a type it does not implement, and MUST
reject a reference whose artifact_type differs from the one expected at the
point of use. The discriminator is what stops an
envelope obtained for one purpose from being redeemed for another, and it lets a
consumer apply the right size bound before fetching anything.
In the retrieval rules below, the maximum document size and validation rules are those of the selected type. Fetching a signature request MUST NOT use the larger execution-plan size limit merely because the envelope is shared.
instruction is producer prose. It is display context and MUST NOT influence
any authorization decision.
Given an envelope, a consumer MUST, in this order:
bytes is present and exceeds the maximum plan size,
before opening any connection or reading any file.file transport MUST reject a file envelope.bytes is present and the retrieved length differs.integrity is present and the digest of the retrieved
bytes differs.Length is checked before the digest because it is the cheaper refutation, and both before parsing because a parser is a larger attack surface than a comparison. Where the transport applied a content encoding, the digest is over the decoded bytes.
Verification says nothing about whether the plan should be executed. A verified plan has been shown to be the plan the envelope named, and that is all; it earns no trust from having a URL.
file transportA file URI names bytes on the consumer's own filesystem — the cheap transport
for a producer on the same machine, such as a local MCP server that writes the
plan where the wallet can read it. The relay carries only the path, the digest,
and the length, never the body, and the URL fully specifies where the bytes are
with no out-of-band agreement.
A consumer fetching a file URL MUST enforce all of the following, and MUST
fail rather than degrade when any cannot be met:
localhost, and names an absolute path.integrity and bytes are present; a file reference without them is
rejected before any read.Consumers SHOULD additionally restrict file reads to one or more
user-configured directories and treat anything outside them as failure. An
unrestricted file transport lets any relay that can reach the wallet name any
file the wallet can read; only the rules above stand between a guessed path and
a read the user never intended.
https transportA consumer fetching an https URL MUST enforce all of the following, and MUST
fail rather than degrade when any cannot be met:
https on the default port.Error messages MUST NOT include any part of the response body. A consumer that reports what a refused endpoint returned has become a way to read its own network from outside it.
data: transportA data: URI MUST have media type application/json, MAY use the ;base64
parameter, and MUST decode to at most the maximum plan size. Consumers MUST
bound the encoded length before decoding and MUST NOT perform network access for
a data: reference. Base64 packs three bytes into four characters and
percent-encoding never contracts, so nothing longer than ⌊max ÷ 3⌋ × 4 + 4
characters can decode to an acceptable body.
A consumer that reports where a plan came from MUST distinguish an https host,
which is a fact the transport proved, from an inline data: URI, which has no
host and no provenance beyond whoever handed over the envelope. A file path
is likewise caller-chosen text, not a statement about who published anything,
and MUST NOT be presented as provenance beyond the local machine. The
transport-proved host is one of the few facts about a plan that is attested
rather than asserted, and a wallet's authorization rules MAY key on it.
wallet_sendCallsA plan maps onto the parameters of EIP-5792 wallet_sendCalls
and back, and a wallet or producer that speaks both MAY convert between them:
| Plan | wallet_sendCalls |
|---|---|
chain_id (decimal) |
chainId (hexadecimal) |
sender |
from |
ordered_steps[n].transaction.to / data / value |
calls[n].to / data / value |
atomic_batch in required_capabilities |
atomicRequired: true |
kind, revert_decode, simulation_failure_policy, extensions |
no equivalent; dropped |
Converting in the other direction, caip2_chain_id is derived, every step's
kind is execution, and atomicRequired: true becomes atomic_batch. The
plan digest is computed from the plan form, so a plan that round-trips through
wallet_sendCalls keeps its digest exactly when its calls are unchanged.
EIP-5792 also carries a batch of calls to a wallet, as
wallet_sendCalls arguments, and this proposal borrows its capability
negotiation. The difference is lifetime. An interface call exists between two
live endpoints; an artifact is a value that can be stored, forwarded, queued for
tomorrow, reviewed on a different device, and compared to what was broadcast a
month later. The use cases that motivate this proposal — a plan prepared by a
service and approved by a person somewhere else, a scheduled job whose output is
audited after the fact — need the value, not the call.
The two compose, and the mapping above is normative so that they do. A wallet
that speaks wallet_sendCalls can accept a plan by converting it, and a
producer can serve a plan to an in-page wallet the same way. What this proposal
adds is a digest that survives the conversion, so the identity of the intent
outlives the connection that carried it, and a reference form that lets the
intent cross an untrusted relay intact.
Including gas would be the cautious choice, and it is the wrong one. Gas limits are re-estimated by the consumer against current state as a matter of course, and a digest that changes on re-estimation would break on every honest path, which teaches implementers to ignore digest mismatches. A digest that changes only when the executed bytes change is one whose mismatches mean something.
The economic risk from gas is real and is addressed where it belongs: the wallet bounds what it is willing to pay through its own policy, described in the wallet's own authorization rules, using its own estimates rather than the producer's hint.
required_capabilities inverts the usual "ignore what you do not understand"
rule. A plan that needs atomicity and does not get it does not fail loudly — it
succeeds partway and leaves a live allowance behind. Fail-open extension
negotiation is right for features whose absence degrades an experience and wrong
for features whose absence silently removes a safety property, and the ones worth
naming here are all of the second kind.
Allowing a plan to span accounts or chains would make the digest cover something no single signer can execute, and would make partial execution the normal case. Cross-chain and multi-account intent is real, and is better expressed as several plans with an outer structure that sequences them, where each plan remains a thing exactly one signer can accept or refuse in full.
Subresource Integrity is the same idea in the browser, and it is not reusable here. Its digests are SHA-2 where everything else a wallet handles — plan digests, transaction hashes, selectors — is keccak256, and mixing hash functions across one trust boundary invites verifying one and reporting the other. It has no type discriminator, so nothing prevents redeeming a reference in the wrong context. It has no inline form. And it is an attribute of an HTML element, not a value that can be relayed through the non-browser transports that are the whole use case.
Re-running admission on every hop is possible and buys less than it costs. Each hop is another chance for a check to be applied inconsistently, and the producer already knows where its own bytes are. Refusing redirects makes the URL in the envelope the URL that was fetched, which is also what makes the reported host meaningful.
integrity is strict when the envelope is notThe envelope is a message that will grow, and a consumer that rejected an enriched envelope would break on a producer's routine improvement. The integrity block is an assertion the consumer either verified in full or did not; there is no partial credit, so tolerating unknown members inside it would let a producer believe it had constrained something the consumer never checked.
The alternative, 0x-prefixed hexadecimal quantities as used in the JSON-RPC
interface, is more idiomatic for Ethereum tooling. Decimal wins here because a
plan is read by people during approval. Amounts, chain identifiers, and step
counts appear on review screens, and 1000000000000000000 is checkable by a
human where 0xde0b6b3a7640000 is not. Byte strings that are never read as
numbers stay hexadecimal.
This proposal introduces a new document format and no changes to the chain, to existing interfaces, or to deployed contracts. Producers and consumers that do not implement it are unaffected.
Typed-data signature requests are a separate, explicitly named document type. They do not alter execution-plan v1 fields, step kinds, digest computation, or test vectors. Existing execution-plan references remain valid. Consumers that only support execution plans reject the new type; existing transaction signing permissions MUST NOT implicitly authorize typed-data signatures. The reference discriminator is now limited to these two types rather than arbitrary names.
The document composes with EIP-5792 as described in the
Rationale, and with EIP-7702, which is one way for a consumer to
satisfy atomic_batch.
schema_version is a string rather than a number so that future revisions may
use qualified values. A consumer MUST reject a version it does not implement.
Typed-data signature request vectors and a fixture runner cover the new request digest, EIP-712 domain binding, delivery constraints, and structural validation. The runner also checks every original execution-plan vector below.
Vectors are provided in the digest test vectors. Each case gives a plan, the exact canonical byte string its digest is taken over, and the resulting digest.
| Case | Digest |
|---|---|
| A one-step plan with empty calldata | 0x93aeec006e55dfe0f54041d53c94387e08c504d4f3b3826cd3426dbc7da38ea5 |
| A two-step plan, whose step 2 carries a gas hint the canonical form omits | 0x13c4b058741ee7bdfe5a51825c71bd205859b89436505a15adf60bd3f2281deb |
The same two-step plan with different gas, extensions, required_capabilities, and a different failure instruction |
0x13c4b058741ee7bdfe5a51825c71bd205859b89436505a15adf60bd3f2281deb |
| The same two-step plan with the final byte of step 2 calldata changed | 0x5bf27f490fa44998b6386dfe1b0fae6df0c1b2a9604b69d83bbbbd7a6d285549 |
The third and fourth cases are the whole point of the digest. Everything the third case changes is invisible to what gets broadcast, so it shares a digest with the second; the one byte the fourth case changes is broadcast, so it does not.
The first case is cross-checked against two independent implementations of the canonical form — one in Rust, one in JavaScript — which agree on its digest. Implementers are encouraged to start there, because a canonicalization mistake produces a plausible-looking digest that is silently incompatible with everyone else's.
Ekubo Wallet, a desktop wallet at github.com/EkuboProtocol/wallet, implements
the execution-plan format as its only route from a plan producer to a broadcast transaction.
Its ExecutionPlan type in crates/ekubo-wallet-core/src/core/execution_plan.rs
performs the validation described above, computes the digest, and rejects unknown
members, over-long capability lists, oversized extension bags, and
retry_same_plan where this proposal forbids it. The wallet displays the digest
at approval and records it against both the approval and the broadcast hash. It
satisfies atomic_batch by executing every multi-step plan as one EIP-7702
batch that reverts as a whole, whether or not the plan asks for it, and executes
a single-step plan as a direct call. It accepts mixed-case addresses and
normalizes them before digesting.
The reference envelope and its admission rules are implemented in
crates/ekubo-wallet-core/src/plan_fetch.rs, which verifies byte count and
digest before parsing, bounds outbound retrievals with a semaphore and per-type
body caps, and surfaces the transport-proved host or the inline origin at
approval time.
The Ekubo protocol's plan producer at mcp.ekubo.org emits this format for
transfers, swaps, liquidity, and yield operations, and returns reference
envelopes for the plans it stores; the relaying agent passes each envelope
through verbatim.
Nothing about a well-formed plan implies it should be executed. A plan is
attacker-controlled input in the general case: producers are untrusted by
construction, and an agent relaying a plan may itself be acting on
prompt-injected instructions. Consumers MUST validate every constraint in this
proposal, MUST evaluate their own authorization rules against the resulting
calls, and MUST NOT treat producer-supplied fields — kind, instruction,
extensions, revert_decode output — as inputs to that decision.
The digest binds bytes to bytes. It does not attest that a producer is honest, that a plan does what its labels claim, or that anyone approved it. A consumer that shows a user a digest and a description has shown them a fingerprint of the calldata and an unverified caption; the description is the producer's word, and only the decoded calls are evidence.
Because the digest excludes gas, two plans sharing a digest may carry different
gas hints. This is intended, and it is why consumers MUST re-estimate rather than
broadcast the hint.
A plan that simulates cleanly may still revert or execute differently when mined, because state moved in between. Consumers SHOULD re-simulate immediately before signing rather than relying on an earlier result, and MUST NOT treat a stored simulation result as standing permission to broadcast.
The step count, calldata, extension, and document limits are load-bearing. A producer that can hand a consumer an unbounded document can exhaust its memory or stall the process that parses it. Consumers MUST apply limits before parsing where the transport allows, and MUST reject oversized input rather than truncate it — a truncated plan that still parses is a plan whose digest no longer describes what a reviewer saw.
revert_decode hands the consumer a producer-controlled ABI fragment and asks it
to decode producer-influenced bytes. Consumers MUST bound the fragment, MUST run
the decoder in a context where a malformed fragment cannot do more than fail, and
MUST treat the output as untrusted text requiring escaping before display.
A verified reference proves the retrieved bytes are the bytes the envelope
named. It proves nothing about whether the producer is honest, whether the plan
does what its instruction claims, or whether it should be executed. A consumer
that treats verification as approval has replaced a review with a checksum.
file reference makes the wallet read on the relay's behalfUnlike the https transport, which is fenced off the user's network by the
admission rules, a file reference aims the consumer at its own filesystem —
inside its trust boundary, read with the wallet's own privileges, on behalf of
a relay that may be remote. The digest check proves the bytes are the bytes the
envelope named, but it does not make the read harmless:
file URI itself performs
no network access, but the posture — the wallet acting inside its boundary on
a relay's naming — is the same one that keeps loopback out of the https
transport.Consumers that implement the file transport accept this posture deliberately,
and the directory restriction above is what keeps a local-producer convenience
from becoming a general file-reading primitive for any relay that can reach
the wallet. Consumers that do not want it MUST reject file envelopes; a
producer that needs every wallet to succeed hands the plan over https or
inline instead.
A consumer that fetches caller-supplied URLs is a request forwarder inside the user's trust boundary. Every admission rule — public unicast only, no redirects, no credentials, no non-default ports, no fragments — is load-bearing. Consumers MUST check the resolved address rather than the hostname alone, and SHOULD connect to the address they checked, because a name that resolves publicly at check time and privately at connect time defeats a hostname-only check. Suppressing response bodies in errors is part of the same control.
Every retrieval consumes a resolver query, a connection, a timeout window, and up to the plan size cap in memory. The concurrency bound, the timeouts, and the pre-fetch size refusal are all required, and belong to the retrieval primitive rather than to any one caller of it. A consumer that decodes a content encoding before hashing MUST bound the decoded stream as it decodes.
data: references are not safer for being localA data: URI avoids the network, and that is its only advantage. Its bytes came
from whoever handed over the envelope and carry no provenance. A consumer that
shows a fetched plan's host and shows nothing for an inline one is reporting
this correctly.
A digest mismatch means the bytes changed or the reference is stale, and the two are indistinguishable from the consumer's side. Consumers MUST treat both as failure and MUST NOT fall back to the retrieved bytes. Producers SHOULD serve immutable bodies at each URL so that a mismatch is unambiguously an integrity failure.
Copyright and related rights waived via CC0.