Multi-proving Aztec’s private state: a design-space report

Report summarising the design work of Artem Grigor, Conor McMenamin, and Mike Connor. This report documents the design space explored by the authors. It does not select a preferred design, recommend adoption, or represent an implementation commitment or roadmap from Aztec or Nethermind. Many thanks to those from Aztec Labs, Aztec Foundation, and Nethermind for their input throughout. The analysis and conclusions are those of the authors.

Summary

This report documents a design-space exploration of mechanisms that could provide an independent check on Aztec’s private client-side execution. The objective was to understand how the network or individual applications might limit the consequences of a client-side proving-system soundness failure.

The exploration began with an application-specific TEE guard for bridged assets. From there, we generalised the mechanisms to re-execute complete private transactions and explored four candidate designs: network-wide, network-opt-in, app-enforced, and subnetwork re-execution. Each introduces different protocol, privacy, liveness, application, and operational trade-offs.

The network-wide and network-opt-in designs received the most detailed protocol analysis. The app-enforced design was examined as an option that could, in principle, be deployed without modifying the Aztec protocol, although it requires substantial application changes. None of the designs has been prototyped, and this work does not select a preferred design, recommend adoption, or represent an implementation commitment or roadmap from Aztec or Nethermind.

The remainder of the report defines the threat model, reviews related approaches, describes the four candidate designs, compares their trade-offs, and records the decisions and alternatives considered during the exploration. The linked materials should be read as research and design notes rather than implementation-ready specifications.

This post should be seen as a checkpoint of our work, and we hope it will guide future researchers and developers interested in implementing multi-proving on Aztec.

The problem

On Aztec, a transaction runs in two parts, private (generated by the user’s PXE) and public (executed in the AVM). While the public part is executed in the clear by both validators and provers, the private part is executed exclusively on the user’s own device, and the only things the network gets to know about it are the effects it produces as well as a client-side zero-knowledge proof that the effects were produced correctly. That means that today a significant part of the network’s integrity rests on those client-side proofs. Once accepted, the protocol may be unable to distinguish forged private effects from valid effects using the proof and on-chain state alone.

Even though there is a lot of effort being made to reduce the chance of such exploits, the possible attack surface is very broad and complex, complicating the effort, with soundness bugs being found and more expected to be found while the network matures from alpha to beta. AI-assistants have accelerated vulnerability discovery in cryptographic protocols, reducing the cost of finding subtle circuit and integration bugs, for both defenders and attackers. Auditing and formal verification can reduce these risks, but neither removes the value of defence-in-depth against implementation, specification, and integration failures.

Prior work

The two most well known privacy networks, Zcash and Monero, also rely on client-generated proofs or cryptographic evidence for private state transitions and therefore provide useful comparison points.

Zcash is a network that allows users to transact either in the clear or privately. It addresses the problem with turnstiles: each shielded pool (Sprout, Sapling, Orchard) carries a publicly tracked balance, and a consensus rule rejects any block that would push it negative. This protects the rest of the network from an exploited pool, since no more value can leave a pool than legitimately entered it. The pool itself is not protected, though. An active exploit can still drain it, and whoever withdraws last may have their transaction rejected once the pool is drained. Zcash does offer some protection for users: one can choose between several pools with different levels of battle-testedness, depending on their risk appetite. Aztec’s general private state does not currently have an equivalent per-pool accounting boundary that would directly contain arbitrary forged private effects.

Monero, which offers only private, anonymous transactions, leans on the algebraic structure of its state. The Pedersen commitments it uses for notes are homomorphic, so anyone can check that a transaction preserves the invariant input values = output values + fees, as long as a small set of primitives holds: the ring signatures and the Bulletproof range proofs are sound. That is a very small surface to audit, and likely why the last known integrity bug was almost ten years ago. Aztec’s general private smart-contract execution does not expose an equivalent algebraic invariant across all state transitions.

One key reason for why we Zcash and Monero can afford to focus less on client-side proving is that they are relatively simple, focusing purely on transfers of a native token. Aztec does much more, so a much closer fit might be other (non-privacy) L2s. Furthermore, the Aztec proving stack is still evolving with the constraints proven being quite involved, which leaves increased scope for bugs. Multi-proving is understood as an eventual must-have defense-in-depth for rollups. Block-level rollup multi-proving re-executes a common public state transition. The designs considered here must independently reconstruct private client-side execution without revealing the underlying private inputs, creating different privacy and data-availability constraints.

Terminology

Multi-proving, as we discuss it in this post, is based on re-executing a transaction’s private, client-side logic on a second, independent system, which should arrive at the same effects. We refer to that system as the re-executor (a TEE, a second zk proving system, a trusted third party), which we trust to execute correctly. The re-executor attests to the result with a re-execution proof, which attests that, under its own execution environment and the transcript it received, it derived the same committed transaction effects. The resulting guarantee is independent only to the extent that the standard prover and re-executor do not share a relevant specification, implementation, compiler, or operational failure.

