Skip to content

fix(circuits): encode foreign point identity flag as a separate public input - #15

Merged
Michael-EY merged 9 commits into
mainfrom
id_point_fix
Oct 8, 2026
Merged

Michael-EY merged 9 commits into
mainfrom
id_point_fix

Conversation

@Michael-EY

Copy link
Copy Markdown

Overview

This cherry-picks the matching fix from midnight. Before, a foreign point's is_id flag was added into the first public-input element. Because that element packs several limbs of x, an identity point could produce the same public inputs as a non-identity point. The flag is now its own public-input element after the x and y limbs.

@Michael-EY Michael-EY self-assigned this Oct 1, 2026

@jtcoolen jtcoolen 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.

Review summary

Taste rating: 🟢 Good taste

The fix is correct and token-identical to upstream midnight-zk#398. The first public-input word packs several limbs of x, so adding 2^LOG2_BASE for the flag was the same as adding 1 to limb 1. An identity point, whose x/y are unconstrained, could therefore share public inputs with a non-identity point; a dedicated flag element closes that. The inline comments cover the two places that still assume the old layout.

General notes

q: This branch is stacked on #14 (benchmark_256_trees), so merging it into main also merges the 255-level map change, which still has changes requested. Could you retarget the base to benchmark_256_trees, or rebase onto main so the identity fix can land separately? The fix doesn't depend on the map change.

Risk assessment

⚠️ 🔴 HIGH — This is a soundness-relevant change to the public-input encoding, and it is deliberately layout- and VK-breaking. Every circuit that exposes a foreign point or a recursive accumulator gets one extra public input per point and a new VK. Every decoder of that layout (this repo's Solidity verifier generator, downstream pins) has to move in lockstep. Recommendation: Do not auto-merge. A human reviewer should confirm that the VK regeneration and the Solidity decoder update land with, or before, any downstream pin of this commit.

Verdict: ✅ Worth merging once the five zk_stdlib VKs are regenerated and the Solidity accumulator decoder is updated (or tracked in a linked follow-up).

Comment thread circuits/CHANGELOG.md Outdated
Comment thread circuits/src/ecc/foreign/ecc_chip.rs

@lydiagarms lydiagarms 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.

LGTM

// The circuit constrains the flag as a bit. Anything else is a
// malformed public input rather than a non-identity point.
ok := lt(flag, 2)
is_id := eq(flag, 1)

@jtcoolen jtcoolen Oct 5, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 (addressed): proof-backed identity coverage. The original successful IVC proof carried is_id = 0 for both accumulator points. Flipping a flag only checks rejection of altered public inputs; it does not show that a valid identity point passes this decoder. I added fix commit 7358054 with a valid identity-accumulator proof accepted by the generated EVM verifier, and 61c525e with recursive neutral/all-zero rejection cases. Focused tests and the full IVC-to-EVM test pass. No exploitable decoder flaw was confirmed. This cryptographic path remains high-risk and merits human review; the separate Merkle-map changes were outside this review.

@jtcoolen

jtcoolen commented Oct 7, 2026 •

Copy link
Copy Markdown

cb80706 checks identity encoding and acceptance. The tests.rs:1490 checks the exact 12-word accumulator layout, including each point’s trailing identity flag and scalar. The test.rs:286 then generates real proofs for an identity pair with both canonical coordinates and deliberately out-of-range coordinate bits. Both must verify: when the identity flag is set, those coordinate words have no point meaning, though the proof still binds them.

9a4a07b checks that an accumulator cannot be substituted. In the ivc_keccak_solidity.rs:1439, the neutral identity pair satisfies the accumulator pairing on its own. Replacing the genuine non-neutral public accumulator with it must nevertheless fail because the original proof binds different public inputs. Trying to reprove the unchanged leaf witness against that neutral accumulator must also fail synthesis or verification.

84e7c56 checks where rejection happens. It changes a leaf opening to another valid G1 point, then proves an outer statement whose PLONK proof verifies but whose carried accumulator is invalid. The ivc_keccak_solidity.rs:1349 confirms the Solidity verifier reaches final pairing, receives false, and reverts. A new evm.rs:275 also distinguishes bad encodings rejected before G1MSM, off-curve points rejected by G1MSM, and changed public inputs rejected at final pairing.

@jtcoolen
jtcoolen force-pushed the id_point_fix branch 3 times, most recently from 539fe26 to e5bcfdb Compare October 7, 2026 09:34
Michael Livesey and others added 5 commits October 7, 2026 11:11
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Foreign point public inputs now carry is_id as a dedicated word after the
x and y limbs instead of packing it into x's first limb. Match that layout
in the accumulator decoder:

* count 5 words per accumulator point (12 for the fully collapsed
  accumulator, 10 for the Moonlight point pair);
* read the trailing flag word, require it to be 0 or 1, and never decode
  x/y when it is set, since the circuit leaves them unconstrained;
* drop the packed-flag p - 1 constants and is_acc_encoded_identity.

Add a unit test pinning the word counts to the circuit's as_public_input
lengths, derive the IVC e2e accumulator offsets from the circuit encoding,
and add non-boolean and flipped identity flag negatives there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check that foreign-point identity flags and carried scalars occupy
the expected public-input words. Prove an identity accumulator
with both canonical and unconstrained coordinate words, showing
that the Solidity decoder ignores coordinates when is_id is set.
Neutral satisfies its pairing equation, but unchanged leaves
compute a non-neutral accumulator. Reusing their outer proof with
four neutral or zero-word public-input encodings must be rejected:
flagged identities change the outer KZG transcript, unflagged
identity coordinates decode to forbidden (0,0), and unflagged raw
zeros decode to off-curve (1,1). A neutral claim with unchanged
leaves also cannot yield a valid outer proof because the circuit
binds its public accumulator to the value it computes.
An incorrect but well-formed inner KZG opening is public inner
proof data carried as outer witness. The partial verifier prepares
its pairing equation without checking it; binding the computed
accumulator as outer public input yields a valid outer proof whose
carried pairing is false. Record 0x0c/0x0f precompile calls even on
revert to prove the bad opening reaches final pairing, while the
four altered accumulator inputs stop at their actual validation
stages: final outer KZG pairing, canonical (0,0) decode, or the
first G1MSM rejecting off-curve (1,1). No production verifier
logic changes.
@Michael-EY
Michael-EY merged commit 2d52f24 into main Oct 8, 2026
17 of 20 checks passed
@Michael-EY
Michael-EY deleted the id_point_fix branch October 8, 2026 11:08
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.

3 participants