Contract classes, upgrades, and default accounts

This post puts forward a proposal for handling default account contracts in a full Account Abstraction scenario, relying on contract classes and briefly touching on upgrades as well.

Contract classes vs instances

Let’s recap contract classes vs instances (as defined by Starknet) before jumping into default accounts. The idea of this separation is having contract code (ie “classes”) being a first class citizen in the protocol. In this scheme, you’d first upload your contract code (identified by its hash), and then create contract instances that point to that code. Creating a contract class should be expensive, creating an instance not so much.

We could also use this separation to simplify upgradeability. Upgrading the code for a contract instance now means changing its contract class to a different one. This could be done via an opcode that changes the contract class of the current address (inspired by RSK’s CODEREPLACE). Contracts that are not meant to be upgradeable would just not include that opcode.

Note that, if we also include an opcode similar to Starknet’s library_call, we can probably remove delegate calls from the system. But that’s a topic for another thread.

Default accounts

In an AA world, having a default account implementation allows anyone to interact with a user before they have deployed their account in the system. This gives us a UX similar to Ethereum: I can create an address being offline, and share it with other people who can send funds to me before I send my first transaction.

One option to achieve this is to allow accounts with no code at all. But this means that the AA interface cannot include methods that are required for receiving txs (eg “encrypt this private note that I’m sending you”). We could place the burden of dealing with this in the contract developers, but this would mean that whenever a contract is interacting with an address, it’d need different paths depending on whether there is code there or not (and we know from Ethereum that it’s not good devex). So we’ll rule out this approach, unless the resulting AA interface has no “receiving” methods.

Another option is to enshrine a default implementation at the protocol level. This way, if a contract is interacting with an address that doesn’t have code yet, the protocol falls back to a default implementation. This could work, but settling on a protocol-wide implementation that can serve all default use cases could be tricky (eg what family of keys should we use for authentication?). It also brings app-level concerns down to the protocol, which it’d be best to avoid.

So we want users to be able to choose which account implementation they want to use, but without having to submit a tx to do so. We can do this by encoding the default contract class they want to use as part of their address. This way, an Aztec address is the concatenation of a contract class identifier and an account identifier (possibly derived from a public key?).

Can we compress the contract class identifier for this purpose so addresses can still fit within a single field? Maybe account contract classes need to be registered somewhere and they get an autoincremental id as an alias? Or should we have each address be two field elements at the protocol level, and concatenate them at the UI only?

When interacting with an address, if no contract instance has ever been deployed to it, then the protocol will use the code referenced by the identifier in the address itself. Eventually, the user can upgrade this account to a different one: this would set the contract class pointer in their account, which takes precedence over the one in their address. We can assume that most account contract implementations will have a CODEREPLACE flow to enable this.

As a reference, EIP4337 faces a similar problem when relaying user txs. If the user doesn’t have an account contract yet, they need to supply an initCode as part of the tx, so there’s code to interpret the user operation. This is different from our situation since we’d also need an initCode for a user that receives a tx, but it’s similar in that it shows the need to specify the code for an account contract before it exists.

Nullifier keys and upgrades

While it’s still being defined, it’s possible that the AA interface will include methods for retrieving the nullifying key for that account. However, this key needs to be immutable to prevent double spends. This means that we’ll need to ensure that 1) the function to retrieve this key is pure (doable via introducing a sort of PURECALL opcode), and 2) it remains the same across upgrades (doable by checking function identifiers).

Alternatively, we can keep the nullifying key out of the AA interface, and just derive it from the address at the protocol level. The derivation algorithm could even be another parameter encoded into the address itself, this time restricted to a set of a few options dictated at the protocol level.

79 Likes

When interacting with an address, if no contract instance has ever been deployed to it, then the protocol will use the code referenced by the identifier in the address itself.

It will be hard to check that no contract has been deployed without producing a nullifier. We can’t ask the users to prove it against the latest root. And this can’t be done in a sequencer cause that will leak user’s identity.

An attacker can then create a proof that refers to an old root when my account hasn’t been created or upgraded, and use that old (probably insecure, easily hackable) way to send a transaction on my behalf.

76 Likes