We call an effect anything a transaction writes to the protocol trees: a note hash, a nullifier, or an L2-to-L1 message.

To silo an effect is to hash it, before it enters the protocol trees, with some value. Today each effect is already siloed with the address of the emitting contract, which isolates one contract’s state from another’s.

A subnetwork is an otherwise identical Aztec network that settles to Aztec rather than to Ethereum, hence an L3. Apps inside it work as they would on the main network, with the only change being the network URLs.

Threat-model boundaries

To keep the multi-proving design tractable, we have made the following scope limitations:

  • Application logic. Application-logic vulnerabilities are outside scope. The mechanism is intended to reproduce the application’s defined private execution, not determine whether the application’s intended logic is safe.
  • Contract compilation. Compiler correctness and the equivalence of source programs to their deployed artifacts are assumed. A compiler or artifact-generation error shared by the standard prover and re-executor would not necessarily be detected.
  • Public execution is sound. Public execution, rollup aggregation, and settlement are assumed sound. These paths are excluded so that the exploration can focus on private client-side proving failures. Independent re-execution by different network roles may improve observability, but correlated implementation and specification failures remain possible.

The four designs

Below we provide a high level description of the four candidate designs, as well as their tradeoffs in terms of amount of required protocol changes, privacy impact on users, and the simplicity of deployment. For more details on each of the designs, follow the links to read their detailed design notes. To understand why we arrived at these specific designs, see the appendix.

Network-wide re-executor (detailed write-up). A re-execution proof from a pre-defined re-executor must be generated for every transaction on the network, and provided alongside the existing client-side proof. The block proposers and validators ensure the re-execution proof exists and is valid before they allow a transaction into a block. Relative to the other protocol-integrated design, this approach keeps the security policy uniform, but still requires coordinated changes across transaction construction, networking, validation, consensus enforcement, and the escape-hatch path.

Network-opt-in re-executor (detailed write-up). While the network-wide design forces a re-execution proof on every transaction on the network, the opt-in design relaxes that, letting the contract deployer decide whether the transactions touching a contract must be re-executed and by which supported re-executor (a TEE, a zk prover, a trusted third party). The cost of this flexibility is a significant increase in the required changes, both to the Aztec protocol and to application contracts. As protected and unprotected state now share the same network, each effect has to be siloed under the re-executor it is tied to, requiring a new kind of siloing done publicly inside the rollup circuits. That public siloing, in addition to changing how effect membership is checked, also incurs a privacy cost, since it reveals, for each effect, the re-executor tied to it. Finally, application contracts may now need to branch depending on which re-executor re-executed a transaction, slightly complicating the developer experience.

Application-enforced re-executor (detailed write-up). The first two designs both require changes to the protocol, so before any application can adopt it, Aztec network has to commit to a particular design and implement it. This design examines whether an application can enforce re-execution without waiting for protocol-level changes. It works by having a user generate a re-execution proof for their already-settled transaction, then share it off-chain with the recipient that later uses proinge the valid origin of their funds. The mechanism does not prevent forged effects from entering Aztec state. It attempts to prevent the protected application from recognising or consuming effects that lack the additional evidence.
It must be noted that the lack of protocol changes in this design comes at a real usability cost. Developers have to make significant changes to their contracts to make them compatible: removing direct calls from private to public functions, no longer relying on msg.sender, making the contracts non-upgradable, and not using some standard contracts, such as AuthRegistry , among other restrictions. It also raises the cost of using the app, since every withdrawal to L1 has to run additional validations, and interacting with a public (AVM) function now takes two transactions across two blocks instead of a single atomic transaction.

Subnetwork re-executor (detailed write-up). The subnetwork re-executor (which can be understood as a full Aztec network deployed on Aztec as an L3) is an alternative design that also does not require changes to the Aztec base protocol. Instead of making each application develop its own re-execution guardrails, it relies on apps running in a subnetwork deployed on Aztec, where, much like the network-wide design, every transaction is accompanied by a re-execution proof. The subnetwork provides a separate security domain in which re-execution can be mandatory, at the cost of separate infrastructure and bridge-mediated interaction with the main Aztec network.
From Aztec’s point of view the subnetwork is just another contract designed to withstand a client-side proving compromise, built on the app-enforced design. The key trade-off of this design is the development effort to build such subnetworks and the infrastructure to maintain them. This requires migrating the existing Aztec rollup contracts from Solidity to Noir, adjusting the rollup proof to no longer provide equivalence between Keccak and Poseidon, and forking existing Aztec nodes to support the new subnetwork stack. We estimate this work to far exceed the development cost of the other designs.


Rough estimate; only the network-wide and opt-in figures have been scoped out completely.

