Grant Proposal: Application State Migration | HashCloak

Summary

This proposal aims for a prototype of on-chain self-migration. After upgrading rollup A to rollup B, users can prove they had certain private notes at rollup A and those notes get added on rollup B. The only thing we trust from the old rollup (A) is the latest archive root on L1.

Following Mike’s and Palla’s suggestion we’ll make use of a migration key embedded in the salt of the contract addresses on both versions of the rollup. The private notes of a user are added on rollup B if they can prove:

  • I have knowledge of migration key key_m and it was used to generate my accounts on rollup A and B
  • These private notes were indeed included in the latest state on rollup A and not nullified

To obtain the archive root from L1 we aim to compare querying L1 directly (via archiveAt) and using the inbox on the inbox of rollup B. In both cases, the goal is to add the root to the public storage of the application contract on rollup B, so it can be used as the reference value when users need to prove their notes really existed.

Finally, to execute the migration the user will execute a private function in the application smart contract. To support a variety of usecases, the input size to this function might vary greatly. For larger inputsizes it might be necessary to use recursive proving in order to not run into current Aztec smart contract limits. For this prototype we’ll assess both approaches, with recursive proving and proving directly in the private function.

Start and end date

Start: January 27th
End: March 6th

About You

HashCloak Inc. is a cryptography R&D lab and consultancy helping teams build secure, scalable, and privacy-preserving blockchain infrastructure. From advancing privacy tools to strengthening cryptographic security and optimizing blockchain performance, we bridge research and real-world deployment. Our expertise spans protocol design, cryptographic implementation, and audits, ensuring teams can confidently integrate cutting-edge cryptography into their systems.

HashCloak has been recipients of past NRGs including

In addition to past NRGs, the team has recently participated at ETHGlobal Buenos Aires with an implementation on the Semaphore protocol in Aztec and won the Aztec bounty for best Identity Infrastructure.

For this project the team consists of:

  • Elena Fuentes (ewynx), cryptography engineer. Worked on the Primus zkTLS attestation verifier for Aztec and won the “Most Intuitive Privacy UX” Technical Excellence Award at NoirHack for zkFace. Contributor to multiple Noir grants, including NRG#3 Semaphore in Noir and NRG#2 Private Shared States. Experienced with recursive proving in Noir.
  • Alex Liao (LYC386), cryptography engineer.
    Alongside Elena, has worked on the Primus zkTLS attestation verifier for Aztec. Contributor to multiple Noir grants, including NRG#3 Semaphore in Noir and NRG#4 Noir Circuit Optimization Developer tooling.

Proposal details

Migration approach: Category B: onchain, self-migration.

Technical architecture

For ease, we call the old version of the rollup “rollup A” and the new version “rollup B”.

Key strategies:

  • Migration key in salt of contract, as per Mike’s comment suggested. The salt of a contract address can be passed on when calling computePartialAddress and we’ll follow the strategy Mike mentioned to use salt = h(actual_salt, h(migration_pubkey, root)). Then, a user needs to prove they know migration_secretkey in the migration proof. Note: in an edit to the comment it is suggested to use the tagging public key (Tpk as described here) instead of the more “hacky” approach with the salt, but we couldn’t find a way to use it via the Aztec SDK, this is why we’re opting for the salt approach.
  • The application contract on rollup B should obtain the archive root in public storage before the users start with self-migration. This can be done via quering archiveAt from L1 and then storing this value in the contract on rollup B. Or use an L1-L2 communication portal where L1 can push messages into the inbox for the contract on rollup B, which then can be used with get_l1_to_l2_membership_witness. These approaches have different assumptions, because the first approach assumes that the application devs correctly obtain and store the root, whereas the second approach doesn’t have additional assumptions. (We want to try both approaches to analyze performance and showcase the different behaviours).
  • The user self migrates by obtaining their private notes from the old pxe environment and proving to the contract on rollup B that these notes are valid (more details below). This can be done directly with a private function in an Aztec smart contract where the inputs are the notes, inclusion (or “non-inclusion”) proofs, migration key. The other strategy is for the input to be a Noir proof itself, and recursively verifying this in a private function. The potential benefit of the second approach is that it allows longer input sizes, which could be needed for certain usecases.

The prototype consists of the following components:

  • (1) Application contract templates for rollup B. The variations will be:
    • With recursive proving and without.
    • Using archiveAt to obtain the last archive root or using L1->L2 messaging via L2 inbox.
  • (2) Messaging contract for L1. (To showcase the approach with L1->L2 messaging)
  • (3) Ts script that executes migration.
    • We plan to use 2 or 3 existing application contracts as testcases.

