Skip to content

Feature: unconsumed l1-to-l2 message migration - #57

Merged
JanKuczma merged 9 commits into
mainfrom
feature/unconsumed-l1-to-l2-msg-migration
Mar 22, 2026
Merged

Feature: unconsumed l1-to-l2 message migration#57
JanKuczma merged 9 commits into
mainfrom
feature/unconsumed-l1-to-l2-msg-migration

Conversation

@JanKuczma

@JanKuczma JanKuczma commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • L1→L2 message migration for Mode B: builder-based migration step plus end-user proof construction helpers and runtime migration entrypoint.
  • Documentation

    • Expanded integration guide, specs, architecture, and security docs (including multi-version migration guidance) and added a top-level README disclaimer.
  • Tests

    • New end-to-end L1→L2 message migration test and a minor test assertion update.
  • Chores

    • CI now runs the new Mode B L1→L2 message test and a new test script was added to package scripts.

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 25f76b93-e8a0-40ba-94e3-61b8be675800

📥 Commits

Reviewing files that changed from the base of the PR and between 15474b7 and 1f9ab06.

📒 Files selected for processing (3)
  • docs/security.md
  • docs/spec/mode-b-spec.md
  • noir/aztec-state-migration/src/mode_b/non_nullification_proof_data.nr

Walkthrough

Adds L1→L2 message migration to Mode B: new Noir proof-data types and builder methods, migration nullifier constant and verification, TypeScript proof builders and types, e2e tests and test-utils exports, CI/test script integration, and documentation updates.

Changes

Cohort / File(s) Summary
CI & Scripts
/.github/workflows/ci.yml, package.json
Added CI step and test:mode-b:l1msg script to run the new L1→L2 message E2E test.
Documentation
docs/architecture.md, docs/integration-guide.md, docs/security.md, docs/spec/migration-spec.md, docs/spec/mode-b-spec.md
Rewrote Mode B docs to builder-style (.with_l1_to_l2_message(), .finish_at_snapshot()/.finish_at_block()), documented L1→L2 message proof semantics, migration nullifier, and multi-version migration risks/mitigations.
E2E Tests & Utils
e2e-tests/migration-mode-b-l1-to-l2-message.test.ts, e2e-tests/migration-mode-b.test.ts, e2e-tests/test-utils.ts
Added comprehensive L1→L2 message migration E2E test; exported produceBlock and waitForL1ToL2Message; adjusted an expected revert string.
Noir: Mode B Core
noir/aztec-state-migration/src/constants.nr, noir/aztec-state-migration/src/mode_b/...
Added DOM_SEP__L1_TO_L2_MIGRATION_NULLIFIER; new L1ToL2MessageProofData and FullL1ToL2MessageProofData; message inclusion and non-inclusion verification; centralized non-inclusion helper; MigrationModeB::_migrate_l1_to_l2_message and .with_l1_to_l2_message() across builder states.
Noir: Test Contracts
noir/test-contracts/example-app/v1/src/main.nr, noir/test-contracts/example-app/v2/src/main.nr, noir/test-contracts/token-migration-app/v2/src/main.nr
Added L1→L2 consume helper in v1; added migrate_l1_to_l2_message_mode_b entrypoint and imports in v2; minor doc update in token migration contract.
TypeScript SDK
ts/aztec-state-migration/mode-b/index.ts, ts/aztec-state-migration/mode-b/proofs.ts, ts/aztec-state-migration/mode-b/types.ts
Added TS types L1ToL2MessageProofData / FullL1ToL2MessageProofData and proof builders: buildL1ToL2MessageProof, buildL1ToL2MessageNullifierProof, buildFullL1ToL2MessageProof; exported from mode-b index.
Repo Top-level
README.md
Added top-level disclaimer block.

Sequence Diagram