Limits of our analysis

  • The re-executor proof system. We explored several mechanisms a re-executor could be built on and recorded them in the design-space write-up linked below, but we did not prioritise a concrete design, and none have been benchmarked. Instead, the designs keep the re-executor pluggable.
  • No implementations. None of the four designs has been prototyped.
  • Limited user research. We did not run extended interviews or surveys with application developers, and instead largely relied on our own experience and that of several other experienced developers.

Conclusion

This exploration identified four candidate approaches for independently checking Aztec’s private client-side execution. Each changes the system’s security, privacy, liveness, complexity, or application model in material ways:

  • A network-wide re-executor offers uniform protection but introduces a network-level dependency and requires protocol integration.
  • A network-opt-in design provides flexibility but requires broader protocol changes and introduces additional privacy and developer-experience costs.
  • An app-enforced design avoids protocol changes but requires substantial contract redesign and restricts existing application patterns.
  • A subnetwork design isolates the protected environment but carries significant implementation and operational overhead.

The analysis does not establish that any candidate is production-ready. None has been prototyped, the re-execution mechanism itself has not been selected or benchmarked, and several protocol-integration questions remain unresolved.

This report therefore does not recommend a specific design or determine whether or when Aztec should adopt multi-proving. Such decisions would require further specification, prototyping, benchmarking, security analysis, and consideration of the protocol’s evolving architecture. Those activities are outside the scope of this design-space exploration.

We are publishing the detailed design notes and decision record so that future work can reuse, challenge, or revise the analysis as the protocol and available proving technologies evolve. Corrections and technical criticism are welcome.

2 Likes

Appendix: Walking through our design decisions

This appendix explains how we arrived at the four designs covered above. We walk through our six design attempts, recording the decisions we made and the alternatives we considered and set aside. The first two attempts establish the core principles every later design follow, while each later attempt is linked to a particular design discussed above. This is the most involved part of the post, written for readers who want to understand our reasoning and conclusion. It is not required to understand the designs above or to compare them between each other, and for that purpose we refer to the detailed write-ups of each design.

Attempt 1: An app-specific guard for bridged tokens

A TEE-signed guard for one bridged token: too narrow in its application, but it set the three principles the later designs follow.

We started by exploring multi-proving for a very specific case of bridged tokens, arguably the main value attackers try to exploit on L2s. In our first design attempt we have every transaction touching the token would send its state changes to a re-executor running inside a TEE. The enclave re-executed the transition, checked the token invariant (tokens in equals tokens out), and signed the resulting token state root, which the transaction emitted as an L2-to-L1 message for the bridge contract to consume at withdrawal. In effect this design, while requiring no protocol changes, established a parallel, TEE-based rollup that mirrored the token, and the bridge released funds only when its attested state matched the token’s actual state on Aztec.

While this became an inspiration for the later app-enforced design, we found that it fell somewhat short in three important areas. First, it required the developer of every application to define their own stand-alone checks the re-executor should re-run, as well as spin up their own infrastructure to re-running these checks. Second, it was specifically designed to assume a TEE-signature-based re-executor and did not consider other re-executor approaches, which felt premature given how large the design space of TEE-based re-executors is. Third, it weakened privacy, since every token interaction emitted a publicly identifiable L2-to-L1 message.

Based on this, we formed the three guiding principles for the future designs:

  • Avoid custom developer constraints. By default a developer should write no new validation logic: the re-executor re-runs their existing private functions instead. The protocol is already complex enough. Asking developers to also write and maintain a separate logic to run in the re-executor would only deter adoption. The existing private functions are already by design the minimal validator of the unconstrained execution, so a hand-written one could not be smaller anyway. And since we already assume the developer’s own logic is correct, it would add no security either.
  • Support arbitrary proof systems. Given that there is no single proof system today that meets every requirement, the design should stay flexible to support different re-executor proofs (TEE attestation, signatures, zk proofs), that might be changed as the network matures. We do accept however some reasonable requirements, enforced on the re-executor (such as a cheap L1 verifier or ability to verify the proof on the AVM).
  • Minimal privacy impact. Adding a re-execution proof may require trading away some privacy, since its presence reveals the transaction interacted with protected state. But Aztec is a privacy network, so that loss must be kept as small as possible.

Attempt 2: A generic opt-in, re-executing sub-calls

A generic per-function opt-in: re-executing only part of a transaction created more problems than it removed, settling us on whole-transaction re-execution.

In our next attempt we explored a more generic mechanism that any contract would be able to opt into to get the added security guarantees of the re-executor. Developer experience relied on annotating a target contract with a #[second_validator(VERIFIER_ADDRESS)] and then, for each private function that emits a note or nullifier, by default using the #[auto_validate] macro to have the re-executor re-run the existing private function logic, or, if they choose to, implementing their own invariants for validation (for a token contract, that input note amounts sum to output amounts). The design worked with any proof system, as long as it could be verified on the AVM as well as locally in private functions. For each re-executor, there would be a contract at VERIFIER_ADDRESS that would store an append-only collection of note hashes and nullifiers generated for this contract. When the user interacted with the protected private function of a contract, locally, they would collect the execution transcript of the protected private function (as well as its sub-calls), submit it to the re-executor to recompute the notes and nullifiers, and then enqueue them in a public call at the end of the private function execution. The re-execution proof verifier would then verify that the note hashes and nullifiers submitted to it were accompanied by a valid re-execution proof (attestation), and then append them to the respective collections.