Interesting! This seems to affect upgrades in general, right? If I upgrade my contract instance to use another contract class, someone could still produce a proof against the old contract class. Does it make sense to try and emit a nullifier on every deployment or upgrade then, as a means to invalidate an old contract “version”?

89 Likes

It totally makes sense to emit a nullifier on a deployment or an upgrade! But the issue remains the same: one can create a proof at the time the nullifier doesn’t exist. What we usually do to make sure something hasn’t been nullified is to emit the nullifier. But we can’t do this every time we interact with an account contract.

I really like the idea of being able to upgrade! But (sorry, me continuing to be a downer) I think being able to upgrade the encryption logic will bring more complexities. The Aztec RPC Server will have to know all contract classes an address has ever implemented and try using each one to decrypt data. It’s still doable. And if we can make sure the old classes can’t be referenced anymore (i.e. I can’t send funds to you using the encryption logic defined in your previous account class), then we only need to perform one decryption per chunk of data. But I suspect this will be hard to enforce.

69 Likes

Sounds like encryption keys should be immutable, just like nullifier keys, to avoid this issue? I keep going back to the multisig example though: if the multisig signers change, then we will want to rotate the encryption keys to reflect the new set of signers. Or if the user encryption keys get somehow leaked, they’ll want to switch to a new one, not necessarily having to abandon their whole identity. I guess we could probably rely on heuristics in the wallet software to choose which key to use for decryption, based on the time the note was emitted and the time the keys were rotated.

I’m thinking how proxy-based upgradeability would prevent this issue, and I believe it ends up emitting a nullifier every time the contract is interacted with, right? Every time you call the proxy, you need to read the address of its current implementation, which means reading a note (I guess this note needs to be public rather than encrypted), which requires nullifying it and recreating it. Is this correct?

I’ll keep thinking on this!

Please keep it up. I’d rather have a solution shot down at this stage than after implementing it!

67 Likes

I think if it was a private proxy account contract, then the ‘pointer address’ could be stored privately in a note. So the note could be:

struct PointerAddressNote {
    pointer_address: Field,
    salt: Field,
}

And the custom nullifier derivation for this note could probably be:

fn compute_nullifier(note) -> Field {
    pedersen(note.salt, some_domain_separator);
}

… as long as the function which allows the pointer to be modified also ensures only the owner of the account contract can call this compute_nullifier method.
There are other ways to program it (which is kind of cool how flexible the language will be).

So yes, a nullifier would need to be emitted every time the pointer address was edited, but the nullifier could be salted, to prevent the world from knowing which contract was just executed or which state variable was changed.

Edit: this doesn’t work. Although the address is salted, the salt is the same for every tx which looks-up this ‘pointer address’, so the nullifier would be the same every time the proxy contract is executed, meaning everyone would be able to see “some contract just got executed once again, because I’ve seen that nullifier before!”. If one day info is leaked to connect a particular contract to this nullifier, then the entire history of when that particular contract was executed will be known.

8 Likes

This will be ideal! But it will also be great to enable the use cases like you mentioned, where one needs to update the encryption key/algorithm. Maybe we should settle on having two parts of the encrypted data: enc(head, immutable_pub_key) + enc(body, contract_pub_key).

This way, I only need to decrypt every encrypted header once using my immutable_priv_key to find the txs I’m associated with. I then call my account contract(s) to decrypt the second part.

Only if we can guarantee that once I’ve upgraded my account, no one can use the old way to encrypt data for me, which seems to me is still a very difficult problem to solve.

Think the note can be private. But yeah, that’s one good solution! I’ve actually thought about something similar. But doing this also means that my transactions will have to be included in the rollups in the correct order. Might be a nice add-on if this is what we want to do @Mike ?

7 Likes

Might be a nice add-on if this is what we want to do @Mike ?

With the current spec (without considering account abstraction) we can have proxy contracts, because we have delegatecall functionality. So this is possible.
I do like the ideas Santiago discusses about contract classes and instances, though.

6 Likes

can you expand on the issues of this?

How would all the code (of the selected smart contract wallet) fit in the address field? Will there be some standard implementations that will be enshrined as enums in the protocol? Or how do we identify them?