sequenceDiagram
    participant Client as Client (TS)
    participant ProofBuilder as Proof Builder (TS)
    participant AztecNode as AztecNode
    participant MigrationBuilder as Mode B Builder (Noir)
    participant NullifierTree as Nullifier Tree

    Client->>ProofBuilder: buildFullL1ToL2MessageProof(node, blockRef, oldApp, message, secret)
    par Inclusion branch
        ProofBuilder->>AztecNode: getL1ToL2MessageMembershipWitness()
        AztecNode-->>ProofBuilder: inclusion witness (sender, content, secret_hash, leaf_index, sibling_path)
        ProofBuilder->>ProofBuilder: construct L1ToL2MessageProofData
    and Non-inclusion branch
        ProofBuilder->>ProofBuilder: compute message_hash from secret
        ProofBuilder->>AztecNode: getLowNullifierMembershipWitness()
        AztecNode-->>ProofBuilder: non-inclusion witness
        ProofBuilder->>ProofBuilder: construct NonNullificationProofData
    end
    ProofBuilder-->>Client: FullL1ToL2MessageProofData

    Client->>MigrationBuilder: .without_owner().with_l1_to_l2_message(proof)
    MigrationBuilder->>MigrationBuilder: verify_message_inclusion(...)
    MigrationBuilder->>NullifierTree: verify_message_nullifier_non_inclusion(...)
    NullifierTree-->>MigrationBuilder: ✓ not consumed
    MigrationBuilder->>MigrationBuilder: compute migration_nullifier using DOM_SEP__L1_TO_L2_MIGRATION_NULLIFIER
    MigrationBuilder-->>Client: builder updated
    Client->>MigrationBuilder: .finish_at_snapshot()
    MigrationBuilder-->>Client: ✓ migration complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • DamianStraszak

Poem

🐰 Hop, hop — a message crossed the rollup line,
With secrets hashed and proofs aligned,
Builders stitch the old to new,
Nullifiers guard against twice-through,
I nibble logs and cheer: migration's fine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: support for unconsumed L1-to-L2 message migration, which is the primary theme across multiple documentation, test, and implementation files in this changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/unconsumed-l1-to-l2-msg-migration

Comment @coderabbitai help to get the list of available commands and usage tips.

@JanKuczma
JanKuczma force-pushed the feature/mode-b-at-block branch from 384141a to 7e21692 Compare March 22, 2026 19:47
Base automatically changed from feature/mode-b-at-block to main March 22, 2026 20:09
@JanKuczma
JanKuczma force-pushed the feature/unconsumed-l1-to-l2-msg-migration branch from 2293ba3 to 5fe0e63 Compare March 22, 2026 20:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/spec/migration-spec.md (1)

428-441: ⚠️ Potential issue | 🟡 Minor

Document the new L1→L2 proof path in the general spec too.

MigrationModeB now exposes .with_l1_to_l2_message(...), but this file still only enumerates private-note and public-state proof semantics, and the earlier scope text still reads as if L1-bridged state is excluded. Please reconcile those sections so the general spec does not contradict the Mode B spec.

As per coding guidelines, "Update docs/spec/mode-a-spec.md, docs/spec/mode-b-spec.md, and/or docs/architecture.md when contract interfaces change (public functions, events, externally visible behavior)."

Also applies to: 453-459

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/spec/migration-spec.md` around lines 428 - 441, The general migration
spec contradicts the Mode B details because MigrationModeB now exposes
.with_l1_to_l2_message(...) but the general-spec text and the listed proof
semantics only describe private-note and public-state paths; update the general
spec text to explicitly include the L1→L2 proof path and describe its semantics
(address/key-note verification, block-hash enqueueing to
MigrationArchiveRegistry, and any L1→L2 message inclusion rules) so it matches
MigrationModeB, and also update docs/spec/mode-b-spec.md (and if relevant
docs/spec/mode-a-spec.md or docs/architecture.md) to reflect the new public API
and remove the earlier scope wording that excludes L1-bridged state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/spec/mode-b-spec.md`:
- Around line 84-90: Add the ```text fence around the ASCII diagram containing
"L1-to-L2 message", "l1_to_l2_message_tree_root", "BlockHeader",
"nullifier_tree_root", and "snapshot_block_hash" in docs/spec/mode-b-spec.md so
the code block is tagged as text (i.e., replace the opening triple backticks
with ```text and keep the closing ```), which satisfies the markdownlint MD040
rule.

In `@e2e-tests/migration-mode-b-l1-to-l2-message.test.ts`:
- Around line 261-293: Add a negative test that proves using the correct message
proof with a different secret fails: after building a proof with
buildFullL1ToL2MessageProof (e.g., messageProof/msgSecret or
consumedMessageProof/consumedSecret), create a differentSecret (not equal to
msgSecret/consumedSecret) and call
newAppUser.methods.migrate_l1_to_l2_message_mode_b(proof, differentSecret,
blockHeader).send({ from: newUserManager.address }) and wrap it in
expectRevert(...) asserting the contract's revert message for a
secret/secretHash mismatch (i.e., the same revert string used by the contract
when the provided secret doesn't match the proof).
- Around line 100-107: The test currently locates the MessageSent log via
msgReceipt.logs and extracts the leaf index by slicing raw log.data
(msgSentLog.data.slice(0,66)), which is brittle; instead decode the event using
the InboxAbi (or the findEvent helper) so you get the typed field. Replace the
raw slice usage for msgLeafIndex with a decoded event call (decodeEventLog({
abi: InboxAbi, data: msgSentLog.data, topics: msgSentLog.topics })) and read the
decoded index field (or call findEvent to obtain the MessageSent event and use
its index property); update both occurrences (the block using msgSentLog and the
second block at lines ~155-162) to use decoded.index rather than slicing data.

In `@noir/aztec-state-migration/src/mode_b/builder.nr`:
- Around line 214-228: The proof of message inclusion
(message_proof.verify_message_inclusion) currently returns only a message_hash
but does not bind the caller-supplied secret to the committed secret in the
proof, allowing secret substitution; fix by extracting or returning the
committed secret hash (e.g., secretHash) from L1ToL2MessageProofData (or make
verify_message_inclusion expose/verify it) and assert before calling
non_null_proof.verify_message_nullifier_non_inclusion that hash(secret) ==
committed secretHash for the included leaf, then pass the verified secret to
verify_message_nullifier_non_inclusion to ensure the nullifier check uses the
same secret bound in the inclusion proof.

In `@ts/aztec-state-migration/mode-b/types.ts`:
- Around line 2-3: Remove the unused import AztecAddress from the top of the
file: keep the existing import of EthAddress and the Fr type but delete the
import line that brings in AztecAddress (references: AztecAddress, EthAddress,
Fr, types.ts) so only used types remain imported.

---

Outside diff comments:
In `@docs/spec/migration-spec.md`:
- Around line 428-441: The general migration spec contradicts the Mode B details
because MigrationModeB now exposes .with_l1_to_l2_message(...) but the
general-spec text and the listed proof semantics only describe private-note and
public-state paths; update the general spec text to explicitly include the L1→L2
proof path and describe its semantics (address/key-note verification, block-hash
enqueueing to MigrationArchiveRegistry, and any L1→L2 message inclusion rules)
so it matches MigrationModeB, and also update docs/spec/mode-b-spec.md (and if
relevant docs/spec/mode-a-spec.md or docs/architecture.md) to reflect the new
public API and remove the earlier scope wording that excludes L1-bridged state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a22f917e-8de1-4589-9bbf-28a5c9978e6c

📥 Commits

Reviewing files that changed from the base of the PR and between 4afcdc2 and 5fe0e63.

📒 Files selected for processing (21)
  • .github/workflows/ci.yml
  • docs/architecture.md
  • docs/integration-guide.md
  • docs/security.md
  • docs/spec/migration-spec.md
  • docs/spec/mode-b-spec.md
  • e2e-tests/migration-mode-b-l1-to-l2-message.test.ts
  • e2e-tests/migration-mode-b.test.ts
  • e2e-tests/test-utils.ts
  • noir/aztec-state-migration/src/constants.nr
  • noir/aztec-state-migration/src/mode_b/builder.nr
  • noir/aztec-state-migration/src/mode_b/l1_to_l2_message_proof_data.nr
  • noir/aztec-state-migration/src/mode_b/mod.nr
  • noir/aztec-state-migration/src/mode_b/non_nullification_proof_data.nr
  • noir/test-contracts/example-app/v1/src/main.nr
  • noir/test-contracts/example-app/v2/src/main.nr
  • noir/test-contracts/token-migration-app/v2/src/main.nr
  • package.json
  • ts/aztec-state-migration/mode-b/index.ts
  • ts/aztec-state-migration/mode-b/proofs.ts
  • ts/aztec-state-migration/mode-b/types.ts

Comment thread docs/spec/mode-b-spec.md
Comment thread e2e-tests/migration-mode-b-l1-to-l2-message.test.ts
Comment thread e2e-tests/migration-mode-b-l1-to-l2-message.test.ts
Comment thread noir/aztec-state-migration/src/mode_b/builder.nr
Comment thread ts/aztec-state-migration/mode-b/types.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
e2e-tests/migration-mode-b-l1-to-l2-message.test.ts (2)

100-107: ⚠️ Potential issue | 🟠 Major

Decode MessageSent via ABI instead of slicing raw log data.

Extracting the leaf index via log.data.slice(0, 66) is brittle and can silently break if event layout changes. Please decode by event signature (InboxAbi) or use the shared findEvent helper.

#!/bin/bash
# Verify brittle raw-log slicing is present and ABI decoding is absent in this test.
rg -n -C2 'data\.slice\(0,\s*66\)|MessageSent event' e2e-tests/migration-mode-b-l1-to-l2-message.test.ts
rg -n -C2 'decodeEventLog|findEvent' e2e-tests/migration-mode-b-l1-to-l2-message.test.ts

Also applies to: 155-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e-tests/migration-mode-b-l1-to-l2-message.test.ts` around lines 100 - 107,
Replace the brittle manual slice of the log data when extracting the MessageSent
leaf index: locate where msgReceipt and msgSentLog are used (the const
msgSentLog = ... and const msgLeafIndex = ... lines) and decode the event using
the InboxAbi (or the shared findEvent helper) instead of log.data.slice(0,66);
e.g., use an ethers Interface for InboxAbi to call decodeEventLog('MessageSent',
msgSentLog.data, msgSentLog.topics) or call the project's findEvent(msgReceipt,
'MessageSent') helper, then read the leaf index from the decoded event result
and assign it to msgLeafIndex.

261-289: 🧹 Nitpick | 🔵 Trivial

Add a mismatched-secret negative test for this security-critical path.

Please add a case where the same message/proof is attempted with a different secret and is expected to revert, to guard secret-to-secretHash binding regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e-tests/migration-mode-b-l1-to-l2-message.test.ts` around lines 261 - 289,
Add a negative test that attempts to migrate the same L1→L2 message using a
different secret to ensure secret-to-secretHash binding is enforced: create a
new wrongSecret, call buildFullL1ToL2MessageProof(oldAztecNode,
provenBlockNumber, oldApp.address, consumedL1ToL2Message, wrongSecret) to
produce a mismatched secret proof, then call
newAppUser.methods.migrate_l1_to_l2_message_mode_b(...) .send({ from:
newUserManager.address }) and assert it reverts (use expectRevert) to verify the
migration fails with a mismatched secret.
docs/spec/mode-b-spec.md (1)

84-84: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced proof-chain block.

The opening fence should be ```text to satisfy MD040.

📝 Proposed fix
-```
+```text
 L1-to-L2 message ──Merkle proof──> l1_to_l2_message_tree_root ──(embedded in)──> BlockHeader
                                                                                       │
 consumption nullifier ──low-leaf non-inclusion──> nullifier_tree_root                 │
                                                                                       │
                                                   BlockHeader.hash() ──(== block_hash)──> snapshot_block_hash
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/spec/mode-b-spec.md at line 84, The fenced proof-chain code block
currently opens with (no language) which triggers MD040; update the opening fence for that block (the one containing "L1-to-L2 message ──Merkle proof──> l1_to_l2_message_tree_root ... snapshot_block_hash") to use a language tag by changing the opening fence totext so the block becomes text ... .


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @docs/spec/migration-spec.md:

  • Around line 453-460: Update the MigrationModeB row in the Migration Library
    Builders table to explicitly mention L1→L2 message migration: include
    "unconsumed L1→L2 messages" (or similar phrasing) in the Description for
    MigrationModeB and ensure the Key Methods listing keeps
    .with_l1_to_l2_message(proof) so readers see that MigrationModeB verifies and
    migrates unconsumed L1→L2 messages via the .with_l1_to_l2_message method.