While this attempt was far from complete when we stoped pursuing it (it only hinted at how to handle public note and nullifier emission and L2-to-L1 message authentication), two key issues made it generally unsuitable. First, every protected private function had to enqueue its own public call to the verifier contract to record its new note hashes and nullifiers. That means (i) the transaction’s limited budget of enqueued public calls (currently 32 per transaction) is spent faster; (ii) the anonymity set shrinks, since each protected private function enqueued public call has a potentially distinctive signature, letting an observer tell the private functions apart, as well as their execution order; and (iii) the verifier contract effectively duplicates in its public storage the same note hashes and nullifiers that already exist in the protocol trees. Another issue is in having the re-executor re-execute only part of the transaction (sub-call-trees inside a protected function), rather than all of it, meant that whenever a transaction calls the same protected contract more than once, each call needs its own proof and enqueued public call. Further, if there is a single callback, it then pulls the rest of the transaction back into re-execution, making re-executing the whole transaction a simpler path. Finally, it complicated the analysis, as the contract developer might want to trust the calls made from the protected functions, however, they might be poisoned by malicious state updates and still be unreliable.

As a result, we settled on two further principles:

  • Re-executing the full transaction. Even though the re-executor could re-run only a chunk of the transaction, the sub-call-tree of a protected function, re-executing the whole transaction turns out to be cleaner, and easier to both implement and reason about. Hence it is the re-execution model all four designs below assume. The cost is that it exposes more of the private execution to the re-executor, so ideally that re-executor would run locally, such as a second zk proving system.
  • Reuse existing protocol state. The protocol already keeps every note hash and nullifier in its own trees, so re-storing them in a verifier contract is pure duplication. A more efficient design would reuse them instead.

Attempt 3: Re-executing the whole transaction (Network-wide re-executor)

Exploring how a whole-transaction proof can be shared across the network and how it would be verified, leading to a network-wide design (full design specification) .

From our second attempt we concluded that the re-executor should re-execute the whole transaction, so its proof attests the entire transaction is valid and well-formed. The next step is to communicate and verify that proof on the network, however here we hit a self-reference issue. If the proof is tied to a particular transaction through a commitment to it (such as the transaction hash), it cannot itself be part of that transaction, because then the commitment changes, and the link no longer holds. The first option we considered was to send the proof outside the linked transaction, in a follow-up transaction or off-chain to the recipient. However, as the validity of the transaction is initially unknown, that restricts a lot of use cases we care about, such as an atomic call from a private function to a public, L2-L1 message emission, or relying on the message sender in public calls. Hence we did not explore it further, returning to it later. The second option was to replace the single proof over the whole transaction with one proof per effect, that would say that a particular effect was part of a valid transaction. However, we had to abandon it due to a selective-disclosure attack: an adversary could generate re-execution proofs over the full transaction effects, but then disclose to the network only the effects that suit them and withhold the rest. For instance, for a transaction that consumes an old note and creates a new one, they would publish only the new note with its re-execution proof while withholding the nullifier that would spend the original. That led us to the third, arguably most intuitive option, of excluding the re-execution proof from the transaction commitment, breaking the self-referential loop. However, since there were no user-controlled fields that we could extend and that were not part of the transaction hash commitment, we had to add a new top-level transaction field to store the re-execution proof, which meant changes to the Aztec protocol.

While adjusting the transaction structure let us embed the re-execution proof in the transaction, it also constrained where the re-execution proof could be verified. In the previous two designs the verification was handled by developer-defined logic, either on L1 or in the public functions executed by the AVM. Running it on L1 for every transaction would likely be too expensive, so doing it in a public function looked like a reasonable compromise: it bought no protocol changes at the price of routing every re-executed transaction through the AVM. However, for a proof that attests to the whole transaction, the AVM approach is was no longer trivial: it would have to expose the new re-execution proof field (a large undertaking, since it is a variable-size hint, much as calldata is exposed today), and the transaction hash, or enough information to reconstruct it. It soon became clear a cache would be needed too, to reuse the verification result rather than have each contract re-verify the same proof, and that cache would be another AVM change. Finally, unlike the second design attempt, where an invalid proof invalidated only a particular public call, here it invalidates the whole transaction, which argues for verifying it in the setup phase and dropping the transaction if the proof fails. Given all of the above, the best path appeared to be to enshrine the verification of the re-execution proofs attached to a transaction in the protocol before the setup phase runs (and drop any transaction where the re-execution proof fails), with the result is cached in a dedicated context field that any public function can then read to learn which re-executor re-executed the transaction.