I do like the concept of receiving funds without having to make any onchain interactions!

5 Likes

I really like the idea of splitting up the problems of finding vs actual decrypting, and tying the header to the address itself (which is immutable). In the msig example, someone ejected from the msig would still be able to identify which notes are directed to it, but won’t be able to decrypt their content, which I think is an acceptable compromise.

Not necessarily. The heuristics I had in mind were something like “when I change my decryption keys, for the next two weeks try with both the old and the new decryption keys on every note”. It’s rare that a tx would be crafted with too old a state (actually, is this assumption correct?), and if it does, the sender could just let the recipient know to adjust the time window for using the old decryption key.

5 Likes

Sure! Let’s say we have an onboarding flow for existing Ethereum users, so we want the account contract to use ECDSA for signature verification. But at the same time, we see that a different signature scheme is more efficient for Aztec, so users who onboard directly to Aztec would benefit from a different one. Which one should we enshrine as the default?

More in general, with AA (or with smart accounts in general), the contract itself is tightly coupled with the wallet software, which lives at the application layer. Having the default contract at the protocol level means that it’d be much more difficult to change, slowing down app development that depends on it. It also has room for a lot of bikeshedding, so if we can push that to another layer of problems, we can have other teams work on it.

That’s where contract classes come in. When you deploy a contract class, you get an identifier for it (the hash of the code?). So you only need to encode this identifier in the address.

7 Likes

makes a lot of sense! Thank you
Please update this post after we reach consensus on how we onboard with this mechanism and any other open questions!

6 Likes

TIL there’s a recent EIP under consideration to add an opcode to replace the current code in a contract: EIP-6913: SETCODE instruction

4 Likes

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?

  1. 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.
  2. If a contract exists as a leaf in the contract tree, it proves that the constructor has already been executed.
  3. 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…

  1. .
    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.
  2. 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.
  3. 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.

7 Likes

Thanks for the read Mike! A few comments here and there:

In my mind, the slow updates tree was combined with the current approach of embedding public keys into the address. So you would rely on the slow updates tree only after the first upgrade to your code or keys. This means that to retrieve the code for a contract, you either:

  • Show that the code hash (ie the contract class identifier) is part of the address preimage, and prove that there is not an entry for this contract in the slow updates tree(*) OR
  • Show the current code hash in the slow updates tree.

(*) To support this, we’d need to structure the slow updates tree similar to the nullifiers tree to prove non-inclusion. Or maybe we can just emit a nullifier on the first upgrade, and prove non-existence in the nullifier tree directly.

This has a very big downside which you haven’t mentioned: any transaction that interacts with an upgraded contract or sends funds to a user with an upgraded public key is suddenly subject to the slow updates epochs. In other words, it becomes stale on every epoch change.

I couldn’t agree more.

That’s why I had Contract Classes in mind. You can trigger an upgrade by just changing the identifier of your class, you don’t need to store the entire code in storage.

As for the mixing of concerns, I think we can use the same tree within the protocol for storing this info, but expose two very different interfaces through the kernel circuits for interacting with each “space”. In other words, a dev would be able to store data, which would go in certain leaves of the tree, or would be able to update code or pubkey, that would go into a completely separate set of leaves.

So, if we define exactly where in the tree the code identifier or pubkey is expected to be found, the kernel can just load that without having to call the contract.

It’s a well-trodden path, not a simple one, and it’s filled with the corpses of the many that were bit by the perils of this godforsaken pattern.

Again, if we have designated slots for where in the tree the pubkey should be, we can allow an external contract to read only that bit of another contract’s state, via a special “get me pubkey” call.


Personally, I think I’m leaning towards the idea of keeping the current model of embedding everything into the address (which allows you to verify someone’s pubkey without requiring them to have deployed anything), but allowing the user to “nullify” it and move that info to the slow updates tree. But there’s still the problem of making excessive use of the slow updates tree and having epochs hurt the UX.

Alternatively, how difficult would be to explore the option of having private functions that cannot revert, in order to support a “get_pubkey” method in a contract, as a means to keep abstracting stuff? It may also help in composability with other chains (see this discussion).

9 Likes