Skip to content

fix(zk): propagate range check to batch_5 + add amount/receiver checks#927

Merged
tcsenpai merged 3 commits into
stabilisationfrom
fix/l2ps-zk-batch5-range-check
Jun 8, 2026
Merged

fix(zk): propagate range check to batch_5 + add amount/receiver checks#927
tcsenpai merged 3 commits into
stabilisationfrom
fix/l2ps-zk-batch5-range-check

Conversation

@Shitikyan

@Shitikyan Shitikyan commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Two ZK soundness fixes on the L2PS batch circuits, surfaced by the PATH-OS L2PS hardening report.

1. Dead constraint on batch_5. The only balance guard on BalanceTransfer in l2ps_batch_5.circom was check <== sender_after * sender_after, which binds nothing. Since sender_after === sender_before - amount is BN254 field subtraction, an overdraw wraps sender_after to a value about the size of the curve order and the proof still accepts. batch_10 already has the proper guard — Num2Bits(64) on sender_after, with an explicit "SECURITY FIX — range check instead of squaring" comment — but the fix was never propagated back to batch_5.

2. Gap on both circuits. Only sender_after was range-checked. An overflow on receiver_after or an oversized amount can also wrap the field and satisfy the proof. Range-check both on both circuits — 64 bits is the documented user-balance limit.

Impact

If anything ever trusts a batch-5 or batch-10 proof on its own (light clients, cross-node aggregation, future bridges), a crafted overdraw / over-receive / oversized-amount transaction passes verification. The direct executor path re-validates balances against L1 state, so defence-in-depth on the happy path — this closes a real soundness hole the moment trust assumptions widen.

⚠️ BLOCKER FOR MERGE — ZK ceremony required

The circom source change is fast, but the proving/verifying keys must be regenerated. Do not merge until the team has rerun the trusted setup and committed the new verification_key artefacts.

Test plan

  • Source-level diff: both circuits now declare Num2Bits(64) range checks on sender_after, receiver_after, and amount. batch_5 also gets the missing include \"bitify.circom\".
  • Trusted setup rerun + new verification keys committed
  • PATH-OS field-arithmetic tombstone (overdraw / oversized amount / receiver overflow / valid transfer) reproduced against the new artefacts

Linked

  • Linear: DEM-756
  • Source: PATH-OS L2PS hardening report, item 5

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security

    • Strengthened zero-knowledge circuit validation with enhanced range checks to prevent overflow vulnerabilities in balance transfer operations across transaction batches.
    • Added protective constraints to ensure all transaction amounts and account states remain within expected bounds during processing.
  • Updates

    • Updated verification key artifacts for batch transaction processing to align with enhanced circuit security requirements.

…s (DEM-756)

The L2PS batch-5 circuit had a dead-constraint balance guard —
`check <== sender_after * sender_after` — that binds nothing. Because
`sender_after === sender_before - amount` is BN254 field subtraction,
an overdraw wraps `sender_after` to a value about the size of the
curve order and the proof still accepts. The same circuit at batch-10
already had the proper `Num2Bits(64)` range check on `sender_after`
with an explicit "SECURITY FIX — range check instead of squaring"
comment; the fix was never propagated back to batch-5.

Plus a related gap on BOTH circuits, surfaced by the PATH-OS L2PS
hardening report: only `sender_after` was range-checked. An overflow
on the receiver side (`receiver_after`) or an oversized `amount` can
also wrap the BN254 field and satisfy the proof.

This change:

- Replaces the dead constraint on batch_5 with the same `Num2Bits(64)`
  range check batch_10 already has on `sender_after`.
- Adds matching range checks on `amount` and `receiver_after` on BOTH
  circuits. 64 bits is the documented user-balance limit.
- Adds `include "bitify.circom"` to batch_5 (batch_10 already had it).

Impact: if anything ever trusts a batch-5 or batch-10 proof on its own
(light clients, cross-node aggregation, future bridges), a crafted
overdraw / over-receive / oversized-amount transaction can no longer
pass verification. The direct executor path already re-validates
balances against L1 state, so defence-in-depth on the happy path —
but this closes a real soundness hole the moment trust assumptions
widen.

BLOCKER FOR MERGE: regenerating the proving/verifying keys requires a
ZK ceremony. This PR ships the circom source change only. Coordination
on the ceremony (rerun trusted setup + commit new `verification_key`)
must happen before merge.

Source: PATH-OS L2PS hardening report, item 5 (severity: yours to
set — defence-in-depth on direct path, critical if anything trusts
the batch proof alone).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0663e1d2-34c5-4d57-9edd-544c3e9bb85e

📥 Commits

Reviewing files that changed from the base of the PR and between c3067fa and c4be612.

📒 Files selected for processing (5)
  • src/features/zk/keys/verification_key_merkle.json
  • src/libs/l2ps/zk/circuits/l2ps_batch_10.circom
  • src/libs/l2ps/zk/circuits/l2ps_batch_5.circom
  • src/libs/l2ps/zk/keys/batch_10/verification_key.json
  • src/libs/l2ps/zk/keys/batch_5/verification_key.json

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

The PR hardens two L2PS batch circuits (batch_5 and batch_10) against field-wrapping exploits in the BalanceTransfer template by introducing 64-bit range checks on pre-state, post-state, and transfer amount signals. Corresponding PLONK verification keys are generated and the parent merkle verification key is updated.

