Skip to content

fix(agglayer): enforce canonical zero padding on leaf data#3003

Open
Fumuran wants to merge 5 commits into
nextfrom
fumuran-claude/enforce-leaf-padding
Open

fix(agglayer): enforce canonical zero padding on leaf data#3003
Fumuran wants to merge 5 commits into
nextfrom
fumuran-claude/enforce-leaf-padding

Conversation

@Fumuran
Copy link
Copy Markdown
Contributor

@Fumuran Fumuran commented May 28, 2026

Summary

Closes #2985.

The bridge leaf is a 113-byte keccak preimage stored as 32 felts: 29 data felts followed by 3 trailing padding felts. pack_leaf_data folds padding[0]'s low 3 bytes into the unused tail (byte positions 113-115) of the final packed u32. The keccak precompile truncates the hash to 113 bytes, so padding never changes LEAF_VALUE, but it requires that tail to be zero - and only enforces it implicitly, via a generic InvalidPadding failure. padding[1] and padding[2] are never read by the packer or the hash at all, so nothing checked them.

On the bridge-in/CLAIM path the leaf data is piped verbatim from the prover-supplied advice map, so the padding felts were never explicitly validated.

Fix

Make the requirement explicit and total in the shared helper. pack_leaf_data now calls a new assert_padding_is_zero helper (under a HELPER PROCEDURES section) that asserts all 3 trailing padding felts are zero before packing, panicking with ERR_LEAF_PADDING_NOT_ZERO otherwise. Because the check lives in the shared helper, it also covers bridge-out (which already zeros these felts) and any future caller, and it yields a clear bridge-specific error instead of the generic precompile padding failure.

This is defense-in-depth: it does not change LEAF_VALUE or the claim nullifier (the nullifier is derived from the leaf index and source network, not the leaf preimage).

Changes

  • crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm
    • New error const ERR_LEAF_PADDING_NOT_ZERO.
    • PADDING_OFFSET derived from PACKED_DATA_NUM_ELEMENTS so it stays coupled to the packing loop bound.
    • New private assert_padding_is_zero helper; called from pack_leaf_data before the loop. Docs on pack_leaf_data / compute_leaf_value updated.
  • crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm
    • get_leaf_value advice-map doc updated to state padding must be zero (and why).
  • crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm
    • No logic change (already zeros padding); PADDING_OFFSET now derived from METADATA_HASH_OFFSET + 8; comment/doc updated.
  • crates/miden-agglayer/SPEC.md
    • CLAIM storage padding row and bridge_in::claim Panics row updated.
  • crates/miden-testing/tests/agglayer/leaf_utils.rs
    • New pack_leaf_data_rejects_non_zero_padding test exercising all three padding offsets (29/30/31).

Testing

cargo test -p miden-testing --test lib agglayer:: - all 61 agglayer tests pass, including the new negative test.

🤖 Generated with Claude Code

Fumuran pushed a commit that referenced this pull request May 28, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm Outdated
Comment thread crates/miden-agglayer/SPEC.md Outdated
Comment thread CHANGELOG.md Outdated
Copy link
Copy Markdown
Collaborator

@mmagician mmagician left a comment

Choose a reason for hiding this comment

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

I'm wondering how easy would it be to add a regression test for what the original issue describes:

a malicious prover can change LEAF_VALUE - and thus the CGI chain hash the AggLayer side must reproduce - without changing any real leaf field.

This would probably require some manipulation of the data loaded in the advice provider, but I haven't thought too much how the exact attack would look like.

@Fumuran
Copy link
Copy Markdown
Contributor Author

Fumuran commented May 28, 2026

I'm wondering how easy would it be to add a regression test for what the original issue describes

I'm not sure that we can assert the CGI chain hash if the leaf data is changed, we never actually read the CGI chain hash value. Or maybe I just misunderstood the idea

Edit: I think implicitly we check it during the existing test: we change the padding values and make sure that this change is caught and will not result in the CGI hash chain corruption

Comment thread crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm Outdated
Comment thread crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm Outdated
claude added 5 commits May 29, 2026 01:07
The bridge leaf is a 113-byte keccak preimage stored as 32 felts (29
data felts + 3 trailing padding felts). pack_leaf_data folds padding[0]'s
low 3 bytes into the unused tail (byte positions 113..115) of the final
packed u32. The keccak precompile truncates the hash to 113 bytes, so
padding never changes LEAF_VALUE, but it requires that tail to be zero
and only implicitly (a generic InvalidPadding failure). padding[1] and
padding[2] are never read by the packer or the hash at all, so nothing
checked them.