For where the validation would run we initially considered AVM, but after weighing the complexity of modifying the AVM to accept re-execution proofs, and its restrictive environment, which would rule out the more involved re-execution proofs such as TEE attestations over non-native curves or alternative zk proofs, we instead shifted the validation from the AVM to the validators. This somewhat weakened the guarantee, since a re-execution proof would be enforced only if the validators are trusted to verify it, but it avoided both the heavy AVM changes and the loss of flexibility in the re-executor that the AVM route would have forced. The one caveat was that it required extra machinery for the Escape Hatch, the path on which the validators are inactive and cannot enforce the verification, though that proved manageable.

These decisions leave us with the first candidate design, the mandatory network-wide re-executor, arguably the simplest of the four we present in this post. In it, the user’s wallet, after generating a transaction, submits all the captured data, as a re-execution transcript, to the re-executor, which produces a proof over the re-derived transaction hash. The wallet attaches that proof to the transaction and broadcasts it to the network. The mempool and the block proposer verify the proof before the transaction is included in a block, and the validators re-run the validation before they attest to it, and so before the transaction settles. The only other part that changes is the Escape Hatch: on its use, every transaction in the block must carry a re-execution proof, verified on L1 before the block is accepted. Note that this Escape Hatch re-execution proof is non-trivial, requiring additional verification and L1 smart contract logic to verify that all of the transactions in a given Aztec block were correctly re-executed.

Attempt 4: Letting each contract pick its re-executor (Network-opt-in re-executor)

Adjusting the network-wide design to allow each contract to select its own re-executor, resulting in the network-opt-in design (full design specification).

Simple as the above design was, the requirements of a single mandatory re-executor could be seen as very strict, allowing no flexibility for different security assumptions, and forcing all user activity to route through a single re-executor. This motivated us to keep exploring in another direction, where each contract could define its own re-executor (or none).

The main draw with letting contracts on the same network use different re-executors is that it creates regions of different security assumptions, which means the protocol trees can no longer stay unified. To illustrate why, consider an attacker who has compromised one re-executor but not another. They could craft a malicious effect intended for a contract protected by the re-executor they have not compromised, and get it inserted by having the compromised re-executor re-execute it instead, defeating that contract’s protection. We considered several ways to keep the effects of different re-executors apart: a standalone copy of each protocol tree per re-executor; a separate metadata tree recording which re-executor each effect is tied to; or amending the existing tree entries to carry the re-executor. All of them required new machinery that would complicate the protocol further. Instead, we took the pattern the network already uses to keep one contract from writing into another’s storage and applied it to re-executors: that pattern hashes (“silos”) each effect, before it enters the protocol tree, with the address of the contract that owns it, and we extend the hash to also include that contract’s re-executor.

The one difference from address siloing was the privacy impact. Address siloing of privately emitted effects is done in private, by the client, since client-side proving is trusted to apply it correctly. For the re-executor we cannot rely on the client-side proof, since we work under the assumption that it is compromised, so all of this siloing has to happen in the AVM or the rollup circuit instead. This is not complex to implement, since the address siloing of publicly emitted effects is already done in public, but it reveals significant information. Before siloing, the presence of a particular re-execution proof in a transaction told an observer only that the transaction touched a contract using that re-executor; it revealed neither how many effects were tied to the re-executor nor their order in the transaction. With siloing, every effect the private part produces has to be tagged with its re-executor, which makes it easier to identify the contract and the function the user called.

Given the significance of privacy for Aztec, we looked for ways to avoid the leakage. One partial mitigation was shuffling the relative order of the effects to make them harder to identify, though it still left the counts exposed. We thus considered siloing not by the single re-executor of each effect but by the whole set of re-executors that re-executed the transaction. That worked for note hashes and L2-to-L1 messages, where it simply increased the information hashed into each effect’s tree entry, but it broke down for nullifiers. A nullifier has to be deterministic so that a double spend can be caught, yet the transaction’s set of re-executors is user-controlled, depending on which contracts the transaction touches, so the nullifier would no longer be deterministic, opening the door to double-spends. One could keep a canonical, non-re-executor-siloed nullifier alongside the siloed one to catch double spends, but that would duplicate storage and create a bespoke mechanism just for nullifiers, again complicating the protocol. More generally, siloing by the set of re-executors rather than a contract’s single re-executor would mean a consumer had to know that set to prove the pre-image of an effect’s hash, yet the set is not maintained anywhere in the protocol and cannot be efficiently guessed, so the consumer would have to fetch it from an archive node, another new mechanism. This led us to accept the privacy leak and focus on the few decisions left.

