Description
Is your feature request related to a problem? Please describe.
We need to recursively verify multiple SNARK proofs, where some proofs may be intentionally invalid. For example, when aggregating up to 100 proofs with only 50 valid ones, or when only one among several circuit proofs must be valid, forcing immediate assertions via api.Assert limits flexibility.
Returning a verification flag (1 for valid, 0 for invalid) in intermediate functions would grant circuit developers finer control over which proofs to enforce.
Describe the solution you'd like.
Introduce a new method (e.g. ProofIsValid) in the std/recursion packages that returns a frontend.Variable
flag indicating whether a proof is valid. This method should accumulate a boolean flag (starting at 1 and multiplying by 0 on any failed check) rather than calling assertions directly. The existing AssertProof method can then be implemented as a wrapper that calls ProofIsValid and asserts that the flag equals 1.
Describe alternatives you've considered.
No viable alternatives offer the same level of flexibility. Current approaches force immediate failure with api.Assert, which doesn’t support conditional aggregation of proofs.
Additional context.
This design aligns with practices in other DSLs (such as Circom) and enables the circuit developer to decide later whether to trigger an assertion. It provides a more composable and modular approach in my opinion.
A non-working example implementation of the new Verifier.ProofIsValid() might look like this. This code does not work because the Pairing functions do only support Asserts, we would need to add other methods to receive the true/false flag.
// Legacy version: AssertProof uses ProofIsValid and then asserts that the proof is valid.
func (v *Verifier[FR, G1El, G2El, GtEl]) AssertProof(
vk VerifyingKey[G1El, G2El, GtEl],
proof Proof[G1El, G2El],
witness Witness[FR],
opts ...VerifierOption,
) error {
flag := v.ProofIsValid(vk, proof, witness, opts...)
v.api.AssertIsEqual(flag, 1)
return nil
}
// ProofIsValid returns a frontend.Variable flag equal to 1 if the proof is valid and 0 otherwise.
func (v *Verifier[FR, G1El, G2El, GtEl]) ProofIsValid(
vk VerifyingKey[G1El, G2El, GtEl],
proof Proof[G1El, G2El],
witness Witness[FR],
opts ...VerifierOption,
) frontend.Variable {
// Start with a valid flag of 1.
valid := frontend.Variable(1)
// Check commitment lengths.
if len(vk.CommitmentKeys) != len(proof.Commitments) {
valid = v.api.Mul(valid, 0)
}
if len(vk.CommitmentKeys) != len(vk.PublicAndCommitmentCommitted) {
valid = v.api.Mul(valid, 0)
}
var fr FR
nbPublicVars := len(vk.G1.K) - len(vk.PublicAndCommitmentCommitted)
if len(witness.Public) != nbPublicVars-1 {
valid = v.api.Mul(valid, 0)
}
// Build the input arrays for MSM.
inP := make([]*G1El, len(vk.G1.K)-1) // skip the one-wire (handled later)
for i := range inP {
inP[i] = &vk.G1.K[i+1]
}
inS := make([]*emulated.Element[FR], len(witness.Public)+len(vk.PublicAndCommitmentCommitted))
for i := range witness.Public {
inS[i] = &witness.Public[i]
}
opt, err := newCfg(opts...)
if err != nil {
valid = v.api.Mul(valid, 0)
}
hashToField, err := recursion.NewHash(v.api, fr.Modulus(), true)
if err != nil {
valid = v.api.Mul(valid, 0)
}
// Compute maximum number of public committed elements (not used further here).
maxNbPublicCommitted := 0
for _, s := range vk.PublicAndCommitmentCommitted {
if len(s) > maxNbPublicCommitted {
maxNbPublicCommitted = len(s)
}
}
commitmentAuxData := make([]*emulated.Element[FR], len(vk.PublicAndCommitmentCommitted))
for i := range vk.PublicAndCommitmentCommitted {
hashToField.Write(v.curve.MarshalG1(proof.Commitments[i].G1El)...)
for j := range vk.PublicAndCommitmentCommitted[i] {
hashToField.Write(v.curve.MarshalScalar(*inS[vk.PublicAndCommitmentCommitted[i][j]-1])...)
}
h := hashToField.Sum()
hashToField.Reset()
res := v.scalarApi.FromBits(v.api.ToBinary(h)...)
inS[nbPublicVars-1+i] = res
commitmentAuxData[i] = res
}
switch len(vk.CommitmentKeys) {
case 0:
// No commitment to verify.
case 1:
if err = v.commitment.AssertCommitment(proof.Commitments[0], proof.CommitmentPok, vk.CommitmentKeys[0], opt.pedopt...); err != nil {
valid = v.api.Mul(valid, 0)
}
default:
// Multiple commitments are not supported.
valid = v.api.Mul(valid, 0)
}
kSum, err := v.curve.MultiScalarMul(inP, inS, opt.algopt...)
if err != nil {
valid = v.api.Mul(valid, 0)
}
kSum = v.curve.Add(kSum, &vk.G1.K[0])
for i := range proof.Commitments {
kSum = v.curve.Add(kSum, &proof.Commitments[i].G1El)
}
if opt.forceSubgroupCheck {
valid = v.api.Mul(valid, v.pairing.IsOnG1(&proof.Ar))
valid = v.api.Mul(valid, v.pairing.IsOnG1(&proof.Krs))
valid = v.api.Mul(valid, v.pairing.IsOnG2(&proof.Bs))
}
pairing, err := v.pairing.Pair(
[]*G1El{kSum, &proof.Krs, &proof.Ar},
[]*G2El{&vk.G2.GammaNeg, &vk.G2.DeltaNeg, &proof.Bs},
)
if err != nil {
valid = v.api.Mul(valid, 0)
}
valid = v.api.Mul(valid, v.pairing.IsEqual(pairing, &vk.E))
return valid
}