In @noir/aztec-state-migration/src/mode_b/builder.nr:

  • Around line 206-235: The spec is inconsistent with the implementation:
    builder.nr computes migration_nullifier using
    poseidon2_hash_with_separator([message_hash, secret, self.old_app.to_field()],
    DOM_SEP__L1_TO_L2_MIGRATION_NULLIFIER) while docs/spec/mode-b-spec.md currently
    states poseidon2([message_hash, old_app], ...); update the spec to state the
    migration nullifier formula includes the secret as the second input (i.e.,
    poseidon2([message_hash, secret, old_app],
    DOM_SEP__L1_TO_L2_MIGRATION_NULLIFIER)) and mention the relation to
    verify_message_inclusion and verify_message_nullifier_non_inclusion to avoid
    confusion about secret binding.

In @ts/aztec-state-migration/mode-b/proofs.ts:

  • Around line 238-245: The returned proof object is using message.index for
    leaf_index but must use the membership witness' index; replace the use of
    message.index with the witness-provided index variable (the _leafIndex from
    const [_leafIndex, siblingPath] = witness) when setting leaf_index
    (convert/format to the expected type if necessary) so the proof uses the
    canonical witness leaf index rather than the caller-supplied message.index.

Duplicate comments:
In @docs/spec/mode-b-spec.md:

  • Line 84: The fenced proof-chain code block currently opens with (no language) which triggers MD040; update the opening fence for that block (the one containing "L1-to-L2 message ──Merkle proof──> l1_to_l2_message_tree_root ... snapshot_block_hash") to use a language tag by changing the opening fence totext so the block becomes text ... .

In @e2e-tests/migration-mode-b-l1-to-l2-message.test.ts:

  • Around line 100-107: Replace the brittle manual slice of the log data when
    extracting the MessageSent leaf index: locate where msgReceipt and msgSentLog
    are used (the const msgSentLog = ... and const msgLeafIndex = ... lines) and
    decode the event using the InboxAbi (or the shared findEvent helper) instead of
    log.data.slice(0,66); e.g., use an ethers Interface for InboxAbi to call
    decodeEventLog('MessageSent', msgSentLog.data, msgSentLog.topics) or call the
    project's findEvent(msgReceipt, 'MessageSent') helper, then read the leaf index
    from the decoded event result and assign it to msgLeafIndex.
  • Around line 261-289: Add a negative test that attempts to migrate the same
    L1→L2 message using a different secret to ensure secret-to-secretHash binding is
    enforced: create a new wrongSecret, call
    buildFullL1ToL2MessageProof(oldAztecNode, provenBlockNumber, oldApp.address,
    consumedL1ToL2Message, wrongSecret) to produce a mismatched secret proof, then
    call newAppUser.methods.migrate_l1_to_l2_message_mode_b(...) .send({ from:
    newUserManager.address }) and assert it reverts (use expectRevert) to verify the
    migration fails with a mismatched secret.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: ASSERTIVE

**Plan**: Pro

**Run ID**: `620b5dad-1b25-470d-9b7b-c1c8a3d0d812`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5fe0e63c562f8f2e1e374b757471ed82f0056328 and 15474b7fe951db39195130c1914330e5bb6c914c.

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `README.md`
* `docs/integration-guide.md`
* `docs/security.md`
* `docs/spec/migration-spec.md`
* `docs/spec/mode-b-spec.md`
* `e2e-tests/migration-mode-b-l1-to-l2-message.test.ts`
* `noir/aztec-state-migration/src/mode_b/builder.nr`
* `noir/aztec-state-migration/src/mode_b/l1_to_l2_message_proof_data.nr`
* `noir/test-contracts/example-app/v2/src/main.nr`
* `ts/aztec-state-migration/mode-b/proofs.ts`
* `ts/aztec-state-migration/mode-b/types.ts`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread docs/spec/migration-spec.md
Comment thread noir/aztec-state-migration/src/mode_b/builder.nr
Comment thread ts/aztec-state-migration/mode-b/proofs.ts
@JanKuczma
JanKuczma merged commit f4d1c41 into main Mar 22, 2026
3 checks passed
@JanKuczma
JanKuczma deleted the feature/unconsumed-l1-to-l2-msg-migration branch March 22, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants