Consensys / gnark

gnark is a fast zk-SNARK library that offers a high-level API to design circuits. The library is open source and developed under the Apache 2.0 license
https://hackmd.io/@gnark
Apache License 2.0
1.42k stars 367 forks source link

bug: in plonk recursion, if the inner circuit has no public witness , outer circuit compile error . #999

Closed readygo67 closed 8 months ago

readygo67 commented 9 months ago

Description

In plonk recursion, if the inner circuit has no public witness, when compile the outer circuit, the following error emitted.

panic: compile failed: parse circuit: runtime error: index out of range [0] with length 0
frontend.parseCircuit.func2
    compile.go:118
runtime.goPanicIndex
    panic.go:114
plonk.(*Verifier[...]).PrepareVerification
    verifier.go:844
plonk.(*Verifier[...]).AssertProof
    verifier.go:1047
plonk_test.(*OuterCircuit[...]).Define
    nonnative_doc_test.go:103

which is caused by unreachable index=0 if witness is nil

func (v *Verifier[FR, G1El, G2El, GtEl]) PrepareVerification(vk VerifyingKey[FR, G1El, G2El], proof Proof[FR, G1El, G2El], witness Witness[FR]) ([]kzg.Commitment[G1El], []kzg.OpeningProof[FR, G1El], []emulated.Element[FR], error) {

    var fr FR
    if len(proof.Bsb22Commitments) != len(vk.Qcp) {
        return nil, nil, nil, fmt.Errorf("BSB22 commitment number mismatch")
    }

    fs, err := recursion.NewTranscript(v.api, fr.Modulus(), []string{"gamma", "beta", "alpha", "zeta"})
    if err != nil {
        return nil, nil, nil, fmt.Errorf("init new transcript: %w", err)
    }
        ...  ...
       xiLi := v.scalarApi.Mul(lagrange, &witness.Public[0]).   //PANIC here
       ... ...

Expected Behavior

outer circuit compile success.

Actual Behavior

panic happens

Steps to Reproduce

  1. make InnerCircuitNative's N as secret(std/recursion/plonk/nonactive_doc_test.go),
  2. rename the "Example_emulated" function to "TestPlonkRecursion_BW761_BN254"
  3. run the TestPlonkRecursion_BW761_BN254, the panic happens.
    
    // InnerCircuitNative is the definition of the inner circuit we want to
    // recursively verify inside an outer circuit. The circuit proves the knowledge
    // of a factorisation of a semiprime.
    type InnerCircuitNative struct {
    P, Q frontend.Variable
    N    frontend.Variable //`gnark:",public"`
    }

func (c InnerCircuitNative) Define(api frontend.API) error { // prove that PQ == N res := api.Mul(c.P, c.Q) api.AssertIsEqual(res, c.N) // we must also enforce that P != 1 and Q != 1 api.AssertIsDifferent(c.P, 1) api.AssertIsDifferent(c.Q, 1) return nil }

// computeInnerProof computes the proof for the inner circuit we want to verify // recursively. In this example the PLONK keys are generated on the fly, but // in practice should be genrated once and using MPC. func computeInnerProof(field, outer *big.Int) (constraint.ConstraintSystem, native_plonk.VerifyingKey, witness.Witness, native_plonk.Proof) { innerCcs, err := frontend.Compile(field, scs.NewBuilder, &InnerCircuitNative{}) if err != nil { panic(err) } // NB! UNSAFE! Use MPC. srs, srsLagrange, err := unsafekzg.NewSRS(innerCcs) if err != nil { panic(err) }

innerPK, innerVK, err := native_plonk.Setup(innerCcs, srs, srsLagrange)
if err != nil {
    panic(err)
}

// inner proof
innerAssignment := &InnerCircuitNative{
    P: 3,
    Q: 5,
    N: 15,
}
innerWitness, err := frontend.NewWitness(innerAssignment, field)
if err != nil {
    panic(err)
}
innerProof, err := native_plonk.Prove(innerCcs, innerPK, innerWitness, plonk.GetNativeProverOptions(outer, field))
if err != nil {
    panic(err)
}
innerPubWitness, err := innerWitness.Public()
if err != nil {
    panic(err)
}
err = native_plonk.Verify(innerProof, innerVK, innerPubWitness, plonk.GetNativeVerifierOptions(outer, field))
if err != nil {
    panic(err)
}
return innerCcs, innerVK, innerPubWitness, innerProof

}

// OuterCircuit is the generic outer circuit which can verify PLONK proofs // using field emulation or 2-chains of curves. type OuterCircuit[FR emulated.FieldParams, G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl algebra.GtElementT] struct { Proof plonk.Proof[FR, G1El, G2El] VerifyingKey plonk.VerifyingKey[FR, G1El, G2El] gnark:"-" // constant verification key InnerWitness plonk.Witness[FR] gnark:",public" }

func (c *OuterCircuit[FR, G1El, G2El, GtEl]) Define(api frontend.API) error { verifier, err := plonk.NewVerifierFR, G1El, G2El, GtEl if err != nil { return fmt.Errorf("new verifier: %w", err) } err = verifier.AssertProof(c.VerifyingKey, c.Proof, c.InnerWitness) return err }

func TestPlonkRecursion_BW761_BN254(t *testing.T) { // compute the proof which we want to verify recursively innerCcs, innerVK, innerWitness, innerProof := computeInnerProof(ecc.BW6_761.ScalarField(), ecc.BN254.ScalarField())

// initialize the witness elements
circuitVk, err := plonk.ValueOfVerifyingKey[sw_bw6761.ScalarField, sw_bw6761.G1Affine, sw_bw6761.G2Affine](innerVK)
if err != nil {
    panic(err)
}
circuitWitness, err := plonk.ValueOfWitness[sw_bw6761.ScalarField](innerWitness)
if err != nil {
    panic(err)
}
circuitProof, err := plonk.ValueOfProof[sw_bw6761.ScalarField, sw_bw6761.G1Affine, sw_bw6761.G2Affine](innerProof)
if err != nil {
    panic(err)
}

outerCircuit := &OuterCircuit[sw_bw6761.ScalarField, sw_bw6761.G1Affine, sw_bw6761.G2Affine, sw_bw6761.GTEl]{
    InnerWitness: plonk.PlaceholderWitness[sw_bw6761.ScalarField](innerCcs),
    Proof:        plonk.PlaceholderProof[sw_bw6761.ScalarField, sw_bw6761.G1Affine, sw_bw6761.G2Affine](innerCcs),
    VerifyingKey: circuitVk,
}
outerAssignment := &OuterCircuit[sw_bw6761.ScalarField, sw_bw6761.G1Affine, sw_bw6761.G2Affine, sw_bw6761.GTEl]{
    InnerWitness: circuitWitness,
    Proof:        circuitProof,
}
// compile the outer circuit
ccs, err := frontend.Compile(ecc.BN254.ScalarField(), scs.NewBuilder, outerCircuit)
if err != nil {
    panic("compile failed: " + err.Error())
}

// NB! UNSAFE! Use MPC.
srs, srsLagrange, err := unsafekzg.NewSRS(ccs)
if err != nil {
    panic(err)
}

// create PLONK setup. NB! UNSAFE
pk, vk, err := native_plonk.Setup(ccs, srs, srsLagrange) // UNSAFE! Use MPC
if err != nil {
    panic("setup failed: " + err.Error())
}

// create prover witness from the assignment
secretWitness, err := frontend.NewWitness(outerAssignment, ecc.BN254.ScalarField())
if err != nil {
    panic("secret witness failed: " + err.Error())
}

// create public witness from the assignment
publicWitness, err := secretWitness.Public()
if err != nil {
    panic("public witness failed: " + err.Error())
}

// construct the PLONK proof of verifying PLONK proof in-circuit
outerProof, err := native_plonk.Prove(ccs, pk, secretWitness)
if err != nil {
    panic("proving failed: " + err.Error())
}

// verify the PLONK proof
err = native_plonk.Verify(outerProof, vk, publicWitness)
if err != nil {
    panic("circuit verification failed: " + err.Error())
}

}



## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* gnark version: master + 5f1643d98071aabc5abbe89573e4c153cd1c6b0e
* gnark-crypto version used:  v0.12.2-0.20231221171913-5d5eded6bb15
* go version (e.g. 1.20.6): 1.24.1
* Operating System and version: darwin/arm64
ivokub commented 9 months ago

Hmm, I think we could add a more descriptive error message, but I'm still not confident we should add such an edge case. For me it doesn't make sense to recursively verify a circuit without a public witness. Or is there any?

readygo67 commented 9 months ago

I hope to create a VDF function by applying hash several times (hash(hash(hash(x)))), so don't expose the immediate hash result。And because of non-exposure, I can not get the finial result.

ivokub commented 9 months ago

But in this case wouldn't you have to prove inside SNARK that out=hash(hash(hash(x))) is actually valid VDF output of x? Wouldn't the circuit's inputs be out and x?

I see that if instead of the claim "out is a valid VDF output of x" you want to claim "I have performed VDF for some input" which indeed is a valid statement to prove. However, if you do not include anything in the public input, then it would be possible to copy the proof and the actual claim would actually be "someone has performed VDF for some input". Particularly, as some SNARKs are malleable (i.e. we can rerandomize the proof without proving), then it wouldn't even possible to perform some checking in database that no such has been provided.

So, I still think that you have to provide some public input.