Refactor receipt part of the invocation#1
Conversation
Signed-off-by: Irakli Gozalishvili <contact@gozala.io>
| > — Anonymous | ||
|
|
||
| UCAN is a chained-capability format. A UCAN contains all of the information that one would need to perform some task, and the provable authority to do so. This begs the question: can UCAN be used directly as an RPC language? | ||
| A `Receipt` is a Invocation of the `/ucan/assert` capability. It represents signed assertion from the [Executor] state describing [Result] and [Effect]s of some task invocation. Receipt is a signed commitment by [Executor] to a state, described by it, within the timeframe of the `Receipt`. |
There was a problem hiding this comment.
I've been thinking about this a bit recently. It's possible that /assert is too broad, since you can assert all kinds of things including content claims, computation receipts, and (oracle) facts about the world. In this current setup, you need to duck type the args field.
How do you feel about making receipt a subtype of assert? It would have all of the same fields, but you'd also know more about the inner structure. Following the new cmd format, this would look like /ucan/assert/receipt or /ucan/assert/ran or similar.
There was a problem hiding this comment.
I've been thinking about this a bit recently. It's possible that
/assertis too broad, since you can assert all kinds of things including content claims, computation receipts, and (oracle) facts about the world. In this current setup, you need to duck type theargsfield.
I have not called that out explicitly but that was kind of the intention behind making facts into a map. The way I was thinking about it is that receipt is just an assertion that specifies two expected {out, run} facts, but it should be possible to assert arbitrary that about entity in the about field they just aren't required by receipt specification.
How do you feel about making
receipta subtype ofassert? It would have all of the same fields, but you'd also know more about the inner structure. Following the newcmdformat, this would look like/ucan/assert/receiptor/ucan/assert/ranor similar.
I suppose ☝️ in comparison might be useful if you want to say allow asserting {out, run} but nothing else 🤔 On the other hand if someone adds additional field even if cmd was /ucan/assert/receipt it probably still should be considered as a valid receipt.
Either way I don't mind changing cmd to /ucan/assert/receipt although I do not know if I buy into the argument.
There was a problem hiding this comment.
in comparison might be useful if you want to say allow asserting {out, run} but nothing else
I think that we're thinking about the design space here very differently. I see the cmd field as filling two roles:
- Define the semantics of the invocation
- Establish a common language / "which fields are available to that function call"
Receipts have a particular mechanical function with promises. They're more than an assertion of fact — while you can look at it through that lens, these are very different in functionality.
This is the same distinction as shows up in the age old arguments about structural vs nominal typing. Just because the same structural information exists doesn't necessarily mean that it has the same meaning. The analogy breaks down a but here since with a type system you can move that extra bit of information to compile time, which we can't do due to the distributed nature of the problem.
What I liked about the idea of subtyping is that we could nominally tag with the cmd field that "the payload is just some assertion, not something to run", and leave the fields blank. You can then subtype for more specific uses.
There was a problem hiding this comment.
I don't think we're thinking that differently it's just you're hoisting all the types all they way to the invocation and use cmd as a discriminant, which I generally agree with. In this instance however I feel like not hoisting the type is a better idea and treating it as somewhat constrained generic makes more sense to me. Here is illustration in typescript syntax
What I'm proposing is something like
type Assert<Facts extends {[key:string]: unknown}> = {
cmd: "ucan/assert"
with: DID
args: { about: Link, facts: Facts }
}
type Receipt = Assert<{ out: Result, run: Link[] }>
// I could also have something like
type LocationCommitment = Assert<{ site: URL }>Where's what you're suggesting is not to make Assert generic but rather hoist to the cmd discriminant
type Receipt = {
cmd: "/ucan/assert/receipt"
with: DID
args: { about: Link, out: Result, run Link[] }
}
type LocationCommitment = {
cmd: "/ucan/assert/commitment"
with: DID
args: { about: Link, site: Link[] }
}I prefer generic route as opposed to hoisted, because I have a neutral component in the system that does not need to worry about the Facts parameter and can index things regardless of the concrete type. Doing this with hoisted variant is possible, but requires maintaining list of cmds or adopting some name-spacing like ucan/assert/, even then I loose one invariant which is all variants should have about field.
On the flip side I do recognize that generic variant above no longer features discriminant which certainly can be a drawback. It does not affect my use case because indexer does not care, and receipts are to be queried from the index so things that don't match will not be returned by query.
That said I understand that in other systems having discriminant may be important which is why I proposed something like this as a compromise (where key is a discriminant)
type Assert<Facts extends {[key:string]: unknown}> = {
cmd: "ucan/assert"
with: DID
args: { about: Link, facts: Facts }
}
type Receipt = Assert<{ receipt: { out: Result, run: Link[] } }>
type Commitment = Assert<{ commitment: { site: URL } }>It also could be sugared more to your liking as follows (although level of advanced typing makes me uncomfortable)
type Assert<Key extends string, Facts extends {[key: string]: unknown}> = {
cmd: `/ucan/assert/${Key}`
with: DID
args: { about: Link } & Facts
}
type Receipt = Assert<"receipt", { out: Result, run: Link[] }>
type Commitment = Assert<"commitment", { site: URL }>I find last two pretty much identical except later seems to reduce nesting and require more powerful type system and one before is less demanding on type system at the expense of bit more nesting
|
Given how receipts interact with promises, is the idea here to make more granular repos? Basically: flowchart TB
Invocation
Delegation -->|depends on| Invocation
Revocation -->|depends on| Invocation
Assertion -.->|kind of| Invocation
Receipt -.->|kind of| Assertion
Promise -->|depends on| Receipt
|
We don't have to break it out necessarily it's just we did talk not too long ago that perhaps receipts are still something we're iterating and others might want to use invocations without receipts so I thought I'd just move it into own thing so people could adopt invocations without necessarily adopting receipts. |
|
|
||
| # Signature from the "iss". | ||
| s Varsig | ||
| run Effect[] |
There was a problem hiding this comment.
@expede your comment about asserts been very generic and my sentiment that receipt is just an assertion of {out, run} got me wondering that perhaps run could be made optional and implicitly []. That way this is even more minimal requirement space.
Or alternatively we could make make the whole thing as
{
cmd: '/ucan/assert'
args: {
about: { '/': 'bafy...thing' },
facts: {
receipt: {
out: { ok: {} },
run: []
}
}
}
}That way /ucan/assert/receipt could be typed as follows and be equivalent of the above
{
cmd: '/ucan/assert/receipt'
args: {
about: { '/': 'bafy...thing' },
facts: {
out: { ok: {} },
run: []
}
}
}There was a problem hiding this comment.
Hmm I'm not sure about adding facts as a wrapper around out and run. Does it add something that I'm missing? This is such a common case, it makes me wonder if we should just put receipts at the same level as assertions ("receipts don't need to be assertions"). Assertions in this style almost feel so generic as to be meaningless, which is something I've been warned against that was a mistake in OAuth 2
There was a problem hiding this comment.
Hmm I'm not sure about adding facts as a wrapper around out and run. Does it add something that I'm missing?
It only makes sense if there are generic assertions and receipt is just a variant of it, kind of like what sturct with known fields is to a map. If we say there is no generic assertions then it no longer matters.
In other words I was treating about as entity, and facts as set of attribute → value pairs for that entity. Trough this lens "receipt" is just a well defined relation akin of schema you can define (e.g. in datomic). My sentiment there is we could define other schemas for other relations over time. In fact someone could also extend receipts to add some extra attributes. e.g. I have contemplated idea of using this similar to session cookies.
Now non of that matters if we decide that generic assertions aren't a think we care in which case wrapper does become a more of redundancy.
There was a problem hiding this comment.
I think one other option to kind of do what I want in terms of generic assertions without having to burden anyone else is to change about to something that is clearly distinct from all other fields of the args in which case same goal could be accomplished. I have in fact considered following variant, but hesitated because of special relation IPLD has with /.
{
cmd: '/ucan/assert/receipt'
args: {
'/': 'bafy...thing',
out: { ok: {} },
run: []
}
}Some other distinct symbol could be utilized of course, but I don't know it never feels right, so I suggested {about, facts} instead.
Yeah that's true. I've also thought it through more since writing the above and it's probably the right thing. There's no drawback AFAICT, and we can always merge them later if need be. It also lets us ship delegation, invocation, and revocation immediately. Promises and receipts have often been a slow burn for it to click, so let's just remove the problem by separating them 👍 |
Could we have a bit more explanation here? How does this loop carry meaning? What does it mean that the Issuer (either the Executor or their deputy) has addressed the Executor here? Anyone may address anyone, so it doesn't provide any further evidence that the Executor has stated something; that comes entirely from the Issuer being the Executor, or from the Executor's trust in some deputy issuing assertions on its behalf via delegation. I would have assumed that the audience would be either some Principal who particularly needs to know about the facts of the assertion (such as the Issuer of the original Invocation), or |
A receipt has always been encoded as a /ucan/assert/receipt invocation, and that wire format is intentional — it tracks the UCAN WG draft (ucan-wg/receipt#1) where a receipt is a /ucan/assert invocation. This change keeps the wire format byte-identical and only reshapes the Go API. Previously `Receipt` embedded `invocation.Invocation`, so `*Receipt` satisfied `ucan.Invocation` and surfaced the full invocation method set (Subject, Audience, Command, Proofs, Cause, Task, ...) — most of which carry no receipt-specific meaning. `receipt.Option` was an alias of `invocation.Option`, exposing options like WithProofs/WithCause that have no defined behavior on a receipt today. Now: - `Receipt` holds the invocation as an unexported field instead of embedding it. `*Receipt` no longer satisfies `ucan.Invocation`; callers get only receipt-meaningful accessors (Issuer, Ran, Out, IssuedAt, Nonce, MetadataBytes, SignedBytes, Signature, Link, Bytes). - `ucan.Receipt` no longer embeds `ucan.Invocation`; it is a standalone interface listing exactly that surface. - `receipt.Option` is its own type that translates to invocation options internally. It exposes WithNonce/WithNoNonce/WithIssuedAt/ WithMetadata — the options meaningful for a receipt today. - Adds `receipt.VerifySignature`, mirroring the invocation/delegation helpers. The API surface is intentionally minimal for now; proof and expiration accessors/options will be added once those semantics settle in the spec. Construction (`issue` -> `invocation.Invoke`) and decoding (via `invocation.Invocation`) are unchanged, so encoded receipts are identical to before.
Locks in the invariant the receipt API rests on: a receipt is its own Go type, but on the wire it is byte-identical to a /ucan/assert/receipt invocation. TestWireFormatIsInvocation encodes a receipt and decodes the same bytes via invocation.Decode, checking the command, issuer, CID, signed bytes, and that Ran/Out are recoverable from the args. If the receipt encoding ever drifts from the invocation encoding, this fails. - the invocation view re-encodes to byte-identical output - the executor is issuer, subject, and audience (per ucan-wg/receipt#1) - the bytes verify through invocation.VerifySignature, not just parse - the Out value decodes back to the original, not just non-nil - Adds TestNotAReceipt in the invocation package as the mirror: a plain invocation with a non-receipt command must be rejected by receipt.Decode. Together the two tests pin the discrimination boundary in both directions.
A receipt has always been encoded as a /ucan/assert/receipt invocation, and that wire format is intentional — it tracks the UCAN WG draft (ucan-wg/receipt#1) where a receipt is a /ucan/assert invocation. This change keeps the wire format byte-identical and only reshapes the Go API. Previously `Receipt` embedded `invocation.Invocation`, so `*Receipt` satisfied `ucan.Invocation` and surfaced the full invocation method set (Subject, Audience, Command, Proofs, Cause, Task, ...) — most of which carry no receipt-specific meaning. `receipt.Option` was an alias of `invocation.Option`, exposing options like WithProofs/WithCause that have no defined behavior on a receipt today. Now: - `Receipt` holds the invocation as an unexported field instead of embedding it. `*Receipt` no longer satisfies `ucan.Invocation`; callers get only receipt-meaningful accessors (Issuer, Ran, Out, IssuedAt, Nonce, MetadataBytes, SignedBytes, Signature, Link, Bytes). - `ucan.Receipt` no longer embeds `ucan.Invocation`; it is a standalone interface listing exactly that surface. - `receipt.Option` is its own type that translates to invocation options internally. It exposes WithNonce/WithNoNonce/WithIssuedAt/ WithMetadata — the options meaningful for a receipt today. - Adds `receipt.VerifySignature`, mirroring the invocation/delegation helpers. The API surface is intentionally minimal for now; proof and expiration accessors/options will be added once those semantics settle in the spec. Construction (`issue` -> `invocation.Invoke`) and decoding (via `invocation.Invocation`) are unchanged, so encoded receipts are identical to before.
Locks in the invariant the receipt API rests on: a receipt is its own Go type, but on the wire it is byte-identical to a /ucan/assert/receipt invocation. TestWireFormatIsInvocation encodes a receipt and decodes the same bytes via invocation.Decode, checking the command, issuer, CID, signed bytes, and that Ran/Out are recoverable from the args. If the receipt encoding ever drifts from the invocation encoding, this fails. - the invocation view re-encodes to byte-identical output - the executor is issuer, subject, and audience (per ucan-wg/receipt#1) - the bytes verify through invocation.VerifySignature, not just parse - the Out value decodes back to the original, not just non-nil - Adds TestNotAReceipt in the invocation package as the mirror: a plain invocation with a non-receipt command must be rejected by receipt.Decode. Together the two tests pin the discrimination boundary in both directions.
## Summary `Receipt` becomes its own Go type, distinct from `Invocation` — **without changing the wire format**. On the wire a receipt is still a `/ucan/assert/receipt` invocation; that encoding tracks the UCAN WG draft ([ucan-wg/receipt#1](ucan-wg/receipt#1)) and is unchanged here. The change is entirely in the Go API surface. ## Motivation A receipt has always been encoded as a `/ucan/assert/receipt` invocation, and the Go `Receipt` type embedded `invocation.Invocation` to reuse the envelope / sigPayload / varsig plumbing. That reuse was a sensible starting point. As more code was built on the API, a few rough edges emerged — all on the Go side, none on the wire — that this PR smooths out: 1. **Clearer type boundaries.** Because `*Receipt` embedded `invocation.Invocation`, it satisfied `ucan.Invocation` — so anything declared `func(inv ucan.Invocation)` would quietly accept a receipt. That has worked in practice, but it makes call-site review harder and pushes helpers like `validator.VerifyInvocationSignature` toward being defensively written. With `Receipt` as its own type, signatures say what they mean. 2. **Tighter discoverability.** Autocomplete on a receipt previously surfaced the full invocation method set — `Subject`, `Audience`, `Command`, `Proofs`, `Cause`, `Task`, … — most of which don't carry receipt-specific information (`Command()` is always the constant, `Subject()`/`Audience()` are always the executor). The type now exposes only the receipt-meaningful accessors: `Issuer`, `Ran`, `Out`, `IssuedAt`, `Nonce`, `MetadataBytes`, `SignedBytes`, `Signature`, plus `Link`/`Bytes`. 3. **A receipt-specific options surface.** `receipt.Option` was a re-export of `invocation.Option`, so options like `WithProofs` / `WithCause` appeared on the receipt API without a defined meaning for a receipt today. `receipt.Option` is now its own type that translates to invocation options internally, exposing just `WithNonce` / `WithNoNonce` / `WithIssuedAt` / `WithMetadata`. ## What changed - `Receipt` holds an `invocation.Invocation` as an **unexported field** instead of embedding it. `*Receipt` no longer satisfies `ucan.Invocation`. - `ucan.Receipt` is a standalone interface — it no longer embeds `ucan.Invocation`. - `receipt.Option` is its own type with a receipt-only option set. - Adds `receipt.VerifySignature`, mirroring the `invocation` / `delegation` helpers. - Construction (`issue` → `invocation.Invoke`) and decoding (via `invocation.Invocation`) are unchanged, so encoded receipts are byte-identical to `main`. ## Intentionally deferred `prf` (proofs) and `exp` (expiration) are load-bearing receipt fields in the WG draft — proofs for delegated receipt issuance, expiration for assertion TTL. They are physically present on the wire today (a receipt is an invocation), but their receipt-level semantics are still under discussion upstream. Rather than ship half-defined accessors, the API stays minimal now and will grow `Proofs` / `Expiration` accessors and options once the spec settles. ## Wire compatibility **Unchanged.** Encoded receipts are byte-identical to `main`. Two tests pin the boundary in both directions: - `TestWireFormatIsInvocation` — a receipt's exact bytes decode via `invocation.Decode`, re-encode verbatim, verify through `invocation.VerifySignature`, and carry the executor as `iss`/`sub`/`aud`. - `TestNotAReceipt` (invocation package) — a non-receipt invocation is rejected by `receipt.Decode`. ## Test plan - [x] `go build ./...` and `go vet ./...` - [x] `go test ./...` — full suite green - [x] `TestWireFormatIsInvocation` — receipt bytes are a valid, verifiable invocation - [x] `TestNotAReceipt` — non-receipt invocations don't decode as receipts - [x] `TestNotInvocation` — `*Receipt` does not satisfy `ucan.Invocation` - [x] `TestVerifySignature`, `TestOptions`, `TestIssueOK` / `TestIssueErr`
📽️ Preview
Attempt at factoring out receipts from the invocation spec. It describes receipts in terms of
/ucan/assertinvocation. This is an early draft and could use substantial lifting, however I thought it was still good idea to capture what we've been discussing in side channels so we could align on exact format.