Edit after writing this: it could be structured more clearly, but I’m going to bed.
I ended up finding my way back here, after revisiting keys.
I was looking for a way to enable users to cycle their keys.
This might be a long post.
Currently, the contract tree looks something like this beauty:
We made a recent change to keys (which itself is temporary) where:
partial_address := hash(salt, contract_code, constructor_hash)
address := hash(public_key, partial_address)
(where presumably contract_code
is the function tree root?)
In this temporary scheme, the public_key
is being used for encryption and decryption.
A question I’ve been asking myself is: “How could we rotate this public key?”. This question is very closely related to the quesetion “How can we swap a contract’s code, similar to SETCODE?”. Many of the problems relating to these questions have been discussed in the thread above.
One problem is that with the current address derivation, both the contract_code
and the public_key
are hashed into the contract address’s preimage. If we want to be able to change the contract_code
and the public_key
, they probably shouldn’t be hashed into the contract address, because we want the contract address to stay the same across updates.
Another problem is if we make the contract_code
(and/or the public_key
) updatable, it becomes tricky to prove from a private function that you’re using the “current” code (or key), as opposed to some outdated version. Most attempts to prove that the value is ‘current’ result in significant information leakage. A “slow updates tree” ideas might enable private reads of mutable information, but such a tree has downsides in this context.
Some options (there could be more I haven’t thought of):
Options for updating contract code might be:
- Modifying the contract’s code in the contract tree (like a SETCODE opcode)
- A proxy pattern using delegatecalls
- Store the contract’s code in the contract’s storage space
- Designing contract classes & instances, and designing a way of updating the class of a contract instance.
Options for updating the public keys might be:
- Store the public key in contract storage (in the slow updates tree)
- Store the public key as metadata in the contract tree (provided we can actually modify the contract’s code in the contract tree).
Options for modifying the contract’s code in the contract tree.
Store Contract Info in an Account-based model
Spoiler: this doesn’t work.
This doesn’t work though, because in order to check the correctness of a function’s bytecode (or of the public key), you need to prove existence of the bytecode in the function tree, and of this leaf_value in this account-based tree. But the ‘current state’ of an account-based tree is only known by the sequencer, so you’d need to reveal which contract you’re executing, in order to execute it!
Store Contract Info in a UTXO/Nullifier-based model
Spoiler: this doesn’t work.
We already store contract info in a UTXO-based, append-only contract tree. It’s just that contracts can’t be updated in the current model. What if contract info could be updated by emitting a nullifier, and pushing new contract info to the next available leaf in the tree?
Note: a nullifier is needed, rather than modifying the old leaf to label it as “old”, because the act of labelling the old leaf as “old” would invalidate the tree root for all in-flight private transactions. It’s one of the reasons nullifiers became a thing in the first place.
This doesn’t work though, because in order to check the correctness of a function’s bytecode (or of the public key), you need to prove existence of the bytecode in the function tree, and of this leaf_value in this utxo-based tree. But in order to show that this leaf is the “current” leaf for this contract address, you’d need to prove that this leaf hasn’t already been nullified. The way to prove something hasn’t been nullified is to reveal its nullifier, so that non-membership of the nullifier can be checked by the sequencer. But this nullifier (always the same nullifier for each stable period of contract bytecode) would be revealed every time a function of the contract is executed. This means everyone would be able to group transactions by this nullifier, effectively grouping each transaction by the contracts they came from. If the exact contract for a given nullifier were ever leaked, then the entire history of the timing of the contract’s execution would be revealed. And of course for any contract logic which is intended to be executed by anyone in the world, the nullifier would need to be derivable by anyone in the world. And so, the property of function privacy would be completely destroyed.
Use a “Slow Updates”-style-tree to store contract info
No picture this time.
Spoiler: this kind-of works.
With this, the latest contract bytecode could be read during each of the slow update tree’s “epochs” from both the private kernel and the public kernel. The bytecode (and/or public keys) could be changed at most once per epoch.
This works but it has downsides:
- How do we deal with new contract deployments or new public keys? Would these have to be enqueued and added to the tree at the start of the next epoch? That wouldn’t be ideal: users would have to wait before doing things on the network.
- Speedy updates to bytecode or public keys wouldn’t be possible: you’d have to wait until the next slow updates tree epoch for changes to take effect.
- The act of updating a public key or updating a contract’s bytecode would be public (since changes to the slow updates tree are public), but that seems acceptable in many cases.
Get rid of the contract tree
@spalladino said recently “Why can’t we just get rid of the contracts tree?”. We all laughed and jeered at him and thought “WHAT?!”. And then we thought for a little while longer and thought “He might just have something here…!”
What if instead of storing contract data in the contract tree, we just store it in the preimage of the contract’s address and (depending on how/if we do ‘upgradable bytecode and/or public keys’) in the contract’s storage.
What’s the contract tree good for?
- If a contract exists as a leaf in the contract tree, it enables transactors to prove that the code they’re executing has actually been deployed. “Deploying” is necessary to:
a. broadcast bytecode to the world;
b. to stake a claim to a contract address; and
c. to execute the constructor function;
d. to prove a relationship between:
- the contract_address;
- the portal contract address;
- the function being executed;
- a user’s public key.
- If a contract exists as a leaf in the contract tree, it proves that the constructor has already been executed.
- A function must only be executed after the constructor is executed. (And as per point 2. the contract tree helps here).
Maybe we don’t need the contract tree for 1, 2 and 3 above…
- .
a. You don’t need the contract tree to exist to broadcast data to the world.
b. You don’t need the contract tree to exist to reserve a contract address. We emit a nullifier of the contract address to achieve this.
c. Executing the constructor function can still be done at the time of deployment. We just wouldn’t push any information to a contract tree.
d. You don’t need the contract tree to exist to bind these things to relate to each-other. An alternative is to bake these things into the preimage of the contract_address, or into the contract’s storage.
- You don’t need the contract tree to exist to demonstrate (to the kernel circuit) that the constructor has been executed. As long as (at the time of deployment) the kernel only allows a contract address nullifier to be emitted if a corresponding* constructor is executed, then the existence of the contract address nullifier is evidence that the constructor function has been executed.
- *The “corresponding” constructor of a contract address nullifier is the constructor (and constructor args) whose
constructor_hash
is hashed into the contract address’s preimage.
- When executing a function, there would be two ways to demonstrate that the constructor (of the contract to which the function belongs) has been executed.
a. prove the existence of the contract address nullifier in the nullifier tree (which can be done privately, because it’s a membership proof and not a non-membership proof);
b. Implement “constructor abstraction” (as I jokingly call it) in the Noir contract. Basically have a state variable is_constructed: bool
which is set to true
by the constructor
function.
So unless I’ve missed something (which happens all the time), we can get rid of the contract tree, as long as:
- We continue to emit a contract_address nullifier;
- The contract_address continues to contain the
constructor_hash
in its preimage
- The contract_address continues to bind together: the
portal_contract_address
, the function_tree_root
and the public_key
. (Alternatively, these values could be bound together via the contract’s storage space).
- Binding these things by hashing them into the preimage of the contract address does not help in our quest to update contract code (the function tree) nor the public key.
- … So, if we chose to get rid of the contract tree, and if we wanted updatable code and public keys, we’d probably need to relate these things to each other by storing them in the contract’s storage space. See the next section.
What are the benefits of getting rid of the contract tree?
- It would be cool.
- It seems possible (with some tradeoffs), and it would reduce the complexity of the world state and the protocol in general.
Storing contract info in the contract’s storage space
Instead of storing contract info in the contract tree, or in the preimage of the contract’s address, the info could be stored in an allotted section contract’s storage space. This would make upgradability of public keys and contract code possible via a proxy pattern, using delegatecalls.
Any constant contract info could be stored in either the private data tree, or the slow updates tree (but not in the public data tree, because the private kernel circuit can’t read from it). Any mutable (updateable) contract info would need to be stored in the slow updates tree (because reading mutable data from the other trees leaks info (see sections above)).
To avoid developers accidentally conflating state variable storage space with “contract info” storage space, the “contract info” storage space could be sensibly domain-separated.
BUT - it’s not very pretty… in fact it might be a terrible suggestion. It’s a conflation of concerns. And it would be costly (in terms of calldata) to emit a log or public state updates informing the world of the new contract code that’s been stored. I’m not actually sure the protocol could support a state update to such a big amount of data as a chunk of bytecode.
How would the kernel even access contract data if it was stored in a contract’s storage space? Would it just be allowed to read the data directly? Would it have to make a call to an unconstrained function (and suffer the king of the hill problem)?
Perhaps the contract tree is there for a good reason…
Proxy pattern via delegatecalls
It’s a simple and well-trodden path… Store a pointer to the contract address (in the slow updates tree) and make delegatecalls to that contract’s code.
This pattern might not help with public key upgradeability, though
Contract Classes and Instances
This is a cool idea, although I’m still not sure of all of the benefits.
I see the benefits as:
- It would reduce the amount of bytecode that’s deployed to the network (because many contract instances could point to already-emitted bytecode (a class)).
- It might simplify the private kernel circuit, because a separate circuit could deal with deployment of a class.
- It’s safer than relying on a delegatecall pattern for code upgradeability, because whilst delegatecalls can execute arbitrary code, a call to a contract instance’s class can only execute one class of bytecode.
If we wanted classes to be updateable (i.e. changing the class that a contract instance points to), this would probably require the slow updates tree to store a hash of the class, so that this class hash can be looked-up privately. Even a delegatecall pattern requires the slow updates tree, to store the “address pointer” that the proxy contract stores.
Options for updating the public keys
Some of these options have been discussed above, as they cross-over with updating contract code.
Store the public key in contract storage (in the slow updates tree)
This was mostly covered above. But I’ll add a few things.
If a contract’s state can only be read by that contract, then every time Contract A wanted to read the public key of some Account Contract B, it would need to make a call to A. This could result in lots of extra kernel iterations. There’s also the king of the hill problem - a modern classic.
I don’t think a contract’s state can be read by another contract, with the current kernel architecture. Perhaps it’s safe for a contract to read another a contract’s public state (incl. slow updates tree state)? – but I know Ethereum doesn’t enable it.
Store the public key as metadata in the contract tree (provided we can actually modify the contract’s code in the contract tree).
Yeah, I guess if we can figure out how to upgrade a contract’s code in the contract tree, we could update the public key via this method.