Skip to content

test(l2): pin chain_id encoding mismatch in l2_in_message_rolling_hashes public-input commitment#6540

Draft
avilagaston9 wants to merge 1 commit intomainfrom
security/l2-rolling-hash-chain-id-encoding
Draft

test(l2): pin chain_id encoding mismatch in l2_in_message_rolling_hashes public-input commitment#6540
avilagaston9 wants to merge 1 commit intomainfrom
security/l2-rolling-hash-chain-id-encoding

Conversation

@avilagaston9
Copy link
Copy Markdown
Contributor

Motivation

The L1 verifier in
OnChainProposer.sol::_getPublicInputsFromCommitment appends, for
every entry in currentBatch.l2InMessageRollingHashes,

abi.encodePacked(publicInputs, bytes32(rh.chainId), rh.rollingHash)

64 bytes per entry: 32 bytes for the chain id (uint256 in
the struct, left-padded by the bytes32 cast) plus 32 bytes for the
rolling hash.

The guest-side ProgramOutput::encode in
crates/guest-program/src/l2/output.rs, used as the SP1 / RISC0
public commitment via commit_slice(&output.encode()), instead
appends

for (chain_id, hash) in &self.l2_in_message_rolling_hashes {
    encoded.extend_from_slice(&chain_id.to_be_bytes());
    encoded.extend_from_slice(&hash.to_fixed_bytes());
}

where chain_id is typed as u64 (Vec<(u64, H256)>), so
u64::to_be_bytes() returns 8 bytes, not 32 — yielding 40 bytes
per entry
.

As soon as a batch contains any L2-in privileged transactions —
i.e. any L2-to-L2 messaging is exercised — the prover commits to a
public input that is shorter than the one the L1 reconstructs, the
two sha256 hashes diverge, and the on-chain proof verification
fails. The committer happily produces commitments and the prover
happily produces proofs; verification just always reverts.

The other rolling-hash field (l1_in_messages_rolling_hash)
doesn't have this issue because it is a single bytes32. The
BalanceDiff::chain_id and top-level chain_id fields encode
correctly because both are U256 and round-trip via
to_big_endian() as 32 bytes. The bug is local to the
(u64, H256) typing of l2_in_message_rolling_hashes in
ProgramOutput.

Description

Adds a unit test
l2_in_message_rolling_hashes_chain_id_is_only_8_bytes_not_32
in crates/guest-program/src/l2/output.rs that builds a
ProgramOutput with everything zeroed except a single
l2_in_message_rolling_hashes entry, isolates the per-entry bytes
past the 256-byte fixed prefix, and asserts:

  • encoded.len() == PREFIX_LEN + 40 (the buggy 8+32 layout).
  • The chain_id bytes at [PREFIX_LEN..PREFIX_LEN+8] are exactly
    chain_id.to_be_bytes() — confirming it's the bare u64 with
    no padding.
  • The rolling hash starts immediately at PREFIX_LEN+8, not at
    PREFIX_LEN+32.
  • The full per-entry slice does NOT match what L1's
    abi.encodePacked(bytes32(chainId), rollingHash) would produce.

The fix is intentionally not in this PR. Two reasonable shapes:

  1. Change l2_in_message_rolling_hashes to Vec<(U256, H256)> and
    encode with chain_id.to_big_endian(), matching the pattern
    already used for BalanceDiff::chain_id and the top-level
    chain_id.
  2. Keep the type but pad on encode: write 24 zero bytes followed by
    chain_id.to_be_bytes() so each entry's wire format is 32+32.

Either makes the per-entry contribution 64 bytes, matching L1.
Once landed, the assert_ne! and length assertions in the test
flip direction, prompting an explicit review of the wire-format
change.

Reproduction

cargo test -p ethrex-guest-program \
  l2_in_message_rolling_hashes_chain_id_is_only_8_bytes_not_32

Passes on main (the bug is real and reachable).

Impact

L2-to-L2 cross-chain messaging is broken at the proof-verification
layer: any batch that includes one or more L2-in privileged
transactions cannot be verified on L1. The committer commits, the
prover proves, the verifier rejects — the L2 stalls. Bridges that
rely on L2↔L2 settlement can't finalize.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync.

…Output.

The L1 verifier in `OnChainProposer.sol::_getPublicInputsFromCommitment`
appends, for every entry in `currentBatch.l2InMessageRollingHashes`,

    abi.encodePacked(publicInputs, bytes32(rh.chainId), rh.rollingHash)

— 32 bytes for the chain id (uint256 in the struct, left-padded by the
bytes32 cast) plus 32 bytes for the rolling hash, for **64 bytes per
entry**.

The guest-side `ProgramOutput::encode` (used as the SP1/RISC0 public
commitment via `commit_slice(&output.encode())`) instead appends

    encoded.extend_from_slice(&chain_id.to_be_bytes());
    encoded.extend_from_slice(&hash.to_fixed_bytes());

where `chain_id` is typed as `u64` (`Vec<(u64, H256)>`), so
`u64::to_be_bytes()` returns 8 bytes, not 32. That comes out to **40
bytes per entry**.

As soon as a batch contains any L2-in privileged transactions — i.e.
any L2-to-L2 messaging is exercised — the prover commits to a public
input that is shorter than the one the L1 reconstructs, the two
sha256 hashes diverge, and the proof fails verification on chain.
The other rolling-hash field (`l1_in_messages_rolling_hash`) doesn't
have this issue because it is a single bytes32, and the
`BalanceDiff::chain_id` and top-level `chain_id` fields encode
correctly because both are `U256` and round-trip via
`to_big_endian()` as 32 bytes — the bug is local to the `(u64, H256)`
typing of `l2_in_message_rolling_hashes`.

The new test `l2_in_message_rolling_hashes_chain_id_is_only_8_bytes_not_32`
in `crates/guest-program/src/l2/output.rs` builds an output with one
rolling-hash entry, isolates it past the 256-byte fixed prefix, and
asserts the per-entry contribution is 40 bytes, that the chain_id is
the bare big-endian u64 with no padding, and that the resulting
layout differs from what L1's `abi.encodePacked(bytes32(chainId),
rollingHash)` produces. After the fix — either widening the field to
`Vec<(U256, H256)>` and using `to_big_endian()` like the other
fields, or padding to 32 bytes on encode — the assertions flip
direction, prompting an explicit review of the wire-format change.
@github-actions
Copy link
Copy Markdown

Lines of code report

Total lines added: 52
Total lines removed: 0
Total lines changed: 52

Detailed view
+----------------------------------------------+-------+------+
| File                                         | Lines | Diff |
+----------------------------------------------+-------+------+
| ethrex/crates/guest-program/src/l2/output.rs | 104   | +52  |
+----------------------------------------------+-------+------+

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L2 Rollup client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant