| layout | default |
|---|---|
| title | Architecture |
The migration framework spans two rollup instances and L1.
Old Rollup L2 L1 New Rollup L2
+-----------------------+ +------------------+ +------------------------------+
| AppV1 (old rollup) | | Migrator.sol | | MigrationArchiveRegistry |
| lock_migration_*() | | reads old | | stores block hashes |
+-----------------------+ | archive root, | | verify_migration_at_block() |
| MigrationKeyRegistry | | sends L1->L2 | | verify_migration_at_snapshot() |
| register(mpk) | | message via | +-------------+----------------+
| (Mode B only) | | Inbox | | reads
+-----------------------+ +------------------+ +------------v-----------------+
| App Contract (new rollup) |
| migrate_mode_a() |
| migrate_mode_b() |
| migrate_to_public_*() |
+------------------------------+
All migrating app contracts on the new rollup share a single MigrationArchiveRegistry instance for block hash verification. The MigrationKeyRegistry on the old rollup is used only by Mode B to bind migration keys to user addresses before snapshot height H.
Quick reference -- the two registries:
MigrationArchiveRegistry(new rollup): Stores verified block hashes bridged from the old rollup via L1. Shared by all migrating apps. Used by both Mode A and Mode B.MigrationKeyRegistry(old rollup): Stores per-user migration public keys (mpk). Mode B only -- users must register before snapshot height H.
A Noir library (not a contract) providing core migration verification logic. App contracts call its functions directly.
Module structure (noir/aztec-state-migration/src/):
| Module | Contents |
|---|---|
mode_a/builder |
MigrationModeA builder (claim), MigrationLock builder (lock) |
mode_a/migration_note |
MigrationNote (lock note committed on old rollup) |
mode_a/migration_data_event |
MigrationDataEvent<T> (encrypted event delivering original data to claimer) |
mode_b/builder |
MigrationModeB builder with finish_at_snapshot() / finish_at_block() variants |
mode_b/key_note_proof_data |
KeyNoteProofData (inclusion proof for MigrationKeyNote) |
mode_b/non_nullification_proof_data |
NonNullificationProofData (nullifier non-membership proof) |
mode_b/public_state_proof_data |
PublicStateProofData<T, N>, PublicStateSlotProofData |
mode_b/l1_to_l2_message_proof_data |
L1ToL2MessageProofData, FullL1ToL2MessageProofData |
note_proof_data |
NoteProofData<T> (shared by both modes) |
signature |
MigrationSignature (Schnorr signature wrapper) |
constants |
Domain separators (DOM_SEP__*). See Constants Reference for the full list and production requirements. |
Singleton contract on the new rollup, shared by all migrating apps. Stores verified block hashes bridged from the old rollup via L1.
- Constructor params:
l1_migrator: EthAddress,old_rollup_version: Field,old_key_registry: AztecAddress - Key functions:
consume_l1_to_l2_message,register_block,set_snapshot_height,verify_migration_at_block(used by Mode A; also available to Mode B),verify_migration_at_snapshot(Mode B only) - Storage:
archive_roots(by proven block number),block_hashes(by block number),snapshot_block_hash(write-once for Mode B)
Mode B identity contract. Uses Owned<PrivateImmutable<MigrationKeyNote>> for per-user write-once key storage.
register(mpk: Point)-- creates aMigrationKeyNotebound to the caller, stored in the note hash treeget(owner: AztecAddress)-- unconstrained view; returnspoint_at_infinityif no key is registered
Permissionless L1 contract (solidity/contracts/Migrator.sol) that bridges old rollup archive roots to the new rollup.
migrateArchiveRoot(oldVersion, l2Migrator)-- reads the old rollup's latestprovenBlockNumberand archive root, sends an L1-to-L2 messagemigrateArchiveRootAtBlock(oldVersion, blockNumber, l2Migrator)-- same, but for a specific block heightgetArchiveInfo(version)-- view function returning current archive root and proven block number
Migration logic is organized in three tiers -- Library, Application, and Client SDK -- each with a distinct responsibility:
Client SDK tier: TS aztec-state-migration Client-side proof building, key derivation,
(ts/aztec-state-migration/) transaction construction, wallet helpers
Application tier: App contracts Wrappers that call library functions, handle
(noir/test-contracts/example-app/) app-specific state (minting, balance updates)
Library tier: Noir aztec_state_migration Core verification logic: proof verification,
(noir/aztec-state-migration/) nullifier emission, signature checking
Library tier (Noir aztec_state_migration) verifies Merkle proofs, checks Schnorr signatures, emits migration nullifiers, and enqueues block hash verification. It is app-agnostic.
Application tier (App contracts) import library functions and add app-specific logic.
Client SDK tier (TS aztec-state-migration) builds proof witnesses from Aztec node data, derives migration keys from account secrets, constructs Schnorr signatures, and orchestrates transaction submission. Exports are split by mode (mode-a/, mode-b/).
The bridge flow anchors migration trust in L1-proven state:
Migrator.sol MigrationArchiveRegistry
| |
| 1. Read archiveRoot from old rollup |
| 2. content = poseidon2_hash(oldVersion, |
| archiveRoot, provenBlockNumber) |
| 3. inbox.sendL2Message(l2Migrator, |
| content, SECRET_HASH_FOR_ZERO) |
| |
| -------- L1-to-L2 message ----------> |
| |
| 4. consume_l1_to_l2_message(archiveRoot,
| provenBlockNumber, secret=0, leafIndex)
| -> stores archive_roots[provenBlockNumber]
| |
| 5. register_block(provenBlockNumber,
| blockHeader, archiveSiblingPath)
| -> verifies header against archive root,
| stores block_hashes[blockNumber]
The SECRET_HASH_FOR_ZERO constant enables permissionless consumption: anyone can consume the L1-to-L2 message using secret=0. The leaf index disambiguates messages with the same secret.
A convenience function consume_l1_to_l2_message_and_register_block combines steps 4 and 5 in a single call.
Storage fields such as old_rollup_app_address (in migrating app contracts), old_key_registry, and old_rollup_version (in MigrationArchiveRegistry) use PublicImmutable rather than constants or private state because:
- They are set at deployment time (not known at compile time)
- They must be readable in both private and public execution contexts
- In Aztec V4,
PublicImmutablesupports direct.read()calls in private contexts, reading from historical public storage at the anchor block without any note management overhead
Private migration functions can then access deployment configuration directly, without note-based state propagation.
- General Specification -- Protocol specification including nullifier formulas and API tables
- Mode A Specification -- Cooperative lock-and-claim migration flow
- Mode B Specification -- Emergency snapshot migration flow
- Integration Guide -- TS SDK, wallet classes, proof data types
- Security -- Trust assumptions and security considerations