Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I wanna use proof without commitment [gnark with BLS12-381] #1316

Open
tjchern opened this issue Nov 16, 2024 · 3 comments
Open

I wanna use proof without commitment [gnark with BLS12-381] #1316

tjchern opened this issue Nov 16, 2024 · 3 comments

Comments

@tjchern
Copy link

tjchern commented Nov 16, 2024

Hello, when using gnark with BLS12-381, I don't want to generate commitments and commitmentPok. What should I do?
Currently, compiling some circuits include commitments and commitmentPok, while others do not.
I would like to avoid using commitments and commitmentPok and work only with inputs and proofs. Thank you!

@ivokub
Copy link
Collaborator

ivokub commented Nov 16, 2024

There should be a way in some of the cases. In some places we check if the builder interface implicitly implements the frontend.Committer interface and fall back to some other solution if it doesn't. But currently we don't have a fallback mechanism for non-native field emulation as it would require completely separate implementation.

What is your use case for trying to avoid the commitment? I guess one issue would be if you want to implement folding on the compiled R1CS which imo currently wouldn't work well with the pairing check necessary. But maybe there could be some other way to do it.

I'll post an example on how to override the Commit function to avoid commitment next week, but in case of non-native arithmetic it will most probably fail to compile at all.

@tjchern
Copy link
Author

tjchern commented Nov 18, 2024

Thank you for your reply.

Actually, it's because I don't know what kind of circuit's vk would generate vk.PublicAndCommitmentCommitted. My expectation is that, since the circuit doesn't use commitments, this vk.PublicAndCommitmentCommitted should always be empty.

@ivokub
Copy link
Collaborator

ivokub commented Nov 20, 2024

Have a look at this example:

package examples

import (
	"fmt"
	"math/big"
	"testing"

	"github.com/consensys/gnark-crypto/ecc"
	"github.com/consensys/gnark/backend/groth16"
	groth16_bn254 "github.com/consensys/gnark/backend/groth16/bn254"
	"github.com/consensys/gnark/frontend"
	"github.com/consensys/gnark/frontend/cs/r1cs"
	"github.com/consensys/gnark/internal/kvstore"
	"github.com/consensys/gnark/std/rangecheck"
	"github.com/consensys/gnark/test"
)

type PublicCommitmentOnly struct {
	A frontend.Variable `gnark:",public"`
	B frontend.Variable `gnark:",secret"`
}

type PrivateCommitmentOnly struct {
	A frontend.Variable `gnark:",public"`
	B frontend.Variable `gnark:",secret"`
}

type PublicAndPrivateCommitment struct {
	A frontend.Variable `gnark:",public"`
	B frontend.Variable `gnark:",secret"`
}

func (c *PublicCommitmentOnly) Define(api frontend.API) error {
	cmter, ok := api.(frontend.Committer)
	if !ok {
		return fmt.Errorf("does not implement commitment")
	}
	r, err := cmter.Commit(c.A)
	if err != nil {
		return fmt.Errorf("commit A: %w", err)
	}
	api.AssertIsDifferent(c.B, r)
	return nil
}

func (c *PrivateCommitmentOnly) Define(api frontend.API) error {
	cmter, ok := api.(frontend.Committer)
	if !ok {
		return fmt.Errorf("does not implement commitment")
	}
	r, err := cmter.Commit(c.B)
	if err != nil {
		return fmt.Errorf("commit B: %w", err)
	}
	api.AssertIsDifferent(c.A, r)
	return nil
}

func (c *PublicAndPrivateCommitment) Define(api frontend.API) error {
	cmter, ok := api.(frontend.Committer)
	if !ok {
		return fmt.Errorf("does not implement commitment")
	}
	r, err := cmter.Commit(c.A, c.B)
	if err != nil {
		return fmt.Errorf("commit A: %w", err)
	}
	api.AssertIsDifferent(c.B, r)
	return nil
}

type NoCommitment struct {
	A frontend.Variable `gnark:",public"`
	B frontend.Variable `gnark:",secret"`
}

func (c *NoCommitment) Define(api frontend.API) error {
	api.AssertIsDifferent(c.A, c.B)
	return nil
}

func TestCommitments(t *testing.T) {
	assert := test.NewAssert(t)
	vk1, p1 := getProof(assert, &PublicCommitmentOnly{}, &PublicCommitmentOnly{A: 1, B: 1})
	vk2, p2 := getProof(assert, &PrivateCommitmentOnly{}, &PrivateCommitmentOnly{A: 1, B: 1})
	vk3, p3 := getProof(assert, &PublicAndPrivateCommitment{}, &PublicAndPrivateCommitment{A: 1, B: 1})
	vk4, p4 := getProof(assert, &NoCommitment{}, &NoCommitment{A: 1, B: 2})
	assert.Log("vk1", len(vk1.PublicAndCommitmentCommitted))
	assert.Log("vk2", len(vk2.PublicAndCommitmentCommitted))
	assert.Log("vk3", len(vk3.PublicAndCommitmentCommitted))
	assert.Log("vk4", len(vk4.PublicAndCommitmentCommitted))
	_, _, _, _ = p1, p2, p3, p4

}

func getProof(assert *test.Assert, circuit frontend.Circuit, assignment frontend.Circuit) (*groth16_bn254.VerifyingKey, *groth16_bn254.Proof) {
	ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, circuit)
	assert.NoError(err)
	pk, vk, err := groth16.Setup(ccs)
	assert.NoError(err)
	w, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField())
	assert.NoError(err)
	pw, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField(), frontend.PublicOnly())
	assert.NoError(err)
	proof, err := groth16.Prove(ccs, pk, w)
	assert.NoError(err)
	err = groth16.Verify(proof, vk, pw)
	assert.NoError(err)

	tVk, ok := vk.(*groth16_bn254.VerifyingKey)
	assert.True(ok)
	tProof, ok := proof.(*groth16_bn254.Proof)
	assert.True(ok)
	return tVk, tProof
}

type wrappedBuilder interface {
	frontend.Builder
	kvstore.Store
}

type NoCommitter struct {
	wrappedBuilder
}

func (c *NoCommitter) SetKeyValue(key any, value any) {
	c.wrappedBuilder.SetKeyValue(key, value)
}

func (c *NoCommitter) GetKeyValue(key any) (value any) {
	return c.wrappedBuilder.GetKeyValue(key)
}

func NewBuilder(nb frontend.NewBuilder) frontend.NewBuilder {
	return func(sc *big.Int, cc frontend.CompileConfig) (frontend.Builder, error) {
		bb, err := nb(sc, cc)
		if err != nil {
			return nil, fmt.Errorf("unwrapped: %w", err)
		}
		bbb, ok := bb.(wrappedBuilder)
		if !ok {
			return nil, fmt.Errorf("does not implement kvstore")
		}
		return &NoCommitter{wrappedBuilder: bbb}, nil
	}
}

type RangecheckCircuit struct {
	A frontend.Variable `gnark:",public"`
	B frontend.Variable `gnark:",secret"`
}

func (C *RangecheckCircuit) Define(api frontend.API) error {
	rc := rangecheck.New(api)
	rc.Check(C.B, 8)
	api.AssertIsDifferent(C.A, C.B)
	return nil
}

func TestOverride(t *testing.T) {
	assert := test.NewAssert(t)
	// we first compile a circuit directly which implements committer
	ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &RangecheckCircuit{})
	assert.NoError(err)
	// then we compile a circuit which does not implement committer
	ccs2, err := frontend.Compile(ecc.BN254.ScalarField(), NewBuilder(r1cs.NewBuilder), &RangecheckCircuit{})
	assert.NoError(err)

	assert.Log("with committemnt", ccs.GetNbConstraints())
	assert.Log("without committemnt", ccs2.GetNbConstraints())
}

First, there are different circuits which show when the PublicAndCommitmentCommitted is for different circuits. When we only use api.Commit for private inputs, then inner slices are empty.

Finally, there is the construction NoCommitter which overrides the builder such that there is no Commit method. By doing this, then the internal implementation automatically chooses another approach for range checks. But keep in mind this only works for range checking, for non-native arithmetic we do not have a fallback.

See if this works for your approach. Otherwise I think it would be more useful to know what kind of circuit you are using and what is the goal of not having PublicAndCommitmentCommitment empty. Our current approach allows to significantly improve the prover performance (4-5x in minor case, but depending on the use cases even more).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants