Skip to content

return a Flag on proof recursive verification, instead of Assert() #1429

Open
@p4u

Description

@p4u

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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions