Digital Objects Explained
In this document, we explore what digital objects are from a concrete lens, rather than an abstract lens, using the following implementation: https://github.com/dhvanipa/digital-objects-zkvm-poc
Note: There are some important implementation details, which we explore here: Digital Objects via zkVM.
Background
A regular software program can be thought of as being composed of 3 different things:
- Input
- Program Code
- Output
The general flow is that a user feeds the input into the program code which then produces an output.
There is a special type of program called a zero-knowledge program. It is composed of the same things as a regular program, plus a special 4th thing
- Input
- Program Code
- Output
- Proof
The general flow is the same where the user feeds the input into the program, but now the program produces two things: (output, proof).
The proof allows one user (the “prover”) to send the output to another user (the “verifier”) who can then verify that the output came as a result of running the program code, without that user having to run the program themselves and (optionally) without them having to see the inputs. Because the verifier user gains knowledge of the output without seeing the inputs, these programs are typically called “zero-knowledge” (ZK) programs.
Note: The verifier user does not need to trust the prover user as the proof mathematically guarantees that the output could only have come from the program code.
One interesting property of ZK programs is that one of the inputs itself can be a proof (called proof recursion/aggregation)! This allows you to use the computational results of others without having to re-run it or see their inputs. In a regular program, we can also input the output of other programs, but we have to trust that those output bytes came from running the program code.
A zkVM allows us to turn regular programs into zero-knowledge programs. There are many things you can do with ZK programs. Here we create and use digital objects.
Crafting Digital Objects
A digital object can be thought of as digital bytes which have the properties of physical objects, such as uncopyable or consumable.
As an example, we can craft a stone object that is defined as a JSON with the following data:
Stone:
{
"key": "0x",
"inputs": [],
"seed": 1,
"blueprint": "stone",
"work": "0x"
}
In plain terms, a stone object is a data struct that has the following fields:
- a key field which is a hash with a certain amount of difficulty
- a seed field which is the number used to construct the key hash
- 0 inputs
- blueprint identifier string set as “stone”
- a work hash which is the result of running some sort of verifiable delay function (VDF), in this case a simple hasher
We can then create this stone data struct locally and run a ZK program which will create the stone object by tying the data struct with a proof.
When saved as a file, the stone object will look like:
{
"object": {
"key": "1b071481b1bc8bbe449ea4cbc30dd820f7de20f050b0feef5257a5c75595cd7b",
"inputs": [],
"seed": 1730,
"blueprint": "stone"
},
"hash": "00002db892be591eccf289d94321f544f70f4edfbaf6f31cdc64bbb9ceb32728",
"work": "8d53e0811b1d9545def78cd27ca96993dadfb33fc902fcfd94863b3dc478f020",
"proof": {
...
}
}
One additional field we have is the “hash”, which is simply the hash of the object.
As we saw before, a ZK program can take a ZK proof as an input. In other words, we can use existing digital objects as inputs when creating a new digital object.
For example, we can define an axe as the following
{
"key": "0x",
"inputs": [
"0x" (wood hash),
"0x" (stone hash)
],
"seed": 1,
"blueprint": "axe"
}
In plain terms, an axe object is a data struct that has the following fields:
- a key field which is a hash with a certain amount of difficulty
- a seed field which is the number used to construct the key hash
- 2 inputs: 1 wood object and 1 stone object, represented by their hashes
- blueprint identifier string set as “axe”
As before we can then create this axe data struct locally after we have wood and stone and run a ZK program which will create the axe object by tying the data struct with a proof. Note that this time, part of the inputs will be the wood and stone proof!
We can save the axe object to a file, which will look like:
{
"object": {
"key": "6c7d40873d95f28857a5eab4217bf72b06db5fc586d73126bd4a6451c8b1be27",
"inputs": [
"00129b88361bcb9bc31922acbea6c39e833eea665c468e9b9e6b1673abd210a1",
"00002db892be591eccf289d94321f544f70f4edfbaf6f31cdc64bbb9ceb32728"
],
"seed": 7667,
"blueprint": "axe"
},
"hash": "0010fde13863d5d858568ab578dbbf1795c16a1834b92cd0ef7330e7cc5b5bf5",
"work": "0000000000000000000000000000000000000000000000000000000000000000",
"proof": {
...
}
}
Using Digital Objects
This has been a great individual experience so far, but now I want to interact with other users.
We will keep all the objects crafted locally and private, but agree on the following global state:
- a list of created object hashes
- a list of consumed object hashes
This way, we can enforce that when something like an axe object is created, the input wood and stone objects are “consumed” and can’t be used again. Much like what happens when you create a physical axe!
We need a canonical way to update this global state and one in which all users can reach consensus on.
To do this, we will post a proof committing to our objects.
Note: We cannot just post the object proof (ie wood proof/axe proof), because for other users to verify these proofs, they’d need to know which program it was for. And if you told them the proof you’re posting is for a wood program, they’d know you have wood!
To solve this problem, we instead create a ZK program which simply wraps these proofs, called commit. The commit proof takes in individual object proofs, verifies them, and simply outputs the object hashes.
We then post these commit proofs to Ethereum blobs. All the users then run a program called a “synchronizer”, which will continuously:
- Read all blobs at a certain specified address, filtered to the commit proof type
- Verify the commit proof
- Update its local state accordingly
This way, if a malicious user tries to craft an object from input objects that were already consumed, all other users running the synchronizer will simply ignore this object and not add it to their “local” global state.
Conclusion
That’s all this proof of concept does right now! Users can craft some objects, commit them, and have a global view on all created and consumed objects.
Note: There are some important implementation details, which we explore here: Digital Objects via zkVM.
There are still many questions to explore:
- Can I enforce that an object can only pass through certain states like a state machine?
- I can send someone my wood and stone proof, and they can produce an axe object without seeing my private wood and stone objects. But can I allow another user to use my axe only a certain number of times if we added a durability field?
- Can I create an object that is not owned by a single person but self-contains how it can be acted upon? For example, a Rubik’s cube object which anyone can turn but no one can see its state.
- Can we get some properties without consensus? For example, use one-shot quantum signatures to consume items.