One of those was whether a contract should have a single re-executor or the option to define several, and if several, whether all would be required, at least one, or an arbitrary combination over them. After some deliberation, we settled on a single re-executor, first of all because it keeps the number of distinct re-executor fingerprints smaller (those fingerprints, as we showed before, leak information about each effect stored on the network), and also because it is simpler to understand and maintain. If a contract needs more flexibility, that can be handled with a proxy re-executor pattern, where several re-executors under an arbitrary DNF are abstracted to the protocol as one.

Another was whether a contract should be allowed to switch its re-executor, and if not, whether to fix it at deployment or even at contract-class registration. We decided against switching the re-executor, since doing so would require re-siloing the contract’s private state, which would be very problematic given the protocol does not even know which state belongs to which contract. Instead, we proposed contract migration: deploy a new contract under the new re-executor and let users move their state across at their own pace. That also meant the re-executor should be set at contract deployment, since migration implies two contracts with the same class ID but different re-executors. Knowing the re-executor should be fixed once per contract and immutable thereafter, we looked at the Private Immutables from AZIP-9 as a way to store it efficiently. However, Private Immutables were expected to carry a contract-defined encoding schema, which did not suit storing a protocol-level value there. So we proposed a separate value sitting next to the Private Immutables hash, carrying this contract’s re-executor ID and hashed into the address, which creates the link from the address to its re-executor.

Next, there was the question of whether to allow arbitrary re-executors. Technically it was trivial: assuming the re-executor’s ID is the L1 contract address that implements its verification logic, a validator could simply look up that logic by the ID and run it. However, we restricted the set of allowed re-executors, for two reasons. The first was privacy: the more distinct re-executors in use on the network, the smaller each contract’s anonymity set, and the easier it is to re-identify what a transaction did. The second was a denial-of-service attack on block proposers and validators. An attacker could create a contract with a special re-executor whose proofs are cheap to generate but expensive to verify, then submit many transactions from that contract, each carrying an invalid re-execution proof. The network cannot charge for the work it spends verifying those proofs, since they are invalid, and it has no way to tell whether the failure comes from a compromised client-side proof or a deliberately invalid re-execution proof. One could make fees payable even when the re-execution proof is invalid, but that would open a fee-draining attack: much like the client-side proof today, the re-execution proof is not authenticated, so a malicious node could tamper with and invalidate the re-execution proof in any transaction, which leaves the user paying fees while the transaction itself is dropped. As a result, we proposed a whitelist of allowed re-executors, much like the existing whitelist of public setup functions: any transaction relying on a re-executor outside that set is dropped. Since the list is only a runtime configuration parameter of the node, the set is easy to adjust over time, adding new re-executors and retiring old ones.

The last question was how much programmatic access developers should have around re-executors. Until now a developer defined a re-executor once for a contract, with no further control over the value and no way to reference it. However, when we looked at realistic applications we came across several cases, such as registries, where the AVM public functions clearly needed to reference the set of re-executors that re-executed the transaction. A good example was the ContractInstanceRegistry, which uses msg.sender to authenticate the address before performing operations on its behalf, and so relies entirely on the security of client-side proving, since nothing requires a valid re-execution proof tied to the msg.sender contract. An attacker who compromised client-side proving could then enqueue such public calls, first taking over registries and then the contracts’ assets. So public functions must be able to check, for a given address such as msg.sender, that its tied re-executor re-executed the transaction. That meant exposing the transaction’s re-executor set from the AVM context, along with the address-to-re-executor mapping. And for msg.sender specifically, given how often re-authentication is needed, we enshrined it by requiring the validators to check that the transaction’s collected re-execution proofs include the re-executor of the msg.sender contract for each enqueued public call. The final part was whether, given developers can read the re-executor set in the transaction, they should also be able to insert into it, forcing the transaction to collect additional re-execution proofs through private-function callbacks. We saw the use, but decided against it, and more generally against a user attaching any more re-execution proofs than the validators require to not drop the transaction. The reason is that the re-execution proof field is not authenticated, so anyone can add or remove proofs without invalidating the transaction itself, and so, as long as it is not dropped, steer how the transaction behaves, which would be a dangerous precedent. Instead, a developer who needs a particular re-executor included can call a proxy contract that has that re-executor defined and emits a single effect. This slightly increases the cost, but it is enough to pull the desired re-executor into the required set, keeping the security model tight.

Having answered these questions, we arrived at the final opt-in design, which at a high level worked as follows. Each contract chose a single re-executor to protect its state (or a no-op re-executor if it wanted no extra protection). The re-executor’s ID was the L1 contract address holding its canonical verification logic. As the client executed a transaction, it paired each effect with the re-executor tied to the contract that emitted it. Once the transaction was ready, the client derived the full set of re-executors the transaction would need, requested their proofs, and, as in the network-wide approach, later injected the proofs into the transaction. Block proposers and validators then verified those re-execution proofs before admitting the transaction, and dropped transactions missing any required re-executor. From there, the public execution in the AVM gained a new context value: the set of re-executors that re-executed the transaction. And the rollup circuit siloed every effect with its paired re-executor before adding it to the protocol trees. The transaction blob data was also extended to include the transaction’s full list of re-executors, so it stayed at hand for later reference, such as during the Escape Hatch.