Changes

ZK Circuit Hardening and Key Regeneration

Layer / File(s) Summary
Batch_5 circuit hardening
src/libs/l2ps/zk/circuits/l2ps_batch_5.circom
Added bitify.circom include for bit decomposition; instantiated five Num2Bits(64) range-check components wired to sender/receiver state and transfer amount, replacing the prior quadratic self-check.
Batch_10 circuit hardening
src/libs/l2ps/zk/circuits/l2ps_batch_10.circom
Instantiated five Num2Bits(64) range-check components wired to sender/receiver pre/post state and transfer amount signals, replacing the previous single sender_after range check.
PLONK verification keys and parent merkle update
src/libs/l2ps/zk/keys/batch_5/verification_key.json, src/libs/l2ps/zk/keys/batch_10/verification_key.json, src/features/zk/keys/verification_key_merkle.json
Added new PLONK verification keys for batch_5 and batch_10 on bn128 curve containing polynomial coefficients, selectors, and commitment parameters; updated parent merkle verification key vk_delta_2 component.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

🐰 A rabbit bounds through circuits tight,
With range checks now to set things right!
Field wraps are caught by sixty-four-bit guards,
And keys regenerate (no more hard cards!)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/l2ps-zk-batch5-range-check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes two ZK soundness gaps in the L2PS batch circuits: the dead sender_after * sender_after no-op in batch_5 is replaced with proper Num2Bits(64) range checks, and both circuits now guard all five magnitudes against BN254 field wraps. New PLONK verification keys for both circuits are committed.

  • l2ps_batch_5.circom: adds include "bitify.circom", removes the no-op squaring constraint, and introduces five 64-bit range checks that correctly rule out overdraw, receiver overflow, and forged pre-state balances.
  • l2ps_batch_10.circom: extends the existing sender_after guard to cover four previously unchecked magnitudes, bringing the constraint set into parity with the fixed batch_5.
  • verification_key_merkle.json: only vk_delta_2 changed in this Groth16 key for the unmodified Merkle circuit — no corresponding proving key update is present, which may silently break Merkle proof verification.

Confidence Score: 4/5

The batch circuit constraint changes are correct, but the partial Merkle verification key update on an unmodified circuit could silently break Merkle proof verification in production.

The circom fixes are logically sound and the new PLONK keys are properly committed. The unexplained partial update to verification_key_merkle.json is the main concern — only vk_delta_2 changed in a Groth16 key for a circuit this PR never touches, and no proving key update accompanies it.

src/features/zk/keys/verification_key_merkle.json — the partial Groth16 key change on an unmodified circuit needs explanation and the corresponding proving key update included if applicable.

Important Files Changed

Filename Overview
src/libs/l2ps/zk/circuits/l2ps_batch_5.circom Adds bitify include and five Num2Bits(64) range checks; removes dead squaring no-op. Constraint system is sound.
src/libs/l2ps/zk/circuits/l2ps_batch_10.circom Extends existing sender_after range check with four additional guards. Logic is correct and symmetric with batch_5.
src/features/zk/keys/verification_key_merkle.json Only vk_delta_2 changed in this Groth16 key for the unmodified Merkle circuit — no proving key update present, risking broken Merkle proof verification.
src/libs/l2ps/zk/keys/batch_5/verification_key.json New PLONK verification key for updated batch_5 circuit, power 14. Well-formed.
src/libs/l2ps/zk/keys/batch_10/verification_key.json New PLONK verification key for updated batch_10 circuit, power 15. Well-formed.

Reviews (3): Last reviewed commit: "new verification keys" | Re-trigger Greptile

Comment thread src/libs/l2ps/zk/circuits/l2ps_batch_5.circom Outdated
Shitikyan and others added 2 commits June 5, 2026 13:58
Greptile P1 on PR #927: the range-check pass only covered `sender_after`,
`receiver_after`, and `amount`. Both pre-state inputs — `sender_before`
and `receiver_before` — remained unconstrained, which left two
field-arithmetic gaps even with the post-state checks in place:

1. `receiver_before = receiver_after - amount (mod p)` — if
   `receiver_after < amount` as integers, this wraps to a value near
   the BN254 order (~2^254). The prover can craft a proof where the
   receiver "started at −X" and still pass the post-state 64-bit
   bound on `receiver_after`.

2. `sender_before = sender_after + amount` — with both terms bounded
   to 64 bits, the integer sum is in `[0, 2^65)`, which exceeds the
   documented user-balance limit even though every individual signal
   passes its own check.

Bind both pre-state inputs through `Num2Bits(64)` on both circuits.
With all five magnitudes (`sender_before`, `receiver_before`, `amount`,
`sender_after`, `receiver_after`) bounded to 64 bits and the equality
constraints in the middle, field arithmetic collapses to integer
arithmetic and a malicious prover can no longer satisfy any of the
exploit shapes Greptile described.

Constraint order is now: pre-state range checks → equality constraints
→ post-state range checks. Order is informational only (circom enforces
all constraints simultaneously) but reads cleanly top-down.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tcsenpai tcsenpai merged commit b3d18ea into stabilisation Jun 8, 2026
2 checks passed
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