Certificate Ocap Ledger HOWTO
This tutorial introduces the basics of how to construct a "ledger" where objects can be inserted into the ledger and access control is enforced through certificate-style object capabilities. It is assumed that anyone can view the ledger’s chain of events, but only entities on the ledger itself can authorize access to themselves.
Object capabilities are an authority mechanism based on authority-by-possession rather than authority-by-identity. Object capabilities can be implemented on a variety of "substrates". In the interest of ease of development and experimentation, this code was written in a mashup of certificate-style object capability system (such as ocap-ld) but implemented on top of a substrate that uses local encapsulation for its integrity. Sorry... if you don’t know Racket and just want to know how to do it with ocap-ld, there is a section that gives the equivalent documents, and bidirectional links to each equivalent subsection.
Originally this document included an introduction to object capabilities as well but the author decided this thing was already way too long. Sorry, the two documents linked in the previous paragraphs contain decent introductions so look at those instead.
1 Just enough Racket to follow along
This document uses the Racket programming language (mainly because the relevant code was written in the author’s off hours to see if all the ideas worked). This is not a full tutorial on the Racket programming language; if you are interested in that, the Racket documentation is very good, and this tutorial is a fun way to get your feet wet. Racket may look a bit different than other programming languages you’ve seen, but it’s frequently used to teach middle schoolers how to program... which means you can probably learn it quickly, too.
It’s recommended that you check out the (associated repository) and play around but obviously not required. If you open a "test.rkt" file in DrRacket in the checkout’s directory and hit "run", you can then enter all the examples in the REPL (and put any data you want to keep in the top part of the program).
Only a few things need to be said about Racket’s syntax. Instead of calling a function like this:
stringAppend("foo", "bar") ;; => "foobar" |
We call it like this:
> (string-append "foo" "bar") "foobar"
These are the main core types we’ll be using:
42 ; numbers (list 1 2 3) ; linked lists '(1 2 3) ; same as above but quoted "foo" ; strings 'foo ; symbols #hasheq((name . "bob") ; immutable hash tables (drinks . "tea")) #t #f ; booleans (true/false)
Defining a variable uses define:
> (define my-name "Alice") > my-name "Alice"
Defining and then invoking a function also uses define, but notice how the function name is wrapped in parentheses:
> (define (greet name) (string-append "Hello " name "!")) > (greet my-name) "Hello Alice!"
Some functions can return multiple values, which we can capture with define-values:
> (define (an-animal-and-noise) (values 'pig 'oink))
> (define-values (animal noise) (an-animal-and-noise)) > animal 'pig
> noise 'oink
Finally, in the long history of lisps there is an idea that code and data are not so far apart. As demonstration of this, we can "quote" an expression, and we’ll be able to reference that expression as data instead of code:
> (+ 2 2) 4
> '(+ 2 2) '(+ 2 2)
Finally there’s a language feature called "quasiquoting" which uses a backtick instead of an apostrophe. Quasiquoting is really a templating language of sorts, which allows us to switch between code and data very easily by "unquoting" with a comma.
> `(evaluating (+ 2 2) gives us ,(+ 2 2)) '(evaluating (+ 2 2) gives us 4)
This is especially frequently combined with the hash table syntax, mentioned above.
Okay, that’s all you need to know for Racket syntax for this tutorial!
2 Let’s get into it
First of all, we’ll want to import a few things:
> (require "ocap-certs.rkt" "ledger.rkt")
2.1 Creating the root of the ledger
See here for ocap-ld examples
On our ledger chain, our ledger itself will have representation as a slightly-special document. Like everything else, it needs a way to delegate capabilities related to itself, so it’ll need a keypair too.
> (define-values (ledger-privkey ledger-pubkey) (new-key-pair 'ledger))
> (define ledger-doc `#hasheq((type . (ledger)) (name . "ledger root") (delegate-key . ,ledger-pubkey)))
Now to create the chain. Our chain is going to be a linked list, for simplicity’s sake. By making it a linked list, we’re leaving out an important detail. While the validity each invocation on this chain is secured, we haven’t enforced an ordering of events, which could lead to malicious reordering. A real ledger would have each item point at the previous "commit" on the chain, but for the simplicity of printing things to screen, we’re just using a linked list. Relatedly, a distributed ledger in a mutually suspicious environment would also have to integrate consensus. Every ledger needs to start somewhere, and our ledger needs a genesis block. Here’s ours:
> (define minimalist-chain (list (genesis ledger-doc basic-ledger-actions basic-objekt-actions)))
The root object on our chain is wrapped in a special genesis structure which holds the document that identifies the ledger itself, actions (ie "methods") that can be used to update the ledger, and actions that can be used so that objects may update themselves. (This can be updated too over time, so we can change the expected behavior of how our ledger operates.) We’ll worry about actions later.
This doesn’t tell us anything about the actual state that’s been generated from our ledger though... we have to process the ledger to get.
> (define minimal-ledger (process-whole-chain minimalist-chain)) > minimal-ledger #<ledger>
Okay, so this is a structure that represents the "state of the ledger" and the objects on it, as well as what actions are "active". We need some sort of identifier / key to fetch the latest version of an object. It would be sensible to generate the identifier based off something identifying the initial object on the chain. We could take the hash of the object’s delegation key for instance, though this adds some complexity (we will look at how to do deal with that later) but a much easier option would be to simply normalize and hash the initial object version and use *that* as the key.
However, this toy ledger is just being done in memory so we will cheat and use the initial object itself as the key (the hash table we are using indexes by whether the object points to the same object in memory, rather than an equivalent one).
There’s only one object our ledger’s state so far, so we might as well extract it:
> (ledger-ref-doc minimal-ledger ledger-doc)
'#hasheq((delegate-key . #<pubkey ledger>)
(name . "ledger root")
(type . (ledger)))
Hm... well we haven’t made any changes yet! So this is just the ledger document as it initially stood. Actually there’s one more bit of information we’re not showing... an object can store metadata state that isn’t represented on the published object but may be used to change behavior of future object invocations. Use ledger-ref to seet he full structure.
2.2 Adding another object to the ledger
See here for ocap-ld examples
It’s silly to have a ledger with only a ledger document on it. This is a toy ledger, so why not add a toy object:
> (define-values (spaceman-privkey spaceman-pubkey) (new-key-pair 'spaceman))
> (define spaceman-doc `#hasheq((type . (toy)) (name . "Gus Lightwave") (catchphrase . "Infinity... the final frontier!") (delegate-key . ,spaceman-pubkey)))
Now we need to make an invocation against the ledger to put our spaceman on the ledger. We’re going to cheat on this first one... an object can always invoke itself as a target. So the ledger will add this one.
> (define add-spaceman-capinv (make-invocation ledger-doc 'register-doc `#hasheq((document . ,spaceman-doc)) ledger-privkey ledger-pubkey))
The first argument to make-invocation is the capability we’re invoking... in this case, the target is always a capability to its own delegate-keys, so we’re letting the ledger invoke the ledger. The second argument is the action we want to invoke... in this case, we want to register the document, so it’s ’register-doc. Next is the arguments; this action takes one arggument, the ’document being added. Finally the private and public keys that have authority to invoke this capability.
Let’s take a look at the generated capability invocation document:
> add-spaceman-capinv
'#hasheq((document
.
#hasheq((catchphrase . "Infinity... the final frontier!")
(delegate-key . #<pubkey spaceman>)
(name . "Gus Lightwave")
(type . (toy))))
(proof
.
#hasheq((capability
.
#hasheq((delegate-key . #<pubkey ledger>)
(name . "ledger root")
(type . (ledger))))
(creator . #<pubkey ledger>)
(proofPurpose . cap-invoke)
(sig . #<signature by ledger>)))
(type . register-doc))
Looks about right... let’s make an updated version of the ledger with this item applied:
> (define ledger-with-spaceman (ledger-update minimal-ledger add-spaceman-capinv)) > (ledger-ref-doc ledger-with-spaceman spaceman-doc)
'#hasheq((catchphrase . "Infinity... the final frontier!")
(delegate-key . #<pubkey spaceman>)
(name . "Gus Lightwave")
(type . (toy)))
Sweet, that looks right.
2.3 Paying to get on the ledger
See here for ocap-ld examples
One problem though... currently only the ledger can add items to itself. Mr. Tomato Head thinks this looks fun and would like to join the ledger:
> (define-values (tomato-delegate-privkey tomato-delegate-pubkey) (new-key-pair 'tomato-delegate))
> (define-values (tomato-autograph-privkey tomato-autograph-pubkey) (new-key-pair 'tomato-autograph))
> (define tomato-head-doc `#hasheq((name . "Tomato Head") (catchphrase . "That's MISTER Tomato Head to you!") (delegate-key . ,tomato-delegate-pubkey) (autograph-key . ,tomato-autograph-pubkey)))
One problem: Mr. Tomato Head doesn’t have access to and doesn’t know anyone with access to the ledger’s private key. But what Mr. Tomato Head has is money. It turns out we can implement money using object capabilities. Money has to come from somewhere, and here’s the mint that issues blox bucks:
> (require "money.rkt")
> (define blox-mint (make-mint 'blox))
And Mr. Tomato Head’s purse is lined with cash:
> (define tomato-purse (send blox-mint make-purse 10000))
As it turns out there’s an excellent opportunity to put that money to use. One way or another a public ledger must keep itself from being spammed and have a way of distributing the upkeep of being run. One approach is a proof-of-work type system, and for now we’ll imagine we’ve added that. Supporting proof of work could be done by having more flexibility in what kinds of proofs we accept than just signatures. We could allow a proof type that involves completing some proof of work, and then delegate a capability to that abstract proof of work specification. Another approach is to introduce "accelerators", entities which have been granted authority to be able to put things on the ledger. Let’s make one:
> (define-values (accelerator-privkey accelerator-pubkey) (new-key-pair 'accelerator))
> (define an-accelerator (new accelerator% [public-key accelerator-pubkey] [private-key accelerator-privkey] [purse (send blox-mint make-purse 0)] [register-cost 15] [invoke-cost 10] [register-doc-cap (cap-delegate ledger-doc ledger-privkey ledger-pubkey (list accelerator-pubkey) #:caveats `(#hasheq((type . action) (action . (register-doc)))))] [post-invocation-cap (cap-delegate ledger-doc ledger-privkey ledger-pubkey (list accelerator-pubkey) #:caveats `(#hasheq((type . action) (action . (post-invocation)))))]))
A few things to note from the above... our accelerator also has a purse, for one. For two, our accelerator has been delegated two object capabilities... one with the caveat that it can only register documents and the other with the caveat that it can only post invocations. (These could also be combined into one caveat listing either action within the caveat.) If there are other ledger actions, such as ones that allow you to update the ledger rules, this particular accelerator doesn’t have access to them.
One interesting thing is that the accelerator is not actually on the ledger itself and does not need to be (though we could put them or a document containing their public keys on the ledger if we wanted to be somewhat meta-circular). This accelerator is an actor and lives Somewhere (TM) but where that is doesn’t particularly matter as long as we can send it messages somehow. We could do so over HTTP or whatever other compatible protocol. But since this is all in memory, we’ll just send it a message using Racket’s send expression:
> (send an-accelerator get-balance) 0
So our accelerator friend here has capabilities to post to the ledger, but no money. Our Mr Tomato Head friend has money, but no capability to post to the ledger. Thankfully an exchange can be made.
> (define register-tomato-payment (send tomato-purse sprout)) > (send register-tomato-payment deposit 15 tomato-purse)
Our shiny red friend doesn’t want to just hand over their entire purse and let the accelerator take whatever they want (who do we look like, credit card payment infrastructure designers?), so they make a one-off purse to contain just the money necessary for the transaction. Now to send to the accelerator:
> (define register-tomato-cap (send an-accelerator buy-register-cap tomato-delegate-pubkey tomato-head-doc register-tomato-payment))
Success! And indeed, the payment wallet is empty while the accelerator’s wallet finally has some cash in it:
> (send register-tomato-payment get-balance) 0
> (send an-accelerator get-balance) 15
The accelerator sent us back that capability, so let’s look at it in a bit more detail:
> register-tomato-cap
'#hasheq((caveats
.
(#hasheq((action . (register-doc)) (type . action))
#hasheq((field . document)
(type . require-value)
(value
.
#hasheq((autograph-key . #<pubkey tomato-autograph>)
(catchphrase . "That's MISTER Tomato Head to you!")
(delegate-key . #<pubkey tomato-delegate>)
(name . "Tomato Head"))))))
(invokers . (#<pubkey tomato-delegate>))
(parent-capability
.
#hasheq((caveats
.
(#hasheq((action . (register-doc)) (type . action))))
(invokers . (#<pubkey accelerator>))
(parent-capability
.
#hasheq((delegate-key . #<pubkey ledger>)
(name . "ledger root")
(type . (ledger))))
(proof
.
#hasheq((creator . #<pubkey ledger>)
(proofPurpose . cap-delegate)
(sig . #<signature by ledger>)))))
(proof
.
#hasheq((creator . #<pubkey accelerator>)
(proofPurpose . cap-delegate)
(sig . #<signature by accelerator>))))
Hoo, that’s quite an eyeful! That’s because there are actually three capability documents nested together here forming the capability chain (not to be confused with the ledger chain) our tomato friend can enact on. If we follow the parentCapability field inward we’ll notice that the innermost capability is the target itself. One layer out from there we see a document where the ledger had delegated to the accelerator authority to invoke the ledger, with the caveat that invocations must apply to the ’register-doc action. At the outermost layer we see that the accelerator then delegates to Mr. Tomato Head, but with the caveats that invocations must apply to the ’register-doc action (a duplicate of the previous caveat, but it doesn’t hurt) and that the field ’document must have the value that is precisely the same document Tomato Head paid to register. This latter one prevents Mr. Tomato Head from reusing this invocation to post more documents. Posting the normalized hash of the expected document would be another acceptable route for a production system, but strictly speaking is an optimization.
Okay, now Tomato Head needs to make an invocation using this capability and post it to the ledger.
> (define register-tomato-capinv (make-invocation register-tomato-cap 'register-doc `#hasheq((document . ,tomato-head-doc)) tomato-delegate-privkey tomato-delegate-pubkey))
> (define ledger-with-tomato-head (ledger-update ledger-with-spaceman register-tomato-capinv)) > (ledger-ref-doc ledger-with-tomato-head tomato-head-doc)
'#hasheq((autograph-key . #<pubkey tomato-autograph>)
(catchphrase . "That's MISTER Tomato Head to you!")
(delegate-key . #<pubkey tomato-delegate>)
(name . "Tomato Head"))
Okay, looks like it works and our Tomato friend is on the ledger!
2.4 An object updates itself on the ledger
See here for ocap-ld examples
Some time passes, and Tomato Head receives an honorary PhD for his fine acting. Tomato Head wants everyone to know about this and so he wants to make an update to his catchphrase. He constructs the following invocation:
> (define doctor-tomato-capinv (make-invocation tomato-head-doc 'update-field #hasheq((field . catchphrase) (value . "That's DOCTOR Tomato Head to you!")) tomato-delegate-privkey tomato-delegate-pubkey))
Except Tomato Head doesn’t have permission to update the ledger with this invocation. So we’re going to have to – hold on to your hats – get a capability to make an invocation that embeds another invocation. As a side note to the ocap-ld aware, this is why framed representation of ocap invocations in ocap-ld is critical. If an invocation embeds an invocation, we need to know which one is the root. Conversion to graph soup is a lossy operation.
We can do that by paying the accelerator again, but this time asking for a capability to post an invocation rather than one to register a document.
> (define update-tomato-payment (send tomato-purse sprout)) > (send update-tomato-payment deposit 10 tomato-purse)
> (define post-update-tomato-cap (send an-accelerator buy-post-invoke-cap tomato-delegate-pubkey doctor-tomato-capinv update-tomato-payment))
> (define post-update-tomato-capinv (make-invocation post-update-tomato-cap 'post-invocation `#hasheq( (invocation . ,doctor-tomato-capinv)) tomato-delegate-privkey tomato-delegate-pubkey))
Want to see this whole thing with an invoctation that embeds an invocation? It’s a doozy: Yo dawg I heard you like capabilities so I put a capability in your capability so you can invoke while you invoke
> post-update-tomato-capinv
'#hasheq((invocation
.
#hasheq((field . catchphrase)
(proof
.
#hasheq((capability
.
#hasheq((autograph-key
.
#<pubkey tomato-autograph>)
(catchphrase
.
"That's MISTER Tomato Head to you!")
(delegate-key . #<pubkey tomato-delegate>)
(name . "Tomato Head")))
(creator . #<pubkey tomato-delegate>)
(proofPurpose . cap-invoke)
(sig . #<signature by tomato-delegate>)))
(type . update-field)
(value . "That's DOCTOR Tomato Head to you!")))
(proof
.
#hasheq((capability
.
#hasheq((caveats
.
(#hasheq((action . (post-invocation))
(type . action))
#hasheq((field . invocation)
(type . require-value)
(value
.
#hasheq((field . catchphrase)
(proof
.
#hasheq((capability
.
#hasheq((autograph-key
.
#<pubkey tomato-autograph>)
(catchphrase
.
"That's MISTER Tomato Head to you!")
(delegate-key
.
#<pubkey tomato-delegate>)
(name
.
"Tomato Head")))
(creator
.
#<pubkey tomato-delegate>)
(proofPurpose
.
cap-invoke)
(sig
.
#<signature by tomato-delegate>)))
(type . update-field)
(value
.
"That's DOCTOR Tomato Head to you!"))))))
(invokers . (#<pubkey tomato-delegate>))
(parent-capability
.
#hasheq((caveats
.
(#hasheq((action . (post-invocation))
(type . action))))
(invokers . (#<pubkey accelerator>))
(parent-capability
.
#hasheq((delegate-key . #<pubkey ledger>)
(name . "ledger root")
(type . (ledger))))
(proof
.
#hasheq((creator . #<pubkey ledger>)
(proofPurpose . cap-delegate)
(sig . #<signature by ledger>)))))
(proof
.
#hasheq((creator . #<pubkey accelerator>)
(proofPurpose . cap-delegate)
(sig . #<signature by accelerator>)))))
(creator . #<pubkey tomato-delegate>)
(proofPurpose . cap-invoke)
(sig . #<signature by tomato-delegate>)))
(type . post-invocation))
Good thing we didn’t have to write out this by hand... it’s nice to have functions that allow us to compose this without having to think too much about the underlying structure, and any reasonable interface should do the same.
Anyway, we can now post that to the ledger and everyone can know what Mr. Tomato’s new catchphrase is:
> (define ledger-with-new-catchphrase (ledger-update ledger-with-tomato-head post-update-tomato-capinv)) > (ledger-ref-doc ledger-with-new-catchphrase tomato-head-doc)
'#hasheq((autograph-key . #<pubkey tomato-autograph>)
(catchphrase . "That's DOCTOR Tomato Head to you!")
(delegate-key . #<pubkey tomato-delegate>)
(name . "Tomato Head"))
Here we saw two distinct types of invocations, composed together:
To update the object: We need to do an invocation against the object itself specifying some method and arguments that allow it to change its state (both the document shown and metadata). This was the role of the basic-objekt-actions behavior passed into the genesis block.
To update the ledger: It’s not enough to just update the object, we need to get that update/invocation on the ledger. This means we need another invocation against the ledger which references the invocation against the object as an argument. However, the ledger needs to protect itself from being overwhelmed with updates, which is why we paid for a capability to post something to the ledger from an accelerator (though again, an abstract capability that permitted proof of work could have also worked).
2.5 Object delegates authority to be updated to another entity
As one final example, Mr. (sorry, Dr.) Tomato Head can delegate the authority to update parts of himself to another entity. Tomato Head’s autographs are very valuable, but he has a tendency of losing his magic signature pen at parties. Because of this, Tomato Head and his agent have agreed that the agent should have authority to update Tomato Head’s autograph-key (but not any other properties) just in case, and gives their agent an emergency fund in case that is necessary.
> (define-values (agent-privkey agent-pubkey) (new-key-pair 'talent-agent))
> (define agent-update-autograph-cap (cap-delegate tomato-head-doc tomato-delegate-privkey tomato-delegate-pubkey (list agent-pubkey) #:caveats `(#hasheq((type . action) (action . (update-field))) #hasheq((type . require-value) (field . field) (value . autograph-key)))))
> (define agent-purse (send tomato-purse sprout)) > (send agent-purse deposit 100 tomato-purse)
Now the next time Tomato Head loses his pen, his agent can quickly replace it with a new one and hand that to Tomato Head later.
> (define-values (tomato-autograph-privkey2 tomato-autograph-pubkey2) (new-key-pair 'tomato-autograph2))
> (define new-autograph-capinv (make-invocation agent-update-autograph-cap 'update-field `#hasheq((field . autograph-key) (value . ,tomato-autograph-pubkey2)) agent-privkey agent-pubkey))
> (define update-autograph-payment (send agent-purse sprout)) > (send update-autograph-payment deposit 10 agent-purse)
> (define post-new-autograph-cap (send an-accelerator buy-post-invoke-cap agent-pubkey new-autograph-capinv update-autograph-payment))
> (define post-new-autograph-capinv (make-invocation post-new-autograph-cap 'post-invocation `#hasheq( (invocation . ,new-autograph-capinv)) agent-privkey agent-pubkey))
> (define ledger-with-new-autograph (ledger-update ledger-with-new-catchphrase post-new-autograph-capinv)) > (ledger-ref-doc ledger-with-new-autograph tomato-head-doc)
'#hasheq((autograph-key . #<pubkey tomato-autograph2>)
(catchphrase . "That's DOCTOR Tomato Head to you!")
(delegate-key . #<pubkey tomato-delegate>)
(name . "Tomato Head"))
Whew, it worked! Note that we did not need to define a document for the agent, and there was no requirement that the agent have a document on the ledger.
That’s it for the examples. But one quick note: we have done each of these updates so far by producing a new ledger state applying the update to the last one. We could instead do this all at once, and indeed in general it is necessary to be able to audit a ledger by applying all updates successively and transforming the internal state. We could so that using the process-whole-chain we used at the begining:
> (define complete-chain (list post-new-autograph-capinv post-update-tomato-capinv register-tomato-capinv add-spaceman-capinv (genesis ledger-doc basic-ledger-actions basic-objekt-actions)))
> (define complete-ledger (process-whole-chain complete-chain)) > (ledger-ref-doc complete-ledger ledger-doc)
'#hasheq((delegate-key . #<pubkey ledger>)
(name . "ledger root")
(type . (ledger)))
> (ledger-ref-doc complete-ledger spaceman-doc)
'#hasheq((catchphrase . "Infinity... the final frontier!")
(delegate-key . #<pubkey spaceman>)
(name . "Gus Lightwave")
(type . (toy)))
> (ledger-ref-doc complete-ledger tomato-head-doc)
'#hasheq((autograph-key . #<pubkey tomato-autograph2>)
(catchphrase . "That's DOCTOR Tomato Head to you!")
(delegate-key . #<pubkey tomato-delegate>)
(name . "Tomato Head"))
2.6 Key takeaways from code examples
Now that we’ve gone through all this, what can we learn from these code examples?
The ledger has a genesis root document and initial set of behaviors for both updates to the ledger structure and for updates to objects.
Each update to the ledger must be an invocation against the ledger object, using one of the specified ledger actions.Not shown: this can include a ledger action to change the behavior of future invocations against actions for the ledger and objects.
One key ledger action is the ability to add objects to the ledger.
Another key ledger action is the ability to post invocations against objects on the ledger. These are then evaluated and applied against the object actions, allowing the object to update "itself". Not shown: allowing different behavior dispatched based on a combination of object + action type.
Targets can delegate capabilities with the option to restrict them via caveats.
Not all participants invoking authority on the ledger need to themselves necessarily be on the ledger.
3 Other object id/key conventions
In the Racket code examples we just used the initial document itself as a key to get the latest object from the ledger or link to a target in a capability document, but in json-ld we want an id. It’s possible that the id is added only on retrieval from the ledger, or is added beforehand, but we should decide on a specific way of generating the id based on the content of the initial objectregardless.
I’d advocate for using a content addressed hash of initial objects on the ledger (including this one) and slap that on as the id. It’s the simplest route, and even if someone puts your key up there, that doesn’t matter because that just means they can’t use the document for anything, and it doesn’t block you from adding another one.
That said, we could use the fingerprint of the capabilityDelegation field to construct the id for the object. This does introduce a race condition we have to account for where a malicious entity wants to "ruin" our document by adding theirs before we can and producing a name collision. One way around this is just to add a proof to the initial document with a specific proofPurpose like so:
{ |
"capabilityDelegation": "<delegation-key-object>", |
"id": "did:foo:<fingerprint-of-delegation-key", |
"name": "Just some DID", |
"proof": [ |
{ |
"creator": "<delegation-key-id/object>", |
"proofPurpose": "thisReallyIsMine", |
"signatureValue": "IOmA4R7TfhkYTYW8...CBMq2/gi25s=", |
"type": "RsaSiagnature2016" |
} |
] |
} |
The initial object has this proof on it, and there you go. Only the person who has control over this key can post this as the initial object with that id. (Implementations still need to check that they don’t do it twice, even though why they’d do that I dunno.)
4 TL;DR, just show me how to do it in ocap-ld
This is an attempt to sketch out the above workflow in ocap-ld. Note that the code below, unlike the code above, has not been tested.
4.1 Creating the root of the ledger, ocap-ld edition
See here for racket / userstory examples
Equivalent ledger-doc:
{ |
"capabilityDelegation": "<ledger-pubkey>", |
"id": "did:foo:<ledger-doc-id>", |
"name": "ledger root", |
"type": "Ledger" |
} |
And stick that on as part of your genesis block of the ledger (along with policy for what behavior is for updating the ledger’s state as well as objects on the ledger).
It might be sensible to just put the policy right on the ledger document.
So maybe that might look like:
{ |
"capabilityDelegation": "<ledger-pubkey>", |
"id": "did:foo:<ledger-doc-id>", |
"ledgerActionPolicy": "urn:sha256:<CAS-id-of-human-readable-policy>", |
"name": "ledger root", |
"objectActionPolicy": "urn:sha256:<CAS-id-of-human-readable-policy>", |
"type": "Ledger" |
} |
Assuming some content-addressed storage system is already used to identify and retrieve documents for this system on, these identifiers could point to human-readable text describing the current policy for all actions that implementors are expected to implement and support. This allows an implementor to switch out their code to the appropriate current ledger behavior depending on this object’s state, and also know if the ledger has moved to new behavior that their code has not yet been set up to support, without actually putting code on the ledger itself.
4.2 Adding another object to the ledger, ocap-ld edition
See here for racket / userstory examples
spaceman-doc:
{ |
"capabilityDelegation": "<spaceman-pubkey>", |
"catchphrase": "Infinity... the final frontier!", |
"id": "did:foo:<spaceman-doc-id>", |
"name": "Gus Lightwave", |
"type": "Toy" |
} |
add-spaceman-capinv:
{ |
"document": { |
"capabilityDelegation": "<spaceman-pubkey>", |
"catchphrase": "Infinity... the final frontier!", |
"id": "did:foo:<spaceman-doc-id>", |
"name": "Gus Lightwave", |
"type": "Toy" |
}, |
"proof": { |
"capability": "did:foo:<ledger-doc-id>", |
"creator": "<ledger-pubkey>", |
"proofPurpose": "capabilityInvocation", |
"signatureValue": "<sig-by-ledger-pubkey>" |
}, |
"type": "CreateLedgerRecordOperation" |
} |
4.3 Paying to get on the ledger, ocap-ld edition
See here for racket / userstory examples
tomato-head-doc:
{ |
"autographKey": "<tomato-autograph-pubkey>", |
"capabilityDelegation": "<tomato-delegate-pubkey>", |
"catchphrase": "That's MISTER Tomato Head to you!", |
"id": "did:foo:<tomato-head-doc-id>", |
"name": "Tomato Head" |
} |
register-tomato-cap ... okay, this one is just really nested, so first let’s look at an oversimplified version:
{ |
"caveat": [ |
{ |
"restrictAction": [ |
"CreateLedgerRecordOperation" |
], |
"type": "RestrictAction" |
}, |
{ |
"requireField": "document", |
"requireValue": "<tomato-head-doc-embedded-here>", |
"type": "RequireValue" |
} |
], |
"invoker": [ |
"<tomato-delegate-pubkey>" |
], |
"parentCapability": "<accelerator-registration-cap-embedded-here>", |
"proof": { |
"creator": "<accelerator-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"signatureValue": "<sig-by-accelerator-pubkey>" |
} |
} |
You’re welcome. Now here’s the real version:
{ |
"caveat": [ |
{ |
"restrictAction": [ |
"CreateLedgerRecordOperation" |
], |
"type": "RestrictAction" |
}, |
{ |
"requireField": "document", |
"requireValue": { |
"autographKey": "<tomato-autograph-pubkey>", |
"capabilityDelegation": "<tomato-delegate-pubkey>", |
"catchphrase": "That's MISTER Tomato Head to you!", |
"id": "did:foo:<tomato-head-doc-id>", |
"name": "Tomato Head" |
}, |
"type": "RequireValue" |
} |
], |
"invoker": [ |
"<tomato-delegate-pubkey>" |
], |
"parentCapability": { |
"caveat": [ |
{ |
"restrictAction": [ |
"CreateLedgerRecordOperation" |
], |
"type": "RestrictAction" |
} |
], |
"invoker": [ |
"<accelerator-pubkey>" |
], |
"parentCapability": "did:foo:<ledger-doc-id>", |
"proof": { |
"creator": "<ledger-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"signatureValue": "<sig-by-ledger-pubkey>" |
} |
}, |
"proof": { |
"creator": "<accelerator-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"signatureValue": "<sig-by-accelerator-pubkey>" |
} |
} |
It’s hard to follow because it’s an invocation of a capability embedded in a delegated capability. A few of these embedded are probably the first time they hit the ledger, so I embedded them.
We don’t show the invocation, but notice how we can only post a very specific document using this capability.
4.4 An object updates itself on the ledger
See here for racket / userstory examples
Below is the "object invocation", and ends up embedded in the "ledger invocation" later. This is the thing Tomato Head *wants* to post to the ledger, updating himself.
doctor-tomato-capinv:
{ |
"proof": { |
"capability": "did:foo:<tomato-head-doc-id>", |
"creator": "<tomato-delegate-pubkey>", |
"proofPurpose": "capabilityInvocation", |
"signatureValue": "<sig-by-tomato-delegate-pubkey>" |
}, |
"type": "UpdateField", |
"updateField": "catchphrase", |
"updateValue": "That's DOCTOR Tomato Head to you!" |
} |
Unfortunately, the final post-update-tomato-capinv is highly, highly nested, and a certain amount of that is probably what realistically things will look like in a production system, since a number of these objects wouldn’t have been previously seen by the ledger. Let’s look at it piece by piece and then bring it together.
This is the simplified version of post-update-tomato-capinv:
{ |
"ledgerPostInvocation": "EMBED: <doctor-tomato-capinv>", |
"proof": { |
"capability": "EMBED: <post-update-tomato-cap>", |
"creator": "<pubkey-tomato-delegate>", |
"proofPurpose": "capabilityInvocation", |
"signatureValue": "<sig-by-tomato-delegate>" |
}, |
"type": "PostLedgerInvocationOperation" |
} |
This is the invocation against the ledger being made by Mr. Potato Head to update himself. He wants to post the value of ledgerPostInvocation, which we’ve seen as doctor-tomato-capinv above. To do so, he uses the capability he paid for from the ledger. Let’s look at that next.
Here’s post-update-tomato-cap, simplified:
{ |
"caveats": [ |
{ |
"restrictAction": [ |
"PostLedgerInvocationOperation" |
], |
"type": "RestrictAction" |
}, |
{ |
"requireField": "ledgerPostInvocation", |
"requireValue": "EMBED: <doctor-tomato-capinv>", |
"type": "RequireValue" |
} |
], |
"invokers": [ |
"<tomato-delegate-pubkey>" |
], |
"parent-capability": "EMBED: <accelerator-post-invocation-cap>", |
"proof": { |
"creator": "<accelerator-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"sig": "<sig-by-accelerator>" |
} |
} |
This is the capability granted to Tomato Head from the accelerator that Tomato Head paid for. We see two caveats: that this can only be used for a "PostLedgerInvocationOperation" action (invocation type) and that this can only be used to post a very specific object invocation to the ledger... that is to say, doctor-tomato-capinv which again we introduced at the top of this section.
The accelerator is granting authority from their capability they got from the ledger, so we should see that. Here is accelerator-post-invocation-cap:
{ |
"caveat": [ |
{ |
"restrictAction": [ |
"PostLedgerInvocationOperation" |
], |
"type": "RestrictAction" |
} |
], |
"invoker": [ |
"<accelerator-pubkey>" |
], |
"parentCapability": "did:foo:<ledger-doc-id>", |
"proof": { |
"creator": "<ledger-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"signatureValue": "<sig-by-ledger-pubkey>" |
} |
} |
Again, the accelerator does not strictly need to be on the ledger (though their keys could be); the workflow in the user story in the Racket section explains why in more detail, but in short, once the ledger grants them a capability they just hold onto it until they need to delegate it to someone else, and it only needs to show up on-ledger once an invocation happens. However, it is likely to show up many times, so embedding it every time would be a waste. This presents another strong argument for having DID documents be named after their content-addressed hash rather than their capabilityDelegation key; we can put any object on the ledger then, including ones without a capabilityDelegation key, such as a capability document itself. This way the ledger can store documents which are not themselves candidates to be updated, but which are valuable to its operation.
Okay. Now that we’ve seen all the pieces, let’s see it all together. Here’s post-update-tomato-capinv in its full glory:
{ |
"ledgerPostInvocation": { |
"proof": { |
"capability": "did:foo:<tomato-head-doc-id>", |
"creator": "<tomato-delegate-pubkey>", |
"proofPurpose": "capabilityInvocation", |
"signatureValue": "<sig-by-tomato-delegate-pubkey>" |
}, |
"type": "UpdateField", |
"updateField": "catchphrase", |
"updateValue": "That's DOCTOR Tomato Head to you!" |
}, |
"proof": { |
"capability": { |
"caveats": [ |
{ |
"restrictAction": [ |
"PostLedgerInvocationOperation" |
], |
"type": "RestrictAction" |
}, |
{ |
"requireField": "ledgerPostInvocation", |
"requireValue": { |
"proof": { |
"capability": "did:foo:<tomato-head-doc-id>", |
"creator": "<tomato-delegate-pubkey>", |
"proofPurpose": "capabilityInvocation", |
"signatureValue": "<sig-by-tomato-delegate-pubkey>" |
}, |
"type": "UpdateField", |
"updateField": "catchphrase", |
"updateValue": "That's DOCTOR Tomato Head to you!" |
}, |
"type": "RequireValue" |
} |
], |
"invokers": [ |
"<tomato-delegate-pubkey>" |
], |
"parent-capability": { |
"caveat": [ |
{ |
"restrictAction": [ |
"PostLedgerInvocationOperation" |
], |
"type": "RestrictAction" |
} |
], |
"invoker": [ |
"<accelerator-pubkey>" |
], |
"parentCapability": "did:foo:<ledger-doc-id>", |
"proof": { |
"creator": "<ledger-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"signatureValue": "<sig-by-ledger-pubkey>" |
} |
}, |
"proof": { |
"creator": "<accelerator-pubkey>", |
"proofPurpose": "capabilityDelegation", |
"sig": "<sig-by-accelerator>" |
} |
}, |
"creator": "<pubkey-tomato-delegate>", |
"proofPurpose": "capabilityInvocation", |
"signatureValue": "<sig-by-tomato-delegate>" |
}, |
"type": "PostLedgerInvocationOperation" |
} |
Whew!