The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 when, and only when, they appear in all capitals, as shown here.
This specification describes a mechanism for extending UCAN Invocations with distributed promise pipelines.
Machines grow faster and memories grow larger. But the speed of light is constant and New York is not getting any closer to Tokyo. As hardware continues to improve, the latency barrier between distant machines will increasingly dominate the performance of distributed computation. When distributed computational steps require unnecessary round trips, compositions of these steps can cause unnecessary cascading sequences of round trips.
A promise is a deferred value that waits on the completion of some function. In effect it says "when that function completes, take the output and substitute it here". Distributed promises do the same, but unlike the familiar async/await
of languages like JavaScript, MAY reference any already running computation, even from other programs. In effect, this allows a significant reduction in latency, and reduces the requirement that all nodes be online to respond to results and dispatch new invocations.
This of course requires a global namespace. Luckily, UCAN Invocation already has globally-unique identifiers for every Action.
Indexing the output of a function by its inputs is called "input addressing". By comparison, "content addressing" acts on static data1.
An Action Identifier (ActID) is the content address of an Action. It can be found direction in an Invocation:
// Pseudocode
const actId = invocation.inv.run.act.asCid()
A Receipt MAY have multiple input addresses. For instance, if an Action contains a promise versus when it's fully reified, the associated Receipt is the same.
If an Action is run multiple times, an ActID MAY refer to many Receipts. Actions SHOULD be fully qualified, and include a unique nonce if the Action is non-idempotent. This ensures that any (correctly run) Receipts for the same ActID will have the same output value.
Input addressing plays nicely as a global memoization table. Since it maps a hash of the inputs to the outputs, someone with access to the cache can pull out values by their input address, and skip re-running potentially expensive computations.
The semantics of invocations say the same with round trips and promises. Here is an example of delegation, invocation, and promise pipelining to show how these relate:
sequenceDiagram
participant Alice πΎ
participant Bob
participant Carol π§
participant Dan
autonumber
Note over Alice πΎ, Dan: Delegation Setup
Alice πΎ -->> Bob: Delegate<Read from Alice's DB>
Bob -->> Carol π§: Delegate<Read from Alice's DB>
Carol π§ -->> Dan: Delegate<Read from Alice's DB>
Carol π§ -->> Dan: Delegate<Send email as Carol>
Note over Alice πΎ, Dan: Synchronous Invocation Flow
Dan ->> Alice πΎ: Read from Alice's DB!
Alice πΎ -->> Dan: Result<β> = "hello"
Dan ->> Carol π§: Send email containing "hello" as Carol!
Carol π§ ->> Carol π§: Send email containing "hello" as Carol!
Note over Alice πΎ, Dan: Async Promise Pipeline Flow
Dan ->> Alice πΎ: Read from Alice's DB!
par Promise
Dan ->> Carol π§: Send email containing Result<β> as Carol!
and Result
Alice πΎ -->> Carol π§: Result<β> = "hello"
end
Carol π§ ->> Carol π§: Send email containing "hello" as Carol!
A Promise is encoded as a map with a single field (the tag) which selects for the branch, and the CID of the relevant Task. Because Tasks uniquely identify their output and MAY be replicated across multiple trustless providers, referencing the entire UCAN Invocation would over-specify the Result.
It has several variants:
Tag | Type | Description |
---|---|---|
await/* |
&Action |
Await any branch |
await/ok |
&Action |
Await an ok branch of a Result, and inline the unwrapped value |
await/error |
&Action |
Await an error branch of a Result, and inline the unwrapped value |
Here are a few examples:
// In isolation
{"await/*": {"/": "bafkr4ig4o5mwufavfewt4jurycn7g7dby2tcwg5q2ii2y6idnwguoyeruq"}}
{"await/ok": {"/": "bafkr4ig4o5mwufavfewt4jurycn7g7dby2tcwg5q2ii2y6idnwguoyeruq"}}
{"await/error": {"/": "bafkr4ig4o5mwufavfewt4jurycn7g7dby2tcwg5q2ii2y6idnwguoyeruq"}}
// In situ
{
"sig": {"/": {bytes: "7aEDQIscUKVuAIB2Yj6jdX5ru9OcnQLxLutvHPjeMD3pbtHIoErFpo7OoC79Oe2ShgQMLbo2e6dvHh9scqHKEOmieA0"}},
"inv": {
"iss": "did:web:example.com",
"aud": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
"run": cid({
"act": cid({
"nnc": "246910121416"
"cmd": "msg/send",
"arg": {
"from": "alice@example.com",
"to": [
"bob@example.com",
"carol@example.com",
{"await/ok": {"/": "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam"}}
// βββββ¬βββββ ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
// Branch Selector ActID
]
}
}),
"mta": {},
"prf": [{"/": "bafkr4iblvgvkmqt46imsmwqkjs7p6wmpswak2p5hlpagl2htiox272xyy4"}]
})
}
}
Using a shared cache2, many cooperating processes can collaborate on multiple separate goals while reusing each others results. The exact mechanism is left to the implementation, but pubsub, gossip, and DHTs are all viable.
The Executor MUST extract the Result from a resolved Receipt, and attempt to match on the tag. If the match passes or fails branch selection, the behavior is as described below.
If the Promise uses the await/*
tag, then any branch MUST be accepted, and the entire Result (including the ok
or error
tag) MUST be substituted. For example:
// Pseudocode
const promised = {
"nnc": "0123456789AB"
"cmd": "msg/send",
"arg": {
"to": "alice@example.com",
"message": {"await/*": {"/": "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam"}}
}
}
returnedReceipt.receipt = {"ok": "hello"}
// ββββββββ¬βββββββ
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
promised.resolve(result, "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam") === { // β
"nnc": "0123456789AB" // β
"cmd": "msg/send", // β
"arg": { // β
"to": "alice@example.com", // β
"message": {"ok": "hello"} // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
}
}
If the Promise uses an await/ok
or await/error
tag, then it MUST only match on Results that match the relevant tag. The inner value MUST be extracted from the outer ok
or error
map and substituted. Extending our earlier example:
// Pseudocode
const promised = {
"nnc": "0123456789AB"
"cmd": "msg/send",
"arg": {
"to": "alice@example.com",
"message": {"await/ok": {"/": "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam"}}
} // β²
} // ββYESββ
// ββ΄ββ
const result = {"ok": "hello"}
// ββββ¬βββ
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
promised.resolve(result, "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam") === { // β
"nnc": "0123456789AB", // β
"cmd": "msg/send", // β
"arg": { // β
"to": "alice@example.com", // β
"message": "hello" // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
}
}
If the branch from the Result doesn't match the branch selector, the Invocation that contains the Promise MUST return an error
Result in its own Receipt.
// Pseudocode
const promised = {
"nnc": "0123456789AB"
"cmd": "msg/send",
"arg": {
"to": "alice@example.com",
"message": {"await/ok": {"/": "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam"}}
} // β²
} // βββNOββββ
// ββββ΄βββ
returnedReceipt.result === {"error": "Divided by zero"}
newReceipt === {
"out": {
"error": {
"reason": "branch mismatch",
"expected": "ok",
"got": "error",
"from": returnedReceipt.cid
}
},
// ...
}
Note that this can also happen when matching on the error
branch:
// Pseudocode
const promised = {
"nnc": "0123456789AB"
"cmd": "log/push",
"arg": {
"msg": {"await/error": {"/": "bafkr4ie7m464donhksutmfqsyqzgcrqhzi2vc5ygiw3ajkhuz6lulnbjam"}}
}
}
returnedReceipt.receipt = {"ok": "hello"}
newReceipt === {
"out": {
"error": {
"reason": "branch mismatch",
"expected": "error",
"got": "ok",
"from": returnedReceipt.cid
}
},
// ...
}
The Capability Transport Protocol (CapTP) is one of the most influential object-capability systems, and forms the basis for much of the rest of the items on this list.
The Object Capability Network (OCapN) protocol extends CapTP with a generalized networking layer. It has implementations from the Spritely Institute and Agoric. At time of writing, it is in the process of being standardized.
Cap 'n Proto RPC is an influential RPC framework based on concepts from CapTP. Their website include much text expounding the benefits of promise pipelining.
Many thanks to Mark Miller for his trail blazing work on capability systems.
Thanks to Philipp KrΓΌger for the enthusiastic feedback on the overall design and encoding.
Thanks to Christine Lemmer-Webber for the many conversations about capability systems and the programming models that they enable.
Footnotes
-
Content addressing can be seen as a special case of input addressing for the identity function. β©
-
Sometimes called a blackboard β©