diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25b32ae..a360357 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,8 @@ jobs: run: yarn noir:compile && yarn noir:codegen - name: Compile Solidity contracts run: yarn sol:compile + - name: Build TypeScript SDK + run: yarn ts:build - name: Run hash compatibility check run: bash e2e-tests/hash-compatibility.sh - name: Run noir/aztec unit tests diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2571d71 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,73 @@ +name: Publish SDK to npm + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 0.2.0). Leave empty to use package.json version.' + required: false + type: string + dry_run: + description: 'Dry run (no actual publish)' + required: false + type: boolean + default: true + +permissions: + contents: read + +env: + NODE_VERSION: '24.12.0' + AZTEC_VERSION: 4.0.0-devnet.2-patch.0 + +jobs: + publish: + name: Build & Publish + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: publish-sdk-npm + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: 'https://registry.npmjs.org' + + - uses: ./.github/actions/setup-aztec + with: + version: ${{ env.AZTEC_VERSION }} + + - name: Install dependencies + run: corepack enable && yarn install --frozen-lockfile + + - name: Compile Noir contracts & generate bindings + run: yarn noir + + - name: Build SDK + run: yarn ts:build + + - name: Set version + if: inputs.version != '' + working-directory: ts/aztec-state-migration + env: + VERSION: ${{ inputs.version }} + run: | + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]] || { echo "Invalid semver: $VERSION"; exit 1; } + npm version "$VERSION" --no-git-tag-version + + - name: Publish (dry run) + if: inputs.dry_run + working-directory: ts/aztec-state-migration + run: npm publish --access public --dry-run + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish + if: ${{ !inputs.dry_run }} + working-directory: ts/aztec-state-migration + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index a653bb1..9f3a8f7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ dependencies target codegenCache.json cache -ts/aztec-state-migration/noir-contracts ts/aztec-state-migration/artifacts e2e-tests/artifacts diff --git a/README.md b/README.md index a17317c..0025b95 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ yarn sol:deps yarn noir:compile # Compile Noir contracts yarn noir:codegen # Generate TypeScript bindings yarn sol:compile # Compile Solidity contracts -yarn clean # Remove all compiled artifacts +yarn ts:build # Build typescript SDK +yarn clean # Remove all compiled and built artifacts ``` ## Formatting diff --git a/e2e-tests/deploy-types.ts b/e2e-tests/deploy-types.ts index fd80235..462c7b0 100644 --- a/e2e-tests/deploy-types.ts +++ b/e2e-tests/deploy-types.ts @@ -9,7 +9,7 @@ import type { import type { AztecNode } from "@aztec/aztec.js/node"; import type { EmbeddedWallet } from "@aztec/wallets/embedded"; import type { AccountManager } from "@aztec/aztec.js/wallet"; -import { MigrationEmbeddedWallet } from "../ts/aztec-state-migration/wallet/migration-embedded-wallet.js"; +import { MigrationEmbeddedWallet } from "aztec-state-migration/wallet/base"; export interface DeploymentResult { [rollupVersion: number]: { diff --git a/e2e-tests/deploy.ts b/e2e-tests/deploy.ts index 7a007c8..0529628 100644 --- a/e2e-tests/deploy.ts +++ b/e2e-tests/deploy.ts @@ -18,7 +18,7 @@ import { fileURLToPath } from "url"; import { getPXEConfig } from "@aztec/pxe/server"; import { getInitialTestAccountsData } from "@aztec/accounts/testing"; import type { DeploymentResult } from "./deploy-types.js"; -import { NodeMigrationEmbeddedWallet } from "../ts/aztec-state-migration/wallet/index.js"; +import { NodeMigrationEmbeddedWallet } from "aztec-state-migration/wallet"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -32,23 +32,6 @@ const ETHEREUM_RPC_URL = const ANVIL_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; -// ============================================================ -// L1 ABIs (exported for event parsing in tests) -// ============================================================ -export const L1MigratorAbi = parseAbi([ - "constructor(address _registry, address _poseidon2)", - "function migrateArchiveRoot(uint256 oldVersion, (bytes32 actor, uint256 version) l2Migrator) external returns (bytes32 leaf, uint256 leafIndex)", - "function getArchiveInfo(uint256 version) external view returns (bytes32 archiveRoot, uint256 provenBlockNumber)", - "function REGISTRY() external view returns (address)", - "function POSEIDON2() external view returns (address)", - "function SECRET_HASH_FOR_ZERO() external view returns (bytes32)", - "event ArchiveRootMigrated(uint256 indexed oldVersion, uint256 indexed newVersion, bytes32 indexed l2Migrator, bytes32 archiveRoot, uint256 provenBlockNumber, bytes32 messageLeaf, uint256 messageLeafIndex)", -]); - -export const InboxAbi = parseAbi([ - "event MessageSent(uint256 indexed checkpointNumber, uint256 index, bytes32 indexed hash, bytes16 rollingHash)", -]); - // ============================================================ // Bytecode loaders // ============================================================ diff --git a/e2e-tests/migration-key-registry.test.ts b/e2e-tests/migration-key-registry.test.ts index 1ee6e60..41cd1e6 100644 --- a/e2e-tests/migration-key-registry.test.ts +++ b/e2e-tests/migration-key-registry.test.ts @@ -1,6 +1,6 @@ import { EmbeddedWallet } from "@aztec/wallets/embedded"; import { createAztecNodeClient } from "@aztec/aztec.js/node"; -import { MigrationKeyRegistryContract } from "../ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.js"; +import { MigrationKeyRegistryContract } from "aztec-state-migration/noir-contracts"; import { Fq, Fr } from "@aztec/foundation/curves/bn254"; import { getInitialTestAccountsData } from "@aztec/accounts/testing"; import { generatePublicKey } from "@aztec/aztec.js/keys"; diff --git a/e2e-tests/migration-mode-a.test.ts b/e2e-tests/migration-mode-a.test.ts index b9d8591..c7f1b15 100644 --- a/e2e-tests/migration-mode-a.test.ts +++ b/e2e-tests/migration-mode-a.test.ts @@ -1,7 +1,7 @@ import { ExampleMigrationAppV1Contract } from "./artifacts/ExampleMigrationAppV1.js"; import { ExampleMigrationAppV2Contract } from "./artifacts/ExampleMigrationAppV2.js"; import { Fr } from "@aztec/foundation/curves/bn254"; -import { signMigrationModeA } from "../ts/aztec-state-migration/index.js"; +import { signMigrationModeA } from "aztec-state-migration/mode-a"; import { deploy } from "./deploy.js"; import { deployAppPair, @@ -106,12 +106,11 @@ async function main() { // Step 5: Bridge archive root // ============================================================ console.log("Step 5. Bridging archive root..."); - const { l1Result, provenBlockNumber, blockHeader } = await bridgeBlock( + const { provenBlockNumber, blockHeader } = await bridgeBlock( env, newArchiveRegistry, ); - console.log(` Proven block: ${l1Result.provenBlockNumber}`); - console.log(` Archive root: ${l1Result.provenArchiveRoot}\n`); + console.log(` Proven block: ${provenBlockNumber}`); // ============================================================ // Steps 6-7: Prepare migration args and call migrate on NEW rollup @@ -129,11 +128,12 @@ async function main() { `Expected exactly 1 migration note, but found ${lockNotesAndData.length}`, ); } + const lockNoteAndData = lockNotesAndData[0]; // Build proofs via wallet, combining note proofs with event data - const [migrationNoteProof] = await oldUserWallet.buildMigrationNoteProofs( + const migrationNoteProof = await oldUserWallet.buildMigrationNoteProof( provenBlockNumber, - lockNotesAndData, + lockNoteAndData, ); // Sign via standalone function @@ -248,12 +248,10 @@ async function main() { console.log("Step 10. Bridging archive root for public lock note..."); const { - l1Result: l1ResultPublic, provenBlockNumber: publicProvenBlockNumber, blockHeader: publicBlockHeader, } = await bridgeBlock(env, newArchiveRegistry); - console.log(` Proven block: ${l1ResultPublic.provenBlockNumber}`); - console.log(` Archive root: ${l1ResultPublic.provenArchiveRoot}\n`); + console.log(` Proven block: ${publicProvenBlockNumber}`); // ============================================================ // Step 11: Get public lock note and merkle proofs @@ -287,12 +285,12 @@ async function main() { `Expected exactly 1 migration note for the public lock, but found ${filteredNotes.length}`, ); } + const migrationNote = filteredNotes[0]; - const [publicMigrationNoteProof] = - await oldUserWallet.buildMigrationNoteProofs( - publicProvenBlockNumber, - filteredNotes, - ); + const publicMigrationNoteProof = await oldUserWallet.buildMigrationNoteProof( + publicProvenBlockNumber, + migrationNote, + ); // Sign via standalone function const publicSignature = await signMigrationModeA( diff --git a/e2e-tests/migration-mode-b.test.ts b/e2e-tests/migration-mode-b.test.ts index be3320f..1e29959 100644 --- a/e2e-tests/migration-mode-b.test.ts +++ b/e2e-tests/migration-mode-b.test.ts @@ -1,5 +1,5 @@ import { Fr } from "@aztec/foundation/curves/bn254"; -import { signMigrationModeB } from "../ts/aztec-state-migration/index.js"; +import { signMigrationModeB } from "aztec-state-migration/mode-b"; import { deploy } from "./deploy.js"; import { deployAppPair, @@ -12,8 +12,8 @@ import { } from "./test-utils.js"; import { ExampleMigrationAppV1Contract } from "./artifacts/ExampleMigrationAppV1.js"; import { ExampleMigrationAppV2Contract } from "./artifacts/ExampleMigrationAppV2.js"; -import { MigrationKeyRegistryContract } from "../ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.js"; -import { UintNote } from "../ts/aztec-state-migration/common-notes.js"; +import { MigrationKeyRegistryContract } from "aztec-state-migration/noir-contracts"; +import { UintNote } from "aztec-state-migration/common-notes"; import { NoteStatus } from "@aztec/stdlib/note"; async function main() { @@ -204,12 +204,12 @@ async function main() { } // The ExampleMigrationApp currently only creates one note per call. - const balanceNotes = balanceNotesActive.slice(0, 1); + const balanceNote = balanceNotesActive[0]; // Build proofs via wallet - const fullProofs = await oldUserWallet.buildFullNoteProofs( + const fullProof = await oldUserWallet.buildFullNoteProof( provenBlockNumber, - balanceNotes, + balanceNote, (note) => UintNote.fromNote(note), ); @@ -227,7 +227,7 @@ async function main() { oldMigrationSigner, blockHeader.global_variables.version, new Fr(env.newRollupVersion), - balanceNotes, + [balanceNote], newUserManager.address, newApp.address, ); @@ -240,8 +240,7 @@ async function main() { console.log("Step 10. Calling migrate_mode_b on NEW rollup..."); // The ExampleMigrationApp currently only supports migrating one note at a time. - const noteProof = fullProofs[0]; - const migrateAmount = noteProof.note_proof_data.data.value; + const migrateAmount = fullProof.note_proof_data.data.value; console.log(` Migrating amount: ${migrateAmount}`); const newBalanceBefore = await newAppUser.methods @@ -252,7 +251,7 @@ async function main() { await newAppUser.methods .migrate_mode_b( signature, - noteProof, + fullProof, blockHeader, oldUserManager.address, publicKeys, @@ -290,9 +289,9 @@ async function main() { // Take one nullified note const nullifiedNote = balanceNotesNullified[0]; - const [nullifiedNoteProof] = await oldUserWallet.buildFullNoteProofs( + const nullifiedNoteProof = await oldUserWallet.buildFullNoteProof( provenBlockNumber, - [nullifiedNote], + nullifiedNote, (note) => UintNote.fromNote(note), ); diff --git a/e2e-tests/migration-public-mode-b.test.ts b/e2e-tests/migration-public-mode-b.test.ts index 552b905..dd98b98 100644 --- a/e2e-tests/migration-public-mode-b.test.ts +++ b/e2e-tests/migration-public-mode-b.test.ts @@ -13,12 +13,12 @@ import { ExampleMigrationAppV2Contract, ExampleMigrationAppV2ContractArtifact, } from "./artifacts/ExampleMigrationAppV2.js"; -import { MigrationKeyRegistryContract } from "../ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.js"; +import { MigrationKeyRegistryContract } from "aztec-state-migration/noir-contracts"; import { AztecAddress } from "@aztec/stdlib/aztec-address"; import { buildPublicDataProof, buildPublicMapDataProof, -} from "../ts/aztec-state-migration/mode-b/proofs.js"; +} from "aztec-state-migration/mode-b"; import { randomBigInt } from "@aztec/foundation/crypto/random"; // Define a struct that matches the one used in the example app contract, diff --git a/e2e-tests/nft-migration-mode-a.test.ts b/e2e-tests/nft-migration-mode-a.test.ts index 2c67dbf..74eb233 100644 --- a/e2e-tests/nft-migration-mode-a.test.ts +++ b/e2e-tests/nft-migration-mode-a.test.ts @@ -1,7 +1,7 @@ import { NftMigrationAppV1Contract } from "./artifacts/NftMigrationAppV1.js"; import { NftMigrationAppV2Contract } from "./artifacts/NftMigrationAppV2.js"; import { Fr } from "@aztec/foundation/curves/bn254"; -import { signMigrationModeA } from "../ts/aztec-state-migration/index.js"; +import { signMigrationModeA } from "aztec-state-migration/mode-a"; import { deploy } from "./deploy.js"; import { deployNftAppPair, @@ -115,11 +115,11 @@ async function main() { // ============================================================ console.log("Step 5. Bridging archive root..."); - const { l1Result, provenBlockNumber, blockHeader } = await bridgeBlock( + const { provenBlockNumber, blockHeader } = await bridgeBlock( env, newArchiveRegistry, ); - console.log(` Proven block: ${l1Result.provenBlockNumber}\n`); + console.log(` Proven block: ${provenBlockNumber}\n`); // ============================================================ // Step 6: Get migration notes and build proofs @@ -136,10 +136,11 @@ async function main() { `Expected exactly 1 migration note, but found ${lockNotesAndData.length}`, ); } + const lockNoteAndData = lockNotesAndData[0]; - const [migrationNoteProof] = await oldUserWallet.buildMigrationNoteProofs( + const migrationNoteProof = await oldUserWallet.buildMigrationNoteProof( provenBlockNumber, - lockNotesAndData, + lockNoteAndData, ); const oldMigrationSigner = await oldUserWallet.getMigrationSignerFromAddress( @@ -259,11 +260,10 @@ async function main() { console.log("Step 11. Bridging archive root for public lock..."); const { - l1Result: l1ResultPublic, provenBlockNumber: publicProvenBlockNumber, blockHeader: publicBlockHeader, } = await bridgeBlock(env, newArchiveRegistry); - console.log(` Proven block: ${l1ResultPublic.provenBlockNumber}\n`); + console.log(` Proven block: ${publicProvenBlockNumber}\n`); // ============================================================ // Step 12: Get notes, filter, build proof @@ -293,12 +293,12 @@ async function main() { `Expected exactly 1 remaining note, but found ${filteredNotes.length}`, ); } + const filteredNote = filteredNotes[0]; - const [publicMigrationNoteProof] = - await oldUserWallet.buildMigrationNoteProofs( - publicProvenBlockNumber, - filteredNotes, - ); + const publicMigrationNoteProof = await oldUserWallet.buildMigrationNoteProof( + publicProvenBlockNumber, + filteredNote, + ); const publicSignature = await signMigrationModeA( oldMigrationSigner, diff --git a/e2e-tests/nft-migration-mode-b.test.ts b/e2e-tests/nft-migration-mode-b.test.ts index 604c221..63e5491 100644 --- a/e2e-tests/nft-migration-mode-b.test.ts +++ b/e2e-tests/nft-migration-mode-b.test.ts @@ -1,5 +1,5 @@ import { Fr } from "@aztec/foundation/curves/bn254"; -import { signMigrationModeB } from "../ts/aztec-state-migration/index.js"; +import { signMigrationModeB } from "aztec-state-migration/mode-b"; import { deploy } from "./deploy.js"; import { deployNftAppPair, @@ -228,9 +228,9 @@ async function main() { const activeNote = nftNotesActive[0]; - const fullProofs = await oldUserWallet.buildFullNoteProofs( + const fullProof = await oldUserWallet.buildFullNoteProof( provenBlockNumber, - [activeNote], + activeNote, (note) => NFTNote.fromNote(note), ); @@ -259,15 +259,14 @@ async function main() { // ============================================================ console.log("Step 10. Calling migrate_nft_mode_b on NEW rollup..."); - const noteProof = fullProofs[0]; - const migratedTokenId = noteProof.note_proof_data.data.token_id; + const migratedTokenId = fullProof.note_proof_data.data.token_id; console.log(` Migrating token_id: ${migratedTokenId}`); await newAppUser.methods .migrate_nft_mode_b( migratedTokenId, signature, - noteProof, + fullProof, blockHeader, oldUserManager.address, publicKeys, @@ -295,7 +294,7 @@ async function main() { .migrate_nft_mode_b( migratedTokenId, signature, - noteProof, + fullProof, blockHeader, oldUserManager.address, publicKeys, @@ -318,9 +317,9 @@ async function main() { const nullifiedNote = nftNotesNullified[0]; - const [nullifiedNoteProof] = await oldUserWallet.buildFullNoteProofs( + const nullifiedNoteProof = await oldUserWallet.buildFullNoteProof( provenBlockNumber, - [nullifiedNote], + nullifiedNote, (note) => NFTNote.fromNote(note), ); diff --git a/e2e-tests/package.json b/e2e-tests/package.json new file mode 100644 index 0000000..55f4a5f --- /dev/null +++ b/e2e-tests/package.json @@ -0,0 +1,25 @@ +{ + "name": "@aztec-state-migration/e2e-tests", + "private": true, + "version": "0.0.0", + "type": "module", + "devDependencies": { + "aztec-state-migration": "*", + "@aztec/accounts": "v4.0.0-devnet.2-patch.0", + "@aztec/wallets": "v4.0.0-devnet.2-patch.0", + "@aztec/aztec.js": "v4.0.0-devnet.2-patch.0", + "@aztec/constants": "v4.0.0-devnet.2-patch.0", + "@aztec/foundation": "v4.0.0-devnet.2-patch.0", + "@aztec/pxe": "v4.0.0-devnet.2-patch.0", + "@aztec/stdlib": "v4.0.0-devnet.2-patch.0", + "@aztec/world-state": "v4.0.0-devnet.2-patch.0", + "@aztec/aztec-node": "v4.0.0-devnet.2-patch.0", + "@aztec/cli": "v4.0.0-devnet.2-patch.0", + "@aztec/ethereum": "v4.0.0-devnet.2-patch.0", + "@aztec/l1-artifacts": "v4.0.0-devnet.2-patch.0", + "@aztec/noir-protocol-circuits-types": "v4.0.0-devnet.2-patch.0", + "@aztec/protocol-contracts": "v4.0.0-devnet.2-patch.0", + "@aztec/telemetry-client": "v4.0.0-devnet.2-patch.0", + "viem": "^2.21.0" + } +} diff --git a/e2e-tests/test-utils.ts b/e2e-tests/test-utils.ts index 93ff5a2..98c929b 100644 --- a/e2e-tests/test-utils.ts +++ b/e2e-tests/test-utils.ts @@ -4,26 +4,19 @@ import { TokenMigrationAppV1Contract } from "./artifacts/TokenMigrationAppV1.js" import { TokenMigrationAppV2Contract } from "./artifacts/TokenMigrationAppV2.js"; import { NftMigrationAppV1Contract } from "./artifacts/NftMigrationAppV1.js"; import { NftMigrationAppV2Contract } from "./artifacts/NftMigrationAppV2.js"; -import { MigrationArchiveRegistryContract } from "../ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.js"; -import { MigrationKeyRegistryContract } from "../ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.js"; +import { + MigrationArchiveRegistryContract, + MigrationKeyRegistryContract, +} from "aztec-state-migration/noir-contracts"; import { Fq, Fr } from "@aztec/foundation/curves/bn254"; import { BlockNumber } from "@aztec/foundation/branded-types"; import { EthAddress } from "@aztec/foundation/eth-address"; import { AztecAddress } from "@aztec/stdlib/aztec-address"; -import { - waitForBlockProof, - migrateArchiveRootOnL1, - waitForL1ToL2Message, - buildArchiveProof, -} from "../ts/aztec-state-migration/index.js"; -import type { - ArchiveProofData, - L1MigrationResult, -} from "../ts/aztec-state-migration/index.js"; -import type { blockHeaderToNoir } from "../ts/aztec-state-migration/noir-helpers/block-header.js"; +import { buildArchiveProof } from "aztec-state-migration"; +import type { ArchiveProofData } from "aztec-state-migration"; +import { blockHeaderToNoir } from "aztec-state-migration"; import type { DeploymentResult } from "./deploy-types.js"; import { EmbeddedWallet } from "@aztec/wallets/embedded"; -import { WaitOpts } from "@aztec/aztec.js/contracts"; import { AccountManager } from "@aztec/aztec.js/wallet"; import { AztecNode } from "@aztec/aztec.js/node"; import { FeeJuicePaymentMethodWithClaim } from "@aztec/aztec.js/fee"; @@ -34,6 +27,17 @@ import { } from "@aztec/aztec.js/ethereum"; import { GrumpkinScalar } from "@aztec/aztec.js/fields"; import { getInitialTestAccountsData } from "@aztec/accounts/testing"; +import { + Account, + Chain, + Hex, + parseAbi, + PublicClient, + Transport, + WalletClient, + toHex, + decodeEventLog, +} from "viem"; export async function assertPrivateNftOwnership( contract: { methods: { get_private_nfts: (...args: any[]) => any } }, @@ -99,41 +103,6 @@ export async function expectRevert( throw new Error("Expected transaction to fail, but it succeeded"); } -export function assertEq(actual: any, expected: any, msg: string) { - const fmt = (v: any) => - typeof v === "object" - ? JSON.stringify( - v, - (_k, val) => (typeof val === "bigint" ? val.toString() : val), - 2, - ) - : String(v); - if (actual !== expected && fmt(actual) !== fmt(expected)) { - throw new Error( - `Mismatch: ${msg}\n Expected: ${fmt(expected)}\n Actual: ${fmt(actual)}`, - ); - } -} - -// ============================================================ -// Types and interfaces -// ============================================================ - -export const NoirOption = { - Some: (value: T) => ({ - _is_some: true, - _value: value, - }), - None: (zeroedValue: T) => ({ - _is_some: false, - _value: zeroedValue, - }), - NoneAddress: { - _is_some: false, - _value: AztecAddress.ZERO, - }, -}; - // ============================================================ // Contract deployment helpers // ============================================================ @@ -334,21 +303,81 @@ export async function deployKeyRegistry(env: DeploymentResult) { return registry; } +/** + * Deploy account with fee juice claim. The L1→L2 message may not be available + * immediately — the sandbox only includes L1→L2 messages when L2 blocks are + * produced. Uses Deployer wallet and address to deploy this account. + */ +export async function deployAndFundAccount( + env: DeploymentResult, + aztecNode: AztecNode, + accountData?: { secret?: Fr; salt?: Fr; signingKey?: Fq }, +): Promise { + const rollup = env[await aztecNode.getVersion()]; + + const { + secret = Fr.random(), + salt = Fr.random(), + signingKey = Fq.random(), + } = accountData ?? {}; + + const accountManager = await rollup.migrationWallet.createSchnorrAccount( + secret, + salt, + signingKey, + ); + + const claim = await fundAccount(env, aztecNode, accountManager.address); + await waitForL1ToL2Message(aztecNode, Fr.fromHexString(claim.messageHash), { + onPoll: async () => { + await produceBlock(env, aztecNode); + }, + intervalMs: 10, + }); + const deployMethod = await accountManager.getDeployMethod(); + await deployMethod.send({ + from: AztecAddress.ZERO, + fee: { + paymentMethod: new FeeJuicePaymentMethodWithClaim( + accountManager.address, + claim, + ), + }, + }); + return accountManager; +} + // ============================================================ -// Bridge helper +// Bridge helpers // ============================================================ export interface BridgeResult { - l1Result: L1MigrationResult; provenBlockNumber: BlockNumber; + provenArchiveRoot: Fr; archiveProof: ArchiveProofData; /** Noir-encoded block header for migration calls (no sibling path). */ blockHeader: ReturnType; } +// ============================================================ +// L1 ABIs (exported for event parsing in tests) +// ============================================================ +export const L1MigratorAbi = parseAbi([ + "constructor(address _registry, address _poseidon2)", + "function migrateArchiveRoot(uint256 oldVersion, (bytes32 actor, uint256 version) l2Migrator) external returns (bytes32 leaf, uint256 leafIndex)", + "function getArchiveInfo(uint256 version) external view returns (bytes32 archiveRoot, uint256 provenBlockNumber)", + "function REGISTRY() external view returns (address)", + "function POSEIDON2() external view returns (address)", + "function SECRET_HASH_FOR_ZERO() external view returns (bytes32)", + "event ArchiveRootMigrated(uint256 indexed oldVersion, uint256 indexed newVersion, bytes32 indexed l2Migrator, bytes32 archiveRoot, uint256 provenBlockNumber, bytes32 messageLeaf, uint256 messageLeafIndex)", +]); + +export const InboxAbi = parseAbi([ + "event MessageSent(uint256 indexed checkpointNumber, uint256 index, bytes32 indexed hash, bytes16 rollingHash)", +]); + /** * Full bridge sequence: wait for proof → L1 migrate → wait for L1→L2 message → register block on new rollup. - * Returns the L1 result, proven block number, archive proof (for registration), and block header (for migration). */ export async function bridgeBlock( env: DeploymentResult, @@ -357,17 +386,19 @@ export async function bridgeBlock( const old_r = env[env.oldRollupVersion]; const new_r = env[env.newRollupVersion]; const blockNumber = await old_r.aztecNode.getBlockNumber(); + const onPoll = () => produceBlock(env, new_r.aztecNode); // Step 1: Wait for block proof - await waitForBlockProof(old_r.aztecNode, blockNumber, { - onPoll: async () => { - await produceBlock(env, new_r.aztecNode); + await waitUntil( + async () => { + const proven = await old_r.aztecNode.getProvenBlockNumber(); + return proven >= blockNumber ? proven : undefined; }, - intervalMs: 100, - }); + { intervalMs: 100, onPoll }, + ); // Step 2: L1 migrateArchiveRoot - const l1Result = await migrateArchiveRootOnL1( + const l1 = await migrateArchiveRootOnL1( env.l1WalletClient, env.publicClient, { @@ -380,14 +411,12 @@ export async function bridgeBlock( ); // Step 3: Wait for L1→L2 message - await waitForL1ToL2Message(new_r.aztecNode, l1Result.l1ToL2MessageHash, { - onPoll: async () => { - await produceBlock(env, new_r.aztecNode); - }, + await waitForL1ToL2Message(new_r.aztecNode, l1.l1ToL2MessageHash, { intervalMs: 10, + onPoll, }); - const provenBlockNumber = BlockNumber(l1Result.provenBlockNumber); + const provenBlockNumber = BlockNumber(l1.provenBlockNumber); const blockHeader = await old_r.aztecNode.getBlockHeader(provenBlockNumber); if (!blockHeader) { @@ -396,83 +425,106 @@ export async function bridgeBlock( ); } const blockHash = await blockHeader.hash(); - - // Build archive proof (block header + sibling path, needed for register_block) const archiveProof = await buildArchiveProof(old_r.aztecNode, blockHash); // Step 4a: Consume L1-to-L2 message (stores trusted archive root) await archiveRegistry.methods .consume_l1_to_l2_message( - l1Result.provenArchiveRoot, - l1Result.provenBlockNumber, + l1.provenArchiveRoot, + l1.provenBlockNumber, Fr.ZERO, - new Fr(l1Result.l1ToL2LeafIndex), + new Fr(l1.l1ToL2LeafIndex), ) .send({ from: new_r.deployerManager.address }); // Step 4b: Register block (verifies block header against stored archive root) await archiveRegistry.methods .register_block( - l1Result.provenBlockNumber, + l1.provenBlockNumber, archiveProof.archive_block_header, archiveProof.archive_sibling_path, ) .send({ from: new_r.deployerManager.address }); return { - l1Result, provenBlockNumber, + provenArchiveRoot: l1.provenArchiveRoot, archiveProof, blockHeader: archiveProof.archive_block_header, }; } -// ============================================================ -// Other helpers -// ============================================================ +async function migrateArchiveRootOnL1( + l1WalletClient: WalletClient, + l1PublicClient: PublicClient, + params: { + l1MigratorAddress: Hex; + oldRollupVersion: number; + newArchiveRegistryAddress: AztecAddress; + newRollupVersion: number; + newInboxAddress: string; + }, +) { + const txHash = await l1WalletClient.writeContract({ + address: params.l1MigratorAddress, + abi: L1MigratorAbi, + functionName: "migrateArchiveRoot", + args: [ + BigInt(params.oldRollupVersion), + { + actor: toHex(params.newArchiveRegistryAddress.toBigInt(), { size: 32 }), + version: BigInt(params.newRollupVersion), + }, + ], + }); + const receipt = await l1PublicClient.waitForTransactionReceipt({ + hash: txHash, + }); -/** - * Deploy account with fee juice claim. The L1→L2 message may not be available - * immediately — the sandbox only includes L1→L2 messages when L2 blocks are - * produced. Uses Deployer wallet and address to deploy this account. - */ -export async function deployAndFundAccount( - env: DeploymentResult, - aztecNode: AztecNode, - accountData?: { secret?: Fr; salt?: Fr; signingKey?: Fq }, -): Promise { - const rollup = env[await aztecNode.getVersion()]; + const archiveArgs = findEvent( + receipt.logs, + L1MigratorAbi, + "ArchiveRootMigrated", + ) as { + archiveRoot: `0x${string}`; + provenBlockNumber: bigint; + }; + const msgArgs = findEvent( + receipt.logs, + InboxAbi, + "MessageSent", + params.newInboxAddress, + ) as { + index: bigint; + hash: `0x${string}`; + }; - const { - secret = Fr.random(), - salt = Fr.random(), - signingKey = Fq.random(), - } = accountData ?? {}; + return { + provenBlockNumber: BlockNumber.fromBigInt(archiveArgs.provenBlockNumber), + provenArchiveRoot: Fr.fromHexString(archiveArgs.archiveRoot), + l1ToL2LeafIndex: msgArgs.index, + l1ToL2MessageHash: new Fr(BigInt(msgArgs.hash)), + }; +} - const accountManager = await rollup.migrationWallet.createSchnorrAccount( - secret, - salt, - signingKey, - ); +// ============================================================ +// Other helpers +// ============================================================ - const claim = await fundAccount(env, aztecNode, accountManager.address); - await waitForL1ToL2Message(aztecNode, Fr.fromHexString(claim.messageHash), { - onPoll: async () => { - await produceBlock(env, aztecNode); - }, - intervalMs: 10, - }); - const deployMethod = await accountManager.getDeployMethod(); - await deployMethod.send({ - from: AztecAddress.ZERO, - fee: { - paymentMethod: new FeeJuicePaymentMethodWithClaim( - accountManager.address, - claim, - ), - }, - }); - return accountManager; +export function assertEq(actual: any, expected: any, msg: string) { + const fmt = (v: any) => + typeof v === "object" + ? JSON.stringify( + v, + (_k, val) => (typeof val === "bigint" ? val.toString() : val), + 2, + ) + : String(v); + if (actual !== expected && fmt(actual) !== fmt(expected)) { + throw new Error( + `Mismatch: ${msg}\n Expected: ${fmt(expected)}\n Actual: ${fmt(actual)}`, + ); + } } async function fundAccount( @@ -515,3 +567,80 @@ async function produceBlock(env: DeploymentResult, aztecNode: AztecNode) { const deployMethod = await accountManager.getDeployMethod(); await deployMethod.send({ from: rollup.deployerManager.address }); } + +async function waitForL1ToL2Message( + aztecNode: AztecNode, + messageHash: Fr, + opts?: { + maxAttempts?: number; + intervalMs?: number; + onPoll?: () => Promise; + }, +): Promise { + await waitUntil( + async () => { + const messageBlock = await aztecNode.getL1ToL2MessageBlock(messageHash); + if (!messageBlock) return undefined; + const proven = await aztecNode.getProvenBlockNumber(); + return proven >= messageBlock ? messageBlock : undefined; + }, + { + maxAttempts: opts?.maxAttempts ?? 30, + intervalMs: opts?.intervalMs ?? 2000, + onPoll: opts?.onPoll, + }, + ); +} + +async function waitUntil( + check: () => Promise, + { + maxAttempts = 60, + intervalMs = 2000, + onPoll, + }: { + maxAttempts?: number; + intervalMs?: number; + onPoll?: () => Promise; + } = {}, +): Promise { + for (let i = 1; i <= maxAttempts; i++) { + const result = await check(); + if (result !== undefined) return result; + if (onPoll) await onPoll(); + if (i < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + } + throw new Error(`waitUntil timed out after ${maxAttempts} attempts`); +} + +function findEvent( + logs: { + data: `0x${string}`; + topics: [`0x${string}`, ...`0x${string}`[]] | []; + address: string; + }[], + abi: TAbi, + eventName: string, + filterAddress?: string, +) { + const filtered = filterAddress + ? logs.filter( + (l) => l.address.toLowerCase() === filterAddress.toLowerCase(), + ) + : logs; + for (const log of filtered) { + try { + const decoded = decodeEventLog({ + abi, + data: log.data, + topics: log.topics, + }); + if (decoded.eventName === eventName) return decoded.args; + } catch { + /* not this event */ + } + } + throw new Error(`Event "${eventName}" not found in logs`); +} diff --git a/package.json b/package.json index 379ac75..0d10ced 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { - "name": "aztec-state-migration", - "author": "Cardinal Cryptography", + "name": "aztec-state-migration-root", "private": true, "version": "0.1.0", "type": "module", @@ -8,8 +7,9 @@ "node": ">=24.12.0" }, "packageManager": "yarn@1.22.22", + "workspaces": ["ts/aztec-state-migration", "e2e-tests"], "scripts": { - "clean": "rm -rf noir/target noir/codegenCache.json solidity/target solidity/cache", + "clean": "rm -rf noir/target e2e-tests/codegenCache.json e2e-tests/artifacts solidity/target solidity/cache && yarn workspace aztec-state-migration run clean", "sol": "yarn sol:deps && yarn sol:compile", "sol:deps": "cd solidity && forge soldeer install", "sol:compile": "cd solidity && forge build", @@ -22,9 +22,11 @@ "noir:codegen:test-contracts": "cd e2e-tests && mkdir -p artifacts && find ../noir/target -maxdepth 1 -name '*.json' ! -name '*MigrationArchiveRegistry*' ! -name '*MigrationKeyRegistry*' -exec cp {} artifacts \\; && aztec codegen ./artifacts -o ./artifacts", "noir:fmt": "cd noir && nargo fmt", "noir:fmt:check": "cd noir && nargo fmt --check", - "ts:build": "tsc", + "ts:build": "yarn workspace aztec-state-migration run build", + "ts:check": "tsc --noEmit", "ts:fmt": "prettier --write 'ts/**/*.ts' 'e2e-tests/**/*.ts'", "ts:fmt:check": "prettier --check 'ts/**/*.ts' 'e2e-tests/**/*.ts'", + "build": "yarn noir && yarn ts:build", "fmt": "yarn sol:fmt && yarn noir:fmt && yarn ts:fmt", "fmt:check": "yarn sol:fmt:check && yarn noir:fmt:check && yarn ts:fmt:check", "test:setup": "./e2e-tests/utils/dual-rollup-setup.sh", @@ -42,30 +44,7 @@ "test:nft:mode-b:public": "npx tsx e2e-tests/nft-migration-public-mode-b.test.ts", "test:hash": "./e2e-tests/hash-compatibility.sh" }, - "exports": { - ".": "./src/index.ts" - }, - "dependencies": { - "@aztec/accounts": "v4.0.0-devnet.2-patch.0", - "@aztec/wallets": "v4.0.0-devnet.2-patch.0", - "@aztec/aztec.js": "v4.0.0-devnet.2-patch.0", - "@aztec/constants": "v4.0.0-devnet.2-patch.0", - "@aztec/entrypoints": "v4.0.0-devnet.2-patch.0", - "@aztec/foundation": "v4.0.0-devnet.2-patch.0", - "@aztec/noir-contracts.js": "v4.0.0-devnet.2-patch.0", - "@aztec/pxe": "v4.0.0-devnet.2-patch.0", - "@aztec/stdlib": "v4.0.0-devnet.2-patch.0", - "viem": "^2.21.0" - }, "devDependencies": { - "@aztec/world-state": "v4.0.0-devnet.2-patch.0", - "@aztec/aztec-node": "v4.0.0-devnet.2-patch.0", - "@aztec/cli": "v4.0.0-devnet.2-patch.0", - "@aztec/ethereum": "v4.0.0-devnet.2-patch.0", - "@aztec/l1-artifacts": "v4.0.0-devnet.2-patch.0", - "@aztec/noir-protocol-circuits-types": "v4.0.0-devnet.2-patch.0", - "@aztec/protocol-contracts": "v4.0.0-devnet.2-patch.0", - "@aztec/telemetry-client": "v4.0.0-devnet.2-patch.0", "prettier": "^3.8.1", "tsx": "^4.19.0", "typescript": "^5.6.0" diff --git a/ts/aztec-state-migration/bridge.ts b/ts/aztec-state-migration/bridge.ts deleted file mode 100644 index f11d8d8..0000000 --- a/ts/aztec-state-migration/bridge.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Fr } from "@aztec/foundation/curves/bn254"; -import { toHex, decodeEventLog, parseAbi } from "viem"; -import { poll } from "./polling.js"; -import type { AztecNode } from "@aztec/aztec.js/node"; -import type { AztecAddress } from "@aztec/stdlib/aztec-address"; -import type { - PublicClient, - WalletClient, - Hex, - Chain, - Transport, - Account, -} from "viem"; -import type { L1MigrationResult } from "./types.js"; -import { BlockNumber } from "@aztec/foundation/branded-types"; - -const L1MigratorAbi = parseAbi([ - "function migrateArchiveRoot(uint256 oldVersion, (bytes32 actor, uint256 version) l2Migrator) external returns (bytes32 leaf, uint256 leafIndex)", - "event ArchiveRootMigrated(uint256 indexed oldVersion, uint256 indexed newVersion, bytes32 indexed l2Migrator, bytes32 archiveRoot, uint256 provenBlockNumber, bytes32 messageLeaf, uint256 messageLeafIndex)", -]); - -const InboxAbi = parseAbi([ - "event MessageSent(uint256 indexed checkpointNumber, uint256 index, bytes32 indexed hash, bytes16 rollingHash)", -]); - -/** - * Wait for a block to be proven on the old rollup. - * Returns the current proven block number once it reaches `blockNumber`. - */ -export async function waitForBlockProof( - aztecNode: AztecNode, - blockNumber: number, - opts?: { - maxAttempts?: number; - intervalMs?: number; - onPoll?: (currentProven: number) => Promise; - }, -): Promise { - const { maxAttempts = 60, intervalMs = 2000, onPoll } = opts ?? {}; - - return poll({ - check: async () => { - const proven = await aztecNode.getProvenBlockNumber(); - return proven >= blockNumber ? proven : undefined; - }, - maxAttempts, - intervalMs, - onPoll, - timeoutMessage: `Block ${blockNumber} not proven after ${maxAttempts} attempts`, - }); -} - -/** - * Call L1 migrateArchiveRoot and parse ArchiveRootMigrated + MessageSent events. - * Does NOT wait for the L1→L2 message to sync or register on the new rollup. - */ -export async function migrateArchiveRootOnL1( - l1WalletClient: WalletClient, - l1PublicClient: PublicClient, - params: { - l1MigratorAddress: Hex; - oldRollupVersion: number; - newArchiveRegistryAddress: AztecAddress; - newRollupVersion: number; - newInboxAddress: string; - }, -): Promise { - // Call L1 migrateArchiveRoot - const txHash = await l1WalletClient.writeContract({ - address: params.l1MigratorAddress, - abi: L1MigratorAbi, - functionName: "migrateArchiveRoot", - args: [ - BigInt(params.oldRollupVersion), - { - actor: toHex(params.newArchiveRegistryAddress.toBigInt(), { size: 32 }), - version: BigInt(params.newRollupVersion), - }, - ], - }); - const receipt = await l1PublicClient.waitForTransactionReceipt({ - hash: txHash, - }); - - // Parse ArchiveRootMigrated event - const archiveRootLog = receipt.logs.find((log) => { - try { - const decoded = decodeEventLog({ - abi: L1MigratorAbi, - data: log.data, - topics: log.topics, - }); - return decoded.eventName === "ArchiveRootMigrated"; - } catch { - return false; - } - }); - if (!archiveRootLog) { - throw new Error( - "ArchiveRootMigrated event not found in L1 transaction receipt", - ); - } - - const archiveEvent = decodeEventLog({ - abi: L1MigratorAbi, - data: archiveRootLog.data, - topics: archiveRootLog.topics, - }); - const eventArgs = archiveEvent.args as { - archiveRoot: `0x${string}`; - provenBlockNumber: bigint; - }; - - const provenArchiveRoot = Fr.fromHexString(eventArgs.archiveRoot); - const provenBlockNumber = BlockNumber.fromBigInt(eventArgs.provenBlockNumber); - - // Parse MessageSent event from Inbox - const inboxLogs = receipt.logs.filter( - (log) => log.address.toLowerCase() === params.newInboxAddress.toLowerCase(), - ); - if (inboxLogs.length === 0) { - throw new Error("No MessageSent event found from Inbox contract"); - } - const messageSentEvent = decodeEventLog({ - abi: InboxAbi, - data: inboxLogs[0].data, - topics: inboxLogs[0].topics, - }); - const l1ToL2LeafIndex = (messageSentEvent.args as { index: bigint }).index; - const l1ToL2MessageHash = new Fr( - BigInt((messageSentEvent.args as { hash: `0x${string}` }).hash), - ); - - return { - provenBlockNumber, - provenArchiveRoot, - l1ToL2LeafIndex, - l1ToL2MessageHash, - }; -} - -/** - * Wait for an L1→L2 message to be synced on the new rollup. - */ -export async function waitForL1ToL2Message( - aztecNode: AztecNode, - messageHash: Fr, - opts?: { - maxAttempts?: number; - intervalMs?: number; - onPoll?: (attempt: number) => Promise; - }, -): Promise { - const { maxAttempts = 30, intervalMs = 2000, onPoll } = opts ?? {}; - - await poll({ - check: async () => { - const messageBlock = await aztecNode.getL1ToL2MessageBlock(messageHash); - if (!messageBlock) { - return undefined; - } - const provenBlockNumber = await aztecNode.getProvenBlockNumber(); - return provenBlockNumber >= messageBlock ? messageBlock : undefined; - }, - maxAttempts, - intervalMs, - onPoll, - timeoutMessage: `L1->L2 message not synced after ${maxAttempts} attempts`, - }); -} diff --git a/ts/aztec-state-migration/index.ts b/ts/aztec-state-migration/index.ts index 79a6a94..81327c1 100644 --- a/ts/aztec-state-migration/index.ts +++ b/ts/aztec-state-migration/index.ts @@ -1,10 +1,5 @@ // Keys -export { - deriveMasterMigrationSecretKey, - signMigrationModeA, - signMigrationModeB, - signPublicStateMigrationModeB, -} from "./keys.js"; +export { deriveMasterMigrationSecretKey } from "./key.js"; // Proofs export { @@ -13,23 +8,8 @@ export { buildBlockHeader, } from "./proofs.js"; -// Bridge -export { - waitForBlockProof, - migrateArchiveRootOnL1, - waitForL1ToL2Message, -} from "./bridge.js"; - // Noir helpers export * from "./noir-helpers/index.js"; -// Polling -export { poll } from "./polling.js"; -export type { PollOptions } from "./polling.js"; - // Types -export type { - NoteProofData, - ArchiveProofData, - L1MigrationResult, -} from "./types.js"; +export type { NoteProofData, ArchiveProofData } from "./types.js"; diff --git a/ts/aztec-state-migration/key.ts b/ts/aztec-state-migration/key.ts new file mode 100644 index 0000000..643044b --- /dev/null +++ b/ts/aztec-state-migration/key.ts @@ -0,0 +1,13 @@ +import { Fr, GrumpkinScalar } from "@aztec/aztec.js/fields"; +import { sha512ToGrumpkinScalar } from "@aztec/foundation/crypto/sha512"; +import { DOM_SEP__MSK_M_GEN } from "./constants.js"; +/** + * Derive the master migration secret key from an account's secret key. + * Uses `sha512ToGrumpkinScalar` with {@link DOM_SEP__MSK_M_GEN} as a domain separator. + * + * @param secretKey - The account's master secret key. + * @returns A Grumpkin scalar used as the migration signing / encryption key. + */ +export function deriveMasterMigrationSecretKey(secretKey: Fr): GrumpkinScalar { + return sha512ToGrumpkinScalar([secretKey, DOM_SEP__MSK_M_GEN]); +} diff --git a/ts/aztec-state-migration/mode-a/index.ts b/ts/aztec-state-migration/mode-a/index.ts index 819f074..1fa0899 100644 --- a/ts/aztec-state-migration/mode-a/index.ts +++ b/ts/aztec-state-migration/mode-a/index.ts @@ -1,3 +1,4 @@ export type { MigrationNoteProofData, MigrationNoteAndData } from "./types.js"; export { MigrationNote } from "./types.js"; export { buildMigrationNoteProof } from "./proofs.js"; +export { signMigrationModeA } from "./signature.js"; diff --git a/ts/aztec-state-migration/mode-a/proofs.ts b/ts/aztec-state-migration/mode-a/proofs.ts index fbaf33e..96175fd 100644 --- a/ts/aztec-state-migration/mode-a/proofs.ts +++ b/ts/aztec-state-migration/mode-a/proofs.ts @@ -4,6 +4,7 @@ import type { NoteDao } from "@aztec/stdlib/note"; import { PrivateEvent } from "@aztec/aztec.js/wallet"; import { MigrationNote, MigrationNoteProofData } from "./types.js"; import { buildNoteProof } from "../index.js"; +import { BlockHash } from "@aztec/stdlib/block"; /** * Build a {@link MigrationNoteProofData} for a Mode A migration note. @@ -13,20 +14,20 @@ import { buildNoteProof } from "../index.js"; * corresponding {@link PrivateEvent} (since the note itself only stores a hash). * * @param node - Aztec node client to query the note hash tree. - * @param blockNumber - Block number at which to prove inclusion. + * @param blockReference - Block number or hash at which to prove inclusion. * @param noteDao - The migration note DAO to prove. * @param migrationData - The decoded migration data for this note. * @typeParam T - The shape of the migration data (e.g. `bigint` for token amounts). */ export async function buildMigrationNoteProof( node: AztecNode, - blockNumber: BlockNumber, + blockReference: BlockNumber | BlockHash, noteDao: NoteDao, migrationData: T, ): Promise> { const noteProof = await buildNoteProof( node, - blockNumber, + blockReference, noteDao, MigrationNote.fromNote, ); diff --git a/ts/aztec-state-migration/mode-a/signature.ts b/ts/aztec-state-migration/mode-a/signature.ts new file mode 100644 index 0000000..2549936 --- /dev/null +++ b/ts/aztec-state-migration/mode-a/signature.ts @@ -0,0 +1,39 @@ +import { Fr } from "@aztec/aztec.js/fields"; +import { DOM_SEP__CLAIM_A } from "../constants.js"; +import { AztecAddress } from "@aztec/stdlib/aztec-address"; +import { NoteDao } from "@aztec/stdlib/note"; +import { poseidon2Hash } from "@aztec/foundation/crypto/poseidon"; +import { MigrationSignature } from "../types.js"; + +/** + * Produce a Schnorr signature over a Mode A (cooperative lock-and-migrate) claim message. + * + * The signed payload is `poseidon2_hash([DOM_SEP__CLAIM_A, oldVersion, newVersion, notesHash, recipient, newApp])`. + * + * @param signer - Signing callback (typically {@link MigrationAccount.migrationKeySigner}). + * @param oldRollupVersion - Version field from the old rollup's block header. + * @param newRollupVersion - Target rollup version the tokens are migrating to. + * @param migrationNotes - The locked migration notes being claimed. + * @param recipient - Address that will call the migration tx on the new rollup (`msg_sender()`). + * @param newAppAddress - Address of the app contract on the new rollup. + * @returns The Schnorr signature as a {@link MigrationSignature}. + */ +export async function signMigrationModeA( + signer: (msg: Buffer) => Promise, + oldRollupVersion: Fr, + newRollupVersion: Fr, + migrationNotes: NoteDao[], + recipient: AztecAddress, + newAppAddress: AztecAddress, +): Promise { + const notesHash = await poseidon2Hash(migrationNotes.map((n) => n.noteHash)); + const msg = await poseidon2Hash([ + DOM_SEP__CLAIM_A, + oldRollupVersion, + newRollupVersion, + notesHash, + recipient, + newAppAddress, + ]); + return signer(msg.toBuffer()); +} diff --git a/ts/aztec-state-migration/mode-b/index.ts b/ts/aztec-state-migration/mode-b/index.ts index ca1ee2d..1742c6d 100644 --- a/ts/aztec-state-migration/mode-b/index.ts +++ b/ts/aztec-state-migration/mode-b/index.ts @@ -10,3 +10,8 @@ export { buildPublicDataProof, buildPublicMapDataProof, } from "./proofs.js"; + +export { + signMigrationModeB, + signPublicStateMigrationModeB, +} from "./signature.js"; diff --git a/ts/aztec-state-migration/mode-b/proofs.ts b/ts/aztec-state-migration/mode-b/proofs.ts index ed58271..b79744c 100644 --- a/ts/aztec-state-migration/mode-b/proofs.ts +++ b/ts/aztec-state-migration/mode-b/proofs.ts @@ -13,23 +13,24 @@ import { } from "./types.js"; import type { NoteDao } from "@aztec/stdlib/note"; import type { AbiType } from "@aztec/stdlib/abi"; +import { BlockHash } from "@aztec/stdlib/block"; /** * Build a {@link NonNullificationProofData} proving that a note has **not** been nullified. * Queries the low-nullifier membership witness from the nullifier tree. * * @param node - Aztec node client to query the nullifier tree. - * @param blockNumber - Block number at which to prove non-inclusion. + * @param blockReference - Block number or hash at which to prove non-inclusion. * @param noteDao - The note DAO whose siloed nullifier is checked. * @returns Low-nullifier witness data for the Noir non-inclusion check. */ export async function buildNullifierProof( node: AztecNode, - blockNumber: BlockNumber, + blockReference: BlockNumber | BlockHash, noteDao: NoteDao, ): Promise { const lowNullifierWitness = await node.getLowNullifierMembershipWitness( - blockNumber, + blockReference, noteDao.siloedNullifier, ); if (!lowNullifierWitness) { @@ -59,13 +60,13 @@ export async function buildNullifierProof( * then queries the public data tree witness from the Aztec node. * * @param aztecNode - Aztec node client to query the public data tree. - * @param blockNumber - Block number at which to prove inclusion. + * @param blockReference - Block number or hash at which to prove inclusion. * @param contractAddress - Address of the contract whose storage is being proven. * @param storageSlot - The un-siloed storage slot to prove. */ export async function buildPublicDataSlotProof( aztecNode: AztecNode, - blockNumber: BlockNumber, + blockReference: BlockNumber | BlockHash, contractAddress: AztecAddress, storageSlot: Fr, ): Promise { @@ -74,7 +75,10 @@ export async function buildPublicDataSlotProof( contractAddress, storageSlot, ); - const witness = await aztecNode.getPublicDataWitness(blockNumber, siloedSlot); + const witness = await aztecNode.getPublicDataWitness( + blockReference, + siloedSlot, + ); if (!witness) { throw new Error( `No public data witness for slot ${storageSlot} (siloed: ${siloedSlot})`, @@ -99,7 +103,7 @@ export async function buildPublicDataSlotProof( * then builds a slot proof for each. * * @param aztecNode - Aztec node client to query the public data tree. - * @param blockNumber - Block number at which to prove inclusion. + * @param blockReference - Block number or hash at which to prove inclusion. * @param data - The data value (passed through to the returned proof, not encoded here). * @param contractAddress - Address of the contract whose storage is being proven. * @param baseSlot - The storage slot of the variable (from the contract's `storageLayout`). @@ -107,7 +111,7 @@ export async function buildPublicDataSlotProof( */ export async function buildPublicDataProof( aztecNode: AztecNode, - blockNumber: BlockNumber, + blockReference: BlockNumber | BlockHash, data: T, contractAddress: AztecAddress, baseSlot: Fr, @@ -119,7 +123,7 @@ export async function buildPublicDataProof( const slot = baseSlot.add(new Fr(i)); const proof = await buildPublicDataSlotProof( aztecNode, - blockNumber, + blockReference, contractAddress, slot, ); @@ -139,7 +143,7 @@ export async function buildPublicDataProof( * a slot proof for each of the data's packed fields. * * @param aztecNode - Aztec node client to query the public data tree. - * @param blockNumber - Block number at which to prove inclusion. + * @param blockReference - Block number or hash at which to prove inclusion. * @param data - The data value (passed through to the returned proof, not encoded here). * @param contractAddress - Address of the contract whose storage is being proven. * @param baseSlot - The base storage slot of the map (from the contract's `storageLayout`). @@ -148,7 +152,7 @@ export async function buildPublicDataProof( */ export async function buildPublicMapDataProof( aztecNode: AztecNode, - blockNumber: BlockNumber, + blockReference: BlockNumber | BlockHash, data: T, contractAddress: AztecAddress, baseSlot: Fr, @@ -164,7 +168,7 @@ export async function buildPublicMapDataProof( const slot = slot_in_map.add(new Fr(i)); const proof = await buildPublicDataSlotProof( aztecNode, - blockNumber, + blockReference, contractAddress, slot, ); diff --git a/ts/aztec-state-migration/keys.ts b/ts/aztec-state-migration/mode-b/signature.ts similarity index 64% rename from ts/aztec-state-migration/keys.ts rename to ts/aztec-state-migration/mode-b/signature.ts index 8f00dc6..d728e4b 100644 --- a/ts/aztec-state-migration/keys.ts +++ b/ts/aztec-state-migration/mode-b/signature.ts @@ -4,67 +4,23 @@ import { DOM_SEP__CLAIM_A, DOM_SEP__CLAIM_B, DOM_SEP__MSK_M_GEN, -} from "./constants.js"; +} from "../constants.js"; import { AztecAddress } from "@aztec/stdlib/aztec-address"; import { NoteDao } from "@aztec/stdlib/note"; import { poseidon2Hash } from "@aztec/foundation/crypto/poseidon"; -import { MigrationSignature } from "./types.js"; +import { MigrationSignature } from "../types.js"; import { type AbiType, encodeArguments, type FunctionAbi, } from "@aztec/stdlib/abi"; -/** - * Derive the master migration secret key from an account's secret key. - * Uses `sha512ToGrumpkinScalar` with {@link DOM_SEP__MSK_M_GEN} as a domain separator. - * - * @param secretKey - The account's master secret key. - * @returns A Grumpkin scalar used as the migration signing / encryption key. - */ -export function deriveMasterMigrationSecretKey(secretKey: Fr): GrumpkinScalar { - return sha512ToGrumpkinScalar([secretKey, DOM_SEP__MSK_M_GEN]); -} - -/** - * Produce a Schnorr signature over a Mode A (cooperative lock-and-migrate) claim message. - * - * The signed payload is `poseidon2_hash([DOM_SEP__CLAIM_A, oldVersion, newVersion, notesHash, recipient, newApp])`. - * - * @param signer - Signing callback (typically {@link BaseMigrationAccount.migrationKeySigner}). - * @param oldRollupVersion - Version field from the old rollup's block header. - * @param newRollupVersion - Target rollup version the tokens are migrating to. - * @param migrationNotes - The locked migration notes being claimed. - * @param recipient - Address that will call the migration tx on the new rollup (`msg_sender()`). - * @param newAppAddress - Address of the app contract on the new rollup. - * @returns The Schnorr signature as a {@link MigrationSignature}. - */ -export async function signMigrationModeA( - signer: (msg: Buffer) => Promise, - oldRollupVersion: Fr, - newRollupVersion: Fr, - migrationNotes: NoteDao[], - recipient: AztecAddress, - newAppAddress: AztecAddress, -): Promise { - const notesHash = await poseidon2Hash(migrationNotes.map((n) => n.noteHash)); - const msg = await poseidon2Hash([ - DOM_SEP__CLAIM_A, - oldRollupVersion, - newRollupVersion, - notesHash, - recipient, - newAppAddress, - ]); - return signer(msg.toBuffer()); -} - /** * Produce a Schnorr signature over a Mode B (emergency snapshot) private note claim message. * * The signed payload is `poseidon2_hash([DOM_SEP__CLAIM_B, oldVersion, newVersion, notesHash, recipient, newApp])`. * - * @param signer - Signing callback (typically {@link BaseMigrationAccount.migrationKeySigner}). + * @param signer - Signing callback (typically {@link MigrationAccount.migrationKeySigner}). * @param oldRollupVersion - Version field from the old rollup's block header. * @param newRollupVersion - Target rollup version the tokens are migrating to. * @param notes - The private notes on the old rollup whose existence is being proven. @@ -100,7 +56,7 @@ export async function signMigrationModeB( * * The signed payload is `poseidon2_hash([DOM_SEP__CLAIM_B, oldVersion, newVersion, dataHash, recipient, newApp])`. * - * @param signer - Signing callback (typically {@link BaseMigrationAccount.migrationKeySigner}). + * @param signer - Signing callback (typically {@link MigrationAccount.migrationKeySigner}). * @param oldRollupVersion - Version field from the old rollup's block header. * @param newRollupVersion - Target rollup version the tokens are migrating to. * @param data - The public state data to sign. Must match the struct shape defined by `abiType`. diff --git a/ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.lazy.ts b/ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.lazy.ts new file mode 100644 index 0000000..655c8e7 --- /dev/null +++ b/ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.lazy.ts @@ -0,0 +1,378 @@ +import { AztecAddress } from "@aztec/aztec.js/addresses"; +import { + type AztecAddressLike, + type ContractArtifact, + type EthAddressLike, + type FieldLike, + loadContractArtifact, + type NoirCompiledContract, +} from "@aztec/aztec.js/abi"; +import { + Contract, + ContractBase, + ContractFunctionInteraction, + type ContractMethod, + type ContractStorageLayout, + DeployMethod, +} from "@aztec/aztec.js/contracts"; +import { EthAddress } from "@aztec/aztec.js/addresses"; +import { Fr } from "@aztec/aztec.js/fields"; +import { PublicKeys } from "@aztec/aztec.js/keys"; +import type { Wallet } from "@aztec/aztec.js/wallet"; + +let cachedArtifact: ContractArtifact | undefined; + +/** + * Lazily loads the MigrationArchiveRegistry contract artifact. + * Uses dynamic import to defer JSON loading until first call. + */ +export async function getMigrationArchiveRegistryContractArtifact(): Promise { + if (!cachedArtifact) { + const { default: json } = await import( + "../artifacts/migration_archive_registry-MigrationArchiveRegistry.json", + { with: { type: "json" } } + ); + cachedArtifact = loadContractArtifact(json as NoirCompiledContract); + } + return cachedArtifact; +} + +/** + * Type-safe interface for contract MigrationArchiveRegistry. + * Lazily loads the contract artifact — static factory methods are async. + */ +export class MigrationArchiveRegistryContract extends ContractBase { + private constructor( + address: AztecAddress, + artifact: ContractArtifact, + wallet: Wallet, + ) { + super(address, artifact, wallet); + } + + /** + * Creates a contract instance. + * @param address - The deployed contract's address. + * @param wallet - The wallet to use when interacting with the contract. + * @returns A new Contract instance. + */ + public static async at( + address: AztecAddress, + wallet: Wallet, + ): Promise { + const artifact = await getMigrationArchiveRegistryContractArtifact(); + return Contract.at( + address, + artifact, + wallet, + ) as MigrationArchiveRegistryContract; + } + + /** + * Creates a tx to deploy a new instance of this contract. + */ + public static async deploy( + wallet: Wallet, + l1_migrator: EthAddressLike, + old_rollup_version: FieldLike, + old_key_registry: AztecAddressLike, + ) { + const artifact = await getMigrationArchiveRegistryContractArtifact(); + return new DeployMethod( + PublicKeys.default(), + wallet, + artifact, + (instance, wallet) => + Contract.at( + instance.address, + artifact, + wallet, + ) as MigrationArchiveRegistryContract, + Array.from(arguments).slice(1), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified public keys hash to derive the address. + */ + public static async deployWithPublicKeys( + publicKeys: PublicKeys, + wallet: Wallet, + l1_migrator: EthAddressLike, + old_rollup_version: FieldLike, + old_key_registry: AztecAddressLike, + ) { + const artifact = await getMigrationArchiveRegistryContractArtifact(); + return new DeployMethod( + publicKeys, + wallet, + artifact, + (instance, wallet) => + Contract.at( + instance.address, + artifact, + wallet, + ) as MigrationArchiveRegistryContract, + Array.from(arguments).slice(2), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified constructor method. + */ + public static async deployWithOpts< + M extends keyof MigrationArchiveRegistryContract["methods"], + >( + opts: { publicKeys?: PublicKeys; method?: M; wallet: Wallet }, + ...args: Parameters + ) { + const artifact = await getMigrationArchiveRegistryContractArtifact(); + return new DeployMethod( + opts.publicKeys ?? PublicKeys.default(), + opts.wallet, + artifact, + (instance, wallet) => + Contract.at( + instance.address, + artifact, + wallet, + ) as MigrationArchiveRegistryContract, + Array.from(arguments).slice(1), + opts.method ?? "constructor", + ); + } + + public static get storage(): ContractStorageLayout< + | "l1_migrator" + | "old_rollup_version" + | "old_key_registry" + | "snapshot_height" + | "snapshot_block_hash" + | "archive_roots" + | "block_hashes" + | "latest_proven_block" + > { + return { + l1_migrator: { slot: new Fr(1n) }, + old_rollup_version: { slot: new Fr(3n) }, + old_key_registry: { slot: new Fr(5n) }, + snapshot_height: { slot: new Fr(7n) }, + snapshot_block_hash: { slot: new Fr(9n) }, + archive_roots: { slot: new Fr(11n) }, + block_hashes: { slot: new Fr(12n) }, + latest_proven_block: { slot: new Fr(13n) }, + } as ContractStorageLayout< + | "l1_migrator" + | "old_rollup_version" + | "old_key_registry" + | "snapshot_height" + | "snapshot_block_hash" + | "archive_roots" + | "block_hashes" + | "latest_proven_block" + >; + } + + /** Type-safe wrappers for the public methods exposed by the contract. */ + declare public methods: { + /** constructor(l1_migrator: struct, old_rollup_version: field, old_key_registry: struct) */ + constructor: (( + l1_migrator: EthAddressLike, + old_rollup_version: FieldLike, + old_key_registry: AztecAddressLike, + ) => ContractFunctionInteraction) & + Pick; + /** consume_l1_to_l2_message(archive_root: field, proven_block_number: integer, secret: field, leaf_index: field) */ + consume_l1_to_l2_message: (( + archive_root: FieldLike, + proven_block_number: bigint | number, + secret: FieldLike, + leaf_index: FieldLike, + ) => ContractFunctionInteraction) & + Pick; + /** consume_l1_to_l2_message_and_register_block(archive_root: field, proven_block_number: integer, secret: field, leaf_index: field, block_header: struct, archive_sibling_path: array) */ + consume_l1_to_l2_message_and_register_block: (( + archive_root: FieldLike, + proven_block_number: bigint | number, + secret: FieldLike, + leaf_index: FieldLike, + block_header: { + last_archive: { root: FieldLike; next_available_leaf_index: FieldLike }; + state: { + l1_to_l2_message_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + partial: { + note_hash_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + nullifier_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + public_data_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + }; + }; + sponge_blob_hash: FieldLike; + global_variables: { + chain_id: FieldLike; + version: FieldLike; + block_number: bigint | number; + slot_number: FieldLike; + timestamp: bigint | number; + coinbase: EthAddressLike; + fee_recipient: AztecAddressLike; + gas_fees: { + fee_per_da_gas: bigint | number; + fee_per_l2_gas: bigint | number; + }; + }; + total_fees: FieldLike; + total_mana_used: FieldLike; + }, + archive_sibling_path: FieldLike[], + ) => ContractFunctionInteraction) & + Pick; + /** get_block_hash(block_number: integer) */ + get_block_hash: (( + block_number: bigint | number, + ) => ContractFunctionInteraction) & + Pick; + /** get_latest_proven_block() */ + get_latest_proven_block: (() => ContractFunctionInteraction) & + Pick; + /** get_old_key_registry() */ + get_old_key_registry: (() => ContractFunctionInteraction) & + Pick; + /** get_snapshot_block_hash() */ + get_snapshot_block_hash: (() => ContractFunctionInteraction) & + Pick; + /** get_snapshot_height() */ + get_snapshot_height: (() => ContractFunctionInteraction) & + Pick; + /** process_message(message_ciphertext: struct, message_context: struct) */ + process_message: (( + message_ciphertext: FieldLike[], + message_context: { + tx_hash: FieldLike; + unique_note_hashes_in_tx: FieldLike[]; + first_nullifier_in_tx: FieldLike; + recipient: AztecAddressLike; + }, + ) => ContractFunctionInteraction) & + Pick; + /** public_dispatch(selector: field) */ + public_dispatch: ((selector: FieldLike) => ContractFunctionInteraction) & + Pick; + /** register_block(proven_block_number: integer, block_header: struct, archive_sibling_path: array) */ + register_block: (( + proven_block_number: bigint | number, + block_header: { + last_archive: { root: FieldLike; next_available_leaf_index: FieldLike }; + state: { + l1_to_l2_message_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + partial: { + note_hash_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + nullifier_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + public_data_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + }; + }; + sponge_blob_hash: FieldLike; + global_variables: { + chain_id: FieldLike; + version: FieldLike; + block_number: bigint | number; + slot_number: FieldLike; + timestamp: bigint | number; + coinbase: EthAddressLike; + fee_recipient: AztecAddressLike; + gas_fees: { + fee_per_da_gas: bigint | number; + fee_per_l2_gas: bigint | number; + }; + }; + total_fees: FieldLike; + total_mana_used: FieldLike; + }, + archive_sibling_path: FieldLike[], + ) => ContractFunctionInteraction) & + Pick; + /** set_snapshot_height(height: integer, snapshot_block_header: struct, proven_block_number: integer, archive_sibling_path: array) */ + set_snapshot_height: (( + height: bigint | number, + snapshot_block_header: { + last_archive: { root: FieldLike; next_available_leaf_index: FieldLike }; + state: { + l1_to_l2_message_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + partial: { + note_hash_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + nullifier_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + public_data_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + }; + }; + sponge_blob_hash: FieldLike; + global_variables: { + chain_id: FieldLike; + version: FieldLike; + block_number: bigint | number; + slot_number: FieldLike; + timestamp: bigint | number; + coinbase: EthAddressLike; + fee_recipient: AztecAddressLike; + gas_fees: { + fee_per_da_gas: bigint | number; + fee_per_l2_gas: bigint | number; + }; + }; + total_fees: FieldLike; + total_mana_used: FieldLike; + }, + proven_block_number: bigint | number, + archive_sibling_path: FieldLike[], + ) => ContractFunctionInteraction) & + Pick; + /** sync_state() */ + sync_state: (() => ContractFunctionInteraction) & + Pick; + /** verify_migration_mode_a(block_number: integer, block_hash: field) */ + verify_migration_mode_a: (( + block_number: bigint | number, + block_hash: FieldLike, + ) => ContractFunctionInteraction) & + Pick; + /** verify_migration_mode_b(block_hash: field) */ + verify_migration_mode_b: (( + block_hash: FieldLike, + ) => ContractFunctionInteraction) & + Pick; + }; +} diff --git a/ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.ts b/ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.ts new file mode 100644 index 0000000..4df21fe --- /dev/null +++ b/ts/aztec-state-migration/noir-contracts/MigrationArchiveRegistry.ts @@ -0,0 +1,400 @@ +/* Autogenerated file, do not edit! */ + +/* eslint-disable */ +import { AztecAddress, CompleteAddress } from "@aztec/aztec.js/addresses"; +import { + type AbiType, + type AztecAddressLike, + type ContractArtifact, + EventSelector, + decodeFromAbi, + type EthAddressLike, + type FieldLike, + type FunctionSelectorLike, + loadContractArtifact, + loadContractArtifactForPublic, + type NoirCompiledContract, + type U128Like, + type WrappedFieldLike, +} from "@aztec/aztec.js/abi"; +import { + Contract, + ContractBase, + ContractFunctionInteraction, + type ContractMethod, + type ContractStorageLayout, + DeployMethod, +} from "@aztec/aztec.js/contracts"; +import { EthAddress } from "@aztec/aztec.js/addresses"; +import { Fr, Point } from "@aztec/aztec.js/fields"; +import { type PublicKey, PublicKeys } from "@aztec/aztec.js/keys"; +import type { Wallet } from "@aztec/aztec.js/wallet"; +import MigrationArchiveRegistryContractArtifactJson from "../artifacts/migration_archive_registry-MigrationArchiveRegistry.json" with { type: "json" }; +export const MigrationArchiveRegistryContractArtifact = loadContractArtifact( + MigrationArchiveRegistryContractArtifactJson as NoirCompiledContract, +); + +/** + * Type-safe interface for contract MigrationArchiveRegistry; + */ +export class MigrationArchiveRegistryContract extends ContractBase { + private constructor(address: AztecAddress, wallet: Wallet) { + super(address, MigrationArchiveRegistryContractArtifact, wallet); + } + + /** + * Creates a contract instance. + * @param address - The deployed contract's address. + * @param wallet - The wallet to use when interacting with the contract. + * @returns A new Contract instance. + */ + public static at( + address: AztecAddress, + wallet: Wallet, + ): MigrationArchiveRegistryContract { + return Contract.at( + address, + MigrationArchiveRegistryContract.artifact, + wallet, + ) as MigrationArchiveRegistryContract; + } + + /** + * Creates a tx to deploy a new instance of this contract. + */ + public static deploy( + wallet: Wallet, + l1_migrator: EthAddressLike, + old_rollup_version: FieldLike, + old_key_registry: AztecAddressLike, + ) { + return new DeployMethod( + PublicKeys.default(), + wallet, + MigrationArchiveRegistryContractArtifact, + (instance, wallet) => + MigrationArchiveRegistryContract.at(instance.address, wallet), + Array.from(arguments).slice(1), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified public keys hash to derive the address. + */ + public static deployWithPublicKeys( + publicKeys: PublicKeys, + wallet: Wallet, + l1_migrator: EthAddressLike, + old_rollup_version: FieldLike, + old_key_registry: AztecAddressLike, + ) { + return new DeployMethod( + publicKeys, + wallet, + MigrationArchiveRegistryContractArtifact, + (instance, wallet) => + MigrationArchiveRegistryContract.at(instance.address, wallet), + Array.from(arguments).slice(2), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified constructor method. + */ + public static deployWithOpts< + M extends keyof MigrationArchiveRegistryContract["methods"], + >( + opts: { publicKeys?: PublicKeys; method?: M; wallet: Wallet }, + ...args: Parameters + ) { + return new DeployMethod( + opts.publicKeys ?? PublicKeys.default(), + opts.wallet, + MigrationArchiveRegistryContractArtifact, + (instance, wallet) => + MigrationArchiveRegistryContract.at(instance.address, wallet), + Array.from(arguments).slice(1), + opts.method ?? "constructor", + ); + } + + /** + * Returns this contract's artifact. + */ + public static get artifact(): ContractArtifact { + return MigrationArchiveRegistryContractArtifact; + } + + /** + * Returns this contract's artifact with public bytecode. + */ + public static get artifactForPublic(): ContractArtifact { + return loadContractArtifactForPublic( + MigrationArchiveRegistryContractArtifactJson as NoirCompiledContract, + ); + } + + public static get storage(): ContractStorageLayout< + | "l1_migrator" + | "old_rollup_version" + | "old_key_registry" + | "snapshot_height" + | "snapshot_block_hash" + | "archive_roots" + | "block_hashes" + | "latest_proven_block" + > { + return { + l1_migrator: { + slot: new Fr(1n), + }, + old_rollup_version: { + slot: new Fr(3n), + }, + old_key_registry: { + slot: new Fr(5n), + }, + snapshot_height: { + slot: new Fr(7n), + }, + snapshot_block_hash: { + slot: new Fr(9n), + }, + archive_roots: { + slot: new Fr(11n), + }, + block_hashes: { + slot: new Fr(12n), + }, + latest_proven_block: { + slot: new Fr(13n), + }, + } as ContractStorageLayout< + | "l1_migrator" + | "old_rollup_version" + | "old_key_registry" + | "snapshot_height" + | "snapshot_block_hash" + | "archive_roots" + | "block_hashes" + | "latest_proven_block" + >; + } + + /** Type-safe wrappers for the public methods exposed by the contract. */ + declare public methods: { + /** constructor(l1_migrator: struct, old_rollup_version: field, old_key_registry: struct) */ + constructor: (( + l1_migrator: EthAddressLike, + old_rollup_version: FieldLike, + old_key_registry: AztecAddressLike, + ) => ContractFunctionInteraction) & + Pick; + + /** consume_l1_to_l2_message(archive_root: field, proven_block_number: integer, secret: field, leaf_index: field) */ + consume_l1_to_l2_message: (( + archive_root: FieldLike, + proven_block_number: bigint | number, + secret: FieldLike, + leaf_index: FieldLike, + ) => ContractFunctionInteraction) & + Pick; + + /** consume_l1_to_l2_message_and_register_block(archive_root: field, proven_block_number: integer, secret: field, leaf_index: field, block_header: struct, archive_sibling_path: array) */ + consume_l1_to_l2_message_and_register_block: (( + archive_root: FieldLike, + proven_block_number: bigint | number, + secret: FieldLike, + leaf_index: FieldLike, + block_header: { + last_archive: { root: FieldLike; next_available_leaf_index: FieldLike }; + state: { + l1_to_l2_message_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + partial: { + note_hash_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + nullifier_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + public_data_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + }; + }; + sponge_blob_hash: FieldLike; + global_variables: { + chain_id: FieldLike; + version: FieldLike; + block_number: bigint | number; + slot_number: FieldLike; + timestamp: bigint | number; + coinbase: EthAddressLike; + fee_recipient: AztecAddressLike; + gas_fees: { + fee_per_da_gas: bigint | number; + fee_per_l2_gas: bigint | number; + }; + }; + total_fees: FieldLike; + total_mana_used: FieldLike; + }, + archive_sibling_path: FieldLike[], + ) => ContractFunctionInteraction) & + Pick; + + /** get_block_hash(block_number: integer) */ + get_block_hash: (( + block_number: bigint | number, + ) => ContractFunctionInteraction) & + Pick; + + /** get_latest_proven_block() */ + get_latest_proven_block: (() => ContractFunctionInteraction) & + Pick; + + /** get_old_key_registry() */ + get_old_key_registry: (() => ContractFunctionInteraction) & + Pick; + + /** get_snapshot_block_hash() */ + get_snapshot_block_hash: (() => ContractFunctionInteraction) & + Pick; + + /** get_snapshot_height() */ + get_snapshot_height: (() => ContractFunctionInteraction) & + Pick; + + /** process_message(message_ciphertext: struct, message_context: struct) */ + process_message: (( + message_ciphertext: FieldLike[], + message_context: { + tx_hash: FieldLike; + unique_note_hashes_in_tx: FieldLike[]; + first_nullifier_in_tx: FieldLike; + recipient: AztecAddressLike; + }, + ) => ContractFunctionInteraction) & + Pick; + + /** public_dispatch(selector: field) */ + public_dispatch: ((selector: FieldLike) => ContractFunctionInteraction) & + Pick; + + /** register_block(proven_block_number: integer, block_header: struct, archive_sibling_path: array) */ + register_block: (( + proven_block_number: bigint | number, + block_header: { + last_archive: { root: FieldLike; next_available_leaf_index: FieldLike }; + state: { + l1_to_l2_message_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + partial: { + note_hash_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + nullifier_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + public_data_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + }; + }; + sponge_blob_hash: FieldLike; + global_variables: { + chain_id: FieldLike; + version: FieldLike; + block_number: bigint | number; + slot_number: FieldLike; + timestamp: bigint | number; + coinbase: EthAddressLike; + fee_recipient: AztecAddressLike; + gas_fees: { + fee_per_da_gas: bigint | number; + fee_per_l2_gas: bigint | number; + }; + }; + total_fees: FieldLike; + total_mana_used: FieldLike; + }, + archive_sibling_path: FieldLike[], + ) => ContractFunctionInteraction) & + Pick; + + /** set_snapshot_height(height: integer, snapshot_block_header: struct, proven_block_number: integer, archive_sibling_path: array) */ + set_snapshot_height: (( + height: bigint | number, + snapshot_block_header: { + last_archive: { root: FieldLike; next_available_leaf_index: FieldLike }; + state: { + l1_to_l2_message_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + partial: { + note_hash_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + nullifier_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + public_data_tree: { + root: FieldLike; + next_available_leaf_index: FieldLike; + }; + }; + }; + sponge_blob_hash: FieldLike; + global_variables: { + chain_id: FieldLike; + version: FieldLike; + block_number: bigint | number; + slot_number: FieldLike; + timestamp: bigint | number; + coinbase: EthAddressLike; + fee_recipient: AztecAddressLike; + gas_fees: { + fee_per_da_gas: bigint | number; + fee_per_l2_gas: bigint | number; + }; + }; + total_fees: FieldLike; + total_mana_used: FieldLike; + }, + proven_block_number: bigint | number, + archive_sibling_path: FieldLike[], + ) => ContractFunctionInteraction) & + Pick; + + /** sync_state() */ + sync_state: (() => ContractFunctionInteraction) & + Pick; + + /** verify_migration_mode_a(block_number: integer, block_hash: field) */ + verify_migration_mode_a: (( + block_number: bigint | number, + block_hash: FieldLike, + ) => ContractFunctionInteraction) & + Pick; + + /** verify_migration_mode_b(block_hash: field) */ + verify_migration_mode_b: (( + block_hash: FieldLike, + ) => ContractFunctionInteraction) & + Pick; + }; +} diff --git a/ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.lazy.ts b/ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.lazy.ts new file mode 100644 index 0000000..378745b --- /dev/null +++ b/ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.lazy.ts @@ -0,0 +1,178 @@ +import { AztecAddress } from "@aztec/aztec.js/addresses"; +import { + type AztecAddressLike, + type ContractArtifact, + type FieldLike, + loadContractArtifact, + type NoirCompiledContract, +} from "@aztec/aztec.js/abi"; +import { + Contract, + ContractBase, + ContractFunctionInteraction, + type ContractMethod, + type ContractStorageLayout, + DeployMethod, +} from "@aztec/aztec.js/contracts"; +import { Fr } from "@aztec/aztec.js/fields"; +import { PublicKeys } from "@aztec/aztec.js/keys"; +import type { Wallet } from "@aztec/aztec.js/wallet"; + +let cachedArtifact: ContractArtifact | undefined; + +/** + * Lazily loads the MigrationKeyRegistry contract artifact. + * Uses dynamic import to defer JSON loading until first call. + */ +export async function getMigrationKeyRegistryContractArtifact(): Promise { + if (!cachedArtifact) { + const { default: json } = await import( + "../artifacts/migration_key_registry-MigrationKeyRegistry.json", + { + with: { type: "json" }, + } + ); + cachedArtifact = loadContractArtifact(json as NoirCompiledContract); + } + return cachedArtifact; +} + +/** + * Type-safe interface for contract MigrationKeyRegistry. + * Lazily loads the contract artifact — static factory methods are async. + */ +export class MigrationKeyRegistryContract extends ContractBase { + private constructor( + address: AztecAddress, + artifact: ContractArtifact, + wallet: Wallet, + ) { + super(address, artifact, wallet); + } + + /** + * Creates a contract instance. + * @param address - The deployed contract's address. + * @param wallet - The wallet to use when interacting with the contract. + * @returns A new Contract instance. + */ + public static async at( + address: AztecAddress, + wallet: Wallet, + ): Promise { + const artifact = await getMigrationKeyRegistryContractArtifact(); + return Contract.at( + address, + artifact, + wallet, + ) as MigrationKeyRegistryContract; + } + + /** + * Creates a tx to deploy a new instance of this contract. + */ + public static async deploy(wallet: Wallet) { + const artifact = await getMigrationKeyRegistryContractArtifact(); + return new DeployMethod( + PublicKeys.default(), + wallet, + artifact, + (instance, wallet) => + Contract.at( + instance.address, + artifact, + wallet, + ) as MigrationKeyRegistryContract, + Array.from(arguments).slice(1), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified public keys hash to derive the address. + */ + public static async deployWithPublicKeys( + publicKeys: PublicKeys, + wallet: Wallet, + ) { + const artifact = await getMigrationKeyRegistryContractArtifact(); + return new DeployMethod( + publicKeys, + wallet, + artifact, + (instance, wallet) => + Contract.at( + instance.address, + artifact, + wallet, + ) as MigrationKeyRegistryContract, + Array.from(arguments).slice(2), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified constructor method. + */ + public static async deployWithOpts< + M extends keyof MigrationKeyRegistryContract["methods"], + >( + opts: { publicKeys?: PublicKeys; method?: M; wallet: Wallet }, + ...args: Parameters + ) { + const artifact = await getMigrationKeyRegistryContractArtifact(); + return new DeployMethod( + opts.publicKeys ?? PublicKeys.default(), + opts.wallet, + artifact, + (instance, wallet) => + Contract.at( + instance.address, + artifact, + wallet, + ) as MigrationKeyRegistryContract, + Array.from(arguments).slice(1), + opts.method ?? "constructor", + ); + } + + public static get storage(): ContractStorageLayout<"registered_keys"> { + return { + registered_keys: { + slot: new Fr(1n), + }, + } as ContractStorageLayout<"registered_keys">; + } + + /** Type-safe wrappers for the public methods exposed by the contract. */ + declare public methods: { + /** constructor() */ + constructor: (() => ContractFunctionInteraction) & + Pick; + /** get(owner: struct) */ + get: ((owner: AztecAddressLike) => ContractFunctionInteraction) & + Pick; + /** process_message(message_ciphertext: struct, message_context: struct) */ + process_message: (( + message_ciphertext: FieldLike[], + message_context: { + tx_hash: FieldLike; + unique_note_hashes_in_tx: FieldLike[]; + first_nullifier_in_tx: FieldLike; + recipient: AztecAddressLike; + }, + ) => ContractFunctionInteraction) & + Pick; + /** public_dispatch(selector: field) */ + public_dispatch: ((selector: FieldLike) => ContractFunctionInteraction) & + Pick; + /** register(mpk: struct) */ + register: ((mpk: { + x: FieldLike; + y: FieldLike; + is_infinite: boolean; + }) => ContractFunctionInteraction) & + Pick; + /** sync_state() */ + sync_state: (() => ContractFunctionInteraction) & + Pick; + }; +} diff --git a/ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.ts b/ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.ts new file mode 100644 index 0000000..9a37671 --- /dev/null +++ b/ts/aztec-state-migration/noir-contracts/MigrationKeyRegistry.ts @@ -0,0 +1,172 @@ +/* Autogenerated file, do not edit! */ + +/* eslint-disable */ +import { AztecAddress, CompleteAddress } from "@aztec/aztec.js/addresses"; +import { + type AbiType, + type AztecAddressLike, + type ContractArtifact, + EventSelector, + decodeFromAbi, + type EthAddressLike, + type FieldLike, + type FunctionSelectorLike, + loadContractArtifact, + loadContractArtifactForPublic, + type NoirCompiledContract, + type U128Like, + type WrappedFieldLike, +} from "@aztec/aztec.js/abi"; +import { + Contract, + ContractBase, + ContractFunctionInteraction, + type ContractMethod, + type ContractStorageLayout, + DeployMethod, +} from "@aztec/aztec.js/contracts"; +import { EthAddress } from "@aztec/aztec.js/addresses"; +import { Fr, Point } from "@aztec/aztec.js/fields"; +import { type PublicKey, PublicKeys } from "@aztec/aztec.js/keys"; +import type { Wallet } from "@aztec/aztec.js/wallet"; +import MigrationKeyRegistryContractArtifactJson from "../artifacts/migration_key_registry-MigrationKeyRegistry.json" with { type: "json" }; +export const MigrationKeyRegistryContractArtifact = loadContractArtifact( + MigrationKeyRegistryContractArtifactJson as NoirCompiledContract, +); + +/** + * Type-safe interface for contract MigrationKeyRegistry; + */ +export class MigrationKeyRegistryContract extends ContractBase { + private constructor(address: AztecAddress, wallet: Wallet) { + super(address, MigrationKeyRegistryContractArtifact, wallet); + } + + /** + * Creates a contract instance. + * @param address - The deployed contract's address. + * @param wallet - The wallet to use when interacting with the contract. + * @returns A new Contract instance. + */ + public static at( + address: AztecAddress, + wallet: Wallet, + ): MigrationKeyRegistryContract { + return Contract.at( + address, + MigrationKeyRegistryContract.artifact, + wallet, + ) as MigrationKeyRegistryContract; + } + + /** + * Creates a tx to deploy a new instance of this contract. + */ + public static deploy(wallet: Wallet) { + return new DeployMethod( + PublicKeys.default(), + wallet, + MigrationKeyRegistryContractArtifact, + (instance, wallet) => + MigrationKeyRegistryContract.at(instance.address, wallet), + Array.from(arguments).slice(1), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified public keys hash to derive the address. + */ + public static deployWithPublicKeys(publicKeys: PublicKeys, wallet: Wallet) { + return new DeployMethod( + publicKeys, + wallet, + MigrationKeyRegistryContractArtifact, + (instance, wallet) => + MigrationKeyRegistryContract.at(instance.address, wallet), + Array.from(arguments).slice(2), + ); + } + + /** + * Creates a tx to deploy a new instance of this contract using the specified constructor method. + */ + public static deployWithOpts< + M extends keyof MigrationKeyRegistryContract["methods"], + >( + opts: { publicKeys?: PublicKeys; method?: M; wallet: Wallet }, + ...args: Parameters + ) { + return new DeployMethod( + opts.publicKeys ?? PublicKeys.default(), + opts.wallet, + MigrationKeyRegistryContractArtifact, + (instance, wallet) => + MigrationKeyRegistryContract.at(instance.address, wallet), + Array.from(arguments).slice(1), + opts.method ?? "constructor", + ); + } + + /** + * Returns this contract's artifact. + */ + public static get artifact(): ContractArtifact { + return MigrationKeyRegistryContractArtifact; + } + + /** + * Returns this contract's artifact with public bytecode. + */ + public static get artifactForPublic(): ContractArtifact { + return loadContractArtifactForPublic( + MigrationKeyRegistryContractArtifactJson as NoirCompiledContract, + ); + } + + public static get storage(): ContractStorageLayout<"registered_keys"> { + return { + registered_keys: { + slot: new Fr(1n), + }, + } as ContractStorageLayout<"registered_keys">; + } + + /** Type-safe wrappers for the public methods exposed by the contract. */ + declare public methods: { + /** constructor() */ + constructor: (() => ContractFunctionInteraction) & + Pick; + + /** get(owner: struct) */ + get: ((owner: AztecAddressLike) => ContractFunctionInteraction) & + Pick; + + /** process_message(message_ciphertext: struct, message_context: struct) */ + process_message: (( + message_ciphertext: FieldLike[], + message_context: { + tx_hash: FieldLike; + unique_note_hashes_in_tx: FieldLike[]; + first_nullifier_in_tx: FieldLike; + recipient: AztecAddressLike; + }, + ) => ContractFunctionInteraction) & + Pick; + + /** public_dispatch(selector: field) */ + public_dispatch: ((selector: FieldLike) => ContractFunctionInteraction) & + Pick; + + /** register(mpk: struct) */ + register: ((mpk: { + x: FieldLike; + y: FieldLike; + is_infinite: boolean; + }) => ContractFunctionInteraction) & + Pick; + + /** sync_state() */ + sync_state: (() => ContractFunctionInteraction) & + Pick; + }; +} diff --git a/ts/aztec-state-migration/noir-contracts/index.ts b/ts/aztec-state-migration/noir-contracts/index.ts new file mode 100644 index 0000000..ded3d67 --- /dev/null +++ b/ts/aztec-state-migration/noir-contracts/index.ts @@ -0,0 +1,8 @@ +export { + MigrationKeyRegistryContract, + MigrationKeyRegistryContractArtifact, +} from "./MigrationKeyRegistry.js"; +export { + MigrationArchiveRegistryContract, + MigrationArchiveRegistryContractArtifact, +} from "./MigrationArchiveRegistry.js"; diff --git a/ts/aztec-state-migration/noir-contracts/lazy.ts b/ts/aztec-state-migration/noir-contracts/lazy.ts new file mode 100644 index 0000000..8649bcf --- /dev/null +++ b/ts/aztec-state-migration/noir-contracts/lazy.ts @@ -0,0 +1,8 @@ +export { + getMigrationKeyRegistryContractArtifact, + MigrationKeyRegistryContract, +} from "./MigrationKeyRegistry.lazy.js"; +export { + getMigrationArchiveRegistryContractArtifact, + MigrationArchiveRegistryContract, +} from "./MigrationArchiveRegistry.lazy.js"; diff --git a/ts/aztec-state-migration/package.json b/ts/aztec-state-migration/package.json new file mode 100644 index 0000000..95da78e --- /dev/null +++ b/ts/aztec-state-migration/package.json @@ -0,0 +1,78 @@ +{ + "name": "aztec-state-migration", + "author": "Cardinal Cryptography", + "version": "0.1.0", + "type": "module", + "engines": { + "node": ">=24.12.0" + }, + "packageManager": "yarn@1.22.22", + "scripts": { + "build": "tsc -p tsconfig.json", + "clean": "rm -rf dist artifacts codegenCache.json" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./wallet": { + "browser": { + "types": "./dist/wallet/entrypoints/browser.d.ts", + "default": "./dist/wallet/entrypoints/browser.js" + }, + "default": { + "types": "./dist/wallet/entrypoints/node.d.ts", + "default": "./dist/wallet/entrypoints/node.js" + } + }, + "./noir-contracts": { + "browser": { + "types": "./dist/noir-contracts/lazy.d.ts", + "default": "./dist/noir-contracts/lazy.js" + }, + "default": { + "types": "./dist/noir-contracts/index.d.ts", + "default": "./dist/noir-contracts/index.js" + } + }, + "./noir-contracts/lazy": { + "types": "./dist/noir-contracts/lazy.d.ts", + "default": "./dist/noir-contracts/lazy.js" + }, + "./wallet/base": { + "types": "./dist/wallet/migration-embedded-wallet.d.ts", + "default": "./dist/wallet/migration-embedded-wallet.js" + }, + "./constants": { + "types": "./dist/constants.d.ts", + "default": "./dist/constants.js" + }, + "./common-notes": { + "types": "./dist/common-notes.d.ts", + "default": "./dist/common-notes.js" + }, + "./mode-a": { + "types": "./dist/mode-a/index.d.ts", + "default": "./dist/mode-a/index.js" + }, + "./mode-b": { + "types": "./dist/mode-b/index.d.ts", + "default": "./dist/mode-b/index.js" + } + }, + "files": [ + "dist/" + ], + "dependencies": { + "@aztec/accounts": "v4.0.0-devnet.2-patch.0", + "@aztec/wallets": "v4.0.0-devnet.2-patch.0", + "@aztec/aztec.js": "v4.0.0-devnet.2-patch.0", + "@aztec/constants": "v4.0.0-devnet.2-patch.0", + "@aztec/entrypoints": "v4.0.0-devnet.2-patch.0", + "@aztec/foundation": "v4.0.0-devnet.2-patch.0", + "@aztec/noir-contracts.js": "v4.0.0-devnet.2-patch.0", + "@aztec/pxe": "v4.0.0-devnet.2-patch.0", + "@aztec/stdlib": "v4.0.0-devnet.2-patch.0" + } +} diff --git a/ts/aztec-state-migration/polling.ts b/ts/aztec-state-migration/polling.ts deleted file mode 100644 index 97e41f5..0000000 --- a/ts/aztec-state-migration/polling.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** Configuration for the generic {@link poll} helper. */ -export interface PollOptions { - /** Async predicate called each iteration. Return a value to stop, or `undefined` to keep polling. */ - check: () => Promise; - /** Maximum number of iterations before throwing. */ - maxAttempts: number; - /** Delay in milliseconds between iterations. */ - intervalMs: number; - /** Optional callback invoked after each unsuccessful check (e.g. to produce blocks). */ - onPoll?: (attempt: number) => Promise; - /** Error message used when `maxAttempts` is exceeded. */ - timeoutMessage: string; -} - -/** - * Generic polling helper. Calls {@link PollOptions.check} repeatedly until it - * returns a non-`undefined` value, or throws after {@link PollOptions.maxAttempts}. - * - * @param opts - Polling configuration. - * @returns The first non-`undefined` result from `check`. - * @throws If `maxAttempts` is exceeded. - */ -export async function poll(opts: PollOptions): Promise { - for (let i = 1; i <= opts.maxAttempts; i++) { - const result = await opts.check(); - if (result !== undefined) return result; - if (opts.onPoll) await opts.onPoll(i); - if (i < opts.maxAttempts) { - await new Promise((resolve) => setTimeout(resolve, opts.intervalMs)); - } - } - throw new Error(opts.timeoutMessage); -} diff --git a/ts/aztec-state-migration/proofs.ts b/ts/aztec-state-migration/proofs.ts index 3674789..42038a2 100644 --- a/ts/aztec-state-migration/proofs.ts +++ b/ts/aztec-state-migration/proofs.ts @@ -33,7 +33,7 @@ export async function buildNoteProof( ); if (!witness) { throw new Error( - `Could not get note hash membership witness for note ${uniqueHash.toString()}`, + `Could not get note hash membership witness for note ${noteDao.noteHash.toString()}`, ); } return { diff --git a/ts/aztec-state-migration/tsconfig.json b/ts/aztec-state-migration/tsconfig.json new file mode 100644 index 0000000..0efa53f --- /dev/null +++ b/ts/aztec-state-migration/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "noEmit": false + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/ts/aztec-state-migration/types.ts b/ts/aztec-state-migration/types.ts index 8cf0a2c..2643206 100644 --- a/ts/aztec-state-migration/types.ts +++ b/ts/aztec-state-migration/types.ts @@ -1,7 +1,6 @@ import type { Fr } from "@aztec/foundation/curves/bn254"; import type { blockHeaderToNoir } from "./noir-helpers/block-header.js"; import { SchnorrSignature } from "@aztec/foundation/crypto/schnorr"; -import { BlockNumber } from "@aztec/foundation/branded-types"; /** Generic note inclusion proof data. */ export interface NoteProofData { @@ -39,19 +38,3 @@ export const MigrationSignature = { bytes: [...sig.toBuffer()], }), }; - -// ============================================================ -// L1 bridge result -// ============================================================ - -/** Result of calling migrateArchiveRoot on L1. */ -export interface L1MigrationResult { - /** The proven block number from the old rollup. */ - provenBlockNumber: BlockNumber; - /** The archive root that was migrated. */ - provenArchiveRoot: Fr; - /** Leaf index of the L1→L2 message in the Inbox tree. */ - l1ToL2LeafIndex: bigint; - /** Hash of the L1→L2 message (for polling sync status). */ - l1ToL2MessageHash: Fr; -} diff --git a/ts/aztec-state-migration/wallet/entrypoints/browser.ts b/ts/aztec-state-migration/wallet/entrypoints/browser.ts index 969f662..c62c513 100644 --- a/ts/aztec-state-migration/wallet/entrypoints/browser.ts +++ b/ts/aztec-state-migration/wallet/entrypoints/browser.ts @@ -7,11 +7,7 @@ import { createPXE, } from "@aztec/pxe/client/lazy"; import { type PXEConfig, getPXEConfig } from "@aztec/pxe/config"; -import { - EmbeddedWallet, - EmbeddedWalletOptions, - WalletDB, -} from "@aztec/wallets/embedded"; +import { type EmbeddedWalletOptions, WalletDB } from "@aztec/wallets/embedded"; import { MigrationEmbeddedWallet } from "../migration-embedded-wallet.js"; import { LazyAccountContractsProvider } from "../account-contract-providers/lazy.js"; import { AccountContractsProvider } from "../account-contract-providers/types.js"; diff --git a/ts/aztec-state-migration/wallet/index.ts b/ts/aztec-state-migration/wallet/index.ts index 6b7e4a2..1325017 100644 --- a/ts/aztec-state-migration/wallet/index.ts +++ b/ts/aztec-state-migration/wallet/index.ts @@ -1,5 +1,3 @@ export type { MigrationAccount } from "./migration-account.js"; export type { MigrationBaseWallet } from "./migration-base-wallet.js"; export type { MigrationEmbeddedWallet } from "./migration-embedded-wallet.js"; -export { BrowserMigrationEmbeddedWallet } from "./entrypoints/browser.js"; -export { NodeMigrationEmbeddedWallet } from "./entrypoints/node.js"; diff --git a/ts/aztec-state-migration/wallet/migration-account.ts b/ts/aztec-state-migration/wallet/migration-account.ts index da76ce8..25667a9 100644 --- a/ts/aztec-state-migration/wallet/migration-account.ts +++ b/ts/aztec-state-migration/wallet/migration-account.ts @@ -2,7 +2,7 @@ import { Account, AccountWithSecretKey, Salt } from "@aztec/aztec.js/account"; import { deriveKeys, PublicKeys } from "@aztec/aztec.js/keys"; import { Schnorr } from "@aztec/foundation/crypto/schnorr"; import { Fq, Fr, Point } from "@aztec/aztec.js/fields"; -import { deriveMasterMigrationSecretKey } from "../keys.js"; +import { deriveMasterMigrationSecretKey } from "../key.js"; import { AztecAddress } from "@aztec/stdlib/aztec-address"; import { MigrationSignature } from "../types.js"; import { diff --git a/ts/aztec-state-migration/wallet/migration-base-wallet.ts b/ts/aztec-state-migration/wallet/migration-base-wallet.ts index 15ca39e..08144d5 100644 --- a/ts/aztec-state-migration/wallet/migration-base-wallet.ts +++ b/ts/aztec-state-migration/wallet/migration-base-wallet.ts @@ -3,11 +3,7 @@ import { Note, NoteDao } from "@aztec/stdlib/note"; import { BaseWallet } from "@aztec/wallet-sdk/base-wallet"; import { MIGRATION_NOTE_STORAGE_SLOT } from "../constants.js"; import type { AztecNode } from "@aztec/stdlib/interfaces/client"; -import { - ArchiveProofData, - MigrationSignature, - NoteProofData, -} from "../types.js"; +import { MigrationSignature, NoteProofData } from "../types.js"; import { FullProofData, NonNullificationProofData, @@ -19,19 +15,19 @@ import { } from "../mode-a/types.js"; import { BlockNumber } from "@aztec/foundation/branded-types"; import type { NotesFilter, PXE } from "@aztec/pxe/server"; -import { buildArchiveProof, buildNoteProof } from "../proofs.js"; +import { buildNoteProof } from "../proofs.js"; import { buildNullifierProof } from "../mode-b/proofs.js"; import { Point } from "@aztec/foundation/schemas"; import { AztecAddress } from "@aztec/stdlib/aztec-address"; import { MigrationAccount } from "./migration-account.js"; import { - signMigrationModeA as signModeA, signMigrationModeB as signModeB, signPublicStateMigrationModeB as signPubStateModeB, -} from "../keys.js"; +} from "../mode-b/signature.js"; +import { signMigrationModeA as signModeA } from "../mode-a/signature.js"; import { PublicKeys } from "@aztec/stdlib/keys"; -import { AbiType, EventSelector } from "@aztec/stdlib/abi"; -import { MigrationKeyRegistryContractArtifact } from "../noir-contracts/MigrationKeyRegistry.js"; +import { AbiType, decodeFromAbi, EventSelector } from "@aztec/stdlib/abi"; +import { MigrationKeyRegistryContract } from "../noir-contracts/MigrationKeyRegistry.js"; import { buildMigrationNoteProof } from "../mode-a/proofs.js"; import { Logger } from "@aztec/foundation/log"; import { BlockHash } from "@aztec/stdlib/block"; @@ -175,26 +171,12 @@ export abstract class MigrationBaseWallet extends BaseWallet { return oldAccount.getMaskedNhk(mask); } - /** - * Build an archive membership proof for the given block. - * - * @param blockHash - The proven block hash to build the proof for. - * @returns An {@link ArchiveProofData} containing the block header and Merkle path. - */ - async buildArchiveProof(blockHash: BlockHash): Promise { - return buildArchiveProof(this.aztecNode, blockHash); - } - /** * Fetch Mode A migration notes and migration data from the PXE, * filtering on the well-known {@link MIGRATION_NOTE_STORAGE_SLOT} storage slot. * * @typeParam T - The shape of the migration data (e.g. `bigint` for token amounts). - * @param contractAddress - The old rollup contract address (used for filtering and event decoding). - * @param owner - The note owner to filter by. - * @param abiType - The ABI type of the migration data (used for event decoding). - * @param scopes - Optional additional scope addresses to filter by (in addition to the owner). - * @returns An array of note+data pairs, where each note is a migration note and each data is the decoded migration data from the corresponding event. + * @param abiType - A single ABI type applied to all events. */ async getMigrationNotesAndData( contractAddress: AztecAddress, @@ -202,6 +184,88 @@ export abstract class MigrationBaseWallet extends BaseWallet { abiType: AbiType, scopes?: AztecAddress[], ): Promise[]> { + const { noteByTxHash, eventSelector, eventFilter } = + await this.getMigrationNotesEventSelector(contractAddress, owner, scopes); + let notesAndData: MigrationNoteAndData[] = []; + for (const [txHash, notes] of noteByTxHash) { + const events = await this.getPrivateEvents( + { + eventSelector, + abiType: abiType, + fieldNames: ["migration_data"], + }, + eventFilter(txHash), + ); + if (events.length !== notes.length) { + throw new Error( + `Mismatched number of events (${events.length}) and notes (${notes.length}) for tx ${txHash}.`, + ); + } + for (let i = 0; i < notes.length; i++) { + notesAndData.push({ note: notes[i], data: events[i].event }); + } + } + return notesAndData; + } + /** + * Like {@link getMigrationNotesAndData}, but for mixed-type data structures. + * + * When a single `lock_state` chain emits events with different data structures, + * pass an ordered `AbiType[]` where `abiTypes[i]` decodes the i-th event + * within each tx (matching `lock_state` call order). + * + * @param abiTypes - Ordered array of ABI types, one per `lock_state` call. + */ + async getMixedMigrationNotesAndData( + contractAddress: AztecAddress, + owner: AztecAddress, + abiTypes: AbiType[], + scopes?: AztecAddress[], + ): Promise[]> { + const { noteByTxHash, eventSelector, eventFilter } = + await this.getMigrationNotesEventSelector(contractAddress, owner, scopes); + + let notesAndData: MigrationNoteAndData[] = []; + + for (const [txHash, notes] of noteByTxHash) { + if (abiTypes.length !== notes.length) { + throw new Error( + `abiTypes array length (${abiTypes.length}) does not match number of notes (${notes.length}) for tx ${txHash}.`, + ); + } + const pxeEvents = await this.pxe.getPrivateEvents( + eventSelector, + eventFilter(txHash), + ); + if (pxeEvents.length !== notes.length) { + throw new Error( + `Mismatched number of events (${pxeEvents.length}) and notes (${notes.length}) for tx ${txHash}.`, + ); + } + for (let i = 0; i < notes.length; i++) { + const decodedEvent = decodeFromAbi( + [abiTypes[i]], + pxeEvents[i].packedEvent, + ) as unknown; + notesAndData.push({ note: notes[i], data: decodedEvent }); + } + } + return notesAndData; + } + + private async getMigrationNotesEventSelector( + contractAddress: AztecAddress, + owner: AztecAddress, + scopes?: AztecAddress[], + ): Promise<{ + noteByTxHash: Map; + eventSelector: EventSelector; + eventFilter: (txHash: string) => { + contractAddress: AztecAddress; + scopes: AztecAddress[]; + txHash: TxHash; + }; + }> { // get all migration notes for the user const allNotes = await this.getNotes({ contractAddress, @@ -209,41 +273,22 @@ export abstract class MigrationBaseWallet extends BaseWallet { scopes: scopes ?? [owner], storageSlot: MIGRATION_NOTE_STORAGE_SLOT, }); - // group events by txHash + // group notes by txHash const noteByTxHash: Map = new Map(); - allNotes.map((n) => { + for (const n of allNotes) { const currentNotes = noteByTxHash.get(n.txHash.toString()) ?? []; - noteByTxHash.set(n.txHash.toString(), [...currentNotes, n]); - }); - // prepare event definition for MigrationDataEvent + currentNotes.push(n); + noteByTxHash.set(n.txHash.toString(), currentNotes); + } + const eventSelector = await EventSelector.fromSignature("MigrationDataEvent"); - const eventDefWithSelector = { - eventSelector, - abiType, - fieldNames: ["migration_data"], - }; - let notesAndData: MigrationNoteAndData[] = []; - // for each txHash, get the corresponding MigrationDataEvent and pair it with the notes - for (const [txHash, notes] of noteByTxHash) { - const events = await this.getPrivateEvents(eventDefWithSelector, { - contractAddress, - scopes: scopes ?? [owner], - txHash: TxHash.fromString(txHash), - }); - if (events.length != notes.length) { - throw new Error( - `Mismatched number of events (${events.length}) and notes (${notes.length}) for tx ${txHash}.`, - ); - } - for (let i = 0; i < notes.length; i++) { - notesAndData.push({ - note: notes[i], - data: events[i].event, - }); - } - } - return notesAndData; + const eventFilter = (txHash: string) => ({ + contractAddress, + scopes: scopes ?? [owner], + txHash: TxHash.fromString(txHash), + }); + return { noteByTxHash, eventSelector, eventFilter }; } /** @@ -280,84 +325,78 @@ export abstract class MigrationBaseWallet extends BaseWallet { return results.filter(({ migrated }) => !migrated).map(({ note }) => note); } /** - * Build note-hash inclusion proofs for a batch of notes. + * Build a note-hash inclusion proof for a single note. * - * @param blockNumber - Block number at which to prove inclusion. - * @param notes - The notes to prove. - * @param noteMapper - Callback that decodes each raw {@link Note} into the desired shape. - * @returns An array of {@link NoteProofData}, one per input note. + * @param blockReference - Block number or hash at which to prove inclusion. + * @param note - The note to prove. + * @param noteMapper - Callback that decodes the raw {@link Note} into the desired shape. + * @returns Proof data containing the decoded note, storage slot, randomness, nonce, and sibling path. */ - async buildNoteProofs( - blockNumber: BlockNumber, - notes: NoteDao[], + async buildNoteProof( + blockReference: BlockNumber | BlockHash, + note: NoteDao, noteMapper: (note: Note) => NoteLike, - ): Promise[]> { - return Promise.all( - notes.map((n) => - buildNoteProof(this.aztecNode, blockNumber, n, noteMapper), - ), - ); + ): Promise> { + return buildNoteProof(this.aztecNode, blockReference, note, noteMapper); } /** - * Build note-hash inclusion proofs for a batch of notes. + * Build a migration note-hash inclusion proof for a single note+data pair. * - * @param blockNumber - Block number at which to prove inclusion. - * @param notes - The notes to prove. - * @param noteMapper - Callback that decodes each raw {@link Note} into the desired shape. - * @returns An array of {@link NoteProofData}, one per input note. + * @param blockReference - Block number or hash at which to prove inclusion. + * @param migrationNotesAndData - The note and its associated migration data. + * @returns Proof data containing the migration data, storage slot, randomness, nonce, and sibling path. */ - async buildMigrationNoteProofs( - blockNumber: BlockNumber, - migrationNotesAndData: MigrationNoteAndData[], - ): Promise[]> { - return Promise.all( - migrationNotesAndData.map(({ note, data }) => - buildMigrationNoteProof(this.aztecNode, blockNumber, note, data), - ), + async buildMigrationNoteProof( + blockReference: BlockNumber | BlockHash, + migrationNotesAndData: MigrationNoteAndData, + ): Promise> { + return buildMigrationNoteProof( + this.aztecNode, + blockReference, + migrationNotesAndData.note, + migrationNotesAndData.data, ); } /** - * Build nullifier non-inclusion proofs for a batch of notes. + * Build a nullifier non-inclusion proof for a single note. * - * @param blockNumber - Block number at which to prove non-inclusion. - * @param notes - The notes whose siloed nullifiers are checked. - * @returns An array of {@link NullifierProofData}, one per input note. + * @param blockReference - Block number or hash at which to prove non-inclusion. + * @param note - The note whose siloed nullifier is checked. + * @returns Proof data containing the nullifier, low-leaf preimage, and sibling path. */ - async buildNullifierProofs( - blockNumber: BlockNumber, - notes: NoteDao[], - ): Promise { - return Promise.all( - notes.map((n) => buildNullifierProof(this.aztecNode, blockNumber, n)), - ); + async buildNullifierProof( + blockReference: BlockNumber | BlockHash, + note: NoteDao, + ): Promise { + return buildNullifierProof(this.aztecNode, blockReference, note); } /** * Build combined note-hash inclusion **and** nullifier non-inclusion proofs. - * Merges the results of {@link buildNoteProofs} and {@link buildNullifierProofs}. + * Merges the results of {@link buildNoteProof} and {@link buildNullifierProof}. * - * @param blockNumber - Block number at which to prove. - * @param notes - The notes to prove. - * @param noteMapper - Callback that decodes each raw {@link Note}. - * @returns An array of {@link FullProofData}, one per input note. + * @param blockReference - Block number or hash at which to prove. + * @param note - The note to prove. + * @param noteMapper - Callback that decodes the raw {@link Note}. + * @returns Proof data containing the decoded note, storage slot, randomness, nonce, and sibling path. */ - async buildFullNoteProofs( - blockNumber: BlockNumber, - notes: NoteDao[], + async buildFullNoteProof( + blockReference: BlockNumber | BlockHash, + note: NoteDao, noteMapper: (note: Note) => NoteLike, - ): Promise[]> { - const noteProofs = await this.buildNoteProofs( - blockNumber, - notes, + ): Promise> { + const noteProof = await this.buildNoteProof( + blockReference, + note, noteMapper, ); - const nullifierProofs = await this.buildNullifierProofs(blockNumber, notes); - return noteProofs.map((noteProof, i) => ({ + const nullifierProof = await this.buildNullifierProof(blockReference, note); + return { note_proof_data: noteProof, - non_nullification_proof_data: nullifierProofs[i], - })); + non_nullification_proof_data: nullifierProof, + }; } /** @@ -365,20 +404,19 @@ export abstract class MigrationBaseWallet extends BaseWallet { * * @param keyRegistry - Address of the key registry contract. * @param owner - Owner of the key note. - * @param blockNumber - Block number at which to prove inclusion. + * @param blockReference - Block number or hash at which to prove inclusion. * @returns Proof data containing the decoded key note, storage slot, randomness, nonce, and sibling path. */ async buildKeyNoteProofData( keyRegistry: AztecAddress, owner: AztecAddress, - blockNumber: BlockNumber, + blockReference: BlockNumber | BlockHash, ): Promise> { const keyNotes = await this.getNotes({ owner: owner, contractAddress: keyRegistry, - storageSlot: - MigrationKeyRegistryContractArtifact.storageLayout.registered_keys.slot, - scopes: [owner], // Only fetch notes owned by the specified address + storageSlot: MigrationKeyRegistryContract.storage.registered_keys.slot, + scopes: [owner], }); if (keyNotes.length === 0) { throw new Error("No key notes found"); @@ -387,7 +425,7 @@ export abstract class MigrationBaseWallet extends BaseWallet { } return await buildNoteProof( this.aztecNode, - blockNumber, + blockReference, keyNotes[0], (note) => KeyNote.fromNote(note), ); diff --git a/ts/aztec-state-migration/wallet/migration-embedded-wallet.ts b/ts/aztec-state-migration/wallet/migration-embedded-wallet.ts index 483e60d..b195489 100644 --- a/ts/aztec-state-migration/wallet/migration-embedded-wallet.ts +++ b/ts/aztec-state-migration/wallet/migration-embedded-wallet.ts @@ -18,12 +18,9 @@ import { mergeExecutionPayloads, TxSimulationResult, } from "@aztec/stdlib/tx"; -import { - getContractInstanceFromInstantiationParams, - InteractionFeeOptions, -} from "@aztec/aztec.js/contracts"; +import { getContractInstanceFromInstantiationParams } from "@aztec/aztec.js/contracts"; import { DefaultAccountEntrypointOptions } from "@aztec/entrypoints/account"; -import { BaseWallet, type FeeOptions } from "@aztec/wallet-sdk/base-wallet"; +import { type FeeOptions } from "@aztec/wallet-sdk/base-wallet"; import { deriveKeys, derivePublicKeyFromSecretKey, @@ -34,7 +31,7 @@ import { MigrationAccountWithSecretKey } from "./migration-account.js"; import { Point } from "@aztec/foundation/schemas"; import { AccountContractsProvider } from "./account-contract-providers/types.js"; import { MigrationSignature } from "../types.js"; -import { deriveMasterMigrationSecretKey } from "../keys.js"; +import { deriveMasterMigrationSecretKey } from "../key.js"; /** * Concrete migration wallet for testing. Creates its own PXE instance and diff --git a/tsconfig.json b/tsconfig.json index efbe989..53e8d10 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,10 +8,8 @@ "strict": true, "skipLibCheck": true, "resolveJsonModule": true, - "outDir": "./dist", - "rootDir": ".", - "declaration": true + "noEmit": true }, - "include": ["e2e-tests/**/*.ts", "ts/**/*.ts"], + "include": ["e2e-tests/**/*.ts"], "exclude": ["node_modules", "dist"] }