Aztec.js functions to get the necessary information for the private notes:

  • getNotes; obtain the private notes in the pxe
  • getMembershipWitness; obtain inclusion proof for a private note
  • getLowNullifierMembershipWitness; use to show that a note has not been nullified. From the code: " Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the “linked list structure” of leaves and proving that a lower nullifier is pointing to a bigger next value than the nullifier we are trying to prove non-inclusion for."

Migration proof.
The key functionality will be the user proving their private notes actually existing on rollup A, are theirs and are not nullified. This will happen in the application contract on rollup B in a private function, either via direct proving by passing on the necessary inputs or recursive proving (to circumvent the limits on input size). We assume that the contract at this point already possess the archive root in public storage, against which we can check the membership (or “non-inclusion”) proofs w.r.t the notes. The functionality for the migration proof:

  • check migration secret key leads to migration public key and that key has been used in the salt for account address a on rollup A and account address b on rollup B.
  • for all notes: verify the membership proofs (obtain the proofs via getMembershipWitness) and show they have not been nullified (proof for this can be obtained via getLowNullifierMembershipWitness).
  • add notes to private storage.

Because the inclusion proof uses public storage, this private function will call a public function at the end to check the archive root value that was passed on against the stored value. If this check fails, the private notes will not be migrated because the state changes are not accepted (this works as expected).

User flow from old roll-up to new roll-up

The user migration process follows these steps:

  1. Pre-migration setup. When using the application on rollup A, users generate a migration key pair, which is used for their account address. Users have the private key stored locally.
  2. (Application side) Obtain root from rollup A. Users don’t have to do anything for this, but chronologically this is needed as a prerequisite.
  3. Self-migration prep. User retrieves private notes on rollup A using getNotes, obtains membership proofs with getMembershipWitness and non-inclusion proofs with getLowNullifierMembershipWitness. Depending on the strategy, the user generates a Noir (Migration) proof for the next step.
  4. Self-migration execution. User calls private function on the smart contract on rollup B. This is either with the separate inputs from the previous step or the Noir proof.

Steps 3&4 are probably executed together once the user trigger self-migration, but are written down separately for clarity. In practice, step 3 could already be done in the background before the user trigger self-migration so the user experiences a better performance.

Trust assumptions

The chosen approach minimizes trust requirements.

  • Minimal trust in old rollup; we only trust the state root that we have on L1 from rollup A.
  • No intermediaries.
  • Optional: trust in application devs, when opting for the approach of manually querying the archive root from L1 and storing it in the new contract on rollup B.

How does your solution generalize across multiple applications

This solution can be used by a range of applications because of the option to use recursive proving or direct proving. Applications with limited state can opt for directly proving in a private function, while those with larger state requirements can use this approach with the recursive proving option.

The goal is for the prototype to be easy to follow and have clear documentation for application devs so they can use the approach that most suits their usecase.

Grant Milestones and Roadmap

Milestone 1: Application contract for a direct migration proof with tests in ts scripts using real notes and (non-)inclusion proofs.
Milestone 2: Full migration flow that queries directly the archive root from L1 and direct proving (no recursion).
Milestone 3: Messaging contract (send root directly to L2 inbox) and the migration flow that uses this.
Milestone 4: Recursive proving in application contract (for migration proof) and the migration flow that uses this.
Milestone 5: Documentation and benchmarks.

Roadmap:
Jan 27 - Feb 6: Milestone 1.

  • Implementation of the “migration proof” that does all the necessary check to verify state was indeed included and the user was indeed the owner
  • Incoporate migration proof verification in a contract that creates new notes when proof is verified
  • Add tests with real notes, inclusion proofs and non-inclusion proofs (for nullifiers)

Feb 9 - Feb 13: Milestone 2.

  • Create test setup that works with the 2 rollups (old and new)
  • Add query to L1 to obtain archive root
  • Add tests with this real setup. This is the first working migration flow (using a “direct” migration proof in the contract and directly querying L1)

Feb 16 - Feb 20: Milestone 3.

  • Add L1->L2 messaging via a L1 messaging contract
  • Migration proof needs to use received message in inbox
  • Add tests and documentation for this flow

Feb 23 - March 6: Mileston 4 & 5.

  • Add recursive proving by creating the migration proof in Vanilla Noir and adding a contract with a private function to verify proof recursively
  • Add tests
  • Documentation of all approaches
  • Simple tutorials of how to use for tutorials
  • Benchmarks to compare different approaches

Grant Amount Requested

Requested funding amount: 20K

Grant Rationale

Due to the team’s past experiences with the Noir RFP process, we are able to rely on this budget with a lean team to effectively deliver the required deliverables for this RFP. As such, the funding amount is solely for paying the salaries of the assigned team of developers.