Attempt 5: Revisiting no protocol changes approach (App-enforced re-executor)

Revisiting communicating re-execution proof out of band via recipient-side checks plus trusted AVM storage, culminating in the app-enforced design (full design specification).

As one can see, allowing smart contracts to define their own re-executor has made the design significantly more involved. This motivated us to revisit some of the early decisions of the third design attempt, given that what initially looked like prohibitive complexity may appear more reasonable now. In particular, we revised the topic of communicating the re-execution proof, and explored an alternative path of communicating it out of band (or in a follow-up transaction). And while still being a rather limited approach, we find that it allows us to produce a design that does not require any protocol changes, where each application can define its own re-executor, and furthermore, that gets even better privacy.

First, consider private-only transactions, which settle without touching the AVM, and emit notes and nullifiers as effects (we leave L2-to-L1 messages and logs aside for now). Under our threat model, client-side proving can be compromised, so a recipient can trust such effects only if each belongs to a transaction that carries a valid re-execution proof and was included in the canonical chain, since the proof attests that the transaction executed correctly but not that it settled. The recipient could delegate these checks to the AVM (which they trust per our assumptions), but that adds a standalone public transaction just to run them, and also reveals that the private transaction touched a protected contract.

Instead, we find that the recipient can perform both checks themselves, avoiding both drawbacks. The first construction we considered had the recipient look up, in the block history, the transaction that emitted the effect they want to use, and then verify its re-execution proof against that transaction’s hash. While suitable for a single sender-recipient hop, the construction failed once more parties were involved. Consider a case where a recipient consumes the received note and becomes a new sender, passing a new note on to a further recipient. Given the initial construction, the new recipient cannot know whether the first recipient checked the original sender’s re-execution and inclusion correctly. So, to avoid being tricked, the new recipient has to re-do all the checks, which requires the previous hop to forward its re-execution proof and the effects it consumed, and so on back down the line. Besides being very inefficient, this also completely compromises privacy, since it rebuilds the chain of asset ownership that Aztec is trying to hide.

Part of the solution is to verify the earlier re-execution proof inside the new re-execution, but that still leaves the effect-inclusion checks. This is where we use the fact that every private transaction on Aztec anchors to a block: if a transaction can show that the block it anchors to descends from the one in which the transaction that emitted the effect it uses was included, that guarantees effect-inclusion. Thus we simply add new requirements for re-execution: it has to guarantee that every effect it consumes from a private transaction carries a valid re-execution proof, and that the emitting transaction was included in an ancestor of the block the current transaction anchors to. This lets the recipient check only the last hop, its re-execution proof and transaction inclusion, which itself guarantees the rest. And because the re-execution proofs are communicated off-chain, the rest of the network, unlike in the network-opt-in design, never learns that some effects came from a protected contract.

Given we solved the pure private-execution functions, we move forward to public execution and how it can validate effects emitted by private transactions. We find that the transition is relatively straightforward, the public execution replaces the recipient, so it has to run the same checks: verify the re-execution proof of the transaction that emitted the effect, and confirm that transaction settled. While verifying the proof inside the AVM limits which proof schemes we can use, it is technically possible, and significantly less complex than checking a transaction’s inclusion in a historic block. One idea we had was to fold the inclusion check into the re-execution proof, where the re-execution proves that the effect was part of a valid transaction, included at a particular epoch root. This significantly reduces the amount of work that public execution needs to do to validate inclusion, offloading it to the re-executor, with the cost being that the re-execution proof can only be generated after the transaction settles (which is likely minor given a block time of 6 seconds).

This covers effects emitted in private execution, but the public execution can also emit them. And even though we assume the AVM and the developer logic it runs are trusted, emitting these effects as before would mix them with the untrusted notes and nullifiers in the protocol trees, since there is no state siloing. One could try to solve this the same way as for the private part, by generating a re-execution-style proof over the AVM execution. But it is not clear how to prove that a given note or nullifier came from the AVM rather than from private execution (without re-executing the AVM), and it would also prevent the note or nullifier emitted in an earlier part of public execution from being used in the later part.

Instead, we looked again at the state-isolation options from the fourth attempt. Most of them required very invasive protocol changes, but one we considered was just right, which we adapt: all trusted effects (emitted by the AVM) are duplicated in a standalone effect tree in the contract AVM storage. Later calls in the same or later public executions can read those values from the storage, and private execution can reference them too, as they are structured as delayed mutables (initialised once and immutable thereafter). This pattern also proved useful not only for publicly emitted effects. When a note or nullifier generated in private is expected to be used in public many times, rather than re-check it each time, it can also be saved into these effect trees after the first verification and read it from there afterwards. The same applies to effects whose re-execution proof might expire (due to re-executor key rotation): saving the effect on-chain records that its proof was valid at that time, and one no longer needs to store it locally. The result is that the check of an effect being valid is now: either there is a re-execution proof and an inclusion proof, or it is recorded in the AVM storage of a given contract.

