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

Commitments as a more generic variant of receipts #33

Open
Gozala opened this issue Apr 2, 2024 · 19 comments
Open

Commitments as a more generic variant of receipts #33

Gozala opened this issue Apr 2, 2024 · 19 comments

Comments

@Gozala
Copy link
Contributor

Gozala commented Apr 2, 2024

We have been finding that receipts right now fail to provide desired accountability, specifically receipts are like universal commitments that running certain task produces certain result and are effectively public.

In our system we are discovering that what we actually want is to issue commitments to a user that submitted a task e.g. when user uploads data we want to issue commitment that we will serve it to the authorized user from certain URL. That is commitment to a specific user, specifically sub of the invocation. This implies that sub can re-delegate our commitment to other actors that can exercise / verify our commitment e.g. by trying to load that content from the URL in the commitment. Crucial piece here is that commitment is to the user not to everyone in the world, because user will be billed for all the reads from the URL so ultimately they get to decide whether to make our commitment open to public or delegate it to a selected set of principals.

Argument could be made that described commitment is perhaps not what receipt is supposed to be, although I am getting more and more convinced that receipts are just limited subset of commitments where that have no ongoing costs e.g. computing hash of some byte array produces receipt and it does not really matter who invoked it as once computed it is done and there are no ongoing costs. However that could be modeled as commitment to "everyone" simply by making audience of the commitment be "did:key" with well known private key so anyone could hold issuer accountable.

@expede
Copy link
Member

expede commented Apr 4, 2024

Yeah agreed that receipts are a form of (possibly subclass of) assertion 👍 There is a question about how wide our concept of assertion should be. Technically anything signed is an assertion.

@expede
Copy link
Member

expede commented Apr 4, 2024

Porting some conversation to public:

// Gozala's proposal
{
   iss: "did:key:w3up",
   aud: "did:key:zAlice",
   sub: "did:key:w3up",
   cmd: "/ucan/assert",
   exp: null // <- permanent
   pol: [
     [
      "==",
      ".",
      {
        assert: [
          // that following invocation
          {
             "cmd": "/math/sum",
             "sub": "did:key:zAlice",
             "args": {
               "0": 2,
               "1": 3 
              },
              "nonce": "auaeu"
          },
          // produces fllowing outcome
          {
            out: { ok: { x: 1 } },
            next: []
          }
        ]
      }
    ]
  ]
}

The exp: null // permanent part I very much agree with 💯

"running this task produces this result"

Yes, this is what we did for IPVM with Task IDs. Instead of inlining the args, we hashed them, which had several advanatges:

  • Shorter, trivial to compare rather than doing deep equality
  • Unambiguous what made something semantically "the same"
  • Easy to compare if run by multiple (e.g. a CRDT)

This was constructed from the tuple hash(subject, command, args, nonce). This can be done directly as the ran, or as a secondary index. I've come to kind of like the secondary index version, since it can be calculated from the complete invocation (being a subset of the fields).

This also lets you escape having to model the whole thing as a delegation with recursively defined fields and leans more into the content addressability that powers so much of UCAN.

const invocation = {
   "iss": "did:key:w3up",
   "aud": "did:key:zAlice",
   "sub": "did:key:zAlice",
   "cmd": "/math/sum",          
   "args": {
     "0": 2,
     "1": 3 
   },
   "nonce": "auaeu"
}

const receiptAssertion = {
   iss: "did:key:w3up",
   aud: "did:key:zAlice",
   sub: "did:key:w3up",
   cmd: "/ucan/assert",
   args: {
     ran: invocation.cid(), // <- NOTE Invocation CID, not a Task ID
     out: { ok: { x: 1 } },
     next: []
   }
}

@expede
Copy link
Member

expede commented Apr 4, 2024

I think I'm starting to catch up to some of your other (I think trapped in DMs) comments @Gozala. There's a few tradeoffs in this design space, and we might be able to do a #whynotboth.

