Designing immutable storage in Aztec Contracts

Ideas and design for implementing the equivalent of Solidity’s immutable storage on Aztec Contracts, after some discussions with @Mike.

What is immutable?

Immutable storage is a Solidity construct for EVM contracts. If there’s a piece of state that gets set during initialization and is never updated, that state gets embedded into the bytecode itself, instead of saved to contract storage. Main reason for this is cost: bytecode is much cheaper than contract storage. This applies not just to EVM chains but also for Aztec.

Why not do the same in Aztec Contracts?

Unlike Ethereum, we have the concept of contract classes, which push for reusing the same bytecode for multiple instances. Mixing state with bytecode would mean contract classes can no longer be reused, which defeats their purpose.

What can we do?

Same as we always do: hash everything and add it to the address preimage. We can add an extra field, along with initialization hash, salt, and deployer, that is the “immutable storage hash”, ie the hash of all immutable pieces of state (either a plain hash or a small merkle tree).

In private-land

Immutable private state gets stored in the client-side database, along with the rest of the contract instance data. Whenever a contract wants to access its private immutable storage, it issues an oracle call to retrieve it, and then proves it can be hashed back to the immutable storage hash, which can get hashed back to its own address.

In public-land

Things get trickier in public land. If a contract needs to access its immutable storage in public, then that data needs to be provably available to every node. This means that broadcasting immutable storage is needed for public deployment, which means changing the contract instance deployer, which is a protocol change. It also means adding a new opcode to the AVM for retrieving public immutable storage.

What can we do with the least amount of changes?

We can handle immutable state in private-land only for now. And instead of introducing a new field, we could hijack an existing one (like salt or init hash) to store the immutable state hash, though I’m not a fan of these hacks. However, we’d still need to introduce a new oracle call to retrieve the immutable state.

Thanks for writing this up!

I was thinking more about this yesterday, when I was driving somewhere.
If we wanted to avoid introducing this new field, we could overload the constructor_args, and simply pass any immutable fields as constructor args. (I know you and I considered this already - I’m writing it here for completeness, and upon reflection, it’s not terrible…)

If the contract is not deployed (private only), then the executor of the function would need to know the preimage of the constructor_hash of the contract instance, in order to prove what certain constructor args are. The private functions of the contract would be written in such a way that they know which constructor args relate to immutables.

If the contract is deployed (and so is available to sequencers), the underlying constructor args of the constructor_hash are not broadcast (because in theory there could be 16k constructor args). So it’d be similar to what you’ve written above: a public function wouldn’t be able to access immutables that are established (implied) through constructor args.
Of course, if the contract is ever deployed, a public constructor function could publicly store all the “immutable” constructor args in public state.

I’m curious to know what usecasesm you had in mind for this: if the data is not very large (i.e. ~10 fields), then the current solutions are very good in terms of performance.

Compared to current immutable private state, the tradeoffs here seem to be:

  • we don’t need to broadcast the constructor data on-chain (though we technically don’t need to do this either for any notes created during construction - we can re-use out-of-band delivery)
  • we don’t need to prove existence of these values in the note hash tree
  • we now need to hash all constructor args and prove them in the address derivation

Note existence is only ~4k gates, so it’s not terrible. Multiple fields can also be stored in the same note, so we would not need to even require multiple note inclusion proofs (though aztec-nr imposes an upper-bound due to it requiring that notes are deliverable via logs, and hence log size limits the struct size).

Compared to public immutable state, the tradeoffs become:

  • we don’t need to use public storage
  • we somehow need to provably deliver the data to the entire network however?

PublicImmutable of any size is read in a single historical proof in private (we store the data + hash in public storage), plus proving the hash of the data, so again this is quite inexpensive.

The issue is that you still need plumbing in your PXE to supply the immutables whenever you need to access them. In other words, you need to supply the “notes” that correspond to those values.

True, but there’s another benefit of private immutable over the current solutions: you don’t need to actually initialize the contract to have access to immutable state. If you want to use an “immutable note”, you still need an initial call to “initialize” to add your note to the note hash tree to prove inclusion later.

The use case we had been discussing were “child account contracts”, where we have the address of their “master contract” as a private immutable field, and we’d love to not have to initialize them to use them.

I see, that sounds like a bit of a different kind of requirement then. Yes, it seems like this is not much more than an extra field in the address derivation, interpreted as the contract wishes.

1 Like