With notes and nullifiers handled, three things we set aside remain: public call requests, logs, and L2-to-L1 messages.

  • Public calls work as before, except that msg.sender can no longer be trusted, since the re-execution proof only reaches the AVM after the transaction settles, so by the time a public call runs anyone could have spoofed the value. For cases that need the caller identity, we propose a pattern where a private function initiates a checkpointed call request that the public part picks up once the proof is ready. It builds on notes and nullifiers, which we already handle securely: the private function creates a note describing the wanted public call, and a follow-up transaction, given that note and its re-execution proof, consumes the note and emits a nullifier to prevent replay. This still does not give atomicity, which would need a more careful two-step lock-and-execute flow.
  • Logs, and specifically constrained delivery, work as before. The re-execution proof binds to the whole transaction, including its logs, so a valid re-execution proof together with inclusion guarantees the referenced logs are correct too.
  • L2-to-L1 messages see the largest change. Like the other effects, they can be emitted from both public and private execution, so by the same argument the public part stores them in the contract’s dedicated effect tree. But unlike notes and nullifiers, which we try to keep unlinkable, an L2-to-L1 message emitted in private is public regardless, so there is no benefit in tying it to a re-execution proof. A private execution therefore emits a special note instead, which the public part consumes and re-emits as a proper L2-to-L1 message, adding it to the effect tree. At L1 consumption, the bridge runs the usual checks and also verifies that the expected message sits in the right slot of the AVM storage tree, which shows it was emitted by the trusted AVM.

This completes the majority of the App-enforced re-executor design exploration that we did. By combining off-chain communication of the re-execution proof with trusted AVM effect storage, it needs no protocol changes, stays opt-in for any application developer, and leaks almost no privacy. The costs: added design complexity, significantly reduced programmability compared to standard contracts, higher bridge withdrawal costs, the private and public halves of a transaction split into two, and a large contract redesign, which still does not protect against all denial attacks.

Attempt 6: Merging designs (Subnetwork re-executor)

Exploring a hybrid of the previous designs for apps to run in secured subnetworks, concluding with the subnetwork design (full design specification).

By now we have explored two opt-in designs, but each was somewhat sub-optimal, either requiring too many protocol changes or a large overhaul of contract logic by developers. Nothing came close to the simplicity of the network-wide re-executor, which would have been ideal but for its requirement of a single re-executor for the whole network. That made us wonder whether we could make it work after all. Though it seemed we had explored every sensible option in the third and fourth attempts, it then occurred to us that, instead of holding the whole Aztec network to a single re-executor, we could deploy a miniature Aztec-like network, a subnetwork, within which the network-wide re-executor is mandatory, while the subnetwork’s state contract, held on the regular Aztec network, is protected using the app-enforced re-executor. This cleanly isolates protected apps from unprotected ones, while staying compatible with the rest of the network through a bridge contract and keeping the developer experience identical to the regular Aztec network. From the Aztec network’s point of view the subnetwork is just another contract, so it needs no protocol change, and any developer or community can spin up their own protected subnetwork. And the only new interface developers would need is the API for communicating across the boundary, to ordinary Aztec contracts and to other subnetworks, though at first this could be handled with the existing inbox and outbox message pattern.

Promising as it looked, on closer inspection this design had three key issues:

  1. Deploying it would be far more involved than the other three designs. Aztec is built to be verified on Ethereum, not on itself: the rollup contracts would have to be rewritten from Solidity into Noir, the proof’s Keccak-based transcript moved to Poseidon, and the blob equivalence proof, which ties Aztec’s Poseidon commitments to the KZG commitments Ethereum’s blobs use, dropped. Further changes involve having the private execution verify subnetwork rollup proofs (only it can do such verification), designing a new data-availability system, possibly based on Aztec’s public logs, and building the fee-payment infrastructure. And this does not even account for the changes needed for validators, block builders, provers, and archivers to interact with a rollup contract deployed on Aztec instead of the current one on Ethereum.
  2. Maintaining a subnetwork would also be costly. While data availability and sequencing can be solved at the rollup-contract level, one still needs to run block explorers and indexers that can interact with the subnetwork, and validators that publish to Aztec rather than Ethereum. All of this would likely start as a fork of the existing Aztec infrastructure, which would then have to be maintained and upgraded in lockstep with the Aztec protocol, which currently evolves on a recurring basis.
  3. There is no other clear application for such subnetworks, and the most obvious one, app-specific rollups, could arguably be built without the full Aztec-like stack.
2 Likes