I really like the idea of being able to arbitrarily restrict assertions. The downside is that args blocks can potentially get really big (e.g. inlined Wasm modules). You suggested elsewhere that we can jump the hash boundary (assuming that it's resolvable). This increasingly makes sense to me, especially once inline-ipld lands (which eliminates the concerns around data availability for things that can be inlined/embedded).

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

Yes, this is what we did for IPVM with Task IDs. Instead of inlining the args, we hashed them, which had several advanatges:

I should have probably provided some explanation why I chose to not inline there, despite advantages you've listed.

  1. Delegating ability to issuer receipts on behalf of sub is really important to me and relatedly I need to be able to restrict that ability as opposed to allowing actor to issuer any receipts. I was imagining that service could delegate to worker ability to issue only receipts for the commands under /math/ namespace:

    {
       iss: "did:key:w3up",
       aud: "did:key:zWorker",
       sub: "did:key:w3up",
       cmd: "/ucan/assert",
       exp: null // <- permanent
       pol: [
         [
          "like",
          ".assert[0].cmd",
          "/math/*"
        ]
      ]
    }

    ❗️ It is worth acknowledging however that this is not perfect. Specifically I wish service could delegate ability to issuer permanent receipts only in a specific time frame. That said I'm not sure whether one is mutually exclusive of the other.

  2. Since coming up with merkle refenences I am more and more convinced that dictating reference boundaries is a bad idea and think that signing over the payload agnostic of them is a way to go. I am not trying to impose this position, but thought it is a worthy context to have never the less

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

For more context this is a direction I have been thinking for our ucan agents (client or server doesn't matter):

  1. Both ends need to maintain local cache of invocations and receipts
    • That also helps to avoid redundant calls to service if receipt for the task is available locally
  2. All of the (client) state is derived from receipts. We probably will have some index and cash eviction strategy, but thinking of state as a view of receipts had been really compelling.
  3. We have been embracing (datomic inspired) datalog engine with triple stores. And receipt in the example translates into a triple like [entity: ucan.sub, att: hash(ucan.args.assert[0), val: ucan.args.assert[1]] which is effectively saying that sub claims that task CID resolves to an outcome.
    • Note that sub is effectively a state of the replica
  4. Exp of the assertion / receipt is also nice property for doing GC or cache eviction.

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

from @expede in side channel

Since these all end up in the args of the assertion invocation
(assuming it's an invocation, that is)

I think it depends what you are referring to.

At the risk of controversy, I have been thinking that commitment is probably more of a delegation than invocation. Rational is that service commits to uphold the commitment, but it is ultimately up to the recipient to decide whether they want to do so. This makes less sense in terms of receipts or things of permanent nature because upholding commitment (typically) requires no further work.

On the other hand commitments that demand some work to uphold are very different, there you do not want to make an invocation unless necessary.

🤔 Perhaps above nuance is unimportant as long as your invocation comes with fully constrained outmost delegation, because audience would be able to rip invocation wrapper and us that instead. However it still makes little sense to me to have an invocation where aud is not a sub, which is why I think of it as commitment given to the invoker so they can uphold sub accountable by exercising it via invocation.

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

I should also perhaps mention that general assertions are becoming really interesting, because they are become somewhat similar to session cookies or a distributed state. Service hands back state to the agent and in next interaction agent can provide credible evidence that state is as follows.

I think plugging some STM like thing into mix might be also very interesting, that is capture revision in the assertions so that service could perform optimistic updates without having to query local state.

@expede
Copy link
Member

expede commented Apr 4, 2024

At the risk of controversy, I have been thinking that commitment is probably more of a delegation than invocation.

This is an interesting distinction!

I think this is probably accurate for (my understanding of) your use case with content claims 💯 I think that's a different thing from an assertion of fact, though. A content claim is more like a promise to be able to retrieve data at some location, which absolutely can (maybe even should) be a delegation. Basically "you can get retrieve this from here", including revocation, expiration, etc etc. You still exercise it by retrieving data from that location, and you can even limit who is allowed to if you want. If I translate this into traditional OCAP, I think this is right way to model your use case.

"Here's the output of running your task" is not something that you need to exercise later; it's just a statement of fact, and we can still hold you to it if you lied (because it's signed), but many cases are not dependent on evolving side effectful state or location. It's not "active" in the same way: there's nothing to ask the subject to later do with it

@expede
Copy link
Member

expede commented Apr 4, 2024

Yeah, I did some Granovetter diagramming, and the above is almost certainly correct.

Here's what I propose (which we talked about briefly in DMs): our use cases for assertions/receipts are actually less similar than they look on the surface because how they use authority. I'm more than happy to help lend cycles to the design of your claims system, but I'm increasingly convinced that it's a different thing from an execution receipt. Let's forego receipts in the spec. This way I can continue to iterate on the invocation version, and we can focus on helping you ship the delegation version of content claims.

How does that feel?

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

I think leaving out receipts or moving them into separate spec seems like reasonable way to unblock the invocation spec. However I am concert about dependency between promise pipelines and receipts specifically I'm not sure how can we go about specifying how await works without agreement on how the receipts are structured so 🤷‍♂️

Above is also partially why I wanted to find an alignment. We could still create receipts that simply wrap commitments but I'm bit uncertain if that provides much value.

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

Oh another, thing that we talked in DMs and I have realized since, you were asking why would aud of the /ucan/assert be the issuer of the invocation especially when the outcome is a permanent fact as opposed to commitment. During our conversation I was suggesting that if it is not point to point (like commitment) but rather public statement (anyone can hold you accountable) it could be delegated to did:key:anyone. I have since realized that we do not need did:key:anyone instead it should just terminate in a loopaud == sub, which effectively allows anyone to hold you accountable.

I also recognize that in that case it is probably an invocation as opposed to delegation.

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

To be clear I am also entirely opposed to idea of wrapping commitments in receipts, but I have started wondering if that makes sense especially if only thing that receipt contains is a commitment. On the other hand I am realizing now that doing so probably addresses this concern I've raised earlier

❗️ It is worth acknowledging however that this is not perfect. Specifically I wish service could delegate ability to issuer permanent receipts only in a specific time frame. That said I'm not sure whether one is mutually exclusive of the other.

Meaning receipt may have expiry yet wrap commitment that lasts forever. So there is some merit to it IMO.

However even if we wrap commitments with receipts I still think it makes more sense for receipts to be UCAN invocations, in fact that would align perfectly with an idea of distributing a state. User could invoke task that has await on the service made assertion effectively declaring that as service claimed state is whatever receipt says.

P.S. There are some interesting questions however if those could be conditioned in some way, but that is definitely out of scope for now.

@expede
Copy link
Member

expede commented Apr 4, 2024

dependency between promise pipelines and receipts

Hmm that's a good point. Do you expect to use pipelining with content claims?

@expede
Copy link
Member

expede commented Apr 4, 2024

(Specifically: I wouldn't expect that to touch delegations at all)

@expede
Copy link
Member

expede commented Apr 4, 2024

However even if we wrap commitments with receipts I still think it makes more sense for receipts to be UCAN invocations, in fact that would align perfectly with an idea of distributing a state.

Yes, I'm basically sold on this (minus a couple edge cases, but I think they're resolvable). What to map all of the fields to is the bigger question to me (e.g. still undecided about how sub and aud should behave)

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

Hmm that's a good point. Do you expect to use pipelining with content claims?

In our current designs they are referenced, however current design also assumes receipt wrapper so perhaps it doesn’t really matter.

I think we’re still refining how we want to use commitments (which is term we use instead of claims for things that are impermanent but verifiable, we use content claims to refer to refer to statements that are permanent and verifiable) however we do really care about pipelining tasks so having that defined is important to us

@Gozala
Copy link
Contributor Author

Gozala commented Apr 4, 2024

Yes, I'm basically sold on this (minus a couple edge cases, but I think they're resolvable). What to map all of the fields to is the bigger question to me (e.g. still undecided about how sub and aud should behave)

I am really not sure how can I help with this, but if there’s a way please let me know. To me it boils down to this #33 (comment)

  1. Like any other ucan root issuer should be a sub, if user wants executor to issue assertion on their (sub) behalf they should explicitly authorize them.
  2. Terminal aud should be root iss and consequently sub.
  3. /ucan/assert pretty explicitly commands sub(ject) to assert specific fact and it makes sense that receipt is just a signed invocation representing it.

I think you were thinking that sub should be same as sub of the invocation. That on the other hand does not make sense to me because:

  1. Executor in majority of cases has no authority over sub(ject) so it shouldn’t be able to produce authorization chain.
  2. User may ask multiple actors run a task and if each came with a different receipt and contradicting results which one is valid ? Sounds like bad actor may affect reputation of the sub(ject) authority. On the other hand each one asserting result under own authority allows invoker to verify and accept one they find to be correct and they can do so by re-issuing that receipt with their own subject (authority)
  3. Trying to infer issuer of the receipt introduces ambiguity and potential errors if worker performing task doesn’t have a key corresponding to aud.

@Gozala
Copy link
Contributor Author

Gozala commented Apr 5, 2024

Expanding on previous thoughts even in compute as a network (IPVM case) described approach makes sense:

  1. User sends invocation to an aud corresponding to coordinator actor.
  2. Coordinator finds actor that are able to perform task and re-delegates ability to issue receipt for a task on coordinators behalf
  3. Selected actor runs a task and issues receipt under coordinator’s authority (sub)

In an alternative design where coordinator wants to verify result before issuing receipt it also aligns well.

  1. User sends invocation to an aud corresponding to coordinator actor.
  2. Coordinator finds actors that are able to perform task and sends dispatches invocation to them.
  3. Coordinator collects receipts from the selected actors and decides which one to certify by issuing receipt under own authority (sub)

What’s interesting is that even coordination logic like majority wins could be expressed as workflow pipeline itself so coordinator doesn’t even need to wait before issuing receipt (assumes we embrace awaits in output)

@expede
Copy link
Member

expede commented Apr 5, 2024

Executor in majority of cases has no authority over sub(ject) so it shouldn’t be able to produce authorization chain.

Totally: unlike a separate receipt, we're saying that "the thing you're acting on" is yourself, or someone who you're letting basically control you (in that they're making assertions on your behalf). I agree that this works mechanically, but it's counterintuitive to explain and I've found myself getting confused by it. I mentioned in DMs the other day that maybe we can reframe the flow to make this more intuitive.

FWIW I'm currently leaning heavily towards going with this design, but want to make sure that we're not overindexing on one delegated execution pattern.

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

No branches or pull requests

2 participants