diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 7115033..6d1fe46 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -9,279 +9,413 @@ title: Integration Guide ## Overview -The migration system is organized into three tiers: +The migration system has three tiers: -1. **Library tier: Noir `aztec_state_migration`** -- Core verification logic: proof verification, nullifier emission, signature checking. This is a library, not a contract. -2. **Application tier: App contracts** -- Wrappers that call library functions and handle app-specific state (minting, balance updates). -3. **Client SDK tier: TS `aztec-state-migration`** -- Client-side proof building, key derivation, transaction construction. +1. **Noir library (`aztec_state_migration`)** -- Core verification: proof verification, nullifier emission, signature checking. This is a library, not a contract. +2. **App contracts (V1 + V2)** -- V1 (old rollup) calls library lock functions; V2 (new rollup) calls library claim functions and handles app-specific state (minting, balance updates). +3. **TS SDK (`aztec-state-migration`)** -- Client-side proof building, key derivation, transaction construction. -Integrators typically work at the Application and Client SDK tiers: writing an app contract that calls into `aztec_state_migration`, and using the TS client library to build proofs and submit transactions. The focus here is the Client SDK tier and the proof data types that bridge Noir and TypeScript. +Integrators work at the App and Client SDK tiers. -## Minimal Flow at a Glance +### Migration Key -**Mode A** (cooperative lock-and-claim): +Migration uses a dedicated keypair (`msk`/`mpk`) rather than the account's existing signing keys: -1. `deriveMasterMigrationSecretKey(secretKey)` -- derive the migration key -2. `oldApp.lock_migration_notes_mode_a(amount, destRollup, mpk)` -- lock on old rollup -3. `migrateArchiveRootOnL1(...)` + `waitForL1ToL2Message(...)` -- bridge archive root via L1 to new rollup -4. `wallet.buildMigrationNoteProofs(blockNumber, lockNotes, events)` -- build proofs -5. `signMigrationModeA(signer, oldVersion, newVersion, notes, recipient, newApp)` -- sign -6. `newApp.migrate_mode_a(amount, mpk, signature, proofs, blockHeader)` -- claim on new rollup +1. **Account contract independence.** Claims must not depend on the old rollup's account contract executing correctly. +2. **Cross-rollup proof compatibility.** Standard Aztec keys are not committed in a form that is easily provable across rollups. +3. **Scoped risk.** If the migration key is compromised, only migration claims are at risk. -**Mode B** (emergency snapshot, private notes): +The `msk` is derived deterministically from the account's secret key: -1. `deriveMasterMigrationSecretKey(secretKey)` -- derive the migration key -2. `keyRegistry.register(mpk)` -- register key on old rollup (before snapshot) -3. `migrateArchiveRootOnL1(...)` + `waitForL1ToL2Message(...)` + `archiveRegistry.set_snapshot_height(...)` -- bridge archive root and set snapshot height -4. `wallet.buildFullNoteProofs(blockNumber, notes, UintNote.fromNote)` -- build inclusion + non-nullification proofs -5. `wallet.buildKeyNoteProofData(keyRegistry, owner, blockNumber)` -- build key proof -6. `signMigrationModeB(signer, oldVersion, newVersion, notes, recipient, newApp)` -- sign -7. `newApp.migrate_mode_b(amount, signature, proofs, blockHeader, owner, publicKeys, partialAddress, keyProof, nhk)` -- claim on new rollup - -Details for each function follow below. +```typescript +msk = sha512ToGrumpkinScalar([secretKey, DOM_SEP__MSK_M_GEN]) +mpk = msk * G (Grumpkin generator) +``` -## Migration Key Rationale +No additional key management is needed. See [security](security.md#migration-key-compromise) for the full analysis. -Migration uses a dedicated keypair (`msk`/`mpk`) rather than the account's existing signing keys for three reasons: +## Prerequisites -1. **Account contract independence.** Migration claims must not depend on the old rollup's account contract executing correctly -- the old rollup may have been upgraded precisely because of bugs in those contracts. A separate keypair avoids this dependency. -2. **Cross-rollup proof compatibility.** The migration circuit needs to verify a signature against a key that is provably bound to the note owner. Standard Aztec account keys are not committed in a form that is easily provable across rollups. -3. **Scoped risk.** If the migration key is compromised, only migration claims are at risk -- not the user's general account security. See [security](security.md#migration-key-compromise) for the full analysis. +This guide assumes that the L1 `Migrator` contract and the `MigrationArchiveRegistry` on the new rollup are already deployed. The V2 app contract must be configured with: -The `msk` is derived deterministically from the account's secret key, so no additional key management is needed. See [Key Derivation](#key-derivation) for details. +- **`old_rollup_app_address`** -- The V1 contract address on the old rollup (set in the V2 constructor). +- **`archive_registry_address`** -- The `MigrationArchiveRegistry` address on the new rollup (set in the V2 constructor). -## Proof Data Types +--- -The following types represent the proof structures exchanged between the Noir library and the TS client. Field-level details are documented in the [spec](spec/migration-spec.md); this section provides a summary for integrators. +## Mode A -- Cooperative Lock-and-Claim -### Proof Structures +Mode A requires the old rollup to be live. Users pre-lock their state on the old rollup, then claim it on the new rollup with a proof. -| Type | Source | Purpose | -|------|--------|---------| -| `NoteProofData` | `note_proof_data.nr` | Note-hash inclusion proof (generic over note type). Fields: `data`, `randomness`, `nonce`, `leaf_index`, `sibling_path`. | -| `MigrationNoteProofData` | `mode_a/mod.nr` (type alias) | Type alias for `NoteProofData`. Used in Mode A claim flows. | -| `FullNoteProofData` | `mode_b/mod.nr` | Combines `NoteProofData` with `NonNullificationProofData`. Used in Mode B private migration. | -| `NonNullificationProofData` | `mode_b/non_nullification_proof_data.nr` | Low-nullifier membership witness proving a note has not been nullified. Fields: `low_nullifier_value`, `low_nullifier_next_value`, `low_nullifier_next_index`, `low_nullifier_leaf_index`, `low_nullifier_sibling_path`. | -| `PublicStateSlotProofData` | `mode_b/public_state_proof_data.nr` | Single public data tree leaf proof. Fields: `next_slot`, `next_index`, `leaf_index`, `sibling_path`. Note: there is no `value` field -- the value comes from `data.pack()` in the parent `PublicStateProofData`. | -| `PublicStateProofData` | `mode_b/public_state_proof_data.nr` | Bundle of `data: T` and `slot_proof_data: [PublicStateSlotProofData; N]`, one proof per packed field. | -| `KeyNoteProofData` | type alias for `NoteProofData` | Inclusion proof for the `MigrationKeyNote` in the old rollup's note hash tree. Used in Mode B for key ownership verification. | +### Contract Integration (Noir) -### Additional Structs +#### Lock Side (V1 contract on old rollup) -| Type | Source | Fields | Notes | -|------|--------|--------|-------| -| `MigrationNote` | `mode_a/migration_note.nr` | `note_creator: AztecAddress`, `mpk: Point`, `destination_rollup: Field`, `migration_data_hash: Field` | Created by `lock_migration_notes`. Consumed by `migrate_notes_mode_a`. | -| `MigrationKeyNote` | `migration-key-registry/migration_key_note.nr` | `mpk: Point` | Used by `MigrationKeyRegistry`. TS counterpart is `KeyNote` (from `mode-b/types.ts`), which has a different representation (`mpk` is expanded into `{ x, y, is_infinite }`). | -| `MigrationDataEvent` | `mode_a/migration_data_event.nr` | `migration_data: T` | Emitted by `lock_migration_notes`. No dedicated TS type -- events are decoded via the general event decoding mechanism. Integrators receive raw event data, not a typed `MigrationDataEvent`. | -| `MigrationSignature` | `signature.nr` | `bytes: [u8; 64]` | Accepted by all `migrate_*` functions. TS counterpart is `MigrationSignature` interface in `ts/aztec-state-migration/types.ts`. | +Import `MigrationLock` and `Point` from the library: -### Noir-to-TS Type Mapping +```rust +use aztec_state_migration::{mode_a::MigrationLock, Point}; +``` -| Noir Type | Noir File | TS Type | TS File | Re-exported from `index.ts`? | -|-----------|-----------|---------|---------|------------------------------| -| `NoteProofData` | `note_proof_data.nr` | `NoteProofData` | `ts/aztec-state-migration/types.ts` | Yes | -| `MigrationNoteProofData` | `mode_a/mod.nr` (alias) | `MigrationNoteProofData` | `ts/aztec-state-migration/mode-a/index.ts` | No (import from `mode-a/`) | -| `FullNoteProofData` | `mode_b/mod.nr` | `FullProofData` | `ts/aztec-state-migration/mode-b/types.ts` | No (import from `mode-b/`) | -| `NonNullificationProofData` | `mode_b/non_nullification_proof_data.nr` | `NonNullificationProofData` | `ts/aztec-state-migration/mode-b/types.ts` | No (import from `mode-b/`) | -| `PublicStateProofData` | `mode_b/public_state_proof_data.nr` | `PublicDataProof` | `ts/aztec-state-migration/mode-b/types.ts` | No (import from `mode-b/`) | -| `PublicStateSlotProofData` | `mode_b/public_state_proof_data.nr` | `PublicDataSlotProof` | `ts/aztec-state-migration/mode-b/types.ts` | No (import from `mode-b/`) | -| `KeyNoteProofData` (alias) | `mode_b/` | `NoteProofData` | `ts/aztec-state-migration/mode-b/types.ts` | No (`KeyNote` from `mode-b/`) | -| `MigrationKeyNote` | `migration_key_note.nr` | `KeyNote` | `ts/aztec-state-migration/mode-b/types.ts` | No (import from `mode-b/`) | -| `MigrationNote` | `mode_a/migration_note.nr` | `MigrationNote` | `ts/aztec-state-migration/mode-a/index.ts` | No (import from `mode-a/`) | -| `MigrationSignature` | `signature.nr` | `MigrationSignature` | `ts/aztec-state-migration/types.ts` | No (returned by signing helpers, not independently re-exported) | -| `Point` (alias for `EmbeddedCurvePoint`) | `lib.nr` re-export | `Point` (from `@aztec/foundation/schemas`) | Aztec native type | N/A | -| `Scalar` (alias for `EmbeddedCurveScalar`) | `lib.nr` re-export | `Scalar` (Aztec native type) | Aztec native type | N/A | +Use the `MigrationLock` builder to lock state: -**Naming discrepancies to note:** -- `PublicStateSlotProofData` (Noir) vs `PublicDataSlotProof` (TS) -- `PublicStateProofData` (Noir) vs `PublicDataProof` (TS) -- `MigrationKeyNote` (Noir) vs `KeyNote` (TS) +```rust +#[external("private")] +fn lock_for_migration_mode_a(private_amount: u128, public_amount: u128, destination_rollup: Field, mpk: Point) { + let note_owner = self.msg_sender(); -## TS Client Data Flow -- Mode A + // 1. Create migration note + emit encrypted event + MigrationLock::new(self.context, mpk, note_owner, destination_rollup) + .lock_state(private_amount + public_amount) + .finish(); -The Mode A (cooperative lock-and-claim) client flow follows this sequence: + // 2. Subtract from user's private balance + self.storage.private_balances.at(note_owner).sub(private_amount) + .deliver(MessageDelivery.ONCHAIN_CONSTRAINED); -1. **Derive migration key:** `deriveMasterMigrationSecretKey(secretKey)` returns a `GrumpkinScalar` used for signing. -2. **Sign the claim message:** `signMigrationModeA(signer, oldRollupVersion, newRollupVersion, migrationNotes, recipient, newAppAddress)` produces a `MigrationSignature` over `poseidon2_hash([DOM_SEP__CLAIM_A, oldVersion, newVersion, notesHash, recipient, newApp])`. -3. **Build migration note proofs:** `buildMigrationNoteProof(node, blockNumber, noteDao, migrationDataEvent)` builds a `MigrationNoteProofData` that includes the note inclusion proof with the original migration data from the encrypted event. -4. **Build block header:** `buildArchiveProof(node, blockHash)` or `buildBlockHeader(node, blockReference)` produces the Noir-compatible block header for archive verification. -5. **Submit transaction** to the new rollup's app contract. + // 3. Enqueue public balance decrement (executes after private phase) + // If this fails (insufficient balance), the entire tx reverts including the MigrationNote + AppV1::at(self.context.this_address()) + ._decrement_public_balance(note_owner, public_amount) + .enqueue(self.context); +} +``` -**Retrieving encrypted events:** `MigrationBaseWallet.getMigrationDataEvents(abiType, eventFilter)` retrieves encrypted `MigrationDataEvent` data emitted during the lock step. The method filters on the `MigrationDataEvent` event selector and decodes the event payload using the provided ABI type. +Each `.lock_state(data)` call creates a `MigrationNote` and emits a `MigrationDataEvent` with an auto-incrementing `data_id` (starting at 0). The `data` can be any type implementing `Packable + Serialize`. -> **Known behavior:** `getMigrationNotes()` returns all migration notes including already-migrated ones. Filtering by nullifier status requires cross-rollup queries (nullifiers are on the new rollup, notes on the old), which is non-trivial. Integrators should filter on the client side. +**Multiple entrypoints:** If a contract has separate lock functions (e.g. one for private, one for public state), use `new_with_offset` to avoid `data_id` collisions: -## TS Client Data Flow -- Mode B (Private) +```rust +// In lock_private(): data_id starts at 0 +MigrationLock::new(self.context, mpk, owner, dest) + .lock_state(private_balance) + .finish(); -The Mode B (emergency snapshot) private note migration flow: +// In lock_public(): data_id starts at 1 +MigrationLock::new_with_offset(self.context, mpk, owner, dest, 1) + .lock_state(public_balance) + .finish(); +``` -1. **Derive migration key:** `deriveMasterMigrationSecretKey(secretKey)` -- same as Mode A. -2. **Sign the claim message:** `signMigrationModeB(signer, oldRollupVersion, newRollupVersion, notes, recipient, newAppAddress)` produces a `MigrationSignature` over `poseidon2_hash([DOM_SEP__CLAIM_B, oldVersion, newVersion, notesHash, recipient, newApp])`. -3. **Build note proofs:** Use `MigrationBaseWallet.buildFullNoteProofs(blockNumber, notes, noteMapper)` to construct combined inclusion and non-nullification proofs (`FullProofData`). This internally calls `buildNoteProof` + `buildNullifierProof` for each note. -4. **Build archive proof:** `buildArchiveProof(node, blockHash)` -- same as Mode A. -5. **Submit transaction** to the new rollup's app contract. +**Batching:** It is recommended to batch multiple pieces of state into one `.lock_state()` call via a custom struct: -Mode-B types are NOT re-exported from the top-level `index.ts`. Import them directly: +```rust +#[derive(Packable, Serialize)] +struct MigrationData { balance: u128, extra: Field } -```typescript -import { - FullProofData, - NonNullificationProofData, - PublicDataSlotProof, - PublicDataProof, - KeyNote, -} from "aztec-state-migration/mode-b"; +MigrationLock::new(self.context, mpk, owner, dest) + .lock_state(MigrationData { balance, extra }) + .finish(); ``` -## TS Client Data Flow -- Mode B (Public) +#### Claim Side (V2 contract on new rollup) -Public state migration uses a separate set of proof builders: +Import the Mode A builder and types: -1. **Build public data proofs:** - - `buildPublicDataProof(node, blockNumber, data, contractAddress, baseSlot, dataAbiType)` -- For standalone `PublicMutable` values. Automatically determines the number of packed slots from the ABI type. - - `buildPublicMapDataProof(node, blockNumber, data, contractAddress, baseSlot, mapKeys, dataAbiType)` -- For values inside `Map` storage. Derives the storage slot from `baseSlot` and `mapKeys` via `poseidon2_hash_with_separator([slot, key], DOM_SEP__PUBLIC_STORAGE_MAP_SLOT)` for each nesting level. - - `buildPublicDataSlotProof(node, blockNumber, contractAddress, storageSlot)` -- Low-level single-slot proof builder. - -2. **Sign for owned entries:** `signPublicStateMigrationModeB(signer, oldRollupVersion, newRollupVersion, data, abiType, recipient, newAppAddress)` produces a `MigrationSignature` over `poseidon2_hash([DOM_SEP__CLAIM_B, oldVersion, newVersion, dataHash, recipient, newApp])` where `dataHash = poseidon2_hash(pack(data))`. +```rust +use aztec_state_migration::{ + MigrationSignature, + mode_a::{MigrationModeA, MigrationNoteProofData}, + Point, +}; +``` -3. **Submit transaction** to the new rollup's app contract. +Verify the lock proof and mint on the new rollup: + +```rust +#[external("private")] +fn migrate_mode_a( + mpk: Point, + signature: MigrationSignature, + note_proof_data: MigrationNoteProofData, + block_header: BlockHeader, +) { + let recipient = self.msg_sender(); + let old_app = self.storage.old_rollup_app_address.read(); + let amount = note_proof_data.data; + + MigrationModeA::new( + self.context, + old_app, + self.storage.archive_registry_address.read(), + block_header, + mpk, + ) + .with_note(note_proof_data) + .finish(recipient, signature); + + // App-specific: mint tokens to the recipient + self.storage.private_balances.at(recipient).add(amount) + .deliver(MessageDelivery.ONCHAIN_CONSTRAINED); +} +``` -## Wallet and Account Classes +Multiple notes can be chained: `.with_note(proof1).with_note(proof2).finish(recipient, sig)`. -The migration library provides two separate inheritance chains for managing accounts and constructing proofs. +### Client Side (TypeScript SDK) -### Account Hierarchy (Authentication Layer) +The full Mode A client flow: -The account classes handle signing and key access: +#### 1. Derive migration key -- **`MigrationAccount`** (interface) -- Extends `Account` with `getMigrationPublicKey()`, `migrationKeySigner(msg)`, `getMaskedNhk(mask: Fq)`, `getNhkApp(contractAddress)`, and `getPublicKeys()`. -- **`MigrationAccountWithSecretKey`** (class, extends `AccountWithSecretKey`) -- Default implementation that derives and stores all migration keys in memory. Suitable for testing; production wallets should protect key material. -- **`SignerlessMigrationAccount`** (class, implements `MigrationAccount`) -- Placeholder for fee-less transactions using `DefaultMultiCallEntrypoint`. All signing methods throw. Used for public-only operations that do not require authentication. +```typescript +import { deriveMasterMigrationSecretKey } from "aztec-state-migration"; -### Wallet Hierarchy (State and Proof Building) +const msk = deriveMasterMigrationSecretKey(secretKey); +const mpk = msk.toPublicKey(); // Grumpkin point +``` -The wallet classes handle proof construction and note management: +#### 2. Lock state on old rollup -- **`MigrationBaseWallet`** (abstract, extends `BaseWallet`) -- Contains the core proof-building and signing methods. Subclasses must implement `getMigrationPublicKey(account)` and `getPublicKeys(account)`. -- **`MigrationEmbeddedWallet`** (extends `MigrationBaseWallet`) -- Adds account registry and creation helpers. NOT re-exported from top-level `index.ts`; import from `aztec-state-migration/wallet`. -- **`NodeMigrationEmbeddedWallet`** (extends `MigrationEmbeddedWallet`) -- Node.js entrypoint with PXE creation and account deployment. Use for server-side / test environments. -- **`BrowserMigrationEmbeddedWallet`** (extends `MigrationEmbeddedWallet`) -- Browser entrypoint. Use for client-side web applications. +```typescript +await oldContract.methods + .lock_for_migration_mode_a(privateAmount, publicAmount, newRollupVersion, mpk) + .send() + .wait(); +``` -**Key methods on `MigrationBaseWallet`:** -- `buildFullNoteProofs(blockNumber, notes, noteMapper)` -- Build complete note proofs (inclusion + non-nullification) for Mode B private migration. -- `buildKeyNoteProofData(keyRegistry, owner, blockNumber)` -- Build proof data for the migration key note from the old rollup's key registry. -- `getMigrationDataEvents(abiType, eventFilter)` -- Retrieve encrypted migration data events from Mode A lock transactions. -- `buildMigrationNoteProofs(blockNumber, migrationNotes, migrationDataEvents)` -- Build note proofs for Mode A claim transactions, pairing each note with its corresponding event data. +#### 3. Bridge archive root -These methods combine multiple lower-level proof-building functions into higher-level wrapper methods. Integrators should prefer these over calling `buildNoteProof`, `buildNullifierProof`, etc. directly. +After locking, a proven archive root that covers the lock transaction must be bridged to the new rollup via L1. This makes the old rollup's state provable on the new rollup and is required before any claim can succeed. -For the end-to-end wallet flow in each migration mode, see the Wallet Integration sections in [Mode A Specification](spec/mode-a-spec.md#wallet-integration) and [Mode B Specification](spec/mode-b-spec.md#wallet-integration). +#### 4. Retrieve migration notes and data -## Key Derivation +```typescript +// Single data type per contract +const notesAndData = await wallet.getMigrationNotesAndData( + contractAddress, + owner, + abiType, // AbiType for the locked data (e.g. AbiType for u128) +); + +// Multiple data types (when contract uses data_id offsets) +const mixed = await wallet.getMixedMigrationNotesAndData( + contractAddress, + owner, + { 0: privateBalanceAbiType, 1: publicBalanceAbiType }, // Record +); +``` -The master migration secret key (MSK) is derived from the account's secret key: +#### 5. Filter already-migrated notes (optional) -``` -msk = sha512ToGrumpkinScalar([secretKey, DOM_SEP__MSK_M_GEN]) +```typescript +const pending = await newWallet.filterOutMigratedNotes(newContractAddress, notesAndData); ``` -The migration public key (MPK) is the corresponding Grumpkin curve point. +#### 6. Build proofs -**Constants** (defined in TS `constants.ts` -- key derivation is entirely TS-side): -- `DOM_SEP__MSK_M_GEN` -- Domain separator for MSK derivation. Poseidon2 hash of `"migration-secret-key"`. +```typescript +import { buildArchiveProof } from "aztec-state-migration"; -> **Known limitation:** TS constants (`constants.ts`) and Noir constants (`constants.nr`) are maintained independently with no cross-validation. Changes to domain separators or storage slots must be synchronized manually. +const noteProof = await wallet.buildMigrationNoteProof(blockNumber, notesAndData[0]); +const archiveProof = await buildArchiveProof(oldNode, blockHash); +``` -## Import Patterns +#### 7. Sign -### Top-Level Exports (`aztec-state-migration`) +```typescript +const signer = await wallet.getMigrationSignerFromAddress(owner); +const signature = await wallet.signMigrationModeA( + signer, recipient, oldRollupVersion, newRollupVersion, newAppAddress, [noteProof], +); +``` -The top-level `index.ts` exports the following (wallet classes are NOT included): +#### 8. Submit claim -- **Keys:** `deriveMasterMigrationSecretKey`, `signMigrationModeA`, `signMigrationModeB`, `signPublicStateMigrationModeB` -- **Proofs:** `buildNoteProof`, `buildArchiveProof`, `buildBlockHeader` -- **Bridge:** `waitForBlockProof`, `migrateArchiveRootOnL1`, `waitForL1ToL2Message` -- **Noir helpers:** `blockHeaderToNoir` (via `noir-helpers/index.ts`) -- **Polling:** `poll`, `PollOptions` (type) -- **Types:** `NoteProofData` (type), `ArchiveProofData` (type), `L1MigrationResult` (type) +```typescript +await newContract.methods.migrate_mode_a( + mpk, signature, noteProof, archiveProof.archive_block_header, +).send().wait(); +``` -### Wallet Sub-module (`aztec-state-migration/wallet`) +--- -Wallet and account classes must be imported from the wallet sub-module: +## Mode B -- Emergency Snapshot Migration -- `MigrationAccount` (interface), `MigrationAccountWithSecretKey`, `SignerlessMigrationAccount` -- `MigrationBaseWallet`, `MigrationEmbeddedWallet` -- `NodeMigrationEmbeddedWallet` (from `aztec-state-migration/wallet/entrypoints/node`) -- `BrowserMigrationEmbeddedWallet` (from `aztec-state-migration/wallet/entrypoints/browser`) +Mode B does not require the old rollup to be live. It uses a fixed snapshot height and Merkle proofs against the old rollup's state trees. -### Mode-A Sub-module (`aztec-state-migration/mode-a/`) +### Contract Integration (Noir) -- `MigrationNote`, `MigrationNoteProofData` (type), `buildMigrationNoteProof` +Import the Mode B builder and types: -### Mode-B Sub-module (`aztec-state-migration/mode-b/`) +```rust +use aztec_state_migration::{ + MigrationSignature, Scalar, + mode_b::{FullNoteProofData, KeyNoteProofData, MigrationModeB, PublicStateProofData}, +}; +``` -- `FullProofData` (type), `NonNullificationProofData` (type), `PublicDataSlotProof` (type), `PublicDataProof` (type) -- `KeyNote` -- `buildPublicDataSlotProof`, `buildPublicDataProof`, `buildPublicMapDataProof` +#### Private Notes + +```rust +#[external("private")] +fn migrate_mode_b( + signature: MigrationSignature, + full_proof_data: FullNoteProofData, + block_header: BlockHeader, + notes_owner: AztecAddress, + public_keys: PublicKeys, + partial_address: Field, + key_note: KeyNoteProofData, + nhk: Scalar, +) { + let recipient = self.msg_sender(); + let old_app = self.storage.old_rollup_app_address.read(); + let balances_slot = STORAGE_LAYOUT_V1.fields.private_balances.slot; + let amount = full_proof_data.note_proof_data.data.value; + + MigrationModeB::new( + self.context, old_app, + self.storage.archive_registry_address.read(), + block_header, + ) + .with_notes_owner(notes_owner, key_note, public_keys, partial_address, nhk) + .with_note(full_proof_data, balances_slot) + .finish(recipient, signature); + + // App-specific: mint tokens + self.storage.private_balances.at(recipient).add(amount) + .deliver(MessageDelivery.ONCHAIN_CONSTRAINED); +} +``` -### NOT Re-exported (Import Directly) +**Custom notes** must use the canonical nullifier formula. Use `assert_note_has_canonical_nullifier` in tests to verify: -- `UintNote`, `FieldNote` from `ts/aztec-state-migration/common-notes.ts` -- `MigrationSignature` from `ts/aztec-state-migration/types.ts` (returned by signing helpers; not independently re-exported) +```rust +use aztec_state_migration::mode_b::assert_note_has_canonical_nullifier; -## Common Pitfalls and Utilities +#[test] +unconstrained fn assert_canonical_nullifier() { + let note = NFTNote { token_id: 0x12345 }; + assert_note_has_canonical_nullifier(note); +} +``` -### Common Note Decoders +#### Owned Public State + +```rust +#[external("private")] +fn migrate_public_balance_mode_b( + proof_data: PublicStateProofData, + block_header: BlockHeader, + old_owner: AztecAddress, + signature: MigrationSignature, + key_note: KeyNoteProofData, +) { + let amount = proof_data.data; + let recipient = self.msg_sender(); + let old_app = self.storage.old_rollup_app_address.read(); + let base_slot = STORAGE_LAYOUT_V1.fields.public_balances.slot; + + MigrationModeB::new( + self.context, old_app, + self.storage.archive_registry_address.read(), + block_header, + ) + .with_owner(old_owner, key_note) + .with_public_map_state(proof_data, base_slot, [old_owner]) + .finish(recipient, signature); + + // App-specific: mint to public balance + Self::at(self.context.this_address()) + ._mint_to_public_external(recipient, amount) + .enqueue(self.context); +} +``` -`ts/aztec-state-migration/common-notes.ts` provides `UintNote` and `FieldNote` decoder callbacks used as `noteMapper` parameters in proof-building functions: +Use `.with_public_state(proof, slot)` for standalone `PublicMutable`, and `.with_public_map_state(proof, slot, [key1, key2])` for nested `Map` entries. -```typescript -import { UintNote } from "aztec-state-migration/common-notes"; +#### Unowned Public State + +For global state with no ownership (e.g. total supply): -const proofs = await wallet.buildFullNoteProofs(blockNumber, notes, UintNote.fromNote); +```rust +MigrationModeB::new(context, old_app, archive_registry, block_header) + .without_owner() + .with_public_state(proof, slot) + .finish(); // no signature needed ``` -- `UintNote.fromNote(note)` -- Decodes `note.items[0]` as a `bigint`. -- `FieldNote.fromNote(note)` -- Decodes `note.items[0]` as an `Fr`. +#### Mixed (Public + Private) -These are NOT re-exported from any `index.ts`; import directly from `common-notes.ts`. +The builder supports chaining owned public state with private notes: -### blockHeaderToNoir +```rust +MigrationModeB::new(context, old_app, archive_registry, block_header) + .with_owner(owner, key_note) + .with_public_state(public_proof, public_slot) + .with_notes_owner(public_keys, partial_address, nhk) + .with_note(note_proof, note_slot) + .finish(recipient, signature); +``` -`blockHeaderToNoir(header)` converts an L2 `BlockHeader` to the Noir-compatible struct format with snake_case keys. This is used internally by `buildArchiveProof` and `buildBlockHeader`, but is also available for direct use when constructing custom transaction payloads. +### Client Side (TypeScript SDK) -### poll and onPoll +#### 0. Register migration key (before snapshot, on old rollup) -The `poll(opts)` utility repeatedly calls a `check()` function until it returns a non-`undefined` value. The optional `onPoll` callback fires after each unsuccessful check -- commonly used to trigger block production in test environments. +```typescript +await keyRegistry.methods.register(mpk).send().wait(); +``` -### buildNullifierProof (NOT Exported) +#### 1. Build proofs -`buildNullifierProof` is NOT exported from `mode-b/`'s public API. Integrators should use `MigrationBaseWallet.buildFullNoteProofs()` (or `buildNullifierProofs()`), which internally calls `buildNullifierProof` for each note. +```typescript +import { buildArchiveProof } from "aztec-state-migration"; +import { buildPublicDataProof, buildPublicMapDataProof } from "aztec-state-migration/mode-b"; -### migrateArchiveRootAtBlock (No TS Wrapper) +// Private notes: inclusion + non-nullification +const fullProof = await wallet.buildFullNoteProof(blockNumber, noteDao, UintNote.fromNote); -The Solidity function `migrateArchiveRootAtBlock(uint256 oldVersion, uint256 blockNumber, DataStructures.L2Actor calldata l2Migrator)` has no TS wrapper in `bridge.ts`. Only `migrateArchiveRoot` (latest proven block) is wrapped. Integrators needing historical block bridging must call the Solidity contract directly via viem or ethers. +// Key note proof +const keyProof = await wallet.buildKeyNoteProofData(keyRegistryAddress, owner, blockNumber); -### On-Curve Assertion +// Public state (standalone PublicMutable) +const publicProof = await buildPublicDataProof(node, blockNumber, data, contract, baseSlot, abiType); -`register()` (in `MigrationKeyRegistry`) and `lock_migration_notes()` (in `noir/aztec-state-migration/src/mode_a/ops.nr`) include an on-curve assertion (`y^2 = x^3 - 17`) for Grumpkin points. If an invalid key is provided, the transaction will revert with `"mpk not on Grumpkin curve"`. Ensure the migration public key is a valid Grumpkin point before calling these functions. +// Public state (Map entry) +const mapProof = await buildPublicMapDataProof(node, blockNumber, data, contract, baseSlot, [mapKey], abiType); -## Deployment Checklist +// Archive proof +const archiveProof = await buildArchiveProof(node, blockHash); +``` -Before running a migration, the following deployment steps are required: +#### 2. Sign -1. **Set `old_rollup_app_address`:** Configure the old rollup's app contract address in the new rollup's app contract via the constructor. Incorrect configuration results in silent migration failures (see [security](security.md)). +```typescript +// Private notes +const sig = await wallet.signMigrationModeB( + signer, recipient, oldVersion, newVersion, newApp, [fullProof], +); + +// Public state (owned) +const sig = await wallet.signPublicStateMigrationModeB( + signer, recipient, oldVersion, newVersion, newApp, data, abiType, +); +``` -2. **Deploy `MigrationArchiveRegistry`:** The constructor requires: - - `l1_migrator: EthAddress` -- Address of the `Migrator.sol` contract on L1. - - `old_rollup_version: Field` -- Version identifier of the old rollup (read from `block_header.global_variables.version`). - - `old_key_registry: AztecAddress` -- Address of the `MigrationKeyRegistry` on the old rollup (Mode B only, used for key note siloing via `get_old_key_registry()`). +#### 3. Submit claim -3. **Bridge first archive root:** Call `migrateArchiveRootOnL1(...)` to bridge the old rollup's proven archive root to the new rollup via L1. Then wait for the L1-to-L2 message to sync (`waitForL1ToL2Message`). The new rollup's `MigrationArchiveRegistry` must have a registered block before any migration transactions can succeed. +```typescript +// Private note claim +await newContract.methods.migrate_mode_b( + sig, fullProof, archiveProof.archive_block_header, + owner, publicKeys, partialAddress, keyProof, nhk, +).send().wait(); + +// Public balance claim +await newContract.methods.migrate_public_balance_mode_b( + publicProof, archiveProof.archive_block_header, + owner, sig, keyProof, +).send().wait(); +``` + +--- ## See Also - [General Specification](spec/migration-spec.md) -- Proof data type field details, API tables - [Mode A Specification](spec/mode-a-spec.md) -- Cooperative lock-and-claim migration flow -- [Mode B Specification](spec/mode-b-spec.md) -- Emergency snapshot migration flow (private and public) +- [Mode B Specification](spec/mode-b-spec.md) -- Emergency snapshot migration flow +- [Architecture](architecture.md) -- Deployment topology, component catalog +- [Security](security.md) -- Trust assumptions, threat model - [README](../README.md) -- Setup, testing, troubleshooting