Reject malformed witness lengths instead of panicking - #503
Open
gianlucamazza wants to merge 1 commit into
Open
Conversation
`RecursiveSNARK` derives `Deserialize` with no validation of its internal
vector lengths, so a verifier that accepts a proof from an untrusted peer —
which is what a succinct proof is for — can be handed any shape. `verify`'s
pre-checks cover `i`, `z0` and the instances' `X` lengths, and the output-hash
check covers neither `W` nor `E`, so a proof with one witness element added or
removed reaches the satisfiability check and aborts the process:
src/r1cs/mod.rs:453
assertion `left == right` failed
left: 9811
right: 9810
Both `is_sat_relaxed` and `is_sat` already return `Result<(), NovaError>`, and
`NovaError` already carries `InvalidWitnessLength` and `InvalidInputLength`, so
this needs no signature changes.
Two of the checks are defence in depth: `multiply_vec` one call deeper already
returns `InvalidWitnessLength` when `z` does not match the shape, so a bad
`W.W` or `U.X` is caught either way — the assert merely fires first, and as a
panic. The `E` check is load-bearing: `E` is not part of `z`, and the
satisfiability loop indexes `W.E[i]` over `0..num_cons`.
The remaining `assert_eq!(Az.len(), self.num_cons)` and friends are invariants
of `multiply_vec` given the checks above, not properties of caller input, and
are left alone.
Adds `test_ivc_malformed_witness_is_rejected` over Pallas/Vesta and
Bn256/Grumpkin, covering the three reachable shapes: one element too many in
the primary running witness, one too few in the primary error vector, and the
secondary incoming witness, which goes through `is_sat` rather than
`is_sat_relaxed`.
Towards microsoft#399.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens proof verification against malformed witness vector lengths arriving via Deserialize, replacing verifier-reachable assert_eq! panics with explicit Result-based validation so untrusted proofs are rejected instead of crashing the process.
Changes:
- Replace witness/input length
assert_eq!s inR1CSShape::{is_sat_relaxed,is_sat}with earlyErr(NovaError::InvalidWitnessLength|InvalidInputLength)returns. - Add regression tests ensuring malformed
RecursiveSNARKwitness shapes are rejected (not panicked) across multiple curve pairings.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/r1cs/mod.rs | Converts verifier-reachable length asserts into explicit error returns for malformed witness / instance input lengths. |
| src/nova/mod.rs | Adds tests that mutate witness vectors to confirm verify returns InvalidWitnessLength instead of aborting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
srinathsetty
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Towards #399, for the case that is reachable from untrusted input.
RecursiveSNARKderivesDeserializewith no validation of its internalvector lengths, and
verifyreachesassert_eq!insideis_sat_relaxed/is_satrather than returningErr:verify's existing pre-checks coveri,z0and theXlengths of theinstances, and the output-hash check does not cover
WorE, so a proofwith one witness element added or removed passes everything and then aborts
the process. For a verifier that accepts proofs from an untrusted peer — which
is what a succinct proof is for — that is a remote crash rather than a rejected
proof, and
catch_unwindis the only thing a downstream user can do about it(and it does nothing under
panic = "abort").The change
Both functions already return
Result<(), NovaError>, andNovaErroralreadycarries
InvalidWitnessLengthandInvalidInputLength. No signature changes.Two of the five checks are strictly defence in depth:
multiply_vecone calldeeper already returns
NovaError::InvalidWitnessLengthwhenz.len() != num_io + num_vars + 1, so a badW.WorU.Xis caught eitherway — the assert merely fires first, and as a panic. Making them explicit keeps
the error precise and local.
The
W.Echeck is load-bearing.Eis not part ofz, so nothing downstreamvalidates it: the satisfiability loop indexes
W.E[i]over0..num_cons, anda short
Eis an out-of-bounds panic. A long one reachesassert!(ck.ck.len() >= v.len())inpedersen::commit.The remaining
assert_eq!(Az.len(), self.num_cons)and friends are invariantsof
multiply_vecgiven the checks above, not properties of caller input, sothey are left alone.
Test
test_ivc_malformed_witness_is_rejected, over Pallas/Vesta and Bn256/Grumpkin,covers the three reachable shapes: one element too many in the primary running
witness, one too few in the primary error vector, and the secondary incoming
witness (which goes through
is_satrather thanis_sat_relaxed). Againstmainit aborts the test process atsrc/r1cs/mod.rs:453(
left: 9811, right: 9810); with this change each case returnsErr(NovaError::InvalidWitnessLength).cargo test --lib,cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean, with and without--features experimental.Not in this PR
pedersen::commit'sassert!(ck.ck.len() >= v.len())is unreachable fromverifyonce the shape checks above are in place, but it is a panic on apublic API that returns
Commitmentrather thanResult. Fixing it properlymeans changing a widely used signature, which seemed worth keeping separate
from a change this small.
neutron::relation::Structure::is_sathas the same hole and no asserts at all— its tensor-form
Eis split atleftand indexed over0..right, so ashort one is an out-of-bounds index. Filed separately rather than fixed here:
the length check is easy, but the existing
test_satconstructsEat adifferent length than
nifs.rsasserts, so the intended invariant is aquestion for you rather than something I should guess at inside an
experimentalmodule.Found while hardening a downstream verifier that accepts recursive proofs from
mutually untrusting parties.