From 6fcb05c9dc06280a04530e5f078705ffff91ccab Mon Sep 17 00:00:00 2001 From: Guido Iribarren Date: Fri, 24 Apr 2026 13:14:44 -0300 Subject: [PATCH] fix(groth16): replace all asserts in IsValidProof codepath Refactor recursive proof and commitment verification to use boolean helpers plus thin assert wrappers, so subgroup and pairing checks can be composed instead of asserted directly. this fulfills the original intention of commit b53af63 "feat: implement flag based recursive groth16 and EdDSA check" namely to have a verifier that doesn't assert anything and just returns a bool. --- std/algebra/emulated/sw_bls12381/pairing.go | 4 + std/algebra/emulated/sw_bn254/pairing.go | 8 ++ std/algebra/emulated/sw_bw6761/pairing.go | 35 +++++++-- std/algebra/emulated/sw_emulated/point.go | 7 +- std/algebra/interfaces.go | 9 +++ std/algebra/native/sw_bls12377/pairing2.go | 45 ++++++++++-- std/commitments/pedersen/verifier.go | 31 ++++++-- std/recursion/groth16/verifier.go | 21 +++--- std/recursion/groth16/verifier_test.go | 81 +++++++++++++++++++++ 9 files changed, 210 insertions(+), 31 deletions(-) diff --git a/std/algebra/emulated/sw_bls12381/pairing.go b/std/algebra/emulated/sw_bls12381/pairing.go index 12f453b674..661a028f23 100644 --- a/std/algebra/emulated/sw_bls12381/pairing.go +++ b/std/algebra/emulated/sw_bls12381/pairing.go @@ -365,6 +365,10 @@ func (pr *Pairing) IsOnG2(Q *G2Affine) frontend.Variable { return pr.api.And(isOnCurve, isInSubgroup) } +func (pr *Pairing) One() *GTEl { + return pr.Ext12.One() +} + // loopCounter = seed in binary // // seed=-15132376222941642752 diff --git a/std/algebra/emulated/sw_bn254/pairing.go b/std/algebra/emulated/sw_bn254/pairing.go index 7eec342480..db2cbdd2e7 100644 --- a/std/algebra/emulated/sw_bn254/pairing.go +++ b/std/algebra/emulated/sw_bn254/pairing.go @@ -319,6 +319,14 @@ func (pr *Pairing) AssertIsOnCurve(P *G1Affine) { pr.curve.AssertIsOnCurve(P) } +func (pr *Pairing) IsOnG1(P *G1Affine) frontend.Variable { + return pr.curve.IsOnCurve(P) +} + +func (pr *Pairing) One() *GTEl { + return pr.Ext12.One() +} + func (pr *Pairing) MuxG2(sel frontend.Variable, inputs ...*G2Affine) *G2Affine { if len(inputs) == 0 { return nil diff --git a/std/algebra/emulated/sw_bw6761/pairing.go b/std/algebra/emulated/sw_bw6761/pairing.go index 8c17bd7126..8ce7fd7828 100644 --- a/std/algebra/emulated/sw_bw6761/pairing.go +++ b/std/algebra/emulated/sw_bw6761/pairing.go @@ -321,7 +321,15 @@ func (pr *Pairing) AssertIsOnCurve(P *G1Affine) { pr.curve.AssertIsOnCurve(P) } +func (pr *Pairing) IsOnCurve(P *G1Affine) frontend.Variable { + return pr.curve.IsOnCurve(P) +} + func (pr *Pairing) AssertIsOnTwist(Q *G2Affine) { + pr.api.AssertIsEqual(pr.IsOnTwist(Q), 1) +} + +func (pr *Pairing) IsOnTwist(Q *G2Affine) frontend.Variable { // Twist: Y² == X³ + aX + b, where a=0 and b=4 // (X,Y) ∈ {Y² == X³ + aX + b} U (0,0) @@ -334,13 +342,16 @@ func (pr *Pairing) AssertIsOnTwist(Q *G2Affine) { right := pr.curveF.Mul(&Q.P.X, &Q.P.X) right = pr.curveF.Mul(right, &Q.P.X) right = pr.curveF.Add(right, b) - pr.curveF.AssertIsEqual(left, right) + return pr.curveF.IsZero(pr.curveF.Sub(left, right)) } func (pr *Pairing) AssertIsOnG1(P *G1Affine) { - // 1- Check P is on the curve - pr.AssertIsOnCurve(P) + pr.api.AssertIsEqual(pr.IsOnG1(P), 1) +} +func (pr *Pairing) IsOnG1(P *G1Affine) frontend.Variable { + // 1- Check P is on the curve + isOnCurve := pr.IsOnCurve(P) // 2- Check P has the right subgroup order // we check that [x₀+1]P == [-x₀³+x₀²-1]ϕ(P) xP := pr.g1.scalarMulBySeed(P) @@ -353,12 +364,20 @@ func (pr *Pairing) AssertIsOnG1(P *G1Affine) { right = pr.g1.phi(right) // [r]P == 0 <==> [x₀+1]P == [-x₀³+x₀²-1]ϕ(P) - pr.curve.AssertIsEqual(left, right) + isEqual := pr.api.And( + pr.curveF.IsZero(pr.curveF.Sub(&left.X, &right.X)), + pr.curveF.IsZero(pr.curveF.Sub(&left.Y, &right.Y)), + ) + return pr.api.And(isOnCurve, isEqual) } func (pr *Pairing) AssertIsOnG2(Q *G2Affine) { + pr.api.AssertIsEqual(pr.IsOnG2(Q), 1) +} + +func (pr *Pairing) IsOnG2(Q *G2Affine) frontend.Variable { // 1- Check Q is on the curve - pr.AssertIsOnTwist(Q) + isOnCurve := pr.IsOnTwist(Q) // 2- Check Q has the right subgroup order // we check that [x₀+1]Q == [-x₀³+x₀²-1]ϕ(Q) @@ -372,7 +391,11 @@ func (pr *Pairing) AssertIsOnG2(Q *G2Affine) { right = pr.g2.phi(right) // [r]Q == 0 <==> [x₀+1]Q == [-x₀³+x₀²-1]ϕ(Q) - pr.g2.AssertIsEqual(left, right) + isEqual := pr.api.And( + pr.curveF.IsZero(pr.curveF.Sub(&left.P.X, &right.P.X)), + pr.curveF.IsZero(pr.curveF.Sub(&left.P.Y, &right.P.Y)), + ) + return pr.api.And(isOnCurve, isEqual) } // seed x₀=9586122913090633729 diff --git a/std/algebra/emulated/sw_emulated/point.go b/std/algebra/emulated/sw_emulated/point.go index e66823728b..f6387781a0 100644 --- a/std/algebra/emulated/sw_emulated/point.go +++ b/std/algebra/emulated/sw_emulated/point.go @@ -207,6 +207,11 @@ func (c *Curve[B, S]) add(p, q *AffinePoint[B]) *AffinePoint[B] { // AssertIsOnCurve asserts if p belongs to the curve. It doesn't modify p. func (c *Curve[B, S]) AssertIsOnCurve(p *AffinePoint[B]) { + c.api.AssertIsEqual(c.IsOnCurve(p), 1) +} + +// IsOnCurve returns a boolean indicating if p belongs to the curve. +func (c *Curve[B, S]) IsOnCurve(p *AffinePoint[B]) frontend.Variable { // (X,Y) ∈ {Y² == X³ + aX + b} U (0,0) // if p=(0,0) we assign b=0 and continue @@ -219,7 +224,7 @@ func (c *Curve[B, S]) AssertIsOnCurve(p *AffinePoint[B]) { } else { check = c.baseApi.Eval([][]*emulated.Element[B]{{&p.X, &p.X, &p.X}, {&c.a, &p.X}, {b}, {&p.Y, &p.Y}}, []int{1, 1, 1, -1}) } - c.baseApi.AssertIsEqual(check, c.baseApi.Zero()) + return c.baseApi.IsZero(check) } // AddUnified adds p and q and returns it. It doesn't modify p nor q. diff --git a/std/algebra/interfaces.go b/std/algebra/interfaces.go index 2dba477b08..f7c2224061 100644 --- a/std/algebra/interfaces.go +++ b/std/algebra/interfaces.go @@ -99,6 +99,9 @@ type Pairing[G1El G1ElementT, G2El G2ElementT, GtEl GtElementT] interface { // when the inputs are of mismatching length. It does not modify the inputs. PairingCheck([]*G1El, []*G2El) error + // One returns the identity element of the target group. + One() *GtEl + // AssertIsEqual asserts the equality of the inputs. AssertIsEqual(*GtEl, *GtEl) @@ -108,6 +111,12 @@ type Pairing[G1El G1ElementT, G2El G2ElementT, GtEl GtElementT] interface { // AssertIsOnG2 asserts that the input is on the G2 curve. AssertIsOnG2(*G2El) + // IsOnG1 returns a boolean indicating whether the input is on G1. + IsOnG1(*G1El) frontend.Variable + + // IsOnG2 returns a boolean indicating whether the input is on G2. + IsOnG2(*G2El) frontend.Variable + // MuxG2 performs a lookup from the G2 inputs and returns inputs[sel]. It is // most efficient for power of two lengths of the inputs, but works for any // number of inputs. diff --git a/std/algebra/native/sw_bls12377/pairing2.go b/std/algebra/native/sw_bls12377/pairing2.go index 09451a117f..7bc69b4b30 100644 --- a/std/algebra/native/sw_bls12377/pairing2.go +++ b/std/algebra/native/sw_bls12377/pairing2.go @@ -343,6 +343,12 @@ func (pr *Pairing) PairingCheck(P []*G1Affine, Q []*G2Affine) error { return nil } +func (pr *Pairing) One() *GT { + var res GT + res.SetOne() + return &res +} + // AssertIsEqual asserts the equality of the target group elements. func (pr *Pairing) AssertIsEqual(e1, e2 *GT) { e1.AssertIsEqual(pr.api, *e2) @@ -469,6 +475,11 @@ func (pr *Pairing) MuxGt(sel frontend.Variable, inputs ...*GT) *GT { // AssertIsOnCurve asserts if p belongs to the curve. It doesn't modify p. func (pr *Pairing) AssertIsOnCurve(p *G1Affine) { + pr.api.AssertIsEqual(pr.IsOnCurve(p), 1) +} + +// IsOnCurve returns a boolean indicating if p belongs to the curve. +func (pr *Pairing) IsOnCurve(p *G1Affine) frontend.Variable { // (X,Y) ∈ {Y² == X³ + 1} U (0,0) // if p=(0,0) we assign b=0 and continue @@ -478,12 +489,16 @@ func (pr *Pairing) AssertIsOnCurve(p *G1Affine) { left := pr.api.Mul(p.Y, p.Y) right := pr.api.Mul(p.X, pr.api.Mul(p.X, p.X)) right = pr.api.Add(right, b) - pr.api.AssertIsEqual(left, right) + return pr.api.IsZero(pr.api.Sub(left, right)) } func (pr *Pairing) AssertIsOnG1(P *G1Affine) { + pr.api.AssertIsEqual(pr.IsOnG1(P), 1) +} + +func (pr *Pairing) IsOnG1(P *G1Affine) frontend.Variable { // 1- Check P is on the curve - pr.AssertIsOnCurve(P) + isOnCurve := pr.IsOnCurve(P) // 2- Check P has the right subgroup order // [x²]ϕ(P) @@ -497,11 +512,19 @@ func (pr *Pairing) AssertIsOnG1(P *G1Affine) { _P.Neg(pr.api, _P) // [r]Q == 0 <==> P = -[x²]ϕ(P) - P.AssertIsEqual(pr.api, _P) + isEqual := pr.api.And( + pr.api.IsZero(pr.api.Sub(P.X, _P.X)), + pr.api.IsZero(pr.api.Sub(P.Y, _P.Y)), + ) + return pr.api.And(isOnCurve, isEqual) } // AssertIsOnTwist asserts if p belongs to the curve. It doesn't modify p. func (pr *Pairing) AssertIsOnTwist(p *G2Affine) { + pr.api.AssertIsEqual(pr.IsOnTwist(p), 1) +} + +func (pr *Pairing) IsOnTwist(p *G2Affine) frontend.Variable { // (X,Y) ∈ {Y² == X³ + 1/u} U (0,0) // if p=(0,0) we assign b=0 and continue @@ -519,12 +542,18 @@ func (pr *Pairing) AssertIsOnTwist(p *G2Affine) { right.Square(pr.api, p.P.X) right.Mul(pr.api, right, p.P.X) right.Add(pr.api, right, b) - left.AssertIsEqual(pr.api, right) + var diff fields_bls12377.E2 + diff.Sub(pr.api, left, right) + return diff.IsZero(pr.api) } func (pr *Pairing) AssertIsOnG2(P *G2Affine) { + pr.api.AssertIsEqual(pr.IsOnG2(P), 1) +} + +func (pr *Pairing) IsOnG2(P *G2Affine) frontend.Variable { // 1- Check P is on the curve - pr.AssertIsOnTwist(P) + isOnTwist := pr.IsOnTwist(P) // 2- Check P has the right subgroup order // [x₀]Q @@ -534,7 +563,11 @@ func (pr *Pairing) AssertIsOnG2(P *G2Affine) { psiP.psi(pr.api, &P.P) // [r]Q == 0 <==> ψ(Q) == [x₀]Q - xP.AssertIsEqual(pr.api, psiP) + var diffX, diffY fields_bls12377.E2 + diffX.Sub(pr.api, xP.X, psiP.X) + diffY.Sub(pr.api, xP.Y, psiP.Y) + isEqual := pr.api.And(diffX.IsZero(pr.api), diffY.IsZero(pr.api)) + return pr.api.And(isOnTwist, isEqual) } // NewG1Affine allocates a witness from the native G1 element and returns it. diff --git a/std/commitments/pedersen/verifier.go b/std/commitments/pedersen/verifier.go index 77cd596be6..35b5c53ed2 100644 --- a/std/commitments/pedersen/verifier.go +++ b/std/commitments/pedersen/verifier.go @@ -27,6 +27,7 @@ type VerifyingKey[G2El algebra.G2ElementT] struct { // Verifier verifies the knowledge proofs for a Pedersen commitments type Verifier[FR emulated.FieldParams, G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl algebra.GtElementT] struct { + api frontend.API pairing algebra.Pairing[G1El, G2El, GtEl] } @@ -36,7 +37,7 @@ func NewVerifier[FR emulated.FieldParams, G1El algebra.G1ElementT, G2El algebra. if err != nil { return nil, fmt.Errorf("get pairing: %w", err) } - return &Verifier[FR, G1El, G2El, GtEl]{pairing: pairing}, nil + return &Verifier[FR, G1El, G2El, GtEl]{api: api, pairing: pairing}, nil } // FoldCommitments folds the given commitments into a single commitment for efficient verification. @@ -54,19 +55,35 @@ func (v *Verifier[FR, G1El, G2El, GtEl]) FoldCommitments(commitments []Commitmen // AssertCommitment verifies the given commitment and knowledge proof against the given verifying key. func (v *Verifier[FR, G1El, G2El, GtEl]) AssertCommitment(commitment Commitment[G1El], knowledgeProof KnowledgeProof[G1El], vk VerifyingKey[G2El], opts ...VerifierOption) error { + isValid, err := v.IsCommitmentValid(commitment, knowledgeProof, vk, opts...) + if err != nil { + return err + } + v.api.AssertIsEqual(isValid, 1) + return nil +} + +// IsCommitmentValid returns a variable that is 1 if the commitment and +// knowledge proof are valid and 0 otherwise. +func (v *Verifier[FR, G1El, G2El, GtEl]) IsCommitmentValid(commitment Commitment[G1El], knowledgeProof KnowledgeProof[G1El], vk VerifyingKey[G2El], opts ...VerifierOption) (frontend.Variable, error) { cfg, err := newCfg(opts...) if err != nil { - return fmt.Errorf("apply options: %w", err) + return 0, fmt.Errorf("apply options: %w", err) } + + isValid := frontend.Variable(1) if cfg.subgroupCheck { - v.pairing.AssertIsOnG1(&commitment.G1El) - v.pairing.AssertIsOnG1(&knowledgeProof.G1El) + isValid = v.api.Mul(isValid, v.pairing.IsOnG1(&commitment.G1El)) + isValid = v.api.Mul(isValid, v.pairing.IsOnG1(&knowledgeProof.G1El)) } - if err = v.pairing.PairingCheck([]*G1El{&commitment.G1El, &knowledgeProof.G1El}, []*G2El{&vk.GSigmaNeg, &vk.G}); err != nil { - return fmt.Errorf("pairing check failed: %w", err) + res, err := v.pairing.Pair([]*G1El{&commitment.G1El, &knowledgeProof.G1El}, []*G2El{&vk.GSigmaNeg, &vk.G}) + if err != nil { + return 0, fmt.Errorf("pairing: %w", err) } - return nil + + isValid = v.api.Mul(isValid, v.pairing.IsEqual(res, v.pairing.One())) + return isValid, nil } // TODO: add asserting with switches between different keys diff --git a/std/recursion/groth16/verifier.go b/std/recursion/groth16/verifier.go index 13fb60c218..bf1f173079 100644 --- a/std/recursion/groth16/verifier.go +++ b/std/recursion/groth16/verifier.go @@ -559,11 +559,6 @@ func (v *Verifier[FR, G1El, G2El, GtEl]) IsValidProof(vk VerifyingKey[G1El, G2El return 0, fmt.Errorf("hash to field: %w", err) } - maxNbPublicCommitted := 0 - for _, s := range vk.PublicAndCommitmentCommitted { // iterate over commitments - maxNbPublicCommitted = max(maxNbPublicCommitted, len(s)) - } - commitmentAuxData := make([]*emulated.Element[FR], len(vk.PublicAndCommitmentCommitted)) for i := range vk.PublicAndCommitmentCommitted { // solveCommitmentWire hashToField.Write(v.curve.MarshalG1(proof.Commitments[i].G1El)...) @@ -580,13 +575,16 @@ func (v *Verifier[FR, G1El, G2El, GtEl]) IsValidProof(vk VerifyingKey[G1El, G2El commitmentAuxData[i] = res } + isValid := frontend.Variable(1) switch len(vk.CommitmentKeys) { case 0: // explicitly do not verify the commitment as there is nothing case 1: - if err = v.commitment.AssertCommitment(proof.Commitments[0], proof.CommitmentPok, vk.CommitmentKeys[0], opt.pedopt...); err != nil { - return 0, fmt.Errorf("assert commitment: %w", err) + commitmentValid, err := v.commitment.IsCommitmentValid(proof.Commitments[0], proof.CommitmentPok, vk.CommitmentKeys[0], opt.pedopt...) + if err != nil { + return 0, fmt.Errorf("check commitment: %w", err) } + isValid = v.api.Mul(isValid, commitmentValid) default: // TODO: we support only a single commitment in the recursion for now return 0, fmt.Errorf("multiple commitments are not supported") @@ -603,15 +601,16 @@ func (v *Verifier[FR, G1El, G2El, GtEl]) IsValidProof(vk VerifyingKey[G1El, G2El } if opt.forceSubgroupCheck { - v.pairing.AssertIsOnG1(&proof.Ar) - v.pairing.AssertIsOnG1(&proof.Krs) - v.pairing.AssertIsOnG2(&proof.Bs) + isValid = v.api.Mul(isValid, v.pairing.IsOnG1(&proof.Ar)) + isValid = v.api.Mul(isValid, v.pairing.IsOnG1(&proof.Krs)) + isValid = v.api.Mul(isValid, 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 { return 0, fmt.Errorf("pairing: %w", err) } - return v.pairing.IsEqual(pairing, &vk.E), nil + isValid = v.api.Mul(isValid, v.pairing.IsEqual(pairing, &vk.E)) + return isValid, nil } // SwitchVerification key switches the verification key based on the provided diff --git a/std/recursion/groth16/verifier_test.go b/std/recursion/groth16/verifier_test.go index 1ec5a76229..490ec9b572 100644 --- a/std/recursion/groth16/verifier_test.go +++ b/std/recursion/groth16/verifier_test.go @@ -342,6 +342,7 @@ func getInnerCommitment(assert *test.Assert, field, outer *big.Int) (constraint. assert.NoError(err) return innerCcs, innerVK, innerPubWitness, innerProof } + func TestBN254InBN254Commitment(t *testing.T) { assert := test.NewAssert(t) @@ -672,3 +673,83 @@ func TestBLS12InBW6InvalidProof(t *testing.T) { assert.NoError(err) }, "invalid=witness") } + +type OuterCircuitIsValidWithSubgroupCheck[FR emulated.FieldParams, G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl algebra.GtElementT] struct { + Proof Proof[G1El, G2El] + VerifyingKey VerifyingKey[G1El, G2El, GtEl] + InnerWitness Witness[FR] + Res frontend.Variable `gnark:",public"` +} + +func (c *OuterCircuitIsValidWithSubgroupCheck[FR, G1El, G2El, GtEl]) Define(api frontend.API) error { + verifier, err := NewVerifier[FR, G1El, G2El, GtEl](api) + if err != nil { + return fmt.Errorf("new verifier: %w", err) + } + res, err := verifier.IsValidProof(c.VerifyingKey, c.Proof, c.InnerWitness, WithSubgroupCheck()) + if err != nil { + return fmt.Errorf("proof check: %w", err) + } + api.AssertIsEqual(res, c.Res) + return nil +} + +func TestBLS12InBW6CommitmentIsValidWithSubgroupCheck(t *testing.T) { + assert := test.NewAssert(t) + + innerCcs, innerVK, innerWitness, innerProof := getInnerCommitment(assert, ecc.BLS12_377.ScalarField(), ecc.BW6_761.ScalarField()) + assert.Equal(len(innerCcs.GetCommitments().CommitmentIndexes()), 1) + + circuitVk, err := ValueOfVerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](innerVK) + assert.NoError(err) + circuitWitness, err := ValueOfWitness[sw_bls12377.ScalarField](innerWitness) + assert.NoError(err) + circuitProof, err := ValueOfProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](innerProof) + assert.NoError(err) + + outerCircuit := &OuterCircuitIsValidWithSubgroupCheck[sw_bls12377.ScalarField, sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT]{ + Proof: PlaceholderProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](innerCcs), + InnerWitness: PlaceholderWitness[sw_bls12377.ScalarField](innerCcs), + VerifyingKey: PlaceholderVerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](innerCcs), + } + + outerAssignment := &OuterCircuitIsValidWithSubgroupCheck[sw_bls12377.ScalarField, sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT]{ + InnerWitness: circuitWitness, + Proof: circuitProof, + VerifyingKey: circuitVk, + Res: 1, + } + err = test.IsSolved(outerCircuit, outerAssignment, ecc.BW6_761.ScalarField()) + assert.NoError(err) +} + +func TestBN254InBN254CommitmentIsValidWithSubgroupCheck(t *testing.T) { + assert := test.NewAssert(t) + + innerCcs, innerVK, innerWitness, innerProof := getInnerCommitment(assert, ecc.BN254.ScalarField(), ecc.BN254.ScalarField()) + assert.Equal(len(innerCcs.GetCommitments().CommitmentIndexes()), 1) + + circuitVk, err := ValueOfVerifyingKey[sw_bn254.G1Affine, sw_bn254.G2Affine, sw_bn254.GTEl](innerVK) + assert.NoError(err) + circuitWitness, err := ValueOfWitness[sw_bn254.ScalarField](innerWitness) + assert.NoError(err) + circuitProof, err := ValueOfProof[sw_bn254.G1Affine, sw_bn254.G2Affine](innerProof) + assert.NoError(err) + + outerCircuit := &OuterCircuitIsValidWithSubgroupCheck[sw_bn254.ScalarField, sw_bn254.G1Affine, sw_bn254.G2Affine, sw_bn254.GTEl]{ + Proof: PlaceholderProof[sw_bn254.G1Affine, sw_bn254.G2Affine](innerCcs), + InnerWitness: PlaceholderWitness[sw_bn254.ScalarField](innerCcs), + VerifyingKey: PlaceholderVerifyingKey[sw_bn254.G1Affine, sw_bn254.G2Affine, sw_bn254.GTEl](innerCcs), + } + + assert.Run(func(assert *test.Assert) { + outerAssignment := &OuterCircuitIsValidWithSubgroupCheck[sw_bn254.ScalarField, sw_bn254.G1Affine, sw_bn254.G2Affine, sw_bn254.GTEl]{ + InnerWitness: circuitWitness, + Proof: circuitProof, + VerifyingKey: circuitVk, + Res: 1, + } + err = test.IsSolved(outerCircuit, outerAssignment, ecc.BN254.ScalarField()) + assert.NoError(err) + }, "valid") +}