On the bridge-in/CLAIM path the leaf data is piped verbatim from the
prover-supplied advice map, so the padding felts were never explicitly
validated. Make the canonical-preimage requirement explicit and total in
the shared helper: pack_leaf_data now calls a new assert_padding_is_zero
helper that asserts all 3 trailing padding felts are zero before packing,
panicking with ERR_LEAF_PADDING_NOT_ZERO. The check is shared, so it also
covers bridge-out (which already zeros these felts) and any future
caller, and it yields a clear bridge-specific error.

Docs updated on pack_leaf_data, compute_leaf_value, get_leaf_value,
add_leaf_bridge, and SPEC.md. Adds a negative test exercising all three
padding offsets.

Closes #2985

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses PR review:
- rename leaf_utils PADDING_OFFSET -> LEAF_DATA_PADDING_OFFSET, simplify comment
- trim get_leaf_value / assert_padding_is_zero docs to the essentials
- standardize Panics wording to "...in the leaf data is non-zero"
- bridge_out: extract padding-zeroing into a zero_padding_felts helper and
  roll back the PADDING_OFFSET derivation / extra comments
- roll back SPEC padding-row note and trim the CHANGELOG entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuse the base padding pointer across the 3 stores instead of recomputing
LEAF_DATA_START_PTR + PADDING_OFFSET each time. Uses `swap` in place of the
suggested `movup.1` (the assembler restricts movup immediates to 2..15).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass the input pointer to assert_padding_is_zero via `dup` before
`loc_store`, avoiding the extra `loc_load` to read it back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Fumuran Fumuran force-pushed the fumuran-claude/enforce-leaf-padding branch from 5b45944 to d485152 Compare May 28, 2026 22:07
@Fumuran Fumuran marked this pull request as ready for review May 28, 2026 22:16
@mmagician
Copy link
Copy Markdown
Collaborator

I think the fix for the stated problem "enforce canonical zero padding on leaf data" is correct, but I wonder whether this is a real issue in the first place, now that I deep dive into this.

It's true that a malicious prover could provide non-zero padding and we're not checking these values explicitly.
But if they do so, then the computed LEAF_VALUE will not be verifiable against any GER that is stored in the bridge, so the CLAIM will be rejected.

So the CGI hash will never be updated with a malicious LEAF_VALUE' because the updating of cgi hash chain is conditional on the verification against GER in the first place:

proc verify_leaf_bridge
    # get the leaf value. We have all the necessary leaf data in the advice map
    exec.get_leaf_value
    # => [LEAF_VALUE[8], PROOF_DATA_KEY]

    # duplicate the leaf value to use it later during the CGI chain hash computation
    movupw.2 dupw.2 dupw.2
    # => [LEAF_VALUE[8], PROOF_DATA_KEY, LEAF_VALUE[8]]

    # delegate proof verification
    exec.verify_leaf  <----------- this will fail if the prover provided non-zero padding
    # => [LEAF_VALUE[8]]

    # update the CGI chain hash 
    exec.update_cgi_chain_hash <------------ if the verification against GER fails, we won't get to updating the CGI chain hash
    # => []
end

@Fumuran
Copy link
Copy Markdown
Contributor Author

Fumuran commented May 30, 2026

Indeed! It turned out that we actually return an error in case the padding is not zero, we do this check in the keccak precompile (here, we return the non-zero padding byte 0x1 at byte position 113 error message). So the execution will fail even before we will try to verify the leaf, it will fail during the computation of this leaf. The only advantage of a new code is that we will be able to return more user friendly error message, but I'm not sure that this forth it to create another helper procedure and to further complicate our code for that.

@mmagician
Copy link
Copy Markdown
Collaborator

The only advantage of a new code is that we will be able to return more user friendly error message, but I'm not sure that this forth it to create another helper procedure and to further complicate our code for that.

Agreed, the advantage is not big enough IMO and I'd probably skip the helper, then

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.

pack_leaf_data does not enforce zero padding on bridge-in leaf data

3 participants