Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor receipt part of the invocation #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Gozala
Copy link
Collaborator

@Gozala Gozala commented Apr 22, 2024

📽️ Preview

Attempt at factoring out receipts from the invocation spec. It describes receipts in terms of /ucan/assert invocation. 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.

Signed-off-by: Irakli Gozalishvili <contact@gozala.io>
@Gozala Gozala requested a review from expede April 22, 2024 22:59

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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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 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.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Define the semantics of the invocation
  2. 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.

Copy link
Collaborator Author

@Gozala Gozala May 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@expede
Copy link
Member

expede commented Apr 25, 2024

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

@Gozala
Copy link
Collaborator Author

Gozala commented Apr 26, 2024

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[]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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: [] 
      }
   }
}

Copy link
Member

@expede expede Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@expede
Copy link
Member

expede commented Apr 30, 2024

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.

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 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants