Open Gozala opened 5 months ago
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.
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 :100:
"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:
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: []
}
}
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).
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.
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.
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
For more context this is a direction I have been thinking for our ucan agents (client or server doesn't matter):
[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.
sub
is effectively a state of the replicafrom @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.
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.
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
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?
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.
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.
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.
dependency between promise pipelines and receipts
Hmm that's a good point. Do you expect to use pipelining with content claims?
(Specifically: I wouldn't expect that to touch delegations at all)
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)
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
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 https://github.com/ucan-wg/invocation/issues/33#issuecomment-2038332815
/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:
aud
.Expanding on previous thoughts even in compute as a network (IPVM case) described approach makes sense:
aud
corresponding to coordinator actor.In an alternative design where coordinator wants to verify result before issuing receipt it also aligns well.
aud
corresponding to coordinator actor.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)
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.
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 thatsub
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.